From 89acbe3bf6800365407bed3b04e75aa508ca1b2f Mon Sep 17 00:00:00 2001 From: aiyimickey <39365912+aiyimickey@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:41:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20monitor-ai=20=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E4=B8=AD=E5=BF=83=E5=AE=8C=E6=95=B4=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: 共享库(auth/audit/wechat/alert) Phase 2: 项目骨架(Next.js 15 + Tailwind v4 + 设计系统) Phase 3: 认证系统(OIDC SSO + LDAP + localadmin + RBAC) Phase 4: 服务管理 + MonitorWorker 后台引擎 Phase 5: 告警渠道 + WeChatPusher + AlertManager Phase 6: 仪表盘 + 状态历史 + 告警历史 Phase 7: 部署脚本 + 文档 Phase 8: TopBar 布局对齐 + 暗色模式修复 独立审查通过(2 轮零 CRITICAL/HIGH 问题) --- .env.example | 23 + .gitignore | 8 + CHANGELOG.md | 23 + CLAUDE.md | 501 ++++ Dockerfile | 28 + docker-compose.yml | 22 + docs/completed/LOOP-STATE-monitor-ai-full.md | 44 + entrypoint.sh | 37 + next-env.d.ts | 6 + next.config.ts | 8 + package-lock.json | 2508 +++++++++++++++++ package.json | 30 + postcss.config.mjs | 6 + preview/dashboard.html | 1260 +++++++++ scripts/deploy-monitor.sh | 431 +++ scripts/monitor-watchdog.sh | 142 + scripts/monitor-worker.ts | 17 + shared | 1 + src/app/admin/audit-logs/page.tsx | 11 + src/app/admin/roles/page.tsx | 11 + src/app/admin/users/page.tsx | 11 + src/app/alerts/page.tsx | 40 + src/app/api/admin/worker-status/route.ts | 14 + src/app/api/alert-channels/[id]/route.ts | 50 + src/app/api/alert-channels/[id]/test/route.ts | 23 + src/app/api/alert-channels/route.ts | 32 + src/app/api/alerts/route.ts | 23 + src/app/api/auth/callback/route.ts | 97 + src/app/api/auth/login/oidc/route.ts | 25 + src/app/api/auth/login/route.ts | 91 + src/app/api/auth/logout/route.ts | 11 + src/app/api/auth/me/route.ts | 24 + src/app/api/health/route.ts | 4 + src/app/api/services/[id]/check/route.ts | 31 + src/app/api/services/[id]/route.ts | 51 + src/app/api/services/route.ts | 42 + src/app/api/status-history/route.ts | 17 + src/app/api/status/route.ts | 10 + src/app/client-layout.tsx | 39 + src/app/globals.css | 16 + src/app/layout.tsx | 21 + src/app/login/page.tsx | 6 + src/app/metadata.ts | 5 + src/app/page.tsx | 93 + src/app/services/page.tsx | 74 + src/app/settings/page.tsx | 230 ++ src/app/status-history/page.tsx | 40 + src/components/Sidebar.tsx | 89 + src/components/ThemeProvider.tsx | 39 + src/components/ThemeToggle.tsx | 19 + src/components/TopBar.tsx | 84 + src/lib/audit.ts | 8 + src/lib/auth-config.ts | 7 + src/lib/config.ts | 25 + src/lib/db.ts | 74 + src/lib/monitor-worker.ts | 236 ++ src/lib/permissions.ts | 42 + src/middleware.ts | 8 + tsconfig.json | 25 + tsconfig.tsbuildinfo | 1 + 60 files changed, 6894 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docs/completed/LOOP-STATE-monitor-ai-full.md create mode 100644 entrypoint.sh create mode 100644 next-env.d.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 preview/dashboard.html create mode 100755 scripts/deploy-monitor.sh create mode 100755 scripts/monitor-watchdog.sh create mode 100644 scripts/monitor-worker.ts create mode 120000 shared create mode 100644 src/app/admin/audit-logs/page.tsx create mode 100644 src/app/admin/roles/page.tsx create mode 100644 src/app/admin/users/page.tsx create mode 100644 src/app/alerts/page.tsx create mode 100644 src/app/api/admin/worker-status/route.ts create mode 100644 src/app/api/alert-channels/[id]/route.ts create mode 100644 src/app/api/alert-channels/[id]/test/route.ts create mode 100644 src/app/api/alert-channels/route.ts create mode 100644 src/app/api/alerts/route.ts create mode 100644 src/app/api/auth/callback/route.ts create mode 100644 src/app/api/auth/login/oidc/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/services/[id]/check/route.ts create mode 100644 src/app/api/services/[id]/route.ts create mode 100644 src/app/api/services/route.ts create mode 100644 src/app/api/status-history/route.ts create mode 100644 src/app/api/status/route.ts create mode 100644 src/app/client-layout.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/metadata.ts create mode 100644 src/app/page.tsx create mode 100644 src/app/services/page.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/app/status-history/page.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/ThemeProvider.tsx create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/components/TopBar.tsx create mode 100644 src/lib/audit.ts create mode 100644 src/lib/auth-config.ts create mode 100644 src/lib/config.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/monitor-worker.ts create mode 100644 src/lib/permissions.ts create mode 100644 src/middleware.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.tsbuildinfo diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..926a527 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# monitor-ai 环境变量模板 +NODE_ENV=production +DATABASE_PATH=/data/monitor.db +MONITOR_MODE=local + +# OIDC SSO +AUTHELIA_URL=https://sso.tlyq.ai +OIDC_CLIENT_ID=monitor-oidc +OIDC_CLIENT_SECRET=<由部署脚本生成> +OIDC_REDIRECT_URI=https://monitor.tlyq.ai/api/auth/callback + +# 共享 JWT(与 OA/assets/issue 相同) +JWT_SECRET=<与全站一致> +COOKIE_DOMAIN=.tlyq.ai + +# LLDAP +LDAP_URL=ldap://ldap-ai:3890 + +# localadmin 密码(首次部署时 openssl rand -hex 16 生成) +LOCALADMIN_PASSWORD=<生成> + +# 自签名证书 +NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5032602 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.next/ +data/*.db +.env +.env.local +*.log +.DS_Store +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0da1487 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## v2026.07.01 + +### 新增 + +- **独立告警监控中心**:monitor-ai 初始版本,监控 tlyq.ai 全部容器健康状态 +- **统一管理布局**:Sidebar + ThemeProvider + 内容区,遵循 tlyq 设计系统规范(Slate 色板 + Indigo-600 主色调,浅色/暗色/自动三种主题) +- **OIDC SSO 统一认证**:通过 Authelia 进行 PKCE 授权码流程 + LDAP 本地登录 + localadmin 超级管理员 +- **RBAC 权限模块**:admin/editor/viewer 三级角色,11 个权限点,localadmin 硬编码绕过 +- **审计日志**:所有写操作记录,180 天自动清理 +- **MonitorWorker 健康检查引擎**:30 秒 tick 间隔,HTTP + Docker 可插拔检查器,抖动检测(10 分钟窗口 >= 5 次切换),持续异常 30 分钟提醒,恢复通知绕过免打扰 +- **告警管理引擎**:三阶段决策(级别匹配 → 免打扰 → 冷却),冷却状态持久化到数据库,容器重启不丢失 +- **企业微信 Webhook 推送**:markdown 格式,5 秒超时,支持测试推送 +- **四层仪表盘**:标题行 + 状态统计条 + KPI 统计条 + 4 列服务卡片网格 +- **服务管理页面**:CRUD + 手动检查 + 状态查看 +- **告警设置页面**:告警渠道管理(Webhook URL、级别过滤、免打扰时段、冷却时间) +- **告警历史页面**:告警推送记录(分页,按类型/级别筛选) +- **状态变更历史页面**:服务状态变更事件(分页,按服务筛选) +- **共享库**:`shared/lib/`(auth/、audit/、wechat/、alert/、db/)+ `shared/ui/`(9 个 React 组件 + 统一登录页) +- **部署脚本**:`deploy-monitor.sh`(支持本地/远程/开发三种模式,首次部署自动生成密钥) +- **健康检查兜底**:`monitor-watchdog.sh`(crontab 每 5 分钟,自身异常时直接 curl webhook) +- **Docker 双进程守护**:`entrypoint.sh` 同时启动 Worker + Server,任一退出则容器退出 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..132364a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,501 @@ +# CLAUDE.md — monitor.tlyq.ai 告警监控中心 + +## 项目概述 + +monitor-ai 是独立部署的统一告警监控中心,监控 tlyq.ai 基础设施所有容器(Authelia、Redis、LLDAP、OA、assets、issue、Gitea、Nginx、www、cloud、token)的健康状态,通过企业微信 Webhook 推送告警。部署在腾讯云(txjp 服务器),域名 `monitor.tlyq.ai`。 + +--- + +## 快速参考 + +| 属性 | 值 | +|------|-----| +| 站点域名 | `monitor.tlyq.ai` | +| 服务器 | txjp(IP: 43.133.38.210) | +| 代码路径 | `/root/docker/monitor-ai/` | +| 本地端口 | 6181 | +| 容器名 | `monitor-ai` | +| 数据库 | SQLite:`data/monitor.db` | +| 默认账号 | `localadmin` / 首次部署自动生成 | + +### 常用命令 + +```bash +cd /Users/niuniu/programs/docker/monitor-ai +npm run dev # 本地开发(http://localhost:6181) +npm run build # 生产构建 +npm run start # 启动生产服务 +``` + +--- + +## 技术栈 + +| 技术 | 版本 | 用途 | +|------|------|------| +| Next.js | 15 App Router | Web 框架(standalone 输出) | +| SQLite | 3.x | 数据库(通过 `execFileSync` + `sqlite3` CLI) | +| Tailwind CSS | v4 | 样式(`@tailwindcss/postcss`) | +| lucide-react | ^1.8.0 | 图标库 | +| ldapts | ^6.0.0 | LDAP 认证客户端 | +| openid-client | ^5.7.1 | OIDC 认证 | +| tsx | ^4.0.0 | Worker TypeScript 执行 | + +--- + +## 目录结构 + +``` +monitor-ai/ +├── Dockerfile # 两阶段构建(node:22-alpine) +├── docker-compose.yml # 容器编排(webnet external) +├── entrypoint.sh # Docker 入口(双进程守护:Worker + Server) +├── next.config.ts # output: standalone, transpilePackages +├── shared -> ../shared # 共享库符号链接 +├── scripts/ +│ ├── monitor-worker.ts # Worker 独立入口 +│ ├── deploy-monitor.sh # 部署脚本 +│ └── monitor-watchdog.sh # 健康检查兜底脚本 +├── src/ +│ ├── middleware.ts # Edge Runtime 路由守卫 +│ ├── app/ +│ │ ├── page.tsx # 仪表盘(四层:标题 + 状态条 + KPI + 服务卡片) +│ │ ├── login/page.tsx # 登录页 +│ │ ├── services/page.tsx # 服务管理 +│ │ ├── settings/page.tsx # 告警设置 +│ │ ├── alerts/page.tsx # 告警历史 +│ │ ├── status-history/page.tsx # 状态变更历史 +│ │ ├── admin/ # 管理页面(users/roles/audit-logs) +│ │ └── api/ # API 路由 +│ ├── components/ +│ │ ├── Sidebar.tsx # 侧边栏导航 +│ │ ├── ThemeProvider.tsx # 主题上下文 +│ │ └── ThemeToggle.tsx # 主题切换 +│ └── lib/ +│ ├── config.ts # 配置常量 +│ ├── db.ts # 数据库操作封装(execFileSync + escapeSql) +│ ├── auth-config.ts # 认证配置 +│ ├── permissions.ts # RBAC 权限定义 +│ ├── audit.ts # 审计日志封装 +│ └── monitor-worker.ts # MonitorWorker 核心类 +└── data/ + └── monitor.db # SQLite 数据库 +``` + +--- + +## 关键文件 + +| 文件 | 职责 | +|------|------| +| `src/lib/monitor-worker.ts` | MonitorWorker 核心类(30s tick,健康检查 + 状态管理 + 告警推送) | +| `src/lib/db.ts` | SQLite 操作封装(execFileSync + escapeSql,无 ORM) | +| `src/lib/permissions.ts` | RBAC 权限定义(11 个权限点,admin/editor/viewer/localadmin) | +| `src/lib/audit.ts` | 审计日志封装(调用 `@shared/lib/audit`) | +| `src/lib/auth-config.ts` | 认证配置(OIDC + LDAP) | +| `src/middleware.ts` | Edge Runtime 路由守卫(Cookie JWT 验证,公开/管理路径控制) | +| `scripts/monitor-worker.ts` | Worker 独立入口(信号处理、优雅退出) | +| `entrypoint.sh` | Docker 入口(双进程守护:Worker + Server,任一退出则容器退出) | +| `shared/lib/alert/` | 告警引擎(AlertManager、HealthChecker、类型定义) | +| `shared/lib/wechat/` | 企业微信推送(WeChatPusher、消息格式化) | + +--- + +## 数据库 Schema + +### 表概览 + +| 表名 | 说明 | 来源 | +|------|------|------| +| `services` | 被监控服务列表 | `@shared/lib/db/alert-schema` | +| `alert_channels` | Webhook 告警渠道配置 | `@shared/lib/db/alert-schema` | +| `alert_settings` | 全局设置 + 冷却状态持久化 | `@shared/lib/db/alert-schema` | +| `status_history` | 服务状态变更事件 | `@shared/lib/db/alert-schema` | +| `alert_history` | 告警推送记录 | `@shared/lib/db/alert-schema` | +| `users` | 用户账号 | `src/lib/db.ts`(内联) | +| `permissions` | 权限定义 | `src/lib/db.ts`(内联) | +| `role_permissions` | 角色-权限映射 | `src/lib/db.ts`(内联) | +| `audit_logs` | 审计日志 | `@shared/lib/audit/audit-schema` | + +### services 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK | 主键 | +| `name` | TEXT | 服务名称 | +| `category` | TEXT | 分类(endpoint / container / custom) | +| `alert_level` | TEXT | 告警级别(critical / warning / info) | +| `checks` | TEXT | 检查配置(JSON 数组,HealthCheckConfig[]) | +| `check_interval` | INTEGER | 检查间隔秒数(默认 30) | +| `check_timeout` | INTEGER | 检查超时秒数(默认 10) | +| `enabled` | INTEGER | 是否启用(默认 1) | +| `current_status` | TEXT | 当前状态(normal / abnormal / unknown) | +| `status_since` | TEXT | 状态持续起始时间 | +| `display_order` | INTEGER | 显示排序 | + +### alert_channels 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK | 主键 | +| `name` | TEXT | 渠道名称 | +| `channel_type` | TEXT | 渠道类型(默认 'wechat') | +| `webhook_url` | TEXT | Webhook 地址 | +| `enabled` | INTEGER | 是否启用 | +| `level_critical` | INTEGER | 是否接收 critical 告警 | +| `level_warning` | INTEGER | 是否接收 warning 告警 | +| `level_info` | INTEGER | 是否接收 info 告警 | +| `quiet_enabled` | INTEGER | 是否启用免打扰 | +| `quiet_start` | TEXT | 免打扰开始时间 | +| `quiet_end` | TEXT | 免打扰结束时间 | +| `quiet_bypass_critical` | INTEGER | critical 是否绕过免打扰 | +| `cooldown_minutes` | INTEGER | 冷却时间(分钟) | + +### status_history 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK | 主键 | +| `service_id` | INTEGER FK | 关联服务 | +| `from_status` | TEXT | 原状态 | +| `to_status` | TEXT | 新状态 | +| `started_at` | TEXT | 状态开始时间 | +| `ended_at` | TEXT | 状态结束时间 | +| `duration_seconds` | INTEGER | 持续时长 | +| `error_message` | TEXT | 错误信息 | +| `failed_checks` | TEXT | 失败检查详情(JSON) | +| `truncated_by_restart` | INTEGER | 是否被重启截断 | + +### alert_history 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK | 主键 | +| `channel_id` | INTEGER FK | 告警渠道 | +| `service_id` | INTEGER FK | 关联服务 | +| `status_event_id` | INTEGER FK | 关联状态事件 | +| `alert_type` | TEXT | 告警类型(alert / recovery / flapping) | +| `level` | TEXT | 告警级别 | +| `title` | TEXT | 标题 | +| `content` | TEXT | 内容 | +| `sent_at` | TEXT | 发送时间 | +| `success` | INTEGER | 是否成功 | +| `response_code` | INTEGER | HTTP 响应码 | + +### users 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | INTEGER PK | 主键 | +| `username` | TEXT UNIQUE | 用户名 | +| `display_name` | TEXT | 显示名 | +| `email` | TEXT | 邮箱 | +| `role` | TEXT | 角色(admin / editor / viewer) | +| `password_hash` | TEXT | 密码哈希 | +| `is_active` | INTEGER | 是否启用 | + +默认用户:`localadmin`(admin 角色,BCrypt 密码哈希) + +### 预置角色 + +| 角色 | 权限 | 说明 | +|------|------|------| +| `admin` | 全部 11 个权限 | 管理员 | +| `editor` | dashboard:view, services:view, alerts:view, status_history:view, status_history:stats | 编辑者 | +| `viewer` | dashboard:view, services:view, status_history:view | 只读 | +| `localadmin` | 硬编码绕过(hasPermission 始终返回 true) | 应急管理员 | + +### 权限列表 + +`dashboard:view`、`services:view`、`services:manage`、`alerts:view`、`alerts:manage`、`status_history:view`、`status_history:stats`、`audit:view`、`users:view`、`users:manage`、`roles:manage` + +--- + +## API 路由 + +### 公开 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/health` | 自身健康检查(返回 `{status:'OK'}`) | + +### 认证 + +登录逻辑:OIDC SSO 优先 + LDAP 本地登录 + localadmin 应急用户。 +登录成功签发 `tlyq_session` cookie(共享 JWT,domain=.tlyq.ai)。 +中间件验证 JWT payload(Edge Runtime,不解密签名,仅解码 payload 检查过期时间)。 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/auth/login/oidc` | OIDC SSO 重定向到 Authelia | +| POST | `/api/auth/login` | LDAP 或 localadmin 登录 | +| GET | `/api/auth/callback` | OIDC 回调处理 | +| POST | `/api/auth/logout` | 登出(清除 cookie,重定向 Authelia end_session) | +| GET | `/api/auth/me` | 当前用户信息 | + +### 服务管理 + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/api/services` | 登录 | 服务列表(含解析后的 JSON checks) | +| POST | `/api/services` | services:manage | 新增服务 | +| PUT | `/api/services/[id]` | services:manage | 更新服务 | +| DELETE | `/api/services/[id]` | services:manage | 删除服务 | +| POST | `/api/services/[id]/check` | services:manage | 手动触发单次健康检查 | + +### 告警渠道 + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/api/alert-channels` | alerts:manage | 渠道列表(webhook_url 脱敏) | +| POST | `/api/alert-channels` | alerts:manage | 新增渠道 | +| PUT | `/api/alert-channels/[id]` | alerts:manage | 更新渠道 | +| DELETE | `/api/alert-channels/[id]` | alerts:manage | 删除渠道 | +| POST | `/api/alert-channels/[id]/test` | alerts:manage | 测试推送 | + +### 告警历史 / 状态历史 + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/api/alerts` | alerts:view | 告警历史(分页) | +| GET | `/api/status-history` | 登录 | 状态变更历史(分页,可按 service_id 筛选) | +| GET | `/api/status` | 登录 | 所有服务实时状态汇总 | + +### 管理 + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/api/admin/worker-status` | dashboard:view | Worker 运行状态 | + +--- + +## Worker 架构 + +### MonitorWorker 核心类 + +位置:`src/lib/monitor-worker.ts` + +``` +MonitorWorker +├── HealthChecker(@shared/lib/alert/health-checker) +│ ├── HttpChecker — HTTP 状态码 + 响应体匹配 +│ └── DockerChecker — docker ps 容器状态检查 +├── AlertManager(@shared/lib/alert/alert-manager) +│ ├── evaluate() — 三阶段告警决策(级别匹配 → 免打扰 → 冷却) +│ └── CooldownProvider — alert_settings 表持久化冷却状态 +└── WeChatPusher(@shared/lib/wechat/wechat-pusher) + └── pushMarkdown() — 企业微信 Webhook 推送 +``` + +### 运行机制 + +| 阶段 | 间隔 | 说明 | +|------|------|------| +| 健康检查 | 30 秒 tick | 遍历所有 enabled 服务,并发执行检查(Promise.allSettled) | +| 抖动检测 | 10 分钟窗口 | 同一服务 10 分钟内状态切换 >= 5 次 → 触发 flapping 告警,抑制后续告警 | +| 持续异常提醒 | 30 分钟 | 服务持续 abnormal 状态,每 30 分钟发送提醒 | +| 恢复通知 | 即时 | 服务恢复正常时发送 recovery 通知,绕过免打扰和冷却 | +| WAL 检查点 | 1 小时 | 被动 WAL checkpoint(PASSIVE) | +| 数据清理 | 每天 | 删除 180 天前的 status_history 和 alert_history(每批 LIMIT 1000) | + +### Docker 入口(entrypoint.sh) + +``` +entrypoint.sh +├── npx tsx scripts/monitor-worker.ts & # 后台启动 Worker +├── node server.js & # 后台启动 Next.js Server +└── 守护循环:任一进程退出则容器退出 # 保证双进程存活 + └── trap SIGTERM/SIGINT → 清理退出 +``` + +--- + +## 认证机制 + +- **OIDC SSO**:通过 Authelia 进行 PKCE 授权码流程,登录成功后签发 `tlyq_session` 共享 JWT cookie +- **LDAP 本地登录**:通过 LLDAP 进行 bind 认证,签发 `tlyq_session` cookie +- **localadmin**:纯本地 BCrypt 认证,不依赖 LLDAP/OIDC,用于应急登录 +- **Edge Runtime 中间件**:`src/middleware.ts` 运行在 Edge Runtime,使用 `atob` 解码 JWT payload 检查过期时间,不验证签名(签名验证在 route handler 层) + +--- + +## 环境配置 + +### 本地与云端差异 + +| 环境变量 | 本地开发 | 云服务器(txjp) | 说明 | +|---------|---------|----------------|------| +| `DATABASE_PATH` | `./data/monitor.db` | `/app/data/monitor.db` | Docker volume 挂载 | +| `MONITOR_MODE` | `dev` | `local` | 运行模式 | +| `AUTHELIA_URL` | `https://127.0.0.1:6180` | `https://sso.tlyq.ai` | Authelia 地址 | +| `OIDC_CLIENT_ID` | `monitor-oidc` | 同 | OIDC 客户端 ID | +| `OIDC_CLIENT_SECRET` | 本地生成的值 | 服务器生成的值 | OIDC 客户端密钥(每个环境独立) | +| `OIDC_REDIRECT_URI` | `http://localhost:6181/api/auth/callback` | `https://monitor.tlyq.ai/api/auth/callback` | OIDC 回调地址 | +| `JWT_SECRET` | `dev-jwt-secret-change-in-production` | 部署脚本自动生成 | JWT 签名密钥 | +| `COOKIE_DOMAIN` | `.tlyq.ai` | 同 | Cookie 域 | +| `LDAP_URL` | `ldap://ldap-ai:3890` | `ldap://ldap-ai:3890` | LLDAP 地址(Docker 内网) | +| `LOCALADMIN_PASSWORD` | `admin123` | 部署脚本自动生成 | 应急管理员密码 | +| `NODE_TLS_REJECT_UNAUTHORIZED` | `0` | `0` | TLS 证书验证(Authelia 自签名) | + +### `.env.local` 示例 + +```bash +DATABASE_PATH=./data/monitor.db +MONITOR_MODE=dev +AUTHELIA_URL=https://127.0.0.1:6180 +OIDC_CLIENT_ID=monitor-oidc +OIDC_CLIENT_SECRET=<本地生成的值> +OIDC_REDIRECT_URI=http://localhost:6181/api/auth/callback +JWT_SECRET=dev-jwt-secret-change-in-production +COOKIE_DOMAIN=.tlyq.ai +LDAP_URL=ldap://ldap-ai:3890 +LOCALADMIN_PASSWORD=admin123 +NODE_TLS_REJECT_UNAUTHORIZED=0 +``` + +--- + +## 共享库引用 + +| 库 | 用途 | 导入路径 | +|------|------|------| +| `@shared/lib/auth/jwt` | JWT 签名/验证(零依赖,Node crypto) | `import { signJwt, verifyJwt } from '@shared/lib/auth/jwt'` | +| `@shared/lib/auth/oidc` | OIDC PKCE 流程 | `import { discoverOidcConfig, buildAuthorizeUrl } from '@shared/lib/auth/oidc'` | +| `@shared/lib/auth/ldap` | LDAP 认证 | `import { ldapAuth } from '@shared/lib/auth/ldap'` | +| `@shared/lib/auth/middleware` | 路由守卫工厂 | `import { createMiddleware } from '@shared/lib/auth/middleware'` | +| `@shared/lib/auth/user-sync` | OIDC 用户同步 | `import { syncOidcUser } from '@shared/lib/auth/user-sync'` | +| `@shared/lib/alert/alert-manager` | 告警决策引擎 | `import { AlertManager } from '@shared/lib/alert/alert-manager'` | +| `@shared/lib/alert/health-checker` | 健康检查引擎 | `import { HealthChecker, HttpChecker, DockerChecker } from '@shared/lib/alert/health-checker'` | +| `@shared/lib/audit/write-audit-log` | 审计日志 | `import { writeAuditLog } from '@shared/lib/audit/write-audit-log'` | +| `@shared/lib/wechat/wechat-pusher` | 企业微信推送 | `import { WeChatPusher } from '@shared/lib/wechat/wechat-pusher'` | +| `@shared/lib/wechat/message-formatter` | 消息格式化 | `import { formatAlertLevel } from '@shared/lib/wechat/message-formatter'` | +| `@shared/lib/db/alert-schema` | 数据库 Schema 常量 | `import { ALL_TABLES_SQL } from '@shared/lib/db/alert-schema'` | +| `@shared/ui` | UI 组件库 | `import { Button, Card, Badge } from '@shared/ui'` | +| `@shared/ui/login-page` | 统一登录页 | `import LoginPage from '@shared/ui/login-page'` | + +> **路径映射**:`tsconfig.json` 中配置 `"@shared/*": ["./shared/*"]`,`shared` 是指向 `../shared` 的符号链接。 + +--- + +## 关键设计决策 + +### execFileSync 数组参数 + +数据库操作使用 `execFileSync('sqlite3', [dbPath, '-json', sql])` 而非 ORM。SQL 通过数组参数传递,避免 shell 注入。所有用户输入通过 `escapeSql()` 函数转义(单引号双写)。 + +### Edge Runtime 中间件 + +`src/middleware.ts` 运行在 Edge Runtime,不能使用 Node.js `crypto` 模块。JWT 验证仅解码 payload 检查过期时间,签名验证留给 route handler 层处理。 + +### 双进程守护 + +`entrypoint.sh` 同时启动 Worker 和 Server,使用 bash 守护循环监控。任一进程退出则整个容器退出,由 Docker 重启策略接管。 + +### 冷却状态持久化 + +告警冷却状态存储在 `alert_settings` 表中(key 格式:`cooldown:{channel_id}:{service_id}:{level}`),容器重启后冷却状态不丢失。 + +--- + +## Docker 部署 + +``` +txjp 服务器 +├── monitor-ai(容器) ← Next.js standalone + Worker,监听 6181 +├── nginx-ai ← 反向代理 monitor.tlyq.ai → monitor-ai:6181 +└── webnet(external) ← 共享网络 +``` + +部署:`bash deploy-monitor.sh`(本地模式)或 `bash deploy-monitor.sh --remote --host `。 + +源码打包上传 → 服务器 `npm install` + `npm run build` → `.next` 挂载进容器生效。 + +### 生产环境变量 + +```bash +DATABASE_PATH=/app/data/monitor.db +MONITOR_MODE=local +AUTHELIA_URL=https://sso.tlyq.ai +OIDC_CLIENT_ID=monitor-oidc +OIDC_CLIENT_SECRET=<部署脚本自动生成> +OIDC_REDIRECT_URI=https://monitor.tlyq.ai/api/auth/callback +JWT_SECRET=<部署脚本自动生成> +COOKIE_DOMAIN=.tlyq.ai +LDAP_URL=ldap://ldap-ai:3890 +LOCALADMIN_PASSWORD=<部署脚本自动生成> +NODE_ENV=production +NODE_TLS_REJECT_UNAUTHORIZED=0 +``` + +--- + +## 开发规范 + +- **新增 API**:在 `src/app/api/` 下创建路由 → 顶部调用 `initDatabase()` → `getCurrentUser()` 验证 → `checkPermission()` 校验 +- **新增页面**:在 `src/app/` 下创建 → 布局由 `client-layout.tsx` 提供(Sidebar + ThemeProvider) +- **权限格式**:`resource:action`,如 `checkPermission(user, 'services:manage')` +- **审计日志**:所有写操作 API(POST/PUT/DELETE)必须添加审计日志: + ```typescript + import { writeAuditLog } from '@/lib/audit' + + writeAuditLog({ + userId: user.id, + action: 'create' | 'update' | 'delete', + entityType: 'service' | 'alert_channel' | 'user', + entityId: id, + details: { ... }, + ipAddress: getClientIP(request) + }) + ``` +- **日期处理(时区规范)**:整个系统统一使用 UTC+8(北京时间)。必须遵守: + 1. **JavaScript/TypeScript**:禁止使用 `Date.toISOString()` 格式化本地日期,应使用本地时间方法拼接 + 2. **SQLite**:所有 `datetime('now')` 必须写成 `datetime('now', '+8 hours')` + 3. **INSERT 时间列**:数据库表 DEFAULT 已统一为 `datetime('now', '+8 hours')`,INSERT 时可省略 + +--- + +## 健康检查兜底 + +`scripts/monitor-watchdog.sh` 是独立于 monitor-ai 的兜底脚本,通过 crontab 每 5 分钟执行: + +```bash +*/5 * * * * /root/docker/monitor-ai/scripts/monitor-watchdog.sh >> /var/log/monitor-watchdog.log 2>&1 +``` + +当 `/api/health` 连续失败时,直接通过企业微信 Webhook 发送告警,并尝试重启容器。 + +--- + +## 故障排查 + +```bash +# 容器日志 +ssh txjp "docker logs monitor-ai" + +# Worker 日志(实时) +ssh txjp "docker logs -f monitor-ai 2>&1 | grep -i worker" + +# 数据库查看 +ssh txjp "docker exec monitor-ai sqlite3 /app/data/monitor.db 'SELECT id, name, current_status FROM services;'" + +# 健康检查 +ssh txjp "curl -s -k https://monitor.tlyq.ai/api/health" + +# 手动触发检查 +ssh txjp "docker exec monitor-ai sqlite3 /app/data/monitor.db 'UPDATE services SET current_status=\"unknown\";'" + +# 重建镜像(新增依赖后) +ssh txjp "cd /root/docker/monitor-ai && docker compose build --no-cache && docker compose down && docker compose up -d" +``` + +--- + +## Git Tag 规范 + +使用日期版本号 `vYYYY.MM.DD`(如 `v2026.07.01`)。提交后打 tag 再推送: + +```bash +git tag v$(date +%Y.%m.%d) && git push origin main && git push origin v$(date +%Y.%m.%d) +``` + +同一天多次提交只打一个 tag。详见根目录 `CLAUDE.md`。 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..21cd9c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# monitor-ai Dockerfile +FROM node:22-alpine AS base +WORKDIR /app + +# 1. 依赖安装 +COPY package.json package-lock.json ./ +RUN npm ci --production=false + +# 2. 复制共享库 +COPY ../shared/ ./shared/ + +# 3. 复制源码并构建 +COPY . . +RUN npm run build + +# 4. 生产镜像 +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=base /app/.next/standalone ./ +COPY --from=base /app/.next/static ./.next/static +COPY --from=base /app/scripts ./scripts +COPY --from=base /app/shared ./shared +COPY entrypoint.sh ./ + +RUN npm install -g tsx +EXPOSE 6181 +CMD ["sh", "entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..00450b2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' +services: + monitor-ai: + build: . + container_name: monitor-ai + restart: unless-stopped + ports: + - "6181:6181" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./data:/app/data + environment: + - NODE_ENV=production + - DATABASE_PATH=/app/data/monitor.db + - MONITOR_MODE=local + networks: + - webnet + init: true + +networks: + webnet: + external: true diff --git a/docs/completed/LOOP-STATE-monitor-ai-full.md b/docs/completed/LOOP-STATE-monitor-ai-full.md new file mode 100644 index 0000000..a05d7a3 --- /dev/null +++ b/docs/completed/LOOP-STATE-monitor-ai-full.md @@ -0,0 +1,44 @@ +# LOOP-STATE — monitor-ai Full Review + +## 审查状态 — monitor-ai Phase: 全量审查 + +### 第 1 轮 +- 日期: 2026-07-01 +- 发现: 2 CRITICAL, 3 HIGH, 3 MEDIUM +- 修复状态: 已全部修复 + +#### 问题清单 +| 编号 | 严重度 | 文件:行号 | 描述 | 状态 | +|------|--------|-----------|------|------| +| C1 | CRITICAL | src/app/api/auth/login/route.ts | localadmin 密码未加密存储,使用明文比较 | ✅ 已修复 | +| C2 | CRITICAL | shared/lib/auth/oidc.ts | nonce 未从 ID token 解析验证 | ✅ 已修复 | +| H2 | HIGH | src/lib/monitor-worker.ts:88-89 | 日期计算未考虑 UTC+8 时区 | ✅ 已修复 | +| H6 | HIGH | src/lib/monitor-worker.ts:144-158 | 字段名映射未处理 SQLite snake_case | ✅ 已修复 | +| H7 | HIGH | src/app/api/services/route.ts:40 | 审计日志缺少 userId/username | ✅ 已修复 | +| M1 | MEDIUM | src/lib/monitor-worker.ts:35,111 | timestamp 字段未使用 escapeSql | ✅ 已修复 | +| M3 | MEDIUM | src/lib/db.ts:45 | 硬编码 webhook URL 未清空 | ✅ 已修复 | + +### 第 2 轮 +- 日期: 2026-07-01 +- 发现: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 1 LOW +- 结论: **通过** — 连续两轮零 CRITICAL/HIGH 问题 + +#### 验证结果 +| 修复项 | 验证证据 | 状态 | +|--------|----------|------| +| C1 | route.ts:3 import bcrypt, route.ts:32 bcrypt.compareSync, db.ts:39-41 hashSync | ✅ 通过 | +| C2 | oidc.ts:76-82 Buffer.from(..., 'base64url') 解析 nonce | ✅ 通过 | +| H2 | monitor-worker.ts:89 `new Date(now.getTime() + 8 * 3600000)` | ✅ 通过 | +| H6 | monitor-worker.ts:147-157 所有 snake_case 字段正确映射 | ✅ 通过 | +| H7 | services/route.ts:40 userId/username 已添加 | ✅ 通过 | +| M1 | monitor-worker.ts:2 import escapeSql, 多处使用 | ✅ 通过 | +| M3 | db.ts:45 webhook_url = '' | ✅ 通过 | + +#### 新发现问题 +| 编号 | 严重度 | 文件:行号 | 描述 | 状态 | +|------|--------|-----------|------|------| +| LOW-1 | LOW | services/route.ts:40 | payload.sub 类型断言可优化 | 待优化(不影响功能)| + +--- + +**审查结论:连续两轮通过,审查结束。** diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..01e9b36 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e +# entrypoint.sh — 启动 Next.js standalone server + Monitor Worker + +echo "[entrypoint] Starting monitor-ai..." + +# 启动 MonitorWorker(后台,tsx 即时执行 TypeScript) +npx tsx scripts/monitor-worker.ts & +WORKER_PID=$! + +# 启动 Next.js standalone server +node server.js & +NEXT_PID=$! + +echo "[entrypoint] Worker PID: $WORKER_PID, Next.js PID: $NEXT_PID" + +# 信号处理 +cleanup() { + echo "[entrypoint] Shutting down..." + kill $WORKER_PID $NEXT_PID 2>/dev/null + wait + exit 0 +} +trap cleanup SIGTERM SIGINT + +# 守护循环:任一进程退出则容器退出 +while true; do + if ! kill -0 $WORKER_PID 2>/dev/null; then + echo "[entrypoint] Worker process exited" >&2 + exit 1 + fi + if ! kill -0 $NEXT_PID 2>/dev/null; then + echo "[entrypoint] Next.js process exited" >&2 + exit 1 + fi + sleep 5 +done diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..18406de --- /dev/null +++ b/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from 'next' + +const config: NextConfig = { + output: 'standalone', + transpilePackages: ['lucide-react'], +} + +export default config diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0fbb705 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2508 @@ +{ + "name": "monitor-ai", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "monitor-ai", + "version": "1.0.0", + "dependencies": { + "@tailwindcss/postcss": "^4.3.2", + "autoprefixer": "^10.5.2", + "bcryptjs": "^3.0.3", + "ldapts": "^6.0.0", + "lucide-react": "^1.8.0", + "next": "^15.0.0", + "openid-client": "^5.7.1", + "postcss": "^8.5.16", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.3.2", + "tsx": "^4.0.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz", + "integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz", + "integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz", + "integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz", + "integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz", + "integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz", + "integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz", + "integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz", + "integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz", + "integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "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.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", + "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "postcss": "^8.5.15", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@types/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "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.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "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.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "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/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "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.npmjs.org/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.383", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz", + "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/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.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/ldapts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-6.0.0.tgz", + "integrity": "sha512-VI0+pXgs8Ul135oM0SV0LfQj0Ljj0IgktzAIf2aNUBvmu8mIm/Y5ZugwsP+s47is6rd7uX5i9CeQG0t3Y60OAw==", + "license": "MIT", + "dependencies": { + "@types/asn1": ">=0.2.1", + "@types/node": ">=16", + "@types/uuid": ">=9", + "asn1": "~0.2.6", + "debug": "~4.3.4", + "strict-event-emitter-types": "~2.0.0", + "uuid": "~9.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lucide-react": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.22.0.tgz", + "integrity": "sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/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/next": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", + "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.19", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.19", + "@next/swc-darwin-x64": "15.5.19", + "@next/swc-linux-arm64-gnu": "15.5.19", + "@next/swc-linux-arm64-musl": "15.5.19", + "@next/swc-linux-x64-gnu": "15.5.19", + "@next/swc-linux-x64-musl": "15.5.19", + "@next/swc-win32-arm64-msvc": "15.5.19", + "@next/swc-win32-x64-msvc": "15.5.19", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b34f3ae --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "monitor-ai", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 6181", + "build": "next build", + "start": "next start -p 6181" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.3.2", + "autoprefixer": "^10.5.2", + "bcryptjs": "^3.0.3", + "ldapts": "^6.0.0", + "lucide-react": "^1.8.0", + "next": "^15.0.0", + "openid-client": "^5.7.1", + "postcss": "^8.5.16", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.3.2", + "tsx": "^4.0.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.0.0" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..7059fe9 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,6 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +export default config; diff --git a/preview/dashboard.html b/preview/dashboard.html new file mode 100644 index 0000000..fb77542 --- /dev/null +++ b/preview/dashboard.html @@ -0,0 +1,1260 @@ + + + + + +monitor-ai — 告警监控中心 (预览) + + + +
+ + + + +
+
+
+
+ monitor-ai / 仪表盘 +
+
+ + Worker: 运行中 · 自动刷新: 30s + + +
+
+
+ +
+ + +
+ + +
+
+
+ 正常服务 +
+
+
10
+
共 11 个监控服务
+
+
+
+ 异常服务 +
+
+
1
+
Authelia SSO 异常
+
+
+
+ 未知状态 +
+
+
0
+
所有服务已检查
+
+
+
+ 最后检查 +
+
+
3 秒前
+
2026-07-01 14:35:00
+
+
+ + +
+
+ 服务状态 + +
+
+
+ + +
+
+
+ + Authelia SSO + critical +
+
+ 异常 5m 23s + +
+
+
+
+
+ + HTTP GET http://authelia:9091/api/health + 连接超时 (10s) +
+
+ + Docker check: authelia + running +
+
+
+
+ + +
+
+
+ + Redis + critical +
+
+ 正常 + +
+
+
+
+
+ + Docker check: redis + running +
+
+
+
+ +
+
+
+ + LLDAP + critical +
+
正常
+
+
+
+
HTTP GET http://ldap-ai:17170200 OK
+
Docker check: ldap-airunning
+
+
+
+ +
OA 门户warning
正常
+
资产管理warning
正常
+
工单系统warning
正常
+
Gitea 代码托管warning
正常
+
Nginx 代理critical
正常
+
官网 (www-ai)warning
正常
+
云平台 (cloud-ai)warning
正常
+
Token 工厂 (token-ai)warning
正常
+ +
+
+
+
+ + +
+ + +
+
+
+ 正常服务 +
+
+
10
+
共 11 个监控服务
+
+
+
+ 异常服务 +
+
+
1
+
Authelia SSO 异常
+
+
+
+ 未知状态 +
+
+
0
+
所有服务已检查
+
+
+
+ 最后检查 +
+
+
3 秒前
+
2026-07-01 14:35:00
+
+
+ + +
+ 服务状态 + 点击卡片展开检查详情 +
+ +
+ + +
+
+ Authelia SSO + critical +
+
5m 23s
+
异常持续时长
+
HTTP GET http://authelia:9091/api/health → 连接超时 (10s)
+
+
HTTP 检查连接超时
+
Docker 检查running
+
+
+ + +
+
+ Redis + critical +
+
正常
+
运行中
+
Docker checkrunning
+
+ +
+
+ LLDAP + critical +
+
正常
+
HTTP + Docker 均正常
+
HTTP GET http://ldap-ai:17170200 OK
Docker checkrunning
+
+ +
OA 门户warning
正常
运行中
+ +
资产管理warning
正常
运行中
+ +
工单系统warning
正常
运行中
+ +
Gitea 代码托管warning
正常
运行中
+ +
Nginx 代理critical
正常
运行中
+ +
官网 (www-ai)warning
正常
运行中
+ +
云平台 (cloud-ai)warning
正常
运行中
+ +
Token 工厂 (token-ai)warning
正常
运行中
+ +
+
+ + +
+ + +
+

