fix(terminal,alerts): quick-cmd enter on send and offline Telegram channel
Append CR when executing quick commands (fixes trim stripping saved \r) and allow optional dedicated Telegram bot for server offline alerts only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1657,3 +1657,433 @@
|
|||||||
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
{"ts":"2026-06-11T17:47:40+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-servers-search-manual-trigger.md 28lines"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-servers-search-manual-trigger.md"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T17:54:51+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-files-chmod-not-effective-fix.md 32lines"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-files-chmod-not-effective-fix.md"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T18:12:57+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-files-upload-www-permissions.md 39lines"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-files-chmod-not-effective-fix.md"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T18:23:41+08:00","date":"2026-06-11","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-files-upload-www-permissions.md 39lines"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-files-upload-www-permissions.md"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T18:24:13+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-embedded-browser.md 38lines"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-embedded-browser.md"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T18:30:51+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-global-floating-browser.md 41lines"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"audit","result":"BLOCK","detail":"missing sections"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T20:11:04+08:00","date":"2026-06-11","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-global-floating-browser.md 41lines"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-global-floating-browser.md"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T20:11:24+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-global-floating-browser.md 47lines"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-global-floating-browser.md"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T20:15:48+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-global-floating-browser.md 48lines"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-global-floating-browser.md"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T20:19:51+08:00","date":"2026-06-11","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-global-floating-browser.md 48lines"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-global-floating-browser.md"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T20:20:04+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-rsync-push-sudo-elevation.md 43lines"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-rsync-push-sudo-elevation.md"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T23:20:24+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-push-default-wwwroot.md 41lines"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-rsync-push-sudo-elevation.md"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T23:43:11+08:00","date":"2026-06-11","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"changelog","result":"PASS","detail":"2026-06-11-push-default-wwwroot.md 41lines"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"audit","result":"PASS","detail":"2026-06-11-push-default-wwwroot.md"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-11T23:45:44+08:00","date":"2026-06-11","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"changelog","result":"BLOCK","detail":"file not found"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"audit","result":"BLOCK","detail":"file not found"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"no audit file"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T00:18:55+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"4/7"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md 25lines"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T00:19:19+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md 25lines"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T00:19:31+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md 25lines"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T01:44:02+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-onboarding-automation.md 25lines"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-remove-browser-push-ui.md"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T01:44:24+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-terminal-server-rail-search-history.md 34lines"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-terminal-server-rail-search-history.md"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T01:47:53+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-push-remove-name-copy.md 29lines"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-push-remove-name-copy.md"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T01:51:46+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-terminal-search-center-empty-state.md 31lines"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-push-terminal-ui.md"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T01:58:39+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-search-history-isolated-scopes.md 35lines"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-search-history-isolated-scopes.md"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T02:03:52+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-watch-slots.md"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T02:55:32+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-watch-slots.md"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T02:56:25+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-watch-slots.md"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T02:56:43+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-server-watch-slots.md"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:08:34+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-files-watch-ui-fixes.md"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:10:05+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-watch-slots.md 54lines"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-files-watch-ui-fixes.md"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:12:19+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-agent-action-hint.md 33lines"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-files-watch-ui-fixes.md"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:40:50+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-agent-action-hint.md 33lines"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-files-watch-ui-fixes.md"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:41:07+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-server-agent-action-hint.md 33lines"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-files-watch-ui-fixes.md"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:42:10+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-ttl-expiry-race-fix.md 35lines"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-watch-slot-monitoring-deploy.md"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T03:48:00+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-install-psutil-ssh.md 40lines"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-watch-install-psutil-ssh.md"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:04:48+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-install-psutil-ssh.md 40lines"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-watch-install-psutil-ssh.md"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:04:59+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-psutil-install-sudo-fix.md 19lines"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-psutil-install-sudo-fix.md"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:09:10+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-audit-log-server-target-name.md 18lines"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-audit-log-server-target-name.md"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:16:46+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-slot-hover-border.md 15lines"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-audit-log-server-target-name.md"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:21:16+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-slot-hover-border.md 15lines"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-watch-slot-hover-border.md"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:21:28+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-watch-slot-ttl-metrics-ui.md 43lines"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-watch-slot-hover-border.md"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T04:32:30+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-browser-top-appbar.md 19lines"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-audit-log-server-target-name.md"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"security","result":"BLOCK","detail":"3 HIGH findings"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T06:49:23+08:00","date":"2026-06-12","gate":"summary","result":"BLOCK","detail":"4/7"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"changelog","result":"PASS","detail":"2026-06-12-browser-top-appbar.md 19lines"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"audit","result":"PASS","detail":"2026-06-12-remote-browser-worker.md"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-12T06:50:36+08:00","date":"2026-06-12","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-offline-alert-telegram-split.md 37lines"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"audit","result":"BLOCK","detail":"file not found"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"no audit file"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-13T01:43:01+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-offline-alert-telegram-split.md 37lines"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-offline-quick-cmd-telegram.md"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 审计 — 快捷命令回车修复 + 离线告警 Telegram 分流
|
||||||
|
|
||||||
|
**Changelog**:
|
||||||
|
- `docs/changelog/2026-06-13-terminal-quick-cmd-enter-fix.md`
|
||||||
|
- `docs/changelog/2026-06-13-offline-alert-telegram-split.md`
|
||||||
|
|
||||||
|
**设计/计划**:
|
||||||
|
- `docs/design/specs/2026-06-13-offline-alert-telegram-split-design.md`
|
||||||
|
- `docs/design/plans/2026-06-13-offline-alert-telegram-split.md`
|
||||||
|
|
||||||
|
## 审计范围
|
||||||
|
|
||||||
|
| 文件 | 变更 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| `frontend/src/composables/useTerminalQuickCommands.ts` | `formatQuickCmdForSend` | ☑ |
|
||||||
|
| `frontend/src/composables/terminal/useTerminalSessions.ts` | `execQuickCmd` | ☑ |
|
||||||
|
| `frontend/src/components/TerminalQuickCommandsSettings.vue` | 保存不再 append `\r` | ☑ |
|
||||||
|
| `frontend/src/pages/SettingsPage.vue` | 离线专用 Bot UI | ☑ |
|
||||||
|
| `frontend/src/types/api.ts` | 离线开关文案 | ☑ |
|
||||||
|
| `frontend/src/utils/auditLabels.ts` | reveal 审计标签 | ☑ |
|
||||||
|
| `server/infrastructure/telegram/__init__.py` | 离线路由 | ☑ |
|
||||||
|
| `server/api/settings.py` | offline API + `_telegram_fetch_chats` | ☑ |
|
||||||
|
| `server/config.py` | `TELEGRAM_OFFLINE_*` | ☑ |
|
||||||
|
| `tests/test_telegram_offline_channel.py` | 新 | ☑ |
|
||||||
|
| `tests/test_terminal_quick_commands.py` | 新 | ☑ |
|
||||||
|
|
||||||
|
## Step 3 规则扫描
|
||||||
|
|
||||||
|
| H | 规则 | 结论 |
|
||||||
|
|---|------|------|
|
||||||
|
| H1 | 离线 Token 敏感掩码 | SAFE — `SENSITIVE_KEYS` |
|
||||||
|
| H2 | reveal 审计 | SAFE — `reveal_offline_telegram_token` |
|
||||||
|
| H3 | 半配离线回退默认 Bot | SAFE — `resolve_offline_telegram_credentials` |
|
||||||
|
| H4 | 资源告警不走离线 Bot | SAFE — `send_telegram_alert` 未改 |
|
||||||
|
| H5 | 快捷命令 `\r` 与 `trim()` | SAFE — 发送时 `formatQuickCmdForSend` |
|
||||||
|
| H6 | Layer3 不同步离线 Bot | SAFE — 设计 intentional |
|
||||||
|
|
||||||
|
## Closure
|
||||||
|
|
||||||
|
| H | 判定 | 依据 |
|
||||||
|
|---|------|------|
|
||||||
|
| H1–H6 | SAFE | 代码 + pytest 10/10 + `test_ops_patrol_sync` 未破坏 |
|
||||||
|
|
||||||
|
## DoD
|
||||||
|
|
||||||
|
- [x] `pytest tests/test_telegram_offline_channel.py tests/test_terminal_quick_commands.py` passed
|
||||||
|
- [x] `bash scripts/local_verify.sh` 26/26
|
||||||
|
- [x] changelog / design / audit
|
||||||
|
- [x] 无 SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL 变更
|
||||||
|
- [x] 前端 `vite build` 待部署脚本执行
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 2026-06-13 子机离线告警独立 Telegram 通道
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
设置页「告警通知项目」下新增可选的离线专用 Bot:子机离线告警可发往另一 Telegram 机器人/群组,其它告警仍走默认 Bot。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
离线告警量大或受众不同,需与 CPU/内存等资源告警分流,避免主告警群被淹没。
|
||||||
|
|
||||||
|
## 变更
|
||||||
|
|
||||||
|
- 新 settings:`telegram_offline_bot_token`、`telegram_offline_chat_id`(二者均配置时离线告警专用;否则回退默认 Bot)。
|
||||||
|
- `resolve_offline_telegram_credentials()` + `send_telegram_offline_alert` 路由。
|
||||||
|
- API:`POST /settings/telegram/offline/test`、`GET /settings/telegram/offline/chats`、`POST /settings/telegram/offline/reveal-token`。
|
||||||
|
- 设置页:「子机离线告警专用 Bot(可选)」配置区。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `server/config.py`
|
||||||
|
- `server/infrastructure/telegram/__init__.py`
|
||||||
|
- `server/api/settings.py`
|
||||||
|
- `frontend/src/pages/SettingsPage.vue`
|
||||||
|
- `frontend/src/types/api.ts`
|
||||||
|
- `tests/test_telegram_offline_channel.py`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
- 后端:拉取后 `supervisorctl restart nexus`(新 API 端点)。
|
||||||
|
- 前端:重新 `vite build` 部署 `web/app/`。
|
||||||
|
- 无 DB 迁移;首次保存写入 `settings` 表。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. 设置页配置离线专用 Token + Chat ID →「测试离线通道」收到消息。
|
||||||
|
2. 仅配置默认 Bot、不填离线专用 → 子机离线仍走默认群。
|
||||||
|
3. `pytest tests/test_telegram_offline_channel.py` 通过。
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 2026-06-13 终端快捷命令发送时自动追加回车修复
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
修复自定义快捷命令点击执行后未自动回车、命令悬停在输入行的问题。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
设置页文案为「命令内容(发送时自动追加回车)」,但保存时在 `cmd` 末尾追加 `\r` 后,`useTerminalQuickCommands.create/update` 的 `cmd.trim()` 会把 `\r` 当作空白剥掉,导致入库命令无回车;终端 `execQuickCmd` 又原样发送,表现为快捷命令不执行。
|
||||||
|
|
||||||
|
## 变更
|
||||||
|
|
||||||
|
- `formatQuickCmdForSend()`:执行时统一追加 `\r`(与命令栏 `sendCmd` 一致),对已含 `\r`/`\n` 的命令幂等。
|
||||||
|
- `execQuickCmd`:发送前经 `formatQuickCmdForSend` 处理。
|
||||||
|
- 设置页保存:仅存 trimmed 命令正文,不再在保存阶段追加 `\r`。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `frontend/src/composables/useTerminalQuickCommands.ts`
|
||||||
|
- `frontend/src/composables/terminal/useTerminalSessions.ts`
|
||||||
|
- `frontend/src/components/TerminalQuickCommandsSettings.vue`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
- 仅前端:重新 `vite build` 并部署 `web/app/`。
|
||||||
|
- 已有 DB 中带 `\r` 的内置/历史命令不受影响(发送时去尾后再追加一次 `\r`)。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. 设置 → 终端快捷命令:添加 `echo hello`,保存后在终端页点击执行,应出现 `hello` 输出并回到新提示符。
|
||||||
|
2. 内置命令(如「磁盘使用」)仍可一键执行。
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 子机离线告警独立 Telegram — 实施说明
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `server/config.py` — 新 settings 属性与 DB_OVERRIDE_MAP
|
||||||
|
- `server/infrastructure/telegram/__init__.py` — 路由与 `send_telegram` 参数化
|
||||||
|
- `server/api/settings.py` — MUTABLE_KEYS、离线 test/chats/reveal
|
||||||
|
- `frontend/src/pages/SettingsPage.vue` — 离线专用 Bot UI
|
||||||
|
- `frontend/src/types/api.ts` — 离线开关文案
|
||||||
|
- `frontend/src/utils/auditLabels.ts`
|
||||||
|
- `tests/test_telegram_offline_channel.py`
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 后端配置与发送路由
|
||||||
|
2. 设置 API 与白名单
|
||||||
|
3. 前端设置页「子机离线专用 Bot」区块
|
||||||
|
4. pytest + local_verify
|
||||||
|
|
||||||
|
## 回滚
|
||||||
|
|
||||||
|
清空 `telegram_offline_*` 设置或删除 DB 行;离线告警自动回退默认 Bot。
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
|
||||||
|
- `resolve_offline_telegram_credentials` 优先离线、回退主 Bot
|
||||||
|
- `send_telegram_offline_alert` 传入正确 token/chat(mock httpx)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 子机离线告警独立 Telegram 通道 — 设计
|
||||||
|
|
||||||
|
**日期**: 2026-06-13
|
||||||
|
**状态**: 已实现
|
||||||
|
|
||||||
|
## 背景与目标
|
||||||
|
|
||||||
|
运维希望将「子机离线」告警与 CPU/内存等资源告警分流:离线通知可发到专用群/专用 Bot,避免主告警群被大量离线消息淹没。
|
||||||
|
|
||||||
|
## 方案对比
|
||||||
|
|
||||||
|
| 方案 | 优点 | 缺点 |
|
||||||
|
|------|------|------|
|
||||||
|
| A. 新建独立 settings 键 + 发送时路由 | 改动小;与现有 Telegram 配置一致 | 需多配一组 Token/Chat |
|
||||||
|
| B. 按服务器标签/项目路由 | 更细粒度 | 复杂度高,当前无项目维度 |
|
||||||
|
| C. 多 Bot 列表 + 规则引擎 | 可扩展 | 过度设计 |
|
||||||
|
|
||||||
|
**选定 A**:`telegram_offline_bot_token` + `telegram_offline_chat_id`;二者均配置时离线告警走专用通道,否则回退默认 Bot。
|
||||||
|
|
||||||
|
## 数据与接口
|
||||||
|
|
||||||
|
| settings 键 | Settings 属性 | 说明 |
|
||||||
|
|-------------|---------------|------|
|
||||||
|
| `telegram_offline_bot_token` | `TELEGRAM_OFFLINE_BOT_TOKEN` | 敏感,GET 掩码 |
|
||||||
|
| `telegram_offline_chat_id` | `TELEGRAM_OFFLINE_CHAT_ID` | 明文 |
|
||||||
|
|
||||||
|
- `send_telegram_offline_alert` → `resolve_offline_telegram_credentials()` → `send_telegram(..., bot_token, chat_id)`
|
||||||
|
- API:`PUT /settings/{key}`、`POST /settings/telegram/offline/test`、`GET /settings/telegram/offline/chats`、`POST /settings/telegram/offline/reveal-token`
|
||||||
|
- Layer 3 巡检仍仅用默认 `telegram_bot_token`(不同步离线 Bot)
|
||||||
|
|
||||||
|
## 安全
|
||||||
|
|
||||||
|
- Token 入 `SENSITIVE_KEYS`;reveal 写审计
|
||||||
|
- 不改变 `notify_alert_offline` 开关语义
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
1. 配置离线专用 Bot+Chat 后,子机离线 Telegram 发到专用群
|
||||||
|
2. 未配置离线专用时,离线告警仍走默认 Bot
|
||||||
|
3. CPU/内存等资源告警始终走默认 Bot
|
||||||
|
4. 设置页可保存、检测 Chat ID、发送测试消息
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 子机离线告警独立 Telegram — 验证报告
|
||||||
|
|
||||||
|
**日期**: 2026-06-13
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
| 项 | 方式 | 结果 |
|
||||||
|
|----|------|------|
|
||||||
|
| 通道路由单测 | `pytest tests/test_telegram_offline_channel.py` | **PASS** 4/4 |
|
||||||
|
| 配置与白名单 | Python import `MUTABLE_KEYS` / `resolve_offline_*` | **PASS** |
|
||||||
|
| API 路由注册 | FastAPI `telegram/offline/*` | **PASS** |
|
||||||
|
| 本地门控 | `bash scripts/local_verify.sh` | **PASS** 26/26 + ruff |
|
||||||
|
| 前端构建 | `npm run build` | **PASS**(含 `SettingsPage-*.js`) |
|
||||||
|
| 构建产物 API 路径 | `telegram/offline/test` 等 | **PASS** |
|
||||||
|
| 真实 Telegram 发送 | 未执行 | **SKIP**(需生产/本地 Bot 凭据) |
|
||||||
|
| 子机离线边沿 E2E | 未执行 | **SKIP**(需可 SSH 子机 + Agent) |
|
||||||
|
|
||||||
|
## 路由清单(已注册)
|
||||||
|
|
||||||
|
- `POST /api/settings/telegram/offline/test`
|
||||||
|
- `GET /api/settings/telegram/offline/chats`
|
||||||
|
- `POST /api/settings/telegram/offline/reveal-token`
|
||||||
|
- `PUT /api/settings/telegram_offline_bot_token`
|
||||||
|
- `PUT /api/settings/telegram_offline_chat_id`
|
||||||
|
|
||||||
|
## 路由逻辑(单测覆盖)
|
||||||
|
|
||||||
|
1. 专用 Token+Chat 均配置 → 离线告警走专用 Bot
|
||||||
|
2. 未配置专用 → 回退默认 `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`
|
||||||
|
3. 仅配置一半专用 → 回退默认(避免半配发不出去)
|
||||||
|
|
||||||
|
## 手动终验(部署后)
|
||||||
|
|
||||||
|
1. **设置** → 配置离线专用 Bot + Chat ID →「测试离线通道」收到消息。
|
||||||
|
2. 停一台子机 Agent → 离线通知进**专用群**(非默认群)。
|
||||||
|
3. 清空离线专用配置 → 离线通知回默认群。
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# 审计报告 — 快捷命令回车修复 + 离线告警 Telegram 分流
|
||||||
|
|
||||||
|
**日期**: 2026-06-13
|
||||||
|
**范围**: 本会话两项功能(不含工作区其它未提交变更如 remote-browser 删除)
|
||||||
|
**方法**: 定向 diff 走读 + 安全/契约检查 + 已有 pytest 证据
|
||||||
|
**审计员**: Agent(定向审计,非全仓 Phase 2 走读)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 变更摘要
|
||||||
|
|
||||||
|
| 功能 | 核心改动 | 风险面 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 终端快捷命令回车 | `formatQuickCmdForSend` + `execQuickCmd` 发送时补 `\r` | 前端 WebSSH 数据流 |
|
||||||
|
| 离线告警 Telegram 分流 | `TELEGRAM_OFFLINE_*` + `resolve_offline_telegram_credentials` + 设置页/API | 凭据存储、出站 Telegram |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 审计文件(5 个)
|
||||||
|
|
||||||
|
| # | 文件 | 行数 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | `server/infrastructure/telegram/__init__.py` | 401 | 逐行完成 ✓ |
|
||||||
|
| 2 | `server/api/settings.py`(offline 段 ~1188–1342) | 段 | 逐行完成 ✓ |
|
||||||
|
| 3 | `frontend/src/composables/useTerminalQuickCommands.ts` | 80 | 逐行完成 ✓ |
|
||||||
|
| 4 | `frontend/src/composables/terminal/useTerminalSessions.ts`(execQuickCmd) | 段 | 逐行完成 ✓ |
|
||||||
|
| 5 | `frontend/src/pages/SettingsPage.vue`(离线 Bot 段) | 段 | 逐行完成 ✓ |
|
||||||
|
|
||||||
|
辅助:`TerminalQuickCommandsSettings.vue`(-2 行)、`server/config.py`(+4 映射)、测试 2 文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 入口表(新增)
|
||||||
|
|
||||||
|
| 方法 | 路径 | 鉴权 |
|
||||||
|
|------|------|------|
|
||||||
|
| PUT | `/api/settings/telegram_offline_bot_token` | JWT `get_current_admin` |
|
||||||
|
| PUT | `/api/settings/telegram_offline_chat_id` | JWT |
|
||||||
|
| POST | `/api/settings/telegram/offline/test` | JWT |
|
||||||
|
| GET | `/api/settings/telegram/offline/chats` | JWT |
|
||||||
|
| POST | `/api/settings/telegram/offline/reveal-token` | JWT + 审计 |
|
||||||
|
|
||||||
|
既有 `broadcast_offline_alert` → `send_telegram_offline_alert` 无新 HTTP 面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Closure 表
|
||||||
|
|
||||||
|
| ID | 规则/检查点 | 文件:行 | 结论 | 说明 |
|
||||||
|
|----|-------------|---------|------|------|
|
||||||
|
| H1 | 敏感凭据不入 GET 明文 | settings.py SENSITIVE_KEYS | **SAFE** | `telegram_offline_bot_token` 已加入掩码集 |
|
||||||
|
| H2 | reveal 写 audit_logs | settings.py:1324–1331 | **SAFE** | `reveal_offline_telegram_token` |
|
||||||
|
| H3 | 出站消息 HTML 转义 | telegram/__init__.py:189–195 | **SAFE** | `html.escape` 用于名称/心跳 |
|
||||||
|
| H4 | 离线专用半配不误发 | telegram/__init__.py:96–107 | **SAFE** | 仅 token 或仅 chat 时整组回退默认 Bot |
|
||||||
|
| H5 | 资源告警仍走默认 Bot | telegram/__init__.py:163 | **SAFE** | `send_telegram_alert` 未改路由 |
|
||||||
|
| H6 | `send_telegram` 参数化不破坏默认调用 | telegram/__init__.py:110–118 | **SAFE** | 默认 `bot_token/chat_id=None` 行为不变 |
|
||||||
|
| H7 | MUTABLE_KEYS 白名单 | settings.py:47–48 | **SAFE** | 两新键已列入 |
|
||||||
|
| H8 | 快捷命令 `\r` 与 `trim()` 冲突 | useTerminalQuickCommands.ts:43–49 | **SAFE** | 发送时 `formatQuickCmdForSend` 根治 |
|
||||||
|
| H9 | 内置命令 DB 带 `\r` 幂等 | useTerminalQuickCommands.ts:21–23 | **SAFE** | 先去尾再追加 |
|
||||||
|
| H10 | 多行快捷命令语义 | useTerminalQuickCommands.ts:21–23 | **SAFE** | 与命令栏一致:整段一条命令 + 单次 `\r` |
|
||||||
|
| H11 | 测试覆盖通道路由 | test_telegram_offline_channel.py | **SAFE** | 4 case 含 dedicated/fallback/partial |
|
||||||
|
| H12 | reveal 加载态共用 | SettingsPage.vue:886–911 vs 离线 toggle | **FINDING P3** | 离线与默认共用 `revealingTelegramToken`,并发点击可能互斥显示 |
|
||||||
|
| H13 | test 端点重复 httpx | settings.py:1268–1309 | **FINDING P3** | 可复用 `send_telegram(..., bot_token, chat_id)` 减重复 |
|
||||||
|
| H14 | 新 API 错误文案 i18n | settings.py:1253,1264 | **FINDING P3** | `http_errors_zh` 未收录 `telegram_offline_*` 键名错误 |
|
||||||
|
| H15 | 子机恢复在线 Telegram | server_offline_monitor.py | **INFO** | 仅 offline 边沿推送,无「恢复在线」通知(既有设计,非回归) |
|
||||||
|
| H16 | Layer3 巡检不同步离线 Bot | settings.py:416–422 | **SAFE** | 仅默认 token 写 .env,符合设计文档 |
|
||||||
|
| H17 | 无静默吞错 | telegram/__init__.py:136–138 | **SAFE** | 失败打 error 日志 |
|
||||||
|
| H18 | CUD 审计 | settings.py set_setting | **SAFE** | PUT 走既有 `update_setting` 审计 |
|
||||||
|
|
||||||
|
`len(H)=18`,Closure=18。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 严重度汇总
|
||||||
|
|
||||||
|
| 级别 | 数量 | 项 |
|
||||||
|
|------|------|-----|
|
||||||
|
| **P0** | 0 | — |
|
||||||
|
| **P1** | 0 | — |
|
||||||
|
| **P2** | 0 | — |
|
||||||
|
| **P3** | 3 | H12 加载态共用;H13 test 重复代码;H14 错误文案 i18n |
|
||||||
|
| **INFO** | 1 | H15 无离线恢复 Telegram |
|
||||||
|
|
||||||
|
**合并结论**: **可合并 / 可部署**。无安全阻断项;P3 可在后续小 PR 打磨。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 测试证据
|
||||||
|
|
||||||
|
```
|
||||||
|
pytest tests/test_telegram_offline_channel.py → 4 passed
|
||||||
|
pytest tests/test_terminal_quick_commands.py → 1 passed
|
||||||
|
bash scripts/local_verify.sh → 26/26 + ruff
|
||||||
|
frontend npm run build → OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 建议(可选跟进)
|
||||||
|
|
||||||
|
1. **H12**: 为离线 Token 增加独立 `revealingOfflineTelegramToken`,避免与默认 Bot 眼睛按钮抢 loading。
|
||||||
|
2. **H13**: `telegram_offline_test_send` 改为 `await send_telegram(message, bot_token=token, chat_id=chat_id)`。
|
||||||
|
3. **H14**: 在 `server/utils/http_errors_zh.py` 补充 `telegram_offline_bot_token 未配置` 等映射。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 验收对照(设计文档)
|
||||||
|
|
||||||
|
| 验收项 | 结果 |
|
||||||
|
|--------|------|
|
||||||
|
| 专用 Bot+Chat 配置后离线走专用通道 | ✓ 代码 + 单测 |
|
||||||
|
| 未配置专用时回退默认 | ✓ 单测 |
|
||||||
|
| CPU/内存等仍走默认 | ✓ 未改 `send_telegram_alert` |
|
||||||
|
| 设置页保存/检测/测试 | ✓ UI + API 已实现 |
|
||||||
|
| 快捷命令点击执行带回车 | ✓ `formatQuickCmdForSend` + 构建验证 |
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 巡检 — 快捷命令回车 + 离线 Telegram 分流 + Layer3 巡检
|
||||||
|
|
||||||
|
**日期**:2026-06-13
|
||||||
|
**范围**:本会话两项功能 + 部署门控 + Layer3 宿主机巡检状态
|
||||||
|
**生产**:https://api.synaglobal.vip(本机外网 curl 不可达,见下)
|
||||||
|
|
||||||
|
## 进度条
|
||||||
|
|
||||||
|
| 步骤 | 状态 |
|
||||||
|
|------|------|
|
||||||
|
| 实现 | ✅ 工作区完成 |
|
||||||
|
| 本地 L2b | ✅ 26/26 + ruff |
|
||||||
|
| 功能单测 | ✅ 10/10(offline 4 + quick-cmd 1 + offline_monitor 5) |
|
||||||
|
| Layer3 单测 | ✅ `test_ops_patrol_sync.py` 7/7 |
|
||||||
|
| 前端 build | ✅(此前已构建) |
|
||||||
|
| 门控 7/7 | ❌ **5/7**(Gate 2/7 审计路径、Gate 7 无审计文件) |
|
||||||
|
| 生产健康 | ⚠️ 外网 `curl` 连接重置;SSH `nexus` 主机名不可解析 |
|
||||||
|
| 本地 API | ✅ `GET /health` → `ok` |
|
||||||
|
| 浏览器终验 | ❌ 待部署 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 功能变更巡检
|
||||||
|
|
||||||
|
### 1.1 终端快捷命令回车
|
||||||
|
|
||||||
|
| 检查项 | 结果 |
|
||||||
|
|--------|------|
|
||||||
|
| `formatQuickCmdForSend` 发送时补 `\r` | ✅ |
|
||||||
|
| 设置页不再 `trim` 掉 `\r` 后入库 | ✅ |
|
||||||
|
| 内置命令 DB 带 `\r` 幂等 | ✅ 单测逻辑 |
|
||||||
|
| `pytest test_terminal_quick_commands` | ✅ 1 passed |
|
||||||
|
|
||||||
|
### 1.2 子机离线告警独立 Telegram
|
||||||
|
|
||||||
|
| 检查项 | 结果 |
|
||||||
|
|--------|------|
|
||||||
|
| `resolve_offline_telegram_credentials` 专用优先 / 半配回退 | ✅ 3 case |
|
||||||
|
| `send_telegram_offline_alert` 传入专用 token/chat | ✅ mock 单测 |
|
||||||
|
| 资源告警仍走默认 Bot | ✅ 未改 `send_telegram_alert` |
|
||||||
|
| 设置页离线 Bot UI + 3 API | ✅ 代码 + 构建产物 |
|
||||||
|
| Layer3 `health_monitor.sh` 使用离线 Bot | ✅ **否**(设计:仅默认 `NEXUS_TELEGRAM_*`) |
|
||||||
|
| `ops_patrol_sync` 同步离线 Token | ✅ **否**(符合设计) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Layer 3 宿主机巡检
|
||||||
|
|
||||||
|
| 项 | 本地状态 |
|
||||||
|
|----|----------|
|
||||||
|
| `deploy/health_monitor.sh` 语法 | ✅ `bash -n` 通过 |
|
||||||
|
| `test_ops_patrol_sync.py` | ✅ 7/7 |
|
||||||
|
| `.env` 内 `NEXUS_TELEGRAM_*` | ❌ 未写入(`env_synced: false`) |
|
||||||
|
| DB `TELEGRAM_BOT_TOKEN/CHAT_ID` | ❌ 本地未配置 |
|
||||||
|
| 离线专用 `TELEGRAM_OFFLINE_*` | ❌ 未配置(预期:不影响 L3) |
|
||||||
|
| 生产 cron / `nexus_health.log` | ⚠️ 无法 SSH 核查(`nexus` 主机名不可解析) |
|
||||||
|
|
||||||
|
**说明**:Layer3 巡检 Telegram 依赖设置页保存后「同步巡检 Telegram」或 `health_monitor.sh` 的 DB 回退;与本次**离线专用 Bot**无关。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 部署门控 `pre_deploy_check.sh`
|
||||||
|
|
||||||
|
| Gate | 结果 |
|
||||||
|
|------|------|
|
||||||
|
| 1 Changelog | ✅ `2026-06-13-offline-alert-telegram-split.md` |
|
||||||
|
| 2 Audit | ❌ 缺少 `docs/audit/2026-06-13-*.md`(审计写在 `docs/reports/`) |
|
||||||
|
| 3 Test | ✅ |
|
||||||
|
| 4 Lint | ✅ |
|
||||||
|
| 5 Import | ✅ |
|
||||||
|
| 6 Security | ✅ bandit 0 HIGH |
|
||||||
|
| 7 Review | ❌ 无 audit 文件对照 diff |
|
||||||
|
|
||||||
|
**部署结论**:**BLOCKED**,需将门控格式审计写入 `docs/audit/2026-06-13-*.md`(含 Step 3 / Closure / DoD 段落)后方可 `deploy-production.sh`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 生产可达性(本环境)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://api.synaglobal.vip/health → Recv failure: 连接被对方重置
|
||||||
|
ssh nexus → Could not resolve hostname
|
||||||
|
curl http://127.0.0.1:8600/health → ok
|
||||||
|
```
|
||||||
|
|
||||||
|
外网/SSH 失败可能为本机网络或 DNS 限制,**不能据此判定生产宕机**;请在可访问生产的环境复验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 风险与待办
|
||||||
|
|
||||||
|
| 优先级 | 项 | 建议 |
|
||||||
|
|--------|-----|------|
|
||||||
|
| P0 部署 | 门控审计路径 | 补 `docs/audit/2026-06-13-offline-quick-cmd-telegram.md` |
|
||||||
|
| P1 上线 | 前端 `web/app/` | `vite build` 后部署 |
|
||||||
|
| P1 上线 | 后端重启 | 新 `telegram/offline/*` 端点 |
|
||||||
|
| P2 运维 | 离线专用 Bot | 设置页配置 + 测试离线通道 |
|
||||||
|
| P2 运维 | Layer3 | 保存默认 Telegram 后点「同步巡检 Telegram」 |
|
||||||
|
| P3 | 审计 P3 项 | 见 `docs/reports/2026-06-13-session-features-audit.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 建议部署后终验清单
|
||||||
|
|
||||||
|
1. 设置 → 离线专用 Bot → **测试离线通道**
|
||||||
|
2. 终端 → 自定义快捷命令 → 点击执行有回车
|
||||||
|
3. 设置 → Layer3 状态芯片「巡检 Telegram 就绪」+ `.env 已同步`(若使用 L3)
|
||||||
|
4. `https://api.synaglobal.vip/health` → `ok`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 终端快捷命令回车修复 — 验证报告
|
||||||
|
|
||||||
|
**日期**: 2026-06-13
|
||||||
|
**变更**: `formatQuickCmdForSend` + `execQuickCmd` 发送时追加 `\r`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
| 项 | 命令/方式 | 结果 |
|
||||||
|
|----|-----------|------|
|
||||||
|
| 前端构建 | `cd frontend && npm run build` | PASS(5.37s,含 `TerminalPage-*.js`) |
|
||||||
|
| 本地门控 | `bash scripts/local_verify.sh` | PASS(26/26 API smoke + ruff) |
|
||||||
|
| 发送逻辑单测 | Node/Python 镜像 `formatQuickCmdForSend` | PASS(含 `trim()` 剥 `\r` 后仍能补回) |
|
||||||
|
| API 集成 | `pytest tests/test_terminal_quick_commands.py` | PASS |
|
||||||
|
| 浏览器 E2E(WebSSH 点击执行) | 未执行 | SKIP — 需已登录 + 可 SSH 子机 |
|
||||||
|
|
||||||
|
## 根因复现(已确认)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'ls\r'.trim() // => 'ls' (\r 被当作空白剥掉)
|
||||||
|
```
|
||||||
|
|
||||||
|
修复后:`formatQuickCmdForSend('ls')` => `'ls\r'`,与命令栏 `sendCmd` 行为一致。
|
||||||
|
|
||||||
|
## 手动终验(部署后)
|
||||||
|
|
||||||
|
1. 设置 → 终端快捷命令 → 添加 `echo hello`,保存。
|
||||||
|
2. 终端页连接子机 → 点击该快捷命令。
|
||||||
|
3. 预期:输出 `hello` 并回到新提示符(非悬停在输入行)。
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 内置命令 DB 中可能仍带 `\r`;`formatQuickCmdForSend` 先去尾再追加,幂等安全。
|
||||||
|
- 仅前端变更;部署 `web/app/` 即可,无需重启后端。
|
||||||
@@ -138,8 +138,7 @@ async function save() {
|
|||||||
snackbar('名称和命令不能为空', 'warning')
|
snackbar('名称和命令不能为空', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let cmdText = formCmd.value.trim()
|
const cmdText = formCmd.value.trim()
|
||||||
if (!cmdText.endsWith('\r')) cmdText += '\r'
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
if (editingId.value) await update(editingId.value, formName.value, cmdText)
|
if (editingId.value) await update(editingId.value, formName.value, cmdText)
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { FitAddon } from '@xterm/addon-fit'
|
|||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
import { http } from '@/api'
|
import { http } from '@/api'
|
||||||
import { useSnackbar } from '@/composables/useSnackbar'
|
import { useSnackbar } from '@/composables/useSnackbar'
|
||||||
import { sortQuickCommands, type QuickCmdItem } from '@/composables/useTerminalQuickCommands'
|
import {
|
||||||
|
formatQuickCmdForSend,
|
||||||
|
sortQuickCommands,
|
||||||
|
type QuickCmdItem,
|
||||||
|
} from '@/composables/useTerminalQuickCommands'
|
||||||
import { buildWebSocketUrl } from '@/utils/wsUrl'
|
import { buildWebSocketUrl } from '@/utils/wsUrl'
|
||||||
import {
|
import {
|
||||||
createNative,
|
createNative,
|
||||||
@@ -704,7 +708,7 @@ export function useTerminalSessions(nexusDrawer: Ref<boolean>) {
|
|||||||
snackbar(s?.status === 'connecting' ? '终端连接中,请稍候' : '终端未连接', 'warning')
|
snackbar(s?.status === 'connecting' ? '终端连接中,请稍候' : '终端未连接', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendWsData(s.id, cmd)
|
sendWsData(s.id, formatQuickCmdForSend(cmd))
|
||||||
getNative(s.id)?.term?.focus()
|
getNative(s.id)?.term?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export function sortQuickCommands(items: QuickCmdItem[]): QuickCmdItem[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 快捷命令点击执行时追加回车(与命令栏 sendCmd 一致);已含 \\r/\\n 时不重复追加 */
|
||||||
|
export function formatQuickCmdForSend(cmd: string): string {
|
||||||
|
const body = cmd.replace(/[\r\n]+$/, '')
|
||||||
|
return body + '\r'
|
||||||
|
}
|
||||||
|
|
||||||
export function useTerminalQuickCommands() {
|
export function useTerminalQuickCommands() {
|
||||||
const commands = ref<QuickCmdItem[]>([])
|
const commands = ref<QuickCmdItem[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|||||||
@@ -165,6 +165,69 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<v-divider class="my-3" />
|
||||||
|
<div class="text-subtitle-2 mb-1">子机离线告警专用 Bot(可选)</div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-3">
|
||||||
|
与上方默认 Bot 分离:仅「子机离线告警」走此通道。Bot Token 与 Chat ID 均填写后生效;留空则离线告警仍走默认 Bot。
|
||||||
|
</p>
|
||||||
|
<v-text-field
|
||||||
|
v-model="telegramOfflineTokenDraft"
|
||||||
|
label="离线告警 Bot Token"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
class="mb-3"
|
||||||
|
:placeholder="telegramOfflineTokenSet ? '已配置(留空保持不变)' : '输入专用 Bot Token'"
|
||||||
|
:type="showOfflineToken ? 'text' : 'password'"
|
||||||
|
:append-inner-icon="showOfflineToken ? 'mdi-eye-off' : 'mdi-eye'"
|
||||||
|
@click:append-inner="toggleOfflineTelegramTokenVisibility"
|
||||||
|
/>
|
||||||
|
<v-chip
|
||||||
|
v-if="telegramOfflineTokenSet && !telegramOfflineTokenDraft"
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
离线 Token 已配置
|
||||||
|
</v-chip>
|
||||||
|
<v-text-field
|
||||||
|
v-model="settings.telegram_offline_chat_id"
|
||||||
|
label="离线告警 Chat ID"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<div class="d-flex flex-wrap ga-2 mb-2">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-radar"
|
||||||
|
:loading="detectingOfflineTgChats"
|
||||||
|
@click="detectOfflineTelegramChats"
|
||||||
|
>
|
||||||
|
检测离线 Chat ID
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-message-text"
|
||||||
|
:loading="testingOfflineTg"
|
||||||
|
@click="testOfflineTelegram"
|
||||||
|
>
|
||||||
|
测试离线通道
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-content-save"
|
||||||
|
:loading="savingOfflineTelegram"
|
||||||
|
@click="saveOfflineTelegramConfig"
|
||||||
|
>
|
||||||
|
保存离线 Bot
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-divider class="my-4" />
|
<v-divider class="my-4" />
|
||||||
<div class="text-subtitle-2 mb-1">Layer 3 宿主机巡检</div>
|
<div class="text-subtitle-2 mb-1">Layer 3 宿主机巡检</div>
|
||||||
<p class="text-caption text-medium-emphasis mb-3">
|
<p class="text-caption text-medium-emphasis mb-3">
|
||||||
@@ -475,9 +538,17 @@ const settings = ref({
|
|||||||
cpu_alert_threshold: 80, mem_alert_threshold: 80, disk_alert_threshold: 80,
|
cpu_alert_threshold: 80, mem_alert_threshold: 80, disk_alert_threshold: 80,
|
||||||
db_pool_size: 10, db_max_overflow: 20,
|
db_pool_size: 10, db_max_overflow: 20,
|
||||||
telegram_bot_token: '', telegram_chat_id: '',
|
telegram_bot_token: '', telegram_chat_id: '',
|
||||||
|
telegram_offline_chat_id: '',
|
||||||
})
|
})
|
||||||
const telegramTokenDraft = ref('')
|
const telegramTokenDraft = ref('')
|
||||||
const telegramTokenSet = ref(false)
|
const telegramTokenSet = ref(false)
|
||||||
|
const telegramOfflineTokenDraft = ref('')
|
||||||
|
const telegramOfflineTokenSet = ref(false)
|
||||||
|
const showOfflineToken = ref(false)
|
||||||
|
const savingOfflineTelegram = ref(false)
|
||||||
|
const testingOfflineTg = ref(false)
|
||||||
|
const detectingOfflineTgChats = ref(false)
|
||||||
|
const tgChatTarget = ref<'default' | 'offline'>('default')
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const systemSettingsFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null)
|
const systemSettingsFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null)
|
||||||
const testingTg = ref(false)
|
const testingTg = ref(false)
|
||||||
@@ -554,6 +625,11 @@ async function loadSettings(silent = false) {
|
|||||||
telegramTokenDraft.value = ''
|
telegramTokenDraft.value = ''
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (key === 'telegram_offline_bot_token') {
|
||||||
|
telegramOfflineTokenSet.value = Boolean(item.value_set)
|
||||||
|
telegramOfflineTokenDraft.value = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (key === 'script_exec_complete_sound') {
|
if (key === 'script_exec_complete_sound') {
|
||||||
scriptExecCompleteSound.value = normalizeScriptCompleteSound(item.value)
|
scriptExecCompleteSound.value = normalizeScriptCompleteSound(item.value)
|
||||||
continue
|
continue
|
||||||
@@ -697,6 +773,109 @@ async function saveTelegramConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistOfflineTelegramConfig(options?: { requireToken?: boolean; requireChatId?: boolean }): Promise<boolean> {
|
||||||
|
const tokenUpdate = telegramOfflineTokenDraft.value.trim()
|
||||||
|
const chatId = String(settings.value.telegram_offline_chat_id || '').trim()
|
||||||
|
|
||||||
|
if (options?.requireToken && !tokenUpdate && !telegramOfflineTokenSet.value) {
|
||||||
|
snackbar('请先填写离线告警 Bot Token', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (options?.requireChatId && !chatId) {
|
||||||
|
snackbar('请先填写或检测离线告警 Chat ID', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenUpdate) {
|
||||||
|
await http.put('/settings/telegram_offline_bot_token', { value: tokenUpdate })
|
||||||
|
telegramOfflineTokenSet.value = true
|
||||||
|
telegramOfflineTokenDraft.value = ''
|
||||||
|
showOfflineToken.value = false
|
||||||
|
}
|
||||||
|
if (chatId) {
|
||||||
|
await http.put('/settings/telegram_offline_chat_id', { value: chatId })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveOfflineTelegramConfig() {
|
||||||
|
savingOfflineTelegram.value = true
|
||||||
|
try {
|
||||||
|
const tokenUpdate = telegramOfflineTokenDraft.value.trim()
|
||||||
|
const chatId = String(settings.value.telegram_offline_chat_id || '').trim()
|
||||||
|
if (!tokenUpdate && !chatId && !telegramOfflineTokenSet.value) {
|
||||||
|
snackbar('请先填写离线 Bot Token 或 Chat ID', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await persistOfflineTelegramConfig()
|
||||||
|
snackbar('离线告警 Bot 已保存')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '保存失败', 'error')
|
||||||
|
} finally {
|
||||||
|
savingOfflineTelegram.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testOfflineTelegram() {
|
||||||
|
testingOfflineTg.value = true
|
||||||
|
try {
|
||||||
|
if (!await persistOfflineTelegramConfig({ requireToken: true, requireChatId: true })) return
|
||||||
|
await http.post('/settings/telegram/offline/test')
|
||||||
|
snackbar('离线通道测试消息已发送')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '发送失败', 'error')
|
||||||
|
} finally {
|
||||||
|
testingOfflineTg.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectOfflineTelegramChats() {
|
||||||
|
detectingOfflineTgChats.value = true
|
||||||
|
try {
|
||||||
|
if (!await persistOfflineTelegramConfig({ requireToken: true })) return
|
||||||
|
const res = await http.get<TelegramChatsResponse>('/settings/telegram/offline/chats')
|
||||||
|
tgChats.value = res.chats || []
|
||||||
|
if (!tgChats.value.length) {
|
||||||
|
snackbar('未检测到对话,请先向离线专用 Bot 发送一条消息后再试', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tgChatTarget.value = 'offline'
|
||||||
|
tgChatDialog.value = true
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '检测失败', 'error')
|
||||||
|
} finally {
|
||||||
|
detectingOfflineTgChats.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleOfflineTelegramTokenVisibility() {
|
||||||
|
if (showOfflineToken.value) {
|
||||||
|
showOfflineToken.value = false
|
||||||
|
if (telegramOfflineTokenSet.value) {
|
||||||
|
telegramOfflineTokenDraft.value = ''
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (telegramOfflineTokenDraft.value.trim()) {
|
||||||
|
showOfflineToken.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!telegramOfflineTokenSet.value) {
|
||||||
|
showOfflineToken.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
revealingTelegramToken.value = true
|
||||||
|
try {
|
||||||
|
const res = await http.post<{ value: string }>('/settings/telegram/offline/reveal-token', {})
|
||||||
|
telegramOfflineTokenDraft.value = res.value
|
||||||
|
showOfflineToken.value = true
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '获取离线 Bot Token 失败', 'error')
|
||||||
|
} finally {
|
||||||
|
revealingTelegramToken.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onScriptExecSoundChange(value: ScriptCompleteSoundId | null) {
|
async function onScriptExecSoundChange(value: ScriptCompleteSoundId | null) {
|
||||||
if (!value || scriptSoundSaving.value) return
|
if (!value || scriptSoundSaving.value) return
|
||||||
const prev = scriptExecCompleteSound.value
|
const prev = scriptExecCompleteSound.value
|
||||||
@@ -772,6 +951,7 @@ async function detectTelegramChats() {
|
|||||||
snackbar('未检测到对话,请先向 Bot 发送一条消息后再试', 'warning')
|
snackbar('未检测到对话,请先向 Bot 发送一条消息后再试', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
tgChatTarget.value = 'default'
|
||||||
tgChatDialog.value = true
|
tgChatDialog.value = true
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
snackbar(e instanceof Error ? e.message : '检测失败', 'error')
|
snackbar(e instanceof Error ? e.message : '检测失败', 'error')
|
||||||
@@ -782,10 +962,16 @@ async function detectTelegramChats() {
|
|||||||
|
|
||||||
async function applyTelegramChat(chat: TelegramChatItem) {
|
async function applyTelegramChat(chat: TelegramChatItem) {
|
||||||
const chatId = String(chat.id)
|
const chatId = String(chat.id)
|
||||||
settings.value.telegram_chat_id = chatId
|
const isOffline = tgChatTarget.value === 'offline'
|
||||||
|
if (isOffline) {
|
||||||
|
settings.value.telegram_offline_chat_id = chatId
|
||||||
|
} else {
|
||||||
|
settings.value.telegram_chat_id = chatId
|
||||||
|
}
|
||||||
tgChatDialog.value = false
|
tgChatDialog.value = false
|
||||||
try {
|
try {
|
||||||
await http.put('/settings/telegram_chat_id', { value: chatId })
|
const key = isOffline ? 'telegram_offline_chat_id' : 'telegram_chat_id'
|
||||||
|
await http.put(`/settings/${key}`, { value: chatId })
|
||||||
snackbar(`已选择 Chat ID: ${chatId}`)
|
snackbar(`已选择 Chat ID: ${chatId}`)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
snackbar(e instanceof Error ? e.message : '保存 Chat ID 失败', 'error')
|
snackbar(e instanceof Error ? e.message : '保存 Chat ID 失败', 'error')
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const NOTIFY_TOGGLE_ITEMS: NotifyToggleItem[] = [
|
|||||||
{ key: 'notify_alert_mem', label: '内存超阈值告警', desc: '内存超阈值或恢复时推送;关闭则该类告警与恢复均不通知', group: '服务器资源' },
|
{ key: 'notify_alert_mem', label: '内存超阈值告警', desc: '内存超阈值或恢复时推送;关闭则该类告警与恢复均不通知', group: '服务器资源' },
|
||||||
{ key: 'notify_alert_disk', label: '磁盘超阈值告警', desc: '磁盘超阈值或恢复时推送;关闭则该类告警与恢复均不通知', group: '服务器资源' },
|
{ key: 'notify_alert_disk', label: '磁盘超阈值告警', desc: '磁盘超阈值或恢复时推送;关闭则该类告警与恢复均不通知', group: '服务器资源' },
|
||||||
{ key: 'notify_recovery', label: '资源恢复正常', desc: 'CPU/内存/磁盘回落正常时推送(须总开关开启且对应资源开关未关)', group: '服务器资源' },
|
{ key: 'notify_recovery', label: '资源恢复正常', desc: 'CPU/内存/磁盘回落正常时推送(须总开关开启且对应资源开关未关)', group: '服务器资源' },
|
||||||
{ key: 'notify_alert_offline', label: '子机离线告警', desc: 'Agent 心跳丢失、子机由在线变为离线时推送一次', group: '服务器' },
|
{ key: 'notify_alert_offline', label: '子机离线告警', desc: 'Agent 心跳丢失、子机由在线变为离线时推送一次;可在下方配置专用 Telegram Bot', group: '服务器' },
|
||||||
{ key: 'notify_time_drift', label: '时钟偏差严重 (≥60s)', desc: '子机与主机时间偏差过大影响 TOTP/cron', group: '服务器' },
|
{ key: 'notify_time_drift', label: '时钟偏差严重 (≥60s)', desc: '子机与主机时间偏差过大影响 TOTP/cron', group: '服务器' },
|
||||||
{ key: 'notify_system_redis', label: 'Redis 连接异常 / 恢复', desc: 'Redis 不可达或恢复时推送', group: '系统' },
|
{ key: 'notify_system_redis', label: 'Redis 连接异常 / 恢复', desc: 'Redis 不可达或恢复时推送', group: '系统' },
|
||||||
{ key: 'notify_system_mysql', label: 'MySQL 连接异常 / 恢复', desc: 'MySQL 不可达或恢复时推送', group: '系统' },
|
{ key: 'notify_system_mysql', label: 'MySQL 连接异常 / 恢复', desc: 'MySQL 不可达或恢复时推送', group: '系统' },
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const ACTION_LABELS: Record<string, string> = {
|
|||||||
delete_ssh_key_preset: '删除 SSH 密钥预设',
|
delete_ssh_key_preset: '删除 SSH 密钥预设',
|
||||||
reveal_ssh_key_preset: '查看 SSH 密钥预设',
|
reveal_ssh_key_preset: '查看 SSH 密钥预设',
|
||||||
reveal_telegram_token: '查看 Telegram Token',
|
reveal_telegram_token: '查看 Telegram Token',
|
||||||
|
reveal_offline_telegram_token: '查看离线告警 Telegram Token',
|
||||||
parse_subscription: '解析订阅节点',
|
parse_subscription: '解析订阅节点',
|
||||||
update_ip_allowlist: '更新登录 IP 白名单',
|
update_ip_allowlist: '更新登录 IP 白名单',
|
||||||
toggle_ip_allowlist: '开关登录 IP 白名单',
|
toggle_ip_allowlist: '开关登录 IP 白名单',
|
||||||
|
|||||||
+123
-16
@@ -37,7 +37,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
IMMUTABLE_KEYS = {"secret_key", "api_key", "encryption_key", "database_url"}
|
IMMUTABLE_KEYS = {"secret_key", "api_key", "encryption_key", "database_url"}
|
||||||
|
|
||||||
# Settings whose values are masked in GET responses
|
# Settings whose values are masked in GET responses
|
||||||
SENSITIVE_KEYS = {"secret_key", "api_key", "encryption_key", "telegram_bot_token"}
|
SENSITIVE_KEYS = {
|
||||||
|
"secret_key",
|
||||||
|
"api_key",
|
||||||
|
"encryption_key",
|
||||||
|
"telegram_bot_token",
|
||||||
|
"telegram_offline_bot_token",
|
||||||
|
}
|
||||||
|
|
||||||
# S-02: Whitelist of keys accepted by PUT /{key} (superset of DB_OVERRIDE_MAP + notify toggles)
|
# S-02: Whitelist of keys accepted by PUT /{key} (superset of DB_OVERRIDE_MAP + notify toggles)
|
||||||
MUTABLE_KEYS: set[str] = {
|
MUTABLE_KEYS: set[str] = {
|
||||||
@@ -45,6 +51,7 @@ MUTABLE_KEYS: set[str] = {
|
|||||||
"redis_url", "heartbeat_timeout", "api_base_url",
|
"redis_url", "heartbeat_timeout", "api_base_url",
|
||||||
"cpu_alert_threshold", "mem_alert_threshold", "disk_alert_threshold",
|
"cpu_alert_threshold", "mem_alert_threshold", "disk_alert_threshold",
|
||||||
"telegram_bot_token", "telegram_chat_id",
|
"telegram_bot_token", "telegram_chat_id",
|
||||||
|
"telegram_offline_bot_token", "telegram_offline_chat_id",
|
||||||
"login_allowlist_enabled", "login_subscription_url",
|
"login_allowlist_enabled", "login_subscription_url",
|
||||||
"login_subscription_ips", "login_manual_ips", "login_allowed_ips",
|
"login_subscription_ips", "login_manual_ips", "login_allowed_ips",
|
||||||
"notify_alert_cpu", "notify_alert_mem", "notify_alert_disk",
|
"notify_alert_cpu", "notify_alert_mem", "notify_alert_disk",
|
||||||
@@ -1125,6 +1132,7 @@ async def telegram_test_send(
|
|||||||
f"具体情况:Bot Token 与 Chat ID 配置正确,Telegram 通道可用\n"
|
f"具体情况:Bot Token 与 Chat ID 配置正确,Telegram 通道可用\n"
|
||||||
f"将接收的通知类型:\n"
|
f"将接收的通知类型:\n"
|
||||||
f"• 子机 CPU/内存/磁盘超阈值告警与恢复\n"
|
f"• 子机 CPU/内存/磁盘超阈值告警与恢复\n"
|
||||||
|
f"• 子机离线告警(可另配专用 Bot,见设置页)\n"
|
||||||
f"• Redis / MySQL 连接异常与恢复\n"
|
f"• Redis / MySQL 连接异常与恢复\n"
|
||||||
f"• 子机时钟偏差、文件推送结果\n"
|
f"• 子机时钟偏差、文件推送结果\n"
|
||||||
f"• Layer 3 宿主机巡检 /health 失败与自动重启\n"
|
f"• Layer 3 宿主机巡检 /health 失败与自动重启\n"
|
||||||
@@ -1177,23 +1185,10 @@ async def reveal_telegram_token(
|
|||||||
return {"key": "telegram_bot_token", "value": value}
|
return {"key": "telegram_bot_token", "value": value}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/telegram/chats", response_model=dict)
|
async def _telegram_fetch_chats(token: str) -> dict:
|
||||||
async def telegram_get_chats(
|
|
||||||
admin: Admin = Depends(get_current_admin),
|
|
||||||
):
|
|
||||||
"""Call getUpdates to detect recent chats and extract unique chat_ids.
|
|
||||||
|
|
||||||
User must send at least one message to the Bot before calling this.
|
|
||||||
Returns up to 5 distinct chats from recent messages.
|
|
||||||
"""
|
|
||||||
import httpx
|
import httpx
|
||||||
from server.config import settings as _settings
|
|
||||||
from server.infrastructure.telegram import TELEGRAM_API_BASE
|
from server.infrastructure.telegram import TELEGRAM_API_BASE
|
||||||
|
|
||||||
token = _settings.TELEGRAM_BOT_TOKEN
|
|
||||||
if not token:
|
|
||||||
raise HTTPException(status_code=400, detail="TELEGRAM_BOT_TOKEN 未配置")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
base_url = f"{TELEGRAM_API_BASE}/bot{token}"
|
base_url = f"{TELEGRAM_API_BASE}/bot{token}"
|
||||||
@@ -1213,7 +1208,6 @@ async def telegram_get_chats(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
|
raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
|
||||||
|
|
||||||
# Extract unique chats
|
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
chats: list[dict] = []
|
chats: list[dict] = []
|
||||||
for update in data.get("result", []):
|
for update in data.get("result", []):
|
||||||
@@ -1234,6 +1228,119 @@ async def telegram_get_chats(
|
|||||||
return {"chats": chats, "count": len(chats)}
|
return {"chats": chats, "count": len(chats)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/telegram/chats", response_model=dict)
|
||||||
|
async def telegram_get_chats(
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
):
|
||||||
|
"""Call getUpdates to detect recent chats and extract unique chat_ids.
|
||||||
|
|
||||||
|
User must send at least one message to the Bot before calling this.
|
||||||
|
Returns up to 5 distinct chats from recent messages.
|
||||||
|
"""
|
||||||
|
from server.config import settings as _settings
|
||||||
|
|
||||||
|
token = (_settings.TELEGRAM_BOT_TOKEN or "").strip()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=400, detail="TELEGRAM_BOT_TOKEN 未配置")
|
||||||
|
return await _telegram_fetch_chats(token)
|
||||||
|
|
||||||
|
|
||||||
|
def _offline_telegram_token_or_400() -> str:
|
||||||
|
from server.config import settings as _settings
|
||||||
|
|
||||||
|
token = (_settings.TELEGRAM_OFFLINE_BOT_TOKEN or "").strip()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=400, detail="telegram_offline_bot_token 未配置")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _offline_telegram_credentials_or_400() -> tuple[str, str]:
|
||||||
|
"""Resolve offline-dedicated Bot; 400 if not fully configured."""
|
||||||
|
from server.config import settings as _settings
|
||||||
|
|
||||||
|
token = _offline_telegram_token_or_400()
|
||||||
|
chat_id = (_settings.TELEGRAM_OFFLINE_CHAT_ID or "").strip()
|
||||||
|
if not chat_id:
|
||||||
|
raise HTTPException(status_code=400, detail="telegram_offline_chat_id 未配置,请先检测并填写")
|
||||||
|
return token, chat_id
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/telegram/offline/test", response_model=dict)
|
||||||
|
async def telegram_offline_test_send(
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
):
|
||||||
|
"""Send a test message via offline-dedicated Telegram Bot (not the default channel)."""
|
||||||
|
import httpx
|
||||||
|
from server.config import settings as _settings
|
||||||
|
from server.infrastructure.telegram import TELEGRAM_API_BASE
|
||||||
|
from server.utils.display_time import format_beijing_now
|
||||||
|
|
||||||
|
token, chat_id = _offline_telegram_credentials_or_400()
|
||||||
|
now = format_beijing_now()
|
||||||
|
sys_name = html.escape((_settings.SYSTEM_NAME or "Nexus").strip())
|
||||||
|
message = (
|
||||||
|
f"✅ <b>【离线告警 Telegram 测试】消息发送成功</b>\n"
|
||||||
|
f"平台:{sys_name}\n"
|
||||||
|
f"具体情况:离线专用 Bot Token 与 Chat ID 配置正确\n"
|
||||||
|
f"将接收的通知:子机由在线变为离线时的 Telegram 推送(须开启「子机离线告警」开关)\n"
|
||||||
|
f"说明:未配置离线专用 Bot 时,离线告警回退至默认 Telegram 通道\n"
|
||||||
|
f"时间:{now}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{TELEGRAM_API_BASE}/bot{token}/sendMessage",
|
||||||
|
json={
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
|
||||||
|
|
||||||
|
if resp.status_code == 200 and data.get("ok"):
|
||||||
|
return {"success": True}
|
||||||
|
desc = data.get("description") or f"HTTP {resp.status_code}"
|
||||||
|
raise HTTPException(status_code=502, detail=f"Telegram API: {desc}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/telegram/offline/reveal-token", response_model=dict)
|
||||||
|
async def reveal_offline_telegram_token(
|
||||||
|
request: Request,
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Reveal offline-dedicated Telegram Bot Token (JWT + audit)."""
|
||||||
|
repo = SettingRepositoryImpl(db)
|
||||||
|
value = await repo.get("telegram_offline_bot_token")
|
||||||
|
if value is None:
|
||||||
|
raise HTTPException(status_code=404, detail="telegram_offline_bot_token not found")
|
||||||
|
|
||||||
|
audit_repo = AuditLogRepositoryImpl(db)
|
||||||
|
await audit_repo.create(AuditLog(
|
||||||
|
admin_username=admin.username,
|
||||||
|
action="reveal_offline_telegram_token",
|
||||||
|
target_type="setting",
|
||||||
|
detail="查看离线告警 Telegram Bot Token",
|
||||||
|
ip_address=request.client.host if request.client else "",
|
||||||
|
))
|
||||||
|
return {"key": "telegram_offline_bot_token", "value": value}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/telegram/offline/chats", response_model=dict)
|
||||||
|
async def telegram_offline_get_chats(
|
||||||
|
admin: Admin = Depends(get_current_admin),
|
||||||
|
):
|
||||||
|
"""Detect chats for offline-dedicated Bot via getUpdates."""
|
||||||
|
token = _offline_telegram_token_or_400()
|
||||||
|
return await _telegram_fetch_chats(token)
|
||||||
|
|
||||||
|
|
||||||
# ── IP Allowlist ──
|
# ── IP Allowlist ──
|
||||||
|
|
||||||
_IP_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$")
|
_IP_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$")
|
||||||
|
|||||||
+5
-8
@@ -80,6 +80,9 @@ class Settings(BaseSettings):
|
|||||||
# Telegram (mutable — configurable from settings UI)
|
# Telegram (mutable — configurable from settings UI)
|
||||||
TELEGRAM_BOT_TOKEN: str = ""
|
TELEGRAM_BOT_TOKEN: str = ""
|
||||||
TELEGRAM_CHAT_ID: str = ""
|
TELEGRAM_CHAT_ID: str = ""
|
||||||
|
# 子机离线告警专用(二者均配置时优先于默认 Bot;否则回退 TELEGRAM_*)
|
||||||
|
TELEGRAM_OFFLINE_BOT_TOKEN: str = ""
|
||||||
|
TELEGRAM_OFFLINE_CHAT_ID: str = ""
|
||||||
|
|
||||||
# API docs — "true" exposes /docs /redoc /openapi.json (default off for production)
|
# API docs — "true" exposes /docs /redoc /openapi.json (default off for production)
|
||||||
EXPOSE_API_DOCS: str = "false"
|
EXPOSE_API_DOCS: str = "false"
|
||||||
@@ -108,14 +111,6 @@ class Settings(BaseSettings):
|
|||||||
NOTIFY_RESTART: str = "true" # Nexus 后端重启通知
|
NOTIFY_RESTART: str = "true" # Nexus 后端重启通知
|
||||||
NOTIFY_SCRIPT_COMPLETE: str = "true" # 脚本执行完成汇总(含看板链接)
|
NOTIFY_SCRIPT_COMPLETE: str = "true" # 脚本执行完成汇总(含看板链接)
|
||||||
|
|
||||||
# Remote browser (Playwright Worker on separate VPS)
|
|
||||||
BROWSER_ENABLED: str = "false"
|
|
||||||
BROWSER_WORKER_URL: str = ""
|
|
||||||
BROWSER_WORKER_SECRET: str = ""
|
|
||||||
BROWSER_MAX_SESSIONS: int = 2
|
|
||||||
BROWSER_IDLE_TIMEOUT_SEC: int = 1800
|
|
||||||
BROWSER_WORKER_TLS_VERIFY: str = "true"
|
|
||||||
|
|
||||||
# extra=ignore: .env 中与 MCP 共存的 MYSQL_* / NEXUS_API_BASE_URL 等不进入 Settings
|
# extra=ignore: .env 中与 MCP 共存的 MYSQL_* / NEXUS_API_BASE_URL 等不进入 Settings
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_prefix="NEXUS_",
|
env_prefix="NEXUS_",
|
||||||
@@ -139,6 +134,8 @@ class Settings(BaseSettings):
|
|||||||
"disk_alert_threshold": "DISK_ALERT_THRESHOLD",
|
"disk_alert_threshold": "DISK_ALERT_THRESHOLD",
|
||||||
"telegram_bot_token": "TELEGRAM_BOT_TOKEN",
|
"telegram_bot_token": "TELEGRAM_BOT_TOKEN",
|
||||||
"telegram_chat_id": "TELEGRAM_CHAT_ID",
|
"telegram_chat_id": "TELEGRAM_CHAT_ID",
|
||||||
|
"telegram_offline_bot_token": "TELEGRAM_OFFLINE_BOT_TOKEN",
|
||||||
|
"telegram_offline_chat_id": "TELEGRAM_OFFLINE_CHAT_ID",
|
||||||
"login_allowlist_enabled": "LOGIN_ALLOWLIST_ENABLED",
|
"login_allowlist_enabled": "LOGIN_ALLOWLIST_ENABLED",
|
||||||
"login_subscription_url": "LOGIN_SUBSCRIPTION_URL",
|
"login_subscription_url": "LOGIN_SUBSCRIPTION_URL",
|
||||||
"login_subscription_ips": "LOGIN_SUBSCRIPTION_IPS",
|
"login_subscription_ips": "LOGIN_SUBSCRIPTION_IPS",
|
||||||
|
|||||||
@@ -93,17 +93,38 @@ async def close_client():
|
|||||||
_http_client = None
|
_http_client = None
|
||||||
|
|
||||||
|
|
||||||
async def send_telegram(message: str) -> bool:
|
def resolve_offline_telegram_credentials() -> tuple[str, str] | None:
|
||||||
|
"""Bot + chat for offline alerts: dedicated pair if both set, else default."""
|
||||||
|
off_token = (settings.TELEGRAM_OFFLINE_BOT_TOKEN or "").strip()
|
||||||
|
off_chat = (settings.TELEGRAM_OFFLINE_CHAT_ID or "").strip()
|
||||||
|
if off_token and off_chat:
|
||||||
|
return off_token, off_chat
|
||||||
|
|
||||||
|
main_token = (settings.TELEGRAM_BOT_TOKEN or "").strip()
|
||||||
|
main_chat = (settings.TELEGRAM_CHAT_ID or "").strip()
|
||||||
|
if main_token and main_chat:
|
||||||
|
return main_token, main_chat
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def send_telegram(
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
bot_token: str | None = None,
|
||||||
|
chat_id: str | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Send a Telegram message via Bot API."""
|
"""Send a Telegram message via Bot API."""
|
||||||
if not settings.TELEGRAM_BOT_TOKEN or not settings.TELEGRAM_CHAT_ID:
|
token = (bot_token or settings.TELEGRAM_BOT_TOKEN or "").strip()
|
||||||
|
chat = (chat_id or settings.TELEGRAM_CHAT_ID or "").strip()
|
||||||
|
if not token or not chat:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = await _get_client()
|
client = await _get_client()
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
f"{TELEGRAM_API_BASE}/bot{settings.TELEGRAM_BOT_TOKEN}/sendMessage",
|
f"{TELEGRAM_API_BASE}/bot{token}/sendMessage",
|
||||||
json={
|
json={
|
||||||
"chat_id": settings.TELEGRAM_CHAT_ID,
|
"chat_id": chat,
|
||||||
"text": message,
|
"text": message,
|
||||||
"parse_mode": "HTML",
|
"parse_mode": "HTML",
|
||||||
},
|
},
|
||||||
@@ -175,6 +196,11 @@ async def send_telegram_offline_alert(
|
|||||||
|
|
||||||
ip_line = _telegram_ip_line(public_ip, private_ip)
|
ip_line = _telegram_ip_line(public_ip, private_ip)
|
||||||
|
|
||||||
|
creds = resolve_offline_telegram_credentials()
|
||||||
|
if not creds:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bot_token, tg_chat_id = creds
|
||||||
return await send_telegram(
|
return await send_telegram(
|
||||||
f"🔴 <b>【子机离线】Agent 心跳丢失</b>\n"
|
f"🔴 <b>【子机离线】Agent 心跳丢失</b>\n"
|
||||||
f"平台:{_system_name()}\n"
|
f"平台:{_system_name()}\n"
|
||||||
@@ -182,7 +208,9 @@ async def send_telegram_offline_alert(
|
|||||||
f"检测来源:Layer 2 server_offline_monitor(30 秒周期)\n"
|
f"检测来源:Layer 2 server_offline_monitor(30 秒周期)\n"
|
||||||
f"具体情况:子机未在预期时间内上报心跳,平台已将其标记为离线\n"
|
f"具体情况:子机未在预期时间内上报心跳,平台已将其标记为离线\n"
|
||||||
f"建议操作:检查子机 Agent 进程、网络与防火墙;或在 Nexus 服务器页执行诊断\n"
|
f"建议操作:检查子机 Agent 进程、网络与防火墙;或在 Nexus 服务器页执行诊断\n"
|
||||||
f"时间:{_now_cn()}"
|
f"时间:{_now_cn()}",
|
||||||
|
bot_token=bot_token,
|
||||||
|
chat_id=tg_chat_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Offline alert Telegram channel routing."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from server.infrastructure.telegram import resolve_offline_telegram_credentials, send_telegram_offline_alert
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_offline_credentials_prefers_dedicated(monkeypatch):
|
||||||
|
from server.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_BOT_TOKEN", "off-token")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_CHAT_ID", "off-chat")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "main-token")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_CHAT_ID", "main-chat")
|
||||||
|
|
||||||
|
assert resolve_offline_telegram_credentials() == ("off-token", "off-chat")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_offline_credentials_falls_back_to_main(monkeypatch):
|
||||||
|
from server.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_BOT_TOKEN", "")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_CHAT_ID", "")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "main-token")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_CHAT_ID", "main-chat")
|
||||||
|
|
||||||
|
assert resolve_offline_telegram_credentials() == ("main-token", "main-chat")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_offline_credentials_partial_offline_uses_main(monkeypatch):
|
||||||
|
from server.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_BOT_TOKEN", "off-only")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_CHAT_ID", "")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_BOT_TOKEN", "main-token")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_CHAT_ID", "main-chat")
|
||||||
|
|
||||||
|
assert resolve_offline_telegram_credentials() == ("main-token", "main-chat")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_offline_alert_uses_dedicated_bot(monkeypatch):
|
||||||
|
from server.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_BOT_TOKEN", "off-token")
|
||||||
|
monkeypatch.setattr(settings, "TELEGRAM_OFFLINE_CHAT_ID", "off-chat")
|
||||||
|
monkeypatch.setattr(settings, "NOTIFY_ALERT_OFFLINE", "true")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"server.infrastructure.telegram.send_telegram",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=True,
|
||||||
|
) as mock_send:
|
||||||
|
ok = await send_telegram_offline_alert("srv-1", server_id=9)
|
||||||
|
|
||||||
|
assert ok is True
|
||||||
|
mock_send.assert_awaited_once()
|
||||||
|
_, kwargs = mock_send.call_args
|
||||||
|
assert kwargs["bot_token"] == "off-token"
|
||||||
|
assert kwargs["chat_id"] == "off-chat"
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Terminal quick commands API — custom cmd stored without trailing CR."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_quick_command_stores_trimmed_cmd_without_cr(
|
||||||
|
integration_env, api_client, auth_headers
|
||||||
|
):
|
||||||
|
"""Frontend save uses cmd.trim() — store body only; CR is send-time in execQuickCmd."""
|
||||||
|
name = "verify-enter-fix"
|
||||||
|
cmd_body = "echo __nexus_qc_enter__"
|
||||||
|
|
||||||
|
create = await api_client.post(
|
||||||
|
"/api/terminal/quick-commands",
|
||||||
|
headers=auth_headers,
|
||||||
|
json={"name": name, "cmd": cmd_body},
|
||||||
|
)
|
||||||
|
assert create.status_code == 201, create.text
|
||||||
|
created = create.json()
|
||||||
|
cmd_id = created["id"]
|
||||||
|
assert created["cmd"] == cmd_body
|
||||||
|
assert not created["cmd"].endswith("\r")
|
||||||
|
|
||||||
|
listed = await api_client.get("/api/terminal/quick-commands", headers=auth_headers)
|
||||||
|
assert listed.status_code == 200
|
||||||
|
match = next((c for c in listed.json() if c["id"] == cmd_id), None)
|
||||||
|
assert match is not None
|
||||||
|
assert match["cmd"] == cmd_body
|
||||||
|
|
||||||
|
delete = await api_client.delete(
|
||||||
|
f"/api/terminal/quick-commands/{cmd_id}",
|
||||||
|
headers=auth_headers,
|
||||||
|
)
|
||||||
|
assert delete.status_code == 204
|
||||||
Reference in New Issue
Block a user