---
title: "落地版 Installer — 需求討論稿 (FR-065)"
brand: "Guidant AI · **FR-065** 落地版 Installer"
eyebrow: "FR-065 · On-premise Installer — 需求討論稿 · 2026-08-16（v1 · D 項大致定案）"
h1: "讓客戶自己把 Guidant AI 裝起來"
lede: "FR-063 把 BE 編成了不帶原始碼的 Nuitka image，FR-064 給了它防竄改與授權鎖定。但今天要在一台客戶主機上從零裝出一套可用的系統，仍然只有一份 `deployment-env.md` 四步手動流程——**沒有 DB 初始化、沒有 FE 交付形態、沒有 image 傳遞通路、沒有升級與回滾**，每一步都要原廠工程師在場。本案要把「出貨 bundle → 客戶主機 → 可登入的系統 → 後續換版」整條路標準化成客戶可自助執行的 installer。"
chips: [
  {text: "D 項大致定案（待轉 design.md）", kind: ok},
  {text: "D1–D10／D12 已定案 2026-08-16", kind: ok},
  {text: "D11 seed 裁決 8/10 已定案", kind: ok},
  {text: "Web 設定精靈 已定案", kind: ok},
  {text: "待決策：D11.3／D11.5 保留＋D13 待公司討論", kind: warn},
  {text: "母案 CM-1189", kind: accent},
  {text: "前作：FR-063 打包 · FR-064 防竄改", kind: accent},
  {text: "盤點依據：CM-1207 DB init 基線", kind: plain}
]
footer: "FR-065 · 落地版 Installer — 需求討論稿 · 2026-08-16 v1（D 項大致定案，餘 D11.3/D11.5 保留）· 母案 CM-1189 · 前作：FR-063（Nuitka 打包，image 1.14.0 出貨）／FR-064（防竄改偵測與授權鎖定，34 卡 Done）· 盤點來源：CM-1207 DB 初始化基線盤點（DEV 唯讀）＋ FR-063 deployment-env.md ＋ docker/production compose 定式 ＋ FE repo 實檔查證"
---

## 需求背景與目標 {#why nav="背景"}