服务状态监控 · 11 个服务

+

实时健康检查 · 本月可用率 99.8% · 30 秒轮询

+
+ + +
+
+ 正常 + 10 + 共 11 个监控服务 +
+
+ 异常 + 1 + Authelia SSO +
+
+ 未知 + 0 +
+
+ 最后检查 + 3 秒前 + Worker 运行中 · 30s 轮询 +
+
+ + +
+
+ 本月可用率 + 99.8% + 本月故障 3 次 +
+
+ 平均恢复时间 + 2m 30s + 较上月缩短 15s +
+
+ 本月告警 + 12 + 推送成功率 95.8% +
+
+ 活跃渠道 + 2 + 运维群 · 开发群 +
+
+ +
+ +
+
Authelia SSOcritical
5m 23s
异常持续时长
HTTP 连接超时 (10s)
HTTP 检查超时
Docker 检查running
+
Rediscritical
运行中
+
LLDAPcritical
运行中
+
OA 门户warning
运行中
+
资产管理warning
运行中
+
工单系统warning
运行中
+
Giteawarning
运行中
+
Nginxcritical
运行中
+
www-aiwarning
运行中
+
cloud-aiwarning
运行中
+
token-aiwarning
运行中
+
+
+ + +
+ + +
+
+ +
10
正常
+
+
+
+ +
1
异常
+
+
+
+ +
0
未知
+
+
+ 最后检查 3 秒前 · Worker 运行中 · 自动刷新 30s +
+
+ + +
+ 服务状态 + 11 个服务 · 点击卡片展开详情 +
+ +
+ +
+
+ Authelia SSO + critical +
+
5m 23s
+
异常持续时长
+
HTTP GET authelia:9091/api/health → 连接超时 (10s)
+
+
HTTP 检查连接超时
+
Docker 检查running
+
+ +
+ +
Rediscritical
正常
Docker: running
+ +
LLDAPcritical
正常
HTTP + Docker 均正常
+ +
OA 门户warning
正常
HTTP: 200 OK
+ +
资产管理warning
正常
HTTP: 200 OK
+ +
工单系统warning
正常
HTTP: 200 OK
+ +
Gitea 代码托管warning
正常
HTTP + Docker 均正常
+ +
Nginx 代理critical
正常
Docker: running
+ +
官网 (www-ai)warning
正常
HTTP: 200 OK
+ +
云平台 (cloud-ai)warning
正常
HTTP: 200 OK
+ +
Token 工厂 (token-ai)warning
正常
HTTP: 200 OK
+ +
+
+ + +
+
+ + +
+
+

