# FR-065 落地版 Installer — STATE（living，只描述此刻）

> **這份是 living 檔**：每棒就地 Edit，只描述「此刻」。歷史脈絡與決策軌跡見同資料夾 `FR-065-LOG.md`。

| 項目 | 值 |
|------|-----|
| 最後更新 | 2026-08-19 上午（首腦第四棒交棒：v1.15.0 進版＋Harbor 出貨＋188 升級驗證；工程全收，剩 PROD 鑰／T-4.2／收尾）|
| branch | BE/FE 皆已 merge main、與 origin 同步、tag v1.15.0 已推；LC/test 無涉 |
| 母案卡 | CM-1189（In progress） |
| 子需求卡 | CM-1243（.1）／CM-1244（.2）／CM-1245（.3）／CM-1246（.4） |
| Notion CLI | `python3 scripts/notion_case.py get/status/append/query`（見 memory reference_notion_case_cli） |
| 接手前必讀 | §0 讀序 |

## 🧭 Arc 定位（WHY）

落地版三階段戰線**第三棒**：FR-063 Nuitka 編譯 ✅ → FR-064 防竄改 ✅ → **FR-065 Installer【本案，母案 CM-1189】**。

前兩案產出了「防竄改的 BE image＋compose 定式」，但那是給我們自己人用的。本案要把它們包成**「客戶拿到一包東西，自己裝得起來、開得了通、升得了版」**的落地安裝方案。四棒拆分：

- **.1**（CM-1243）full-stack compose 地基
- **.2**（CM-1244）建庫＋install.sh＋出貨 bundle
- **.3**（CM-1245）開通精靈＋升級
- **.4**（CM-1246）手冊＋真機驗收
- **.5**（CM-1275，2026-08-18 追加）SeaweedFS 物件儲存替換（D14）

## 角色定位

本 arc 首腦 session＝**分析決策／協調**：需求釐清、拍板、拆卡、派工單、親驗抽查（不信 subagent 自報）。**實作全走 runner session**——派工只給卡號一句話，內容都在卡裡。**push 永遠 user 本人做**。

## §0 接手讀序

### 0.1 🔒 先懂需求 gate（全讀，讀完才能碰任何東西）
1. 本檔 🧭 WHY 節＋「重大定案速記」節
2. `docs/features/FR-065-2608-onprem-installer/design.md` §2（決策表）＋§5（拆分表）全讀；要細節再開 §4
3. `notion-fetch CM-1189`＋對應子需求卡

### 0.2 現況與待辦
4. 本檔 §1 當前狀態、§3 待辦地圖
5. 本檔 **§8 .1 棒實測發現**（四條實證，其中兩條推翻既有文件記載——不讀會照著錯的前提做）
6. `FR-065-LOG.md` 最後 2 個 block

### 0.3 ✅ 冷接自檢（答不出來回 0.1，不要碰任何東西）
1. 本案要解決誰的什麼問題？（客戶自裝自開通自升版）
2. 四棒各自產出什麼？
3. D10 為何是「純 bash＋Web 設定精靈」混合式？
4. D13 root 的定位是什麼？殘項是什麼？

## §1 當前狀態（2026-08-19 上午，首腦第四棒交棒）

**v1.15.0 已進版並出貨到 Harbor；FR-065 工程面全數收口，只剩 PROD 簽章鑰＋T-4.2 驗收＋全案收尾。** 所有工作卡（含實測回修）皆 Done——本棒一次收 15 卡＋CM-1302，Notion 無「修正待驗證」殘留。母卡 CM-1189／子需求卡 CM-1246（.4）／CM-1275（.5）維持 In progress，待全案收尾時收。

