# 路 B 整合計畫：STK_UPDATER 單一入口（含 UpdJson32 + C:\update 上傳）

> 目標：把目前**各自獨立**的兩套更新（Python 的 STK_UPDATER 檔案分發、Harbour 的 UpdJson32 資料庫更新）
> 整合成「**STK_UPDATER 當單一入口**」：下載 → 備份 → 關站 → 分發檔案 → 叫起 UpdJson32 做資料庫級更新。
> 並把 dev 端 `C:\update` 更新包（setnew/DDD.ZIP/UpdRule.json/STR/UpdJson32.exe）納入上傳流程。
>
> 路線：**路 B**（merge 仍由 UpdJson32 做，append 時 COMIX RDD 自動維護索引，零 reindex 問題）。
> 版本：草案 v1，2026-06-26。

---

## 0. 為什麼是路 B（已定案）
- UpdJson32 的 mergeset 在 `dbAppend` 時，COMIX RDD **同步維護 .CDX 索引** → merge 留 Harbour 完全不需額外 reindex。
- Python 無法重建 COMIX/.CDX（只有 RDD 能）→ 凡「索引」一律留 Harbour。
- 路 A（Python 做 merge + UpdJson32 逐站 reindex）會多出「逐站 SET 重建索引」的工，且無索引維護優勢 → 不採用。

---

## 1. 四個元件與角色

| 元件 | 語言/位置 | 角色（整合後） |
|---|---|---|
| **ftp_upload.py / stk_uploader** | Python, dev | 把更新包上傳 FTP：原有 EXE/SET/FRM/MEM/LBL + **新增 C:\update 包（STR/setnew/UpdRule.json/UpdJson32.exe/DDD.ZIP）** |
| **stk_updater.py** | Python, 客戶 S001 | **單一入口**：下載 → 備份 → 關站 → distribute → 叫起 UpdJson32 |
| **UpdateByJson.PRG / UpdJson32.exe** | Harbour, 客戶 | 資料庫級：struct 重建 / field / prgchk / **mergeset** / reindex / HotKey（**DDD.ZIP 解壓停用**，見決定 B1） |
| **UpdRule.json** | 設定, 隨包下載 | 驅動 UpdJson32；`always.program.enabled=false`（B1） |

---

## 2. 端到端資料流

```
[dev] C:\update (setnew/DDD.ZIP/UpdRule.json/STR/UpdJson32.exe)
      + EXE/SET/FRM/MEM/LBL
   │  ftp_upload.py / stk_uploader（上傳 + manifest）
   ▼
[FTP] /H/stk-update/update-32/  (files/ + manifest.json)
   │  stk_updater.py 下載到 temp_dir
   ▼
[客戶 S001] stk_updater.exe 流程（見 §3）
   └─ 叫起 ▼
        UpdJson32.exe（指向 temp_dir，做 DB 級更新）
```

---

## 3. 客戶端執行順序（★時機點，最關鍵）

```
1. FTP 下載：EXE/SET/FRM/MEM/LBL/SOCKET/ZIPs + 更新包(STR/setnew/Dbf/Frm/Mem/set/tmp/UpdRule.json/UpdJson32.exe/DDD.ZIP) → temp_dir
2. SOCKET/agent 部署            （agent 必須活著收 RESTART_AGENT，故在關站前）
3. relay_shutdown_all + wait_for_exit   （全站關閉並等退出）
4. ★備份（原子 + 每天一份）      ← 在所有更新之前；此時站台已關、檔案不鎖
5. ★叫起 UpdJson32.exe(指向 temp_dir)   struct/field/prgchk/mergeset/reindex/HotKey（DDD.ZIP 停用）  ← 資料先
6. distribute()                  檔案分發：EXE/PrnAsk/FRM/字型/INI + SET(★排除 6 個版面檔)            ← 程式最後
7. save_local_version            （全部成功才存版本號）
8. cleanup temp
```