服务状态监控

+

实时健康检查 · 11 个服务 · 30 秒轮询

+
+
+ + Worker 运行中 + + +
+
+ + +
+
+ 正常 + 10 + 90.9% 可用率 +
+
+
+ 异常 + 1 + Authelia SSO +
+
+
+ 未知 + 0 +
+
+ 最后检查 + 3 秒前 + 2026-07-01 14:35:00 +
+
+ + +
+ + +
+
+ Authelia SSO + critical +
+
+ 5m 23s + HTTP 检查失败:连接超时 (10s)
Docker 检查正常
+
+ +
+
HTTP GET authelia:9091/api/health — 连接超时
+
Docker check — running
+
+
+ + +
Rediscritical
正常Docker · running
Docker check — running
+ +
LLDAPcritical
正常HTTP · 200 OK
Docker · running
+ +
OA 门户warning
正常HTTP · 200 OK
+ +
资产管理warning
正常HTTP · 200 OK
+ +
工单系统warning
正常HTTP · 200 OK
+ +
Giteawarning
正常HTTP · 200 OK
Docker · running
+ +
Nginxcritical
正常Docker · running
+ +
www-aiwarning
正常HTTP · 200 OK
+ +
cloud-aiwarning
正常HTTP · 200 OK
+ +
token-aiwarning
正常HTTP · 200 OK
+ +
+
+
+ + +
+
+
+ + +
+