| 面向 | 實況 |
|---|---|
| **版本** | **v1.15.0 已發版**（`a5e32bb9`）：RN＋pyproject＋FE package.json＋spec 快照 `docs/specs/v1.15.0/` 四步齊；BE/FE 兩 repo 已 merge main、tag `v1.15.0` 已打並由 user 推上 GitLab |
| **Harbor** | **已推**（`harbor.jedicogy.com.tw:8081` 專案 `guidant-ai`）：be:1.15.0＝latest（`sha256:d558ada9…`）／fe:1.15.0（`cd9bb261…`）／init:1.15.0（`207a2f76…`）；pull 回比對 digest 複驗通過 |
| **188 實測機** | 已從 1.14.0 **升級**至 1.15.0（`install.sh --upgrade` 正規流程，非重裝）；六服務 healthy、`/api/1.0/version` 回 `a5e32bb9`；資料全保留（admin／blsadmin／兩租戶／SMTP 列／8 capability 旗標）。升級前備份在 `/srv/guidant-ai/backup/` |
| **PROD 簽章鑰** | 🔴 **唯一未做的出貨前置**——`public_keys.py` 現有 DEV/STG/POC 三把、無 PROD。`build_bundle` 會擋（內測包需 `--skip-prod-key-check`）。四步一組見 §3 |
| 手冊 | T-4.1 收（`0f056776`，11 章）；CM-1302 另補「同版號重套 --force」段 |
| seed 鐵則 | **改基線庫必跑 gen_seed_sql.sh regenerate＋commit**（scripts/init/README.md） |
| jedi | jedi-file-upload 0.0.22／jedi-project 0.0.8／jedi-oscal-v2 2.2.3；lock 內 pymupdf 歸零 |
| 待 push | 無（BE/FE 皆與 origin 同步，tag 亦已推） |
| 權限備忘 | 188 `jedi` 帳號**無 sudo 免密**——`install.sh`（安裝/升級/uninstall）必須由 user 本人執行；Claude 可做 build／打包／docker push／唯讀查驗。`docker manifest inspect` 亦因 certs.d 私鑰權限失敗，驗 Harbor 改用 pull 比 digest |

## §2 重大定案速記（不要重新討論；完整背景見 discussion.md 各 D 項）

- **D1** PG／Redis 全包進 compose（外接 DB＝客戶自己改註解，不做 `--external-db` 參數）
- **D2** FE 出 image＋`nginx.onprem.conf`；**HTTPS 憑證由 install.sh 產 per-install 自簽**（2026-08-17 補裁，T-2.5 實作；掛載路徑 T-1.2 已釘死，見 §8.4）
- **D3** 出貨形式＝tar bundle＋digest
- **D4** 建庫＝分段腳本＋one-shot init 容器
- **D5** 升級四步
- **D6** 指紋取得以精靈頁為主
- **D7** PROD 簽章鑰劃入 T-2.6（含 FR-064 SUMMARY §10 出貨遺留：進版、推 Harbor）
- **D8** Turnstile 預設停用
- **D10** 純 bash＋**Web 設定精靈**（setup token 防搶注；不做 TUI）
- **D11.1** 框架只出 CMMC 2.0（user 備初始化資料）；**D11.9** debug view 連 DEV 都刪；**D11.10** DB 名＝`guidant_ai`（新制無後綴）
- **D13** root＝**原廠系統殼**：root admin 原廠持有，精靈負責建客戶第一個業務租戶

## §3 待辦地圖

**🔴 下一個動作（三選一，等 user 發令）**：

1. **PROD 簽章鑰四步**（出貨硬前置，缺了客戶匯照必報「未知的 kid」）：
   ① LC 端（188 `/opt/license_center`）生成 PROD 鑰對——🔴 私鑰只留 LC 端，絕不進 BE repo／bundle／版控
   ② kid＋公鑰加進 `common/license/public_keys.py`（現有 DEV `2ce3bb59…`／STG `04b1e65f…`／POC `f3b562a6…` 三把）
   ③ **重 build BE image**（公鑰是編譯進去的，不是設定檔——漏這步等於白貼）
   ④ 用含 PROD 鑰的 image 重跑 `build_bundle.sh`（此時不必再帶 `--skip-prod-key-check`）
   完成後版號處置需裁示：同版重出（1.15.0 重打包，客戶端升級需 `--force`）或 bump 1.15.1。

