# HANDOFF — 钉钉通知优化 + Toggle 开关修复 + 时区问题修复

> 最后更新：2026-07-21 19:30 ｜ 分支：troubleshoot-ai-assistant ｜ 负责人：czj

---

## 1. 我们在做什么

本轮解决用户反馈的**钉钉通知功能完善**问题，涉及三个层面：

1. **钉钉配置与通知格式优化**：原来只有 `webhook` 字段，缺少 `webhook_url`/`secret` 区分；通知消息只展示汇总数字，不展示异常项详情；secret 可被复制粘贴。

2. **Toggle 开关不可点击**：通知配置页面的邮件/钉钉/企业微信三组 toggle 开关在用户浏览器上无法点击展开，原因是 `toggle-slider`（span 元素）覆盖在 checkbox 上，`pointer-events` 未设置为 `none`，拦截了点击事件。

3. **定时任务不执行 & 时区偏差**：承接上一轮遗留问题，最终定位到 `FLASK_DEBUG` 默认值不一致（`'1'` vs `'0'`）、服务器 UTC 时区导致 cron 偏移 8 小时。

> 历史背景：上两轮已完成"定时任务弹窗优化 + 通知配置 + 定时任务不执行三根因修复"，本轮在此基础上完善钉钉通知和修复遗留问题。

---

## 2. 已经完成了什么

### ✅ 钉钉通知优化（commit a7f7e175）

**配置字段更新**：
- `webhook` → `webhook_url`（更清晰，符合钉钉官方命名）
- 兼容旧字段加载：`_load_config()` 中自动迁移 `webhook` → `webhook_url`

**通知格式升级**：
- 消息类型从 `text` 改为 `markdown`
- 异常项分类展示：🔴 严重（最多 5 项）、🟡 警告（最多 3 项）
- 格式：`- 模块名：指标名 = 值（阈值：xxx）`
- 报告链接使用 Markdown 格式：`[查看完整报告](url)`

**安全控制**：
- `secret` 输入框允许粘贴、禁止复制/剪切/右键菜单

**验证**：51 用例全绿 ✅；钉钉测试消息发送成功 ✅

### ✅ Toggle 开关修复（commit a19fdbb2, e1e812c9, ff9181c9）

**点击无响应根因**：
- `toggle-slider`（span 元素）覆盖在 checkbox 上，`pointer-events` 默认为 `auto`，拦截了点击事件
- 点击 slider 时事件无法传递到内部的 label 和 checkbox input

**修复**：
- `toggle-slider` 和 `toggle-slider:before` 加 `pointer-events: none`
- `toggle-switch` 加 `cursor: pointer`

**样式优化**：
- 尺寸加大：48x26px → 52x28px
- 禁用色加深：`var(--gray-300)` → `#d1d5db`
- 添加边框：`1px solid #9ca3af`
- 圆点添加阴影：立体感
- 添加 hover 外发光效果

### ✅ FLASK_DEBUG 默认值修复（commit 0d84df9f）

**根因**：`schedule_service.py:383` 的 `FLASK_DEBUG` 默认值为 `'1'`，与 `server.py:151` 的 `'0'` 不一致。systemd 启动时未设环境变量，`init_scheduler()` 判定为 debug 模式直接 return，APScheduler 未初始化。

**修复**：
- `schedule_service.py:385`：`FLASK_DEBUG` 默认值改为 `'0'`
- `/etc/systemd/system/troubleshoot.service`：添加 `Environment=FLASK_DEBUG=0`

**验证**：`/api/health` 返回 `scheduler: {running: true, job_count: 3}` ✅

### ✅ 时区问题修复（commit 1147566e, 712feaa9）

**根因**：服务器 UTC 时区，`CronTrigger.from_crontab()` 默认使用本地时区解析 cron 表达式。用户配"每天 18:15"实际在 UTC 18:15（北京时间凌晨 02:15）触发。

**修复**：
- 新增 `SHANGHAI_TZ = pytz_timezone("Asia/Shanghai")` 常量
- `_add_job()`：`CronTrigger.from_crontab(..., timezone=SHANGHAI_TZ)`
- `_calc_next_run()`：使用 `datetime.now(SHANGHAI_TZ)` 作为基准
- `_now()`：返回北京时间字符串
- 所有 `datetime.now()` 改为 `datetime.now(SHANGHAI_TZ)` 或 `_now()`