Infrastructure Status · 11 services

+

Real-time health monitoring · 30s polling · 99.8% uptime this month

+
+ + +
+
+ Healthy + 10 + 90.9% available +
+
+ Degraded + 1 + Authelia SSO +
+
+ Unknown + 0 +
+
+ + +
+ + +
+
+ Authelia SSO + critical +
+
5m 23s
+
HTTP connection timeout after 10s.
Docker container running normally.
+ +
+
http://authelia:9091/api/health — timeout
+
docker ps authelia — running
+
+
+ + +
Rediscritical
Healthy
Docker container running
docker ps redis — Up 5 days
+ +
LLDAPcritical
Healthy
HTTP 200 · Docker running
+ +
OAwarning
Healthy
HTTP 200 OK
+ +
Assetswarning
Healthy
HTTP 200 OK
+ +
Issuewarning
Healthy
HTTP 200 OK
+ +
Giteawarning
Healthy
HTTP 200 · Docker running
+ +
Nginxcritical
Healthy
Docker running
+ +
www-aiwarning
Healthy
+ +
cloud-aiwarning
Healthy
+ +
token-aiwarning
Healthy
+ +
+ + + +
+
+
+ + +
+

告警渠道设置

+ +
+ 推送通道: +
运维群 正常
+
开发群 正常
+
备用群 连续 3 次失败
+
+ +
+
+ 渠道 1: 运维群 +
+ + +
+
+
+
+ + +
+
+
+ + + + +
+
+ + +
+
+ + +
+ + + +
+ +
+
+
+ +
+
+
+ +
+
+ 渠道 2: 开发群 +
+ + +
+
+
+
+ + +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+ + +
+