- **母案**：CM-1189（[FR-065 Installer](https://app.notion.com/p/FR-065-Installer-3bc346da4cd081c4abf3ce74a28ba384)）
- **前作**：FR-063（Nuitka 落地版打包，2026-08-15 收口）→ FR-064（防竄改偵測與授權鎖定，2026-08-16 收口）
- **盤點輸入**：CM-1207《DB 初始化基線盤點》（本資料夾 `db-init-inventory.md`，其 D1–D10 待裁決清單整合進本稿 §D11）

FR-063／064 兩案已把「跑起來之後」的問題解完：image 不帶原始碼、產物唯讀化、防竄改鎖定、license 綁機。**還沒解的是「怎麼跑起來」**：

1. **客戶側安裝資產現況＝零**。唯一的安裝素材是 `docker/production/docker-compose.yml` ＋ `deployment-env.md` §0 的四步手動流程——那是寫給「會 docker、看得懂我們文件的自己人」的，不是給客戶的。
2. **DB 從零建庫沒有機制**。186 張表、107 條 RLS policy、5 個 schema、一票必備 seed——現況全靠 DEV 這顆活了兩年的庫，客戶主機上沒有任何工具能重建它（CM-1207 已盤點完整清單與方案草案）。
3. **FE 沒有落地交付形態**。正式部署至今是 zip dist scp 上主機手工擺放；有 Dockerfile 但只有 E2E 環境在用容器形態。
4. **image 沒有交付通路**。build_image.sh 不含 push／save；Harbor 是內部 registry，air-gapped 客戶碰不到。
5. **升級／回滾整段空白**。換版指令只有「改 `GUIDANT_VERSION` 重跑 `up -d`」一句，無 migration 套用、無備份、無回滾語意。

**目標（從 CM-1189 出發）**：讓客戶落地安裝標準化——一份出貨 bundle ＋ 一支 installer，客戶自己（或一般 IT 人員照手冊）完成安裝、開通、日後升級，**不依賴原廠工程師到場**。硬前提是 FR-064 design 明文的部署假設：**客戶環境可能完全 air-gapped，一切須離線自洽**。

## 端到端旅程 {#journey nav="旅程"}

### 2.1 首次安裝＋開通旅程

安裝與開通有一個「雞生蛋」結構：license 綁機器指紋（`sha256(/etc/machine-id)`），但取指紋的唯一現成介面 `GET /license/activation/machine-code` 需要 JWT 登入——而無照態下系統雖起得來、license 軸以外全 403（`/license/` 與 `/login` 豁免，是設計好的逃生門）。所以旅程必須在裝機當下就把指紋交到客戶手上——D10 精靈定案後主要展示面是精靈頁，`installer fingerprint` 為保底（D6）。

```{.mermaid cap="圖 1 — 首次安裝與開通端到端時序（含原廠簽照往返）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant V as 原廠（build＋LC 簽發站）
    participant C as 客戶安裝者
    participant H as 客戶主機
    participant S as Guidant AI 服務

    V->>C: 出貨 bundle（install.sh＋image tars＋bundle manifest＋SQL/seed＋手冊）
    C->>H: 解開 bundle，執行 install.sh
    H->>H: 環境檢查（docker/compose 版本、/etc/machine-id 非空、磁碟/記憶體）
    H->>H: docker load 各 image ＋ 比對 bundle manifest digest（D3）
    H->>H: 產生基礎設施密碼組（cmmgr/cm_app）＋ 寫 guidant.env
    H->>H: DB init（one-shot init 容器跑分段腳本：schema＋字典 seed，不建 admin，D4）
    H->>S: docker compose up -d（PG/Redis/BE api/BE socketio/FE，D1/D2）
    S-->>H: healthcheck 全綠
    H-->>C: 印出：「請開 http://<主機>/ 完成設定」＋ 一次性 setup token（D10）
    C->>S: 開站進 Web 設定精靈：貼 setup token → 建 admin 帳號（密碼自設）
    S-->>C: 精靈顯示機器指紋（sha256 machine-id，D6）＋ license 匯入引導
    C->>V: 抄指紋（離線通路：mail／電話）申請簽照
    V->>V: LC 簽發站簽 license（綁指紋）
    V-->>C: 回傳 .license 檔（PEM armor）
    C->>S: 在精靈匯入 license（復用 FR-062 開通頁）→ 完成安裝進登入頁
    S-->>C: 開通完成，全功能可用
```

### 2.2 升級旅程

```{.mermaid cap="圖 2 — 換版升級時序（含備份與回滾語意，D5）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant V as 原廠
    participant C as 客戶安裝者
    participant H as 客戶主機
    participant D as PostgreSQL

    V->>C: 升級 bundle（新版 image tars＋增量 migration＋manifest）
    C->>H: 執行 install.sh --upgrade
    H->>D: ① pg_dump 全量備份（落地到指定備份目錄）
    H->>H: ② docker load 新 image ＋ digest 驗證
    H->>D: ③ one-shot init 容器套增量 migration（依 schema_migrations 判斷起點，align_migrations 演算法移植）
    H->>H: ④ 改 GUIDANT_VERSION → docker compose up -d（自動 recreate）
    H-->>C: healthcheck 全綠，升級完成
    Note over C,D: 回滾＝restore 備份 ＋ GUIDANT_VERSION 回舊 tag 重跑 up -d
```

## 現況盤點 {#inventory nav="現況盤點"}

以下是撰稿前實際查證的現況（來源：FR-063 `deployment-env.md`、`docker/production/`、CM-1207 盤點、FE repo 實檔、FR-064 SUMMARY §9/§10）。

| 元件 | 現況 | 本案動作 |
|------|------|---------|
| **BE image** | `guidant-ai-be:1.14.0`：ubuntu:22.04 基底（glibc 對齊 build 機）、中文字型／LibreOffice／WeasyPrint 原生庫已進 image、產物 root:root ＋ entrypoint setpriv 降權 uid 1000、不帶 python/原始碼 | 直接沿用，不動 |
| **BE build 管線** | `scripts/build/`：build_all.sh → smoke → build_image.sh；integrity manifest 簽章在 build_release.sh Step 4 尾端送 LC 簽 | 擴充：FE image build（D2）＋ bundle 打包段（D3） |
| **compose 定式** | `docker/production/docker-compose.yml`：僅 guidant-api(8000)＋guidant-socketio(8002) 兩服務；read_only＋tmpfs；7 個掛載（static/log/home/tmp/pki/machine-id/agent 憑證）；**無 DB/Redis/MinIO/FE**；檔頭明文警語：獨立 compose project、`up -d` 必帶 service 名以免 recreate 同 project 的 DB | 擴充為完整 stack（D1/D2），並處理 recreate 風險 |
| **FE 交付** | 有 Dockerfile（node build → nginx:alpine，`ARG NGINX_CONF` 可換 conf）；`build:PRD` 走相對路徑（`VITE_API_URL=/api/1.0`、WS 空字串→同源）故**不需 per-customer rebuild**；`nginx.e2e.conf` 是現成同源反代範本（/api/1.0→be:8000、/socket.io→be-socketio:8002 含 WS upgrade），但檔頭明標「限 E2E」；正式部署現況＝zip dist scp 手工擺放；build-time 綁死的只剩 `VITE_TURNSTILE_SITE_KEY` 與 `VITE_DEV_MODE`（PRD 現值 `true`，落地要檢討） | 新增 production 容器形態（D2） |
| **DB 初始化** | 零。DEV 實況：5 schema／186 表／107 條 RLS policy／156 sequence；cmmgr/cm_app 帳號建立紀錄不在 repo；CM-1207 已產出完整基線盤點＋方案 A/B/C 草案（傾向 B 分段腳本＋C one-shot init 容器載體） | 本案核心交付（D4） |
| **migration／升級** | `scripts/sql/manifest.tsv` active 138 筆（127 筆 envs=`*`、11 筆環境限定不屬基線）；`align_migrations.sh` 的 diff＋`--apply` 演算法現成，但 target 寫死內部 IP、密碼讀 repo `.env`——客戶端要重做連線層並把 SQL＋manifest 打進交付物；換版現況只有「改 GUIDANT_VERSION 重跑 up -d」，無備份/回滾/相容性檢查 | 升級流程（D5） |
| **license／指紋** | 指紋＝`sha256(/etc/machine-id)`（`common/license/machine_fingerprint.py`）；取指紋唯一介面需 JWT 登入；無照態服務照起但 license 軸外全 403（`/license/` 與 `/login` 豁免）；license 檔 PEM armor（`.license`）貼 FE 頁匯入，BE 不落檔；unlock.token 落 `/opt/guidant/pki` | 開通旅程（D6）；宿主前提 `/etc/machine-id` 非空（CM-1242 實測） |
| **簽章公鑰** | `common/license/public_keys.py` 硬編進 image，現有 DEV/STG/POC 三把，**無正式 PROD 鑰**——FR-064 遺留待辦，出貨前必須新增鑰＋重 build image | D7 |
| **外網依賴** | Turnstile captcha 內網會因連不到 Cloudflare 登入失敗（deployment-env.md §3 標「需評估」未定案）；LC 回報是 best-effort 已離線自洽 | D8 |
| **image 交付通路** | 完全沒寫。build_image.sh 無 push/save；Harbor 是內部 registry；docs 內 docker save 先例只有 FR-058 agent image | D3 |
| **FR-064 出貨遺留** | 未進版（現標 1.14.0）；image 未推 Harbor（本機 tag 已打）；K8s liveness 下鎖定殼會 CrashLoop（compose 不受影響） | D12 歸屬裁決 |
| **客戶側安裝資產** | 零（唯一素材＝compose ＋ deployment-env.md §0 四步手動流程） | 本案主體 |

## e2e-env 借鑑分析 {#e2e-borrow nav="e2e 借鑑"}

test repo `compliance-manager-test/e2e-env/` 已有一套跑得起來的 8-service 全套 compose（postgres／redis／mailpit／minio／minio-init／be／be-socketio／fe）——它每天在驗證「這組服務能在一個 compose 內良好共處」。user 指示：**分析借用、不整抄**。逐項拆成可移植與不可抄兩表：

### 可移植（借拓撲與機制）

| # | 項目 | 為什麼要借 |
|---|------|-----------|
| 1 | **PG service 形態**：postgres:16＋顯式 `POSTGRES_INITDB_ARGS`（`--encoding=UTF8 --locale=en_US.utf8`）＋named volume＋`pg_isready` healthcheck | 不顯式指定 encoding/locale，客戶主機的預設 locale 會滲進 initdb，跨機不可重現 |
| 2 | **Redis 必須 ACL user 而非 requirepass** | BE config 組的是 `redis://<user>:<pass>@host` 形式，requirepass 只認 default user 會連不上；`--user "$REDIS_USER" on ... "+@all"` 段必抄＋`appendonly no`＋healthcheck 用 ACL user 實測 ping |
| 3 | **同源 nginx 反代拓撲**：`/api/1.0`→be:8000、`/socket.io`→be-socketio:8002（**必須分開兩 service**）、SPA `try_files` | 與 `build:PRD` 相對路徑產物正好互補，單一入口不需 per-customer rebuild |
| 4 | **nginx resolver 動態解析**：`resolver 127.0.0.11 valid=10s`＋變數 upstream | 不寫的話 BE restart 換 IP 後 nginx 快取舊 IP、全站 502——落地客戶自己重啟服務是常態，必抄 |
| 5 | **反代調校**：`client_max_body_size 0`、API `read_timeout 600s`＋buffering off、socket.io 3600s＋WS upgrade | 大檔上傳與長工時匯出（SSP docx/PDF）會踩預設 timeout |
| 6 | **`depends_on condition: service_healthy` 啟動順序** | DB/Redis ready 才起 BE、BE healthy 才起 FE——installer「up 完即可用」的前提 |
| 7 | **一 image 兩服務（`RUN_MODE`）＋YAML anchor** | 產品 compose 已有同形態，e2e-env 驗證了與 DB/FE 同 compose 時也成立 |
| 8 | **MinIO 選配段形態**：minio＋mc init job（`--ignore-existing` 冪等） | D1 已定 MinIO 選配，bucket init 的冪等 job 形態直接可用 |
| 9 | **零硬編憑證＋`.env.example` 規範**（只列 key 與用途） | 與本案「密碼由 installer 產生、憑證不入版控」一致 |
| 10 | **BE 必要 env 踩坑清單**：`ENABLE_MULTI_TENANT` 必須顯式 `"true"`（缺了 RLS 靜默擋、查無資料無錯誤）；`DRIVE_TOKEN_ENCRYPTION_KEY` 啟動即檢；`DETECTION_TOOL_ENCRYPTION_KEY` 是另一支（錯誤訊息會誤導）；`DB_PORT` 顯式 5432（BaseConfig 硬編 25432） | 每一條都是 e2e-env 實踩過的啟動失敗／靜默故障，.env 範本照單全收 |
| 11 | **容器內 vs 對外 port 分離**（BE 連 `service:5432`，對外映射另計） | 客戶主機 port 衝突時只改對外映射，服務互連不動 |
| 12 | **HTTPS 是功能必要不是加分**：FE aiChatStore 用 `crypto.randomUUID()`，非 secure context 下為 undefined 且錯誤被吞→登入卡死零訊息；443＋憑證掛載（bind ro、私鑰不進 image）是必要設計 | 不是資安加分項——不上 HTTPS（或 localhost 之外裸 HTTP）產品直接壞 |

### 不可抄（測試專用）

| # | 項目 | 為什麼不抄 |
|---|------|-----------|
| 1 | golden dump→TEMPLATE clone 整套 | 產品是空庫 init（CM-1207），不是 restore 測試基準 |
| 2 | `pg_dumpall --roles-only` 搬 role（連 SCRAM 雜湊） | 產品 init 新建 role＋安裝時生成密碼，不搬既有雜湊 |
| 3 | sanitize-golden 全套 | 空庫沒有「洗掉開發資料」的問題 |
| 4 | mailpit 假 SMTP | 產品接客戶真 SMTP |
| 5 | BE E2E image（帶原始碼跑 root dev server） | 產品用 `docker/production/Dockerfile` Nuitka 產物（不帶原始碼、降權） |
| 6 | `ENV=DEVELOP_PREMISE` | 產品走正式 config |
| 7 | fe-image 兩層疊 image | 產品單層乾淨 build |
| 8 | 測試帳號／auth-state／排他鎖／污染偵測／奇數 port／`e2e-postgres` 寫死 service 名 | 全是測試編排產物；產品 service 名用中性名（如 `guidant-db`） |

**結論**：e2e-env 驗證了「這組服務能在一個 compose 內良好共處」——借**拓撲與機制**三條線（服務組成／互連／啟動順序），不借**資料來源、image 血統、測試編排**三條線。

## 決策清單 D1–D12 {#decisions nav="決策清單"}

> 2026-08-16 批次拍板：D1–D10／D12 全數定案，D11 十項落裁決（8 定案、2 保留——D11.3 與 PM 討論、D11.5 .2 棒前定）。

::: {.callout .decided}
**✅ D1 — 依賴服務形狀（已定案 2026-08-16）：PG／Redis compose 全包為預設**

現行 compose 定式只有 BE 兩服務，DB/Redis/MinIO 都假設外部已存在。落地客戶（尤其 air-gap 單機）多半沒有現成 PG 16／Redis，要求自備等於把最難的一段推回給客戶——故拍板**全包為預設**：

- **PG／Redis image 隨 bundle 出貨，compose 全包**（air-gap 單機客戶是主場景）。
- **客戶已有自備服務時，由客戶自行修改 docker-compose 內容切外部**——compose 內寫註解引導：改 `DB_HOST`／`REDIS_HOST` 環境變數、移除對應 service 段。installer **不做 `--external-db` 類參數**；參數化外接模式列為未來可選加分項。
- **MinIO 選配不預設**（storage 預設 local）維持。
- **recreate 風險防護照原建議進實作設計**：DB 與應用同 compose project 時 `up -d` 不帶 service 名會 recreate 所有容器（含 DB，CLAUDE.md 有案）——傾向 DB/Redis 獨立成另一個 compose project（或至少檔內大字警語＋installer 包裝所有 up 指令、永遠帶 service 名），不讓客戶裸打 `up -d`。
:::

::: {.callout .decided}
**✅ D2 — FE production image 與反向代理（已定案 2026-08-16）：`nginx.onprem.conf`＋FE production image＋單一入口 port**

**定案**：照原建議採納——新增 `nginx.onprem.conf`＋FE production image，進 build 管線、tag 與 BE 同版號；單一入口 port（80/443）由 FE nginx 統一對外。`VITE_DEV_MODE` 與 `VITE_TURNSTILE_SITE_KEY` 在實作棒查清語意後定值。

FE 已具備容器化的全部零件：Dockerfile 支援 `ARG NGINX_CONF` 換 conf、`build:PRD` 產物走相對路徑（同源即可用，不需 per-customer rebuild）、`nginx.e2e.conf` 是現成的同源反代範本（含 /socket.io WS upgrade 與長工時 timeout）。缺的只是「產品版」的定案：一支正式 nginx conf（以 e2e 版為底、upstream 指向 compose service 名）＋ FE image 進 build 管線與 BE 版號對齊。單一入口 port（80/443）由 FE nginx 統一對外，BE 8000/8002 收進內網。

連帶要檢討兩個 build-time 值：`VITE_DEV_MODE`（PRD 現值 `true`——語意與影響面待實作棒查清，落地版直覺上不該開）與 `VITE_TURNSTILE_SITE_KEY`（現值是 Cloudflare 測試 key，與 D8 連動）。
:::

::: {.callout .decided}
**✅ D3 — image 交付通路與完整性驗證（已定案 2026-08-16）：docker save tar bundle 為主＋manifest digest 比對必做**

**定案**：照原建議——tar bundle 為主（air-gap 必然）；bundle manifest digest 比對進 installer 必做；Harbor pull 文件化為有網客戶的替代路徑；cosign 評估後另議。

air-gap 客戶拉不到 Harbor，通路只剩實體傳遞。選項：(a) `docker save` tar bundle——BE/FE/PG/Redis 全部 image 打進一份出貨包；(b) Harbor pull（僅限有網路且信任外連的客戶）。

FR-064 D7 已把「image 層驗證」劃入本案：bundle 內附 manifest 列各 tar 的 sha256 digest，installer `docker load` 後比對，確保客戶手上的 image 與原廠出貨一致（傳遞途中未被替換）。cosign 之類的簽章方案列為評估項，不在 v1 硬性範圍。
:::

::: {.callout .decided}
**✅ D4 — DB init 載體（已定案 2026-08-16）：分段 SQL 腳本（B）＋one-shot init 容器（C 載體），照 CM-1207 §5.4**

**定案**：照原建議——B＋C 載體。DB init＝分段 SQL 腳本（基線真相取 DEV pg_dump 整理，參數化注入密碼／命名）＋one-shot init 容器為執行載體（跑完即棄，最高權限憑證只存在建庫當下）。init 容器與升級（D5）的 migration 容器共用同一形態。

CM-1207 §5 已完整比較三案：A（單一 3 萬行 init.sql，不建議）、B（分段腳本組 `scripts/init/00-cluster … 99-stamp`＋init.sh）、C（entrypoint 自動 init，服務容器得持有 cmmgr 憑證、違反最小權限）。§5.4 取捨結論：**B 為主、C 的 one-shot init-job 容器作為執行載體**——installer 產參數（兩組 DB 密碼、admin 初始密碼）→ 起 init 容器跑 init.sh → 成功後才起服務容器（服務只拿 cm_app 憑證）。基線真相取 DEV pg_dump（非 224 支 migration 重放）；權限不逐表 GRANT 而是複製 default privileges＋event trigger 機制；收尾 stamp `schema_migrations` baseline marker 讓後續增量 migration 無縫接軌。
:::

::: {.callout .decided}
**✅ D5 — 升級流程與回滾語意（已定案 2026-08-16）：四步包成 `install.sh --upgrade`＋回滾＝災難逃生**

**定案**：照原建議——升級定式化為四步、包成 `install.sh --upgrade`；回滾＝restore 備份＋回舊 tag（明文：災難逃生非常規操作）；相容性檢查 v1 最簡版（bundle manifest 標適用起始版號區間，不符即拒跑）。

四步：① `pg_dump` 全量備份 → ② load 新 image（含 digest 驗證）→ ③ init 容器套增量 migration（`align_migrations.sh` 的 diff＋apply 演算法移植到客戶端形態：連線參數外部注入、SQL＋manifest.tsv 隨 bundle 出貨、依 `schema_migrations` 判斷起點）→ ④ `up -d` recreate。回滾語意＝restore 備份＋`GUIDANT_VERSION` 回舊 tag（明文告知：回滾會丟升級後產生的資料，屬災難逃生不是常規操作）。
:::

::: {.callout .decided}
**✅ D6 — 首次開通的「雞生蛋」（已定案 2026-08-16）：指紋主展示面＝Web 設定精靈頁＋`installer fingerprint` 保底**

**定案**：指紋主要展示面＝Web 設定精靈頁（顯示＋複製鈕）；`installer fingerprint` 子命令為保底查詢工具（裝機尾端自動印＋可隨時重跑，服務起不來時可用），演算法與 `machine_fingerprint.py` 一字不差；實作棒需加「兩邊算出同值」的驗證步驟。

取機器指紋的唯一現成介面需 JWT 登入，但簽照又需要指紋——雖然無照態 `/login` 有豁免（登入後可打 machine-code API），流程仍繞。installer 在裝機尾端直接算指紋（與 BE 同演算法：`sha256(/etc/machine-id)`，宿主上一行 shell 即可），客戶抄指紋走離線通路給原廠簽照，拿到 `.license` 後進系統貼照開通。
:::

::: {.callout .decided}
**✅ D7 — 正式簽章鑰（PROD kid）（已定案 2026-08-16）：劃入本案 .2 棒**

**定案**：PROD 簽章鑰劃入本案 scope（.2 棒，隨 bundle 打包段）——LC 端生成 PROD 鑰對 → `public_keys.py` 新增 PROD kid → 重 build image；出貨 bundle 一律以含 PROD 鑰的 image 產出。

`public_keys.py` 現有 DEV/STG/POC 三把，無 PROD 鑰。這是出貨的硬前置：客戶拿到的 image 必須能驗「正式簽發鑰」簽出的 license／unlock token／integrity manifest，否則出貨即「未知的 kid」（[[feedback_multi_env_signing_key_pairs_with_public_key_commit]] 四步一組的教訓適用）。
:::

::: {.callout .decided}
**✅ D8 — 外網依賴停用（已定案 2026-08-16）：落地版預設停用 Turnstile**

**定案**：落地版預設停用 Turnstile（BE/FE 一對開關，實作棒查清現況後補齊缺的開關）；bundle 手冊附「本產品可能外連清單與停用法」。

Turnstile captcha 在內網連不到 Cloudflare 會直接讓登入失敗，deployment-env.md 標「需評估」至今未定案。BE 端 `TURNSTILE_SECRET_KEY`、FE 端 `VITE_TURNSTILE_SITE_KEY` 各自的「停用開關」現況（不填是否即停用？有無顯式 flag？）待實作棒查清。其他外網呼叫盤點：LC 在線回報已是 best-effort 離線自洽；AI API／Google Drive 屬功能模組、客戶不啟用即不外連——仍應在實作棒做一次完整外連盤點收進手冊。
:::

::: {.callout .decided}
**✅ D9 — Windows 支援（已定案 2026-08-16）：僅出評估文件，不實作不承諾**

**定案**：本案僅出評估文件＋前提清單（WSL2 machine-id 行為、Docker Desktop licensing、效能界線），不實作不承諾支援；有真實客戶需求時另開案。

母案卡內文傾向：不做原生 Windows 版，引導客戶裝 Docker Desktop／WSL2 跑同一套 Linux image。技術上可行但有未驗證變數：WSL2 的 `/etc/machine-id` 穩定性（指紋錨點）、volume 效能、Docker Desktop 授權條款（企業要付費）。
:::

::: {.callout .decided}
**✅ D10 — installer 形式（已定案 2026-08-16，兩次拍板）：純 bash（不做 TUI）＋首次啟動 Web 設定精靈**

user 兩次拍板：第一次定「純 bash install.sh，不做 TUI」；同日續拍**採混合式**——bash 管基礎設施段＋「首次啟動 Web 設定精靈」管應用設定段（業界主流模式：Portainer／GitLab／Nextcloud／Jenkins 式）。

分工定式：

1. **`install.sh`（bash 純文字）**：環境檢查 → load image＋digest 比對 → DB init（one-shot init 容器：schema＋字典 seed，**不建 admin**）→ 起服務 → 尾端印「請開 `http://<主機>/` 完成設定」＋**一次性 setup token**（防搶注，Jenkins 式——避免精靈開放期間被非安裝者搶先建帳號）
2. **Web 設定精靈**（首次開站偵測「無 admin」自動進入）：貼 setup token → 建管理員帳號（密碼客戶自設，不經終端留痕）→ 顯示機器指紋＋license 匯入引導（復用 FR-062 開通頁）→ 完成安裝、進正常登入頁
3. SMTP／storage 等裝完後台設定頁慢慢設（不進精靈必填）

代價明文：精靈是新的 FE＋BE 開發（未初始化態偵測、setup token 驗證、建 admin 的特殊通道——繞過既有權限體系的一次性 API，用後即封），約多一棒工作量；user 拍板值得（體驗優先）。TUI 維持不做；systemd 維持不另立 unit（installer 確保 `systemctl enable docker` 即可）。

被排除方案：純 shell 印生成密碼＋首登強制改密（機制現成但體驗差一階，列 fallback）。
:::

### D11 — seed 出貨範圍裁決（承接 CM-1207 §3.4） {#d11}

CM-1207 盤點把「拿不準的 seed／schema 項目」列成十項待裁決，原編號 D1–D10 為避免與本稿撞號改編為 **D11.1–D11.10**。字典類必備 seed（operations／system_menus／ui_routes／capabilities／root tenant+org／Administrator 角色／RUNTIME_CONFIG 等，CM-1207 §3.1）已明確屬出貨集，不在此重列；十項已於 2026-08-16 逐項落 user 裁決——**8 項定案、2 項保留待議（D11.3 與 PM 討論／D11.5 .2 棒開工前定）**：

::: {.callout .decided}
**✅ D11.1 — CMMC 框架 catalog 整族（已定案 2026-08-16）：出貨框架只有 CMMC 2.0，走應用層匯入**

**定案**：出貨框架清單只有 **CMMC 2.0**；初始化資料由 user（決策者）準備。出貨形式採「應用層匯入」——出廠帶 OSCAL JSON 檔、服務起來後走既有匯入流程灌入（與 D11.4 同一模式），DB init 不 seed catalog 資料列。

背景：DEV 的 79 個 catalog 混雜「內建 CMMC」與「開發測試匯入的」，schema 上分不出來——正因如此不走 seed dump 直灌。
:::

::: {.callout .decided}
**✅ D11.2 — 內建 workflow_templates（已定案 2026-08-16，範圍待核對）：只帶「預設內建」最小集**

**定案**：只帶「預設內建」的最小集——DEV 的 191 筆（provider=Billows-Official）屬混雜舊資料，**非全部出貨**；snapshot 類專案運行產物與 tenant 1 的 59 筆不帶。**實作棒需與決策者核對確切出貨清單**後落地。
:::

::: {.callout .pending}
**⏸ D11.3 — 樣板 SSP／MF defaults 家族（保留——與 PM 討論後另定）**

**裁示（2026-08-16）**：**先留空**——出貨基線不帶樣板 SSP／MF defaults，與 PM 討論後另定範圍。

背景：新客戶初始該有哪些「內建模組框架＋樣板 SSP」？DEV 現況分不出產品內建 vs 開發試作。
:::

::: {.callout .decided}
**✅ D11.4 — 檢測 Profile 庫（已定案 2026-08-16）：隨 bundle 帶檔＋既有匯入機制灌入**

**定案**：採「隨 bundle 帶檔案＋服務起來後走既有匯入機制灌入」——DB init **不 seed 資料列**，與 D11.1 CMMC 初始化同一模式（單一維護面；避免 seed 列與 storage 檔案脫鉤的壞連結）。

背景：資料列是產品資產（dev-sec 基準＋TWGCB 8 支），但 **profile 版本的檔案實體在 storage（MinIO/local）**——DB seed 灌了列、檔案不在就是壞連結，正是選匯入路線的原因。
:::

::: {.callout .pending}
**⏸ D11.5 — `public.labels` 35 筆（保留——.2 棒開工前定）**

**裁示（2026-08-16）**：先不裁，**.2 棒開工前定**。傾向：預設只帶前段系統字典（系統回饋／系統操作／選單分類），後段 textbook-*／resource-* 不帶。
:::

::: {.callout .decided}
**✅ D11.6 — `ISSUE_INTEGRATE_CONFIG`（已定案 2026-08-16）：出廠帶 disabled 空殼**

**定案**：功能對落地客戶開放——出廠帶 disabled 空殼，客戶後台自行啟用接自己的 GitHub／GitLab。
:::

::: {.callout .decided}
**✅ D11.7 — `public.alembic_version`（已定案 2026-08-16）：暫留**

**定案**：此表係舊版 flask-migrate 遺留（user 證實，來源已明）——**暫留**，基線帶此表、不清除。
:::

::: {.callout .decided}
**✅ D11.8 — root tenant／org／admin 出廠命名（已定案 2026-08-16）：由客戶命名**

**定案**：root tenant 顯示名**由客戶命名**——在 Web 設定精靈輸入（D10 流程內）；root id=1 是硬約定不可變，這裡只裁顯示名。DEV 值 `Guidant.AI`／`Guidant.AI Manage`／`admin` 不沿用為出廠固定值。

連動註：若 D13 採納系統殼方案，客戶命名對象改為第一個業務租戶、root 顯示名固定（如 System）。
:::

::: {.callout .decided}
**✅ D11.9 — debug view 與 `cm_app_group`（已定案 2026-08-16，同日追加裁示）：debug view 確認零引用後連 DEV 一併刪除、空群組保留**

**定案（追加裁示後）**：debug view 不只「不出貨」——`debug_org_units`／`v_tenant_parent_debug` 兩支 **確認沒程式在用後，連 DEV 也一併刪掉**。實作棒動作：grep 全 codebase 確認零引用 → 開 migration drop 兩支 view（DEV 先套，走既有 sql-migration 流程與 DROP 前安全查核鐵則）→ 出貨基線自然不含。`cm_app_group` 空群組**保留**（利未來多 app 帳號，照 CM-1207 傾向）。
:::

::: {.callout .decided}
**✅ D11.10 — 落地版 DB 名（已定案 2026-08-16）：統一 `guidant_ai`**

**定案**：落地版 DB 名統一 **`guidant_ai`**——採 2026-08-08 DB 命名新制（`<系統>_{dev|stg}`、prod 不帶後綴，落地新客戶屬新建置適用新制）；installer 的 `INIT_DB_NAME` 預設值定此。
:::

::: {.callout .decided}
**✅ D12 — FR-064 出貨遺留歸屬（已定案 2026-08-16）：①②劃入本案，③不實作但設計預留**

FR-064 收口時明文留下三件：① 進版（現標 1.14.0，未走 version-bump）；② image 推 Harbor（本機 tag 已打）；③ K8s liveness 下鎖定殼會 CrashLoop（compose 落地不受影響，上 K8s 才需把鎖定態排除在 liveness 外）。

**定案**：①進版（version-bump）與 ②image 推 Harbor **劃入本案出貨動作**（出貨 bundle 必然要定版，推 Harbor 是原廠內部歸檔）。③ **不在本案實作，但設計預留**——user 明示「要預留 K8s」：design.md 需把「鎖定態排除在 liveness 之外的設計要點」與 cluster 化前提備忘一起記為**正式預留節**（作為未來 K8s 案的起點），compose 服務契約保持可翻譯性（服務名／port／env 介面清楚）。見文末〈cluster 化前提備忘〉同步標註。
:::

::: {.callout .pending}
**🏢 D13 — root tenant 定位：系統管理殼 vs 可做業務（待公司討論）**

議題：落地版 root tenant（id=1）要不要給客戶做業務，還是只當 super admin 系統層？

市面對照（皆為「系統層＝管理殼、業務在下層」同構）：

- **Keycloak**：master realm 只管其他 realm，官方明文建議業務絕不放 master realm
- **GitLab self-managed**：Admin Area 獨立系統介面，業務在 Group/Project 層
- **Atlassian DC**：系統管理員與空間/專案管理分層
- **Payload CMS**：無 root tenant 概念，super admin 是「無租戶歸屬的旗標」跨租戶管理（架構與我們 B-2 相反——我們系統資源必掛 root tenant 避免 NULL 孤兒，不可學它的形狀，但「系統身分不做業務」的體驗邊界值得抄）

我方架構現況：B-2 模型下 root tenant 本來就是系統資源掛載點（平台 capability／系統設定／Administrator 角色），DEV 大家拿 root admin 直接做業務是開發便利非設計意圖。

建議方案：**root tenant＝super admin 的家，只做系統管理（license 匯入／SMTP／storage／建租戶／系統日誌）；業務一律走第一層租戶**。精靈流程配合改為：建 super admin（root 層）→ 引導建第一個業務租戶（D11.8 客戶命名的就是它，root 顯示名可固定如 System）→ 建租戶管理員 → 日常用租戶管理員登入。

強制程度兩檔（待公司裁）：

- **(a) 軟邊界（v1 便宜）**：精靈強制引導＋手冊明文，root 下技術上仍可按到業務功能
- **(b) 硬邊界（多工）**：root 層介面實際收斂藏掉業務功能——現況距離多遠需實作棒盤點

[**首腦建議**：v1 軟邊界＋精靈強制引導；硬邊界視盤點與 PM 意見排 v2。]{.rec}

連動註記：此裁決影響 .3 棒精靈流程與 D11.8 的落法（客戶命名對象從 root tenant 改為第一個業務租戶）。
:::

## 階段拆分草案 {#phases nav="階段拆分"}

粗略切棒，轉 design.md 時細拆。**階段重切（user 2026-08-16 指示）**：先切一個「docker compose 起整個服務」可獨立驗收的**地基棒**當 .1；原 .1 的 bundle 打包與 PROD 簽章鑰移到 .2。

| 階段 | 內容 | 驗收 |
|------|------|------|
| **FR-065.1 full-stack compose（地基棒）** | 產品 compose 全 stack：PG＋Redis＋BE api/socketio＋FE production image（nginx 同源反代單一入口）＋MinIO 選配段（D1/D2）；FE image build 定案（`nginx.onprem.conf`＋`build:PRD`）；`.env` 範本；自備 DB/Redis 的註解引導。**DB 先用暫行手段（如手動 restore）驗通拓撲，正式空庫 init 屬 .2** | 乾淨機器上 `docker compose up -d` 全組 healthy、FE 反代登入通 |
| **FR-065.2 DB init＋installer 殼** | 分段 init 腳本＋one-shot init 容器（CM-1207 方案，D4；schema＋字典 seed，**不建 admin**）；install.sh（環境檢查／load image／產基礎設施密碼／起 stack／尾端印 setup token 而非 admin 密碼，D10）；bundle 打包（docker save＋digest manifest，D3）；PROD 簽章鑰（D7 定案：LC 生 PROD 鑰對＋public_keys.py 新增 kid＋重 build image）；seed 依 D11 裁決落地（D11.2 清單開工前與決策者核對、D11.5 labels 開工前定；CMMC／檢測 Profile 走應用層匯入不 seed） | 空機一鍵裝到「服務全綠、待精靈設定」 |
| **FR-065.3 開通精靈與升級** | Web 設定精靈（FE 精靈頁＋BE setup token／建 admin 通道，D10）；`installer fingerprint` 子命令＋開通流程整合（D6）；`--upgrade` 四步＋回滾（D5）；Turnstile 停用落地（D8） | 空機裝完 → 瀏覽器精靈建帳號 → 匯照開通 → 升級 → 回滾端到端 |
| **FR-065.4 文件與驗收** | 客戶安裝手冊；乾淨主機端到端真機驗收；Windows 評估文件（D9） | 全鏈真機走一遍 |

依賴關係相應調整：.1 依 D1/D2（均已定案）即可開工；.2 依 D3/D4/D7/D10/D11（均已定案或裁定，餘 D11.2 清單核對與 D11.5 於開工前補定）＋.1 完成；.3（含 Web 設定精靈，依 D10 定案）依 .2；.4 收全鏈。D12 的進版與推 Harbor 屬出貨動作，掛在 .2 bundle 打包段前後執行。

## 附：宿主前提清單草案 {#host-prereq nav="宿主前提"}

installer 環境檢查段的初稿，門檻數值待實作棒定案：

- **OS**：Linux x86_64（glibc ≥ 2.35——BE binary 在 Ubuntu 22.04 編出，基底鏡像已自帶 glibc，宿主只需能跑 docker；此條實際約束的是 docker 本身）
- **Docker**：Docker Engine（最低版本待定，需支援 compose plugin v2）＋ `docker compose` plugin；`systemctl enable docker`（開機自起前提）
- **`/etc/machine-id` 非空**：`test -s /etc/machine-id` 必過（CM-1242 實測：缺檔時 docker 會建成目錄、指紋退回 MAC 且每次重啟漂移；systemd 系開機安裝即有，極精簡系統用 `systemd-machine-id-setup` 補）。⚠️ machine-id 一旦重生成，既有綁機憑證全部失效——只在「本來就沒有」時補
- **磁碟**：門檻待定（image tars＋DB volume＋上傳檔＋log＋pg_dump 備份空間；bundle 本身估數 GB 級）
- **記憶體／CPU**：門檻待定（PG＋Redis＋BE gunicorn 4 workers＋socketio＋nginx 全包時的最低規格，實作棒實測定）
- **時區**：主機 TZ 與部署一致（容器 image 內建 Asia/Taipei，跨時區覆寫但不可各自為政——deployment-env.md §4 時區段）
- **網路**：無外網前提可運作（air-gap 自洽）；需開放的對內 port：單一入口 80/443（D2 已定案）

## 附：cluster 化前提備忘 {#cluster nav="cluster 備忘"}

user 問過未來 cluster（K8s／多節點）支援。compose→K8s 的翻譯本身是機械工，**真門檻在應用層假設**，先記錄不動：

1. **機器指紋綁單機**（`sha256(/etc/machine-id)`）——多節點下每台指紋不同，license 模型要改（改綁 cluster identity 或浮動授權）。
2. **鎖定殼 vs liveness**：FR-064 已知，K8s liveness 下鎖定態會 CrashLoop，需把鎖定態排除在 liveness 判定外。
3. **tamper 偵測的 FS 落點**（`/opt/guidant/pki`、unlock.token）——多副本共享或各自持有需重新設計。
4. **storage 需切 MinIO/S3**：local storage 綁單機磁碟，多副本必然外部化。
5. **socketio 多副本需 sticky session**（或改 message-queue adapter）。

**D12 裁示同步（2026-08-16）**：user 明示「要預留 K8s」——本案**不做 cluster 實作**，但 design.md 需把本清單＋「鎖定態排除在 liveness 之外的設計要點」（上列第 2 項）記為**正式預留節**，作為未來 K8s 案的起點；compose 寫成乾淨的服務契約（服務名／port／env 介面清楚）保持可翻譯性。未來有真實需求另開 FR。