2. **T-4.2 正式真機驗收（CM-1264，範圍已縮減，卡上有定案段）**：①全鏈一輪用 **STG 簽的照**即可（不必等 PROD 鑰）②PROD 鑰窄驗證（四步做完簽一張照匯入成功，數分鐘）③負面案只挑「竄改 image tar」＋「setup token 重放」。移出項（升級回滾／machine-id 擋下／版號區間／air-gap 登入）引用前幾棒既有驗證，不重跑。

3. **全案收尾**（走 `closing-and-handoff` skill）：SUMMARY 由本資料夾 LOG 各 block 濃縮、母卡 CM-1189＋子需求卡 CM-1246/1275 收 Done、spec 與使用手冊盤點、memory 教訓入檔。

**上版 checklist（1.15.0 若要上 STG/POC）**：本版 10 支 migration ＋ 前版遺留三支（fr058-22／fr063-1g／fr064-3）需補套；🔴 依環境鐵律等決策者當次明示才可動 STG/POC。

**全案後另排**：CM-1274（log 轉拋 Fluent Bit）／CM-1279（儲存後端遷移工具，含 per-tenant key 前綴連動）／CM-1204（手冊下載端點死碼，待裁是否活化）／CM-1265（T-4.3 Windows 評估，user 已裁本次跳過）。

**另一條戰線（user 已表示接下來優先做這個）**：**FR-066 檢測 Agent 原生安裝包**——母案 CM-1284＋16 張子卡（CM-1285~1300）已開好在 Notion，設計文件 `docs/features/FR-066-2608-agent-native-installer/`（discussion＋design 已定案、D8 含 Windows 可行性初判）。方向：agent 從 docker 三容器改成 Nuitka 單檔＋tarball＋systemd（agent 要貼宿主量測，容器化是反模式）。**與 FR-065 無依賴**，可平行推進。

> ⚠️ 本節是**建議清單，不是執行授權**。接手方讀完盤點完即停，等 user 發令。

## §4 關鍵座標

| 素材 | 路徑 |
|---|---|
| design（§2 決策表／§4 詳細設計／§5 拆分表） | `docs/features/FR-065-2608-onprem-installer/design.md` |
| 討論稿（D 項完整背景） | 同資料夾 `discussion.md` |
| DB init 輸入（CM-1207 產出） | 同資料夾 `db-init-inventory.md` |
| env 契約 | `docs/features/FR-063-2608-nuitka-packaging/deployment-env.md` |
| FR-064 出貨遺留（進版、推 Harbor→本案 T-2.6） | `docs/features/FR-064-2608-tamper-detection/handoff/` SUMMARY §10 |
| compose 定式（.1 已擴充為 full-stack） | `docker/production/docker-compose.yml` |
| compose 變數範本 | `docker/production/.env.example`（含 guidant.env 最小必填對照） |
| 落地版 nginx 設定 | FE repo `nginx.onprem.conf` |
| 落地版 FE build mode | FE repo `.env.onprem`（＋`npm run build:ONPREM`） |
| FE image build 腳本 | `scripts/build/build_fe_image.sh` |
| HTML 開啟方式 | 資料夾內 `python3 -m http.server` 後瀏覽對應 `.html` |
| Notion CLI | `scripts/notion_case.py` |

## §5 Pre-flight（必跑）

```bash
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git status -sb            # 應在 main、與 origin 同步、working tree 乾淨
git log --oneline -5      # 頂端應為 a5e32bb9（v1.15.0 進版）
git tag -l "v1.15.0"      # BE/FE 兩 repo 都應有此 tag
grep -m1 '^version' pyproject.toml            # 1.15.0
python3 scripts/notion_case.py get CM-1189    # 母案 In progress
python3 scripts/notion_case.py query --status "修正待驗證"   # 應為空（本棒已全收）
ssh jedi@192.168.50.188 'curl -sk https://localhost/api/1.0/version'   # 1.15.0 / a5e32bb9
```