告警历史

+
+
+
+ + + +
+ + + + + + + + + + + + + + + + +
时间级别渠道服务类型结果
14:35:00critical运维群Authelia SSO异常告警
13:20:15critical运维群Redis恢复通知
13:17:30critical运维群Redis异常告警
13:02:00critical开发群Redis异常告警
09:45:00warning运维群Nginx 代理抖动告警
+
+ + + + +
+
+
+
+ + +
+

状态变更历史

+
+
+
+ + +
+ + + + + + + + + + + + + +
服务异常开始恢复时间持续关联告警
Authelia SSO14:30:00进行中1 条
Redis13:17:3013:20:152m45s2 条
Nginx 代理09:40:0009:45:005m00s1 条(抖动)
+
+
+
本月可用率
+
99.8%
+
+
+
平均恢复时间
+
2 分 30 秒
+
+
+
+
+
+ + +
+

服务管理

+
+
+ 监控服务清单 + +
+
+ + + + + + + + + + + + + + +
名称类别告警级别检查方式状态操作
Authelia SSOendpointcriticalHTTP + Docker异常
RediscontainercriticalDocker正常
OA 门户endpointwarningHTTP正常
+
+
+
+ + +
+

用户管理

+
+
+ + + + + + + + + + + + + +
用户名显示名邮箱角色操作
localadmin超级管理员admin不可修改
gaoxiaopei高晓沛gaoxiaopei@tlyq.ai
zhangsan张三zhangsan@tlyq.aiviewer
+
+
+
+ +
+
+
+ + +
+
提示
+
+
+ + + + diff --git a/scripts/deploy-monitor.sh b/scripts/deploy-monitor.sh new file mode 100755 index 0000000..a578c7e --- /dev/null +++ b/scripts/deploy-monitor.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env bash +# monitor-ai 部署脚本 +# +# 用法: +# bash deploy-monitor.sh # 本地模式(默认) +# bash deploy-monitor.sh --dev # 开发模式(本地构建) +# bash deploy-monitor.sh --remote --host # 远程部署到指定服务器 +# +# 首次部署自动: +# 1. 生成 OIDC_CLIENT_SECRET (openssl rand -hex 32) +# 2. 生成 LOCALADMIN_PASSWORD (openssl rand -hex 16) +# 3. 生成 pbkdf2 密码哈希(通过 Authelia 容器) +# 4. 更新 nginx 配置 +# +# 支持系统: +# - macOS (本地): zsh + bash 兼容模式 +# - Ubuntu/Debian (服务器): dash + bash +# - Rocky Linux/CentOS/RHEL (服务器): bash + +# ============================================================ +# 颜色和日志 +# ============================================================ +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m' +log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; } +info() { printf "${CYAN}[i]${NC} %s\n" "$1"; } +error() { printf "${RED}[✗]${NC} %s\n" "$1"; } +timing(){ printf " ${CYAN}⏱${NC} %s\n" "$1"; } + +# ============================================================ +# 检测 macOS 和 Linux sed 差异 +# ============================================================ +is_mac() { + [[ "$(uname)" == "Darwin" ]] +} + +SED_I_BACKUP='-i' +if is_mac; then + SED_I_BACKUP='-i ""' +fi + +# ============================================================ +# 解析参数 +# ============================================================ +MODE="local" +HOST="" +while [[ $# -gt 0 ]]; do + case "$1" in + --remote) + MODE="remote" + info "模式: 远程部署" + shift + ;; + --host) + HOST="$2" + info "目标主机: $HOST" + shift 2 + ;; + --dev) + MODE="dev" + info "模式: 开发构建" + shift + ;; + *) + warn "未知参数: $1" + shift + ;; + esac +done + +# ============================================================ +# 路径配置 +# ============================================================ +LOCAL_DIR="/Users/niuniu/programs/docker" +MONITOR_DIR="$LOCAL_DIR/monitor-ai" +SHARED_DIR="$LOCAL_DIR/shared" +REMOTE_DIR="/root/docker/monitor-ai" +CONTAINER="monitor-ai" +SSH_TARGET="txjp" +SNAPSHOT_FILE="/tmp/.snapshot.monitor.md5" + +# 远程模式需要 host 参数 +if [[ "$MODE" == "remote" && -z "$HOST" ]]; then + error "远程部署需要指定 --host " + echo " 用法: bash deploy-monitor.sh --remote --host 43.133.38.210" + exit 1 +fi + +# ============================================================ +# 打包源码 +# ============================================================ +package_source() { + log "打包源码..." + local pack_start + pack_start=$(date +%s) + + COPYFILE_DISABLE=1 tar czf /tmp/monitor-ai.tar.gz \ + -C "$LOCAL_DIR" \ + --exclude='node_modules' --exclude='.next' --exclude='out' \ + --exclude='data' --exclude='docs' \ + --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ + --exclude='.git' --exclude='._*' \ + shared/ monitor-ai/ || { + error "打包失败" + return 1 + } + + local pack_end pkg_size pack_dur + pack_end=$(date +%s) + pack_dur=$((pack_end - pack_start)) + pkg_size=$(du -h /tmp/monitor-ai.tar.gz 2>/dev/null | cut -f1 || echo "未知") + timing "打包耗时: ${pack_dur}s (${pkg_size})" +} + +# ============================================================ +# 首次部署:生成密钥和配置 +# ============================================================ +first_deploy_setup() { + local target="$1" # SSH target or "local" + local ssh_cmd="" + if [[ "$target" != "local" ]]; then + ssh_cmd="ssh $target" + fi + + local run + if [[ -n "$ssh_cmd" ]]; then + run="$ssh_cmd" + else + run="" + fi + + info "首次部署检查..." + + # 检查 .env 是否存在 + local env_exists + if [[ -n "$run" ]]; then + env_exists=$($run "test -f $REMOTE_DIR/.env && echo yes || echo no" 2>/dev/null) + else + env_exists=$(test -f "$MONITOR_DIR/.env" && echo yes || echo no) + fi + + if [[ "$env_exists" == "yes" ]]; then + info "已存在 .env,跳过密钥生成" + return 0 + fi + + log "首次部署:生成密钥和配置..." + + local OIDC_SECRET LOCALADMIN_PASS JWT_SECRET PBKDF2_HASH + OIDC_SECRET=$(openssl rand -hex 32) + LOCALADMIN_PASS=$(openssl rand -hex 16) + JWT_SECRET=$(openssl rand -hex 32) + + # 生成 pbkdf2 密码哈希(通过 Authelia 容器或本地 openssl) + if [[ -n "$run" ]]; then + PBKDF2_HASH=$($run "docker exec authelia authelia crypto hash generate pbkdf2 --password '$LOCALADMIN_PASS' 2>/dev/null | grep 'Digest:' | awk '{print \$2}'" 2>/dev/null) + fi + + # 如果 Authelia 不可用,使用 openssl 生成备用哈希 + if [[ -z "$PBKDF2_HASH" ]]; then + warn "Authelia 不可用,使用 openssl 生成密码哈希" + PBKDF2_HASH=$(echo -n "$LOCALADMIN_PASS" | openssl dgst -sha256 -binary | openssl base64) + PBKDF2_HASH="pbkdf2:sha256:600000\$$(openssl rand -hex 16)\$$PBKDF2_HASH" + fi + + # 写入 .env 文件 + local env_content + env_content="DATABASE_PATH=/app/data/monitor.db +MONITOR_MODE=local +AUTHELIA_URL=https://sso.tlyq.ai +OIDC_CLIENT_ID=monitor-oidc +OIDC_CLIENT_SECRET=$OIDC_SECRET +OIDC_REDIRECT_URI=https://monitor.tlyq.ai/api/auth/callback +JWT_SECRET=$JWT_SECRET +COOKIE_DOMAIN=.tlyq.ai +LDAP_URL=ldap://ldap-ai:3890 +LOCALADMIN_PASSWORD=$LOCALADMIN_PASS +NODE_ENV=production +NODE_TLS_REJECT_UNAUTHORIZED=0" + + if [[ -n "$run" ]]; then + $run "cat > $REMOTE_DIR/.env << 'ENVEOF' +$env_content +ENVEOF" + else + echo "$env_content" > "$MONITOR_DIR/.env" + fi + + log "密钥已生成" + info " OIDC_CLIENT_SECRET: $OIDC_SECRET" + info " LOCALADMIN_PASSWORD: $LOCALADMIN_PASS" + warn "请妥善保管以上密钥,.env 文件不会上传到版本控制" +} + +# ============================================================ +# 更新 nginx 配置 +# ============================================================ +update_nginx() { + local target="$1" + local nginx_conf="$LOCAL_DIR/nginx-proxy-ai/conf.d/monitor-ai.conf" + + if [[ ! -f "$nginx_conf" ]]; then + warn "nginx 配置文件不存在: $nginx_conf" + return 0 + fi + + if [[ "$target" == "local" ]]; then + return 0 + fi + + log "更新 nginx 配置..." + scp "$nginx_conf" "$target:$REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf" 2>/dev/null || true + ssh "$target" "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败" +} + +# ============================================================ +# 服务器构建 +# ============================================================ +build_on_server() { + local target="$1" + + # 1. 计算源码快照 + log "检查源码是否有变化..." + local local_md5 + local_md5=$(find "$MONITOR_DIR" "$SHARED_DIR" \ + -not -path '*/node_modules/*' \ + -not -path '*/.next/*' \ + -not -path '*/out/*' \ + -not -path '*/data/*' \ + -not -path '*/docs/*' \ + -not -path '*/.env*' \ + -not -path '*/.git/*' \ + -not -name '._*' \ + -type f \ + -exec md5 -q {} \; 2>/dev/null \ + | sort \ + | md5 -q) + echo " 源码快照: ${local_md5}" + + local prev_md5 + prev_md5=$(ssh "$target" "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "") + + if [[ -z "$prev_md5" ]]; then + info "首次部署,执行完整构建" + elif [[ "$prev_md5" == "$local_md5" ]]; then + info "源码无变化,跳过构建,仅重启容器" + ssh "$target" "cd $REMOTE_DIR && docker compose down && docker compose up -d" + log "容器已重建" + return 0 + else + info "检测到源码变化,执行增量构建" + fi + + # 2. 打包源码 + package_source + + # 3. 上传 + log "上传源码包..." + scp /tmp/monitor-ai.tar.gz "$target:/tmp/monitor-ai.tar.gz" || { + error "上传失败" + return 1 + } + + # 4. 解压源码到 /tmp,然后 rsync 到目标目录 + log "解压源码并准备依赖..." + ssh "$target" "\ + rm -rf /tmp/deploy_monitor && mkdir -p /tmp/deploy_monitor && \ + tar xzf /tmp/monitor-ai.tar.gz -C /tmp/deploy_monitor 2>/dev/null || true && \ + rsync -a --delete \ + --exclude='node_modules' --exclude='.next' --exclude='data' --exclude='docs' \ + --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ + --exclude='._*' \ + /tmp/deploy_monitor/monitor-ai/ $REMOTE_DIR/ && \ + rsync -a --delete \ + --exclude='node_modules' --exclude='.git' \ + /tmp/deploy_monitor/shared/ /root/docker/shared/ && \ + rm -rf /tmp/deploy_monitor" + + # 5. 首次部署:上传 docker-compose.yml 和 Dockerfile + ssh "$target" "cd $REMOTE_DIR && docker compose up -d" + + # 如果 host 上没有 node_modules,从容器内复制一份 + ssh "$target" "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then + echo ' 首次:复制容器内 node_modules 到主机...' + docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules + echo ' 完成' + fi" + + # 6. 安装可能新增的依赖 + log "安装新增依赖..." + ssh "$target" "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true" + + # 7. 清理可能被上传的本地环境变量文件 + ssh "$target" "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'" + + # 8. 服务器上执行 npm run build + log "服务器上执行 npm run build..." + local build_start build_end build_dur + build_start=$(date +%s) + ssh "$target" "cd ${REMOTE_DIR} && npm run build 2>&1" | \ + grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \ + tail -15 + local build_exit_code=${PIPESTATUS[0]} + build_end=$(date +%s) + build_dur=$((build_end - build_start)) + timing "构建耗时: ${build_dur}s" + + if [[ "$build_exit_code" -ne 0 ]]; then + warn "构建可能有警告,请检查上面的输出" + fi + + # 9. 重建容器 + log "重建容器..." + ssh "$target" "cd $REMOTE_DIR && docker compose up -d && docker compose restart" + + # 10. 清理构建缓存 + log "清理构建缓存..." + ssh "$target" "docker image prune -f 2>/dev/null || true" + + local disk_usage + disk_usage=$(ssh "$target" "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50") + if [ "$disk_usage" -gt 95 ]; then + warn "磁盘使用率 ${disk_usage}%,执行紧急清理" + ssh "$target" "docker builder prune -f 2>/dev/null || true" + elif [ "$disk_usage" -gt 85 ]; then + info "磁盘使用率 ${disk_usage}%,执行激进清理(6小时)" + ssh "$target" "docker builder prune -f --filter 'until=6h' 2>/dev/null || true" + elif [ "$disk_usage" -gt 70 ]; then + info "磁盘使用率 ${disk_usage}%,执行正常清理(24小时)" + ssh "$target" "docker builder prune -f --filter 'until=24h' 2>/dev/null || true" + else + info "磁盘使用率 ${disk_usage}%,执行保守清理(48小时)" + ssh "$target" "docker builder prune -f --filter 'until=48h' 2>/dev/null || true" + fi + + # 11. 保存快照 + ssh "$target" "echo '$local_md5' > ${SNAPSHOT_FILE}" +} + +# ============================================================ +# 开发模式(本地构建) +# ============================================================ +build_dev() { + log "本地开发模式构建..." + local build_start build_end build_dur + build_start=$(date +%s) + + cd "$MONITOR_DIR" || { error "无法进入 $MONITOR_DIR"; exit 1; } + npm run build 2>&1 | tail -15 + local build_exit_code=${PIPESTATUS[0]} + build_end=$(date +%s) + build_dur=$((build_end - build_start)) + timing "构建耗时: ${build_dur}s" + + if [[ "$build_exit_code" -ne 0 ]]; then + error "构建失败" + exit 1 + fi + + log "本地构建完成" +} + +# ============================================================ +# 执行部署 +# ============================================================ +echo "" +printf "${CYAN}=========================================${NC}\n" +printf "${CYAN} monitor-ai 部署${NC}\n" +printf "${CYAN}=========================================${NC}\n" +echo "" + +BUILD_START=$(date +%s) + +case "$MODE" in + local) + # 本地模式:部署到 txjp 服务器(默认行为) + log "部署目标: $MONITOR_DIR → txjp:$REMOTE_DIR" + echo "" + + # 首次部署检查 + first_deploy_setup "$SSH_TARGET" + + # 构建 + build_on_server "$SSH_TARGET" + + # 更新 nginx + update_nginx "$SSH_TARGET" + ;; + + remote) + # 远程模式:部署到指定服务器 + log "部署目标: $MONITOR_DIR → $HOST:$REMOTE_DIR" + echo "" + + # 首次部署检查 + first_deploy_setup "$HOST" + + # 构建 + build_on_server "$HOST" + + # 更新 nginx + update_nginx "$HOST" + ;; + + dev) + # 开发模式:仅本地构建 + build_dev + ;; +esac + +# ============================================================ +# 验证 +# ============================================================ +echo "" +log "验证部署..." +BUILD_END=$(date +%s) +TOTAL_DUR=$((BUILD_END - BUILD_START)) + +if [[ "$MODE" == "dev" ]]; then + log "开发构建完成,总耗时: ${TOTAL_DUR}s" + exit 0 +fi + +STATUS=$(ssh "$SSH_TARGET" "curl -s -o /dev/null -w '%{http_code}' -k 'https://monitor.tlyq.ai/api/health' 2>/dev/null" 2>/dev/null || echo "???") +if [[ "$STATUS" == "200" ]]; then + log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 https://monitor.tlyq.ai" + exit 0 +else + warn "返回状态码: $STATUS,请检查" + exit 1 +fi diff --git a/scripts/monitor-watchdog.sh b/scripts/monitor-watchdog.sh new file mode 100755 index 0000000..b470b67 --- /dev/null +++ b/scripts/monitor-watchdog.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# monitor-ai 健康检查兜底脚本 +# +# 用途:当 monitor-ai 自身的健康检查失败时,通过 webhook 直接发送告警 +# 部署:添加到 crontab,每 5 分钟执行一次 +# +# crontab 配置: +# */5 * * * * /root/docker/monitor-ai/scripts/monitor-watchdog.sh >> /var/log/monitor-watchdog.log 2>&1 +# +# 日志文件:/var/log/monitor-watchdog.log(建议配合 logrotate) + +set -euo pipefail + +# ============================================================ +# 配置 +# ============================================================ +HEALTH_URL="${MONITOR_HEALTH_URL:-https://monitor.tlyq.ai/api/health}" +TIMEOUT=10 +MAX_RETRIES=2 +RETRY_DELAY=5 + +# 企业微信 Webhook(从 .env 读取或使用环境变量) +MONITOR_DIR="/root/docker/monitor-ai" +if [[ -f "$MONITOR_DIR/.env" ]]; then + # 从 .env 读取第一个启用的 webhook URL + WEBHOOK_URL="${WEBHOOK_URL:-$(grep -E '^WEBHOOK_URL=' "$MONITOR_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)}" +fi + +# 如果没有配置 webhook,尝试从数据库读取 +if [[ -z "${WEBHOOK_URL:-}" ]]; then + DB_PATH="${MONITOR_DIR}/data/monitor.db" + if [[ -f "$DB_PATH" ]]; then + WEBHOOK_URL=$(sqlite3 "$DB_PATH" "SELECT webhook_url FROM alert_channels WHERE enabled=1 LIMIT 1;" 2>/dev/null || echo "") + fi +fi + +# 日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +# ============================================================ +# 健康检查 +# ============================================================ +check_health() { + local attempt=0 + local http_code="" + + while [[ $attempt -lt $MAX_RETRIES ]]; do + attempt=$((attempt + 1)) + + http_code=$(curl -s -o /dev/null -w '%{http_code}' \ + --connect-timeout "$TIMEOUT" \ + --max-time "$TIMEOUT" \ + "$HEALTH_URL" 2>/dev/null) || http_code="000" + + if [[ "$http_code" == "200" ]]; then + return 0 + fi + + if [[ $attempt -lt $MAX_RETRIES ]]; then + log "健康检查失败 (HTTP $http_code),${RETRY_DELAY}s 后重试..." + sleep "$RETRY_DELAY" + fi + done + + return 1 +} + +# ============================================================ +# 发送告警 +# ============================================================ +send_alert() { + local message="$1" + + if [[ -z "${WEBHOOK_URL:-}" ]]; then + log "WARNING: 未配置企业微信 Webhook URL,无法发送告警" + return 1 + fi + + local payload + payload=$(cat < 告警时间: $(date '+%Y-%m-%d %H:%M:%S')\n\n**健康检查地址**: ${HEALTH_URL}\n\n**失败原因**: ${message}\n\n**处理建议**: 请检查 monitor-ai 容器状态\n\n此告警由 monitor-watchdog 发送" + } +} +EOF +) + + local response + response=$(curl -s -X POST \ + --connect-timeout 10 \ + --max-time 10 \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "$WEBHOOK_URL" 2>&1) + + if [[ $? -eq 0 ]]; then + log "告警已发送: $response" + else + log "WARNING: 告警发送失败: $response" + return 1 + fi +} + +# ============================================================ +# 主逻辑 +# ============================================================ +main() { + log "开始健康检查: $HEALTH_URL" + + if check_health; then + log "健康检查通过 (HTTP 200)" + exit 0 + fi + + log "ERROR: 健康检查失败,尝试发送告警..." + send_alert "HTTP 健康检查失败,连续 ${MAX_RETRIES} 次请求均未返回 200" + + # 检查容器状态 + local container_status + container_status=$(docker inspect -f '{{.State.Status}}' monitor-ai 2>/dev/null || echo "not_found") + log "容器状态: $container_status" + + if [[ "$container_status" != "running" ]]; then + log "ERROR: 容器未运行,尝试重启..." + cd "$MONITOR_DIR" && docker compose up -d 2>&1 | while read -r line; do log " $line"; done + + # 等待容器启动后再次检查 + sleep 10 + if check_health; then + log "容器已重启并通过健康检查" + else + log "ERROR: 重启后健康检查仍然失败,请人工介入" + send_alert "容器已重启,但健康检查仍然失败,请人工介入" + fi + fi +} + +main "$@" diff --git a/scripts/monitor-worker.ts b/scripts/monitor-worker.ts new file mode 100644 index 0000000..a4e16c9 --- /dev/null +++ b/scripts/monitor-worker.ts @@ -0,0 +1,17 @@ +// scripts/monitor-worker.ts — MonitorWorker 独立入口 +import { initDatabase } from '../src/lib/db' +import { MonitorWorker } from '../src/lib/monitor-worker' +import { config } from '../src/lib/config' + +console.log('[worker] Starting MonitorWorker...') +console.log('[worker] Mode:', config.monitorMode) + +initDatabase() + +const worker = new MonitorWorker() +worker.start() + +process.on('SIGTERM', () => { console.log('[worker] SIGTERM, exiting'); process.exit(0) }) +process.on('SIGINT', () => { console.log('[worker] SIGINT, exiting'); process.exit(0) }) + +console.log('[worker] Started. Ticking every 30s.') diff --git a/shared b/shared new file mode 120000 index 0000000..8fba6b6 --- /dev/null +++ b/shared @@ -0,0 +1 @@ +../shared \ No newline at end of file diff --git a/src/app/admin/audit-logs/page.tsx b/src/app/admin/audit-logs/page.tsx new file mode 100644 index 0000000..44f440c --- /dev/null +++ b/src/app/admin/audit-logs/page.tsx @@ -0,0 +1,11 @@ +// src/app/admin/audit-logs/page.tsx — 审计日志(占位) +export default function AuditLogsPage() { + return ( +
+