**★順序鐵則：UpdJson32（資料）先、distribute（換 EXE/程式）後 —— 等同 legacy「program update LAST」。**
- **失敗退化較輕（最關鍵）**：UpdJson32 先、distribute 後失敗 → 舊 EXE 配新資料（舊 exe 容忍多餘新欄位，多半還能開）；
  反之 distribute 先換 EXE、UpdJson32 後失敗 → 新 EXE 配半套舊資料 → 找不到新欄位直接當掉。
- **UpdJson32 不依賴 distribute 產出**：它的料（STR/setnew/Dbf/Frm/UpdRule.json/UpdJson32.exe 本體）全來自下載到 temp 的 UPDATE 包，非 distribute 寫出 → 可以也應該先跑。
- **不互相覆蓋**：UpdJson32 動業務 DBF/struct；distribute 動 EXE/FRM/SET（DBF+CDX 整組自帶索引、略過 6 版面檔交 mergeset）→ 兩邊碰的檔不重疊；reindex(PrgChk/Rights，distribute 不碰)留 UpdJson32 安全。
- **前置**：UpdJson32.exe 從 temp 跑（cLocUpdate 指 temp），引擎新版在下載階段就到位，不靠 distribute。
- 站台關閉需求：struct/mergeset/reindex 都需站台關閉 → 第 5 步在第 3 步之後成立。

---

## 4. 重疊衝突與分工決定

| 重疊 | 風險 | 決定 |
|---|---|---|
| **SET 檔** | distribute 對 SET「整檔覆蓋」會洗掉 mergeset 的客戶版面 | **決定 A（必做）**：distribute 略過 6 個版面檔（MAINFORM/SUBFORM1/MAINWIN/SUBWIN/DBFMWIN/DBFSWIN），交 mergeset |
| **程式檔 EXE/FRM/SET** | UpdJson32 的 DDD.ZIP 與 distribute 重複部署 | **決定 B1（建議）**：distribute 管 EXE/FRM；UpdJson32 `always.program.enabled=false` 停 DDD.ZIP |

### ⚠️ 決定 B1 的前置風險（待驗證）
- 停 DDD.ZIP 前，要確認 **FTP 的 EXE/SET/FRM 包涵蓋 DDD.ZIP 原本部署的所有程式檔**，否則停了會漏檔。
- 待辦：列出 DDD.ZIP 內容 vs FTP manifest 的 EXE/SET/FRM 清單，逐項對照無遺漏才停。

---

## 5. 備份補強（concern 2）

- **每天一份**：已有 `today_backup_exists`（比對 `STK_<今天>_*` 資料夾）。
- **★補洞（原子化）**：`backup_stk` 改成先 copytree 到暫存名（`STK_<日期_時間>.tmp` 或 `_tmp_STK_...`），**成功才 rename 成正式 `STK_<日期_時間>`**；`today_backup_exists` 只認正式名。
  - 解決：copytree 中途失敗留半套資料夾 → 被誤判「今天已備份」→ 當天後續全跳過 → 只剩壞備份。
- **位置**：關站後、distribute/UpdJson32 之前（§3 第 4 步）。

---

## 6. 上傳端（concern 3：C:\update 上傳）

- 現況：`ftp_upload.py` 掃 source dir 的 EXE/SET/FRM/MEM/LBL 子夾 + manifest；`stk_uploader.py` 另有群組式上傳。
- **要新增**：把 `C:\update` 的更新包（`STR/`、`setnew/`、`UpdRule.json`、`UpdJson32.exe`、`DDD.ZIP`）納入上傳，使其進 FTP `files/`，讓 stk_updater 下載到 temp_dir。
- **待決**：
  - 走哪支上傳？（建議擴充 ftp_upload.py，把 update 包當額外目錄群組；或 stk_uploader 加 update 群組）
  - UpdJson32.exe 本身的版本部署管道（隨 update 包下載到 temp_dir，由 stk_updater 放到固定執行位置）。
  - manifest 是否要納入 update 包的版本/檔案清單（供漂移偵測）。