## §6 環境紀律

- 🔴 開發只動 DEV；STG／POC 寫入類異動一律等決策者當次明示；唯讀操作不必請示。
- 🔴 部署機（188）程式碼一律走 git pull，禁 rsync／scp／直接編輯。
- 不切 branch、不自動 push（push 永遠 user 本人做）。

## §7 本棒新立規則（接手要遵守）

- **設計文件先概述後詳細**（已入 `docs/claude/docs-conventions.md`＋big-feature-workflow skill）
- **開卡必須夠白話**：卡名寫做什麼、內文首段白話（已入 big-feature-workflow skill）

## §8 .1 棒實測發現（仍有效的實證輸入，勿當背景雜訊略過）

> 後續棒次新增的操作性事實（sudo 權限、Harbor 驗證方式、版號單一來源）記在 §1 表格與 §3，不重複於此。

四條都是**實跑撞到、與既有文件記載不符或未記載**的事實。

### 8.1 🔴 `ENABLE_MULTI_TENANT` 漏設的症狀與文件寫的不同

- **既有文件（e2e-env 註解／deployment-env.md）說**：服務全綠但「查無資料且無錯誤訊息」
- **實測（拿掉該項重起 api）**：服務仍 healthy、**登入仍成功拿得到 JWT**，但所有走租戶脈絡的端點回 **`LICENSE_403001 此模組未在授權範圍內`**——授權判定要讀租戶資訊，讀不到就一律當無授權
- **為什麼重要**：這個訊息**指向錯誤的方向**，看起來像 license 沒裝好或過期。裝機／客服排錯時第一個要查的是這個環境變數。已補回即恢復，已驗可逆
- 影響面：客戶手冊（T-4.1）的排錯章節應收錄此對照

### 8.2 `DB_PORT` 不填不會連錯埠（既有記載已過時）

`config/config.py:37` 現行是 `os.getenv("DB_PORT", "5432")`，預設就是 5432，實測拿掉照樣連得上。e2e-env 註解記載的「BaseConfig 硬編 25432」已不成立。仍建議顯式寫出以防日後 config 改動，但**不要再把它當成「不填就會壞」的事實**。

### 8.3 🔴 空庫／新庫必須先 grant cm_app，否則 BE crash loop（exit 139）

restore 資料後 BE 初次啟動反覆 exit 139，根因是業務帳號 `cm_app` 完全沒有 schema/table/sequence 權限。**症狀極具誤導性**——exit 139 是 SIGSEGV，第一直覺會懷疑 Nuitka 產物或跨架構模擬，實際是權限。

> **給 T-2.1（CM-1252）**：這正是該卡「cm_app 權限掃描零缺口」驗收條件的實證。init 腳本除了建表建 RLS，**必須逐 schema 掃 GRANT**（USAGE on schema／SELECT,INSERT,UPDATE,DELETE on tables／USAGE,SELECT on sequences），且要含 `ALTER DEFAULT PRIVILEGES`（否則之後新建的表又漏）。

### 8.4 給 T-2.5 的憑證產生規格已由 T-1.2 約定

T-1.2 已把掛載路徑釘死為容器內 `/etc/nginx/certs/server.crt` 與 `server.key`（唯讀 bind mount，私鑰不進 image），並實測**憑證缺失時 nginx 直接 emerg 啟動失敗、不會退回 http 供站**（刻意設計：退回 http 會變成「站台活著但登不進去」的靜默故障）。T-2.5 產憑證時照這兩個路徑落檔即可，不需再協商。

---

## §9 給 fresh session 的短 prompt

```
接手 FR-065 落地版 installer（母案 CM-1189）。先讀
docs/features/FR-065-2608-onprem-installer/handoff/FR-065-STATE.md（含 §0 讀序全走）
＋ FR-065-LOG.md 最後 2 block，答完自檢 4 問、跑完 §5 pre-flight 後回報現況。
讀取與盤點完成後不做任何事——不派工、不跑測試、不改檔，等我下指令才開始作業。
```