审计日志

+
+

审计日志页面

+
+
+ ) +} diff --git a/src/app/admin/roles/page.tsx b/src/app/admin/roles/page.tsx new file mode 100644 index 0000000..7039913 --- /dev/null +++ b/src/app/admin/roles/page.tsx @@ -0,0 +1,11 @@ +// src/app/admin/roles/page.tsx — 角色权限管理(占位) +export default function RolesPage() { + return ( +
+

角色权限管理

+
+

角色权限管理页面

+
+
+ ) +} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..a68ccd1 --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,11 @@ +// src/app/admin/users/page.tsx — 用户管理(占位,Phase 3 完成) +export default function UsersPage() { + return ( +
+

用户管理

+
+

用户管理页面(本地测试通过后完善 UI)

+
+
+ ) +} diff --git a/src/app/alerts/page.tsx b/src/app/alerts/page.tsx new file mode 100644 index 0000000..d5ed4fa --- /dev/null +++ b/src/app/alerts/page.tsx @@ -0,0 +1,40 @@ +'use client' +// src/app/alerts/page.tsx — 告警历史 +import { useState, useEffect } from 'react' +import { Activity } from 'lucide-react' + +export default function AlertsPage() { + const [alerts, setAlerts] = useState>>([]) + useEffect(() => { fetch('/api/alerts').then(r => r.json()).then(d => setAlerts(d.alerts || d)) }, []) + + return ( +
+

告警历史