---

## 7. 分階段實作

- **Phase B-1（上傳端）**：ftp_upload/stk_uploader 納入 C:\update 包上傳 + manifest。
- **Phase B-2（下載+備份）**：stk_updater 下載 update 包到 temp_dir；備份原子化補強。
- **Phase B-3（分工）**：distribute 略過 6 SET（決定 A）；UpdRule.json 停 DDD.ZIP（決定 B1，前置風險驗證後）。
- **Phase B-4（叫起 UpdJson32）**：stk_updater 在 §3 第 6 步啟動 UpdJson32.exe 指向 temp_dir，判讀結果。
- **Phase B-5（端到端測試）**：沙箱/單站實測；逐項對照 distribute + UpdJson32 結果，確認無雙重部署、mergeset 未被覆蓋、備份每天一份。

---

## 7.5 Phase B-1 完成記錄（2026-06-26）
- **make_update_pkg.py**（E:\ConvertFW\900\）：由 UpdRule.json 的 copy src 驅動 glob 精確挑檔組裝 `900\UPDATE`（防白名單飄移）。
  - ★安全：`900\dbf` 是 1.4 GB 客戶資料，**只挑樣板檔不整夾**。
  - 垃圾過濾：`.bak*/.asm/.obj/.old/.new/.lst/.map/.prn`、dump `.dbf.txt/.cdx.txt`；根層多濾 `.com/.mem/.exe`（`*.??M` 過度匹配 SYSTEM.MEM 會經 SYS_ALLSTATION 蓋站台設定）。
  - ★決定 A 落地於包：`set\` 排除 6 版面檔（MAINFORM/SUBFORM1/MAINWIN/SUBWIN/DBFMWIN/DBFSWIN），只走 setnew\/mergeset，免被 `set\*.*` 覆蓋。
  - 結果：包 21.3MB，含 UpdJson32.exe/UpdRule.json/DDD.ZIP/setnew(12)/STR/dbf 樣板/frm/mem/set(去6)/tmp/USRFONT.*M/*.TBL。
- **MAKEDDD.BAT**：尾端加 `make_update_pkg.py` 呼叫（純 ASCII）。
- **stk_uploader.py**：加 `update_src` 來源欄位（預設 900\UPDATE）+ `UPDATE` 群組（**預設不勾**，B-2 完成前不上傳）+ tooltip + pack 健檢核心檔；重編 32-bit exe。

### B-1 待後續處理的發現
1. **SET 分發重疊（B-3 要處理）**：UpdRule.json 有 `set\*.*` → dest `SET_ALLSTATION`（UpdJson32 也會送整包 SET 到各站）。與 distribute 的 SET 分發**重複**。包內已去 6 版面檔故不衝突 mergeset，但兩邊都送其餘 SET = 冗餘。B-3 決定 SET 由誰負責（建議：UpdJson32 的 `set\*.*` 與 distribute 擇一）。
2. **3 個 frm 樣板來源不存在**：`frm\StkForm.*`、`frm\AdjForm.*`、`frm\DepForm.*` 在 900 找不到 → copy 規則 no-op（client 也會 no-op）。需確認是已移除的舊規則還是漏放檔。

## 7.6 Phase B-2 完成記錄（2026-06-26）
- **下載 UPDATE 包**：`ftp_download_files` 本就下載 manifest 所有 directories → UPDATE 群組會自動下到 `temp_dir\UPDATE`，無需改下載。
- **★distribute 跳過 UPDATE**：`SKIP_DIRS` 由 `{'SOCKET'}` 改為 `{'SOCKET','UPDATE'}`——UPDATE 包是給 UpdJson32 的，絕不可整包派到各站（內含 UpdJson32.exe/setnew/dbf 樣板，派到站台會亂塞甚至蓋資料）。
- **★備份原子化**：`backup_stk` 先 copytree 到 `STK_<日期_時間>.tmp`，**完整成功才 `os.rename` 成正式名**（同磁碟 rename 為原子）。解決「copytree 中途失敗留半套 → `today_backup_exists` 誤判今日已備份 → 當天後續全跳過 → 只剩壞備份」。
- **today_backup_exists 排除 `.tmp`**：半套 `.tmp` 也 startswith `STK_<日期>_`，不排除會被誤算成有效備份。
- 既有「每天一份」（today_backup_exists）+「失敗不 bump 版本」邏輯維持不變。
- 單元測試：正常備份無 .tmp 殘留；殘留 .tmp 不被誤判、仍能正常備份。重編 32-bit `dist\stk_updater.exe`。
- **尚未做（B-4）**：在 Step 8(備份) 與 Step 9(distribute) 之間插入「叫起 `temp_dir\UPDATE\UpdJson32.exe`（cLocUpdate 指 temp_dir\UPDATE）」。順序=資料先程式後。

## 7.7 Phase B-3 第一件 完成記錄（2026-06-26）
- **★決定 A 在 distribute 端落地**：distribute 的 SET 來源是 **SET 群組**下載的 `temp_dir\SET`（=整包 900\SET，**含 6 版面檔**），故即使 UPDATE 包已排除 6 版面檔，distribute 仍會覆蓋各站 6 版面檔 → 破壞 mergeset。修：`distribute()` 處理 `SET` 時，主檔名屬 `SET_LAYOUT_STEMS`（mainform/subform1/mainwin/subwin/dbfmwin/dbfswin）的 .DBF/.CDX **跳過**，交 UpdJson32 mergeset。
- 單元測試：MAINFORM/SUBWIN 跳過、MAINMENU 正常複製。重編 32-bit `dist\stk_updater.exe`。
- **B-3 第二件（未做）**：UpdRule.json 停 DDD.ZIP（決定 B1），須先驗證「DDD.ZIP 內容 ⊆ FTP 的 EXE/SET/FRM 包」才安全停，否則漏檔。

## 7.8 Phase B-4 完成記錄（2026-06-26）
- **調查結論**：UpdJson32 是 **per-station**（struct/field/reindex 看 `LOC_DBF`）+ **互動式**（17 MessageBox + ShowRemind modal）+ 無結束碼。`cLocUpdate` 來源 = argv[1] > env `loc_upd` > ExeDir。`DoMergeSet` 內部自己迴圈全站（`cBase`=LOC_EXE 上層，列舉 S00X）。
- **PRG（UpdateByJson.PRG + UPDSTK.PRG）新增 `UPD_BATCH=1` 無人值守**：
  - MessageBox 批次短路 → `MsgBoxBatch` 寫 `jsonbatch.txt`、回預設、不跳框（一改全包 17 處 modal）。
  - ShowRemind 批次 → 寫 `jsonremind.txt`。
  - DoReindex 批次 → 不無限等待（睡 1 秒重試、逾 60 秒放棄），不依賴 ESC。
  - 結束碼：開頭 `ErrorLevel(2)`（未完成），正常結尾 `ErrorLevel(0)`；早退保持非 0。
  - `UPD_SKIP_MERGESET=1` → 跳 mergeset（站 2..N）。
  - no-rule fallback 批次不呼叫互動式 UpdStk()。
- **驗證 PRG**：UPD_BATCH 實跑 → preflight 的 MessageBox 改寫進 jsonbatch.txt（沒跳框）、ErrorLevel=2、process 乾淨結束無殘留視窗。重編 UpdJson32.exe（14:24）。
- **Python（stk_updater）新增 `run_updjson()`**：備份後、distribute 前，列舉站台逐站設 env（loc_upd/LOC_EXE/LOC_DBF/LOC_TMP/LOC_MEM/LOC_FRM/UPD_BATCH=1，站 2..N 加 UPD_SKIP_MERGESET=1）叫起 `temp\UPDATE\UpdJson32.exe`，判 exit code，收集 jsonremind。失敗只記錄+續跑+不 bump 版本；結尾彙整失敗站台與提醒寫 `update_result.txt` 並 log 提示。
- **驗證 Python**：單元測試站台順序/skipmerge 只站2..N/batch 全開/LOC_DBF 正確/S003 失敗收集/提醒收集 全通過。重編 32-bit stk_updater.exe。
- **時機點鐵則落地**：Step 8(備份) → **Step 8b(UpdJson32 資料)** → Step 9(distribute 換程式)。

## 7.9 Phase B-5 端到端實測（2026-06-26）— 揪出設計級錯誤並修正
- **來源校正（dist 為權威）**：使用者在 `UpdStk\build\dist` 編輯 UpdRule.json。`make_update_pkg.py` 改為從 **dist** 拉 UpdJson32.exe + UpdRule.json（原本誤指 build）。
- **build.bat 重構**：原本建 updstk32（舊版），改為建 **updjson32** → 複製到 dist → 自動跑 make_update_pkg 重生 900\UPDATE。發布鏈一鍵完成。（舊 UpdStk32 改用 rebuild.bat。）
- **900\UPDATE 自動產生**：由 `build.bat` 與 `MAKEDDD.BAT` 兩支都呼叫 `make_update_pkg.py` 自動重生。
- **★實測揪出設計級錯誤：DBF 是「共用」非 per-station**：真實 layout 只有資料站 `S001\dbf`(1.1GB)，S002..S00X 無自己的 dbf(共用 S001\dbf)；SET/MEM/FRM/TMP 才 per-station。stk.ini 亦印證 `LOC_DBF=w:\s001\dbf`、`LOC_SET=w:\s002\set`。
  - 原 B-4 per-station 迴圈對 S002 設 LOC_DBF=S002\dbf(不存在) → struct/field/reindex 開檔失敗 genCode21。
  - **修正：run_updjson 改 single-run**（找含 dbf 的資料站，跑一次）：struct/field/reindex 作用共用 dbf、copy(ALLSTATION)與 mergeset(自迴圈)涵蓋全站。符合 legacy(All.bat 實際只對 s001 跑一次)。
- **★env 被 stk.ini 覆蓋 bug**：`LoadIni` 無條件 `hb_SetEnv` → 套件 stk.ini 寫死的 `LOC_*=w:\s00x` 蓋掉 run_updjson 設的 per-station env → UpdJson32 跑去操作**真實 W: 站台**(幸 mergeset +0 冪等、copy 同內容,無損)。**修正：LoadIni 在 UPD_BATCH=1 且 LOC_ 鍵且 env 已設時 env 優先**(正常單站 env 空→仍用 stk.ini,不變)。
- **★errorsys Alert() 卡死**：unhandled RTE → 錯誤處理器 `Alert()` modal 在批次卡死。修正：批次模式跳過 Alert，直接 ErrDump+ErrorLevel(1)+QUIT(該站標記失敗,不卡)。(主 handler UsrFontErrorHandler 本就無 Alert,此為保險。)
- **實測結果（對真實 s001 複本,env-wins 確保只動沙箱絕不碰生產）**：single-run **exit 0、無 ErrDump、無 hang**；mergeset 涵蓋 S001+S002、reindex OK、always.field 在真實 DBF 不再 crash；對比空沙箱的 genCode21 → 證明共用 dbf 模型正確。
- 重編：UpdJson32.exe(15:07 經 build.bat)、stk_updater.exe(32-bit 15:07)。
- **B-5 結論：路 B 端到端整合驗證通過。**

## 8. 待確認決策（開工前）
1. 備份原子化 + 每天一份 + 排在關站後/資料更新前 —— OK？
2. 決定 A：distribute 略過 6 個版面檔 —— OK？
3. 決定 B：B1（distribute 管程式、UpdJson32 停 DDD.ZIP）或 B2？
4. 上傳端：擴充 ftp_upload.py，還是 stk_uploader？
5. 先從哪個 Phase 開始？（建議 B-1 上傳端，讓包先進得了 FTP）
```
