! 本篇文章將會介紹 進度追蹤:30 天作戰地圖,期望大家都能不架任何監控平台,用狀態檔和幾十行 bash 一眼看清完賽進度 :D
昨天(D23)把重試鏈、備稿、告警三層保命機制補完之後,這套系統已經會自己出貨、自己擦屁股、自己叫人了。但每晚看著 Discord 跳出綠色告警,我心裡還是有一個問題:它現在到底跑得怎麼樣? 已經打到第幾天、還剩幾發彈藥、斷更的風險離我多遠?
標準答案是裝個 Grafana 之類的監控儀表板。但這個系列的主題是耍廢——能讀檔案就不要架服務。而且真相其實早就躺在 state/ 目錄裡了,今天只是把它畫出來。
讀完這篇你會學到:
published.log 和 published-*.flag 各自記了什麼、誰在寫、誰在讀延續 D22 的管線,跑了一段時間之後,state/ 裡已經有東西:
$ ls state/
published-20260912.flag # d02 發佈成功的存在證明(空檔案,只看有沒有)
published-20260913.flag
# ...(一天一個,到昨天為止共 22 個)
published.log # 每次成功發佈追加一行:日期|天數|文章 URL
其他需求非常樸素:
config.sh 裡的 START_DATE 與 TOTAL_DAYS——進度腳本直接沿用,不另設常數自動化系統最怕「為了做儀表板,再養一套系統」。好消息是這條管線從第一天起就是「事件發生當下順手記一筆」的設計。publish.sh 在確認發佈成功(輪詢到 URL 變成 /articles/10xxxx)之後做了兩件事:
# publish.sh:發佈成功當下順手記帳(append-only)
touch "$ROOT/state/published-$(date +%Y%m%d).flag" # 存在證明:給機器看
echo "$(date +%F)|${DD}|${AFTER}" >> "$ROOT/state/published.log" # 明細:給人看
兩個檔案,兩種讀者:
設計原則一句話:append-only、事實單一來源。「發佈成功」這個事件發生時寫一次,之後不管是冪等檢查、補發判斷,還是今天的進度儀表板,都只是換個角度讀同一份事實,不需要另外維護一套「進度系統」。
另一條路是反過來做:直接去 iThelp 的系列頁爬自己發過幾篇。能動,但代價是儀表板從此依賴外部網站的頁面結構——網站一改版、尖峰時段一變慢,儀表板就跟著抖。狀態檔在自己家裡,讀它零網路、零依賴、零風險。這就是我說「真相早就寫好了」的意思。
有了狀態檔,儀表板就只是字串處理。progress.sh 的核心:
#!/bin/bash
# progress.sh:讀狀態檔、畫進度。純唯讀,跑幾次都不會弄髒系統
ROOT="/Users/benben/ai/automations/ironman"
source "$ROOT/config.sh" # START_DATE、TOTAL_DAYS 從這裡來
# 今天是第幾天(與 generate.sh 同一套算法)
TODAY=$(date +%Y-%m-%d)
DAY=$(( ($(date -j -f "%Y-%m-%d" "$TODAY" +%s) \
- $(date -j -f "%Y-%m-%d" "$START_DATE" +%s)) / 86400 + 1 ))
# 已發佈天數:published.log 一行 = 一次成功發佈
DONE=$(wc -l < "$ROOT/state/published.log" | tr -d ' ')
PCT=$(( DONE * 100 / TOTAL_DAYS ))
# 進度條:20 格,█=走過的路 ░=剩下的賽程
FILLED=$(( DONE * 20 / TOTAL_DAYS ))
BAR=$(printf '█%.0s' $(seq 1 "$FILLED"))$(printf '░%.0s' $(seq $((FILLED+1)) 20))
echo "鐵人賽進度:${BAR} ${DONE}/${TOTAL_DAYS} 天(${PCT}%)"
輸出長這樣:
鐵人賽進度:██████████████░░░░░░ 22/30 天(73%)
兩個「沿用而非重造」的細節:算天數的公式跟 generate.sh 完全同一套(START_DATE 的 epoch 差除以 86400 加 1),TOTAL_DAYS 也是直接 source config.sh。儀表板和被儀表的系統必須共用同一份常數,否則遲早出現「儀表板以為賽期是 35 天」這種平行宇宙。
小小小測驗:你知道為什麼腳本算出來是 22 天,但這個系列明明已經發了 23 篇嗎?(答案在踩坑記錄第一條,這個坑我踩得非常踏實 :D)
進度條回答「還剩多少」,但鐵人賽真正的問題是「哪幾天打下了、今天在哪、接下來是什麼」。所以再往下畫一張 30 格的地圖:
# 30 格作戰地圖:● 已發佈、◉ 今天、· 未來的戰場
MAP=""
for d in $(seq 1 "$TOTAL_DAYS"); do
if grep -q "|$(printf '%02d' "$d")|" "$ROOT/state/published.log"; then MAP+="●"
elif (( d == DAY )); then MAP+="◉"
else MAP+="·"
fi
done
echo "作戰地圖:${MAP}"
# 今天與明天的主題(outline.md 格式:DD|標題|摘要)
TDD=$(printf '%02d' $(( DAY + 1 )))
awk -F'|' -v d="$DD" '$1==d {print "今天(d" $1 "):" $2}' "$ROOT/outline.md"
awk -F'|' -v d="$TDD" '$1==d {print "明天(d" $1 "):" $2}' "$ROOT/outline.md"
完整輸出(就是寫這篇的當下跑出來的):
鐵人賽進度:██████████████░░░░░░ 22/30 天(73%)
作戰地圖:·●●●●●●●●●●●●●●●●●●●●●◉······
今天(d24):進度追蹤:30 天作戰地圖
明天(d25):日誌與稽核:自動化系統的黑盒子
一眼看完戰況:22 個 ● 是已經打下的城,◉ 是今天——也就是此刻正在生成的這篇,剩下六個 · 是還沒來的戰場。至於最左邊那個孤零零的 · 是歷史遺跡,踩坑記錄見。
順帶一提,「今天(d24)/明天(d25)」那兩行不是裝飾。outline.md 是整個系統的唯一事實來源,地圖直接從它抓標題,所以「今天該寫什麼、明天會是什麼」我也不用另外記——大綱改了,地圖自動跟著變。
這支腳本純唯讀:不寫檔、不碰網路、跑幾次輸出都一樣,跟 publish 那種有副作用的腳本完全相反,所以可以隨手跑、跑心安的。想每天在 Discord 看戰況的話,接上 D13 的通知腳本一行就搞定:
# 發文 job 收工前順手回報進度(discord-notify.sh 見 D13)
MSG="$(/bin/bash "$ROOT/scripts/progress.sh")"
"$NOTIFY" -t "鐵人賽進度" -s ok "$MSG"
Q:為什麼進度條顯示 22/30,但系列明明已經發了 23 篇?
A:因為 published.log 這個機制是 d02 才加上去的——第一天發文時還沒有狀態檔,d01 就這樣消失在歷史裡(就是地圖最左邊那個 ·)。狀態檔跟著系統一起演化,起點常常不齊。解法很簡單:手動回補一行(echo "2026-09-11|01|<文章URL>" >> state/published.log),或者知道誤差存在、心裡有數。真正的教訓是:儀表板做好後,第一件事是拿已知事實驗證它,不然你只是在看一部畫得很漂亮的科幻小說。
Q:為什麼讀 published.log,不直接 ls articles/*.md | wc -l 數文章檔案?
A:因為它們是兩種真相。articles/ 下的 .md 是「生成成功」的證據,不是「發佈成功」的證據——生成成功但發佈失敗的日子,檔案在、文章卻沒出去。更別提 state/ 裡還躺著 quarantine-d09-*.md 這種被機敏掃描隔離的檔案,直接數檔案一定會被騙。真相分層:articles=生成、flag=發佈與否、log=發佈+URL。要看哪一層真相,就讀哪一層的檔案。
Q:腳本裡的 date -j -f 是什麼寫法?網路上的範例都是 date -d?
A:這是 macOS 的坑。內建的是 BSD date,解析日期字串要寫 date -j -f "%Y-%m-%d" "$TODAY" +%s;Linux(GNU date)才是 date -d "$TODAY" +%s,照抄網路範例會直接噴錯。順帶一提這也是 D11 的延伸:如果哪天想把進度腳本也交給 launchd 排程,記得它一樣沒有你的 shell rc,PATH 要自己掛。
config.sh,跟主系統共用下一篇我們要介紹 日誌與稽核:自動化系統的黑盒子,敬請期待!進度儀表板看的是「現在」,明天往回看——logs/ 裡那幾十個檔案怎麼分層、flight recorder 思維怎麼讓事後除錯不靠通靈。
參考資料:
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D