diff --git a/templates/monitor-page.md b/templates/monitor-page.md index 7d5cf6e..f67211b 100644 --- a/templates/monitor-page.md +++ b/templates/monitor-page.md @@ -1,20 +1,23 @@ # 助理巡檢 — {主機名}/{登入帳號} > 由 `jsc-assist` 維護。這是監控頁 `MONITOR_{HASH}`。 -> 這頁是這台機器的巡檢軌跡:一次巡檢附加一節,節標題帶時間戳,舊的節一個字都不動。 -> 附加是刻意的。助理的寫入是背景行為,覆寫錯了沒人在現場,軌跡被抹掉也看不出斷在哪一輪。 -> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,寫入語意與這頁不同,不要混用。 +> 這頁固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。 +> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。 +> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。 +> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,別台機器的列一個字都不動。 ```mermaid flowchart LR - A[巡檢一輪] --> B[收攏各類結果] - B --> C[附加一節,節標題帶時間戳] - C --> D[既有的節原樣保留] - D --> E[回頭更新 MONITOR_CONTENTS 自己那一列] - E --> F[最後才寫心跳] + A[巡檢一輪] --> B[收攏四項結果] + B --> C[讀回舊頁] + C --> D[換掉最新一輪那一塊] + D --> E[本輪摘要列插到表格最上面,截到 24 列] + E --> F[整頁寫回] + F --> G[更新 MONITOR_CONTENTS 自己那一列] + G --> H[最後才寫心跳] ``` -心跳排在最後一步,不能提前。心跳新鮮的意思就是「上一輪跑到這一步了」:這一節沒寫上來,心跳就不寫,讓它自己過期。那是巡檢在空轉的唯一訊號。 +心跳排在最後一步,不能提前。心跳新鮮的意思就是「上一輪跑到這一步了」:這一輪沒寫上來,心跳就不寫,讓它自己過期。那是巡檢在空轉的唯一訊號。 ## 本頁基本資料 @@ -27,9 +30,11 @@ flowchart LR | 雜湊來源 | `{主機名}/{登入帳號}` | | 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) | -## 巡檢 {yyyy-MM-dd HH:mm} +## 最新一輪 -一輪巡檢就是這樣一節,最新的一節放在最下面。六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。 +這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。 + +六個子節固定都寫;某個來源讀不到,就在那個子節寫明是哪個路徑讀不到,不要整節略過。還沒實作的子節也照寫,寫明「這一輪不做這一項」——空表格會被讀成「查過了,沒問題」。 | 項目 | 內容 | | --- | --- | @@ -49,9 +54,9 @@ flowchart LR | session | {工作階段代號} | | pid | {數字}。只給要找行程的人參考,不參與判定 | -這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一節寫上來之後才寫。 +這一欄讀到的是**上一輪**巡檢寫的心跳:心跳由巡檢寫,本輪那一次要等這一頁寫成之後才寫。 -心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看本節上面的「本輪判定」。 +心跳的判準只看 `ts` 距現在有沒有超過門檻,預設 300 秒。不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳。心跳新鮮代表上一輪巡檢跑完了,不代表那一輪四項都成功——那要看這一塊上面的「本輪判定」。 ### 技能與呼叫鏈使用統計 @@ -111,11 +116,21 @@ flowchart LR | --- | --- | --- | | {一句話講完要處理什麼} | {上面六個子節之一} | {技能名或指令} | +## 近 24 輪摘要 + +一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。 + +| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | +| --- | --- | --- | ---: | +| {yyyy-MM-dd HH:mm} | {正常、警示、異常 三選一} | {成功項數}/{總項數} | {待人處理筆數} | + ## 寫入規則 -- 一次巡檢附加一節,節標題帶時間戳,節名不重複。 -- 既有的節原樣保留,一個字都不改。 -- 禁止整頁覆寫。覆寫等於把這台機器的巡檢軌跡刪掉。 -- 讀不到舊內容就中止,不附加,也不寫入。 -- 附加成功之後,才回頭更新 `MONITOR_CONTENTS` 自己那一列。 +- 讀不到舊頁就中止,不重組,也不寫入。舊頁讀不回來就沒有摘要表可以接下去。 +- 本頁基本資料原樣保留,一個字都不改。 +- 最新一輪整塊換掉,只留這一輪的完整內容。 +- 本輪的摘要列插到摘要表最上面,舊的列往下移,超過 24 列就丟掉最舊的那一列。 +- 三塊重組成一整頁再整頁寫回。除了這三塊,頁上沒有別的東西。 +- 舊格式的頁(一輪一節疊起來的那種)第一次重組時,基本資料留著,那些節收掉,摘要表從本輪這一列開始,並在回報裡說明。 +- 整頁寫成之後,才回頭更新 `MONITOR_CONTENTS` 自己那一列,別台機器的列一個字都不動。 - 兩頁都寫成之後,才寫這一輪的心跳。任一頁沒寫成就不寫心跳,讓它過期。 diff --git a/tools/patrol.sh b/tools/patrol.sh index b8bd4ee..791cfa8 100755 --- a/tools/patrol.sh +++ b/tools/patrol.sh @@ -8,7 +8,7 @@ # # collect 帶了 --out,finish 與 abort 就要帶同一個目錄,不然換不到本輪的用量快照。 # -# collect 讀四項來源、組出監控頁要附加的那一節、把鎖拿在手上。 +# collect 讀四項來源、組出監控頁那三塊、把鎖拿在手上。 # finish 在監控頁寫成功之後才呼叫:寫心跳、換上用量快照、放掉鎖。 # abort 在監控頁沒寫成時呼叫:只放掉鎖,不寫心跳。 # @@ -16,10 +16,10 @@ # 0 collect:四項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、 # 鎖放掉;abort:鎖放掉,本來就沒鎖也算 # 1 collect:部分成功——至少一項失敗,也至少一項有結果。**結果照樣印得出來,呼叫端 -# 照樣要把這一節寫上監控頁**,只是本輪判定要標成警示 +# 照樣要把這一輪寫上監控頁**,只是本輪判定要標成警示 # 2 finish:找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可寫。collect 不會回這 # 一碼——心跳讀不到只是 D-09 這一項失敗,另外三項照跑 -# 3 collect:四項全部失敗,一項資料都沒有。這一節還是要寫上監控頁,本輪判定標成異常 +# 3 collect:四項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常 # 4 上一輪還在跑,本輪讓開(collect),或鎖已經不在自己手上(finish、abort)。這不是 # 失敗,是刻意讓開:不寫心跳、不寫監控頁,下一輪再來 # 5 檔案系統失敗:鎖建不起來或放不掉、暫存檔寫不進去、快照換不上,或 heartbeat.sh write @@ -43,8 +43,7 @@ # # 一輪巡檢包含一次 CLI 呼叫與兩次 wiki 寫入,跑過一個排程週期是有可能的。所以整輪拿一把 # 鎖:$JSC_HOME/assistant/patrol.lock 是目錄,mkdir 是原子操作,搶不到就是別人在跑。 -# 搶不到的那一輪回 4 直接讓開,不排隊、不並行——並行的兩輪會在同一頁附加兩節,還會互相 -# 蓋掉用量快照。 +# 搶不到的那一輪回 4 直接讓開,不排隊、不並行——並行的兩輪會互相蓋掉監控頁與用量快照。 # 鎖會逾時自動搶回來,門檻取心跳門檻(heartbeat.sh report 的 ttl 欄):上一輪跑得比門檻 # 還久,它本來就已經維持不住心跳新鮮了,讓新的一輪接手才對。被搶回來的那一輪,finish 會 # 拿 --round 比對出鎖不是自己的,回 4 且不寫心跳。 @@ -65,6 +64,16 @@ # 巡檢照抄第四欄原字,不自己補查遠端版本、不把「查詢失敗」寫成「相符」或「最新」。 # 查不到就是沒有證據,寫成別的字等於幫一個既有缺陷蓋章。 # +# --- 監控頁固定三塊 --- +# +# 監控頁不再一輪附加一節。15 分鐘一輪,一天就是 96 節,那樣的頁沒有人讀得完,也就沒有人 +# 會發現壞掉。改成固定三塊: +# 本頁基本資料 建頁時寫一次,之後一個字都不動 +# 最新一輪 每輪整塊換掉,只留最新那一輪的完整內容 +# 近 24 輪摘要 一輪一列,最新的在最上面,超過 24 列就丟掉最舊的 +# 軌跡留在摘要表:一輪一列,看得出是從哪一輪開始壞的。完整內容只留最新一輪,因為頁面要能 +# 讀完才有人讀。 +# # --- collect 的輸出 --- # # stdout 是 key=value,一行一個鍵,供呼叫端逐行取值。監控頁要用的 markdown 不印在 @@ -79,8 +88,11 @@ # verdict= 正常、警示、異常 # failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」 # tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一列用 -# section_file= 要附加到 MONITOR_{HASH} 的那一節 -# newpage_file= MONITOR_{HASH} 不存在時要建的整頁內容 +# pending= 本輪待人處理的筆數 +# latest_file= 「最新一輪」那一塊,整塊換掉舊頁同名那一塊 +# summary_file= 「近 24 輪摘要」那一塊,表格裡先放本輪這一列,舊頁的資料列接在下面 +# summary_row_file= 只有本輪那一列,方便直接插到既有表格最上面 +# newpage_file= MONITOR_{HASH} 不存在時要建的整頁內容,三塊都已經排好 # contents_file= MONITOR_CONTENTS 那一列的欄位值 # # 環境變數: @@ -95,6 +107,7 @@ set -u JSC_HOME="${JSC_HOME:-$HOME/.jsc}" STATE_DIR="$JSC_HOME/assistant" +CURRENT="$JSC_HOME/current" LOCK="$STATE_DIR/patrol.lock" PREV_SNAP="$STATE_DIR/usage-prev.tsv" RD="$STATE_DIR/patrol" @@ -126,12 +139,17 @@ die() { # $1=結束碼 $2=訊息 } # 找一支別的 domain 的腳本。搜尋順序比照 schedule.sh 的 heartbeat_sh():先環境變數覆寫, -# 再開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。 +# 再 $JSC_HOME/current 那一組連結,然後開發用的並排存取庫版面,最後已安裝的 plugin 快取 +# 版面。current 排在快取前面是刻意的:技能與權限規則都以 current 為準,腳本內部再自己去挑 +# 另一個版本,同一輪就會跑到混版的工具,而那種不一致查起來沒有任何線索。 find_tool() { # $1=domain 短名 $2=domain 內相對路徑 $3=環境變數覆寫值(可為空) if [ -n "$3" ]; then [ -f "$3" ] && { printf '%s\n' "$3"; return 0; } return 1 fi + for _c in "$CURRENT/jsc-$1/$2" "$CURRENT/$1/$2"; do + [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } + done _root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}" for _c in "$_root/../$1/$2" "$_root/../jsc-$1/$2"; do [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } @@ -519,7 +537,7 @@ d09() { printf '| pid | %s。只給要找行程的人參考,不參與判定 |\n' "$(cell "${_pid:--}")" printf '| 心跳檔 | `%s` |\n' "$(cell "${_file:--}")" printf '\n心跳的判準只看 `ts` 距現在有沒有超過門檻,不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳,不參與判定。\n' - printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看本節上面的「本輪判定」。\n' + printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看這一塊上面的「本輪判定」。\n' } >>"$RD/d09.md" case "$_st" in @@ -546,7 +564,7 @@ count_tasks() { return 0 } -# --- 組出監控頁那一節 --- +# --- 組出監控頁那三塊 --- tally() { # $1=項目狀態 case "$1" in @@ -562,8 +580,12 @@ compose() { [ "$OK_COUNT" -eq 0 ] && _v='異常' VERDICT="$_v" + ITEM_TOTAL=$(( OK_COUNT + FAIL_COUNT )) + PEND_COUNT=$(grep -c '^| ' "$RD/pend.md" 2>/dev/null); [ -n "$PEND_COUNT" ] || PEND_COUNT=0 + { - printf '## 巡檢 %s\n\n' "$AT" + printf '## 最新一輪\n\n' + printf '這一塊每輪整塊換掉,只留最新那一輪的完整內容。再往前的軌跡看下面的摘要表。\n\n' printf '| 項目 | 內容 |\n' printf '| --- | --- |\n' printf '| 巡檢時間 | %s |\n' "$AT" @@ -589,21 +611,38 @@ compose() { printf '| 項目 | 來源子節 | 建議入口 |\n' printf '| --- | --- | --- |\n' if [ -s "$RD/pend.md" ]; then cat "$RD/pend.md"; else printf '| (無) | - | - |\n'; fi - } >"$RD/section.md" + } >"$RD/latest.md" + + # 摘要表的那一列。欄位刻意只有四個:時間、判定、四項成敗、待人處理筆數——一列要能一眼 + # 看完,才看得出是從哪一輪開始壞的。 + printf '| %s | %s | %s/%s | %s |\n' \ + "$AT" "$VERDICT" "$OK_COUNT" "$ITEM_TOTAL" "$PEND_COUNT" >"$RD/summary-row.md" + + # 摘要那一塊:標題、表頭,加上本輪這一列。呼叫端把舊頁的資料列接在這一列下面,截到 24 列。 + { + printf '## 近 24 輪摘要\n\n' + printf '一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。\n\n' + printf '| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 |\n' + printf '| --- | --- | --- | ---: |\n' + cat "$RD/summary-row.md" + } >"$RD/summary.md" # 監控頁不存在時要建的整頁內容。基本資料建頁時寫一次,之後不再更動。 { printf '# 助理巡檢 — %s/%s\n\n' "$HOST" "$USER_NAME" printf '> 由 `jsc-assist` 維護。這是監控頁 `%s`。\n' "$PAGE" - printf '> 這頁是這台機器的巡檢軌跡:一次巡檢附加一節,節標題帶時間戳,舊的節一個字都不動。\n' - printf '> 附加是刻意的。助理的寫入是背景行為,覆寫錯了沒人在現場,軌跡被抹掉也看不出斷在哪一輪。\n' - printf '> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,寫入語意與這頁不同,不要混用。\n\n' + printf '> 這頁固定三塊:本頁基本資料、最新一輪、近 24 輪摘要。\n' + printf '> 最新一輪每輪整塊換掉;摘要表一輪一列往上疊,只留 24 列;基本資料建頁時寫一次就不動。\n' + printf '> 完整內容只留最新一輪,頁面才讀得完;軌跡留在摘要表,看得出是從哪一輪開始壞的。\n' + printf '> 目錄頁 `MONITOR_CONTENTS` 只更新自己那一列,別台機器的列一個字都不動。\n\n' printf '```mermaid\nflowchart LR\n' printf ' A[巡檢一輪] --> B[收攏四項結果]\n' - printf ' B --> C[附加一節,節標題帶時間戳]\n' - printf ' C --> D[既有的節原樣保留]\n' - printf ' D --> E[回頭更新 MONITOR_CONTENTS 自己那一列]\n' - printf ' E --> F[最後才寫心跳]\n' + printf ' B --> C[讀回舊頁]\n' + printf ' C --> D[換掉最新一輪那一塊]\n' + printf ' D --> E[本輪摘要列插到表格最上面,截到 24 列]\n' + printf ' E --> F[整頁寫回]\n' + printf ' F --> G[更新 MONITOR_CONTENTS 自己那一列]\n' + printf ' G --> H[最後才寫心跳]\n' printf '```\n\n' printf '## 本頁基本資料\n\n' printf '建頁時寫一次,之後不再更動。\n\n' @@ -613,7 +652,8 @@ compose() { printf '| 帳號 | %s |\n' "$(cell "$USER_NAME")" printf '| 雜湊來源 | `%s/%s` |\n' "$(cell "$HOST")" "$(cell "$USER_NAME")" printf '| 狀態檔根目錄 | `$JSC_HOME/assistant/`(`$JSC_HOME` 未設定就退回 `~/.jsc`) |\n\n' - cat "$RD/section.md" + cat "$RD/latest.md"; printf '\n' + cat "$RD/summary.md" } >"$RD/newpage.md" { @@ -695,7 +735,10 @@ case "$CMD" in printf 'failed_sources=%s\n' "${FAILED_SOURCES:-無}" printf 'tasks_total=%s\n' "$TASKS_TOTAL" printf 'tasks_failing=%s\n' "$TASKS_FAILING" - printf 'section_file=%s\n' "$RD/section.md" + printf 'pending=%s\n' "$PEND_COUNT" + printf 'latest_file=%s\n' "$RD/latest.md" + printf 'summary_file=%s\n' "$RD/summary.md" + printf 'summary_row_file=%s\n' "$RD/summary-row.md" printf 'newpage_file=%s\n' "$RD/newpage.md" printf 'contents_file=%s\n' "$RD/contents.tsv"