diff --git a/deploy/gate_log.jsonl b/deploy/gate_log.jsonl index 4ec70e3b..ccccb621 100644 --- a/deploy/gate_log.jsonl +++ b/deploy/gate_log.jsonl @@ -846,3 +846,43 @@ {"ts":"2026-06-08T12:47:57+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} {"ts":"2026-06-08T12:47:57+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"} {"ts":"2026-06-08T12:47:57+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-servers-unset-path-batch-parity.md 27lines"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-servers-unset-path-batch-parity.md"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"review","result":"BLOCK","detail":"audit missing changed files"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-08T12:55:08+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"6/7"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-servers-unset-path-batch-parity.md 27lines"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-servers-unset-path-batch-parity.md"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-08T12:55:20+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-servers-inline-detail-expand-fix.md 28lines"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-notify-toggle-inline-detail-deploy.md"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"test","result":"BLOCK","detail":"tests failed"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-08T13:04:19+08:00","date":"2026-06-08","gate":"summary","result":"BLOCK","detail":"6/7"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"changelog","result":"PASS","detail":"2026-06-08-servers-inline-detail-expand-fix.md 28lines"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"audit","result":"PASS","detail":"2026-06-08-notify-toggle-inline-detail-deploy.md"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"test","result":"PASS","detail":"all passed"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"review","result":"PASS","detail":"audit covers changes"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-08T13:04:52+08:00","date":"2026-06-08","gate":"summary","result":"PASS","detail":"7/7"} diff --git a/docs/audit/2026-06-08-notify-toggle-inline-detail-deploy.md b/docs/audit/2026-06-08-notify-toggle-inline-detail-deploy.md new file mode 100644 index 00000000..5bdf20b8 --- /dev/null +++ b/docs/audit/2026-06-08-notify-toggle-inline-detail-deploy.md @@ -0,0 +1,31 @@ +# 审计 — Telegram 开关多 Worker 同步 + 服务器行内详情展开 + +**Changelog**: +- `docs/changelog/2026-06-08-notify-toggle-multi-worker-sync.md` +- `docs/changelog/2026-06-08-servers-inline-detail-expand-fix.md` + +## 变更文件 + +`server/config.py` · `server/infrastructure/settings_broadcast.py` · `server/utils/notify_toggles.py` · `server/api/settings.py` · `server/main.py` · `server/infrastructure/telegram/__init__.py` · `tests/test_notify_toggle_sync.py` · `frontend/src/pages/ServersPage.vue` · `frontend/src/components/servers/ServerUnsetPathPanel.vue` + +## Step 3 + +| 项 | 结论 | +|----|------| +| NOTIFY_* 关闭后 Telegram 不发送 | PASS(helper + 单测 9 开关) | +| 多 worker 热更新 | PASS(Redis `nexus:settings` 广播) | +| WebSocket / alert_logs 仍记录 | PASS(仅 Telegram 门控,符合设置页文案) | +| expanded 与 item-value 数字 id 一致 | PASS(number[] + computed 桥接) | +| 无静默吞错 | PASS(broadcast publish 失败打 warning) | + +## Closure + +Gate 7/7 待跑 · pytest notify 31/31 · frontend build PASS + +## DoD + +- [x] Settings 关 CPU 告警 → 任意 worker 不推 Telegram +- [x] 点击服务器名称展开 ServerInlineDetail +- [x] 未设路径区块同样可展开 +- [x] changelog ×2 +- [ ] 生产 /health + 浏览器验收 diff --git a/docs/changelog/2026-06-08-notify-toggle-multi-worker-sync.md b/docs/changelog/2026-06-08-notify-toggle-multi-worker-sync.md new file mode 100644 index 00000000..12630448 --- /dev/null +++ b/docs/changelog/2026-06-08-notify-toggle-multi-worker-sync.md @@ -0,0 +1,43 @@ +# 2026-06-08 — 告警 Telegram 开关多 Worker 同步 + +## 摘要 + +修复 Settings 关闭「CPU/内存/磁盘等资源告警 Telegram 通知」后仍收到 Telegram 的问题。根因:多 uvicorn worker 下仅处理 PUT 的进程热更新了内存 `settings`,处理 Agent 心跳的其他 worker 仍认为开关为开启。 + +## 动机 + +用户关闭「CPU 超阈值告警」等 notify_* 开关后 Telegram 仍推送;设置页文案已说明「关闭后不发送 Telegram」,但生产 `--workers 4` 时行为不一致。 + +## 变更 + +1. **`server/config.py`** — 新增 `apply_db_override(db_key, value)`,统一单条设置热更新逻辑。 +2. **`server/infrastructure/settings_broadcast.py`** — 新增 Redis 频道 `nexus:settings`;PUT 后 publish,各 worker 订阅并 `apply_db_override`。 +3. **`server/main.py`** — lifespan 启动/停止 settings subscriber。 +4. **`server/api/settings.py`** — `set_setting` 热更新后调用 `publish_setting_change`。 +5. **`server/utils/notify_toggles.py`** — 集中 `notify_enabled` / `resource_alert_notify_enabled` 等 helper。 +6. **`server/infrastructure/telegram/__init__.py`** — 全部 Telegram 发送入口复用 notify helper(行为不变,逻辑集中)。 +7. **`tests/test_notify_toggle_sync.py`** — 开关解析、Telegram 跳过、广播 publish 测试。 + +## 涉及文件 + +- `server/config.py` +- `server/infrastructure/settings_broadcast.py` +- `server/main.py` +- `server/api/settings.py` +- `server/utils/notify_toggles.py` +- `server/infrastructure/telegram/__init__.py` +- `tests/test_notify_toggle_sync.py` + +## 迁移 / 重启 + +- 无 DB 迁移。 +- 需重启后端(或部署)以加载 settings subscriber。 + +## 验证 + +```bash +pytest tests/test_notify_toggle_sync.py tests/test_settings_telegram.py -q +``` + +- Settings 关闭 CPU 告警 → 任意 worker 处理心跳时 `send_telegram_alert` 返回 False,不调用 Telegram API。 +- WebSocket 告警与 `alert_logs` 仍记录(与设置页说明一致)。 diff --git a/docs/changelog/2026-06-08-servers-inline-detail-expand-fix.md b/docs/changelog/2026-06-08-servers-inline-detail-expand-fix.md new file mode 100644 index 00000000..c5874134 --- /dev/null +++ b/docs/changelog/2026-06-08-servers-inline-detail-expand-fix.md @@ -0,0 +1,28 @@ +# 2026-06-08 — 服务器名称点击展开详情修复 + +## 摘要 + +修复服务器列表点击名称后箭头旋转但行内详情(CPU/内存/同步日志)不显示的问题。 + +## 动机 + +`v-data-table-server` 的 `item-value="id"` 为数字,但 `v-model:expanded` 误用 `string[]`(`String(item.id)`),Vuetify 无法匹配展开行,仅自定义 chevron 图标会切换。 + +## 变更 + +- `frontend/src/pages/ServersPage.vue` — `expandedMainIds` / `expandedUnsetIds` 改为 `number[]` +- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 同步 expanded ids 类型 + +## 涉及文件 + +- `frontend/src/pages/ServersPage.vue` +- `frontend/src/components/servers/ServerUnsetPathPanel.vue` + +## 迁移 / 重启 + +无;前端构建部署即可。 + +## 验证 + +- 服务器页表格模式:点击名称 → 展开 `ServerInlineDetail`(资源指标 + 同步日志) +- 未设路径区块同样可展开 diff --git a/frontend/src/components/servers/ServerUnsetPathPanel.vue b/frontend/src/components/servers/ServerUnsetPathPanel.vue index 6d4e9b02..bc18c688 100644 --- a/frontend/src/components/servers/ServerUnsetPathPanel.vue +++ b/frontend/src/components/servers/ServerUnsetPathPanel.vue @@ -189,7 +189,7 @@ const props = defineProps<{ page: number itemsPerPage: number itemsPerPageOptions: ReadonlyArray<{ readonly value: number; readonly title: string }> - expandedIds: string[] + expandedIds: number[] selectedItems: Array selectAllLoading: boolean editingPathId: number | null @@ -202,7 +202,7 @@ const emit = defineEmits<{ 'update:page': [page: number] 'update:items-per-page': [n: number] 'update:selected-items': [items: Array] - 'update:expanded-ids': [ids: string[]] + 'update:expanded-ids': [ids: number[]] 'update:editing-path-value': [value: string] 'select-all-filtered': [] 'toggle-expand': [item: ServerApiItem] @@ -238,12 +238,17 @@ const headers = [ const selectedCount = computed(() => normalizeServerIds(props.selectedItems).length) const expandedModel = computed({ - get: () => props.expandedIds, - set: (ids: string[]) => emit('update:expanded-ids', ids), + get: () => props.expandedIds as unknown as readonly string[], + set: (ids: readonly string[]) => { + emit( + 'update:expanded-ids', + ids.map((id) => Number(id)).filter((n) => !Number.isNaN(n)), + ) + }, }) function isExpandedId(id: number): boolean { - return props.expandedIds.includes(String(id)) + return props.expandedIds.includes(id) } diff --git a/frontend/src/pages/ServersPage.vue b/frontend/src/pages/ServersPage.vue index 72ca60c1..26784493 100644 --- a/frontend/src/pages/ServersPage.vue +++ b/frontend/src/pages/ServersPage.vue @@ -122,7 +122,7 @@ show-select v-model="selectedItems" v-model:sort-by="sortBy" - v-model:expanded="expandedMainIds" + v-model:expanded="expandedMainIdsModel" show-expand class="server-main-table" @update:page="onMainPageChange" @@ -141,7 +141,7 @@ @click.stop="toggleMainExpand(item)" > - {{ expandedMainIds.includes(String(item.id)) ? 'mdi-chevron-down' : 'mdi-chevron-right' }} + {{ expandedMainIds.includes(item.id) ? 'mdi-chevron-down' : 'mdi-chevron-right' }} {{ item.name }} @@ -588,7 +588,7 @@