**依赖补充**：`pytz>=2024.1` 在三处同步声明

**验证**：`_calc_next_run('30 18 * * *')` 返回北京时间 18:30 ✅

### ✅ 定时任务执行状态展示 + 立即执行（commit d10c77e4）

- 表格新增「上次执行」列（时间 + 成功/失败徽章 + 报告链接）
- 操作列新增「执行」按钮（后台线程执行，3 秒轮询自动刷新）
- 响应式适配

---

## 3. 当前卡在哪

**无卡点**。所有功能已实现、部署到 5.60、验证通过。

**确认状态**：
- 定时任务已能正常触发和执行 ✅
- 钉钉通知已能正常发送 ✅（用户确认"定时任务是会发送的"）
- 通知配置页面 toggle 开关可点击 ✅

---

## 4. 下一步计划

1. **创建 Merge Request**（P1）
   - 访问 http://git.ubainsyun.com/bing/ubains-module-test/merge_requests/new?merge_request%5Bsource_branch%5D=troubleshoot-ai-assistant
   - 将 troubleshoot-ai-assistant 合并到 master

2. **清理测试数据**（P2）
   - 删除测试用的定时任务（工作日巡检、每周巡检）
   - 在 5.60 上配置 `MONITOR_ENC_KEY` 环境变量（当前使用开发兜底密钥）

3. **企业微信通知格式优化**（P3）
   - 与钉钉格式统一，增加异常项展示

---

## 5. 踩过的坑（绝对不要再踩）

### 坑 1：Toggle 开关点击无响应 - pointer-events 拦截（本轮）

**坑**：`toggle-slider`（span 元素）覆盖在 checkbox 上，`pointer-events` 默认为 `auto`，点击 slider 时事件被拦截，无法传递到内部的 label 和 checkbox input。用户点击 toggle 开关没反应，反复点击以为页面坏了。

**避免方法**：
- 覆盖层元素（如 slider）必须加 `pointer-events: none`
- 自定义 checkbox 样式时，点击事件必须能穿透到 input 元素
- 测试时用 `elementFromPoint()` 检查点击命中目标

### 坑 2：FLASK_DEBUG 默认值不一致导致 scheduler 不初始化（最严重）

**坑**：`schedule_service.py:383` 的 `FLASK_DEBUG` 默认值为 `'1'`，`server.py:151` 的默认值为 `'0'`。systemd 启动时未设环境变量，`init_scheduler()` 判定为 debug 模式直接 return 不初始化 APScheduler。

**症状**：`/api/health` 返回 `scheduler: {running: false, reason: "APScheduler 未初始化"}`，定时任务永远不执行。

**避免方法**：
- 新增环境变量默认值时，**必须检查所有使用该变量的地方是否一致**
- systemd 服务文件显式设置 `Environment=FLASK_DEBUG=0`（双保险）

### 坑 3：CronTrigger 默认使用系统时区

**坑**：`CronTrigger.from_crontab(cron)` 默认使用本地系统时区。服务器 UTC 时区时，用户配的"18:15"实际在 UTC 18:15（北京时间凌晨 02:15）才触发。

**避免方法**：显式指定时区：`CronTrigger.from_crontab(cron, timezone=SHANGHAI_TZ)`

### 坑 4：时间戳显示 UTC 时间误导用户

**坑**：`datetime.now()` 返回服务器本地时间（UTC），`last_run_at` 显示 `10:39` 用户以为是北京时间 10:39，实际是 UTC 10:39（北京时间 18:39）。

**避免方法**：所有面向用户的时间戳统一使用 `datetime.now(SHANGHAI_TZ)` 或封装的 `_now()` 函数。

### 坑 5：定时任务"看起来没执行"，实际是页面没展示

**坑**：`schedule.html` 的"状态"列只显示启用/禁用开关，`last_run_at`/`last_run_status` 等字段数据都有但 UI 没渲染。用户反馈"没执行"，实际可能已执行只是看不到。

**避免方法**：定时任务功能必须有执行可见性 + 立即执行按钮可主动验证。