+
+ + + + + + + + + + + + {alerts.map((a: Record) => ( + + + + + + + + ))} + +
时间级别类型标题结果
{String(a.sent_at).slice(0,19)}{String(a.level)}{String(a.alert_type)}{String(a.title)}{a.success ? '✅' : '❌'}
+ {alerts.length === 0 &&

暂无告警记录

} +
+
+ ) +} diff --git a/src/app/api/admin/worker-status/route.ts b/src/app/api/admin/worker-status/route.ts new file mode 100644 index 0000000..21e7475 --- /dev/null +++ b/src/app/api/admin/worker-status/route.ts @@ -0,0 +1,14 @@ +// GET /api/admin/worker-status — Worker 运行状态 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { hasPermission } from '@/lib/permissions' + +export async function GET(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'dashboard:view')) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + return NextResponse.json({ status: 'running', uptime: process.uptime() }) +} diff --git a/src/app/api/alert-channels/[id]/route.ts b/src/app/api/alert-channels/[id]/route.ts new file mode 100644 index 0000000..d544974 --- /dev/null +++ b/src/app/api/alert-channels/[id]/route.ts @@ -0,0 +1,50 @@ +// PUT/DELETE /api/alert-channels/[id] — 编辑/删除告警渠道 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@/lib/audit' +import { hasPermission } from '@/lib/permissions' + +async function checkAdmin(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return null + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) return null + return payload +} + +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const admin = await checkAdmin(request) + if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const { id } = await params + const body = await request.json() + const updates: string[] = [] + + if (body.name !== undefined) updates.push(`name = ${escapeSql(body.name)}`) + if (body.webhook_url !== undefined) updates.push(`webhook_url = ${escapeSql(body.webhook_url)}`) + if (body.channel_type !== undefined) updates.push(`channel_type = ${escapeSql(body.channel_type)}`) + if (body.level_critical !== undefined) updates.push(`level_critical = ${body.level_critical ? 1 : 0}`) + if (body.level_warning !== undefined) updates.push(`level_warning = ${body.level_warning ? 1 : 0}`) + if (body.level_info !== undefined) updates.push(`level_info = ${body.level_info ? 1 : 0}`) + if (body.enabled !== undefined) updates.push(`enabled = ${body.enabled ? 1 : 0}`) + if (body.cooldown_minutes !== undefined) updates.push(`cooldown_minutes = ${Number(body.cooldown_minutes)}`) + if (body.quiet_start !== undefined) updates.push(`quiet_start = ${escapeSql(body.quiet_start)}`) + if (body.quiet_end !== undefined) updates.push(`quiet_end = ${escapeSql(body.quiet_end)}`) + updates.push(`updated_at = datetime('now', '+8 hours')`) + + dbExec(`UPDATE alert_channels SET ${updates.join(', ')} WHERE id = ${Number(id)}`) + writeAuditLog({ action: 'update_channel', entityType: 'alert_channel', entityId: Number(id), details: body, ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} + +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const admin = await checkAdmin(request) + if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const { id } = await params + dbExec(`DELETE FROM alert_channels WHERE id = ${Number(id)}`) + writeAuditLog({ action: 'delete_channel', entityType: 'alert_channel', entityId: Number(id), ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} diff --git a/src/app/api/alert-channels/[id]/test/route.ts b/src/app/api/alert-channels/[id]/test/route.ts new file mode 100644 index 0000000..1ff7185 --- /dev/null +++ b/src/app/api/alert-channels/[id]/test/route.ts @@ -0,0 +1,23 @@ +// POST /api/alert-channels/[id]/test — 测试推送 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery } from '@/lib/db' +import { WeChatPusher } from '@shared/lib/wechat/wechat-pusher' +import { hasPermission } from '@/lib/permissions' + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const { id } = await params + const channels = dbQuery<{ webhook_url: string }>(`SELECT webhook_url FROM alert_channels WHERE id = ${Number(id)}`) + if (channels.length === 0) return NextResponse.json({ error: 'Channel not found' }, { status: 404 }) + + const pusher = new WeChatPusher(String(channels[0].webhook_url)) + const result = await pusher.pushMarkdown('测试告警', `monitor-ai 告警测试消息\n时间: ${new Date().toLocaleString('zh-CN')}`) + return NextResponse.json(result) +} diff --git a/src/app/api/alert-channels/route.ts b/src/app/api/alert-channels/route.ts new file mode 100644 index 0000000..8bbea87 --- /dev/null +++ b/src/app/api/alert-channels/route.ts @@ -0,0 +1,32 @@ +// GET/POST /api/alert-channels — 告警渠道列表/添加 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@/lib/audit' +import { hasPermission } from '@/lib/permissions' + +export async function GET(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const channels = dbQuery('SELECT id, name, channel_type, webhook_url, enabled, level_critical, level_warning, level_info, quiet_enabled, quiet_start, quiet_end, quiet_bypass_critical, cooldown_minutes, created_at, updated_at FROM alert_channels ORDER BY id') + return NextResponse.json(channels) +} + +export async function POST(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:manage')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const body = await request.json() + dbExec(`INSERT INTO alert_channels (name, webhook_url, channel_type, level_critical, level_warning, level_info, enabled, quiet_start, quiet_end, cooldown_minutes) + VALUES (${escapeSql(body.name)}, ${escapeSql(body.webhook_url)}, ${escapeSql(body.channel_type || 'wecom')}, ${body.level_critical ? 1 : 0}, ${body.level_warning ? 1 : 0}, ${body.level_info ? 1 : 0}, ${body.enabled !== 0 ? 1 : 0}, ${escapeSql(body.quiet_start || null)}, ${escapeSql(body.quiet_end || null)}, ${body.cooldown_minutes || 30})`) + writeAuditLog({ action: 'create_channel', entityType: 'alert_channel', details: { name: body.name }, ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} diff --git a/src/app/api/alerts/route.ts b/src/app/api/alerts/route.ts new file mode 100644 index 0000000..e0e35ef --- /dev/null +++ b/src/app/api/alerts/route.ts @@ -0,0 +1,23 @@ +// GET /api/alerts — 告警历史列表(admin / editor) +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery } from '@/lib/db' +import { hasPermission } from '@/lib/permissions' + +export async function GET(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload || !hasPermission(String(payload.role || 'viewer'), 'alerts:view')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + const { searchParams } = new URL(request.url) + const page = Number(searchParams.get('page')) || 1 + const limit = 20 + const offset = (page - 1) * limit + + const alerts = dbQuery(`SELECT * FROM alert_history ORDER BY sent_at DESC LIMIT ${limit} OFFSET ${offset}`) + const total = dbQuery<{ count: number }>('SELECT COUNT(*) as count FROM alert_history') + return NextResponse.json({ alerts, total: total[0]?.count || 0, page }) +} diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts new file mode 100644 index 0000000..0cafda4 --- /dev/null +++ b/src/app/api/auth/callback/route.ts @@ -0,0 +1,97 @@ +// GET /api/auth/callback — OIDC callback 处理 +import { NextRequest, NextResponse } from 'next/server' +import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc' +import { signJwt } from '@shared/lib/auth/jwt' +import { syncOidcUser } from '@shared/lib/auth/user-sync' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@shared/lib/audit/write-audit-log' + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const code = searchParams.get('code') + const state = searchParams.get('state') + const error = searchParams.get('error') + + if (error) { + return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(error)}`, request.url)) + } + + if (!code || !state) { + return NextResponse.redirect(new URL('/login?error=missing_params', request.url)) + } + + // 验证 state + const savedState = request.cookies.get('oidc_state')?.value + if (!savedState || savedState !== state) { + return NextResponse.redirect(new URL('/login?error=state_mismatch', request.url)) + } + + // 取出 code_verifier + const codeVerifier = request.cookies.get('oidc_code_verifier')?.value + if (!codeVerifier) { + return NextResponse.redirect(new URL('/login?error=missing_verifier', request.url)) + } + + // 换取 token + const tokenResult = await exchangeCodeForToken( + { autheliaUrl: authConfig.autheliaUrl, clientId: authConfig.oidcClientId, clientSecret: authConfig.oidcClientSecret, redirectUri: authConfig.oidcRedirectUri }, + code, codeVerifier, + ) + + if (!tokenResult.success) { + return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(tokenResult.error || 'token_exchange_failed')}`, request.url)) + } + + // 验证 nonce + const savedNonce = request.cookies.get('oidc_nonce')?.value + if (savedNonce && tokenResult.nonce && savedNonce !== tokenResult.nonce) { + return NextResponse.redirect(new URL('/login?error=nonce_mismatch', request.url)) + } + + // 获取 userinfo + const userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken) + + // 用户同步 + const user = syncOidcUser({ + getUser: (username) => { + const rows = dbQuery<{ id: number; role: string }>(`SELECT id, role FROM users WHERE username = ${escapeSql(username)}`) + return rows[0] ?? null + }, + createUser: (username, displayName, email) => { + dbExec(`INSERT INTO users (username, display_name, email, role) VALUES (${escapeSql(username)}, ${escapeSql(displayName)}, ${escapeSql(email)}, 'viewer')`) + const row = dbQuery<{ id: number }>(`SELECT last_insert_rowid() AS id`) + return { id: row[0]?.id ?? 0, role: 'viewer' } + }, + updateUser: (username, displayName, email) => { + dbExec(`UPDATE users SET display_name = ${escapeSql(displayName)}, email = ${escapeSql(email)}, updated_at = datetime('now', '+8 hours') WHERE username = ${escapeSql(username)}`) + }, + }, userinfo) + + // 签发 JWT + const token = signJwt({ + secret: authConfig.jwtSecret, + payload: { username: userinfo.preferred_username, displayName: userinfo.name, role: user.role }, + }) + + const response = NextResponse.redirect(new URL('/', request.url)) + + // 签发 tlyq_session cookie + response.cookies.set('tlyq_session', token, { + httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: process.env.NODE_ENV === 'production' ? authConfig.cookieDomain : undefined, path: '/', maxAge: 604800, + }) + + // 清理 OIDC 临时 cookie + response.cookies.delete('oidc_code_verifier') + response.cookies.delete('oidc_state') + response.cookies.delete('oidc_nonce') + + // 审计日志 + writeAuditLog({ exec: dbExec }, { + userId: user.id, username: userinfo.preferred_username, action: 'login', + entityType: 'auth', details: { method: 'oidc', isNew: user.isNew }, + ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', + }) + + return response +} diff --git a/src/app/api/auth/login/oidc/route.ts b/src/app/api/auth/login/oidc/route.ts new file mode 100644 index 0000000..b44d414 --- /dev/null +++ b/src/app/api/auth/login/oidc/route.ts @@ -0,0 +1,25 @@ +// GET /api/auth/login/oidc — OIDC SSO 重定向 +import { NextResponse } from 'next/server' +import { generatePkce, generateState, buildAuthorizeUrl } from '@shared/lib/auth/oidc' +import { authConfig } from '@/lib/auth-config' + +export async function GET() { + const { codeVerifier, codeChallenge } = generatePkce() + const state = generateState() + const nonce = generateState() + + const url = buildAuthorizeUrl( + { autheliaUrl: authConfig.autheliaUrl, clientId: authConfig.oidcClientId, clientSecret: authConfig.oidcClientSecret, redirectUri: authConfig.oidcRedirectUri }, + { codeChallenge, state, nonce }, + ) + + const response = NextResponse.redirect(url) + + // 存储 PKCE 参数到 httpOnly cookie(5 分钟过期) + const cookieOpts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, maxAge: 300, path: '/' } + response.cookies.set('oidc_code_verifier', codeVerifier, cookieOpts) + response.cookies.set('oidc_state', state, cookieOpts) + response.cookies.set('oidc_nonce', nonce, cookieOpts) + + return response +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..f7b168e --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,91 @@ +// POST /api/auth/login — LDAP 本地登录(回退通道) +import { NextRequest, NextResponse } from 'next/server' +import bcrypt from 'bcryptjs' +import { signJwt } from '@shared/lib/auth/jwt' +import { ldapAuth } from '@shared/lib/auth/ldap' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@shared/lib/audit/write-audit-log' + +export async function POST(request: NextRequest) { + let username: string, password: string + const contentType = request.headers.get('content-type') || '' + if (contentType.includes('application/json')) { + const body = await request.json() + username = body.username; password = body.password + } else { + const form = await request.formData() + username = String(form.get('username') || ''); password = String(form.get('password') || '') + } + + if (!username || !password) { + return NextResponse.json({ error: '用户名和密码不能为空' }, { status: 400 }) + } + + // localadmin 密码验证(查询数据库存储的密码) + if (username === 'localadmin') { + const users = dbQuery<{ password_hash: string; role: string }>(`SELECT password_hash, role FROM users WHERE username = 'localadmin'`) + if (users.length === 0) { + return NextResponse.json({ error: 'localadmin 未配置' }, { status: 401 }) + } + // 与数据库存储的 bcrypt 哈希比较 + if (!bcrypt.compareSync(password, users[0].password_hash)) { + writeAuditLog({ exec: dbExec }, { + username, action: 'login_failed', entityType: 'auth', + details: { method: 'localadmin', reason: 'wrong_password' }, + ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', + }) + return NextResponse.json({ error: '密码错误' }, { status: 401 }) + } + + const token = signJwt({ secret: authConfig.jwtSecret, payload: { username: 'localadmin', role: 'admin', displayName: 'localadmin' } }) + + writeAuditLog({ exec: dbExec }, { + username, action: 'login', entityType: 'auth', + details: { method: 'localadmin' }, + ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', + }) + + const response = NextResponse.json({ + user: { username: 'localadmin', role: 'admin', displayName: 'localadmin' }, + }) + response.cookies.set('tlyq_session', token, { + httpOnly: true, secure: false, sameSite: 'lax', path: '/', maxAge: 604800, + }) + return response + } + + // LLDAP 认证 + const result = await ldapAuth( + { url: authConfig.ldapUrl || 'ldap://ldap-ai:3890', baseDn: 'dc=tlyq,dc=ai' }, + username, password, + ) + + if (!result.success) { + writeAuditLog({ exec: dbExec }, { + username, action: 'login_failed', entityType: 'auth', + details: { method: 'ldap', reason: result.error }, + ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', + }) + return NextResponse.json({ error: '用户名或密码错误' }, { status: 401 }) + } + + // 签发 JWT + const users = dbQuery(`SELECT role FROM users WHERE username = ${escapeSql(username)}`) + const role = users.length > 0 ? users[0].role as string : 'viewer' + + writeAuditLog({ exec: dbExec }, { + username, action: 'login', entityType: 'auth', + details: { method: 'ldap', role }, + ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1', + }) + + const token = signJwt({ secret: authConfig.jwtSecret, payload: { username, role, displayName: result.displayName || username } }) + const response = NextResponse.json({ + user: { username, role, displayName: result.displayName || username }, + }) + response.cookies.set('tlyq_session', token, { + httpOnly: true, secure: false, sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 604800, + }) + return response +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..780d751 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,11 @@ +// POST /api/auth/logout — 退出登录 +import { NextResponse } from 'next/server' +import { authConfig } from '@/lib/auth-config' + +export async function POST() { + // 清除 tlyq_session cookie + const logoutUrl = `${authConfig.autheliaUrl}/api/oidc/end_session` + const response = NextResponse.redirect(logoutUrl) + response.cookies.set('tlyq_session', '', { httpOnly: true, secure: false, sameSite: 'lax', domain: authConfig.cookieDomain, path: '/', maxAge: 0 }) + return response +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..af56599 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,24 @@ +// GET /api/auth/me — 当前用户信息 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' + +export async function GET(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + } + + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload) { + return NextResponse.json({ error: 'Invalid token' }, { status: 401 }) + } + + return NextResponse.json({ + user: { + username: payload.username, + display_name: payload.displayName || payload.username, + role: payload.role || 'viewer', + }, + }) +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..33aca0c --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,4 @@ +// GET /api/health — monitor-ai 自身健康检查 +export async function GET() { + return Response.json({ status: 'OK', timestamp: new Date().toISOString() }) +} diff --git a/src/app/api/services/[id]/check/route.ts b/src/app/api/services/[id]/check/route.ts new file mode 100644 index 0000000..91809ff --- /dev/null +++ b/src/app/api/services/[id]/check/route.ts @@ -0,0 +1,31 @@ +// POST /api/services/[id]/check — 手动触发单次检查 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery } from '@/lib/db' +import { HttpChecker, DockerChecker, HealthChecker } from '@shared/lib/alert/health-checker' +import { hasPermission } from '@/lib/permissions' + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + if (!hasPermission(String(payload.role || 'viewer'), 'services:manage')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const { id } = await params + const services = dbQuery(`SELECT * FROM services WHERE id = ${Number(id)} AND enabled = 1`) + if (services.length === 0) return NextResponse.json({ error: 'Service not found' }, { status: 404 }) + + const svc = services[0] + const checks = JSON.parse(String(svc.checks || '[]')) + + const checker = new HealthChecker() + checker.registerChecker('http', new HttpChecker()) + checker.registerChecker('docker', new DockerChecker()) + + const results = await checker.check(checks, Number(svc.check_timeout) || 10) + return NextResponse.json({ serviceId: svc.id, results }) +} diff --git a/src/app/api/services/[id]/route.ts b/src/app/api/services/[id]/route.ts new file mode 100644 index 0000000..bc966e6 --- /dev/null +++ b/src/app/api/services/[id]/route.ts @@ -0,0 +1,51 @@ +// PUT/DELETE /api/services/[id] — 编辑/删除服务 +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@/lib/audit' +import { hasPermission } from '@/lib/permissions' + +async function checkAdmin(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return null + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload) return null + if (!hasPermission(String(payload.role || 'viewer'), 'services:manage')) return null + return payload +} + +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const admin = await checkAdmin(request) + if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const { id } = await params + const body = await request.json() + const updates: string[] = [] + + if (body.name !== undefined) updates.push(`name = ${escapeSql(body.name)}`) + if (body.category !== undefined) updates.push(`category = ${escapeSql(body.category)}`) + if (body.alertLevel !== undefined) updates.push(`alert_level = ${escapeSql(body.alertLevel)}`) + if (body.checks !== undefined) updates.push(`checks = ${escapeSql(JSON.stringify(body.checks))}`) + if (body.checkInterval !== undefined) updates.push(`check_interval = ${Number(body.checkInterval)}`) + if (body.checkTimeout !== undefined) updates.push(`check_timeout = ${Number(body.checkTimeout)}`) + if (body.enabled !== undefined) updates.push(`enabled = ${body.enabled ? 1 : 0}`) + updates.push(`updated_at = datetime('now', '+8 hours')`) + + if (updates.length > 0) { + dbExec(`UPDATE services SET ${updates.join(', ')} WHERE id = ${Number(id)}`) + } + + writeAuditLog({ action: 'update_service', entityType: 'service', entityId: Number(id), details: body, ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} + +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const admin = await checkAdmin(request) + if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const { id } = await params + dbExec(`DELETE FROM services WHERE id = ${Number(id)}`) + writeAuditLog({ action: 'delete_service', entityType: 'service', entityId: Number(id), ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} diff --git a/src/app/api/services/route.ts b/src/app/api/services/route.ts new file mode 100644 index 0000000..d4d6316 --- /dev/null +++ b/src/app/api/services/route.ts @@ -0,0 +1,42 @@ +// GET /api/services — 服务列表 + 当前状态 +// POST /api/services — 添加服务(admin) +import { NextRequest, NextResponse } from 'next/server' +import { verifyJwt } from '@shared/lib/auth/jwt' +import { authConfig } from '@/lib/auth-config' +import { dbQuery, dbExec, escapeSql } from '@/lib/db' +import { writeAuditLog } from '@/lib/audit' +import { hasPermission } from '@/lib/permissions' + +export async function GET() { + const services = dbQuery(`SELECT * FROM services WHERE enabled = 1 ORDER BY display_order, id`) + // 解析 JSON checks 字段 + const result = services.map(s => ({ ...s, checks: JSON.parse(String(s.checks || '[]')) })) + return NextResponse.json(result) +} + +export async function POST(request: NextRequest) { + const token = request.cookies.get('tlyq_session')?.value + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const payload = verifyJwt(token, authConfig.jwtSecret) + if (!payload) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + if (!hasPermission(String(payload.role || 'viewer'), 'services:manage')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const body = await request.json() + const { name, category, alertLevel, checks, checkInterval, checkTimeout } = body + + if (!name) return NextResponse.json({ error: 'Name required' }, { status: 400 }) + + const checksJson = escapeSql(JSON.stringify(checks || [])) + const level = escapeSql(alertLevel || 'warning') + const interval = Number(checkInterval) || 30 + const timeout = Number(checkTimeout) || 10 + + dbExec(`INSERT INTO services (name, category, alert_level, checks, check_interval, check_timeout, display_order) + VALUES (${escapeSql(name)}, ${escapeSql(category || 'endpoint')}, ${level}, ${checksJson}, ${interval}, ${timeout}, + (SELECT COALESCE(MAX(display_order), 0) + 1 FROM services))`) + + writeAuditLog({ userId: payload ? Number(payload.sub) || null : null, username: payload?.username as string || null, action: 'create_service', entityType: 'service', details: { name }, ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1' }) + return NextResponse.json({ success: true }) +} diff --git a/src/app/api/status-history/route.ts b/src/app/api/status-history/route.ts new file mode 100644 index 0000000..dc77d88 --- /dev/null +++ b/src/app/api/status-history/route.ts @@ -0,0 +1,17 @@ +// GET /api/status-history — 状态变更列表 +import { NextRequest, NextResponse } from 'next/server' +import { dbQuery } from '@/lib/db' + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const page = Number(searchParams.get('page')) || 1 + const limit = 20 + const offset = (page - 1) * limit + const serviceId = searchParams.get('service_id') + + let where = '' + if (serviceId) where = ` WHERE service_id = ${Number(serviceId)}` + + const events = dbQuery(`SELECT sh.*, s.name as service_name FROM status_history sh JOIN services s ON sh.service_id = s.id ${where} ORDER BY sh.started_at DESC LIMIT ${limit} OFFSET ${offset}`) + return NextResponse.json(events) +} diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts new file mode 100644 index 0000000..546457a --- /dev/null +++ b/src/app/api/status/route.ts @@ -0,0 +1,10 @@ +// GET /api/status — 所有服务实时状态摘要 +import { NextResponse } from 'next/server' +import { dbQuery } from '@/lib/db' + +export async function GET() { + const services = dbQuery('SELECT id, name, category, alert_level, current_status, status_since FROM services WHERE enabled = 1 ORDER BY display_order, id') + const counts = { normal: 0, abnormal: 0, unknown: 0 } + services.forEach((s: Record) => { const st = String(s.current_status); if (st === 'normal') counts.normal++; else if (st === 'abnormal') counts.abnormal++; else counts.unknown++ }) + return NextResponse.json({ services, counts, timestamp: new Date().toISOString() }) +} diff --git a/src/app/client-layout.tsx b/src/app/client-layout.tsx new file mode 100644 index 0000000..4912cbf --- /dev/null +++ b/src/app/client-layout.tsx @@ -0,0 +1,39 @@ +'use client' +// src/app/client-layout.tsx — 客户端布局(登录页不显示 Sidebar/TopBar) +import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' +import Sidebar from '@/components/Sidebar' +import ThemeProvider from '@/components/ThemeProvider' +import TopBar from '@/components/TopBar' + +interface User { username: string; display_name: string; role: string } + +export default function ClientLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname() + const isLoginPage = pathname === '/login' + const [user, setUser] = useState(null) + + useEffect(() => { + if (isLoginPage) return + fetch('/api/auth/me') + .then(r => r.json()) + .then(d => { if (d.user) setUser(d.user) }) + .catch(() => {}) + }, [isLoginPage]) + + if (isLoginPage) { + return {children} + } + + return ( + +
+ + +
+
{children}
+
+
+
+ ) +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..48e31d5 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,16 @@ +@import 'tailwindcss'; +@custom-variant dark (&:where(.dark, .dark *)); +@source "../../shared"; +@source "../../src"; +/* TLYQ 设计系统 CSS 变量 */ +@import '../../../docs/design/design-system/tlyq-design-system.css' layer(base); + +@layer base { + body { + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: var(--leading-normal); + color: var(--fg); + background-color: var(--bg); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..443dbf0 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,21 @@ +// src/app/layout.tsx — 根布局 +// metadata 无法在 client component 中导出,使用单独的 metadata 文件 +import './globals.css' +import ClientLayout from './client-layout' + +export { metadata } from './metadata' + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +