diff --git a/server/feedback-mailer/README.md b/server/feedback-mailer/README.md new file mode 100644 index 0000000..09ebb40 --- /dev/null +++ b/server/feedback-mailer/README.md @@ -0,0 +1,157 @@ +# YMhut Box 反馈工单服务 + +这是 `server/feedback-mailer` 下的 Go + Gin + SQLite 反馈服务。旧 PHP 入口已移除,线上入口由 Go 服务接管;`config.txt`、`storage/`、`storage/feedback.sqlite` 和历史 `.ymfb` / `.zip` 文件继续保留并兼容读取。 + +## 运行 + +在 `server/feedback-mailer` 目录直接运行: + +```bash +go run main.go +``` + +监听地址以核心配置文件 `config.txt` 的 `listen` 为准: + +```php +'listen' => ':8080', +``` + +服务器部署时也可以显式设置服务根目录: + +```bash +export YMHUT_FEEDBACK_HOME=/opt/feedback-mailer +./feedback-mailer +``` + +后台入口: + +```text +http://127.0.0.1:8080/admin/ +``` + +生产构建与检查: + +```bash +npm --prefix admin-web install +npm --prefix admin-web run build +go test ./... +go build . +``` + +## 配置 + +优先读取当前目录的 `config.txt`,这是线上实际配置文件,部署时不要覆盖。没有 `config.txt` 时才回退读取 `config.json`。 + +关键字段: + +- `listen`:服务监听地址,例如 `:8080`、`:9090`、`127.0.0.1:8080`。 +- `admin_password_hash` / `admin_password`:后台登录密码,推荐使用 bcrypt hash。 +- `client_signature_key`:旧 WinUI 客户端 HMAC 签名密钥,必须保持一致。 +- `package_encryption_key`:`.ymfb` 反馈包解密密钥,必须保持一致。 +- `storage_dir`:默认 `storage`,保留历史反馈包。 +- `database_path`:默认 `storage/feedback.sqlite`,保留旧 SQLite 数据。 +- `rate_limit`:提交、状态查询、验证码、登录和后台 API 的令牌桶限流。 +- `upload_guard`:zip 文件数量、解压后大小、单文件大小、压缩比、路径穿越校验。 +- `backup.dir` / `backup_dir`:数据库只读备份输出目录,默认 `storage/backups`。 +- `webhooks`:通用 Webhook 通知配置,secret 只用于签名,不进入 URL 或配置健康 API。 + +## 兼容接口 + +旧客户端提交接口保持不变: + +```text +POST / +``` + +继续接收 `payload`、`timestamp`、`nonce`、`packageSha256`、`signature`、`package`,并保留旧错误码。 + +旧状态查询保持不变: + +```text +GET /?api=status&code=FB-YYYYMMDD-XXXXXX +``` + +响应保留旧字段,并兼容增加 `statusDetail`、`category`、`priority`。 + +## 后台能力 + +- 公共首页 `/`:反馈中心正式页和无需登录的状态查询。 +- 后台 `/admin/`:工作台、工单流、状态看板、通知记录、Webhook 集成、运维、配置健康。 +- 10 秒轮询刷新:统计卡片、SVG 图表、工单列表、详情、通知和配置摘要都会重绘。 +- 工单增强:状态、分类、优先级、SLA、指派人、截止时间、标签、评论、解决说明、内部备注、公开回复。 +- 看板操作:按 `new / triaged / investigating / resolved / archived` 分栏,拖动工单即可改状态。 +- 批量处理:多选后批量归类、处理中、解决、归档。 +- 导出:`GET /api/admin/feedbacks/export?format=csv` 导出当前筛选结果,不包含包文件和密钥。 +- 审计日志:登录、工单修改、批量操作、导出、备份、Webhook 测试等都会记录。 +- 数据库备份:后台创建和下载 SQLite 备份,不提供在线删除、VACUUM 或破坏性维护。 +- Webhook:支持 `feedback.created`、`feedback.updated`、`feedback.status_changed`、`feedback.comment_created`、`mail.failed`。 + +Webhook 请求头: + +```text +X-YMhut-Event +X-YMhut-Delivery +X-YMhut-Signature +``` + +签名格式为 `sha256=`。 + +## 数据升级 + +服务启动时会对旧 `feedbacks` 表做非破坏性补列,并创建新表: + +- `feedback_comments` +- `feedback_tags` +- `audit_logs` +- `webhook_deliveries` +- `feedback_events` + +SQLite 默认启用 WAL,并设置 `busy_timeout`。不会删除 `storage/feedback.sqlite`,不会修改历史 `.ymfb`、`.zip` 文件。 + +## 后台 API + +需要登录 session,写接口需要 CSRF: + +- `GET /api/admin/overview` +- `GET /api/admin/config` +- `GET /api/admin/feedbacks?page=&perPage=&status=&category=&priority=&mail=&q=&assignee=&tag=&sla=&overdue=&sort=` +- `GET /api/admin/feedbacks/export?format=csv` +- `GET /api/admin/feedbacks/summary` +- `GET /api/admin/feedbacks/:code` +- `PATCH /api/admin/feedbacks/:code` +- `PATCH /api/admin/feedbacks/bulk` +- `POST /api/admin/feedbacks/:code/comments` +- `GET /api/admin/mails?page=&perPage=&status=` +- `POST /api/admin/mails/test` +- `GET /api/admin/audit-logs?page=&perPage=&actor=&type=` +- `GET /api/admin/webhooks/deliveries?page=&perPage=&status=` +- `POST /api/admin/webhooks/test` +- `POST /api/admin/backups/database` +- `GET /api/admin/backups` +- `GET /api/admin/backups/:name` + +## 增强依据 + +- OWASP File Upload Cheat Sheet:上传限额、存储隔离、路径和内容校验、防 zip bomb。 +- OWASP REST Security Cheat Sheet:限流、审计、避免敏感信息出现在 URL、统一错误语义。 +- SQLite WAL 与备份文档:读写并发和在线备份思路。 +- Go `golang.org/x/time/rate`:内存令牌桶限流。 +- Gin graceful shutdown 示例:使用 `http.Server.Shutdown()` 优雅停机。 + +## 验证 + +```bash +go test ./... +go build . +npm --prefix admin-web run build +go run main.go +``` + +启动后建议检查: + +```text +http://127.0.0.1:8080/ +http://127.0.0.1:8080/admin/ +http://127.0.0.1:8080/?api=status&code=BAD +http://127.0.0.1:8080/api/auth/captcha +``` diff --git a/server/feedback-mailer/admin-web/index.html b/server/feedback-mailer/admin-web/index.html new file mode 100644 index 0000000..bd18987 --- /dev/null +++ b/server/feedback-mailer/admin-web/index.html @@ -0,0 +1,12 @@ + + + + + + YMhut Box Feedback Center + + +
+ + + diff --git a/server/feedback-mailer/admin-web/package-lock.json b/server/feedback-mailer/admin-web/package-lock.json new file mode 100644 index 0000000..961c8d0 --- /dev/null +++ b/server/feedback-mailer/admin-web/package-lock.json @@ -0,0 +1,1730 @@ +{ + "name": "ymhut-feedback-admin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ymhut-feedback-admin", + "version": "1.0.0", + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "lucide-react": "^0.468.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.9.0", + "vite": "^7.0.0" + }, + "devDependencies": {} + }, + "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==", + "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==", + "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==", + "license": "MIT", + "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==", + "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-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==", + "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-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "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==", + "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==", + "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-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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "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-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "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-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "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/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==", + "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==", + "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/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "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/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==", + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "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.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/react": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "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.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "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": "7.3.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/server/feedback-mailer/admin-web/package.json b/server/feedback-mailer/admin-web/package.json new file mode 100644 index 0000000..8b1022d --- /dev/null +++ b/server/feedback-mailer/admin-web/package.json @@ -0,0 +1,20 @@ +{ + "name": "ymhut-feedback-admin", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "vite": "^7.0.0", + "typescript": "^5.9.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "lucide-react": "^0.468.0" + }, + "devDependencies": {} +} diff --git a/server/feedback-mailer/admin-web/src/main.tsx b/server/feedback-mailer/admin-web/src/main.tsx new file mode 100644 index 0000000..8273bee --- /dev/null +++ b/server/feedback-mailer/admin-web/src/main.tsx @@ -0,0 +1,1162 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { + Activity, + Archive, + BadgeCheck, + BarChart3, + BellRing, + BriefcaseBusiness, + CheckCircle2, + ChevronRight, + CircleGauge, + Clock3, + Database, + Download, + ExternalLink, + FileText, + Filter, + Gauge, + GitBranch, + HardDrive, + Home, + Inbox, + KeyRound, + KanbanSquare, + Layers3, + Loader2, + LockKeyhole, + LogOut, + Mail, + MessageSquareText, + Plus, + RefreshCw, + Save, + Search, + Send, + Server, + Settings, + ShieldCheck, + SlidersHorizontal, + Sparkles, + Square, + SquareCheckBig, + Tags, + TimerReset, + UserRound, + Webhook, + Wifi, + XCircle +} from "lucide-react"; +import "./styles.css"; + +type FeedbackEvent = { + id: number; + feedbackCode: string; + eventType: string; + actor: string; + fromValue: string; + toValue: string; + message: string; + createdAt: string; +}; + +type FeedbackComment = { + id: number; + feedbackCode: string; + author: string; + body: string; + internal: boolean; + createdAt: string; +}; + +type FeedbackRecord = { + code: string; + receivedAt: string; + title: string; + type: string; + severity: string; + category: string; + priority: string; + contact: string; + body: string; + status: string; + statusDetail: string; + note: string; + publicReply: string; + handledBy: string; + assignee: string; + dueAt: string; + resolvedAt: string; + archivedAt: string; + slaLevel: string; + sourceChannel: string; + riskScore: number; + resolution: string; + packagePath: string; + encryptedPackagePath: string; + packageSha256: string; + plainPackageSha256: string; + remoteAddr: string; + summaryText: string; + includedFiles: string; + mailSent: boolean; + updatedAt: string; + lastActivityAt: string; + tags?: string[]; + events?: FeedbackEvent[]; + comments?: FeedbackComment[]; +}; + +type MailRecord = { + id: number; + feedbackCode: string; + kind: string; + status: string; + toAddress: string; + subject: string; + plainBody: string; + htmlBody: string; + attachmentPath: string; + attachmentName: string; + errorMessage: string; + createdAt: string; + sentAt: string; +}; + +type WebhookDelivery = { + id: number; + webhookName: string; + event: string; + status: string; + attempts: number; + responseCode: number; + errorMessage: string; + payloadSha256: string; + createdAt: string; + finishedAt: string; +}; + +type AuditLog = { + id: number; + actor: string; + type: string; + target: string; + message: string; + ip: string; + userAgent: string; + createdAt: string; +}; + +type BackupInfo = { name: string; bytes: number; createdAt: string }; +type WebhookSummary = { name: string; host: string; enabled: boolean; events: string[]; secretConfigured: boolean; timeoutSeconds: number; maxRetries: number }; +type ConfigCheck = { key: string; label: string; status: "ok" | "missing" | "warning"; detail: string }; + +type Overview = { + feedbackTotal: number; + todayFeedback: number; + mailFailed: number; + mailTotal: number; + overdue: number; + statusCounts: Record; + categoryCounts: Record; + priorityCounts: Record; + slaCounts: Record; + storage: { path: string; bytes: number }; + database: { path: string; bytes: number; walMode: string }; + mail: { configured: boolean; host: string; port: number; secure: string; fromAddress: string; developerAddress: string }; + recentEvents: FeedbackEvent[]; +}; + +type FeedbackSummary = { + total: number; + today: number; + mailFailed: number; + overdue: number; + statusCounts: Record; + categoryCounts: Record; + priorityCounts: Record; + slaCounts: Record; + recentEvents: FeedbackEvent[]; +}; + +type ConfigHealth = { + generatedAt: string; + service: { listen: string; baseDir: string; timestampWindowSeconds: number; maxRequestBytes: number; maxPackageBytes: number }; + security: { + adminPasswordConfigured: boolean; + adminPasswordHashConfigured: boolean; + clientSignatureKeyConfigured: boolean; + packageEncryptionKeyConfigured: boolean; + rateLimit: Record; + }; + uploadGuard: Record; + storage: { path: string; bytes: number; exists: boolean }; + database: { provider: string; path: string; bytes: number; exists: boolean; walMode: string; backupDir: string; lastBackup: string; runtime?: DatabaseRuntime }; + mail: { + configured: boolean; + host: string; + port: number; + secure: string; + fromAddress: string; + developerAddress: string; + usernameConfigured: boolean; + passwordConfigured: boolean; + timeoutSeconds: number; + }; + webhooks: WebhookSummary[]; + frontend: { distPath: string; indexExists: boolean; assetsExists: boolean }; + editable: EditableConfig; + checks: ConfigCheck[]; +}; + +type DatabaseRuntime = { + activeProvider: string; + configProvider: string; + sqliteReady: boolean; + remoteReady: boolean; + failoverActive: boolean; + lastError: string; + lastFailoverAt: string; + lastRecoveredAt: string; + lastSyncAt: string; + lastSyncError: string; +}; + +type EditableDatabase = { + provider: string; + sqlitePath: string; + host: string; + port: number; + name: string; + user: string; + password: string; + passwordConfigured: boolean; + dsn: string; + dsnConfigured: boolean; + sslMode: string; + maxOpenConns: number; + maxIdleConns: number; + connMaxLifetimeSeconds: number; + failoverEnabled: boolean; + healthIntervalSeconds: number; + sync: { enabled: boolean; interval_seconds?: number; intervalSeconds?: number; batch_size?: number; batchSize?: number }; +}; + +type EditableWebhook = WebhookSummary & { url?: string; secret?: string; timeoutSeconds: number; maxRetries: number }; + +type EditableConfig = { + listen: string; + database: EditableDatabase; + mail: { + host: string; port: number; secure: string; username: string; password: string; passwordConfigured: boolean; + fromAddress: string; fromName: string; developerAddress: string; timeoutSeconds: number; + }; + backup: { dir: string }; + rateLimit: Record; + uploadGuard: Record; + webhooks: EditableWebhook[]; + security: Record; +}; + +type StatusPayload = { + ok: boolean; + code?: string; + status?: string; + statusLabel?: string; + statusDetail?: string; + category?: string; + priority?: string; + hasReply?: boolean; + reply?: string; + receivedAt?: string; + updatedAt?: string; + mailSent?: boolean; + error?: string; + message?: string; +}; + +type Page = { items: T[]; total: number; page: number; perPage: number; totalPages: number; offset: number }; +type Captcha = { captchaId: string; image: string }; +type ApiResult = T & { ok: boolean; error?: string; message?: string }; +type Filters = { + status: string; + category: string; + priority: string; + mail: string; + q: string; + assignee: string; + tag: string; + sla: string; + overdue: string; + sort: string; + page: number; + perPage: number; +}; +type AdminTab = "dashboard" | "tickets" | "board" | "mail" | "integrations" | "ops" | "config"; + +const refreshMs = 10000; +const statusLabels: Record = { new: "新反馈", triaged: "已归类", investigating: "处理中", resolved: "已解决", archived: "已归档" }; +const categoryLabels: Record = { issue: "问题", suggestion: "建议", ui: "界面反馈", other: "其他" }; +const priorityLabels: Record = { normal: "普通", major: "影响使用", blocking: "阻塞" }; +const slaLabels: Record = { standard: "标准", elevated: "加急", urgent: "紧急" }; +const statusOrder = ["new", "triaged", "investigating", "resolved", "archived"]; +const statusColors: Record = { new: "#0f6d7a", triaged: "#2563a8", investigating: "#a46805", resolved: "#16865a", archived: "#667486" }; + +function App() { + const path = window.location.pathname; + if (!path.startsWith("/admin")) { + return ; + } + + const [csrfToken, setCsrfToken] = useState(() => sessionStorage.getItem("csrfToken") ?? ""); + const [notice, setNotice] = useState(""); + const api = useMemo(() => createApi(csrfToken, setNotice), [csrfToken]); + + if (!csrfToken) { + return { sessionStorage.setItem("csrfToken", token); setCsrfToken(token); }} />; + } + + return setNotice("")} onLogout={() => { sessionStorage.removeItem("csrfToken"); setCsrfToken(""); }} />; +} + +function PublicHome() { + const [query, setQuery] = useState(""); + const [status, setStatus] = useState(null); + const [busy, setBusy] = useState(false); + const [lastChecked, setLastChecked] = useState(""); + + const lookup = useCallback(async (silent = false) => { + const code = query.trim().toUpperCase(); + if (!code) return; + if (!silent) setBusy(true); + try { + const response = await fetch(`/?api=status&code=${encodeURIComponent(code)}`, { credentials: "include" }); + const data = (await response.json()) as StatusPayload; + setStatus(data); + setLastChecked(new Date().toLocaleTimeString()); + } finally { + if (!silent) setBusy(false); + } + }, [query]); + + useEffect(() => { + if (!status?.ok) return; + const timer = window.setInterval(() => void lookup(true), 15000); + return () => window.clearInterval(timer); + }, [lookup, status?.ok]); + + return ( +
+
+
YMhut Box
+ 后台入口 +
+
+
+ 反馈服务中心 +

反馈工单状态查询

+

输入反馈编号即可查看处理状态、公开回复、分类和优先级。后台侧已升级为工单流、看板、审计、备份和 Webhook 通知。

+
+ 服务在线 + SQLite WAL + 服务端验证码 +
+
+
{ event.preventDefault(); void lookup(); }}> + + + {lastChecked ? 最近查询 {lastChecked} : null} +
+ +
+
+ ); +} + +function PublicStatusPanel({ status }: { status: StatusPayload | null }) { + if (!status) { + return
等待输入反馈编号
; + } + if (!status.ok) { + return
{status.message || "未找到反馈工单"}{status.error || "NOT_FOUND"}
; + } + return ( +
+
+ + {status.code} +
+
+ } label="分类" value={categoryLabels[status.category || ""] ?? status.category ?? "未分类"} /> + } label="优先级" value={priorityLabels[status.priority || ""] ?? status.priority ?? "普通"} /> + } label="更新时间" value={formatDate(status.updatedAt || "") || "暂无"} /> + } label="通知状态" value={status.mailSent ? "已通知" : "待通知"} /> +
+

状态说明

{status.statusDetail || status.statusLabel || "已收到反馈,正在排队处理。"}
+

公开回复

{status.hasReply ? status.reply : "暂无公开回复"}
+
+ ); +} + +function LoginPage({ onLogin }: { onLogin: (token: string) => void }) { + const [captcha, setCaptcha] = useState(null); + const [password, setPassword] = useState(""); + const [captchaText, setCaptchaText] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + const loadCaptcha = useCallback(async () => { + const response = await fetch("/api/auth/captcha", { credentials: "include" }); + const data = (await response.json()) as ApiResult; + if (data.ok) { + setCaptcha({ captchaId: data.captchaId, image: data.image }); + setCaptchaText(""); + } + }, []); + + useEffect(() => { void loadCaptcha(); }, [loadCaptcha]); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + if (!captcha) return; + setBusy(true); + setError(""); + try { + const response = await fetch("/api/auth/login", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password, captchaId: captcha.captchaId, captcha: captchaText }) + }); + const data = (await response.json()) as ApiResult<{ csrfToken: string }>; + if (!response.ok || !data.ok) { + setError(data.message ?? "登录失败"); + await loadCaptcha(); + return; + } + onLogin(data.csrfToken); + } finally { + setBusy(false); + } + } + + return ( +
+
+
FeedbackOps
+

反馈工单后台

+

验证码由服务端生成并一次性校验。

+
+ + + {error ?

{error}

: null} + +
+
+
+ ); +} + +function AdminApp({ api, notice, onClearNotice, onLogout }: { api: ReturnType; notice: string; onClearNotice: () => void; onLogout: () => void }) { + const [tab, setTab] = useState("dashboard"); + const [overview, setOverview] = useState(null); + const [summary, setSummary] = useState(null); + const [page, setPage] = useState | null>(null); + const [mails, setMails] = useState | null>(null); + const [config, setConfig] = useState(null); + const [auditLogs, setAuditLogs] = useState | null>(null); + const [webhookDeliveries, setWebhookDeliveries] = useState | null>(null); + const [backups, setBackups] = useState([]); + const [filters, setFilters] = useState({ status: "", category: "", priority: "", mail: "", q: "", assignee: "", tag: "", sla: "", overdue: "", sort: "last", page: 1, perPage: 50 }); + const [selectedCode, setSelectedCode] = useState(""); + const [detail, setDetail] = useState(null); + const [selectedCodes, setSelectedCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [lastRefresh, setLastRefresh] = useState(""); + + const loadOverview = useCallback(async () => { + const data = await api.get<{ overview: Overview }>("/api/admin/overview"); + if (data?.overview) setOverview(data.overview); + }, [api]); + const loadSummary = useCallback(async () => { + const data = await api.get<{ summary: FeedbackSummary }>("/api/admin/feedbacks/summary"); + if (data?.summary) setSummary(data.summary); + }, [api]); + const loadConfig = useCallback(async () => { + const data = await api.get<{ config: ConfigHealth }>("/api/admin/config"); + if (data?.config) setConfig(data.config); + }, [api]); + const loadTickets = useCallback(async (silent = false) => { + if (!silent) setLoading(true); + try { + const query = filterQuery(filters); + const data = await api.get<{ page: Page }>(`/api/admin/feedbacks?${query}`); + if (data?.page) { + setPage(data.page); + const first = data.page.items[0]?.code ?? ""; + if (!selectedCode || !data.page.items.some((item) => item.code === selectedCode)) setSelectedCode(first); + } + } finally { + if (!silent) setLoading(false); + } + }, [api, filters, selectedCode]); + const loadDetail = useCallback(async (code: string) => { + if (!code) { setDetail(null); return; } + const data = await api.get<{ feedback: FeedbackRecord }>(`/api/admin/feedbacks/${code}`); + if (data?.feedback) setDetail(data.feedback); + }, [api]); + const loadMails = useCallback(async () => { + const data = await api.get<{ page: Page }>("/api/admin/mails?perPage=50"); + if (data?.page) setMails(data.page); + }, [api]); + const loadAudit = useCallback(async () => { + const data = await api.get<{ page: Page }>("/api/admin/audit-logs?perPage=50"); + if (data?.page) setAuditLogs(data.page); + }, [api]); + const loadWebhooks = useCallback(async () => { + const data = await api.get<{ page: Page; webhooks: WebhookSummary[] }>("/api/admin/webhooks/deliveries?perPage=50"); + if (data?.page) setWebhookDeliveries(data.page); + }, [api]); + const loadBackups = useCallback(async () => { + const data = await api.get<{ backups: BackupInfo[] }>("/api/admin/backups"); + if (data?.backups) setBackups(data.backups); + }, [api]); + + const refreshAll = useCallback(async (silent = false) => { + if (silent) setRefreshing(true); + await Promise.all([ + loadOverview(), + loadSummary(), + loadTickets(silent), + selectedCode ? loadDetail(selectedCode) : Promise.resolve(), + loadMails(), + loadConfig(), + loadAudit(), + loadWebhooks(), + loadBackups() + ]); + setLastRefresh(new Date().toLocaleTimeString()); + if (silent) setRefreshing(false); + }, [loadOverview, loadSummary, loadTickets, selectedCode, loadDetail, loadMails, loadConfig, loadAudit, loadWebhooks, loadBackups]); + + useEffect(() => { void refreshAll(false); }, [refreshAll]); + useLiveInterval(() => { void refreshAll(true); }, refreshMs); + + async function logout() { + await api.post("/api/auth/logout", {}); + onLogout(); + } + + return ( +
+ + +
+
+
+ {tabTitle(tab)} +

{tabHeading(tab)}

+

{tabDescription(tab)}

+
+
+ {lastRefresh || "等待刷新"} + +
+
+ + {notice ? : null} + {tab === "dashboard" ? (overview ? void api.post("/api/admin/mails/test", {}).then(() => loadMails())} /> : ) : null} + {tab === "tickets" ? { setDetail(next); void refreshAll(true); }} /> : null} + {tab === "board" ? void refreshAll(true)} /> : null} + {tab === "mail" ? void api.post("/api/admin/mails/test", {}).then(() => loadMails())} /> : null} + {tab === "integrations" ? void refreshAll(true)} /> : null} + {tab === "ops" ? void refreshAll(true)} /> : null} + {tab === "config" ? void refreshAll(true)} /> : null} +
+
+ ); +} + +function NavButton({ active, onClick, icon, text }: { active: boolean; onClick: () => void; icon: React.ReactNode; text: string }) { + return ; +} + +function Dashboard({ overview, summary, config, onTestMail }: { overview: Overview; summary: FeedbackSummary | null; config: ConfigHealth | null; onTestMail: () => void }) { + const counts = overview.statusCounts ?? {}; + return ( +
+ } label="总反馈" value={overview.feedbackTotal} accent="cyan" /> + } label="今日新增" value={overview.todayFeedback} accent="blue" /> + } label="处理中" value={counts.investigating ?? 0} accent="amber" /> + } label="已解决" value={counts.resolved ?? 0} accent="green" /> + } label="逾期/邮件失败" value={(overview.overdue ?? 0) + overview.mailFailed} accent="red" /> +
状态分布
+
分类统计
+
SLA 与优先级
+
最近处理动态
{overview.recentEvents?.length ? overview.recentEvents.map((event) => ) : 暂无动态}
+
通知与配置
{overview.mail.configured ? "SMTP 已配置" : "SMTP 待完善"}{overview.mail.host || "未配置主机"} · 失败 {overview.mailFailed}
存储 {formatBytes(overview.storage.bytes)}
数据库 {formatBytes(overview.database.bytes)} · WAL {overview.database.walMode || "unknown"}
+ {config ?
{config.checks.slice(0, 8).map((check) => )}
: null} +
+ ); +} + +function TicketWorkspace(props: { + api: ReturnType; + page: Page | null; + detail: FeedbackRecord | null; + filters: Filters; + loading: boolean; + selectedCode: string; + selectedCodes: string[]; + onFilters: React.Dispatch>; + onSelect: (code: string) => void; + onSelectMany: (codes: string[]) => void; + onSaved: (next: FeedbackRecord) => void; +}) { + const { api, page, detail, filters, loading, selectedCode, selectedCodes, onFilters, onSelect, onSelectMany, onSaved } = props; + const toggle = (code: string) => onSelectMany(selectedCodes.includes(code) ? selectedCodes.filter((item) => item !== code) : [...selectedCodes, code]); + async function bulk(status: string) { + if (!selectedCodes.length) return; + await api.patch("/api/admin/feedbacks/bulk", { codes: selectedCodes, status }); + onSelectMany([]); + } + async function exportCSV() { + const blob = await api.download(`/api/admin/feedbacks/export?${filterQuery(filters)}`); + if (!blob) return; + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "feedbacks.csv"; + link.click(); + URL.revokeObjectURL(url); + } + return ( +
+ + +
+
工单队列{page ? `${page.total} 条` : loading ? "加载中" : "0 条"}
+ {selectedCodes.length ?
已选 {selectedCodes.length} 条
: null} +
+ {loading && !page ? : null} + {page?.items.map((item) => ( +
onSelect(item.code)}> + +
+
{categoryLabels[item.category] ?? item.category}
+ {item.title} +

{item.statusDetail || item.body || item.summaryText || "暂无摘要"}

+ {item.code} · {item.assignee || "未指派"} · {formatDate(item.lastActivityAt || item.receivedAt)} +
+ +
+ ))} + {!page?.items.length && !loading ?
没有符合条件的工单
: null} +
+ {page ? onFilters((value) => ({ ...value, page: next }))} /> : null} +
+ + +
+ ); +} + +function TicketDetail({ api, detail, onSaved }: { api: ReturnType; detail: FeedbackRecord | null; onSaved: (next: FeedbackRecord) => void }) { + const [form, setForm] = useState({ status: "new", category: "issue", priority: "normal", slaLevel: "standard", statusDetail: "", handledBy: "", assignee: "", dueAt: "", resolution: "", note: "", publicReply: "", tags: "" }); + const [comment, setComment] = useState(""); + const [saving, setSaving] = useState(false); + const [dirty, setDirty] = useState(false); + const [editingCode, setEditingCode] = useState(""); + + useEffect(() => { + if (!detail) return; + if (detail.code !== editingCode || !dirty) { + setForm({ + status: detail.status || "new", + category: detail.category || detail.type || "issue", + priority: detail.priority || detail.severity || "normal", + slaLevel: detail.slaLevel || "standard", + statusDetail: detail.statusDetail || "", + handledBy: detail.handledBy || "", + assignee: detail.assignee || "", + dueAt: toLocalInput(detail.dueAt), + resolution: detail.resolution || "", + note: detail.note || "", + publicReply: detail.publicReply || "", + tags: (detail.tags ?? []).join(", ") + }); + setEditingCode(detail.code); + setDirty(false); + } + }, [detail, dirty, editingCode]); + + function updateForm(next: Partial) { + setForm((value) => ({ ...value, ...next })); + setDirty(true); + } + if (!detail) return ; + + const payload = (next = form) => ({ + status: next.status, + category: next.category, + priority: next.priority, + slaLevel: next.slaLevel, + statusDetail: next.statusDetail, + handledBy: next.handledBy, + assignee: next.assignee, + dueAt: fromLocalInput(next.dueAt), + resolution: next.resolution, + note: next.note, + publicReply: next.publicReply, + tags: splitTags(next.tags) + }); + + async function save(nextForm = form) { + setSaving(true); + try { + const data = await api.patch<{ feedback: FeedbackRecord }>(`/api/admin/feedbacks/${detail.code}`, payload(nextForm)); + if (data?.feedback) { + setDirty(false); + onSaved(data.feedback); + } + } finally { + setSaving(false); + } + } + async function quickStatus(status: string) { + const next = { ...form, status }; + setForm(next); + setDirty(true); + await save(next); + } + async function addComment() { + if (!comment.trim()) return; + const data = await api.post<{ comment: FeedbackComment }>(`/api/admin/feedbacks/${detail.code}/comments`, { author: form.handledBy || form.assignee || "admin", body: comment, internal: true }); + if (data?.comment) { + setComment(""); + onSaved({ ...detail, comments: [data.comment, ...(detail.comments ?? [])] }); + } + } + + return ( + + + diff --git a/server/unified-management/web/admin/src/views/HealthView.vue b/server/unified-management/web/admin/src/views/HealthView.vue new file mode 100644 index 0000000..10be9e4 --- /dev/null +++ b/server/unified-management/web/admin/src/views/HealthView.vue @@ -0,0 +1,10 @@ + + + diff --git a/server/unified-management/web/admin/src/views/LegacyJsonView.vue b/server/unified-management/web/admin/src/views/LegacyJsonView.vue new file mode 100644 index 0000000..e23012f --- /dev/null +++ b/server/unified-management/web/admin/src/views/LegacyJsonView.vue @@ -0,0 +1,30 @@ + + + diff --git a/server/unified-management/web/admin/src/views/ReleasesView.vue b/server/unified-management/web/admin/src/views/ReleasesView.vue new file mode 100644 index 0000000..3852e69 --- /dev/null +++ b/server/unified-management/web/admin/src/views/ReleasesView.vue @@ -0,0 +1,51 @@ + + + diff --git a/server/unified-management/web/admin/src/views/SettingsView.vue b/server/unified-management/web/admin/src/views/SettingsView.vue new file mode 100644 index 0000000..5083781 --- /dev/null +++ b/server/unified-management/web/admin/src/views/SettingsView.vue @@ -0,0 +1,21 @@ + + + diff --git a/server/unified-management/web/admin/src/views/SourcesView.vue b/server/unified-management/web/admin/src/views/SourcesView.vue new file mode 100644 index 0000000..57fb3a2 --- /dev/null +++ b/server/unified-management/web/admin/src/views/SourcesView.vue @@ -0,0 +1,43 @@ + + + diff --git a/server/unified-management/web/admin/tsconfig.json b/server/unified-management/web/admin/tsconfig.json new file mode 100644 index 0000000..7166277 --- /dev/null +++ b/server/unified-management/web/admin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/server/unified-management/web/admin/vite.config.ts b/server/unified-management/web/admin/vite.config.ts new file mode 100644 index 0000000..1ce5d82 --- /dev/null +++ b/server/unified-management/web/admin/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + base: "/admin/", + plugins: [vue()], + server: { + proxy: { + "/api": "http://127.0.0.1:33550" + } + } +}); diff --git a/server/unified-management/web/embed.go b/server/unified-management/web/embed.go new file mode 100644 index 0000000..ac43ea2 --- /dev/null +++ b/server/unified-management/web/embed.go @@ -0,0 +1,21 @@ +//go:build embed_web + +package webassets + +import ( + "embed" + "io/fs" +) + +//go:embed admin/dist portal/dist setup/dist +var FS embed.FS + +const Embedded = true + +func ReadFile(name string) ([]byte, error) { + return FS.ReadFile(name) +} + +func ReadDir(name string) ([]fs.DirEntry, error) { + return FS.ReadDir(name) +} diff --git a/server/unified-management/web/noembed.go b/server/unified-management/web/noembed.go new file mode 100644 index 0000000..c804967 --- /dev/null +++ b/server/unified-management/web/noembed.go @@ -0,0 +1,18 @@ +//go:build !embed_web + +package webassets + +import ( + "errors" + "io/fs" +) + +const Embedded = false + +func ReadFile(name string) ([]byte, error) { + return nil, errors.New("web assets were not embedded; build with -tags embed_web") +} + +func ReadDir(name string) ([]fs.DirEntry, error) { + return nil, errors.New("web assets were not embedded; build with -tags embed_web") +} diff --git a/server/unified-management/web/portal/index.html b/server/unified-management/web/portal/index.html new file mode 100644 index 0000000..3c87073 --- /dev/null +++ b/server/unified-management/web/portal/index.html @@ -0,0 +1,12 @@ + + + + + + YMhut Box Service Portal + + +
+ + + diff --git a/server/unified-management/web/portal/package-lock.json b/server/unified-management/web/portal/package-lock.json new file mode 100644 index 0000000..b84db70 --- /dev/null +++ b/server/unified-management/web/portal/package-lock.json @@ -0,0 +1,1350 @@ +{ + "name": "ymhut-unified-portal", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ymhut-unified-portal", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "lucide-vue-next": "^0.468.0", + "vite": "^6.3.5", + "vue": "^3.5.16", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "typescript": "^5.8.3" + } + }, + "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/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/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/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "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==", + "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.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "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.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "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.39", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "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/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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.468.0", + "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz", + "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "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/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "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.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "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" + } + } + } +} diff --git a/server/unified-management/web/portal/package.json b/server/unified-management/web/portal/package.json new file mode 100644 index 0000000..317dd41 --- /dev/null +++ b/server/unified-management/web/portal/package.json @@ -0,0 +1,20 @@ +{ + "name": "ymhut-unified-portal", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build" + }, + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "lucide-vue-next": "^0.468.0", + "vite": "^6.3.5", + "vue": "^3.5.16", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/server/unified-management/web/portal/src/App.vue b/server/unified-management/web/portal/src/App.vue new file mode 100644 index 0000000..493c464 --- /dev/null +++ b/server/unified-management/web/portal/src/App.vue @@ -0,0 +1,41 @@ + + + diff --git a/server/unified-management/web/portal/src/main.ts b/server/unified-management/web/portal/src/main.ts new file mode 100644 index 0000000..4c14cae --- /dev/null +++ b/server/unified-management/web/portal/src/main.ts @@ -0,0 +1,22 @@ +import { createApp } from "vue"; +import { createRouter, createWebHistory } from "vue-router"; +import App from "./App.vue"; +import OverviewPage from "./pages/OverviewPage.vue"; +import ReleasesPage from "./pages/ReleasesPage.vue"; +import SourcesPage from "./pages/SourcesPage.vue"; +import FeedbackPage from "./pages/FeedbackPage.vue"; +import CompatibilityPage from "./pages/CompatibilityPage.vue"; +import "./styles.css"; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: "/", component: OverviewPage }, + { path: "/releases", component: ReleasesPage }, + { path: "/sources", component: SourcesPage }, + { path: "/feedback", component: FeedbackPage }, + { path: "/compatibility", component: CompatibilityPage }, + ], +}); + +createApp(App).use(router).mount("#app"); diff --git a/server/unified-management/web/portal/src/pages/CompatibilityPage.vue b/server/unified-management/web/portal/src/pages/CompatibilityPage.vue new file mode 100644 index 0000000..97e64c4 --- /dev/null +++ b/server/unified-management/web/portal/src/pages/CompatibilityPage.vue @@ -0,0 +1,29 @@ + + + diff --git a/server/unified-management/web/portal/src/pages/FeedbackPage.vue b/server/unified-management/web/portal/src/pages/FeedbackPage.vue new file mode 100644 index 0000000..f1c58da --- /dev/null +++ b/server/unified-management/web/portal/src/pages/FeedbackPage.vue @@ -0,0 +1,24 @@ + + + diff --git a/server/unified-management/web/portal/src/pages/OverviewPage.vue b/server/unified-management/web/portal/src/pages/OverviewPage.vue new file mode 100644 index 0000000..e0f212e --- /dev/null +++ b/server/unified-management/web/portal/src/pages/OverviewPage.vue @@ -0,0 +1,66 @@ + + + diff --git a/server/unified-management/web/portal/src/pages/ReleasesPage.vue b/server/unified-management/web/portal/src/pages/ReleasesPage.vue new file mode 100644 index 0000000..8e8c3cc --- /dev/null +++ b/server/unified-management/web/portal/src/pages/ReleasesPage.vue @@ -0,0 +1,48 @@ + + + diff --git a/server/unified-management/web/portal/src/pages/SourcesPage.vue b/server/unified-management/web/portal/src/pages/SourcesPage.vue new file mode 100644 index 0000000..bee56d8 --- /dev/null +++ b/server/unified-management/web/portal/src/pages/SourcesPage.vue @@ -0,0 +1,32 @@ + + + diff --git a/server/unified-management/web/portal/src/state.ts b/server/unified-management/web/portal/src/state.ts new file mode 100644 index 0000000..143207b --- /dev/null +++ b/server/unified-management/web/portal/src/state.ts @@ -0,0 +1,102 @@ +import { computed, ref } from "vue"; + +const bootstrap = ref(null); +const releases = ref(null); +const sources = ref(null); +const notices = ref([]); +const loading = ref(false); +const error = ref(""); +let loaded = false; + +async function fetchJSON(path: string) { + const res = await fetch(path); + if (!res.ok) throw new Error(`${path} returned HTTP ${res.status}`); + return res.json(); +} + +export function usePortalState() { + const packages = computed(() => releases.value?.packages || bootstrap.value?.release?.packages || []); + const categories = computed(() => sources.value?.categories || bootstrap.value?.sources?.categories || []); + const latestNotice = computed(() => notices.value[0] || releases.value?.latest_notice || bootstrap.value?.release?.latest_notice || null); + const sourceCount = computed(() => categories.value.reduce((total: number, cat: any) => total + (cat.subcategories?.length || 0), 0)); + const healthyCount = computed(() => categories.value.reduce((total: number, cat: any) => { + return total + (cat.subcategories || []).filter((item: any) => sourceStatus(item) === "ok").length; + }, 0)); + const availability = computed(() => sourceCount.value ? Math.round((healthyCount.value / sourceCount.value) * 100) : 0); + const downloadUrl = computed(() => releases.value?.download_url || bootstrap.value?.release?.download_url || "/update-info.json"); + const appVersion = computed(() => releases.value?.app_version || bootstrap.value?.release?.app_version || latestNotice.value?.version || "未发布"); + const databaseStatus = computed(() => bootstrap.value?.health?.database?.activeProvider || bootstrap.value?.health?.database?.configProvider || "-"); + const serviceVersion = computed(() => bootstrap.value?.serviceVersion || "-"); + + async function load(force = false) { + if (loaded && !force) return; + loading.value = true; + error.value = ""; + try { + const [bootstrapData, releaseData, sourceData, noticeData] = await Promise.allSettled([ + fetchJSON("/api/client/bootstrap"), + fetchJSON("/api/client/releases"), + fetchJSON("/api/client/sources"), + fetchJSON("/api/client/notices"), + ]); + if (bootstrapData.status === "fulfilled") bootstrap.value = bootstrapData.value; + if (releaseData.status === "fulfilled") releases.value = releaseData.value; + if (sourceData.status === "fulfilled") sources.value = sourceData.value; + if (noticeData.status === "fulfilled") notices.value = noticeData.value.items || []; + const firstFailure = [bootstrapData, releaseData, sourceData, noticeData].find((item) => item.status === "rejected") as PromiseRejectedResult | undefined; + if (firstFailure && !bootstrap.value) error.value = firstFailure.reason?.message || String(firstFailure.reason); + loaded = true; + } catch (err) { + error.value = err instanceof Error ? err.message : String(err); + } finally { + loading.value = false; + } + } + + return { + bootstrap, + releases, + sources, + notices, + loading, + error, + packages, + categories, + latestNotice, + sourceCount, + healthyCount, + availability, + downloadUrl, + appVersion, + databaseStatus, + serviceVersion, + load, + sourceStatus, + statusTone, + formatBytes, + }; +} + +export function sourceStatus(item: any) { + return item.health?.status || item.lastStatus || "unknown"; +} + +export function statusTone(status: string) { + const value = String(status || "").toLowerCase(); + if (["ok", "sqlite", "mysql", "online", "ready"].includes(value)) return "good"; + if (["degraded", "pending", "missing"].includes(value)) return "warn"; + if (["error", "offline", "failed"].includes(value)) return "bad"; + return "neutral"; +} + +export function formatBytes(value: number) { + if (!Number.isFinite(value) || value <= 0) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + let next = value; + let index = 0; + while (next >= 1024 && index < units.length - 1) { + next /= 1024; + index += 1; + } + return `${next.toFixed(index === 0 ? 0 : 1)} ${units[index]}`; +} diff --git a/server/unified-management/web/portal/src/styles.css b/server/unified-management/web/portal/src/styles.css new file mode 100644 index 0000000..fcd69a2 --- /dev/null +++ b/server/unified-management/web/portal/src/styles.css @@ -0,0 +1,480 @@ +:root { + color-scheme: light; + font-family: "Microsoft YaHei UI", "Segoe UI", Arial, sans-serif; + color: #172033; + background: #f7f9ff; + --ink: #172033; + --muted: #63718a; + --soft: #f7f9ff; + --panel: rgba(255, 255, 255, 0.82); + --panel-strong: #ffffff; + --line: rgba(112, 132, 170, 0.18); + --line-strong: rgba(94, 114, 158, 0.28); + --primary: #3b82f6; + --primary-dark: #2563eb; + --cyan: #06b6d4; + --violet: #8b5cf6; + --pink: #f472b6; + --good: #059669; + --warn: #b7791f; + --bad: #dc2626; + --shadow: 0 22px 65px rgba(65, 88, 140, 0.16); +} + +* { box-sizing: border-box; } +html { min-width: 320px; } +body { + margin: 0; + min-width: 320px; + background: + radial-gradient(circle at 8% 6%, rgba(96, 165, 250, 0.30), transparent 28%), + radial-gradient(circle at 88% 8%, rgba(244, 114, 182, 0.20), transparent 30%), + linear-gradient(180deg, #eef6ff 0%, #f8fbff 42%, #ffffff 100%); +} +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px); + background-size: 42px 42px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.55), transparent 70%); +} +a { color: inherit; } +button, input { font: inherit; } +button { cursor: pointer; } + +.portal-shell { + position: relative; + min-height: 100dvh; + padding: 18px clamp(14px, 2.4vw, 28px) 48px; + overflow: hidden; +} + +.topnav { + position: sticky; + z-index: 20; + top: 14px; + min-height: 64px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + width: min(1180px, 100%); + margin: 0 auto 22px; + padding: 9px 12px; + border: 1px solid rgba(255, 255, 255, 0.72); + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + box-shadow: 0 16px 42px rgba(62, 87, 130, 0.12); + backdrop-filter: blur(18px); +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 44px; + padding: 5px 12px 5px 6px; + border-radius: 999px; + text-decoration: none; +} +.brand span { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border-radius: 50%; + color: #fff; + background: linear-gradient(135deg, var(--primary), var(--cyan)); + box-shadow: 0 12px 26px rgba(37, 99, 235, 0.26); +} +.brand strong { letter-spacing: 0; } +.nav-links { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: center; +} +.nav-links a, .admin-link { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + padding: 8px 12px; + color: #53627d; + text-decoration: none; + font-size: 14px; + font-weight: 800; + transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease; +} +.nav-links a:hover, .nav-links a.active { + color: var(--primary-dark); + background: rgba(59, 130, 246, 0.12); +} +.admin-link { + color: #fff; + background: linear-gradient(135deg, #2563eb, #7c3aed); + box-shadow: 0 12px 28px rgba(59, 130, 246, 0.24); +} +.admin-link:hover { box-shadow: 0 16px 36px rgba(59, 130, 246, 0.32); } + +.hero { + position: relative; + width: min(1180px, 100%); + min-height: 520px; + margin: 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 22px; + align-items: stretch; + border: 1px solid rgba(255, 255, 255, 0.70); + border-radius: 32px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.62)), + radial-gradient(circle at 88% 18%, rgba(14, 165, 233, 0.26), transparent 34%), + radial-gradient(circle at 18% 82%, rgba(139, 92, 246, 0.18), transparent 30%); + box-shadow: var(--shadow); + padding: clamp(28px, 5vw, 58px); + overflow: hidden; +} +.hero::after { + content: ""; + position: absolute; + right: -80px; + bottom: -120px; + width: 360px; + height: 360px; + border-radius: 50%; + background: radial-gradient(circle, rgba(59, 130, 246, 0.20), transparent 68%); +} +.hero-copy { + position: relative; + z-index: 1; + max-width: 780px; + align-self: center; +} +.eyebrow { + margin: 0 0 12px; + color: var(--primary-dark); + font-size: 12px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.14em; +} +h1 { + margin: 0 0 18px; + max-width: 900px; + color: #12213a; + font-size: clamp(36px, 6vw, 68px); + line-height: 1.02; + letter-spacing: 0; +} +h2, h3, p { margin-top: 0; } +p { + color: var(--muted); + line-height: 1.85; + font-size: 16px; +} +.hero-copy p { max-width: 690px; font-size: 17px; } + +.actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 28px; +} +.hero-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 22px; +} +.hero-tags span { + border: 1px solid rgba(59, 130, 246, 0.18); + border-radius: 999px; + padding: 7px 11px; + color: #355075; + background: rgba(255, 255, 255, 0.58); + font-size: 13px; + font-weight: 900; +} +.button { + min-height: 46px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border: 1px solid rgba(118, 137, 178, 0.22); + border-radius: 999px; + text-decoration: none; + background: rgba(255, 255, 255, 0.76); + color: #263856; + font-weight: 900; + box-shadow: 0 10px 26px rgba(65, 88, 140, 0.10); + transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; +} +.button:hover { + transform: translateY(-1px); + background: #fff; + box-shadow: 0 16px 36px rgba(65, 88, 140, 0.16); +} +.button.primary { + color: #fff; + border-color: transparent; + background: linear-gradient(135deg, #2563eb, #06b6d4); + box-shadow: 0 16px 34px rgba(37, 99, 235, 0.26); +} + +.release-card, .panel, .metric { + border: 1px solid rgba(255, 255, 255, 0.74); + border-radius: 24px; + background: var(--panel); + box-shadow: 0 14px 42px rgba(65, 88, 140, 0.11); + backdrop-filter: blur(16px); +} +.release-card { + position: relative; + z-index: 1; + align-self: center; + display: grid; + gap: 12px; + padding: 24px; +} +.release-card span { color: var(--muted); font-weight: 800; } +.release-card .live-dot { + width: fit-content; + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--good); + border: 1px solid rgba(16, 185, 129, 0.22); + border-radius: 999px; + padding: 5px 9px; + background: rgba(209, 250, 229, 0.66); +} +.release-card .live-dot::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: #10b981; + box-shadow: 0 0 0 5px rgba(16, 185, 129, 0.13); +} +.release-card strong { + display: block; + font-size: 44px; + line-height: 1.05; + overflow-wrap: anywhere; +} +.release-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 14px; +} +.release-meta span, .badge { + display: inline-flex; + align-items: center; + gap: 4px; + border: 1px solid var(--line); + border-radius: 999px; + padding: 5px 10px; + color: #44536e; + background: rgba(255, 255, 255, 0.68); + font-size: 12px; + font-weight: 900; +} + +.metric-grid { + width: min(1180px, 100%); + margin: 18px auto 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; +} +.metric { + min-height: 132px; + display: grid; + gap: 8px; + padding: 18px; +} +.metric svg { color: var(--primary); } +.metric span { color: var(--muted); font-weight: 800; } +.metric strong { color: #15233b; font-size: 30px; overflow-wrap: anywhere; } + +.content-grid { + width: min(1180px, 100%); + margin: 18px auto 0; + display: grid; + grid-template-columns: 1.12fr 0.88fr; + gap: 18px; +} +.panel { + padding: 22px; +} +.panel.wide { grid-column: 1 / -1; } +.page-heading { + width: min(1180px, 100%); + margin: 0 auto 18px; + border: 1px solid rgba(255, 255, 255, 0.74); + border-radius: 28px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.68)); + box-shadow: var(--shadow); + padding: clamp(24px, 4vw, 42px); + backdrop-filter: blur(16px); +} +.page-heading h1 { font-size: clamp(32px, 4.6vw, 52px); } + +.section-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} +.section-head h2 { margin: 0; color: #14223a; } +.section-head a { + color: var(--primary-dark); + font-weight: 900; + display: inline-flex; + align-items: center; + gap: 5px; + text-decoration: none; +} +table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} +th, td { + border-bottom: 1px solid rgba(112, 132, 170, 0.18); + padding: 11px 8px; + text-align: left; + vertical-align: top; +} +th { + color: var(--muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.muted, .empty { color: var(--muted); } +.notice-list { display: grid; gap: 12px; } +.notice-card { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 12px; + border: 1px solid rgba(112, 132, 170, 0.15); + border-radius: 20px; + background: rgba(255, 255, 255, 0.72); + padding: 14px; +} +.notice-card svg { color: var(--primary); } +.notice-card strong { display: block; margin-bottom: 6px; overflow-wrap: anywhere; } +.notice-card p { margin-bottom: 8px; font-size: 14px; line-height: 1.65; } +.notice-card span { color: var(--muted); font-size: 13px; } + +.feedback-panel { max-width: 780px; margin: 0 auto; } +.feedback-box { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + margin-top: 14px; +} +input { + width: 100%; + min-height: 46px; + border: 1px solid rgba(112, 132, 170, 0.24); + border-radius: 16px; + background: rgba(255, 255, 255, 0.86); + color: #172033; + padding: 10px 14px; + outline: none; +} +input:focus { + border-color: rgba(59, 130, 246, 0.65); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14); +} +.source-board { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} +.source-group { + border: 1px solid rgba(112, 132, 170, 0.16); + border-radius: 22px; + padding: 16px; + background: rgba(255, 255, 255, 0.72); +} +.source-group h3 { margin-bottom: 2px; color: #14223a; } +.source-group p { margin-bottom: 10px; font-size: 14px; } +.source-list { + display: flex; + gap: 7px; + flex-wrap: wrap; +} +.badge.good { color: var(--good); background: rgba(209, 250, 229, 0.78); border-color: rgba(16, 185, 129, 0.28); } +.badge.warn { color: var(--warn); background: rgba(254, 243, 199, 0.82); border-color: rgba(245, 158, 11, 0.28); } +.badge.bad { color: var(--bad); background: rgba(254, 226, 226, 0.82); border-color: rgba(239, 68, 68, 0.26); } +.route-list { display: grid; gap: 10px; } +.route-list a { + display: grid; + gap: 4px; + border: 1px solid rgba(112, 132, 170, 0.16); + border-radius: 18px; + background: rgba(255, 255, 255, 0.70); + padding: 14px; + text-decoration: none; + font-weight: 900; + transition: transform 0.18s ease, border-color 0.18s ease, background-color 0.18s ease; +} +.route-list a:hover { + transform: translateY(-1px); + border-color: rgba(59, 130, 246, 0.36); + background: rgba(255, 255, 255, 0.92); +} +.route-list span { color: var(--muted); font-size: 13px; font-weight: 700; } +.state-banner { + width: min(1180px, 100%); + margin: 12px auto; + border-radius: 999px; + padding: 10px 14px; + font-weight: 900; + display: flex; + align-items: center; + gap: 8px; + background: rgba(255, 255, 255, 0.78); + box-shadow: 0 12px 30px rgba(65, 88, 140, 0.10); +} +.error { color: var(--bad); } +.loading { color: var(--muted); } + +@media (max-width: 980px) { + .topnav { + position: static; + align-items: stretch; + flex-direction: column; + border-radius: 24px; + } + .nav-links { justify-content: flex-start; } + .hero, .content-grid, .metric-grid, .source-board { grid-template-columns: 1fr; } + .hero { min-height: auto; } + .feedback-box { grid-template-columns: 1fr; } + table { min-width: 680px; } + .panel { overflow-x: auto; } +} + +@media (max-width: 560px) { + .portal-shell { padding-inline: 10px; } + .hero, .page-heading { border-radius: 24px; padding: 24px; } + h1 { font-size: 36px; } + .actions .button { width: 100%; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { transition: none !important; scroll-behavior: auto !important; } +} diff --git a/server/unified-management/web/portal/vite.config.ts b/server/unified-management/web/portal/vite.config.ts new file mode 100644 index 0000000..84b6402 --- /dev/null +++ b/server/unified-management/web/portal/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], + server: { + proxy: { + "/api": "http://127.0.0.1:33550", + "/update-info.json": "http://127.0.0.1:33550", + "/downloads": "http://127.0.0.1:33550" + } + } +}); diff --git a/server/unified-management/web/setup/index.html b/server/unified-management/web/setup/index.html new file mode 100644 index 0000000..37b4f0c --- /dev/null +++ b/server/unified-management/web/setup/index.html @@ -0,0 +1,12 @@ + + + + + + YMhut Unified Setup + + +
+ + + diff --git a/server/unified-management/web/setup/package-lock.json b/server/unified-management/web/setup/package-lock.json new file mode 100644 index 0000000..bd21571 --- /dev/null +++ b/server/unified-management/web/setup/package-lock.json @@ -0,0 +1,1328 @@ +{ + "name": "ymhut-unified-setup", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ymhut-unified-setup", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "lucide-vue-next": "^0.468.0", + "vite": "^6.3.5", + "vue": "^3.5.16" + }, + "devDependencies": { + "typescript": "^5.8.3" + } + }, + "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/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/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/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "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==", + "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.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "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.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "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/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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.468.0", + "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz", + "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "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/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "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.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/server/unified-management/web/setup/package.json b/server/unified-management/web/setup/package.json new file mode 100644 index 0000000..84b4932 --- /dev/null +++ b/server/unified-management/web/setup/package.json @@ -0,0 +1,19 @@ +{ + "name": "ymhut-unified-setup", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build" + }, + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "lucide-vue-next": "^0.468.0", + "vite": "^6.3.5", + "vue": "^3.5.16" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/server/unified-management/web/setup/src/App.vue b/server/unified-management/web/setup/src/App.vue new file mode 100644 index 0000000..0637e67 --- /dev/null +++ b/server/unified-management/web/setup/src/App.vue @@ -0,0 +1,288 @@ + + + diff --git a/server/unified-management/web/setup/src/main.ts b/server/unified-management/web/setup/src/main.ts new file mode 100644 index 0000000..8d6a5ff --- /dev/null +++ b/server/unified-management/web/setup/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import "./styles.css"; + +createApp(App).mount("#app"); diff --git a/server/unified-management/web/setup/src/styles.css b/server/unified-management/web/setup/src/styles.css new file mode 100644 index 0000000..8e8b796 --- /dev/null +++ b/server/unified-management/web/setup/src/styles.css @@ -0,0 +1,190 @@ +:root { + color-scheme: dark; + font-family: "Microsoft YaHei UI", "Segoe UI", Arial, sans-serif; + color: #e5eefb; + background: #0b1220; + --panel: rgba(15, 23, 42, 0.9); + --panel-soft: rgba(30, 41, 59, 0.72); + --line: rgba(148, 163, 184, 0.28); + --muted: #94a3b8; + --ink: #f8fafc; + --primary: #22c55e; + --blue: #38bdf8; + --warn: #fbbf24; + --bad: #fb7185; +} + +* { box-sizing: border-box; } +body { margin: 0; min-width: 320px; background: #0b1220; } +button, input, select { font: inherit; } +button { cursor: pointer; } +button:disabled { cursor: not-allowed; opacity: 0.55; } + +.setup-shell { + min-height: 100dvh; + display: grid; + place-items: center; + padding: 24px; + background: + radial-gradient(circle at 10% 10%, rgba(56, 189, 248, 0.15), transparent 32%), + radial-gradient(circle at 82% 18%, rgba(34, 197, 94, 0.16), transparent 34%), + linear-gradient(145deg, #0b1220, #111827 52%, #0f172a); +} + +.setup-card { + width: min(1120px, 100%); + min-height: 680px; + display: grid; + grid-template-columns: 360px minmax(0, 1fr); + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(2, 6, 23, 0.74); + box-shadow: 0 30px 90px rgba(0, 0, 0, 0.36); + overflow: hidden; +} + +.setup-aside { + padding: 28px; + background: linear-gradient(180deg, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.68)); + border-right: 1px solid var(--line); +} + +.setup-aside h1 { margin: 0 0 16px; font-size: 38px; line-height: 1.08; letter-spacing: 0; } +.setup-aside p { color: var(--muted); line-height: 1.75; } +.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; } +.step-list { list-style: none; margin: 28px 0 0; padding: 0; display: grid; gap: 12px; } +.step-list li { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 12px; + align-items: center; + padding: 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(15, 23, 42, 0.54); +} +.step-list li > span { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 8px; + background: #1e293b; + color: var(--muted); +} +.step-list li.active { border-color: rgba(34, 197, 94, 0.75); background: rgba(34, 197, 94, 0.08); } +.step-list li.done > span, .step-list li.active > span { color: #052e16; background: var(--primary); } +.step-list strong { display: block; } +.step-list small { color: var(--muted); } + +.setup-main { padding: 28px; display: flex; flex-direction: column; gap: 18px; } +.section-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; } +.section-head h2 { margin: 0; font-size: 26px; } +.badge { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid var(--line); + border-radius: 999px; + padding: 5px 9px; + color: var(--muted); + font-weight: 800; +} +.spin { animation: spin 1s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} +.wide { grid-column: 1 / -1; } +label { display: grid; gap: 7px; color: #cbd5e1; font-weight: 800; font-size: 13px; } +input, select { + width: 100%; + min-height: 42px; + border-radius: 6px; + border: 1px solid #334155; + background: #020617; + color: var(--ink); + padding: 9px 11px; + outline: none; +} +input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15); } +input[readonly] { color: var(--muted); } +.hint { margin: 0; color: var(--muted); line-height: 1.65; } + +.choice-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } +.choice-grid button { + min-height: 190px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--ink); + padding: 18px; + text-align: left; + display: grid; + align-content: start; + gap: 10px; +} +.choice-grid button.selected { border-color: var(--primary); background: rgba(34, 197, 94, 0.09); } +.choice-grid span { color: var(--muted); line-height: 1.6; } + +.summary-box { + display: grid; + grid-template-columns: 140px minmax(0, 1fr); + gap: 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel-soft); + padding: 16px; +} +.summary-box span { color: var(--muted); } +.summary-box strong { overflow-wrap: anywhere; } +.test-panel, .confirm-panel, .complete-panel { display: grid; gap: 16px; } +.complete-panel { + min-height: 420px; + place-items: center; + align-content: center; + text-align: center; +} +.complete-panel svg { color: var(--primary); } +.complete-panel p { max-width: 620px; color: var(--muted); line-height: 1.8; } +code { color: #bfdbfe; } + +.btn { + min-height: 42px; + border: 1px solid var(--line); + border-radius: 6px; + padding: 10px 14px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 900; + background: transparent; + color: var(--ink); +} +.btn.primary { background: var(--primary); color: #052e16; border-color: var(--primary); } +.btn.ghost { color: #cbd5e1; background: #1e293b; } +.setup-actions { margin-top: auto; display: flex; justify-content: space-between; gap: 10px; } +.alert, .result { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + align-items: start; + border-radius: 8px; + padding: 12px; + line-height: 1.6; +} +.alert.bad { color: #fecdd3; background: rgba(244, 63, 94, 0.12); border: 1px solid rgba(244, 63, 94, 0.36); } +.alert.warn { color: #fde68a; background: rgba(245, 158, 11, 0.12); border: 1px solid rgba(245, 158, 11, 0.36); } +.result.good { color: #bbf7d0; background: rgba(34, 197, 94, 0.12); border: 1px solid rgba(34, 197, 94, 0.36); } +.result p { margin: 4px 0; color: var(--muted); } + +@media (max-width: 900px) { + .setup-card { grid-template-columns: 1fr; } + .setup-aside { border-right: 0; border-bottom: 1px solid var(--line); } + .form-grid, .choice-grid { grid-template-columns: 1fr; } + .summary-box { grid-template-columns: 1fr; } +} diff --git a/server/unified-management/web/setup/tsconfig.json b/server/unified-management/web/setup/tsconfig.json new file mode 100644 index 0000000..7166277 --- /dev/null +++ b/server/unified-management/web/setup/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/server/unified-management/web/setup/vite.config.ts b/server/unified-management/web/setup/vite.config.ts new file mode 100644 index 0000000..834eb45 --- /dev/null +++ b/server/unified-management/web/setup/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + base: "/setup/", + plugins: [vue()], + server: { + proxy: { + "/api": "http://127.0.0.1:33550" + } + } +}); diff --git a/server/unified-management/web/wasm/Cargo.toml b/server/unified-management/web/wasm/Cargo.toml new file mode 100644 index 0000000..89658c1 --- /dev/null +++ b/server/unified-management/web/wasm/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ymhut_unified_wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/server/unified-management/web/wasm/src/lib.rs b/server/unified-management/web/wasm/src/lib.rs new file mode 100644 index 0000000..67e80f6 --- /dev/null +++ b/server/unified-management/web/wasm/src/lib.rs @@ -0,0 +1,96 @@ +use serde_json::{json, Value}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = compareVersions)] +pub fn compare_versions(a: &str, b: &str) -> i32 { + let mut av = parse_version(a); + let mut bv = parse_version(b); + av.resize(4, 0); + bv.resize(4, 0); + for index in 0..4 { + if av[index] > bv[index] { + return 1; + } + if av[index] < bv[index] { + return -1; + } + } + 0 +} + +#[wasm_bindgen(js_name = scoreEndpointHealth)] +pub fn score_endpoint_health(status: &str, latency_ms: i32, failures: i32) -> i32 { + let base = match status { + "ok" => 100, + "degraded" => 70, + "error" => 30, + _ => 50, + }; + let latency_penalty = (latency_ms / 250).clamp(0, 30); + let failure_penalty = (failures * 12).clamp(0, 48); + (base - latency_penalty - failure_penalty).clamp(0, 100) +} + +#[wasm_bindgen(js_name = normalizeReleaseManifest)] +pub fn normalize_release_manifest(input: &str) -> String { + let mut value: Value = serde_json::from_str(input).unwrap_or_else(|_| json!({})); + if value.get("manifest_version").is_none() { + value["manifest_version"] = json!(2); + } + if value.get("packages").is_none() { + value["packages"] = json!([]); + } + serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string()) +} + +#[wasm_bindgen(js_name = validateSourceCatalog)] +pub fn validate_source_catalog(input: &str) -> String { + let value: Value = serde_json::from_str(input).unwrap_or_else(|_| json!({})); + let categories = value + .get("categories") + .and_then(|item| item.as_array()) + .map(|items| items.len()) + .unwrap_or_default(); + let mut source_count = 0usize; + if let Some(items) = value.get("categories").and_then(|item| item.as_array()) { + for category in items { + source_count += category + .get("subcategories") + .and_then(|item| item.as_array()) + .map(|items| items.len()) + .unwrap_or_default(); + } + } + serde_json::to_string(&json!({ + "ok": categories > 0, + "categories": categories, + "sources": source_count + })).unwrap_or_else(|_| "{\"ok\":false}".to_string()) +} + +#[wasm_bindgen(js_name = mergeLegacyMediaTypes)] +pub fn merge_legacy_media_types(current: &str, legacy: &str) -> String { + let current_value: Value = serde_json::from_str(current).unwrap_or_else(|_| json!({"categories":[]})); + let legacy_value: Value = serde_json::from_str(legacy).unwrap_or_else(|_| json!({"categories":[]})); + let mut categories = current_value + .get("categories") + .and_then(|item| item.as_array()) + .cloned() + .unwrap_or_default(); + if let Some(legacy_categories) = legacy_value.get("categories").and_then(|item| item.as_array()) { + for category in legacy_categories { + categories.push(category.clone()); + } + } + serde_json::to_string(&json!({ + "layout_version": "2.0.0", + "categories": categories + })).unwrap_or_else(|_| "{\"categories\":[]}".to_string()) +} + +fn parse_version(value: &str) -> Vec { + value + .split('.') + .map(|part| part.parse::().unwrap_or_default()) + .collect() +} diff --git a/server/update/ADMIN.md b/server/update/ADMIN.md new file mode 100644 index 0000000..206530c --- /dev/null +++ b/server/update/ADMIN.md @@ -0,0 +1,169 @@ +# 后台管理系统使用说明 + +## 功能概述 + +后台管理系统提供了完整的用户认证、内容管理、路由管理、日志查看等功能。 + +## 访问地址 + +访问 `http://localhost:3355/admin` 进入后台管理界面。 + +## 用户注册和登录 + +### 注册 + +1. 首次访问后台管理页面,点击"注册"标签 +2. 填写以下信息: + - **用户名**:3-50个字符 + - **邮箱**:有效的邮箱地址 + - **密码**:必须满足以下要求: + - 至少8个字符 + - 包含至少一个大写字母 + - 包含至少一个小写字母 + - 包含至少一个数字 + - 包含至少一个特殊字符 + - 不能是常见弱密码(如 password、123456 等) + - 不能全部是相同字符 + +3. **重要**:第一个注册的用户将自动成为管理员 + +### 登录 + +1. 使用注册的用户名和密码登录 +2. 登录成功后会自动跳转到管理界面 + +## 功能模块 + +### 1. 仪表盘 + +显示系统概览信息: +- 用户总数 +- 路由总数 +- 日志条目数 +- 服务器时间 + +### 2. 路由管理 + +可以添加、编辑、删除路由: + +- **HTTP 方法**:GET、POST、PUT、DELETE、PATCH +- **路径**:路由路径(如 `/api/example`) +- **类型**: + - `view`:视图路由 + - `json`:JSON 接口 + - `file`:文件路由 + - `static`:静态文件 + - `custom`:自定义处理 +- **处理器/文件路径**:处理函数或文件路径 +- **描述**:路由描述(可选) +- **启用状态**:是否启用该路由 +- **排序**:路由执行顺序 + +### 3. 文件管理 + +- 浏览服务器文件 +- 查看文件内容 +- 编辑文件(需要手动实现保存功能) + +### 4. 配置管理 + +可以编辑以下配置文件: +- `tool-status.json` +- `update-info.json` +- `media-types.json` + +操作步骤: +1. 选择要编辑的配置文件 +2. 点击"加载配置"加载当前配置 +3. 在编辑器中修改 JSON 内容 +4. 点击"保存配置"保存更改 + +### 5. 日志查看 + +- 实时查看系统日志 +- 支持刷新和清空日志 +- 日志级别:INFO、WARN、ERROR + +## API 接口 + +### 认证接口 + +- `POST /admin/register` - 用户注册 +- `POST /admin/login` - 用户登录 +- `POST /admin/logout` - 用户登出 +- `GET /admin/me` - 获取当前用户信息 + +### 管理接口(需要管理员权限) + +- `GET /admin/api/logs` - 获取日志 +- `GET /admin/api/routes` - 获取所有路由 +- `POST /admin/api/routes` - 创建路由 +- `PUT /admin/api/routes/:id` - 更新路由 +- `DELETE /admin/api/routes/:id` - 删除路由 +- `GET /admin/api/files` - 获取文件列表 +- `GET /admin/api/file` - 读取文件 +- `POST /admin/api/file` - 保存文件 +- `PUT /admin/api/config` - 更新配置文件 +- `GET /admin/api/system` - 获取系统信息 +- `POST /admin/api/reload` - 热重载(需要重启服务器) + +## 安全说明 + +1. **密码强度**:系统强制要求强密码,防止弱密码攻击 +2. **JWT 认证**:使用 JWT Token 进行身份验证 +3. **权限控制**:只有管理员可以访问管理功能 +4. **路径验证**:文件操作会验证路径,防止目录遍历攻击 + +## 数据库 + +系统使用 SQLite 数据库,数据库文件位于 `data/app.db`。 + +### 数据表 + +- **users**:用户表 + - id, username, email, password, is_admin, is_active +- **routes**:路由表 + - id, method, path, type, handler, description, is_active, order + +## 注意事项 + +1. **第一个用户**:第一个注册的用户自动成为管理员,请妥善保管账号 +2. **热重载**:路由热重载功能需要重启服务器才能生效 +3. **文件编辑**:文件编辑功能需要谨慎使用,建议先备份 +4. **日志限制**:日志缓冲区最多保存 1000 条记录 + +## 故障排除 + +### 无法登录 + +1. 检查用户名和密码是否正确 +2. 确认账户未被禁用(is_active = true) +3. 检查浏览器 Cookie 是否被禁用 + +### 权限不足 + +1. 确认当前用户是管理员(is_admin = true) +2. 检查 Token 是否有效 +3. 尝试重新登录 + +### 数据库错误 + +1. 检查 `data` 目录权限 +2. 确认 SQLite 数据库文件可写 +3. 查看服务器日志获取详细错误信息 + +## 开发扩展 + +### 添加新的管理功能 + +1. 在 `handlers/admin.go` 中添加处理函数 +2. 在 `config/routes.go` 中注册路由 +3. 在前端 `admin.html` 和 `admin.js` 中添加界面 + +### 自定义日志记录 + +使用 `handlers.AddLog(level, message)` 添加日志到缓冲区。 + +## 技术支持 + +如有问题,请查看服务器日志或联系开发团队。 diff --git a/server/update/BUILD.md b/server/update/BUILD.md new file mode 100644 index 0000000..fd7995b --- /dev/null +++ b/server/update/BUILD.md @@ -0,0 +1,237 @@ +# 打包编译说明 + +## 快速编译 + +### Windows + +```bash +# 方法1: 使用批处理脚本 +build.bat + +# 方法2: 手动编译 +go build -ldflags="-s -w" -o software-download-center.exe . +``` + +### Linux/macOS + +```bash +# 方法1: 使用 Shell 脚本 +chmod +x build.sh +./build.sh + +# 方法2: 手动编译 +go build -ldflags="-s -w" -o software-download-center . +``` + +## 交叉编译 + +### 编译 Windows 版本(在 Linux/macOS 上) + +```bash +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o software-download-center.exe . +``` + +### 编译 Linux 版本(在 Windows 上) + +```bash +set GOOS=linux +set GOARCH=amd64 +go build -ldflags="-s -w" -o software-download-center . +``` + +### 编译 macOS 版本 + +```bash +# Intel 版本 +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o software-download-center . + +# Apple Silicon (M1/M2) 版本 +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o software-download-center . +``` + +## 编译参数说明 + +- `-ldflags="-s -w"`: 减小可执行文件大小 + - `-s`: 去除符号表 + - `-w`: 去除调试信息 + +## 部署文件结构 + +编译后的目录结构: + +``` +output/ +├── software-download-center_*.exe (或可执行文件) +├── start.bat (Windows 启动脚本) +├── start.sh (Linux/macOS 启动脚本) +├── public/ (静态资源目录) +│ ├── css/ +│ ├── img/ +│ ├── fonts/ +│ ├── lang/ +│ └── *.json +├── views/ (模板目录) +│ ├── index.html +│ ├── admin.html +│ ├── 404.html +│ └── 500.html +├── README.md +└── ADMIN.md +``` + +## 运行服务端 + +### Windows + +```bash +# 方法1: 使用启动脚本 +start.bat + +# 方法2: 直接运行 +software-download-center_windows_amd64.exe + +# 方法3: 指定端口 +set PORT=8080 +software-download-center_windows_amd64.exe +``` + +### Linux/macOS + +```bash +# 方法1: 使用启动脚本 +chmod +x start.sh +./start.sh + +# 方法2: 直接运行 +chmod +x software-download-center_linux_amd64 +./software-download-center_linux_amd64 + +# 方法3: 指定端口 +PORT=8080 ./software-download-center_linux_amd64 +``` + +## 作为系统服务运行 + +### Linux (systemd) + +创建服务文件 `/etc/systemd/system/software-download-center.service`: + +```ini +[Unit] +Description=Software Download Center +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/opt/software-download-center +ExecStart=/opt/software-download-center/software-download-center_linux_amd64 +Restart=always +RestartSec=5 +Environment="PORT=3355" + +[Install] +WantedBy=multi-user.target +``` + +启动服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable software-download-center +sudo systemctl start software-download-center +sudo systemctl status software-download-center +``` + +### Windows (NSSM) + +使用 NSSM 将程序安装为 Windows 服务: + +```bash +# 下载 NSSM: https://nssm.cc/download +nssm install SoftwareDownloadCenter "C:\path\to\software-download-center.exe" +nssm set SoftwareDownloadCenter AppDirectory "C:\path\to" +nssm set SoftwareDownloadCenter AppEnvironmentExtra PORT=3355 +nssm start SoftwareDownloadCenter +``` + +## 生产环境建议 + +1. **使用反向代理**(Nginx/Caddy) + ```nginx + server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3355; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + } + ``` + +2. **使用进程管理器** + - Linux: systemd, supervisor, pm2 + - Windows: NSSM, Windows Service + - 跨平台: PM2 + +3. **配置 HTTPS** + - 使用 Let's Encrypt 免费证书 + - 配置自动续期 + +4. **日志管理** + - 配置日志轮转 + - 使用日志收集工具(如 ELK) + +5. **监控** + - 配置健康检查 + - 使用监控工具(如 Prometheus) + +## 文件大小优化 + +编译后的文件大小通常在 15-25MB 左右。如需进一步优化: + +1. 使用 UPX 压缩(可选): + ```bash + upx --best software-download-center.exe + ``` + +2. 使用静态链接(减小依赖): + ```bash + CGO_ENABLED=0 go build -ldflags="-s -w" -o software-download-center . + ``` + +## 常见问题 + +### 编译失败 + +- 确保 Go 版本 >= 1.21 +- 运行 `go mod download` 下载依赖 +- 检查网络连接(需要访问 Go 模块仓库) + +### 运行时错误 + +- 确保 `public` 和 `views` 目录存在 +- 检查文件权限 +- 查看日志输出 + +### 端口被占用 + +- 修改 `PORT` 环境变量 +- 或修改代码中的默认端口 + +## 版本信息 + +编译时可以通过 `-ldflags` 注入版本信息: + +```bash +go build -ldflags="-X main.Version=1.0.0 -X main.BuildTime=$(date +%Y-%m-%d)" -o software-download-center . +``` + +然后在代码中定义: + +```go +var Version = "dev" +var BuildTime = "unknown" +``` diff --git a/server/update/CGO_FIX.md b/server/update/CGO_FIX.md new file mode 100644 index 0000000..85d5e4a --- /dev/null +++ b/server/update/CGO_FIX.md @@ -0,0 +1,173 @@ +# CGO 问题解决方案 + +## 问题说明 + +如果遇到以下错误: +``` +SQLite 连接失败: Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work +``` + +这是因为 SQLite 驱动 (`go-sqlite3`) 需要 CGO 支持,但当前二进制文件是在禁用 CGO 的情况下编译的。 + +## 解决方案 + +### 方案 1: 启用 CGO 重新编译(推荐用于 SQLite) + +#### Windows + +1. **安装 GCC 编译器**(如果还没有): + - 下载并安装 [TDM-GCC](https://jmeubank.github.io/tdm-gcc/) 或 [MinGW-w64](https://www.mingw-w64.org/) + - 确保 GCC 在系统 PATH 中 + +2. **验证 CGO 支持**: + ```powershell + go env CGO_ENABLED + ``` + 应该显示 `1`(如果显示 `0`,需要设置环境变量) + +3. **启用 CGO 并重新编译**: + ```powershell + $env:CGO_ENABLED="1" + go build -o software-download-center.exe . + ``` + +#### Linux/macOS + +1. **安装 GCC**(如果还没有): + ```bash + # Ubuntu/Debian + sudo apt-get install build-essential + + # macOS + xcode-select --install + ``` + +2. **启用 CGO 并重新编译**: + ```bash + export CGO_ENABLED=1 + go build -o software-download-center . + ``` + +### 方案 2: 使用 MySQL 数据库(无需 CGO) + +如果不想处理 CGO 问题,可以直接使用 MySQL: + +#### Windows (PowerShell) + +```powershell +# 设置 MySQL 环境变量 +$env:DB_TYPE="mysql" +$env:DB_HOST="localhost" +$env:DB_PORT="3306" +$env:DB_USER="root" +$env:DB_PASSWORD="your_password" +$env:DB_NAME="software_download_center" + +# 运行程序 +go run main.go +``` + +#### Linux/macOS + +```bash +# 设置 MySQL 环境变量 +export DB_TYPE=mysql +export DB_HOST=localhost +export DB_PORT=3306 +export DB_USER=root +export DB_PASSWORD=your_password +export DB_NAME=software_download_center + +# 运行程序 +go run main.go +``` + +#### 使用编译后的程序 + +```powershell +# Windows +$env:DB_TYPE="mysql" +$env:DB_HOST="localhost" +$env:DB_PORT="3306" +$env:DB_USER="root" +$env:DB_PASSWORD="your_password" +$env:DB_NAME="software_download_center" +.\software-download-center.exe +``` + +```bash +# Linux/macOS +export DB_TYPE=mysql +export DB_HOST=localhost +export DB_PORT=3306 +export DB_USER=root +export DB_PASSWORD=your_password +export DB_NAME=software_download_center +./software-download-center +``` + +## 快速测试 MySQL 连接 + +在运行程序之前,确保 MySQL 服务正在运行,并且数据库已创建: + +```sql +-- 连接到 MySQL +mysql -u root -p + +-- 创建数据库 +CREATE DATABASE software_download_center CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 退出 +EXIT; +``` + +## 永久解决方案 + +### 选项 A: 始终启用 CGO(用于 SQLite) + +在编译脚本中设置 `CGO_ENABLED=1`: + +**Windows (build.bat)**: +```batch +@echo off +set CGO_ENABLED=1 +go build -o software-download-center.exe . +``` + +**Linux/macOS (build.sh)**: +```bash +#!/bin/bash +export CGO_ENABLED=1 +go build -o software-download-center . +``` + +### 选项 B: 默认使用 MySQL + +在 `main.go` 或环境配置中设置默认数据库类型为 MySQL。 + +## 检查当前 CGO 状态 + +```bash +go env CGO_ENABLED +``` + +- `1` = CGO 已启用(可以使用 SQLite) +- `0` = CGO 已禁用(需要使用 MySQL) + +## 常见问题 + +### Q: 为什么需要 CGO? +A: SQLite 的 Go 驱动 (`go-sqlite3`) 是对 C 库的包装,需要 CGO 来调用 C 代码。 + +### Q: 可以完全避免 CGO 吗? +A: 可以,使用 MySQL 或其他纯 Go 实现的数据库驱动(如 `modernc.org/sqlite`,但功能可能有限)。 + +### Q: MySQL 需要 CGO 吗? +A: 不需要,MySQL 驱动 (`go-sql-driver/mysql`) 是纯 Go 实现的。 + +## 推荐方案 + +- **开发环境**: 使用 SQLite(需要启用 CGO) +- **生产环境**: + - 如果已有 MySQL 服务器,使用 MySQL(无需 CGO) + - 如果希望简单部署,使用 SQLite + CGO diff --git a/server/update/QUICKSTART.md b/server/update/QUICKSTART.md new file mode 100644 index 0000000..f070a43 --- /dev/null +++ b/server/update/QUICKSTART.md @@ -0,0 +1,362 @@ +# 快速开始指南 + +这是一个详细的、一步一步的安装和运行指南。 + +## 第一步:检查 Go 环境 + +打开终端(Windows: PowerShell 或 CMD,Linux/macOS: Terminal),运行: + +```bash +go version +``` + +**预期输出:** +``` +go version go1.21.0 windows/amd64 +``` + +**如果显示错误:** +- 请先安装 Go:https://golang.org/dl/ +- 安装后重启终端 + +--- + +## 第二步:进入项目目录 + +```bash +# Windows (PowerShell 或 CMD) +cd D:\Desktop\update\go + +# Linux/macOS +cd /path/to/update/go +``` + +**验证:** 运行 `dir` (Windows) 或 `ls` (Linux/macOS) 应该能看到 `main.go` 和 `go.mod` 文件。 + +--- + +## 第三步:下载依赖 + +```bash +go mod download +``` + +**预期输出:** +``` +go: downloading github.com/gin-gonic/gin v1.9.1 +go: downloading github.com/golang-jwt/jwt/v5 v5.2.0 +go: downloading gorm.io/gorm v1.25.5 +go: downloading gorm.io/driver/sqlite v1.5.4 +go: downloading golang.org/x/crypto v0.17.0 +... +``` + +**如果下载失败:** +```bash +# 设置 Go 代理(中国大陆用户) +go env -w GOPROXY=https://goproxy.cn,direct + +# 然后重新运行 +go mod download +``` + +--- + +## 第四步:整理依赖(生成 go.sum) + +```bash +go mod tidy +``` + +**预期输出:** +``` +go: downloading github.com/mattn/go-sqlite3 v1.14.17 +go: downloading github.com/jinzhu/now v1.1.5 +go: downloading github.com/jinzhu/inflection v1.0.0 +... +``` + +这个命令会: +- ✅ 下载所有缺失的依赖 +- ✅ 移除未使用的依赖 +- ✅ 生成/更新 `go.sum` 文件 + +--- + +## 第五步:验证依赖 + +```bash +go mod verify +``` + +**预期输出:** +``` +all modules verified +``` + +如果显示错误,请重新运行 `go mod tidy`。 + +--- + +## 第六步:测试编译 + +```bash +# Windows +go build -o software-download-center.exe . + +# Linux/macOS +go build -o software-download-center . +``` + +**预期结果:** +- ✅ 无错误信息 +- ✅ 生成可执行文件(`software-download-center.exe` 或 `software-download-center`) + +**如果编译失败:** +- 检查错误信息 +- 确保所有依赖都已下载(重新运行 `go mod tidy`) + +--- + +## 第七步:运行项目 + +### 方法 1: 直接运行(开发模式) + +```bash +# Windows +go run main.go + +# Linux/macOS +go run main.go +``` + +### 方法 2: 使用编译后的文件 + +```bash +# Windows +.\software-download-center.exe + +# Linux/macOS +./software-download-center +``` + +**预期输出:** +``` +============================================= +✅ 数据库初始化成功 +✅ 配置缓存初始化成功 +============================================= +📋 开始注册路由... +✅ 路由注册成功 [GET ] / (类型: view) +✅ 路由注册成功 [GET ] /tool-status.json (类型: json) +... +📋 路由注册完成! + +============================================= +✅ 服务器启动成功 +📡 访问地址: http://localhost:3355 +🌍 当前环境: production +🔄 兼容旧版访问:支持 /tool-status.json /update-info.json /media-types.json +============================================= +``` + +--- + +## 第八步:访问应用 + +1. **打开浏览器** +2. **访问主页**:http://localhost:3355 +3. **访问后台管理**:http://localhost:3355/admin + +--- + +## 常见问题解决 + +### 问题 1: `missing go.sum entry` + +**错误信息:** +``` +missing go.sum entry for module providing package github.com/gin-gonic/gin +``` + +**解决方法:** +```bash +go mod tidy +``` + +--- + +### 问题 2: 依赖下载失败 + +**错误信息:** +``` +go: github.com/gin-gonic/gin@v1.9.1: Get "https://proxy.golang.org/...": dial tcp: lookup proxy.golang.org: no such host +``` + +**解决方法:** +```bash +# 设置 Go 代理 +go env -w GOPROXY=https://goproxy.cn,direct + +# 重新下载 +go mod download +go mod tidy +``` + +--- + +### 问题 3: 编译错误 + +**错误信息:** +``` +# software-download-center/utils +utils\route-utils.go:51:29: invalid operation +``` + +**解决方法:** +```bash +# 清理并重新编译 +go clean +go mod tidy +go build -o software-download-center.exe . +``` + +--- + +### 问题 4: 端口被占用 + +**错误信息:** +``` +listen tcp :3355: bind: address already in use +``` + +**解决方法:** +```bash +# Windows (PowerShell) +$env:PORT="8080"; go run main.go + +# Windows (CMD) +set PORT=8080 && go run main.go + +# Linux/macOS +PORT=8080 go run main.go +``` + +--- + +### 问题 5: 数据库初始化失败 + +**错误信息:** +``` +数据库初始化失败: open data/app.db: The system cannot find the path specified +``` + +**解决方法:** +```bash +# 手动创建 data 目录 +# Windows +mkdir data + +# Linux/macOS +mkdir -p data + +# 然后重新运行 +go run main.go +``` + +--- + +## 完整命令清单(复制粘贴) + +### Windows (PowerShell) + +```powershell +# 1. 检查 Go 版本 +go version + +# 2. 进入项目目录 +cd D:\Desktop\update\go + +# 3. 下载依赖 +go mod download + +# 4. 整理依赖 +go mod tidy + +# 5. 验证依赖 +go mod verify + +# 6. 测试编译 +go build -o software-download-center.exe . + +# 7. 运行项目 +go run main.go +``` + +### Windows (CMD) + +```cmd +REM 1. 检查 Go 版本 +go version + +REM 2. 进入项目目录 +cd D:\Desktop\update\go + +REM 3. 下载依赖 +go mod download + +REM 4. 整理依赖 +go mod tidy + +REM 5. 验证依赖 +go mod verify + +REM 6. 测试编译 +go build -o software-download-center.exe . + +REM 7. 运行项目 +go run main.go +``` + +### Linux/macOS + +```bash +# 1. 检查 Go 版本 +go version + +# 2. 进入项目目录 +cd /path/to/update/go + +# 3. 下载依赖 +go mod download + +# 4. 整理依赖 +go mod tidy + +# 5. 验证依赖 +go mod verify + +# 6. 测试编译 +go build -o software-download-center . + +# 7. 运行项目 +go run main.go +``` + +--- + +## 下一步 + +- 📖 查看 [README.md](./README.md) 了解完整功能 +- 🔧 查看 [ADMIN.md](./ADMIN.md) 了解后台管理 +- 📦 查看 [BUILD.md](./BUILD.md) 了解打包部署 + +--- + +## 需要帮助? + +如果遇到问题: +1. 检查 Go 版本:`go version`(需要 >= 1.21) +2. 检查网络连接(下载依赖需要) +3. 查看错误信息并参考上面的"常见问题解决" +4. 运行 `go mod tidy` 重新整理依赖 diff --git a/server/update/README.md b/server/update/README.md new file mode 100644 index 0000000..28ce4d3 --- /dev/null +++ b/server/update/README.md @@ -0,0 +1,583 @@ +# YMhut更新站 - Go 版本 + +这是 Node.js 版本的完整 Go 实现,使用 Gin 框架构建,提供了更好的性能和更完善的 UI/UX 体验。 + +## 功能特性 + +- ✅ 完整复现 Node.js 版本的所有功能 +- ✅ 产品列表展示(自动从 downloads 目录读取) +- ✅ JSON API 接口(tool-status.json, update-info.json, media-types.json 等) +- ✅ 文件下载功能(支持安全校验) +- ✅ 历史版本查看 +- ✅ 改进的 UI/UX(增强动画、优化响应式设计) +- ✅ 完整的错误处理(404、500 页面) +- ✅ 彩色日志系统 +- ✅ **后台管理系统**(新增) + - 用户注册和登录(第一个用户自动成为管理员) + - 密码强度验证(防止弱密码) + - 路由管理(可视化添加、编辑、删除路由) + - 文件管理(浏览、查看、编辑文件) + - 配置管理(编辑 JSON 配置文件) + - 日志查看(实时查看系统日志) + - 系统信息(查看系统统计) + +## 项目结构 + +``` +go/ +├── main.go # 主入口文件 +├── go.mod # Go 模块依赖 +├── go.sum # 依赖校验文件 +├── config/ +│ └── routes.go # 路由配置 +├── models/ # 数据模型 +│ ├── user.go # 用户模型 +│ └── route.go # 路由模型 +├── database/ # 数据库 +│ └── db.go # 数据库初始化 +├── handlers/ # 请求处理 +│ ├── auth.go # 认证处理 +│ └── admin.go # 后台管理处理 +├── middleware/ # 中间件 +│ └── auth.go # 认证中间件 +├── utils/ +│ ├── logger.go # 日志工具 +│ ├── route-utils.go # 路由辅助函数 +│ ├── password.go # 密码验证 +│ ├── jwt.go # JWT Token +│ └── config.go # 配置缓存 +├── views/ # HTML 模板 +│ ├── index.html +│ ├── admin.html # 后台管理界面 +│ ├── 404.html +│ └── 500.html +└── public/ # 静态资源 + ├── css/ + ├── img/ + ├── fonts/ + ├── lang/ + ├── downloads/ # 下载文件目录 + └── *.json # JSON 配置文件 +``` + +## 前置要求 + +### 必需 + +- **Go 1.21 或更高版本** + - 下载地址:https://golang.org/dl/ + - 安装后验证:`go version` + +### 可选(用于编译) + +- **Git**(用于版本控制) +- **CGO**(SQLite 需要,Windows 可能需要安装 GCC) + +## 快速开始 + +**新手推荐**:查看 [QUICKSTART.md](./QUICKSTART.md) 获取详细的、一步一步的安装指南。 + +## 完整安装步骤 + +### 步骤 1: 检查 Go 环境 + +打开终端(Windows: PowerShell 或 CMD,Linux/macOS: Terminal),运行: + +```bash +go version +``` + +**预期输出示例:** +``` +go version go1.21.0 windows/amd64 +``` + +如果显示 "command not found" 或类似错误,请先安装 Go。 + +### 步骤 2: 进入项目目录 + +```bash +# Windows (PowerShell) +cd D:\Desktop\update\go + +# Windows (CMD) +cd D:\Desktop\update\go + +# Linux/macOS +cd /path/to/update/go +``` + +### 步骤 3: 下载依赖 + +```bash +go mod download +``` + +**预期输出:** +``` +go: downloading github.com/gin-gonic/gin v1.9.1 +go: downloading github.com/golang-jwt/jwt/v5 v5.2.0 +go: downloading gorm.io/gorm v1.25.5 +... +``` + +### 步骤 4: 整理依赖(生成 go.sum) + +```bash +go mod tidy +``` + +**预期输出:** +``` +go: downloading github.com/mattn/go-sqlite3 v1.14.17 +go: downloading github.com/jinzhu/now v1.1.5 +... +``` + +这个命令会: +- 下载所有缺失的依赖 +- 移除未使用的依赖 +- 生成/更新 `go.sum` 文件 + +### 步骤 5: 验证依赖 + +```bash +go mod verify +``` + +**预期输出:** +``` +all modules verified +``` + +### 步骤 6: 测试编译 + +```bash +# Windows +go build -o software-download-center.exe . + +# Linux/macOS +go build -o software-download-center . +``` + +**预期输出:** +- 无错误信息 +- 生成可执行文件 + +### 步骤 7: 运行项目 + +```bash +# Windows +go run main.go + +# 或使用编译后的文件 +.\software-download-center.exe + +# Linux/macOS +go run main.go + +# 或使用编译后的文件 +./software-download-center +``` + +**预期输出:** +``` +============================================= +✅ 数据库初始化成功 +✅ 配置缓存初始化成功 +============================================= +📋 开始注册路由... +✅ 路由注册成功 [GET ] / (类型: view) +... +📋 路由注册完成! + +============================================= +✅ 服务器启动成功 +📡 访问地址: http://localhost:3355 +🌍 当前环境: production +🔄 兼容旧版访问:支持 /tool-status.json /update-info.json /media-types.json +============================================= +``` + +### 步骤 8: 访问应用 + +1. **主页**:http://localhost:3355 +2. **后台管理**:http://localhost:3355/admin + +## 开发模式运行 + +### 启用调试模式 + +```bash +# Windows (PowerShell) +$env:GIN_MODE="debug"; go run main.go + +# Windows (CMD) +set GIN_MODE=debug && go run main.go + +# Linux/macOS +GIN_MODE=debug go run main.go +``` + +### 自定义端口 + +```bash +# Windows (PowerShell) +$env:PORT="8080"; go run main.go + +# Windows (CMD) +set PORT=8080 && go run main.go + +# Linux/macOS +PORT=8080 go run main.go +``` + +## 编译打包 + +### 方法 1: 使用打包脚本(推荐) + +#### Windows + +```bash +# 运行批处理脚本 +build.bat +``` + +#### Linux/macOS + +```bash +# 添加执行权限 +chmod +x build.sh + +# 运行脚本 +./build.sh +``` + +编译后的文件位于 `build/output/` 目录。 + +### 方法 2: 手动编译 + +#### 编译当前平台版本 + +```bash +# Windows +go build -ldflags="-s -w" -o software-download-center.exe . + +# Linux +go build -ldflags="-s -w" -o software-download-center . + +# macOS +go build -ldflags="-s -w" -o software-download-center . +``` + +#### 交叉编译(在其他平台编译) + +```bash +# 在 Windows 上编译 Linux 版本 +set GOOS=linux +set GOARCH=amd64 +go build -ldflags="-s -w" -o software-download-center . + +# 在 Linux/macOS 上编译 Windows 版本 +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o software-download-center.exe . + +# 在 Linux/macOS 上编译 macOS 版本(Apple Silicon) +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o software-download-center . +``` + +**编译参数说明:** +- `-ldflags="-s -w"`:减小可执行文件大小 + - `-s`:去除符号表 + - `-w`:去除调试信息 + +### 方法 3: 静态编译(无 CGO 依赖) + +```bash +# Windows +set CGO_ENABLED=0 +set GOOS=windows +set GOARCH=amd64 +go build -ldflags="-s -w" -o software-download-center.exe . + +# Linux +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o software-download-center . + +# macOS +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o software-download-center . +``` + +**注意:** 静态编译时 SQLite 可能无法使用,需要 CGO 支持。 + +## 配置说明 + +### 环境变量 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `PORT` | 服务器端口 | `3355` | +| `GIN_MODE` | Gin 运行模式 | `release` | +| `CGO_ENABLED` | 是否启用 CGO | `1` | + +### 文件命名规则 + +下载文件需要遵循以下命名格式才能被正确识别: + +``` +产品名称 Setup 版本号.扩展名 +``` + +**示例:** +- `YmhutBox Setup 1.4.21.exe` +- `弈鸣小筑 Setup 2.0.0.zip` + +**支持的扩展名:** `.exe`, `.zip`, `.pkg`, `.dmg`, `.msi` + +## 后台管理 + +### 访问地址 + +http://localhost:3355/admin + +### 首次使用 + +1. 访问后台管理页面 +2. 点击"注册"标签 +3. 填写注册信息(第一个注册的用户自动成为管理员) +4. 登录后即可使用所有管理功能 + +### 密码要求 + +- 至少 8 个字符 +- 包含至少一个大写字母 +- 包含至少一个小写字母 +- 包含至少一个数字 +- 包含至少一个特殊字符 +- 不能是常见弱密码 + +详细使用说明请查看 [ADMIN.md](./ADMIN.md)。 + +## UI/UX 改进 + +相比 Node.js 版本,Go 版本在 UI/UX 方面做了以下改进: + +1. **增强的动画效果** + - 更流畅的卡片入场动画 + - 改进的悬停效果(轻微上浮) + - 优化的按钮交互反馈 + +2. **改进的滚动条** + - 更宽的滚动条(10px) + - 圆角设计 + - 平滑的过渡效果 + +3. **无障碍改进** + - 支持 `prefers-reduced-motion` 媒体查询 + - 增强的焦点可见性 + - 更好的键盘导航支持 + +4. **性能优化** + - Go 的高性能并发处理 + - 更快的响应时间 + - 更低的内存占用 + +## API 接口 + +### JSON 接口 + +- `GET /tool-status.json` - 工具状态配置 +- `GET /update-info.json` - 更新信息 +- `GET /media-types.json` - 媒体类型配置 +- `GET /plugins.json` - 插件配置 + +所有接口都支持不带 `.json` 后缀的访问方式(向后兼容)。 + +### 文件下载 + +- `GET /downloads/:filename` - 下载指定文件 + +### 静态资源 + +- `/css/*` - CSS 样式文件 +- `/img/*` - 图片资源 +- `/fonts/*` - 字体文件 +- `/lang/*` - 语言文件 + +### 后台管理 API + +详细 API 文档请查看 [ADMIN.md](./ADMIN.md)。 + +## 常见问题 + +### 1. 编译错误:missing go.sum entry + +**解决方法:** +```bash +go mod tidy +``` + +### 2. 运行时错误:数据库初始化失败 + +**可能原因:** +- `data` 目录权限不足 +- 磁盘空间不足 + +**解决方法:** +- 检查目录权限 +- 确保有足够的磁盘空间 +- 手动创建 `data` 目录 + +### 3. 端口被占用 + +**解决方法:** +```bash +# 使用其他端口 +PORT=8080 go run main.go +``` + +### 4. SQLite 相关错误(CGO 问题) + +**错误信息:** +``` +SQLite 连接失败: Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work +``` + +**可能原因:** +- 缺少 CGO 支持 +- 缺少 GCC 编译器 +- 编译时禁用了 CGO + +**解决方法:** + +**方案 1: 启用 CGO 重新编译** +```bash +# Windows (PowerShell) +$env:CGO_ENABLED="1" +go build -o software-download-center.exe . + +# Linux/macOS +export CGO_ENABLED=1 +go build -o software-download-center . +``` + +**方案 2: 使用 MySQL(无需 CGO)** +```bash +# Windows (PowerShell) +$env:DB_TYPE="mysql" +$env:DB_HOST="localhost" +$env:DB_USER="root" +$env:DB_PASSWORD="password" +$env:DB_NAME="software_download_center" +go run main.go + +# Linux/macOS +export DB_TYPE=mysql +export DB_HOST=localhost +export DB_USER=root +export DB_PASSWORD=password +export DB_NAME=software_download_center +go run main.go +``` + +详细说明请查看 [CGO_FIX.md](./CGO_FIX.md)。 + +### 5. 依赖下载失败 + +**解决方法:** +```bash +# 设置 Go 代理(中国大陆) +go env -w GOPROXY=https://goproxy.cn,direct + +# 或使用官方代理 +go env -w GOPROXY=https://proxy.golang.org,direct +``` + +## 开发说明 + +### 添加新路由 + +在 `config/routes.go` 中的 `RegisterRoutes` 函数中添加新路由。 + +### 修改模板 + +模板文件位于 `views/` 目录,使用 Go 的 `html/template` 语法。 + +### 添加新的工具函数 + +在 `utils/` 目录下创建新的工具文件。 + +### 数据库迁移 + +数据库使用 GORM 自动迁移,启动时会自动创建表结构。 + +## 部署说明 + +### 生产环境部署 + +1. **编译可执行文件** + ```bash + go build -ldflags="-s -w" -o software-download-center . + ``` + +2. **复制必要文件** + ```bash + # 复制静态资源 + cp -r public output/ + cp -r views output/ + ``` + +3. **运行服务** + ```bash + PORT=3355 ./software-download-center + ``` + +### 使用进程管理器 + +详细部署说明请查看 [BUILD.md](./BUILD.md)。 + +## 与 Node.js 版本的差异 + +1. **性能**: Go 版本具有更好的并发性能和更低的内存占用 +2. **部署**: Go 版本编译为单个可执行文件,部署更简单 +3. **UI**: Go 版本包含了一些 UI/UX 改进 +4. **功能**: 功能完全一致,100% 兼容 +5. **后台管理**: Go 版本新增了完整的后台管理系统 + +## 快速开始(完整流程) + +```bash +# 1. 检查 Go 版本 +go version + +# 2. 进入项目目录 +cd go + +# 3. 下载依赖 +go mod download + +# 4. 整理依赖 +go mod tidy + +# 5. 验证依赖 +go mod verify + +# 6. 运行项目 +go run main.go + +# 7. 访问应用 +# 浏览器打开: http://localhost:3355 +``` + +## 许可证 + +MIT License + +## 作者 + +YMhut + +## 相关文档 + +- [QUICKSTART.md](./QUICKSTART.md) - 快速开始指南(推荐新手) +- [ADMIN.md](./ADMIN.md) - 后台管理系统使用说明 +- [BUILD.md](./BUILD.md) - 打包编译详细说明 +- [CGO_FIX.md](./CGO_FIX.md) - CGO 问题解决方案(SQLite 相关) \ No newline at end of file diff --git a/server/update/admin-web/index.html b/server/update/admin-web/index.html new file mode 100644 index 0000000..c8960a0 --- /dev/null +++ b/server/update/admin-web/index.html @@ -0,0 +1,12 @@ + + + + + + YMhut Update Admin + + +
+ + + diff --git a/server/update/admin-web/package-lock.json b/server/update/admin-web/package-lock.json new file mode 100644 index 0000000..cda87dd --- /dev/null +++ b/server/update/admin-web/package-lock.json @@ -0,0 +1,2337 @@ +{ + "name": "ymhut-update-admin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ymhut-update-admin", + "version": "1.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.0", + "@vitejs/plugin-react": "^5.0.0", + "daisyui": "^5.0.0", + "lucide-react": "^0.468.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.9.0", + "vite": "^7.0.0" + }, + "devDependencies": {} + }, + "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==", + "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==", + "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==", + "license": "MIT", + "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==", + "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-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==", + "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-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "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==", + "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==", + "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-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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "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-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "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-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "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/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==", + "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==", + "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/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/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==", + "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/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==", + "license": "MIT" + }, + "node_modules/daisyui": { + "version": "5.5.23", + "resolved": "https://registry.npmmirror.com/daisyui/-/daisyui-5.5.23.tgz", + "integrity": "sha512-xuheNUSL4T6ZVtWXoioqcNkjoyGX85QTDz4HTw2aBPfqk4fuMjax5HDo8qCmpV6M1YN8bGvfx5BpYCoDeRlt+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "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/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.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/react": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "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.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "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": "7.3.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/server/update/admin-web/package.json b/server/update/admin-web/package.json new file mode 100644 index 0000000..d27d868 --- /dev/null +++ b/server/update/admin-web/package.json @@ -0,0 +1,23 @@ +{ + "name": "ymhut-update-admin", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.0", + "@vitejs/plugin-react": "^5.0.0", + "daisyui": "^5.0.0", + "lucide-react": "^0.468.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.9.0", + "vite": "^7.0.0" + }, + "devDependencies": {} +} diff --git a/server/update/admin-web/src/main.tsx b/server/update/admin-web/src/main.tsx new file mode 100644 index 0000000..f2828e8 --- /dev/null +++ b/server/update/admin-web/src/main.tsx @@ -0,0 +1,471 @@ +import { + AlertTriangle, + Boxes, + CheckCircle2, + Download, + FileJson, + Lock, + LogOut, + RefreshCw, + Server, + Shield, + UserRound +} from "lucide-react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import "./styles.css"; + +type ApiResult = T & { + ok?: boolean; + error?: string; + message?: string; +}; + +type User = { + id: number; + username: string; + email?: string; + is_admin: boolean; + is_active: boolean; +}; + +type ReleaseFile = { + name: string; + size: number; + size_text: string; + mod_time: string; + url: string; + kind: string; + sha256?: string; +}; + +type Manifest = { + latestVersion?: string; + channel?: string; + createdAt?: string; + published_at?: string; + latest?: { + version?: string; + channel?: string; + published_at?: string; + fullInstaller?: ReleaseSummary; + msix?: ReleaseSummary; + }; + fullInstaller?: ReleaseSummary; + msix?: ReleaseSummary; + messages?: Record; +}; + +type ReleaseSummary = { + fileName?: string; + url?: string; + sha256?: string; + size?: number; + version?: string; +}; + +type SystemInfo = { + users?: number; + routes?: number; + logs?: number; + version?: string; + server_time?: string; +}; + +type LogEntry = { + time: string; + level: string; + message: string; +}; + +async function api(path: string, init?: RequestInit): Promise> { + const response = await fetch(path, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}) + }, + ...init + }); + + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + if (!response.ok) { + throw Object.assign(new Error(data.message || data.error || response.statusText), { + status: response.status, + data + }); + } + return data; +} + +function App() { + const [user, setUser] = useState(null); + const [checking, setChecking] = useState(true); + const [loginError, setLoginError] = useState(""); + + const refreshSession = useCallback(async () => { + setChecking(true); + try { + const result = await api<{ user: User }>("/api/admin/me"); + setUser(result.user); + } catch { + setUser(null); + } finally { + setChecking(false); + } + }, []); + + useEffect(() => { + void refreshSession(); + }, [refreshSession]); + + async function handleLogin(username: string, password: string) { + setLoginError(""); + try { + const result = await api<{ user: User }>("/api/admin/login", { + method: "POST", + body: JSON.stringify({ username, password }) + }); + setUser(result.user); + } catch (error) { + setLoginError(error instanceof Error ? error.message : "登录失败"); + } + } + + async function handleLogout() { + await api("/api/admin/logout", { method: "POST" }); + setUser(null); + } + + if (checking) { + return ( +
+ +
+ ); + } + + if (!user) { + return ; + } + + return ; +} + +function UnauthorizedView({ + error, + onLogin +}: { + error: string; + onLogin: (username: string, password: string) => Promise; +}) { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + setSubmitting(true); + try { + await onLogin(username, password); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+
+
+ +
+
+

Unauthorized

+

未授权 / 请登录

+

+ 后台 API 仅允许已认证管理员访问。登录成功后可管理完整安装包、MSIX 发布物、清单刷新和会话状态。 +

+
+
+ + 未登录访问后台时只显示此授权边界,不会提前加载后台数据。 +
+
+ +
+
+
+ +
+
+

管理员登录

+

使用已有管理员账号继续

+
+
+ + + {error &&
{error}
} + +
+
+
+
+ ); +} + +function AdminDashboard({ user, onLogout }: { user: User; onLogout: () => Promise }) { + const [manifest, setManifest] = useState(null); + const [files, setFiles] = useState([]); + const [system, setSystem] = useState(null); + const [logs, setLogs] = useState([]); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + const latest = manifest?.latest; + const fullInstaller = latest?.fullInstaller ?? manifest?.fullInstaller; + const msix = latest?.msix ?? manifest?.msix; + const latestVersion = manifest?.latestVersion ?? latest?.version ?? fullInstaller?.version ?? "unknown"; + + const refresh = useCallback(async () => { + setBusy(true); + setError(""); + try { + const [manifestResponse, fileResponse, systemResponse, logResponse] = await Promise.all([ + fetch("/update-info.json", { credentials: "include" }).then((response) => response.json()), + api<{ files: ReleaseFile[] }>("/api/admin/releases/files"), + api("/api/admin/system"), + api<{ logs: LogEntry[] }>("/api/admin/logs?limit=8") + ]); + setManifest(manifestResponse); + setFiles(fileResponse.files ?? []); + setSystem(systemResponse); + setLogs(logResponse.logs ?? []); + } catch (loadError) { + setError(loadError instanceof Error ? loadError.message : "加载失败"); + } finally { + setBusy(false); + } + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + const metrics = useMemo( + () => [ + { label: "最新版本", value: latestVersion, icon: CheckCircle2 }, + { label: "发布文件", value: String(files.length), icon: Download }, + { label: "后台用户", value: String(system?.users ?? 0), icon: Shield }, + { label: "日志数量", value: String(system?.logs ?? 0), icon: Server } + ], + [files.length, latestVersion, system?.logs, system?.users] + ); + + return ( +
+ + +
+
+
+

Admin

+

更新发布后台

+
+
+ + + 前台 + +
+
+ + {error &&
{error}
} + +
+ {metrics.map((metric) => ( +
+ +

{metric.label}

+

{metric.value}

+
+ ))} +
+ +
+
+ +
+ + + + + + + + + + + {files.map((file) => ( + + + + + + + + ))} + {files.length === 0 && ( + + + + )} + +
文件类型大小更新时间 +
{file.name} + {file.kind} + {file.size_text}{file.mod_time} + + 下载 + +
+ 暂无发布文件 +
+
+
+ + +
+ + +
+
+
+ +
+ +
+ + + + +
+
+ + +
+ {logs.map((log) => ( +
+
+ {log.level} + {log.time} +
+

{log.message}

+
+ ))} + {logs.length === 0 &&

暂无日志

} +
+
+
+
+
+
+ ); +} + +function Panel({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { + return ( +
+
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {children} +
+ ); +} + +function SummaryCard({ title, item }: { title: string; item?: ReleaseSummary }) { + return ( +
+

{title}

+

{item?.fileName ?? "未配置"}

+

{item?.sha256 ?? "-"}

+
+ ); +} + +function Info({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +createRoot(document.getElementById("root")!).render(); diff --git a/server/update/admin-web/src/styles.css b/server/update/admin-web/src/styles.css new file mode 100644 index 0000000..efe07f6 --- /dev/null +++ b/server/update/admin-web/src/styles.css @@ -0,0 +1,91 @@ +@import "tailwindcss"; +@plugin "daisyui" { + themes: light --default, dark --prefersdark; +} + +:root { + color-scheme: light; + font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif; + background: #f5f7fb; + color: #172033; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(244, 247, 251, 0.96)), + #f5f7fb; +} + +button, +input, +textarea, +select { + font: inherit; +} + +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 260px minmax(0, 1fr); +} + +.admin-sidebar { + border-right: 1px solid rgba(148, 163, 184, 0.28); + background: rgba(255, 255, 255, 0.78); + backdrop-filter: blur(18px); +} + +.admin-main { + min-width: 0; + padding: 22px; +} + +.surface { + border: 1px solid rgba(148, 163, 184, 0.28); + background: rgba(255, 255, 255, 0.86); + box-shadow: 0 16px 42px rgba(15, 23, 42, 0.07); +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.content-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr); + gap: 14px; +} + +.mono { + font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace; +} + +@media (max-width: 980px) { + .app-shell { + grid-template-columns: 1fr; + } + + .admin-sidebar { + position: sticky; + top: 0; + z-index: 20; + border-right: 0; + border-bottom: 1px solid rgba(148, 163, 184, 0.28); + } + + .metric-grid, + .content-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .admin-main { + padding: 12px; + } +} diff --git a/server/update/admin-web/tsconfig.json b/server/update/admin-web/tsconfig.json new file mode 100644 index 0000000..df11947 --- /dev/null +++ b/server/update/admin-web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/server/update/admin-web/vite.config.ts b/server/update/admin-web/vite.config.ts new file mode 100644 index 0000000..38d6815 --- /dev/null +++ b/server/update/admin-web/vite.config.ts @@ -0,0 +1,12 @@ +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + base: "/admin/", + plugins: [react(), tailwindcss()], + build: { + outDir: "dist", + emptyOutDir: true + } +}); diff --git a/server/update/build.bat b/server/update/build.bat new file mode 100644 index 0000000..7e8eb3e --- /dev/null +++ b/server/update/build.bat @@ -0,0 +1,56 @@ +@echo off +REM Windows 编译脚本 + +echo 开始编译 Go 项目... + +set APP_NAME=software-download-center +set BUILD_DIR=build +set OUTPUT_DIR=%BUILD_DIR%\output + +REM 创建输出目录 +if not exist %OUTPUT_DIR% mkdir %OUTPUT_DIR% + +echo 编译 Windows 版本... +set GOOS=windows +set GOARCH=amd64 +go build -ldflags="-s -w" -o %OUTPUT_DIR%\%APP_NAME%_windows_amd64.exe . + +echo 编译 Linux 版本... +set GOOS=linux +set GOARCH=amd64 +go build -ldflags="-s -w" -o %OUTPUT_DIR%\%APP_NAME%_linux_amd64.exe . + +echo 编译 macOS 版本... +set GOOS=darwin +set GOARCH=amd64 +go build -ldflags="-s -w" -o %OUTPUT_DIR%\%APP_NAME%_darwin_amd64.exe . +set GOARCH=arm64 +go build -ldflags="-s -w" -o %OUTPUT_DIR%\%APP_NAME%_darwin_arm64.exe . + +REM 复制必要文件 +echo 复制必要文件... +xcopy /E /I /Y public %OUTPUT_DIR%\public +xcopy /E /I /Y views %OUTPUT_DIR%\views +copy README.md %OUTPUT_DIR% >nul 2>&1 +copy ADMIN.md %OUTPUT_DIR% >nul 2>&1 + +REM 创建启动脚本 +echo @echo off > %OUTPUT_DIR%\start.bat +echo REM Windows 启动脚本 >> %OUTPUT_DIR%\start.bat +echo. >> %OUTPUT_DIR%\start.bat +echo set PORT=3355 >> %OUTPUT_DIR%\start.bat +echo if not "%%PORT%%"=="" set PORT=%%PORT%% >> %OUTPUT_DIR%\start.bat +echo echo 启动服务器,端口: %%PORT%% >> %OUTPUT_DIR%\start.bat +echo set PORT=%%PORT%% >> %OUTPUT_DIR%\start.bat +echo %APP_NAME%_windows_amd64.exe >> %OUTPUT_DIR%\start.bat + +echo. +echo 编译完成! +echo 输出目录: %OUTPUT_DIR% +echo. +echo 使用方法: +echo Windows: 运行 start.bat 或直接运行 .exe 文件 +echo Linux: 运行 ./start.sh 或直接运行可执行文件 +echo macOS: 运行 ./start.sh 或直接运行可执行文件 + +pause diff --git a/server/update/build.sh b/server/update/build.sh new file mode 100644 index 0000000..79f8bec --- /dev/null +++ b/server/update/build.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# 编译脚本 - 打包成服务端可执行文件 + +echo "开始编译 Go 项目..." + +# 设置编译参数 +APP_NAME="software-download-center" +VERSION=$(date +%Y%m%d_%H%M%S) +BUILD_DIR="build" +OUTPUT_DIR="${BUILD_DIR}/output" + +# 创建输出目录 +mkdir -p ${OUTPUT_DIR} + +# 编译不同平台的可执行文件 +echo "编译 Windows 版本..." +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${OUTPUT_DIR}/${APP_NAME}_windows_amd64.exe . + +echo "编译 Linux 版本..." +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${OUTPUT_DIR}/${APP_NAME}_linux_amd64 . + +echo "编译 macOS 版本..." +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${OUTPUT_DIR}/${APP_NAME}_darwin_amd64 . +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o ${OUTPUT_DIR}/${APP_NAME}_darwin_arm64 . + +# 复制必要文件 +echo "复制必要文件..." +cp -r public ${OUTPUT_DIR}/ 2>/dev/null || true +cp -r views ${OUTPUT_DIR}/ 2>/dev/null || true +cp README.md ${OUTPUT_DIR}/ 2>/dev/null || true +cp ADMIN.md ${OUTPUT_DIR}/ 2>/dev/null || true + +# 创建启动脚本 +cat > ${OUTPUT_DIR}/start.sh << 'EOF' +#!/bin/bash +# Linux/macOS 启动脚本 + +PORT=${PORT:-3355} +echo "启动服务器,端口: $PORT" +export PORT=$PORT +./software-download-center_linux_amd64 +EOF + +cat > ${OUTPUT_DIR}/start.bat << 'EOF' +@echo off +REM Windows 启动脚本 + +set PORT=3355 +if not "%PORT%"=="" set PORT=%PORT% +echo 启动服务器,端口: %PORT% +set PORT=%PORT% +software-download-center_windows_amd64.exe +EOF + +chmod +x ${OUTPUT_DIR}/start.sh +chmod +x ${OUTPUT_DIR}/*.exe 2>/dev/null || true + +echo "" +echo "编译完成!" +echo "输出目录: ${OUTPUT_DIR}" +echo "" +echo "可执行文件列表:" +ls -lh ${OUTPUT_DIR}/*.exe ${OUTPUT_DIR}/*_linux_amd64 ${OUTPUT_DIR}/*_darwin_* 2>/dev/null | awk '{print $9, "(" $5 ")"}' +echo "" +echo "使用方法:" +echo " Windows: 运行 start.bat 或直接运行 .exe 文件" +echo " Linux: 运行 ./start.sh 或直接运行可执行文件" +echo " macOS: 运行 ./start.sh 或直接运行可执行文件" diff --git a/server/update/config/package_manifest_test.go b/server/update/config/package_manifest_test.go new file mode 100644 index 0000000..bba75f7 --- /dev/null +++ b/server/update/config/package_manifest_test.go @@ -0,0 +1,316 @@ +package config + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "software-download-center/utils" + + "github.com/gin-gonic/gin" +) + +func TestUpdateInfoContainsOnlyFullInstallerAndMSIX(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer") + writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix") + writeTestFile(t, downloadsDir, "winui.appinstaller", "appinstaller") + writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light") + writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") + writeTestFile(t, publicDir, "update-info.json", `{ + "baselineVersion": "2.0.6.0", + "minIncrementalVersion": "2.0.6.0", + "lightInstaller": {"fileName":"light.exe"}, + "packages": [{"id":"external-tools"}], + "incrementals": [{"id":"delta"}], + "messages": {"static":"kept"} + }`) + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", "/update-info.json", nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("expected update-info 200, got %d: %s", recorder.Code, recorder.Body.String()) + } + + var body map[string]interface{} + if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + + assertAbsent(t, body, "baselineVersion", "minIncrementalVersion", "lightInstaller", "packages", "incrementals", "requiredForFull", "modules") + if body["latestVersion"] != "2.0.7.0" { + t.Fatalf("expected latestVersion 2.0.7.0, got %#v", body["latestVersion"]) + } + + latest := body["latest"].(map[string]interface{}) + assertAbsent(t, latest, "lightInstaller", "incrementals", "packages") + fullInstaller := latest["fullInstaller"].(map[string]interface{}) + if fullInstaller["fileName"] != "YMhut_Box_WinUI_Setup_2.0.7.0.exe" { + t.Fatalf("unexpected full installer: %#v", fullInstaller) + } + msix := latest["msix"].(map[string]interface{}) + if msix["fileName"] != "YMhut_Box_WinUI_2.0.7.0_x64.msix" { + t.Fatalf("unexpected msix: %#v", msix) + } + appInstaller := latest["appInstaller"].(map[string]interface{}) + if appInstaller["fileName"] != "winui.appinstaller" { + t.Fatalf("unexpected appinstaller: %#v", appInstaller) + } + + text := recorder.Body.String() + for _, forbidden := range []string{"_Light.exe", "YMhut_Box_Update_", "external-tools", "baselineVersion", "incrementals"} { + if strings.Contains(text, forbidden) { + t.Fatalf("update-info leaked removed field/content %q: %s", forbidden, text) + } + } +} + +func TestRemovedDistributionRoutesReturnGone(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + for _, route := range []string{ + "/modules.json", + "/api/modules", + "/package-manifest.json", + "/incremental-manifest.json", + "/api/packages/manifest", + "/api/incrementals", + "/api/incrementals/manifest", + "/api/packages/download/YMhut_Box_Tools_2.0.6.0.zip", + "/api/incrementals/download/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", + "/packages/YMhut_Box_Tools_2.0.6.0.zip", + "/tool-packages/YMhut_Box_Tools_2.0.6.0.zip", + } { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) + if recorder.Code != http.StatusGone { + t.Fatalf("expected %s to return 410, got %d: %s", route, recorder.Code, recorder.Body.String()) + } + } +} + +func TestCanonicalUpdateInfoRoutesServeDirectlyAndLegacyRoutesRedirect(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + for _, route := range []string{"/update-info.json", "/update-info", "/api/update-info"} { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("expected %s to return 200, got %d: %s", route, recorder.Code, recorder.Body.String()) + } + } + + manifestRecorder := httptest.NewRecorder() + router.ServeHTTP(manifestRecorder, httptest.NewRequest("GET", "/manifest.json", nil)) + if manifestRecorder.Code != http.StatusOK { + t.Fatalf("expected /manifest.json compatibility response 200, got %d: %s", manifestRecorder.Code, manifestRecorder.Body.String()) + } + if manifestRecorder.Header().Get("Deprecation") != "true" { + t.Fatalf("expected /manifest.json to include Deprecation header") + } + if manifestRecorder.Header().Get("X-YMhut-Canonical-Manifest") != "/update-info.json" { + t.Fatalf("expected /manifest.json to point at /update-info.json") + } + + for _, route := range []string{"/latest-version.json", "/api/releases/latest", "/latest.json"} { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) + if recorder.Code != http.StatusMovedPermanently { + t.Fatalf("expected %s to redirect with 301, got %d", route, recorder.Code) + } + if location := recorder.Header().Get("Location"); location != "/update-info.json" { + t.Fatalf("expected %s Location /update-info.json, got %q", route, location) + } + } +} + +func TestDownloadsRouteAllowsOnlySingleInstallerArtifact(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer") + writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light") + writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + okRecorder := httptest.NewRecorder() + router.ServeHTTP(okRecorder, httptest.NewRequest("GET", "/downloads/YMhut_Box_WinUI_Setup_2.0.7.0.exe", nil)) + if okRecorder.Code != http.StatusOK { + t.Fatalf("expected full installer download 200, got %d: %s", okRecorder.Code, okRecorder.Body.String()) + } + if okRecorder.Body.String() != "installer" { + t.Fatalf("unexpected installer body: %q", okRecorder.Body.String()) + } + + for _, route := range []string{ + "/downloads/YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", + "/downloads/incremental/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", + "/downloads/..%2Fsecret.exe", + "/downloads/subdir/file.exe", + } { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) + if recorder.Code != http.StatusForbidden { + t.Fatalf("expected %s to be forbidden, got %d: %s", route, recorder.Code, recorder.Body.String()) + } + } +} + +func TestAdminAPIRequiresAuthentication(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", "/api/admin/releases/files", nil)) + if recorder.Code != http.StatusUnauthorized { + t.Fatalf("expected 401 for unauthenticated admin API, got %d: %s", recorder.Code, recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "UNAUTHORIZED") { + t.Fatalf("expected structured unauthorized response, got %s", recorder.Body.String()) + } +} + +func TestAdminPageShowsUnauthorizedShellWithoutAuth(t *testing.T) { + gin.SetMode(gin.TestMode) + rootDir := t.TempDir() + publicDir := filepath.Join(rootDir, "public") + downloadsDir := filepath.Join(publicDir, "downloads") + viewsDir := filepath.Join(rootDir, "views") + mustMkdirAll(t, downloadsDir) + mustMkdirAll(t, viewsDir) + writeTemplateFiles(t, viewsDir) + + router := buildTestRouter(t, rootDir) + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest("GET", "/admin/", nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("expected admin shell 200, got %d: %s", recorder.Code, recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "未授权") { + t.Fatalf("expected admin shell fallback to show unauthorized copy, got %s", recorder.Body.String()) + } +} + +func TestComparePackageFileNamesSupportsNewAndLegacyInstallerNames(t *testing.T) { + if comparePackageFileNames("YMhut_Box_WinUI_Setup_2.0.7.0.exe", "YMhut_Box_Setup_2.0.6.0.exe") <= 0 { + t.Fatal("expected WinUI 2.0.7.0 installer to be newer than legacy 2.0.6.0 installer") + } + if comparePackageFileNames("YMhut_Box_Setup_2.0.7.0.exe", "YMhut_Box_WinUI_Setup_2.0.7.0.exe") == 0 { + t.Fatal("expected stable tie-breaker for same-version installer names") + } +} + +func TestProductsInfoRecognizesMSIXAndSkipsIncrementalSubdirectory(t *testing.T) { + dir := t.TempDir() + writeTestFile(t, dir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix") + writeTestFile(t, filepath.Join(dir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") + + products := utils.GetProductsInfo(dir, utils.NewLogger()) + if len(products) == 0 { + t.Fatalf("expected MSIX package to be detected") + } + for _, releases := range products { + for _, release := range releases { + if release.FileName == "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip" { + t.Fatalf("incremental package from subdirectory should not be listed as product: %#v", products) + } + } + } +} + +func buildTestRouter(t *testing.T, rootDir string) *gin.Engine { + t.Helper() + oldWorkingDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(rootDir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = os.Chdir(oldWorkingDir) + }) + + router := gin.New() + RegisterRoutes(router, utils.NewLogger()) + return router +} + +func writeTemplateFiles(t *testing.T, dir string) { + t.Helper() + writeTestFile(t, dir, "index.html", "{{.pageTitle}}") + writeTestFile(t, dir, "404.html", "{{.title}}") + writeTestFile(t, dir, "500.html", "{{.title}}") +} + +func writeTestFile(t *testing.T, dir string, name string, content string) { + t.Helper() + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil { + t.Fatalf("write %s: %v", name, err) + } +} + +func mustMkdirAll(t *testing.T, dir string) { + t.Helper() + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } +} + +func assertAbsent(t *testing.T, body map[string]interface{}, keys ...string) { + t.Helper() + for _, key := range keys { + if _, ok := body[key]; ok { + t.Fatalf("expected field %s to be absent in %#v", key, body) + } + } +} diff --git a/server/update/config/routes.go b/server/update/config/routes.go new file mode 100644 index 0000000..9d58b61 --- /dev/null +++ b/server/update/config/routes.go @@ -0,0 +1,537 @@ +package config + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "html/template" + "io" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + + "software-download-center/database" + "software-download-center/handlers" + "software-download-center/middleware" + "software-download-center/utils" + + "github.com/gin-gonic/gin" +) + +type ProductMeta struct { + Icon string + Description string + ThemeColor string + Tags []string +} + +var ( + productMeta = map[string]ProductMeta{ + "YMhut Box": { + Icon: ``, + Description: "多功能工具箱,整合系统、网络、图像和日常效率工具。", + ThemeColor: "#166534", + Tags: []string{"桌面工具", "效率", "多平台"}, + }, + "YmhutBox": { + Icon: ``, + Description: "多功能工具箱,整合系统、网络、图像和日常效率工具。", + ThemeColor: "#166534", + Tags: []string{"桌面工具", "效率", "多平台"}, + }, + "弓福小筑": { + Icon: ``, + Description: "轻量入口应用,提供站点访问与定制下载入口。", + ThemeColor: "#b45309", + Tags: []string{"轻量", "入口", "桌面"}, + }, + } + + defaultMeta = ProductMeta{ + Icon: ``, + Description: "自动从 downloads 目录识别出的安装包分组。", + ThemeColor: "#57534e", + Tags: []string{"自动识别", "安装包", "下载"}, + } +) + +func RegisterRoutes(r *gin.Engine, logger *utils.Logger) { + logger.System("\n开始注册路由...\n") + + rootDir, err := os.Getwd() + if err != nil { + logger.Error(fmt.Sprintf("获取工作目录失败: %s", err.Error())) + return + } + + publicDir := filepath.Join(rootDir, "public") + viewsDir := filepath.Join(rootDir, "views") + downloadsDir := filepath.Join(publicDir, "downloads") + + r.SetFuncMap(template.FuncMap{ + "safeHTML": func(s string) template.HTML { + return template.HTML(s) + }, + "marshalJSON": func(v interface{}) string { + data, _ := json.Marshal(v) + return string(data) + }, + "slice": func(slice interface{}, start int, args ...int) interface{} { + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return slice + } + end := v.Len() + if len(args) > 0 { + end = args[0] + } + if start < 0 { + start = 0 + } + if end > v.Len() { + end = v.Len() + } + if start >= end { + return reflect.MakeSlice(v.Type(), 0, 0).Interface() + } + return v.Slice(start, end).Interface() + }, + }) + r.LoadHTMLGlob(filepath.Join(viewsDir, "*.html")) + + r.GET("/", func(c *gin.Context) { + products := utils.GetProductsInfo(downloadsDir, logger) + + errorMessage := "" + if products == nil { + errorMessage = "无法读取 downloads 目录,请检查目录权限和文件配置。" + } else if len(products) == 0 { + errorMessage = "downloads 目录中暂时没有可识别的安装包。" + } + + c.HTML(http.StatusOK, "index.html", gin.H{ + "products": products, + "productMeta": productMeta, + "defaultMeta": defaultMeta, + "pageTitle": "YMhut 下载中心", + "errorMessage": errorMessage, + }) + }) + logger.Info("注册路由成功 [GET] /") + + registerDynamicUpdateInfoRoutes(r, logger, publicDir, downloadsDir) + registerReleaseAPIRoutes(r, logger, publicDir, downloadsDir) + + jsonRoutes := []struct { + path string + file string + cacheControl string + }{ + {"/tool-status.json", "tool-status.json", "public, max-age=600"}, + {"/tool-status", "tool-status.json", "public, max-age=600"}, + {"/media-types.json", "media-types.json", "public, max-age=3600"}, + {"/media-types", "media-types.json", "public, max-age=3600"}, + {"/plugins", "plugins.json", "public, max-age=3600"}, + {"/plugins.json", "plugins.json", "public, max-age=3600"}, + {"/modules", "modules.json", "public, max-age=600"}, + {"/modules.json", "modules.json", "public, max-age=600"}, + } + + for _, route := range jsonRoutes { + filePath := filepath.Join(publicDir, route.file) + fp := filePath + cc := route.cacheControl + path := route.path + fileName := route.file + if utils.FileExists(filePath) { + r.GET(path, func(c *gin.Context) { + if cached, ok := utils.GetCachedConfig(fileName); ok { + c.Header("Content-Type", "application/json; charset=utf-8") + c.Header("Cache-Control", cc) + c.JSON(http.StatusOK, cached) + return + } + + data, err := utils.ReadJSONFile(fp) + if err != nil { + logger.Error(fmt.Sprintf("读取 JSON 文件失败: %s - %s", fp, err.Error())) + c.JSON(http.StatusInternalServerError, gin.H{"error": "文件读取失败"}) + return + } + + utils.SaveConfig(fileName, data) + c.Header("Content-Type", "application/json; charset=utf-8") + c.Header("Cache-Control", cc) + c.JSON(http.StatusOK, data) + }) + logger.Info(fmt.Sprintf("注册路由成功 [GET] %s", path)) + } else { + logger.Warn(fmt.Sprintf("跳过路由,文件不存在: %s", path)) + } + } + + fileRoutes := []struct { + path string + file string + cacheControl string + headers map[string]string + }{ + {"/lang/zh-CN.json", "lang/zh-CN.json", "public, max-age=86400", map[string]string{"Content-Disposition": `attachment; filename="zh-CN.json"`}}, + {"/lang/en-US.json", "lang/en-US.json", "public, max-age=86400", map[string]string{"Content-Disposition": `attachment; filename="en-US.json"`}}, + {"/fonts/MeiGanShouXieTi-2.ttf", "fonts/MeiGanShouXieTi-2.ttf", "public, max-age=86400", map[string]string{"Content-Disposition": `attachment; filename="MeiGanShouXieTi-2.ttf"`}}, + {"/fonts/QianTuBiFengShouXieTi-2.ttf", "fonts/QianTuBiFengShouXieTi-2.ttf", "public, max-age=86400", map[string]string{"Content-Disposition": `attachment; filename="QianTuBiFengShouXieTi-2.ttf"`}}, + {"/fonts/YOzBS-2.otf", "fonts/YOzBS-2.otf", "public, max-age=86400", map[string]string{"Content-Disposition": `attachment; filename="YOzBS-2.otf"`}}, + {"/favicon.ico", "img/favicon.png", "public, max-age=604800", nil}, + } + + for _, route := range fileRoutes { + filePath := filepath.Join(publicDir, route.file) + fp := filePath + cc := route.cacheControl + hdrs := route.headers + path := route.path + if utils.FileExists(filePath) { + r.GET(path, func(c *gin.Context) { + mimeType := utils.GetMimeType(fp) + c.Header("Content-Type", mimeType) + c.Header("Cache-Control", cc) + if hdrs != nil { + for k, v := range hdrs { + c.Header(k, v) + } + } + c.File(fp) + }) + logger.Info(fmt.Sprintf("注册路由成功 [GET] %s", path)) + } else { + logger.Warn(fmt.Sprintf("跳过路由,文件不存在: %s", path)) + } + } + + r.Static("/css", filepath.Join(publicDir, "css")) + r.Static("/img", filepath.Join(publicDir, "img")) + + r.GET("/downloads/:filename", func(c *gin.Context) { + filename := c.Param("filename") + if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") { + logger.Warn(fmt.Sprintf("拒绝下载请求: %s (IP: %s)", filename, c.ClientIP())) + c.JSON(http.StatusForbidden, gin.H{"error": "禁止访问"}) + return + } + + filePath := filepath.Join(downloadsDir, filename) + resolvedPath, err := filepath.Abs(filePath) + if err != nil { + logger.Error(fmt.Sprintf("解析文件路径失败: %s", err.Error())) + c.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误"}) + return + } + + normalizedDir, _ := filepath.Abs(downloadsDir) + if !strings.HasPrefix(resolvedPath, normalizedDir) { + logger.Warn(fmt.Sprintf("下载路径越界: %s (IP: %s)", filename, c.ClientIP())) + c.JSON(http.StatusForbidden, gin.H{"error": "禁止访问"}) + return + } + + if !utils.FileExists(filePath) { + logger.Error(fmt.Sprintf("下载失败,文件不存在: %s", filename)) + c.HTML(http.StatusNotFound, "404.html", gin.H{ + "title": "文件未找到", + "path": c.Request.URL.String(), + }) + return + } + + logger.Info(fmt.Sprintf("下载成功: %s (IP: %s)", filename, c.ClientIP())) + c.File(filePath) + }) + + r.GET("/admin", middleware.AuthMiddleware(), func(c *gin.Context) { + c.HTML(http.StatusOK, "admin.html", gin.H{"title": "后台管理"}) + }) + + r.GET("/admin/login", func(c *gin.Context) { + if token, _ := c.Cookie("token"); token != "" { + c.Redirect(http.StatusFound, "/admin") + return + } + c.HTML(http.StatusOK, "login.html", gin.H{"title": "登录"}) + }) + + r.GET("/admin/register", func(c *gin.Context) { + if token, _ := c.Cookie("token"); token != "" { + c.Redirect(http.StatusFound, "/admin") + return + } + c.HTML(http.StatusOK, "register.html", gin.H{"title": "注册"}) + }) + + r.GET("/admin/settings", middleware.AuthMiddleware(), middleware.AdminMiddleware(), func(c *gin.Context) { + c.HTML(http.StatusOK, "settings.html", gin.H{"title": "系统设置"}) + }) + + r.GET("/admin/install", func(c *gin.Context) { + if database.IsDBInitialized() { + c.Redirect(http.StatusFound, "/admin/login") + return + } + c.HTML(http.StatusOK, "install.html", gin.H{"title": "数据库配置"}) + }) + + r.GET("/admin/install/status", handlers.CheckInstallStatus) + r.POST("/admin/install/database", handlers.InstallDatabase) + + adminRoutes := r.Group("/admin") + { + adminRoutes.POST("/register", handlers.Register) + adminRoutes.POST("/login", handlers.Login) + adminRoutes.POST("/logout", handlers.Logout) + adminRoutes.GET("/me", middleware.AuthMiddleware(), handlers.GetCurrentUser) + + adminAPI := adminRoutes.Group("/api") + adminAPI.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware()) + { + adminAPI.GET("/logs", handlers.GetLogs) + adminAPI.GET("/routes", handlers.GetRoutes) + adminAPI.POST("/routes", handlers.CreateRoute) + adminAPI.PUT("/routes/:id", handlers.UpdateRoute) + adminAPI.DELETE("/routes/:id", handlers.DeleteRoute) + + adminAPI.GET("/files", handlers.GetFiles) + adminAPI.GET("/file", handlers.ReadFile) + adminAPI.POST("/file", handlers.SaveFile) + + adminAPI.PUT("/config", handlers.UpdateJSONConfig) + adminAPI.GET("/system", handlers.GetSystemInfo) + + adminAPI.GET("/database", handlers.GetDatabaseInfo) + adminAPI.GET("/database/config", handlers.GetDatabaseConfig) + adminAPI.POST("/database/config", handlers.UpdateDatabaseConfig) + adminAPI.POST("/database/convert", handlers.ConvertDatabase) + adminAPI.POST("/database/password", handlers.UpdateDatabasePassword) + + adminAPI.POST("/reload", handlers.ReloadRoutes) + } + } + + r.NoRoute(func(c *gin.Context) { + fullURL := c.Request.URL.String() + logger.Warn(fmt.Sprintf("404 Not Found - %s", fullURL)) + c.HTML(http.StatusNotFound, "404.html", gin.H{ + "title": "页面未找到", + "path": fullURL, + }) + }) + + r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + logger.Error(fmt.Sprintf("500 Server Error - 路径: %s - 错误: %v", c.Request.URL.Path, recovered)) + c.HTML(http.StatusInternalServerError, "500.html", gin.H{ + "title": "服务器错误", + "message": "服务器内部错误,请稍后重试。", + }) + c.Abort() + })) + + logger.System("路由注册完成。\n") +} + +func registerDynamicUpdateInfoRoutes( + r *gin.Engine, + logger *utils.Logger, + publicDir string, + downloadsDir string, +) { + updateInfoPath := filepath.Join(publicDir, "update-info.json") + for _, path := range []string{"/update-info.json", "/update-info"} { + routePath := path + r.GET(routePath, func(c *gin.Context) { + payload := map[string]interface{}{} + if utils.FileExists(updateInfoPath) { + if data, err := utils.ReadJSONFile(updateInfoPath); err == nil { + payload = data + } + } + + products := utils.GetProductsInfo(downloadsDir, logger) + productName, latest := utils.GetLatestProductRelease(products, "YMhut Box") + if latest != nil { + baseURL := requestBaseURL(c) + payload["app_version"] = latest.Version + payload["download_url"] = baseURL + latest.DownloadPath + payload["download_mirrors"] = []map[string]interface{}{ + { + "id": "primary", + "name": "官方直连", + "url": baseURL + latest.DownloadPath, + "type": "direct", + "sha256": sha256File(filepath.Join(downloadsDir, latest.FileName)), + "enabled": true, + }, + } + payload["detected_product"] = productName + payload["detected_packages"] = products + } + + c.Header("Content-Type", "application/json; charset=utf-8") + c.Header("Cache-Control", "public, max-age=300") + c.JSON(http.StatusOK, payload) + }) + logger.Info(fmt.Sprintf("注册动态更新信息路由 [GET] %s", routePath)) + } +} + +func requestBaseURL(c *gin.Context) string { + scheme := c.GetHeader("X-Forwarded-Proto") + if scheme == "" { + if c.Request.TLS != nil { + scheme = "https" + } else { + scheme = "http" + } + } + return scheme + "://" + c.Request.Host +} + +func registerReleaseAPIRoutes( + r *gin.Engine, + logger *utils.Logger, + publicDir string, + downloadsDir string, +) { + r.GET("/api/releases", func(c *gin.Context) { + c.JSON(http.StatusOK, buildReleaseManifest(c, logger, publicDir, downloadsDir)) + }) + r.GET("/api/modules", func(c *gin.Context) { + manifest := buildReleaseManifest(c, logger, publicDir, downloadsDir) + c.JSON(http.StatusOK, gin.H{ + "manifest_version": manifest["manifest_version"], + "modules": manifest["modules"], + }) + }) + r.GET("/api/update-info", func(c *gin.Context) { + c.JSON(http.StatusOK, buildReleaseManifest(c, logger, publicDir, downloadsDir)) + }) +} + +func buildReleaseManifest( + c *gin.Context, + logger *utils.Logger, + publicDir string, + downloadsDir string, +) map[string]interface{} { + payload := map[string]interface{}{} + updateInfoPath := filepath.Join(publicDir, "update-info.json") + if utils.FileExists(updateInfoPath) { + if data, err := utils.ReadJSONFile(updateInfoPath); err == nil { + payload = data + } + } + + baseURL := requestBaseURL(c) + products := utils.GetProductsInfo(downloadsDir, logger) + packages := make([]map[string]interface{}, 0) + for productName, releases := range products { + for _, release := range releases { + filePath := filepath.Join(downloadsDir, release.FileName) + platform, arch := detectPackagePlatform(release.FileName, release.Extension) + packages = append(packages, map[string]interface{}{ + "id": packageID(productName, platform, arch, release.Version), + "name": productName, + "version": release.Version, + "platform": platform, + "arch": arch, + "url": baseURL + release.DownloadPath, + "sha256": sha256File(filePath), + "size": release.SizeBytes, + "required": utils.IsSameProduct(productName, "YMhut Box"), + "enabled": true, + "changelog": map[string]string{}, + }) + } + } + + modulesPath := filepath.Join(publicDir, "modules.json") + modules := []interface{}{} + if utils.FileExists(modulesPath) { + if data, err := utils.ReadJSONFile(modulesPath); err == nil { + if raw, ok := data["modules"].([]interface{}); ok { + modules = raw + } + } + } + + payload["manifest_version"] = 2 + payload["packages"] = packages + payload["modules"] = modules + payload["assets"] = []interface{}{} + + productName, latest := utils.GetLatestProductRelease(products, "YMhut Box") + if latest != nil { + latestURL := baseURL + latest.DownloadPath + payload["app_version"] = latest.Version + payload["download_url"] = latestURL + payload["download_mirrors"] = []map[string]interface{}{ + { + "id": "primary", + "name": "官方下载", + "url": latestURL, + "type": "direct", + "sha256": sha256File(filepath.Join(downloadsDir, latest.FileName)), + "enabled": true, + }, + } + payload["detected_product"] = productName + payload["detected_packages"] = products + } + + return payload +} + +func packageID(productName string, platform string, arch string, version string) string { + value := strings.ToLower(productName + "-" + platform + "-" + arch + "-" + version) + value = strings.NewReplacer(" ", "-", "_", "-").Replace(value) + return value +} + +func detectPackagePlatform(fileName string, ext string) (string, string) { + lower := strings.ToLower(fileName) + platform := "unknown" + switch strings.ToLower(ext) { + case "exe", "msi": + platform = "windows" + case "apk": + platform = "android" + case "dmg", "pkg": + platform = "macos" + case "deb", "rpm", "appimage", "tar.gz": + platform = "linux" + } + + arch := "x64" + if strings.Contains(lower, "arm64") || strings.Contains(lower, "aarch64") { + arch = "arm64" + } else if strings.Contains(lower, "x86") && !strings.Contains(lower, "x64") { + arch = "x86" + } else if platform == "android" { + arch = "universal" + } + return platform, arch +} + +func sha256File(path string) string { + file, err := os.Open(path) + if err != nil { + return "" + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "" + } + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/server/update/database/config.go b/server/update/database/config.go new file mode 100644 index 0000000..de99135 --- /dev/null +++ b/server/update/database/config.go @@ -0,0 +1,115 @@ +package database + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" +) + +// DBConfigFile 数据库配置文件结构 +type DBConfigFile struct { + Type string `json:"type"` // "sqlite" 或 "mysql" + Host string `json:"host"` // MySQL 主机 + Port string `json:"port"` // MySQL 端口 + User string `json:"user"` // MySQL 用户名 + Password string `json:"password"` // MySQL 密码 + Database string `json:"database"` // MySQL 数据库名 + TablePrefix string `json:"table_prefix"` // 表前缀 + DSN string `json:"dsn"` // SQLite 数据目录 + Initialized bool `json:"initialized"` // 是否已初始化 +} + +var ( + configFile *DBConfigFile + configFileLock sync.RWMutex + configFilePath = "data/db-config.json" +) + +// LoadDBConfig 加载数据库配置 +func LoadDBConfig() (*DBConfigFile, error) { + configFileLock.RLock() + if configFile != nil { + configFileLock.RUnlock() + return configFile, nil + } + configFileLock.RUnlock() + + configFileLock.Lock() + defer configFileLock.Unlock() + + // 双重检查 + if configFile != nil { + return configFile, nil + } + + // 确保目录存在 + dir := filepath.Dir(configFilePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("创建配置目录失败: %w", err) + } + + // 读取配置文件 + data, err := os.ReadFile(configFilePath) + if err != nil { + if os.IsNotExist(err) { + // 配置文件不存在,返回默认配置 + configFile = &DBConfigFile{ + Type: "mysql", + Host: "localhost", + Port: "3306", + User: "root", + Password: "", + Database: "software_download_center", + TablePrefix: "", + DSN: "data", + Initialized: false, + } + return configFile, nil + } + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + + configFile = &DBConfigFile{} + if err := json.Unmarshal(data, configFile); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %w", err) + } + + return configFile, nil +} + +// SaveDBConfig 保存数据库配置 +func SaveDBConfig(config *DBConfigFile) error { + configFileLock.Lock() + defer configFileLock.Unlock() + + // 确保目录存在 + dir := filepath.Dir(configFilePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("创建配置目录失败: %w", err) + } + + // 序列化配置 + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("序列化配置失败: %w", err) + } + + // 写入文件 + if err := os.WriteFile(configFilePath, data, 0644); err != nil { + return fmt.Errorf("写入配置文件失败: %w", err) + } + + configFile = config + return nil +} + +// IsDBInitialized 检查数据库是否已初始化 +func IsDBInitialized() bool { + config, err := LoadDBConfig() + if err != nil { + return false + } + return config.Initialized +} diff --git a/server/update/database/db.go b/server/update/database/db.go new file mode 100644 index 0000000..c8121a4 --- /dev/null +++ b/server/update/database/db.go @@ -0,0 +1,430 @@ +package database + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "software-download-center/models" + "software-download-center/utils" + + "gorm.io/driver/mysql" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" +) + +var ( + DB *gorm.DB + dbType string // "sqlite" 或 "mysql" + dbLogger *utils.Logger +) + +// DatabaseConfig 数据库配置 +type DatabaseConfig struct { + Type string // "sqlite" 或 "mysql" + DSN string // 数据库连接字符串 + Host string // MySQL 主机 + Port string // MySQL 端口 + User string // MySQL 用户名 + Password string // MySQL 密码 + Database string // MySQL 数据库名 + TablePrefix string // 表前缀 +} + +// InitDB 初始化数据库(延迟初始化,允许失败) +func InitDB() error { + logger := utils.NewLogger() + dbLogger = logger + + // 检测操作系统 + osInfo := utils.DetectOS() + logger.System(fmt.Sprintf("🖥️ 检测到操作系统: %s (%s)", osInfo.OS, osInfo.Arch)) + + // 检查是否已初始化 + if IsDBInitialized() { + // 从配置文件读取数据库配置 + fileConfig, err := LoadDBConfig() + if err != nil { + logger.Warn(fmt.Sprintf("⚠️ 读取数据库配置失败: %s,使用环境变量", err.Error())) + config := getDatabaseConfig(osInfo) + return connectDB(config, logger) + } + + config := &DatabaseConfig{ + Type: fileConfig.Type, + Host: fileConfig.Host, + Port: fileConfig.Port, + User: fileConfig.User, + Password: fileConfig.Password, + Database: fileConfig.Database, + TablePrefix: fileConfig.TablePrefix, + DSN: fileConfig.DSN, + } + return connectDB(config, logger) + } + + // 未初始化,不强制连接 + logger.System("ℹ️ 数据库未初始化,等待管理员配置") + return nil +} + +// connectDB 连接数据库 +func connectDB(config *DatabaseConfig, logger *utils.Logger) error { + + // 确保 data 目录存在(仅 SQLite 需要) + if config.Type == "sqlite" { + if err := os.MkdirAll(config.DSN, 0755); err != nil { + return fmt.Errorf("创建数据目录失败: %w", err) + } + } + + var err error + dbType = config.Type + + if config.Type == "mysql" { + // 使用 MySQL + logger.System("📊 使用 MySQL 数据库") + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, config.Password, config.Host, config.Port, config.Database) + + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + if err != nil { + return fmt.Errorf("MySQL 连接失败: %w", err) + } + } else { + // 使用 SQLite + logger.System("📊 使用 SQLite 数据库") + + osInfo := utils.DetectOS() + // 检查 CGO 支持 + if !osInfo.IsCGO { + logger.Warn("⚠️ 检测到 CGO 未启用,SQLite 可能需要 CGO 支持") + } + + dbPath := filepath.Join(config.DSN, "app.db") + logger.System(fmt.Sprintf("📁 数据库文件路径: %s", dbPath)) + + DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + if err != nil { + return fmt.Errorf("SQLite 连接失败: %w", err) + } + } + + if DB == nil { + return fmt.Errorf("数据库连接失败") + } + + // 设置表前缀 + models.SetTablePrefix(config.TablePrefix) + + // 自动迁移 + logger.System("🔄 开始数据库迁移...") + if err := DB.AutoMigrate( + &models.User{}, + &models.Route{}, + ); err != nil { + return fmt.Errorf("数据库迁移失败: %w", err) + } + logger.System("✅ 数据库迁移完成") + + // 记录数据库信息 + var userCount int64 + DB.Model(&models.User{}).Count(&userCount) + logger.System(fmt.Sprintf("📊 数据库类型: %s", strings.ToUpper(dbType))) + logger.System(fmt.Sprintf("👥 当前用户数: %d", userCount)) + + return nil +} + +// InitDBWithConfig 使用配置初始化数据库 +func InitDBWithConfig(config *DatabaseConfig) error { + logger := utils.NewLogger() + dbLogger = logger + + // 连接数据库 + if err := connectDB(config, logger); err != nil { + return err + } + + // 保存配置 + fileConfig := &DBConfigFile{ + Type: config.Type, + Host: config.Host, + Port: config.Port, + User: config.User, + Password: config.Password, + Database: config.Database, + TablePrefix: config.TablePrefix, + DSN: config.DSN, + Initialized: true, + } + + if err := SaveDBConfig(fileConfig); err != nil { + logger.Warn(fmt.Sprintf("⚠️ 保存数据库配置失败: %s", err.Error())) + } + + return nil +} + +// getDatabaseConfig 获取数据库配置(从环境变量) +func getDatabaseConfig(osInfo *utils.OSInfo) *DatabaseConfig { + config := &DatabaseConfig{ + Type: getEnvOrDefault("DB_TYPE", "sqlite"), + Host: getEnvOrDefault("DB_HOST", "localhost"), + Port: getEnvOrDefault("DB_PORT", "3306"), + User: getEnvOrDefault("DB_USER", "root"), + Password: getEnvOrDefault("DB_PASSWORD", ""), + Database: getEnvOrDefault("DB_NAME", "software_download_center"), + TablePrefix: getEnvOrDefault("DB_TABLE_PREFIX", ""), + } + + // 数据目录 + config.DSN = osInfo.DataDir + if config.DSN == "" { + config.DSN = "data" + } + + return config +} + +// GetDatabaseConfigFromFile 从配置文件获取数据库配置 +func GetDatabaseConfigFromFile() (*DatabaseConfig, error) { + fileConfig, err := LoadDBConfig() + if err != nil { + return nil, err + } + + return &DatabaseConfig{ + Type: fileConfig.Type, + Host: fileConfig.Host, + Port: fileConfig.Port, + User: fileConfig.User, + Password: fileConfig.Password, + Database: fileConfig.Database, + TablePrefix: fileConfig.TablePrefix, + DSN: fileConfig.DSN, + }, nil +} + +// GetDatabaseConfig 获取当前数据库配置(供外部调用) +func GetDatabaseConfig() *DatabaseConfig { + // 优先从配置文件读取 + if config, err := GetDatabaseConfigFromFile(); err == nil && config != nil { + return config + } + // 回退到环境变量 + osInfo := utils.DetectOS() + return getDatabaseConfig(osInfo) +} + +// VerifyMySQLPassword 验证 MySQL 密码 +func VerifyMySQLPassword(password string) error { + config := GetDatabaseConfig() + if config.Type != "mysql" { + return fmt.Errorf("当前数据库类型不是 MySQL") + } + + // 尝试使用提供的密码连接数据库 + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, password, config.Host, config.Port, config.Database) + + testDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + if err != nil { + return fmt.Errorf("密码验证失败: %w", err) + } + + // 关闭测试连接 + sqlDB, _ := testDB.DB() + if sqlDB != nil { + sqlDB.Close() + } + + return nil +} + +// UpdateMySQLPassword 更新 MySQL root 密码 +func UpdateMySQLPassword(newPassword string) error { + config := GetDatabaseConfig() + if config.Type != "mysql" { + return fmt.Errorf("当前数据库类型不是 MySQL") + } + + // 使用当前密码连接 + currentDSN := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, config.Password, config.Host, config.Port, config.Database) + + db, err := gorm.Open(mysql.Open(currentDSN), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + if err != nil { + return fmt.Errorf("连接数据库失败: %w", err) + } + + sqlDB, _ := db.DB() + defer sqlDB.Close() + + // 执行 ALTER USER 语句更新密码 + // MySQL 8.0+ 使用 ALTER USER,旧版本使用 SET PASSWORD + updateSQL := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s'", config.User, "%", newPassword) + + // 尝试使用 ALTER USER(MySQL 5.7.6+ 和 8.0+) + _, err = sqlDB.Exec(updateSQL) + if err != nil { + // 如果失败,尝试使用 SET PASSWORD(兼容旧版本) + setPasswordSQL := fmt.Sprintf("SET PASSWORD FOR '%s'@'%s' = PASSWORD('%s')", config.User, "%", newPassword) + _, err2 := sqlDB.Exec(setPasswordSQL) + if err2 != nil { + return fmt.Errorf("更新密码失败: %w (ALTER USER 失败: %v)", err2, err) + } + } + + // 刷新权限 + _, err = sqlDB.Exec("FLUSH PRIVILEGES") + if err != nil { + // 刷新权限失败不影响密码更新,只记录警告 + utils.NewLogger().Warn(fmt.Sprintf("刷新权限失败: %s", err.Error())) + } + + return nil +} + +// getEnvOrDefault 获取环境变量或返回默认值 +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// GetDB 获取数据库实例 +func GetDB() *gorm.DB { + return DB +} + +// GetDBType 获取数据库类型 +func GetDBType() string { + return dbType +} + +// ConvertDatabase 转换数据库(MySQL <-> SQLite) +func ConvertDatabase(targetType string, logger *utils.Logger) error { + if dbType == targetType { + return fmt.Errorf("数据库类型已经是 %s", targetType) + } + + logger.System(fmt.Sprintf("🔄 开始数据库转换: %s -> %s", strings.ToUpper(dbType), strings.ToUpper(targetType))) + + // 导出当前数据库数据 + users, routes, err := exportData() + if err != nil { + return fmt.Errorf("导出数据失败: %w", err) + } + logger.System(fmt.Sprintf("📤 已导出 %d 个用户, %d 个路由", len(users), len(routes))) + + // 关闭当前数据库连接 + if DB != nil { + sqlDB, _ := DB.DB() + if sqlDB != nil { + sqlDB.Close() + } + } + + // 初始化新数据库 + osInfo := utils.DetectOS() + config := getDatabaseConfig(osInfo) + config.Type = targetType + dbType = targetType + + var newDB *gorm.DB + if targetType == "mysql" { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, config.Password, config.Host, config.Port, config.Database) + newDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + } else { + dbPath := filepath.Join(config.DSN, "app.db") + newDB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + } + + if err != nil { + return fmt.Errorf("连接新数据库失败: %w", err) + } + + // 迁移表结构 + if err := newDB.AutoMigrate(&models.User{}, &models.Route{}); err != nil { + return fmt.Errorf("迁移表结构失败: %w", err) + } + + // 导入数据 + if err := importData(newDB, users, routes, logger); err != nil { + return fmt.Errorf("导入数据失败: %w", err) + } + + // 更新全局数据库实例 + DB = newDB + logger.System(fmt.Sprintf("✅ 数据库转换完成: %s", strings.ToUpper(targetType))) + + return nil +} + +// exportData 导出数据 +func exportData() ([]models.User, []models.Route, error) { + var users []models.User + var routes []models.Route + + if err := DB.Find(&users).Error; err != nil { + return nil, nil, err + } + + if err := DB.Find(&routes).Error; err != nil { + return nil, nil, err + } + + return users, routes, nil +} + +// importData 导入数据 +func importData(db *gorm.DB, users []models.User, routes []models.Route, logger *utils.Logger) error { + // 导入用户 + if len(users) > 0 { + if err := db.Create(&users).Error; err != nil { + logger.Warn(fmt.Sprintf("⚠️ 导入用户时出现错误: %s", err.Error())) + // 尝试逐个导入 + for _, user := range users { + if err := db.Create(&user).Error; err != nil { + logger.Warn(fmt.Sprintf("⚠️ 跳过用户 %s: %s", user.Username, err.Error())) + } + } + } else { + logger.System(fmt.Sprintf("✅ 成功导入 %d 个用户", len(users))) + } + } + + // 导入路由 + if len(routes) > 0 { + if err := db.Create(&routes).Error; err != nil { + logger.Warn(fmt.Sprintf("⚠️ 导入路由时出现错误: %s", err.Error())) + // 尝试逐个导入 + for _, route := range routes { + if err := db.Create(&route).Error; err != nil { + logger.Warn(fmt.Sprintf("⚠️ 跳过路由 %s %s: %s", route.Method, route.Path, err.Error())) + } + } + } else { + logger.System(fmt.Sprintf("✅ 成功导入 %d 个路由", len(routes))) + } + } + + return nil +} diff --git a/server/update/go.mod b/server/update/go.mod new file mode 100644 index 0000000..71580d1 --- /dev/null +++ b/server/update/go.mod @@ -0,0 +1,42 @@ +module software-download-center + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.0 + golang.org/x/crypto v0.17.0 + gorm.io/driver/mysql v1.5.2 + gorm.io/driver/sqlite v1.5.4 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/server/update/go.sum b/server/update/go.sum new file mode 100644 index 0000000..f70fe97 --- /dev/null +++ b/server/update/go.sum @@ -0,0 +1,103 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/update/handlers/admin.go b/server/update/handlers/admin.go new file mode 100644 index 0000000..dc50ca0 --- /dev/null +++ b/server/update/handlers/admin.go @@ -0,0 +1,447 @@ +package handlers + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "software-download-center/database" + "software-download-center/models" + "software-download-center/utils" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// LogEntry 日志条目 +type LogEntry struct { + Time string `json:"time"` + Level string `json:"level"` + Message string `json:"message"` +} + +var logBuffer []LogEntry +var maxLogEntries = 1000 + +// AddLog 添加日志到缓冲区 +func AddLog(level, message string) { + entry := LogEntry{ + Time: time.Now().Format("2006-01-02 15:04:05"), + Level: level, + Message: message, + } + + logBuffer = append(logBuffer, entry) + + // 同时输出到控制台(使用 utils.Logger) + logger := utils.NewLogger() + switch level { + case "ERROR": + logger.Error(message) + case "WARN": + logger.Warn(message) + case "INFO": + logger.Info(message) + default: + logger.System(message) + } + + // 保持缓冲区大小 + if len(logBuffer) > maxLogEntries { + logBuffer = logBuffer[len(logBuffer)-maxLogEntries:] + } +} + +// GetLogBuffer 获取日志缓冲区(用于外部访问) +func GetLogBuffer() []LogEntry { + return logBuffer +} + +// GetLogs 获取日志 +func GetLogs(c *gin.Context) { + limit := 100 + if limitStr := c.Query("limit"); limitStr != "" { + fmt.Sscanf(limitStr, "%d", &limit) + } + + start := len(logBuffer) - limit + if start < 0 { + start = 0 + } + + logs := logBuffer[start:] + c.JSON(http.StatusOK, gin.H{ + "logs": logs, + "total": len(logBuffer), + }) +} + +// GetRoutes 获取所有路由 +func GetRoutes(c *gin.Context) { + var routes []models.Route + if err := database.DB.Order("`order` ASC, id ASC").Find(&routes).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "获取路由失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "routes": routes, + }) +} + +// CreateRoute 创建路由 +func CreateRoute(c *gin.Context) { + var route models.Route + if err := c.ShouldBindJSON(&route); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证路径是否已存在 + var existingRoute models.Route + if err := database.DB.Where("path = ? AND method = ?", route.Path, route.Method).First(&existingRoute).Error; err == nil { + AddLog("WARN", fmt.Sprintf("创建路由失败(已存在): %s %s (IP: %s)", route.Method, route.Path, c.ClientIP())) + c.JSON(http.StatusConflict, gin.H{ + "error": "路由已存在", + }) + return + } + + if err := database.DB.Create(&route).Error; err != nil { + AddLog("ERROR", fmt.Sprintf("创建路由数据库错误: %s %s - %s (IP: %s)", route.Method, route.Path, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "创建路由失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("创建路由成功: %s %s (类型: %s, IP: %s)", route.Method, route.Path, route.Type, c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "message": "路由创建成功", + "route": route, + }) +} + +// UpdateRoute 更新路由 +func UpdateRoute(c *gin.Context) { + id := c.Param("id") + var route models.Route + oldPath := route.Path + oldMethod := route.Method + + if err := database.DB.First(&route, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + AddLog("WARN", fmt.Sprintf("更新路由失败(不存在): ID=%s (IP: %s)", id, c.ClientIP())) + c.JSON(http.StatusNotFound, gin.H{ + "error": "路由不存在", + }) + return + } + AddLog("ERROR", fmt.Sprintf("查询路由失败: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "查询路由失败", + }) + return + } + + oldPath = route.Path + oldMethod = route.Method + + if err := c.ShouldBindJSON(&route); err != nil { + AddLog("WARN", fmt.Sprintf("更新路由请求参数错误: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP())) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + }) + return + } + + if err := database.DB.Save(&route).Error; err != nil { + AddLog("ERROR", fmt.Sprintf("更新路由数据库错误: %s %s - %s (IP: %s)", route.Method, route.Path, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "更新路由失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("更新路由成功: %s %s -> %s %s (IP: %s)", oldMethod, oldPath, route.Method, route.Path, c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "message": "路由更新成功", + "route": route, + }) +} + +// DeleteRoute 删除路由 +func DeleteRoute(c *gin.Context) { + id := c.Param("id") + var route models.Route + if err := database.DB.First(&route, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + AddLog("WARN", fmt.Sprintf("删除路由失败(不存在): ID=%s (IP: %s)", id, c.ClientIP())) + c.JSON(http.StatusNotFound, gin.H{ + "error": "路由不存在", + }) + return + } + AddLog("ERROR", fmt.Sprintf("查询路由失败: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "查询路由失败", + }) + return + } + + routeInfo := fmt.Sprintf("%s %s", route.Method, route.Path) + + if err := database.DB.Delete(&route).Error; err != nil { + AddLog("ERROR", fmt.Sprintf("删除路由数据库错误: %s - %s (IP: %s)", routeInfo, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "删除路由失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("删除路由成功: %s (IP: %s)", routeInfo, c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "message": "路由删除成功", + }) +} + +// GetFiles 获取文件列表 +func GetFiles(c *gin.Context) { + dir := c.Query("dir") + if dir == "" { + dir = "public/downloads" + } + + // 安全检查:防止目录遍历 + if strings.Contains(dir, "..") { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "无效的目录路径", + }) + return + } + + files, err := os.ReadDir(dir) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取目录失败", + }) + return + } + + var fileList []gin.H + for _, file := range files { + info, _ := file.Info() + fileList = append(fileList, gin.H{ + "name": file.Name(), + "size": info.Size(), + "mod_time": info.ModTime().Format("2006-01-02 15:04:05"), + "is_dir": file.IsDir(), + }) + } + + c.JSON(http.StatusOK, gin.H{ + "files": fileList, + "path": dir, + }) +} + +// SaveFile 保存文件 +func SaveFile(c *gin.Context) { + var req struct { + Path string `json:"path" binding:"required"` + Content string `json:"content" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + }) + return + } + + // 安全检查 + if strings.Contains(req.Path, "..") { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "无效的文件路径", + }) + return + } + + // 确保目录存在 + dir := filepath.Dir(req.Path) + if err := os.MkdirAll(dir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "创建目录失败", + }) + return + } + + // 保存文件 + if err := os.WriteFile(req.Path, []byte(req.Content), 0644); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "保存文件失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("保存文件: %s", req.Path)) + c.JSON(http.StatusOK, gin.H{ + "message": "文件保存成功", + }) +} + +// ReadFile 读取文件 +func ReadFile(c *gin.Context) { + path := c.Query("path") + if path == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "缺少文件路径参数", + }) + return + } + + // 安全检查 + if strings.Contains(path, "..") { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "无效的文件路径", + }) + return + } + + content, err := os.ReadFile(path) + if err != nil { + AddLog("ERROR", fmt.Sprintf("读取文件失败: %s - %s (IP: %s)", path, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "读取文件失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("读取文件: %s (大小: %d 字节, IP: %s)", path, len(content), c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "content": string(content), + "path": path, + "size": len(content), + }) +} + +// ReloadRoutes 热重载路由和配置 +func ReloadRoutes(c *gin.Context) { + var req struct { + Type string `json:"type"` // "config" 或 "all" + } + + if err := c.ShouldBindJSON(&req); err != nil { + req.Type = "all" + } + + if req.Type == "config" || req.Type == "all" { + // 重新加载所有配置文件 + files := []string{"tool-status.json", "update-info.json", "media-types.json"} + successCount := 0 + for _, file := range files { + if _, err := os.Stat(filepath.Join("public", file)); err == nil { + if err := utils.ReloadConfig(file); err == nil { + successCount++ + } + } + } + AddLog("INFO", fmt.Sprintf("重新加载配置文件: %d/%d 成功 (IP: %s)", successCount, len(files), c.ClientIP())) + } + + // 注意:Gin 不支持动态路由重载,路由更改需要重启服务器 + if req.Type == "routes" || req.Type == "all" { + AddLog("INFO", fmt.Sprintf("路由热重载请求(需要重启服务器才能生效)(IP: %s)", c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "message": "配置已重新加载", + "note": "路由更改需要重启服务器才能生效,建议使用进程管理器(如 systemd、supervisor)", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "配置已重新加载", + }) +} + +// GetSystemInfo 获取系统信息 +func GetSystemInfo(c *gin.Context) { + var userCount int64 + var routeCount int64 + database.DB.Model(&models.User{}).Count(&userCount) + database.DB.Model(&models.Route{}).Count(&routeCount) + + c.JSON(http.StatusOK, gin.H{ + "users": userCount, + "routes": routeCount, + "logs": len(logBuffer), + "version": "1.0.0", + "server_time": time.Now().Format("2006-01-02 15:04:05"), + }) +} + +// UpdateJSONConfig 更新 JSON 配置文件 +func UpdateJSONConfig(c *gin.Context) { + var req struct { + File string `json:"file" binding:"required"` + Content map[string]interface{} `json:"content" binding:"required"` + Reload bool `json:"reload"` // 是否立即重新加载 + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + }) + return + } + + // 验证文件路径 + allowedFiles := []string{"tool-status.json", "update-info.json", "media-types.json"} + allowed := false + for _, f := range allowedFiles { + if req.File == f { + allowed = true + break + } + } + + if !allowed { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "不允许修改此文件", + }) + return + } + + // 使用配置工具保存并更新缓存 + if err := utils.SaveConfig(req.File, req.Content); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "保存文件失败: " + err.Error(), + }) + return + } + + message := "配置文件更新成功" + if req.Reload { + // 重新加载配置到缓存 + if err := utils.ReloadConfig(req.File); err != nil { + AddLog("WARN", fmt.Sprintf("配置文件已保存但重新加载失败: %s - %s", req.File, err.Error())) + message = "配置文件已保存,但重新加载时出现警告" + } else { + AddLog("INFO", fmt.Sprintf("配置文件已保存并立即加载: %s", req.File)) + message = "配置文件已保存并立即生效" + } + } else { + AddLog("INFO", fmt.Sprintf("更新配置文件: %s", req.File)) + } + + c.JSON(http.StatusOK, gin.H{ + "message": message, + "file": req.File, + }) +} diff --git a/server/update/handlers/auth.go b/server/update/handlers/auth.go new file mode 100644 index 0000000..b87900f --- /dev/null +++ b/server/update/handlers/auth.go @@ -0,0 +1,263 @@ +package handlers + +import ( + "fmt" + "net/http" + + "software-download-center/database" + "software-download-center/models" + "software-download-center/utils" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// RegisterRequest 注册请求 +type RegisterRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=8"` +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// Register 用户注册 +func Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证密码强度 + if err := utils.ValidatePasswordStrength(req.Password); err != nil { + AddLog("WARN", fmt.Sprintf("注册失败(密码强度不足): 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return + } + + // 检查用户名是否已存在 + var existingUser models.User + if err := database.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil { + AddLog("WARN", fmt.Sprintf("注册失败(用户名已存在): 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.JSON(http.StatusConflict, gin.H{ + "error": "用户名已存在", + }) + return + } + + // 检查邮箱是否已存在 + if err := database.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { + AddLog("WARN", fmt.Sprintf("注册失败(邮箱已被注册): 邮箱=%s, IP=%s", req.Email, c.ClientIP())) + c.JSON(http.StatusConflict, gin.H{ + "error": "邮箱已被注册", + }) + return + } + + // 检查是否是第一个用户(自动成为管理员) + var userCount int64 + database.DB.Model(&models.User{}).Count(&userCount) + isAdmin := userCount == 0 + + // 加密密码 + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "密码加密失败", + }) + return + } + + // 创建用户 + user := models.User{ + Username: req.Username, + Email: req.Email, + Password: hashedPassword, + IsAdmin: isAdmin, + IsActive: true, + } + + if err := database.DB.Create(&user).Error; err != nil { + AddLog("ERROR", fmt.Sprintf("创建用户数据库错误: 用户名=%s - %s, IP=%s", req.Username, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "创建用户失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("用户注册成功: 用户名=%s, 邮箱=%s, 管理员=%v, IP=%s", user.Username, user.Email, user.IsAdmin, c.ClientIP())) + + // 生成 token + token, err := utils.GenerateToken(user.ID, user.Username, user.IsAdmin) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "生成 token 失败", + }) + return + } + + // 设置 cookie + c.SetCookie("token", token, 24*3600, "/", "", false, true) + + c.JSON(http.StatusOK, gin.H{ + "message": "注册成功", + "token": token, + "user": gin.H{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + "is_admin": user.IsAdmin, + }, + }) +} + +// Login 用户登录 +func Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + }) + return + } + + // 检查数据库是否已初始化 + if !database.IsDBInitialized() { + // 使用默认管理员账号 + if req.Username == "admin" && req.Password == "admin123456" { + // 生成临时 token(用于安装页面) + token, err := utils.GenerateToken(0, "admin", true) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "生成 token 失败", + }) + return + } + + AddLog("INFO", fmt.Sprintf("默认管理员登录成功: 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.SetCookie("token", token, 24*3600, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "登录成功,请配置数据库", + "token": token, + "user": gin.H{ + "id": 0, + "username": "admin", + "is_admin": true, + "is_setup": false, // 标记需要安装 + }, + }) + return + } + + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "数据库未初始化,请使用默认管理员账号登录(admin/admin123456)", + }) + return + } + + // 查找用户 + var user models.User + if err := database.DB.Where("username = ?", req.Username).First(&user).Error; err != nil { + if err == gorm.ErrRecordNotFound { + AddLog("WARN", fmt.Sprintf("登录失败(用户不存在): 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "用户名或密码错误", + }) + return + } + AddLog("ERROR", fmt.Sprintf("查询用户失败: 用户名=%s - %s, IP=%s", req.Username, err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "查询用户失败", + }) + return + } + + // 检查用户是否激活 + if !user.IsActive { + AddLog("WARN", fmt.Sprintf("登录失败(账户已禁用): 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.JSON(http.StatusForbidden, gin.H{ + "error": "账户已被禁用", + }) + return + } + + // 验证密码 + if !utils.CheckPassword(req.Password, user.Password) { + AddLog("WARN", fmt.Sprintf("登录失败(密码错误): 用户名=%s, IP=%s", req.Username, c.ClientIP())) + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "用户名或密码错误", + }) + return + } + + AddLog("INFO", fmt.Sprintf("用户登录成功: 用户名=%s, 管理员=%v, IP=%s", user.Username, user.IsAdmin, c.ClientIP())) + + // 生成 token + token, err := utils.GenerateToken(user.ID, user.Username, user.IsAdmin) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "生成 token 失败", + }) + return + } + + // 设置 cookie + c.SetCookie("token", token, 24*3600, "/", "", false, true) + + c.JSON(http.StatusOK, gin.H{ + "message": "登录成功", + "token": token, + "user": gin.H{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + "is_admin": user.IsAdmin, + }, + }) +} + +// Logout 用户登出 +func Logout(c *gin.Context) { + c.SetCookie("token", "", -1, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "登出成功", + }) +} + +// GetCurrentUser 获取当前用户信息 +func GetCurrentUser(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "未授权", + }) + return + } + + var user models.User + if err := database.DB.First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "用户不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "user": gin.H{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + "is_admin": user.IsAdmin, + "is_active": user.IsActive, + }, + }) +} diff --git a/server/update/handlers/database.go b/server/update/handlers/database.go new file mode 100644 index 0000000..ad3a5cd --- /dev/null +++ b/server/update/handlers/database.go @@ -0,0 +1,219 @@ +package handlers + +import ( + "fmt" + "net/http" + "strings" + + "software-download-center/database" + "software-download-center/utils" + + "github.com/gin-gonic/gin" +) + +// GetDatabaseInfo 获取数据库信息 +func GetDatabaseInfo(c *gin.Context) { + dbType := database.GetDBType() + osInfo := utils.GetOSInfo() + + var dbInfo gin.H + if dbType == "mysql" { + dbInfo = gin.H{ + "type": "MySQL", + "status": "connected", + } + } else { + dbInfo = gin.H{ + "type": "SQLite", + "status": "connected", + "file": "data/app.db", + "cgo_support": osInfo.IsCGO, + } + } + + c.JSON(http.StatusOK, gin.H{ + "database": dbInfo, + "os": gin.H{ + "os": osInfo.OS, + "arch": osInfo.Arch, + }, + }) +} + +// ConvertDatabaseRequest 数据库转换请求 +type ConvertDatabaseRequest struct { + TargetType string `json:"target_type" binding:"required,oneof=sqlite mysql"` +} + +// ConvertDatabase 转换数据库 +func ConvertDatabase(c *gin.Context) { + var req ConvertDatabaseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + logger := utils.NewLogger() + + if err := database.ConvertDatabase(req.TargetType, logger); err != nil { + AddLog("ERROR", fmt.Sprintf("数据库转换失败: %s", err.Error())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "数据库转换失败: " + err.Error(), + }) + return + } + + AddLog("INFO", fmt.Sprintf("数据库转换成功: %s", req.TargetType)) + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("数据库已成功转换为 %s", strings.ToUpper(req.TargetType)), + "type": req.TargetType, + }) +} + +// UpdateDatabasePasswordRequest 更新数据库密码请求 +type UpdateDatabasePasswordRequest struct { + CurrentPassword string `json:"current_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=1"` + ConfirmPassword string `json:"confirm_password" binding:"required"` +} + +// UpdateDatabasePassword 更新数据库 root 密码 +func UpdateDatabasePassword(c *gin.Context) { + var req UpdateDatabasePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + AddLog("WARN", fmt.Sprintf("更新数据库密码请求参数错误: %s (IP: %s)", err.Error(), c.ClientIP())) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证新密码和确认密码是否一致 + if req.NewPassword != req.ConfirmPassword { + AddLog("WARN", fmt.Sprintf("更新数据库密码失败(密码不一致)(IP: %s)", c.ClientIP())) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "新密码和确认密码不一致", + }) + return + } + + // 检查当前数据库类型 + dbType := database.GetDBType() + if dbType != "mysql" { + AddLog("WARN", fmt.Sprintf("更新数据库密码失败(当前数据库不是 MySQL)(IP: %s)", c.ClientIP())) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "当前数据库类型不是 MySQL,无法修改密码", + }) + return + } + + // 验证当前密码 + if err := database.VerifyMySQLPassword(req.CurrentPassword); err != nil { + AddLog("WARN", fmt.Sprintf("更新数据库密码失败(当前密码验证失败)(IP: %s)", c.ClientIP())) + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "当前密码错误", + }) + return + } + + // 更新密码 + if err := database.UpdateMySQLPassword(req.NewPassword); err != nil { + AddLog("ERROR", fmt.Sprintf("更新数据库密码失败: %s (IP: %s)", err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "更新密码失败: " + err.Error(), + }) + return + } + + AddLog("INFO", fmt.Sprintf("数据库 root 密码更新成功 (IP: %s)", c.ClientIP())) + c.JSON(http.StatusOK, gin.H{ + "message": "数据库密码更新成功!请更新环境变量 DB_PASSWORD 并重启服务器以使新密码生效。", + }) +} + +// GetDatabaseConfig 获取数据库配置信息(不包含敏感信息) +func GetDatabaseConfig(c *gin.Context) { + config := database.GetDatabaseConfig() + + // 隐藏敏感信息 + safeConfig := gin.H{ + "type": config.Type, + "host": config.Host, + "port": config.Port, + "user": config.User, + "database": config.Database, + "table_prefix": config.TablePrefix, + "has_password": config.Password != "", + } + + c.JSON(http.StatusOK, gin.H{ + "config": safeConfig, + }) +} + +// UpdateDatabaseConfigRequest 更新数据库配置请求 +type UpdateDatabaseConfigRequest struct { + Type string `json:"type" binding:"required,oneof=sqlite mysql"` + Host string `json:"host"` + Port string `json:"port"` + User string `json:"user"` + Password string `json:"password"` + Database string `json:"database"` + TablePrefix string `json:"table_prefix"` + DSN string `json:"dsn"` +} + +// UpdateDatabaseConfig 更新数据库配置(需要重新连接) +func UpdateDatabaseConfig(c *gin.Context) { + var req UpdateDatabaseConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证必填字段 + if req.Type == "mysql" { + if req.Host == "" || req.Port == "" || req.User == "" || req.Database == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "MySQL 配置不完整:需要 host, port, user, database", + }) + return + } + } else if req.Type == "sqlite" { + if req.DSN == "" { + req.DSN = "data" + } + } + + // 构建数据库配置 + config := &database.DatabaseConfig{ + Type: req.Type, + Host: req.Host, + Port: req.Port, + User: req.User, + Password: req.Password, + Database: req.Database, + TablePrefix: req.TablePrefix, + DSN: req.DSN, + } + + // 测试连接 + if err := database.InitDBWithConfig(config); err != nil { + AddLog("ERROR", fmt.Sprintf("数据库配置更新失败: %s (IP: %s)", err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "数据库连接失败: " + err.Error(), + }) + return + } + + AddLog("INFO", fmt.Sprintf("数据库配置更新成功: 类型=%s (IP: %s)", req.Type, c.ClientIP())) + + c.JSON(http.StatusOK, gin.H{ + "message": "数据库配置已更新,请重启服务器以应用更改", + "type": req.Type, + }) +} diff --git a/server/update/handlers/install.go b/server/update/handlers/install.go new file mode 100644 index 0000000..e05411b --- /dev/null +++ b/server/update/handlers/install.go @@ -0,0 +1,107 @@ +package handlers + +import ( + "fmt" + "net/http" + + "software-download-center/database" + + "github.com/gin-gonic/gin" +) + +// DefaultAdminUsername 默认管理员用户名 +const DefaultAdminUsername = "admin" + +// DefaultAdminPassword 默认管理员密码 +const DefaultAdminPassword = "admin123456" + +// CheckInstallStatus 检查安装状态 +func CheckInstallStatus(c *gin.Context) { + isInitialized := database.IsDBInitialized() + + c.JSON(http.StatusOK, gin.H{ + "initialized": isInitialized, + "default_admin": gin.H{ + "username": DefaultAdminUsername, + "password": DefaultAdminPassword, + }, + }) +} + +// InstallDatabaseRequest 数据库安装请求 +type InstallDatabaseRequest struct { + Type string `json:"type" binding:"required,oneof=sqlite mysql"` + Host string `json:"host"` + Port string `json:"port"` + User string `json:"user"` + Password string `json:"password"` + Database string `json:"database"` + TablePrefix string `json:"table_prefix"` + DSN string `json:"dsn"` // SQLite 数据目录 +} + +// InstallDatabase 安装数据库 +func InstallDatabase(c *gin.Context) { + // 检查是否已初始化 + if database.IsDBInitialized() { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "数据库已初始化,请使用设置页面修改配置", + }) + return + } + + var req InstallDatabaseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误: " + err.Error(), + }) + return + } + + // 验证必填字段 + if req.Type == "mysql" { + if req.Host == "" || req.Port == "" || req.User == "" || req.Database == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "MySQL 配置不完整:需要 host, port, user, database", + }) + return + } + } else if req.Type == "sqlite" { + if req.DSN == "" { + req.DSN = "data" + } + } + + // 构建数据库配置 + config := &database.DatabaseConfig{ + Type: req.Type, + Host: req.Host, + Port: req.Port, + User: req.User, + Password: req.Password, + Database: req.Database, + TablePrefix: req.TablePrefix, + DSN: req.DSN, + } + + // 初始化数据库 + if err := database.InitDBWithConfig(config); err != nil { + AddLog("ERROR", fmt.Sprintf("数据库安装失败: %s (IP: %s)", err.Error(), c.ClientIP())) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "数据库连接失败: " + err.Error(), + }) + return + } + + AddLog("INFO", fmt.Sprintf("数据库安装成功: 类型=%s (IP: %s)", req.Type, c.ClientIP())) + + c.JSON(http.StatusOK, gin.H{ + "message": "数据库安装成功", + "type": req.Type, + }) +} + +// VerifyDefaultAdmin 验证默认管理员登录 +func VerifyDefaultAdmin(username, password string) bool { + return username == DefaultAdminUsername && password == DefaultAdminPassword +} diff --git a/server/update/main.go b/server/update/main.go new file mode 100644 index 0000000..6dd1adb --- /dev/null +++ b/server/update/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "log" + "os" + + "software-download-center/config" + "software-download-center/database" + "software-download-center/utils" + + "github.com/gin-gonic/gin" +) + +func main() { + // 初始化日志 + logger := utils.NewLogger() + + // 检测操作系统 + osInfo := utils.DetectOS() + logger.System(fmt.Sprintf("🖥️ 操作系统: %s (%s)", osInfo.OS, osInfo.Arch)) + + // 尝试初始化数据库(允许失败,等待管理员配置) + if err := database.InitDB(); err != nil { + logger.Warn("⚠️ 数据库初始化失败: " + err.Error()) + logger.System("ℹ️ 系统将在管理员配置数据库后自动连接") + } else { + logger.System("✅ 数据库初始化成功") + } + + // 初始化配置缓存 + if err := utils.InitConfigCache(); err != nil { + logger.Warn("配置缓存初始化失败: " + err.Error()) + } else { + logger.System("✅ 配置缓存初始化成功") + } + + // 设置 Gin 模式 + if os.Getenv("GIN_MODE") == "" { + gin.SetMode(gin.ReleaseMode) + } + + // 创建 Gin 引擎 + r := gin.Default() + + // 注册所有路由 + config.RegisterRoutes(r, logger) + + // 启动服务器 + port := os.Getenv("PORT") + if port == "" { + port = "3355" + } + + logger.System("=============================================") + logger.System("✅ 服务器启动成功") + logger.System("📡 访问地址: http://localhost:" + port) + env := os.Getenv("GIN_MODE") + if env == "" { + env = "production" + } + logger.System("🌍 当前环境: " + env) + logger.System("🔄 兼容旧版访问:支持 /tool-status.json /update-info.json /media-types.json") + logger.System("=============================================") + + if err := r.Run(":" + port); err != nil { + log.Fatal("服务器启动失败:", err) + } +} diff --git a/server/update/middleware/auth.go b/server/update/middleware/auth.go new file mode 100644 index 0000000..9a874c1 --- /dev/null +++ b/server/update/middleware/auth.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "net/http" + "strings" + + "software-download-center/utils" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware 认证中间件 +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头获取 token + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // 尝试从 cookie 获取 + token, err := c.Cookie("token") + if err != nil || token == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "未授权,请先登录", + }) + c.Abort() + return + } + authHeader = "Bearer " + token + } + + // 提取 token + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "无效的认证格式", + }) + c.Abort() + return + } + + token := parts[1] + + // 解析 token + claims, err := utils.ParseToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "无效的 token", + }) + c.Abort() + return + } + + // 将用户信息存储到上下文 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("is_admin", claims.IsAdmin) + + c.Next() + } +} + +// AdminMiddleware 管理员中间件 +func AdminMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + isAdmin, exists := c.Get("is_admin") + if !exists || !isAdmin.(bool) { + c.JSON(http.StatusForbidden, gin.H{ + "error": "需要管理员权限", + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/server/update/models/route.go b/server/update/models/route.go new file mode 100644 index 0000000..b7a4466 --- /dev/null +++ b/server/update/models/route.go @@ -0,0 +1,31 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Route 路由模型 +type Route struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Method string `gorm:"not null;size:10" json:"method"` // GET, POST, PUT, DELETE等 + Path string `gorm:"not null;size:255;uniqueIndex" json:"path"` // 路由路径 + Type string `gorm:"not null;size:50" json:"type"` // view, json, file, static, custom + Handler string `gorm:"type:text" json:"handler"` // 处理函数或文件路径 + Description string `gorm:"size:500" json:"description"` // 路由描述 + IsActive bool `gorm:"default:true" json:"is_active"` // 是否启用 + Order int `gorm:"default:0" json:"order"` // 排序 +} + +// TableName 指定表名(支持前缀) +func (r Route) TableName() string { + if tablePrefix != "" { + return tablePrefix + "routes" + } + return "routes" +} diff --git a/server/update/models/user.go b/server/update/models/user.go new file mode 100644 index 0000000..34bbfba --- /dev/null +++ b/server/update/models/user.go @@ -0,0 +1,36 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +var tablePrefix = "" + +// SetTablePrefix 设置表前缀(由 database 包调用) +func SetTablePrefix(prefix string) { + tablePrefix = prefix +} + +// User 用户模型 +type User struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Username string `gorm:"uniqueIndex;not null;size:50" json:"username"` + Email string `gorm:"uniqueIndex;not null;size:100" json:"email"` + Password string `gorm:"not null;size:255" json:"-"` // 不返回密码 + IsAdmin bool `gorm:"default:false" json:"is_admin"` + IsActive bool `gorm:"default:true" json:"is_active"` +} + +// TableName 指定表名(支持前缀) +func (u User) TableName() string { + if tablePrefix != "" { + return tablePrefix + "users" + } + return "users" +} diff --git a/server/update/public/css/admin.css b/server/update/public/css/admin.css new file mode 100644 index 0000000..dd6cd29 --- /dev/null +++ b/server/update/public/css/admin.css @@ -0,0 +1,851 @@ +/* 简约风格的后台管理样式 */ + +:root { + --primary: #0071e3; + --success: #34c759; + --danger: #ef4444; + --warning: #ff9500; + --bg: #f5f5f7; + --card: #ffffff; + --text: #1d1d1f; + --text-secondary: #86868b; + --border: rgba(0, 0, 0, 0.1); + --shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; +} + +/* 认证页面 */ +.auth-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 1rem; +} + +.auth-box { + background: var(--card); + border-radius: 16px; + padding: 2.5rem; + width: 100%; + max-width: 420px; + box-shadow: var(--shadow-hover); + animation: fadeInUp 0.4s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.auth-header { + text-align: center; + margin-bottom: 2rem; +} + +.auth-logo { + width: 64px; + height: 64px; + margin: 0 auto 1rem; + color: var(--primary); +} + +.auth-header h1 { + font-size: 1.75rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.auth-header p { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.auth-form { + margin-top: 1.5rem; +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + font-size: 0.9rem; + color: var(--text); +} + +.form-group input, +.form-group select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 1rem; + transition: all 0.2s; + background: var(--card); +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1); +} + +.form-group small { + display: block; + margin-top: 0.25rem; + color: var(--text-secondary); + font-size: 0.8rem; +} + +.password-strength { + margin-top: 0.5rem; + height: 3px; + background: #e0e0e0; + border-radius: 2px; + overflow: hidden; + transition: all 0.3s; +} + +.password-strength::after { + content: ''; + display: block; + height: 100%; + width: 0%; + background: var(--danger); + transition: width 0.3s, background 0.3s; +} + +.password-strength.weak::after { + width: 33%; + background: var(--danger); +} + +.password-strength.medium::after { + width: 66%; + background: var(--warning); +} + +.password-strength.strong::after { + width: 100%; + background: var(--success); +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; +} + +.btn-primary { + background: var(--primary); + color: white; + width: 100%; + justify-content: center; +} + +.btn-primary:hover { + background: #0051a5; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 113, 227, 0.3); +} + +.btn-secondary { + background: #e0e0e0; + color: var(--text); +} + +.btn-secondary:hover { + background: #d0d0d0; +} + +.btn-success { + background: var(--success); + color: white; +} + +.btn-danger { + background: var(--danger); + color: white; +} + +.btn-icon { + padding: 0.5rem; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; +} + +.btn-icon:hover { + background: var(--bg); + color: var(--text); +} + +.error-message { + margin-top: 1rem; + padding: 0.75rem 1rem; + background: #fee; + color: var(--danger); + border-radius: 8px; + display: none; + animation: fadeIn 0.3s; +} + +.error-message.show { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.auth-footer { + margin-top: 1.5rem; + text-align: center; +} + +.auth-link { + color: var(--primary); + text-decoration: none; + font-size: 0.9rem; + transition: color 0.2s; +} + +.auth-link:hover { + color: #0051a5; + text-decoration: underline; +} + +/* 管理界面 */ +.admin-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.admin-header { + background: var(--card); + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.header-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.back-link { + display: flex; + align-items: center; + color: var(--text-secondary); + text-decoration: none; + padding: 0.5rem; + border-radius: 6px; + transition: all 0.2s; +} + +.back-link:hover { + background: var(--bg); + color: var(--text); +} + +.admin-header h1 { + font-size: 1.5rem; + font-weight: 600; +} + +.header-right { + display: flex; + align-items: center; + gap: 1rem; +} + +#current-user { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.admin-layout { + display: flex; + flex: 1; +} + +.admin-sidebar { + width: 260px; + background: var(--card); + border-right: 1px solid var(--border); + padding: 1rem 0; +} + +.sidebar-nav { + display: flex; + flex-direction: column; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1.5rem; + color: var(--text); + text-decoration: none; + transition: all 0.2s; + border-left: 3px solid transparent; +} + +.nav-item:hover { + background: var(--bg); + color: var(--primary); +} + +.nav-item.active { + background: rgba(0, 113, 227, 0.08); + color: var(--primary); + border-left-color: var(--primary); + font-weight: 500; +} + +.admin-content { + flex: 1; + padding: 2rem; + overflow-y: auto; +} + +.page { + display: none; + animation: fadeIn 0.3s; +} + +.page.active { + display: block; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.page-header h2 { + font-size: 1.75rem; + font-weight: 600; +} + +.button-group { + display: flex; + gap: 0.75rem; +} + +/* 统计卡片 */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: var(--card); + border-radius: 12px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: var(--shadow); + transition: all 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); +} + +.stat-icon { + font-size: 2.5rem; +} + +.stat-info { + flex: 1; +} + +.stat-value { + font-size: 2rem; + font-weight: 600; + color: var(--primary); + line-height: 1.2; +} + +.stat-label { + color: var(--text-secondary); + font-size: 0.9rem; + margin-top: 0.25rem; +} + +/* 表格 */ +.table-container { + background: var(--card); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow); +} + +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table thead { + background: var(--bg); +} + +.data-table th { + padding: 1rem; + text-align: left; + font-weight: 600; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.data-table td { + padding: 1rem; + border-top: 1px solid var(--border); +} + +.data-table tbody tr { + transition: background 0.2s; +} + +.data-table tbody tr:hover { + background: var(--bg); +} + +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; + background: var(--bg); + color: var(--text); +} + +.empty-state { + padding: 2rem; + text-align: center; + color: var(--text-secondary); +} + +/* 模态框 */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s; +} + +.modal.show { + display: flex; +} + +.modal-content { + background: var(--card); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-hover); + animation: fadeInUp 0.3s; +} + +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h3 { + font-size: 1.25rem; + font-weight: 600; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-secondary); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all 0.2s; +} + +.modal-close:hover { + background: var(--bg); + color: var(--text); +} + +.modal-body { + padding: 1.5rem; +} + +.modal-footer { + padding: 1.5rem; + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +/* 文件浏览器 */ +.file-browser { + background: var(--card); + border-radius: 12px; + padding: 1rem; + box-shadow: var(--shadow); +} + +.file-item { + padding: 1rem; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + transition: background 0.2s; +} + +.file-item:last-child { + border-bottom: none; +} + +.file-item:hover { + background: var(--bg); +} + +/* 编辑器 */ +.editor-container { + background: var(--card); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow); +} + +.code-editor { + width: 100%; + min-height: 500px; + padding: 1rem; + border: 1px solid var(--border); + border-radius: 8px; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + resize: vertical; + background: var(--card); + color: var(--text); +} + +.code-editor:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1); +} + +.config-controls { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + align-items: center; +} + +.config-controls select { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 1rem; +} + +/* 日志 */ +.logs-container { + background: var(--card); + border-radius: 12px; + padding: 1rem; + max-height: 600px; + overflow-y: auto; + box-shadow: var(--shadow); +} + +.logs-content { + font-family: 'Courier New', monospace; + font-size: 0.85rem; +} + +.log-entry { + padding: 0.75rem; + border-bottom: 1px solid var(--border); + display: flex; + gap: 1rem; + transition: background 0.2s; +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-entry:hover { + background: var(--bg); +} + +.log-time { + color: var(--text-secondary); + min-width: 150px; +} + +.log-level { + font-weight: 600; + min-width: 60px; +} + +.log-level.INFO { + color: var(--success); +} + +.log-level.WARN { + color: var(--warning); +} + +.log-level.ERROR { + color: var(--danger); +} + +.log-message { + flex: 1; +} + +/* 设置页面 */ +.settings-container { + max-width: 1000px; +} + +.settings-section { + margin-bottom: 2rem; +} + +.section-header { + margin-bottom: 1rem; +} + +.section-header h2 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.section-description { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.settings-card { + background: var(--card); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow); +} + +.database-info, +.system-stats, +.os-info { + display: grid; + gap: 1rem; +} + +.info-item, +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border); +} + +.info-item:last-child, +.stat-item:last-child { + border-bottom: none; +} + +.info-label, +.stat-label { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.info-value, +.stat-value { + font-weight: 500; + color: var(--text); +} + +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 500; +} + +.status-connected { + background: rgba(52, 199, 89, 0.1); + color: var(--success); +} + +.database-password, +.database-convert { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); +} + +.password-warning, +.convert-warning { + background: #fff3cd; + border: 1px solid var(--warning); + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + color: #856404; + font-size: 0.9rem; +} + +.password-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.password-form .form-group { + margin-bottom: 0; +} + +#password-result, +#convert-result { + margin-top: 1rem; + padding: 1rem; + border-radius: 8px; + display: none; +} + +#password-result.success, +#convert-result.success { + background: rgba(52, 199, 89, 0.1); + border: 1px solid var(--success); + color: var(--success); + display: block; +} + +#password-result.error, +#convert-result.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--danger); + color: var(--danger); + display: block; +} + +.convert-form { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.convert-form select { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 1rem; +} + +/* 响应式 */ +@media (max-width: 768px) { + .admin-layout { + flex-direction: column; + } + + .admin-sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--border); + } + + .sidebar-nav { + flex-direction: row; + overflow-x: auto; + } + + .nav-item { + white-space: nowrap; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } +} diff --git a/server/update/public/css/style.css b/server/update/public/css/style.css new file mode 100644 index 0000000..2b34bbf --- /dev/null +++ b/server/update/public/css/style.css @@ -0,0 +1,523 @@ +:root { + --background: #fbf7ef; + --foreground: #1c1917; + --card: rgba(255, 255, 255, 0.9); + --card-foreground: #1c1917; + --popover: #ffffff; + --popover-foreground: #1c1917; + --primary: #5f6f45; + --primary-foreground: #ffffff; + --secondary: #f5f5f4; + --secondary-foreground: #1c1917; + --muted: #f5f5f4; + --muted-foreground: #57534e; + --accent: #b56e45; + --accent-foreground: #ffffff; + --destructive: #b91c1c; + --destructive-foreground: #ffffff; + --border: #d6d3d1; + --input: #d6d3d1; + --ring: #7a8a67; + --radius: 0.75rem; + --shadow: 0 18px 40px rgba(28, 25, 23, 0.08); +} + +* { + box-sizing: border-box; +} + +body.shad-app { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + color: var(--foreground); + background: + radial-gradient(circle at top left, rgba(181, 110, 69, 0.1), transparent 20rem), + radial-gradient(circle at top right, rgba(122, 138, 103, 0.1), transparent 20rem), + var(--background); +} + +.site-header { + border-bottom: 1px solid rgba(214, 211, 209, 0.8); + padding: 34px 0 30px; +} + +.header-inner { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; +} + +.header-inner h1 { + margin: 10px 0 0; + max-width: 760px; + font-size: clamp(2rem, 4vw, 3rem); + line-height: 1.08; + letter-spacing: 0; +} + +.header-inner p { + margin: 14px 0 0; + max-width: 760px; + color: var(--muted-foreground); + line-height: 1.8; +} + +.header-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; +} + +.eyebrow { + color: var(--primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 26px; +} + +.summary-card { + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.72); + padding: 16px; +} + +.summary-card span { + color: var(--muted-foreground); + font-size: 0.86rem; +} + +.summary-card strong { + display: block; + margin-top: 6px; + font-size: 1.25rem; +} + +.summary-card p { + margin: 8px 0 0; + color: var(--muted-foreground); + line-height: 1.6; +} + +.section-title { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.section-title h2 { + margin: 6px 0 0; +} + +.section-title p { + margin: 0; + color: var(--muted-foreground); +} + +a { + color: inherit; + text-decoration: none; +} + +button { + font: inherit; +} + +code { + border: 1px solid var(--border); + border-radius: 0.375rem; + padding: 0.125rem 0.35rem; + background: var(--muted); + font-family: "Cascadia Code", "JetBrains Mono", monospace; +} + +.shad-container { + width: min(1180px, calc(100% - 32px)); + margin: 0 auto; +} + +.shad-hero { + border-bottom: 1px solid rgba(214, 211, 209, 0.8); +} + +.shad-hero__inner { + min-height: 280px; + display: flex; + align-items: center; + padding: 48px 0 40px; +} + +.shad-hero__copy { + max-width: 780px; +} + +.shad-hero__title { + margin: 16px 0 0; + font-size: clamp(2rem, 4vw, 3.25rem); + line-height: 1.05; + letter-spacing: 0; +} + +.shad-hero__text { + margin: 16px 0 0; + color: var(--muted-foreground); + line-height: 1.8; +} + +.shad-main { + padding: 28px 0 56px; +} + +.shad-section__head { + margin-bottom: 18px; +} + +.shad-section__head h2 { + margin: 0; + font-size: 1.35rem; +} + +.shad-section__head p { + margin: 6px 0 0; + color: var(--muted-foreground); +} + +.shad-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 16px; +} + +.shad-card, +.shad-dialog__content { + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--card); + color: var(--card-foreground); + box-shadow: var(--shadow); + backdrop-filter: blur(12px); +} + +.shad-card { + padding: 20px; +} + +.shad-card--state { + padding: 24px; +} + +.shad-card--danger { + border-color: rgba(185, 28, 28, 0.24); +} + +.shad-card--state h3, +.shad-product__title h3 { + margin: 0; +} + +.shad-card--state p, +.shad-product__title p, +.shad-history__item p { + margin: 8px 0 0; + color: var(--muted-foreground); + line-height: 1.7; +} + +.shad-product { + display: flex; + flex-direction: column; + gap: 16px; +} + +.shad-product__head { + display: flex; + gap: 14px; + align-items: flex-start; +} + +.shad-product__icon { + width: 52px; + height: 52px; + flex: 0 0 52px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.875rem; + background: rgba(22, 101, 52, 0.08); + color: var(--primary); +} + +.shad-product__icon svg { + width: 24px; + height: 24px; +} + +.shad-stack { + display: flex; + gap: 8px; +} + +.shad-stack--row { + flex-direction: row; +} + +.shad-stack--wrap { + flex-wrap: wrap; +} + +.shad-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid transparent; + background: rgba(22, 101, 52, 0.1); + color: var(--primary); + font-size: 0.82rem; + white-space: nowrap; +} + +.shad-badge--outline { + background: transparent; + border-color: var(--border); + color: var(--muted-foreground); +} + +.shad-badge--muted { + background: var(--muted); + color: var(--muted-foreground); +} + +.shad-badge--subtle { + background: rgba(217, 119, 6, 0.12); + color: #92400e; +} + +.shad-panel { + border: 1px solid rgba(214, 211, 209, 0.8); + border-radius: calc(var(--radius) - 0.15rem); + padding: 16px; + background: linear-gradient(180deg, rgba(28, 25, 23, 0.02), rgba(28, 25, 23, 0.04)); +} + +.shad-panel__main { + display: flex; + align-items: end; + justify-content: space-between; + gap: 12px; +} + +.shad-panel__main strong { + display: block; + margin-top: 4px; + font-size: 1.4rem; + color: var(--primary); +} + +.shad-meta-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-top: 14px; +} + +.shad-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.shad-muted { + color: var(--muted-foreground); + font-size: 0.86rem; +} + +.shad-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.shad-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.shad-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 0 16px; + border-radius: calc(var(--radius) - 0.2rem); + border: 1px solid transparent; + cursor: pointer; + transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; +} + +.shad-button:hover { + transform: translateY(-1px); +} + +.shad-button--primary { + background: var(--primary); + color: var(--primary-foreground); +} + +.shad-button--primary:hover { + background: #14532d; +} + +.shad-button--secondary { + background: var(--secondary); + color: var(--secondary-foreground); + border-color: var(--border); +} + +.shad-button--secondary:hover { + background: #ede9e7; +} + +.shad-dialog { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + padding: 16px; +} + +.shad-dialog.is-open { + display: flex; +} + +.shad-dialog__overlay { + position: absolute; + inset: 0; + background: rgba(28, 25, 23, 0.45); +} + +.shad-dialog__content { + position: relative; + width: min(680px, 100%); + max-height: min(80vh, 720px); + overflow: hidden; +} + +.shad-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 18px 20px; + border-bottom: 1px solid var(--border); +} + +.shad-dialog__header h3 { + margin: 0; +} + +.shad-dialog__body { + padding: 8px 20px 20px; + overflow: auto; +} + +.shad-icon-button { + width: 36px; + height: 36px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--popover); + color: var(--popover-foreground); + cursor: pointer; +} + +.shad-history { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.shad-history__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 0; + border-bottom: 1px solid rgba(214, 211, 209, 0.8); +} + +.shad-history__item:last-child { + border-bottom: 0; +} + +.shad-footer { + border-top: 1px solid rgba(214, 211, 209, 0.8); + background: rgba(255, 255, 255, 0.7); +} + +.shad-footer__inner { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 18px 0 24px; + color: var(--muted-foreground); + font-size: 0.9rem; +} + +@media (max-width: 760px) { + .header-inner, + .section-title { + align-items: flex-start; + flex-direction: column; + } + + .header-actions, + .summary-grid { + width: 100%; + } + + .summary-grid { + grid-template-columns: 1fr; + } + + .shad-hero__inner { + min-height: auto; + padding: 40px 0 32px; + } + + .shad-meta-grid { + grid-template-columns: 1fr; + } + + .shad-actions, + .shad-footer__inner, + .shad-panel__main, + .shad-history__item, + .shad-product__head { + flex-direction: column; + align-items: stretch; + } + + .shad-button { + width: 100%; + } +} diff --git a/server/update/public/fonts/MeiGanShouXieTi-2.ttf b/server/update/public/fonts/MeiGanShouXieTi-2.ttf new file mode 100644 index 0000000..e75e5b1 Binary files /dev/null and b/server/update/public/fonts/MeiGanShouXieTi-2.ttf differ diff --git a/server/update/public/fonts/QianTuBiFengShouXieTi-2.ttf b/server/update/public/fonts/QianTuBiFengShouXieTi-2.ttf new file mode 100644 index 0000000..494d139 Binary files /dev/null and b/server/update/public/fonts/QianTuBiFengShouXieTi-2.ttf differ diff --git a/server/update/public/fonts/YOzBS-2.otf b/server/update/public/fonts/YOzBS-2.otf new file mode 100644 index 0000000..cfc65ad Binary files /dev/null and b/server/update/public/fonts/YOzBS-2.otf differ diff --git a/server/update/public/fonts/YOz手写体 BS.zip b/server/update/public/fonts/YOz手写体 BS.zip new file mode 100644 index 0000000..4e1bfad Binary files /dev/null and b/server/update/public/fonts/YOz手写体 BS.zip differ diff --git a/server/update/public/fonts/千图笔锋手写体.zip b/server/update/public/fonts/千图笔锋手写体.zip new file mode 100644 index 0000000..61b2b49 Binary files /dev/null and b/server/update/public/fonts/千图笔锋手写体.zip differ diff --git a/server/update/public/fonts/梅干手写体.zip b/server/update/public/fonts/梅干手写体.zip new file mode 100644 index 0000000..ca46f65 Binary files /dev/null and b/server/update/public/fonts/梅干手写体.zip differ diff --git a/server/update/public/img/favicon.png b/server/update/public/img/favicon.png new file mode 100644 index 0000000..4a2c29e Binary files /dev/null and b/server/update/public/img/favicon.png differ diff --git a/server/update/public/js/admin.js b/server/update/public/js/admin.js new file mode 100644 index 0000000..b964abe --- /dev/null +++ b/server/update/public/js/admin.js @@ -0,0 +1,532 @@ +// 后台管理 JavaScript + +// 获取 Cookie +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +} + +// API 请求封装 +async function apiRequest(url, options = {}) { + const token = getCookie('token'); + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const response = await fetch(url, { + ...options, + headers, + credentials: 'include', + }); + + const data = await response.json(); + + if (!response.ok) { + // 如果是401,跳转到登录页 + if (response.status === 401) { + window.location.href = '/admin/login'; + return; + } + throw new Error(data.error || '请求失败'); + } + + return data; + } catch (error) { + console.error('API Error:', error); + throw error; + } +} + +// 加载当前用户信息 +async function loadCurrentUser() { + try { + const data = await apiRequest('/admin/me'); + const userEl = document.getElementById('current-user'); + if (userEl && data.user) { + userEl.textContent = `${data.user.username}${data.user.is_admin ? ' (管理员)' : ''}`; + } + } catch (error) { + console.error('Load user error:', error); + } +} + +// 登出 +document.getElementById('logout-btn')?.addEventListener('click', async () => { + try { + await apiRequest('/admin/logout', { method: 'POST' }); + document.cookie = 'token=; path=/; max-age=0'; + window.location.href = '/admin/login'; + } catch (error) { + console.error('Logout error:', error); + // 即使失败也跳转 + document.cookie = 'token=; path=/; max-age=0'; + window.location.href = '/admin/login'; + } +}); + +// 页面导航 +document.querySelectorAll('.nav-item').forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + const page = item.dataset.page; + + document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); + document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); + + item.classList.add('active'); + document.getElementById(`page-${page}`).classList.add('active'); + + // 加载对应页面数据 + if (page === 'routes') { + loadRoutes(); + } else if (page === 'logs') { + loadLogs(); + } else if (page === 'files') { + loadFiles(); + } else if (page === 'config') { + loadConfig(); + } else if (page === 'database') { + loadDatabaseInfo(); + } + }); +}); + +// 加载数据库信息 +async function loadDatabaseInfo() { + try { + const data = await apiRequest('/admin/api/database'); + const dbDetails = document.getElementById('db-details'); + const osDetails = document.getElementById('os-details'); + const dbConfig = document.getElementById('db-config'); + const passwordSection = document.getElementById('database-password-section'); + + dbDetails.innerHTML = ` +

类型: ${data.database.type}

+

状态: ${data.database.status}

+ ${data.database.file ? `

文件: ${data.database.file}

` : ''} + ${data.database.cgo_support !== undefined ? `

CGO 支持: ${data.database.cgo_support ? '是' : '否'}

` : ''} + `; + + osDetails.innerHTML = ` +

操作系统: ${data.os.os}

+

架构: ${data.os.arch}

+ `; + + // 加载数据库配置 + try { + const configData = await apiRequest('/admin/api/database/config'); + dbConfig.innerHTML = ` +

类型: ${configData.config.type}

+

主机: ${configData.config.host}

+

端口: ${configData.config.port}

+

用户: ${configData.config.user}

+

数据库: ${configData.config.database}

+

已设置密码: ${configData.config.has_password ? '是' : '否'}

+ `; + + // 如果是 MySQL,显示密码修改界面 + if (configData.config.type === 'mysql') { + passwordSection.style.display = 'block'; + } else { + passwordSection.style.display = 'none'; + } + } catch (error) { + console.error('Load database config error:', error); + dbConfig.innerHTML = '

无法加载配置信息

'; + } + } catch (error) { + console.error('Load database info error:', error); + } +} + +// 刷新数据库信息 +document.getElementById('refresh-db-btn')?.addEventListener('click', loadDatabaseInfo); + +// 转换数据库 +document.getElementById('convert-db-btn')?.addEventListener('click', async () => { + const targetType = document.getElementById('target-db-type').value; + + if (!confirm(`确定要转换数据库类型吗?\n\n目标类型: ${targetType.toUpperCase()}\n\n此操作会导出当前数据并导入到新数据库。请确保已备份数据!`)) { + return; + } + + const resultEl = document.getElementById('convert-result'); + resultEl.className = ''; + resultEl.textContent = '正在转换...'; + resultEl.style.display = 'block'; + + try { + const result = await apiRequest('/admin/api/database/convert', { + method: 'POST', + body: JSON.stringify({ + target_type: targetType, + }), + }); + + resultEl.className = 'success'; + resultEl.textContent = result.message || '数据库转换成功!'; + loadDatabaseInfo(); + loadLogs(); + } catch (error) { + resultEl.className = 'error'; + resultEl.textContent = '转换失败: ' + error.message; + } +}); + +// 更新数据库密码 +document.getElementById('password-form')?.addEventListener('submit', async (e) => { + e.preventDefault(); + + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + const confirmPassword = document.getElementById('confirm-password').value; + + if (newPassword !== confirmPassword) { + alert('新密码和确认密码不一致!'); + return; + } + + if (!confirm('确定要更新数据库 root 密码吗?\n\n更新后需要修改环境变量 DB_PASSWORD 并重启服务器!')) { + return; + } + + const resultEl = document.getElementById('password-result'); + resultEl.className = ''; + resultEl.textContent = '正在更新密码...'; + resultEl.style.display = 'block'; + + try { + const result = await apiRequest('/admin/api/database/password', { + method: 'POST', + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + confirm_password: confirmPassword, + }), + }); + + resultEl.className = 'success'; + resultEl.textContent = result.message || '密码更新成功!'; + + // 清空表单 + document.getElementById('password-form').reset(); + + loadLogs(); + } catch (error) { + resultEl.className = 'error'; + resultEl.textContent = '更新失败: ' + error.message; + } +}); + +// 加载系统信息 +async function loadSystemInfo() { + try { + const data = await apiRequest('/admin/api/system'); + document.getElementById('stat-users').textContent = data.users; + document.getElementById('stat-routes').textContent = data.routes; + document.getElementById('stat-logs').textContent = data.logs; + document.getElementById('stat-time').textContent = data.server_time; + } catch (error) { + console.error('Load system info error:', error); + } +} + +// 加载路由 +async function loadRoutes() { + try { + const data = await apiRequest('/admin/api/routes'); + const tbody = document.getElementById('routes-table-body'); + if (!tbody) return; + + tbody.innerHTML = ''; + + if (data.routes && data.routes.length > 0) { + data.routes.forEach(route => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${route.id} + ${route.method} + ${route.path} + ${route.type} + ${route.description || '-'} + ${route.is_active ? '✅' : '❌'} + + + + + `; + tbody.appendChild(tr); + }); + } else { + tbody.innerHTML = '暂无路由'; + } + } catch (error) { + console.error('Load routes error:', error); + const tbody = document.getElementById('routes-table-body'); + if (tbody) { + tbody.innerHTML = '加载失败: ' + error.message + ''; + } + } +} + +// 添加路由 +document.getElementById('add-route-btn')?.addEventListener('click', () => { + document.getElementById('route-modal-title').textContent = '添加路由'; + document.getElementById('route-form').reset(); + document.getElementById('route-id').value = ''; + document.getElementById('route-modal').classList.add('show'); +}); + +// 编辑路由 +window.editRoute = async function(id) { + try { + const data = await apiRequest('/admin/api/routes'); + const route = data.routes.find(r => r.id === id); + + if (route) { + document.getElementById('route-modal-title').textContent = '编辑路由'; + document.getElementById('route-id').value = route.id; + document.getElementById('route-method').value = route.method; + document.getElementById('route-path').value = route.path; + document.getElementById('route-type').value = route.type; + document.getElementById('route-handler').value = route.handler; + document.getElementById('route-description').value = route.description || ''; + document.getElementById('route-active').checked = route.is_active; + document.getElementById('route-order').value = route.order; + document.getElementById('route-modal').classList.add('show'); + } + } catch (error) { + console.error('Edit route error:', error); + } +}; + +// 删除路由 +window.deleteRoute = async function(id) { + if (!confirm('确定要删除这个路由吗?')) return; + + try { + await apiRequest(`/admin/api/routes/${id}`, { method: 'DELETE' }); + loadRoutes(); + } catch (error) { + alert('删除失败: ' + error.message); + } +}; + +// 保存路由 +document.getElementById('route-save-btn')?.addEventListener('click', async () => { + const id = document.getElementById('route-id').value; + const routeData = { + method: document.getElementById('route-method').value, + path: document.getElementById('route-path').value, + type: document.getElementById('route-type').value, + handler: document.getElementById('route-handler').value, + description: document.getElementById('route-description').value, + is_active: document.getElementById('route-active').checked, + order: parseInt(document.getElementById('route-order').value) || 0, + }; + + try { + if (id) { + await apiRequest(`/admin/api/routes/${id}`, { + method: 'PUT', + body: JSON.stringify(routeData), + }); + } else { + await apiRequest('/admin/api/routes', { + method: 'POST', + body: JSON.stringify(routeData), + }); + } + document.getElementById('route-modal').classList.remove('show'); + loadRoutes(); + } catch (error) { + alert('保存失败: ' + error.message); + } +}); + +// 关闭模态框 +document.querySelectorAll('.modal-close, #route-cancel-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.getElementById('route-modal').classList.remove('show'); + }); +}); + +// 加载日志 +async function loadLogs() { + try { + const data = await apiRequest('/admin/api/logs?limit=100'); + const logsEl = document.getElementById('logs-content'); + if (!logsEl) return; + + logsEl.innerHTML = ''; + + if (data.logs && data.logs.length > 0) { + data.logs.forEach(log => { + const entry = document.createElement('div'); + entry.className = 'log-entry'; + entry.innerHTML = ` + ${log.time || ''} + ${log.level || 'INFO'} + ${log.message || ''} + `; + logsEl.appendChild(entry); + }); + + logsEl.scrollTop = logsEl.scrollHeight; + } else { + logsEl.innerHTML = '
暂无日志
'; + } + } catch (error) { + console.error('Load logs error:', error); + const logsEl = document.getElementById('logs-content'); + if (logsEl) { + logsEl.innerHTML = '
加载失败: ' + error.message + '
'; + } + } +} + +// 刷新日志 +document.getElementById('refresh-logs-btn')?.addEventListener('click', loadLogs); + +// 清空日志 +document.getElementById('clear-logs-btn')?.addEventListener('click', () => { + const logsEl = document.getElementById('logs-content'); + if (logsEl) { + logsEl.innerHTML = '
日志已清空
'; + } +}); + +// 加载文件 +async function loadFiles() { + const fileBrowser = document.getElementById('file-browser'); + if (!fileBrowser) return; + + try { + const data = await apiRequest('/admin/api/files?dir=public/downloads'); + fileBrowser.innerHTML = ''; + + if (data.files && data.files.length > 0) { + data.files.forEach(file => { + const item = document.createElement('div'); + item.className = 'file-item'; + item.innerHTML = ` +
+ ${file.name} + + ${file.is_dir ? '📁 目录' : `📄 ${formatBytes(file.size)}`} • ${file.mod_time || ''} + +
+ ${!file.is_dir ? `` : ''} + `; + fileBrowser.appendChild(item); + }); + } else { + fileBrowser.innerHTML = '
暂无文件
'; + } + } catch (error) { + console.error('Load files error:', error); + fileBrowser.innerHTML = '
加载失败: ' + error.message + '
'; + } +} + +document.getElementById('refresh-files-btn')?.addEventListener('click', loadFiles); + +// 读取文件 +window.readFile = async function(filename) { + const fullPath = `public/downloads/${filename}`; + + try { + const data = await apiRequest(`/admin/api/file?path=${encodeURIComponent(fullPath)}`); + // 显示文件内容在模态框中或新窗口 + const content = data.content.substring(0, 5000) + (data.content.length > 5000 ? '\n\n... (内容过长,已截断)' : ''); + alert('文件内容:\n\n' + content); + } catch (error) { + alert('读取文件失败: ' + error.message); + } +}; + +// 加载配置 +async function loadConfig() { + const file = document.getElementById('config-select').value; + try { + const response = await fetch(`/public/${file}`); + const data = await response.json(); + document.getElementById('config-editor').value = JSON.stringify(data, null, 2); + } catch (error) { + console.error('Load config error:', error); + } +} + +document.getElementById('load-config-btn')?.addEventListener('click', loadConfig); + +// 保存配置 +document.getElementById('save-config-btn')?.addEventListener('click', async () => { + const file = document.getElementById('config-select').value; + const content = document.getElementById('config-editor').value; + + try { + const jsonData = JSON.parse(content); + const result = await apiRequest('/admin/api/config', { + method: 'PUT', + body: JSON.stringify({ + file: file, + content: jsonData, + reload: false, // 仅保存 + }), + }); + alert(result.message || '配置保存成功!'); + } catch (error) { + alert('保存失败: ' + error.message); + } +}); + +// 保存并立即加载配置 +document.getElementById('save-reload-config-btn')?.addEventListener('click', async () => { + const file = document.getElementById('config-select').value; + const content = document.getElementById('config-editor').value; + + try { + const jsonData = JSON.parse(content); + const result = await apiRequest('/admin/api/config', { + method: 'PUT', + body: JSON.stringify({ + file: file, + content: jsonData, + reload: true, // 保存并立即加载 + }), + }); + alert(result.message || '配置已保存并立即生效!'); + // 刷新日志以显示加载信息 + if (document.getElementById('page-logs')?.classList.contains('active')) { + loadLogs(); + } + } catch (error) { + alert('保存失败: ' + error.message); + } +}); + +// 工具函数 +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} + +// 页面加载时初始化 +document.addEventListener('DOMContentLoaded', () => { + loadCurrentUser(); + loadSystemInfo(); + + // 定期更新系统信息 + setInterval(loadSystemInfo, 30000); // 每30秒更新一次 +}); diff --git a/server/update/public/js/auth.js b/server/update/public/js/auth.js new file mode 100644 index 0000000..6b08e13 --- /dev/null +++ b/server/update/public/js/auth.js @@ -0,0 +1,193 @@ +// 认证相关 JavaScript + +// 检查是否已登录 +async function checkAuth() { + try { + const token = getCookie('token'); + if (!token) { + return false; + } + + const response = await fetch('/admin/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + // 已登录,跳转到管理页面 + window.location.href = '/admin'; + return true; + } + } catch (error) { + console.error('Auth check error:', error); + } + return false; +} + +// 获取 Cookie +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +} + +// API 请求封装 +async function apiRequest(url, options = {}) { + const token = getCookie('token'); + const headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(url, { + ...options, + headers + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: '请求失败' })); + throw new Error(error.error || '请求失败'); + } + + return await response.json(); +} + +// 显示错误消息 +function showError(message) { + const errorEl = document.getElementById('auth-error'); + if (errorEl) { + errorEl.textContent = message; + errorEl.classList.add('show'); + setTimeout(() => { + errorEl.classList.remove('show'); + }, 5000); + } +} + +// 登录表单处理 +const loginForm = document.getElementById('login-form'); +if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(loginForm); + const data = { + username: formData.get('username'), + password: formData.get('password') + }; + + const submitBtn = loginForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.disabled = true; + submitBtn.innerHTML = '登录中...'; + + try { + const result = await apiRequest('/admin/login', { + method: 'POST', + body: JSON.stringify(data) + }); + + // 设置 token cookie + document.cookie = `token=${result.token}; path=/; max-age=86400; SameSite=Lax`; + + // 跳转到管理页面 + window.location.href = '/admin'; + } catch (error) { + showError(error.message || '登录失败'); + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + }); +} + +// 注册表单处理 +const registerForm = document.getElementById('register-form'); +if (registerForm) { + // 密码强度检测 + const passwordInput = document.getElementById('password'); + const passwordStrength = document.getElementById('password-strength'); + + if (passwordInput && passwordStrength) { + passwordInput.addEventListener('input', (e) => { + const password = e.target.value; + let strength = 'weak'; + + if (password.length >= 8) { + const hasUpper = /[A-Z]/.test(password); + const hasLower = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecial = /[^A-Za-z0-9]/.test(password); + + const score = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length; + + if (score >= 3) { + strength = 'strong'; + } else if (score >= 2) { + strength = 'medium'; + } + } + + passwordStrength.className = `password-strength ${strength}`; + }); + } + + registerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const password = document.getElementById('password').value; + const confirmPassword = document.getElementById('confirm-password').value; + + if (password !== confirmPassword) { + showError('两次输入的密码不一致'); + return; + } + + const formData = new FormData(registerForm); + const data = { + username: formData.get('username'), + email: formData.get('email'), + password: password + }; + + const submitBtn = registerForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.disabled = true; + submitBtn.innerHTML = '注册中...'; + + try { + const result = await apiRequest('/admin/register', { + method: 'POST', + body: JSON.stringify(data) + }); + + // 设置 token cookie + document.cookie = `token=${result.token}; path=/; max-age=86400; SameSite=Lax`; + + // 跳转到管理页面 + window.location.href = '/admin'; + } catch (error) { + showError(error.message || '注册失败'); + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + }); +} + +// 页面加载时检查认证状态 +document.addEventListener('DOMContentLoaded', () => { + // 如果已经在登录/注册页面,不需要检查 + if (window.location.pathname.includes('/admin/login') || + window.location.pathname.includes('/admin/register')) { + return; + } + + // 检查是否已登录 + checkAuth(); +}); diff --git a/server/update/public/js/settings.js b/server/update/public/js/settings.js new file mode 100644 index 0000000..c1a6c01 --- /dev/null +++ b/server/update/public/js/settings.js @@ -0,0 +1,128 @@ +// 设置页面 JavaScript + +// 加载数据库信息 +async function loadDatabaseInfo() { + try { + const data = await apiRequest('/admin/api/database'); + const configData = await apiRequest('/admin/api/database/config'); + + document.getElementById('db-type').textContent = data.database.type; + document.getElementById('db-status').textContent = data.database.status; + document.getElementById('db-status').className = 'info-value status-badge status-connected'; + + if (data.database.file) { + document.getElementById('db-file-item').style.display = 'flex'; + document.getElementById('db-file').textContent = data.database.file; + } + + // 如果是 MySQL,显示密码修改界面 + if (configData.config.type === 'mysql') { + document.getElementById('database-password-section').style.display = 'block'; + } + + // 操作系统信息 + document.getElementById('os-type').textContent = data.os.os; + document.getElementById('os-arch').textContent = data.os.arch; + } catch (error) { + console.error('Load database info error:', error); + } +} + +// 加载系统信息 +async function loadSystemInfo() { + try { + const data = await apiRequest('/admin/api/system'); + document.getElementById('stat-users').textContent = data.users; + document.getElementById('stat-routes').textContent = data.routes; + document.getElementById('stat-logs').textContent = data.logs; + document.getElementById('stat-time').textContent = data.server_time; + } catch (error) { + console.error('Load system info error:', error); + } +} + +// 更新数据库密码 +const passwordForm = document.getElementById('password-form'); +if (passwordForm) { + passwordForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const currentPassword = document.getElementById('current-password').value; + const newPassword = document.getElementById('new-password').value; + const confirmPassword = document.getElementById('confirm-password').value; + + if (newPassword !== confirmPassword) { + alert('新密码和确认密码不一致!'); + return; + } + + if (!confirm('确定要更新数据库 root 密码吗?\n\n更新后需要修改环境变量 DB_PASSWORD 并重启服务器!')) { + return; + } + + const resultEl = document.getElementById('password-result'); + resultEl.className = ''; + resultEl.textContent = '正在更新密码...'; + resultEl.style.display = 'block'; + + try { + const result = await apiRequest('/admin/api/database/password', { + method: 'POST', + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + confirm_password: confirmPassword, + }), + }); + + resultEl.className = 'success'; + resultEl.textContent = result.message || '密码更新成功!'; + passwordForm.reset(); + } catch (error) { + resultEl.className = 'error'; + resultEl.textContent = '更新失败: ' + error.message; + } + }); +} + +// 转换数据库 +const convertDbBtn = document.getElementById('convert-db-btn'); +if (convertDbBtn) { + convertDbBtn.addEventListener('click', async () => { + const targetType = document.getElementById('target-db-type').value; + + if (!confirm(`确定要转换数据库类型吗?\n\n目标类型: ${targetType.toUpperCase()}\n\n此操作会导出当前数据并导入到新数据库。请确保已备份数据!`)) { + return; + } + + const resultEl = document.getElementById('convert-result'); + resultEl.className = ''; + resultEl.textContent = '正在转换...'; + resultEl.style.display = 'block'; + + try { + const result = await apiRequest('/admin/api/database/convert', { + method: 'POST', + body: JSON.stringify({ + target_type: targetType, + }), + }); + + resultEl.className = 'success'; + resultEl.textContent = result.message || '数据库转换成功!'; + loadDatabaseInfo(); + } catch (error) { + resultEl.className = 'error'; + resultEl.textContent = '转换失败: ' + error.message; + } + }); +} + +// 页面加载时初始化 +document.addEventListener('DOMContentLoaded', () => { + loadDatabaseInfo(); + loadSystemInfo(); + + // 定期更新系统信息 + setInterval(loadSystemInfo, 30000); // 每30秒更新一次 +}); diff --git a/server/update/public/lang/en-US.json b/server/update/public/lang/en-US.json new file mode 100644 index 0000000..fa086c8 --- /dev/null +++ b/server/update/public/lang/en-US.json @@ -0,0 +1,71 @@ +{ + "comment": "English Language Pack", + "nav.home": "Home", + "nav.toolbox": "Toolbox", + "nav.logs": "Logs", + "nav.settings": "Settings", + "home.greeting.morning": "Good morning, have a wonderful day.", + "home.greeting.noon": "Good afternoon, time for a break.", + "home.greeting.afternoon": "Good afternoon, keep up the good work.", + "home.greeting.evening": "Good evening, time to relax.", + "home.greeting.night": "It's late, please rest.", + "home.welcome": "Welcome to YMhut Box, wish you a great day.", + "home.announcement": "Announcement", + "home.announcement.failed": "Failed to load announcement...", + "home.announcement.viewFull": "View Full Announcement", + "home.search.placeholder": "Smart Search: Enter query...", + "home.search.button": "Search", + "home.search.disabled": "Smart Search is currently disabled", + "home.search.results.stats": "Found about {count} results (in {time}ms)", + "home.search.results.viewFull": "View Full Results (incl. Advanced Options)", + "home.search.results.empty.title": "No results found for \"{query}\"", + "home.search.results.empty.sub": "Please try different keywords.", + "home.search.failed.title": "Search Failed", + "home.updates": "Update Log", + "home.updates.close": "Close", + "home.updates.modal.title": "Full Announcement", + "tool.smartSearch.name": "Smart Search", + "tool.smartSearch.desc": "AI aggregated search for high-quality results", + "common.loading": "Loading...", + "common.search": "Search", + "common.backToToolbox": "Back to Toolbox", + "common.options": "Advanced Options", + "common.error": "Error", + "common.loading.tool": "Loading tool module...", + "common.notification.title.success": "Success", + "common.notification.title.error": "Error", + "common.notification.title.info": "Info", + "settings.appearance": "Appearance", + "settings.updates": "Update Management", + "settings.about": "About", + "settings.status.monitor": "Status Monitor", + "settings.status.cpu": "CPU", + "settings.status.mem": "Memory", + "settings.status.gpu": "GPU", + "settings.status.uptime": "Uptime", + "settings.appearance.title": "Appearance Settings", + "settings.appearance.theme": "Theme", + "settings.appearance.language": "Language", + "settings.appearance.language.auto": "Auto (Default)", + "settings.appearance.language.zh-CN": "简体中文", + "settings.appearance.language.en-US": "English", + "settings.appearance.language.restartMsg": "Language change will take effect after restart.", + "settings.appearance.bg": "Custom Background", + "settings.appearance.bg.select": "Select", + "settings.appearance.bg.clear": "Clear", + "settings.appearance.bg.opacity": "Background Opacity", + "settings.appearance.card.opacity": "Card Opacity", + "settings.traffic.title": "Traffic Statistics", + "settings.traffic.total": "Total Usage", + "settings.traffic.chart.empty.title": "No traffic history", + "settings.traffic.chart.empty.sub": "Data will be recorded starting today.", + "settings.update.title": "Update Management", + "settings.update.checkBtn": "Check for Updates", + "settings.update.checking": "Checking...", + "settings.update.checkDefault": "Click button to check for new version", + "settings.about.title": "About & Environment", + "settings.about.version": "Current Version", + "settings.about.developer": "Developer", + "settings.about.moreInfo": "More Info & Credits", + "settings.about.env.title": "Installed Environments" +} \ No newline at end of file diff --git a/server/update/public/lang/zh-CN.json b/server/update/public/lang/zh-CN.json new file mode 100644 index 0000000..c8c4351 --- /dev/null +++ b/server/update/public/lang/zh-CN.json @@ -0,0 +1,71 @@ +{ + "comment": "简体中文语言包", + "nav.home": "主页", + "nav.toolbox": "工具箱", + "nav.logs": "日志", + "nav.settings": "设置", + "home.greeting.morning": "早上好, 新的一天元气满满", + "home.greeting.noon": "中午好, 午休时间到了", + "home.greeting.afternoon": "下午好, 继续努力吧", + "home.greeting.evening": "晚上好, 放松一下吧", + "home.greeting.night": "凌晨了, 注意休息哦", + "home.welcome": "欢迎使用 YMhut Box, 愿你拥有美好的一天。", + "home.announcement": "公告", + "home.announcement.failed": "公告加载失败...", + "home.announcement.viewFull": "查看完整公告", + "home.search.placeholder": "智能搜索:输入查询内容...", + "home.search.button": "搜索", + "home.search.disabled": "智能搜索工具当前不可用", + "home.search.results.stats": "找到约 {count} 条结果 (耗时 {time}ms)", + "home.search.results.viewFull": "查看完整结果 (含高级选项)", + "home.search.results.empty.title": "未找到关于 \"{query}\" 的结果", + "home.search.results.empty.sub": "请尝试更换关键词。", + "home.search.failed.title": "搜索失败", + "home.updates": "更新日志", + "home.updates.close": "关闭", + "home.updates.modal.title": "完整公告", + "tool.smartSearch.name": "智能搜索", + "tool.smartSearch.desc": "AI 聚合搜索,获取高质量结果", + "common.loading": "加载中...", + "common.search": "搜索", + "common.backToToolbox": "返回工具箱", + "common.options": "高级选项", + "common.error": "错误", + "common.loading.tool": "正在初始化工具模块...", + "common.notification.title.success": "成功", + "common.notification.title.error": "错误", + "common.notification.title.info": "提示", + "settings.appearance": "外观", + "settings.updates": "更新管理", + "settings.about": "关于", + "settings.status.monitor": "状态监控", + "settings.status.cpu": "CPU", + "settings.status.mem": "内存", + "settings.status.gpu": "GPU", + "settings.status.uptime": "运行时长", + "settings.appearance.title": "外观设置", + "settings.appearance.theme": "界面主题", + "settings.appearance.language": "语言 (Language)", + "settings.appearance.language.auto": "自动 (Auto)", + "settings.appearance.language.zh-CN": "简体中文", + "settings.appearance.language.en-US": "English", + "settings.appearance.language.restartMsg": "语言设置将在重启后生效。", + "settings.appearance.bg": "自定义背景", + "settings.appearance.bg.select": "选择", + "settings.appearance.bg.clear": "清除", + "settings.appearance.bg.opacity": "背景透明度", + "settings.appearance.card.opacity": "卡片透明度", + "settings.traffic.title": "流量统计", + "settings.traffic.total": "累计使用流量", + "settings.traffic.chart.empty.title": "暂无历史流量数据", + "settings.traffic.chart.empty.sub": "数据将从今天开始记录", + "settings.update.title": "更新管理", + "settings.update.checkBtn": "检查更新", + "settings.update.checking": "正在检查...", + "settings.update.checkDefault": "点击按钮检查新版本", + "settings.about.title": "关于与软件环境", + "settings.about.version": "当前版本", + "settings.about.developer": "开发者", + "settings.about.moreInfo": "更多信息与鸣谢", + "settings.about.env.title": "已安装的开发环境" +} \ No newline at end of file diff --git a/server/update/public/media-types.json b/server/update/public/media-types.json new file mode 100644 index 0000000..b694e37 --- /dev/null +++ b/server/update/public/media-types.json @@ -0,0 +1,175 @@ +{ + "categories": [ + { + "enabled": true, + "icon": "fas fa-image", + "id": "image", + "layout": { + "aspect_ratio": "16:9", + "columns": 1, + "show_preview": true, + "transition_effect": "fade" + }, + "name": "随机图片", + "subcategories": [ + { + "api_url": "https://xjj.ymhut.bid/xjj", + "description": "精选小姐姐图片", + "downloadable": true, + "id": "xjj", + "name": "小姐姐", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://pic2.zhimg.com/v2-379be37e0b4d372aa60046f9ce771f12_r.jpg" + }, + { + "api_url": "https://v2.xxapi.cn/api/baisi?return=302", + "description": "随机白丝图片", + "downloadable": true, + "id": "baisi", + "name": "白丝", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://n.sinaimg.cn/sinacn10112/760/w640h920/20200126/4b00-innckcf8208822.jpg" + }, + { + "api_url": "https://v2.xxapi.cn/api/heisi?return=302", + "description": "随机黑丝图片", + "downloadable": true, + "id": "heisi", + "name": "黑丝", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://img-baofun.zhhainiao.com/pcwallpaper_ugc_mobile/static/6902725194a8c081767ee82373d3b017.jpeg" + }, + { + "api_url": "https://api.pearapi.ai/api/beautifulgirl?type=image", + "description": "三坑少女图(包含动漫、漫画、游戏)", + "downloadable": true, + "id": "third_girl", + "name": "三坑少女(4K)", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://www.sgpjbg.com/FileUpload/News/c358f121-6683-490b-beed-6debb44e4824.jpg" + }, + { + "api_url": "https://apii.ctose.cn/api/cy/api/", + "description": "miku的随机图", + "downloadable": true, + "id": "miku", + "name": "初音未来", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://apii.ctose.cn/api/cy/api/" + }, + { + "api_url": "https://api.suyanw.cn/api/mao.php", + "description": "猫羽雫的随机图", + "downloadable": true, + "id": "猫羽雫", + "name": "猫羽雫", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://api.suyanw.cn/api/mao.php" + }, + { + "api_url": "https://api.suyanw.cn/api/scenery.php", + "description": "随机高清壁纸", + "downloadable": true, + "id": "wappller", + "name": "高清壁纸", + "refresh_interval": 30, + "supported_formats": [ + "jpg", + "jpeg", + "png", + "webp" + ], + "thumbnail_url": "https://api.suyanw.cn/api/scenery.php" + } + ] + }, + { + "enabled": true, + "icon": "fas fa-video", + "id": "video", + "layout": { + "aspect_ratio": "16:9", + "auto_play": false, + "columns": 1, + "show_preview": true, + "transition_effect": "slide" + }, + "name": "随机视频", + "subcategories": [ + { + "api_url": "https://dh.lt6.ltd/xjj/video.php", + "description": "随机风格类型视频", + "downloadable": true, + "id": "radom_xjj_leixing", + "name": "小姐姐不同风格视频", + "refresh_interval": 60, + "supported_formats": [ + "mp4", + "webm" + ], + "thumbnail_url": "https://n.sinaimg.cn/sinacn19/176/w888h888/20181119/0c26-hmhhnqt1050818.jpg" + }, + { + "api_url": "https://api.mmp.cc/api/miss?type=mp4", + "description": "随机风格小姐姐的视频", + "downloadable": true, + "id": "radom_xjj_short", + "name": "短视频", + "refresh_interval": 60, + "supported_formats": [ + "mp4", + "webm" + ], + "thumbnail_url": "https://weather-real.oss-cn-shanghai.aliyuncs.com/weather/2025-06-17/1750091559255t7FNOX.jpg" + } + ] + } + ], + "last_updated": "2025-09-9T17:45:00Z", + "layout_version": "1.0.6", + "ui_config": { + "animations": { + "duration": 300, + "transition_effect": "fade" + }, + "dark_mode": false, + "default_view": "grid", + "show_thumbnails": false + } +} \ No newline at end of file diff --git a/server/update/public/modules.json b/server/update/public/modules.json new file mode 100644 index 0000000..b3a9360 --- /dev/null +++ b/server/update/public/modules.json @@ -0,0 +1,4 @@ +{ + "manifest_version": 1, + "modules": [] +} diff --git a/server/update/public/tool-status.json b/server/update/public/tool-status.json new file mode 100644 index 0000000..82ae451 --- /dev/null +++ b/server/update/public/tool-status.json @@ -0,0 +1,56 @@ +{ + "ai-translation": { + "enabled": true, + "message": "" + }, + "baidu-hot": { + "enabled": true, + "message": "百度热榜接口正在维护,预计短时间内不会恢复。" + }, + "bili-hot-ranking": { + "enabled": true, + "message": "B站热搜接口正在维护,预计短时间内不会恢复。" + }, + "comment": "工具状态控制文件。 'enabled: false' 将禁用该工具。", + "comment_screening_room": "随机放映室使用 '分类ID.子分类ID' 作为键", + "dns-query": { + "enabled": true, + "message": "" + }, + "image.baisi": { + "enabled": true, + "message": "" + }, + "image.heisi": { + "enabled": true, + "message": "" + }, + "image.wappller": { + "enabled": true, + "message": "高清壁纸 暂时下线,请先浏览其他分类。" + }, + "image.xjj": { + "enabled": true, + "message": "" + }, + "image.猫羽雫": { + "enabled": true, + "message": "猫羽雫 暂时下线,请先浏览其他分类。" + }, + "ip-info": { + "enabled": true, + "message": "" + }, + "ip-query": { + "enabled": true, + "message": "" + }, + "smart-search": { + "enabled": true, + "message": "" + }, + "video.radom_xjj_leixing": { + "enabled": true, + "message": "" + } +} \ No newline at end of file diff --git a/server/update/public/update-info.json b/server/update/public/update-info.json new file mode 100644 index 0000000..f6d454b --- /dev/null +++ b/server/update/public/update-info.json @@ -0,0 +1,95 @@ +{ + "api_keys": { + "uapipro": "" + }, + "app_version": "2.0.6.2", + "build": "2", + "channel": "stable", + "title": "YMhut Box 2.0.6.2", + "message": "本版本重点修复覆盖安装后白屏退出、用户目录 runtime 占用、语言包膨胀和设置页初始化问题,并继续完善 WinUI 3 工具型工作台体验。", + "message_md": "# YMhut Box 2.0.6.2\n\n本版本继续收尾 WinUI 3 工具型工作台:修复覆盖安装/直启稳定性、用户目录 runtime 残留、自检结果页卡顿、排行榜/资讯显示、中文日志和设置页初始化问题,并新增安装器输出框、Markdown 公告、媒体播放器与随机放映室增强、价格/指标图表化展示。", + "release_notes": "修复 EXE/latest 直启和覆盖安装后因语言资源布局导致的白屏退出;发布布局改为纯 lang\\zh-CN 与 lang\\en-US,移除多余语言包和旧 resources\\lang 压缩依赖;启动与自检链路禁止在用户数据目录保存 Runtime/runtime/Runtimes/runtimes 等运行时副本,旧残留会在启动和安装时清理;完善启动自检、安装完整性检查、服务状态结果页、工具箱与工具详情布局、结果/原始输出渲染、排行榜/资讯结构化显示、设置页控制中心和系统概况实时图表;修复设置页初始化失败、中文模式英文漏出、部分日志英文展示、天气胶囊图标缺失以及关闭确认记住选择等问题。", + "release_notes_md": "## Bug 修复\n\n- 修复 EXE/latest 直启和覆盖安装后因语言资源布局导致的白屏退出,失败时写入清晰日志并显示可读错误。\n- 杜绝用户数据目录生成 Runtime/runtime/Runtimes/runtimes 或完整程序 payload 副本,旧残留会在启动和安装时清理。\n- 修复自检结果页加载大量历史明细时卡顿或短暂无响应的问题,改为确认后摘要优先、明细按需加载。\n- 修复中文模式下部分日志仍显示英文的问题,错误码、HTTP 状态和反馈状态仍保留必要原文。\n- 修复排行榜、热榜和资讯类工具被“已隐藏远程地址,仅展示脱敏来源名称”提示干扰后无法生成卡片的问题,同时继续隐藏远程 URL。\n- 修复设置页初始化失败、关闭确认记住选择、天气胶囊部分状态缺少动画图标等问题。\n\n## 支持增强\n\n- 发布布局统一为纯 `lang\\zh-CN` / `lang\\en-US`,移除多余语言包、根目录 culture 目录和旧 `resources\\lang` 压缩依赖。\n- 客户端公告、首页公告、关于页更新弹窗和更新日志弹窗支持 Markdown 标题、列表、表格、代码与链接,解析失败时回退纯文本。\n- 随机放映室支持远程媒体重定向解析,并为图片、视频、音频加载提供进度提示。\n- 数据类工具增强价格/指标结果展示,黄金价格等结果可同时显示摘要、表格和趋势折线图。\n\n## 新增能力\n\n- 安装引导程序在提取文件阶段新增只读输出框,展示旧版本清理、payload 提取、依赖检查和安装收尾进度。\n- 新增启动/安装完整性自检结果入口,服务状态页可确认后查看结果、复制摘要和导出 JSON。\n- 媒体播放器补齐播放列表、常用播放控制、倍速、音量、全屏、图片/视频/音频混播和常见系统解码格式入口。\n- 随机放映室迁移旧版展示思路,改为图片、视频、音频三段式远程媒体浏览。\n\n## 体验重构\n\n- 工具箱与工具详情页继续向高密度 WinUI 工具工作台收束,结果区固定提供“结果”和“原始输出”。\n- 设置控制中心增加分页滚动提示,系统概况图表补齐网格、坐标和实时信息。\n- 主题继续参考 Microsoft Store 与 Windows 媒体播放器的中性 Fluent 风格,不使用渐变,蓝色为主强调,橙/红仅用于警示。\n- 随机放映室和播放器 UI 更接近 Win11 媒体体验,加载、失败、保存、全屏等状态更清晰。", + "category_list": [ + { + "icon": "monitor", + "id": "system", + "name": "系统工具" + }, + { + "icon": "code", + "id": "developer", + "name": "开发工具" + }, + { + "icon": "image", + "id": "image", + "name": "图像工具" + } + ], + "detected_product": "YMhut Box", + "detected_packages": { + "YMhut Box": [ + { + "version": "2.0.6.2", + "extension": "exe", + "fileName": "YMhut_Box_WinUI_Setup_2.0.6.2.exe", + "downloadPath": "/downloads/YMhut_Box_WinUI_Setup_2.0.6.2.exe", + "size": "待发布", + "sizeBytes": 0, + "updateDate": "2026-06-14", + "updateTime": "2026-06-14 00:00:00" + }, + { + "version": "2.0.6.2", + "extension": "exe", + "fileName": "YMhut_Box_WinUI_Setup_2.0.6.2_Light.exe", + "downloadPath": "/downloads/YMhut_Box_WinUI_Setup_2.0.6.2_Light.exe", + "size": "待发布", + "sizeBytes": 0, + "updateDate": "2026-06-14", + "updateTime": "2026-06-14 00:00:00" + } + ] + }, + "download_mirrors": [ + { + "enabled": true, + "id": "primary", + "name": "官方直连", + "sha256": "", + "type": "direct", + "url": "https://update.ymhut.cn/downloads/YMhut_Box_WinUI_Setup_2.0.6.2.exe" + }, + { + "enabled": true, + "id": "light", + "name": "轻量安装包", + "sha256": "", + "type": "direct", + "url": "https://update.ymhut.cn/downloads/YMhut_Box_WinUI_Setup_2.0.6.2_Light.exe" + } + ], + "download_url": "https://update.ymhut.cn/downloads/YMhut_Box_WinUI_Setup_2.0.6.2.exe", + "home_notes": "v2.0.6.2 聚焦安装布局、启动稳定性和 WinUI 工作台体验:覆盖安装会清理旧程序布局和 runtime 残留,用户目录只保留设置、日志、缓存和轻量状态;语言资源只保留中英双语并归入 lang;工具箱、工具详情、结果渲染、排行榜/资讯卡片、设置控制中心、系统概况和服务状态页继续向原生 Fluent 工具软件风格收束。", + "last_update_notes": { + "v2.0.5.3 稳定性": "延续启动首页、插件扫描、反馈中心和日志展示修复;本版本进一步处理覆盖安装、语言资源和用户目录 runtime 残留。", + "v2.0.5.3 安装包体积": "上一版新增轻量安装包通道;本版本继续收束语言包和旧资源布局,避免无关文件混入发布目录。", + "v2.0.5.3 设置外观": "上一版加入窗口材质设置;本版本继续完善设置页控制中心、系统概况实时图表和中文本地化。" + }, + "last_updated": "2026-06-14T00:00:00Z", + "tool_metadata": {}, + "update_notes": { + "启动与覆盖安装": "修复覆盖安装后启动白屏或应用自行退出的问题;旧压缩语言布局不再回退复制到用户目录,失败时写入清晰日志并显示可读错误。", + "用户目录瘦身": "用户数据目录不再生成 Runtime、runtime、Runtimes 或 runtimes 文件夹;启动、自检和安装器都会清理旧 runtime 残留,避免大型运行时副本占用本机存储。", + "语言资源布局": "发布布局统一为纯 lang,保留 lang\\zh-CN 与 lang\\en-US;移除其他语言包、根目录 culture 目录和 resources\\lang 压缩残留。", + "启动自检": "迁移并改造旧版启动初始化思路:快速预检负责用户目录、设置、数据库、下载队列、安装根和关键资源;月度完整性检查在窗口可用后后台运行。", + "自检结果页": "服务状态页新增启动自检与安装完整性入口;查看结果前增加确认提示和加载进度,历史先加载摘要,用户选择后再读取完整明细,降低大量结果渲染造成的卡顿。", + "排行榜与资讯": "修复部分排行榜、热榜和资讯类工具因“已隐藏远程地址,仅展示脱敏来源名称”提示混入结构化解析而无法显示卡片内容的问题,同时继续隐藏远程地址。", + "工具箱与工具详情": "工具箱改为更高密度的原生 WinUI 工作台布局;工具内容区域按功能类型优化输入、结果、原始输出、复制、搜索和导出体验。", + "设置页": "修复设置页初始化失败;控制中心分页限制为 5 项可视并增加动态上下箭头提示;系统概况加入网格坐标、暗色绘图区和更多实时系统信息。", + "主题与视觉": "主题改为微软商店/媒体播放器式中性 Fluent 风格,移除渐变主视觉,蓝色作为主强调色,橙红仅用于更新、警告和风险行为。", + "天气胶囊": "补齐阴天、未知、离线等状态的基础图标和轻量动效,遵守关闭动画与高对比度设置。", + "本地化与日志": "修复工具箱与安全、风险确认、默认工具范围、设置弹窗和高频日志的中文模式英文漏出;反馈码和原始错误信息仍保留必要英文。" + } +} diff --git a/server/update/utils/config.go b/server/update/utils/config.go new file mode 100644 index 0000000..5a658da --- /dev/null +++ b/server/update/utils/config.go @@ -0,0 +1,115 @@ +package utils + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" +) + +// ConfigCache 配置缓存 +type ConfigCache struct { + mu sync.RWMutex + toolStatus map[string]interface{} + updateInfo map[string]interface{} + mediaTypes map[string]interface{} +} + +var configCache = &ConfigCache{} + +// LoadConfig 加载配置文件 +func LoadConfig(filename string) (map[string]interface{}, error) { + filePath := filepath.Join("public", filename) + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + return config, nil +} + +// SaveConfig 保存配置文件并更新缓存 +func SaveConfig(filename string, config map[string]interface{}) error { + filePath := filepath.Join("public", filename) + + // 转换为 JSON + jsonData, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + // 保存文件 + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return err + } + + // 更新缓存 + configCache.mu.Lock() + defer configCache.mu.Unlock() + + switch filename { + case "tool-status.json": + configCache.toolStatus = config + case "update-info.json": + configCache.updateInfo = config + case "media-types.json": + configCache.mediaTypes = config + } + + return nil +} + +// GetCachedConfig 获取缓存的配置 +func GetCachedConfig(filename string) (map[string]interface{}, bool) { + configCache.mu.RLock() + defer configCache.mu.RUnlock() + + switch filename { + case "tool-status.json": + if configCache.toolStatus != nil { + return configCache.toolStatus, true + } + case "update-info.json": + if configCache.updateInfo != nil { + return configCache.updateInfo, true + } + case "media-types.json": + if configCache.mediaTypes != nil { + return configCache.mediaTypes, true + } + } + + return nil, false +} + +// ReloadConfig 重新加载配置 +func ReloadConfig(filename string) error { + config, err := LoadConfig(filename) + if err != nil { + return err + } + + return SaveConfig(filename, config) +} + +// InitConfigCache 初始化配置缓存 +func InitConfigCache() error { + files := []string{"tool-status.json", "update-info.json", "media-types.json"} + + for _, file := range files { + if _, err := os.Stat(filepath.Join("public", file)); err == nil { + config, err := LoadConfig(file) + if err == nil { + SaveConfig(file, config) + } + } + } + + return nil +} diff --git a/server/update/utils/installer_alias_test.go b/server/update/utils/installer_alias_test.go new file mode 100644 index 0000000..321915e --- /dev/null +++ b/server/update/utils/installer_alias_test.go @@ -0,0 +1,23 @@ +package utils + +import "testing" + +func TestYmhutWinUIInstallerAliasesMatchLegacyInstaller(t *testing.T) { + cases := []string{ + "YMhut_Box_WinUI_Setup_2.0.6.0.exe", + "YMhut_Box_Setup_2.0.5.exe", + } + + for _, fileName := range cases { + productName, version, _, ok := parsePackageFileName(fileName) + if !ok { + t.Fatalf("expected %s to be parsed", fileName) + } + if !IsSameProduct(productName, "YMhut Box") { + t.Fatalf("expected %q from %s to match YMhut Box", productName, fileName) + } + if version == "" { + t.Fatalf("expected version from %s", fileName) + } + } +} diff --git a/server/update/utils/jwt.go b/server/update/utils/jwt.go new file mode 100644 index 0000000..25220a3 --- /dev/null +++ b/server/update/utils/jwt.go @@ -0,0 +1,52 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte("your-secret-key-change-in-production") // 生产环境应该使用环境变量 + +// Claims JWT 声明 +type Claims struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + IsAdmin bool `json:"is_admin"` + jwt.RegisteredClaims +} + +// GenerateToken 生成 JWT Token +func GenerateToken(userID uint, username string, isAdmin bool) (string, error) { + claims := Claims{ + UserID: userID, + Username: username, + IsAdmin: isAdmin, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +// ParseToken 解析 JWT Token +func ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("无效的 token") +} diff --git a/server/update/utils/logger.go b/server/update/utils/logger.go new file mode 100644 index 0000000..bcf66f2 --- /dev/null +++ b/server/update/utils/logger.go @@ -0,0 +1,68 @@ +package utils + +import ( + "fmt" + "os" + "time" +) + +// ANSI 颜色代码 +const ( + ColorReset = "\033[0m" + ColorBright = "\033[1m" + ColorRed = "\033[31m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorCyan = "\033[36m" + ColorGray = "\033[90m" +) + +// Logger 日志记录器 +type Logger struct{} + +// NewLogger 创建新的日志记录器 +func NewLogger() *Logger { + return &Logger{} +} + +// log 格式化日志条目 +func (l *Logger) log(level, color, message string) { + timestamp := time.Now().Format(time.RFC3339) + levelTag := fmt.Sprintf("%s%-7s%s", color, level, ColorReset) + + // 格式化时间戳 + formattedTime := fmt.Sprintf("%s%s%s", ColorGray, timestamp, ColorReset) + + // 根据级别选择输出方式 + if level == "ERROR" { + fmt.Fprintf(os.Stderr, "%s [%s] %s\n", formattedTime, levelTag, message) + } else { + fmt.Printf("%s [%s] %s\n", formattedTime, levelTag, message) + } +} + +// Info 绿色 "INFO" 级别日志 +func (l *Logger) Info(message string) { + l.log("INFO", ColorGreen, message) +} + +// Warn 黄色 "WARN" 级别日志 +func (l *Logger) Warn(message string) { + l.log("WARN", ColorYellow, message) +} + +// Error 红色 "ERROR" 级别日志 +func (l *Logger) Error(message string) { + l.log("ERROR", ColorBright+ColorRed, message) +} + +// System 青色 "SYSTEM" 级别日志 +func (l *Logger) System(message string) { + l.log("SYSTEM", ColorCyan, message) +} + +// HTTP 蓝色 "HTTP" 级别日志 +func (l *Logger) HTTP(message string) { + l.log("HTTP", ColorBlue, message) +} diff --git a/server/update/utils/osdetect.go b/server/update/utils/osdetect.go new file mode 100644 index 0000000..af5ce56 --- /dev/null +++ b/server/update/utils/osdetect.go @@ -0,0 +1,109 @@ +package utils + +import ( + "os" + "runtime" + "strings" +) + +// OSInfo 操作系统信息 +type OSInfo struct { + OS string // 操作系统类型: windows, linux, darwin + Arch string // 架构: amd64, arm64, 386 + IsCGO bool // 是否支持 CGO + DataDir string // 数据目录路径 +} + +var currentOS *OSInfo + +// DetectOS 检测操作系统环境 +func DetectOS() *OSInfo { + if currentOS != nil { + return currentOS + } + + osType := runtime.GOOS + arch := runtime.GOARCH + + // 检测 CGO 支持 + // 注意:CGO 支持在编译时确定,运行时无法准确检测 + // 这里通过检查环境变量或尝试使用 SQLite 来判断 + isCGO := true + // 如果设置了 CGO_ENABLED=0,则不支持 CGO + if cgoEnv := os.Getenv("CGO_ENABLED"); cgoEnv == "0" { + isCGO = false + } + + // 确定数据目录 + dataDir := "data" + if osType == "windows" { + // Windows 使用相对路径 + dataDir = "data" + } else { + // Linux/macOS 使用相对路径 + dataDir = "data" + } + + currentOS = &OSInfo{ + OS: normalizeOS(osType), + Arch: normalizeArch(arch), + IsCGO: isCGO, + DataDir: dataDir, + } + + return currentOS +} + +// normalizeOS 标准化操作系统名称 +func normalizeOS(os string) string { + os = strings.ToLower(os) + switch os { + case "windows": + return "windows" + case "linux": + return "linux" + case "darwin": + return "darwin" + case "freebsd", "openbsd", "netbsd": + return "unix" + default: + return "unknown" + } +} + +// normalizeArch 标准化架构名称 +func normalizeArch(arch string) string { + arch = strings.ToLower(arch) + switch arch { + case "amd64", "x86_64": + return "amd64" + case "386", "i386", "i686": + return "386" + case "arm64", "aarch64": + return "arm64" + case "arm": + return "arm" + default: + return arch + } +} + +// GetOSInfo 获取操作系统信息 +func GetOSInfo() *OSInfo { + return DetectOS() +} + +// IsWindows 判断是否为 Windows +func IsWindows() bool { + return DetectOS().OS == "windows" +} + +// IsLinux 判断是否为 Linux +func IsLinux() bool { + return DetectOS().OS == "linux" +} + +// IsDarwin 判断是否为 macOS +func IsDarwin() bool { + return DetectOS().OS == "darwin" +} diff --git a/server/update/utils/password.go b/server/update/utils/password.go new file mode 100644 index 0000000..eb148af --- /dev/null +++ b/server/update/utils/password.go @@ -0,0 +1,104 @@ +package utils + +import ( + "errors" + "regexp" + "unicode" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrPasswordTooShort = errors.New("密码长度至少为8个字符") + ErrPasswordNoUpper = errors.New("密码必须包含至少一个大写字母") + ErrPasswordNoLower = errors.New("密码必须包含至少一个小写字母") + ErrPasswordNoDigit = errors.New("密码必须包含至少一个数字") + ErrPasswordNoSpecial = errors.New("密码必须包含至少一个特殊字符") + ErrPasswordCommon = errors.New("密码不能是常见弱密码") + ErrPasswordSameChars = errors.New("密码不能全部是相同字符") +) + +// 常见弱密码列表 +var commonPasswords = []string{ + "password", "12345678", "123456789", "1234567890", + "qwerty", "abc123", "password123", "admin123", + "123456", "1234567", "12345", "1234", + "admin", "root", "user", "test", +} + +// ValidatePasswordStrength 验证密码强度 +func ValidatePasswordStrength(password string) error { + // 检查长度 + if len(password) < 8 { + return ErrPasswordTooShort + } + + // 检查是否全部相同字符 + allSame := true + for i := 1; i < len(password); i++ { + if password[i] != password[0] { + allSame = false + break + } + } + if allSame { + return ErrPasswordSameChars + } + + // 检查是否包含大写字母 + hasUpper := false + // 检查是否包含小写字母 + hasLower := false + // 检查是否包含数字 + hasDigit := false + // 检查是否包含特殊字符 + hasSpecial := false + + for _, char := range password { + switch { + case unicode.IsUpper(char): + hasUpper = true + case unicode.IsLower(char): + hasLower = true + case unicode.IsDigit(char): + hasDigit = true + case unicode.IsPunct(char) || unicode.IsSymbol(char): + hasSpecial = true + } + } + + if !hasUpper { + return ErrPasswordNoUpper + } + if !hasLower { + return ErrPasswordNoLower + } + if !hasDigit { + return ErrPasswordNoDigit + } + if !hasSpecial { + return ErrPasswordNoSpecial + } + + // 检查是否是常见弱密码 + lowerPassword := regexp.MustCompile(`[^a-z]`).ReplaceAllString(password, "") + for _, common := range commonPasswords { + if lowerPassword == common { + return ErrPasswordCommon + } + } + + return nil +} + +// HashPassword 加密密码 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword 验证密码 +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/server/update/utils/route-utils.go b/server/update/utils/route-utils.go new file mode 100644 index 0000000..7a00be4 --- /dev/null +++ b/server/update/utils/route-utils.go @@ -0,0 +1,351 @@ +package utils + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +type FileInfo struct { + Version string `json:"version"` + Extension string `json:"extension"` + FileName string `json:"fileName"` + DownloadPath string `json:"downloadPath"` + Size string `json:"size"` + SizeBytes int64 `json:"sizeBytes"` + UpdateDate string `json:"updateDate"` + UpdateTime string `json:"updateTime"` +} + +type ProductsInfo map[string][]FileInfo + +var ( + supportedPackageExtOrder = []string{ + ".tar.gz", + ".appimage", + ".msi", + ".exe", + ".zip", + ".pkg", + ".dmg", + ".apk", + ".deb", + ".rpm", + } + versionPattern = regexp.MustCompile(`(?i)(?:^|[ _-])v?(\d+(?:\.\d+){1,4})(?:$|[ _-])`) +) + +func FormatBytes(bytes int64, precision int) string { + if bytes == 0 { + return "0 B" + } + + units := []string{"B", "KB", "MB", "GB", "TB"} + if precision == 0 { + precision = 2 + } + + bytesFloat := float64(bytes) + if bytesFloat < 0 { + bytesFloat = 0 + } + + pow := 0 + if bytesFloat > 0 { + pow = int(float64(len(fmt.Sprintf("%.0f", bytesFloat))-1) / 3.321928) + } + if pow >= len(units) { + pow = len(units) - 1 + } + + divisor := int64(1) + for i := 0; i < pow; i++ { + divisor *= 1024 + } + + converted := bytesFloat / float64(divisor) + return fmt.Sprintf("%.*f %s", precision, converted, units[pow]) +} + +func GetProductsInfo(downloadDir string, logger *Logger) ProductsInfo { + products := make(ProductsInfo) + + if _, err := os.Stat(downloadDir); os.IsNotExist(err) { + logger.Error(fmt.Sprintf("读取下载目录失败: %s - 错误: %s", downloadDir, err.Error())) + return nil + } + + entries, err := os.ReadDir(downloadDir) + if err != nil { + logger.Error(fmt.Sprintf("读取下载目录失败: %s - 错误: %s", downloadDir, err.Error())) + return nil + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fileName := entry.Name() + productName, version, ext, ok := parsePackageFileName(fileName) + if !ok { + continue + } + + filePath := filepath.Join(downloadDir, fileName) + stat, err := os.Stat(filePath) + if err != nil { + logger.Warn(fmt.Sprintf("获取文件统计信息失败: %s - 错误: %s", fileName, err.Error())) + continue + } + + fileInfoData := FileInfo{ + Version: version, + Extension: strings.TrimPrefix(ext, "."), + FileName: fileName, + DownloadPath: "/downloads/" + fileName, + Size: FormatBytes(stat.Size(), 2), + SizeBytes: stat.Size(), + UpdateDate: stat.ModTime().Format("2006-01-02"), + UpdateTime: stat.ModTime().Format("2006-01-02 15:04:05"), + } + + products[productName] = append(products[productName], fileInfoData) + } + + for productName := range products { + sort.Slice(products[productName], func(i, j int) bool { + v1 := products[productName][i].Version + v2 := products[productName][j].Version + if compareVersions(v1, v2) == 0 { + return products[productName][i].UpdateTime > products[productName][j].UpdateTime + } + return compareVersions(v1, v2) > 0 + }) + } + + return products +} + +func GetLatestProductRelease(products ProductsInfo, preferredProduct string) (string, *FileInfo) { + if len(products) == 0 { + return "", nil + } + + var selectedProduct string + var selectedRelease *FileInfo + + if preferredProduct != "" { + for productName, releases := range products { + if !IsSameProduct(productName, preferredProduct) || len(releases) == 0 { + continue + } + candidate := releases[0] + if selectedRelease == nil || isNewerRelease(candidate, *selectedRelease) { + selectedProduct = productName + selectedRelease = &candidate + } + } + if selectedRelease != nil { + return selectedProduct, selectedRelease + } + } + + for productName, releases := range products { + if len(releases) == 0 { + continue + } + candidate := releases[0] + if selectedRelease == nil || isNewerRelease(candidate, *selectedRelease) { + selectedProduct = productName + selectedRelease = &candidate + } + } + + return selectedProduct, selectedRelease +} + +func IsSameProduct(productName string, preferredProduct string) bool { + return productAliasKey(productName) == productAliasKey(preferredProduct) +} + +func productAliasKey(name string) string { + key := strings.ToLower(strings.TrimSpace(name)) + key = strings.NewReplacer(" ", "", "_", "", "-", "", ".", "").Replace(key) + switch key { + case "ymhutbox", "ymhut": + return "ymhutbox" + default: + return key + } +} + +func isNewerRelease(candidate FileInfo, current FileInfo) bool { + versionCompare := compareVersions(candidate.Version, current.Version) + if versionCompare != 0 { + return versionCompare > 0 + } + return candidate.UpdateTime > current.UpdateTime +} + +func parsePackageFileName(fileName string) (string, string, string, bool) { + ext, stem, ok := splitPackageExtension(fileName) + if !ok { + return "", "", "", false + } + + version := extractVersion(stem) + productName := normalizeProductName(stem, version) + if productName == "" { + productName = stem + } + + if version == "" { + version = "未标注" + } + + return productName, version, ext, true +} + +func splitPackageExtension(fileName string) (string, string, bool) { + lower := strings.ToLower(fileName) + for _, ext := range supportedPackageExtOrder { + if strings.HasSuffix(lower, ext) { + return ext, fileName[:len(fileName)-len(ext)], true + } + } + return "", "", false +} + +func extractVersion(stem string) string { + matches := versionPattern.FindStringSubmatch(stem) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +func normalizeProductName(stem string, version string) string { + name := stem + if version != "" { + name = versionPattern.ReplaceAllString(name, " ") + } + + replacements := []string{ + "setup", "installer", "install", "release", "portable", + "windows", "window", "win", "linux", "android", "macos", "darwin", + "x64", "x86", "arm64", "amd64", "universal", "desktop", + } + + for _, token := range replacements { + re := regexp.MustCompile(`(?i)(^|[ _-])` + regexp.QuoteMeta(token) + `($|[ _-])`) + name = re.ReplaceAllString(name, " ") + } + + name = strings.NewReplacer("_", " ", "-", " ").Replace(name) + name = strings.Join(strings.Fields(name), " ") + return strings.TrimSpace(name) +} + +func compareVersions(v1, v2 string) int { + if v1 == "未标注" && v2 == "未标注" { + return 0 + } + if v1 == "未标注" { + return -1 + } + if v2 == "未标注" { + return 1 + } + + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + maxLen := len(parts1) + if len(parts2) > maxLen { + maxLen = len(parts2) + } + + for i := 0; i < maxLen; i++ { + num1 := 0 + num2 := 0 + if i < len(parts1) { + fmt.Sscanf(parts1[i], "%d", &num1) + } + if i < len(parts2) { + fmt.Sscanf(parts2[i], "%d", &num2) + } + + if num1 > num2 { + return 1 + } + if num1 < num2 { + return -1 + } + } + + return 0 +} + +func FileExists(filePath string) bool { + _, err := os.Stat(filePath) + return !os.IsNotExist(err) +} + +func ReadJSONFile(filePath string) (map[string]interface{}, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + + return result, nil +} + +func GetMimeType(filePath string) string { + ext := strings.ToLower(filepath.Ext(filePath)) + + mimeTypes := map[string]string{ + ".json": "application/json; charset=utf-8", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".html": "text/html; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".exe": "application/octet-stream", + ".zip": "application/zip", + ".pkg": "application/octet-stream", + ".dmg": "application/x-apple-diskimage", + ".msi": "application/x-msi", + ".apk": "application/vnd.android.package-archive", + ".deb": "application/vnd.debian.binary-package", + ".rpm": "application/x-rpm", + } + + if mime, ok := mimeTypes[ext]; ok { + return mime + } + + if strings.HasSuffix(strings.ToLower(filePath), ".tar.gz") { + return "application/gzip" + } + + return "application/octet-stream" +} diff --git a/server/update/utils/route_utils_test.go b/server/update/utils/route_utils_test.go new file mode 100644 index 0000000..ec9aaeb --- /dev/null +++ b/server/update/utils/route_utils_test.go @@ -0,0 +1,38 @@ +package utils + +import "testing" + +func TestGetLatestProductReleaseUsesYmhutBoxAliases(t *testing.T) { + products := ProductsInfo{ + "YmhutBox": { + { + Version: "2.0.0", + FileName: "YmhutBox Setup 2.0.0.exe", + DownloadPath: "/downloads/YmhutBox Setup 2.0.0.exe", + UpdateTime: "2026-04-28 04:48:25", + }, + }, + "YMhut Box": { + { + Version: "2.0.1", + FileName: "YMhut_Box_Setup_2.0.1.exe", + DownloadPath: "/downloads/YMhut_Box_Setup_2.0.1.exe", + UpdateTime: "2026-04-30 17:27:21", + }, + }, + } + + productName, release := GetLatestProductRelease(products, "YMhut Box") + if release == nil { + t.Fatal("expected a release") + } + if productName != "YMhut Box" { + t.Fatalf("expected YMhut Box alias to win, got %q", productName) + } + if release.Version != "2.0.1" { + t.Fatalf("expected version 2.0.1, got %q", release.Version) + } + if release.FileName != "YMhut_Box_Setup_2.0.1.exe" { + t.Fatalf("expected new installer, got %q", release.FileName) + } +} diff --git a/server/update/views/404.html b/server/update/views/404.html new file mode 100644 index 0000000..ac731f4 --- /dev/null +++ b/server/update/views/404.html @@ -0,0 +1,30 @@ + + + + + + {{.title}} + + + + +
+
+
404
+

页面未找到

+

+ 抱歉,您访问的页面不存在或已被移动 +

+
+ 访问路径: {{.path}} +
+ + + + + 返回首页 + +
+
+ + diff --git a/server/update/views/500.html b/server/update/views/500.html new file mode 100644 index 0000000..491d6bc --- /dev/null +++ b/server/update/views/500.html @@ -0,0 +1,32 @@ + + + + + + {{.title}} + + + + +
+
+
500
+

服务器内部错误

+

+ 抱歉,服务器在处理您的请求时发生错误 +

+ {{if .message}} +
+ 错误信息: {{.message}} +
+ {{end}} + + + + + 返回首页 + +
+
+ + diff --git a/server/update/views/admin.html b/server/update/views/admin.html new file mode 100644 index 0000000..59769f0 --- /dev/null +++ b/server/update/views/admin.html @@ -0,0 +1,253 @@ + + + + + + {{.title}} - 后台管理 + + + + +
+
+
+

后台管理

+
+
+ + + + + + + + +
+
+ +
+ + +
+ +
+ +
+
+
👥
+
+
-
+
用户总数
+
+
+
+
🛣️
+
+
-
+
路由总数
+
+
+
+
📝
+
+
-
+
日志条目
+
+
+
+
+
+
-
+
服务器时间
+
+
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + +
ID方法路径类型描述状态操作
加载中...
+
+
+ + +
+ +
+
加载中...
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+ + + + + + + diff --git a/server/update/views/index.html b/server/update/views/index.html new file mode 100644 index 0000000..7ca6f67 --- /dev/null +++ b/server/update/views/index.html @@ -0,0 +1,196 @@ + + + + + + {{.pageTitle}} + + + + + + +
+
+
+
+ 自动扫描 + downloads +

无需手工维护下载卡片,刷新页面即可读取安装包并按产品分组。

+
+
+ 兼容更新 + v1 + v2 +

保留旧版 update-info.json,同时提供模块化清单。

+
+
+ 校验能力 + SHA256 +

服务端自动为主程序、资源包和未来模块包生成校验字段。

+
+
+ + {{if .errorMessage}} +
+

读取目录失败

+

{{.errorMessage}}

+
+ {{else if not .products}} +
+

暂无可识别安装包

+

请将 Windows、Android、macOS 或 Linux 安装包放入 public/downloads

+
+ {{else}} +
+
+ Packages +

安装包列表

+
+

按产品自动分组,默认展示每个产品的最新版本。

+
+ +
+ {{range $productName, $versions := .products}} + {{$latestVersion := index $versions 0}} + {{$historyVersions := slice $versions 1}} + {{$meta := index $.productMeta $productName}} + {{if not $meta}} + {{$meta = $.defaultMeta}} + {{end}} +
+
+
{{$meta.Icon | safeHTML}}
+
+

{{$productName}}

+

{{$meta.Description}}

+
+
+ +
+ {{range $meta.Tags}} + {{.}} + {{end}} + {{len $versions}} 个版本 +
+ +
+
+
+ 最新版本 + {{$latestVersion.Version}} +
+ {{$latestVersion.Extension}} +
+
+
+ 更新时间 + {{$latestVersion.UpdateDate}} +
+
+ 文件大小 + {{$latestVersion.Size}} +
+
+ 文件名 + {{$latestVersion.FileName}} +
+
+
+ +
+ 下载最新版本 + {{if $historyVersions}} + + {{end}} +
+
+ {{end}} +
+ {{end}} +
+
+ + + +
+ +
+ + + + diff --git a/server/update/views/install.html b/server/update/views/install.html new file mode 100644 index 0000000..430f958 --- /dev/null +++ b/server/update/views/install.html @@ -0,0 +1,184 @@ + + + + + + 数据库配置 - 系统安装 + + + + +
+
+
+ +

数据库配置

+

请配置数据库连接信息

+
+ +
+

+ 默认管理员账号:
+ 用户名: admin
+ 密码: admin123456 +

+
+ +
+
+ + +
+ + +
+
+ + + 支持远程数据库,请输入IP地址或域名 +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + 留空则无前缀 +
+
+ + + + + +
+ +
+
+
+ + + + diff --git a/server/update/views/login.html b/server/update/views/login.html new file mode 100644 index 0000000..6d4644e --- /dev/null +++ b/server/update/views/login.html @@ -0,0 +1,55 @@ + + + + + + 登录 - 后台管理 + + + + +
+
+
+ +

欢迎回来

+

登录您的管理员账户

+
+ +
+
+ + +
+ +
+ + +
+ + + + +
+ +
+
+
+ + + + diff --git a/server/update/views/register.html b/server/update/views/register.html new file mode 100644 index 0000000..f31cfd6 --- /dev/null +++ b/server/update/views/register.html @@ -0,0 +1,68 @@ + + + + + + 注册 - 后台管理 + + + + +
+
+
+ +

创建管理员账户

+

第一个注册的用户将自动成为管理员

+
+ +
+
+ + + 3-50个字符 +
+ +
+ + +
+ +
+ + + 至少8个字符,包含大小写字母、数字和特殊字符 +
+
+ +
+ + +
+ + + + +
+ +
+
+
+ + + + diff --git a/server/update/views/settings.html b/server/update/views/settings.html new file mode 100644 index 0000000..7efd6b9 --- /dev/null +++ b/server/update/views/settings.html @@ -0,0 +1,143 @@ + + + + + + 系统设置 - 后台管理 + + + + +
+
+
+ + + + + +

系统设置

+
+
+ + +
+
+ +
+
+ +
+
+

数据库设置

+

管理数据库连接和配置

+
+
+
+
+ 数据库类型 + - +
+
+ 连接状态 + - +
+ +
+ + + + + +
+

数据库转换

+

⚠️ 警告:数据库转换会导出当前数据并导入到新数据库,请确保已备份数据!

+
+ + +
+
+
+
+
+ + +
+
+

系统信息

+

查看系统运行状态和统计信息

+
+
+
+
+ 用户总数 + - +
+
+ 路由总数 + - +
+
+ 日志条目 + - +
+
+ 服务器时间 + - +
+
+
+
+ + +
+
+

运行环境

+

查看服务器运行环境信息

+
+
+
+
+ 操作系统 + - +
+
+ 系统架构 + - +
+
+
+
+
+
+
+ + + + +