### 坑 6：SSH 后台启动进程随 channel 关闭被杀

**坑**：用 `nohup python3 server.py &` 后台启动，SSH channel 关闭后进程被 SIGHUP 杀掉。

**避免方法**：用 `setsid` 或 `screen` 创建新会话，或 `nohup cmd > log 2>&1 < /dev/null & disown`。

---

## 6. 关键文件与命令速查

### 本次修改文件

| 文件 | 改动 |
|------|------|
| `skill/code/web/service_monitor/services/notification_service.py` | 配置字段 `webhook`→`webhook_url`、兼容旧字段、钉钉通知改 markdown 格式、异常项提取展示 |
| `skill/code/web/templates/service_monitor/notification.html` | toggle 点击修复 + 样式优化、secret 安全控制、字段名更新 |
| `skill/code/web/service_monitor/services/schedule_service.py` | FLASK_DEBUG 默认值修复、时区固定为 Asia/Shanghai、`_now()` 北京时间、`run_now()` 手动触发 |
| `skill/code/web/service_monitor/routes.py` | `page_schedule` 注入徽章、新增 `POST .../schedules/<id>/run` |
| `skill/code/web/templates/service_monitor/schedule.html` | 表格 7→8 列、执行状态徽章、「执行」按钮、3 秒轮询 |
| `requirements.txt` / `skill/code/requirements.txt` / `deploy/upload_to_server.py` | +`pytz>=2024.1` |

### 本次新增文件

| 文件 | 用途 |
|------|------|
| `Docs/需求文档/服务监测/PRD_需求文档_钉钉通知优化.md` | 钉钉通知优化需求 |
| `Docs/需求文档/服务监测/PRD_计划执行_钉钉通知优化.md` | 钉钉通知优化计划 |
| `Docs/需求文档/服务监测/PRD_需求文档_Toggle开关样式优化.md` | Toggle 开关样式优化需求 |
| `Docs/需求文档/服务监测/PRD_计划执行_Toggle开关样式优化.md` | Toggle 开关样式优化计划 |
| `Docs/需求文档/服务监测/PRD_问题处理_定时任务时区问题.md` | 时区问题处理 |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务时区问题.md` | 时区问题修复计划 |

### 服务器配置

| 配置 | 位置 |
|------|------|
| `FLASK_DEBUG=0` | `/etc/systemd/system/troubleshoot.service` [Service] 段 |
| 通知配置 | `/opt/troubleshoot/web/service_monitor/data/notifications.json` |
| 定时任务 | `/opt/troubleshoot/web/service_monitor/data/schedules.json` |

### 常用命令

```bash
# 本地启动
python skill/code/web/server.py

# 部署到 5.60
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py

# 健康检查
curl -s http://192.168.5.60:8088/api/health

# 查看定时任务数据
ssh ubains@192.168.5.60 "cat /opt/troubleshoot/web/service_monitor/data/schedules.json"

# 通知配置数据
ssh ubains@192.168.5.60 "cat /opt/troubleshoot/web/service_monitor/data/notifications.json"

# 服务日志
ssh ubains@192.168.5.60 "journalctl -u troubleshoot --since '30 min ago' --no-pager"

# 单元测试
cd skill/code && python -m pytest -v
cd skill/code && python -m pytest web/service_monitor/tests/ -v
```

### 未提交文件

- `HANDOFF.md`（本轮待提交）
- `skill/code/web/users.json`（仅登录统计更新，无需提交）

---

## 提交历史（本轮）

```
e1e812c9 style(service-monitor): toggle 开关样式优化
ff9181c9 fix(service-monitor): secret 允许粘贴但禁止复制
a19fdbb2 fix(service-monitor): toggle 开关点击无响应修复
a7f7e175 feat(service-monitor): 钉钉通知优化
712feaa9 fix(service-monitor): 所有时间戳统一使用北京时区
1147566e fix(service-monitor): 定时任务时区固定为 Asia/Shanghai
0d84df9f fix(service-monitor): init_scheduler FLASK_DEBUG 默认值与 server.py 统一为 '0'
d10c77e4 feat(service-monitor): 定时任务执行状态展示 + 立即执行按钮
```