# FR-052 — 雲端整合 Tab 顯示 Gate（Cloud Integration Tab Gate）

> **狀態**：設計定稿待 user review（Phase 2 產出）
> **提出**：2026-07-21
> **來源 Notion case**：`3a4346da-4cd0-80fd`「雲端整合的Tab要有跟**雲端空間整合設定**做連接的才看得到」（功能=專案規劃）
> **arc 關聯**：relates to FR-016（Google Drive 整合）、FR-042（框架儲存脫鉤 / storage scope）

---

## 1. 需求白話（Phase 0 已與 user 對齊）

專案規劃頁的「**雲端整合**」Tab（TabView tab index 5），只有在該**租戶（tenant）的雲端空間整合狀態為 CONNECTED** 時才顯示；未連接就**直接隱藏**該 tab（不渲染）。

### 1.1 設計決策（user 拍板）

| # | 決策 | 選定 |
|---|------|------|
| 1 | Gate 門檻 | **只認 `CONNECTED`**（EXPIRED / REVOKED / DISCONNECTED 皆隱藏）|
| 2 | 未連接時 tab | **直接隱藏**（不渲染，非 disabled）|
| 3 | FE 信號來源 | **新增輕量 status endpoint**（user 選）—— 但見 §2.2 ⚠️ 已有現成非 admin status endpoint，實作前請 user 二選一 |

---

## 2. 現況盤點（實地 grep）

### 2.1 落點

- **FE**：`compliance-manager-fe/src/views/project/ProjectPlanningView.vue:2110`
  ```html
  <TabPanel :header="t('lang.project_planning.tab_cloud_integrations') ?? '雲端整合'">
      <ProjectCloudIntegrationsPanel :project-id="projectId" :can-edit="isProjectManager" />
  </TabPanel>
  ```
  TabView tab index：`0 控制項實作 / 1 SSP / 2 專案基本資訊 / 3 參與人員 / 4 程序書管理 / 5 雲端整合`（`:283` 註解）。雲端整合是**最後一個 tab（index 5）**。
- **BE 狀態信號**：`app/cloud_integration/service/google_drive_integration_service.py::get_status(tenant_id)` → `GoogleDriveIntegrationStatusDTO`，`status` ∈ `{CONNECTED / EXPIRED / REVOKED / DISCONNECTED}`（tenant 層級，DISCONNECTED = 從無整合）。

### 2.2 ⚠️ 已有現成 status endpoint（避免重複造輪子，實作前須拍板）

`api/cloud_integration/routes/google_drive_integration_route.py:22`：

```python
class GoogleDriveIntegrationRoute(MethodResource):
    # GET /integrations/google-drive — 取當前 tenant Drive 狀態，jwt_required()（非 admin gated）
    def get(self):
        dto = service.get_status(user.tenant_id)
        return return_response(True, GoogleDriveStatusResponse().dump(asdict(dto)))
    # DELETE 才是 super_admin gated
```

**GET 已對所有登入者開放**、回 `status`。故 FR-052 有兩條路（CLAUDE.md「先找既有能力、禁止重複造輪子」，本設計**建議走 A**）：

| 選項 | 做法 | BE 改動 | 取捨 |
|------|------|---------|------|
| **A（建議）reuse 既有** | FE 直接打 `GET /integrations/google-drive`，判 `status === 'CONNECTED'` | **零** | 回傳多帶 email/folder（已對全 JWT 開放，非新暴露）；最省 |
| **B（user 原選）新輕量端點** | 新增 `GET /integrations/google-drive/connected` 回 `{connected: bool}` | 一支 route + service 一行 | 語意最窄、不外洩 email/folder；但多一支端點與既有重疊 |

> user Phase 0 選了 B（新輕量端點）——但當時我尚未 grep 出既有 GET 已非 admin gated。**實作前一句話讓 user 二選一**（見 implementation-plan §Step 0）。以下設計兩案並存，預設 A。

---

## 3. User Mental Walkthrough

```
情境一：tenant 已 CONNECTED
1. Manager 進專案規劃頁 → onMounted 取 drive 狀態 → connected=true。
2. TabView 顯示 6 個 tab，最後一個「雲端整合」可見、可點。→ 行為同今。

情境二：tenant 未連接（DISCONNECTED / EXPIRED / REVOKED）
1. Manager 進專案規劃頁 → 取狀態 → connected=false。
2. TabView 只顯示前 5 個 tab（控制項/SSP/基本資訊/參與人員/程序書），
   「雲端整合」tab 根本不出現。
3. Manager 若要用雲端整合 → 需先由 super_admin 於系統層「雲端空間整合設定」接好 Drive，
   之後重進本頁才看得到（本案不做頁內引導文案——直接隱藏，決策 2）。

邊界：狀態查詢失敗（API error）
- connected 預設 false → tab 隱藏（fail-closed：拿不到狀態時不顯示，比誤顯示一個連不上的 tab 好）。
```

---

## 4. FE 設計（本案 FE 為主，BE 視 §2.2 選項）

`ProjectPlanningView.vue`：

```html
<!-- 只有 driveConnected 才渲染整個 TabPanel -->
<TabPanel v-if="driveConnected" :header="t('lang.project_planning.tab_cloud_integrations') ?? '雲端整合'">
    <ProjectCloudIntegrationsPanel :project-id="projectId" :can-edit="isProjectManager" />
</TabPanel>
```

```js
const driveConnected = ref(false)
async function loadDriveStatus() {
    try {
        // 選項 A：既有 endpoint
        const s = await CloudIntegrationService.getDriveStatus()  // GET /integrations/google-drive
        driveConnected.value = s?.status === 'CONNECTED'
        // 選項 B：新端點 → driveConnected.value = s?.connected === true
    } catch (e) {
        driveConnected.value = false   // fail-closed
    }
}
onMounted(loadDriveStatus)
```

### 4.1 PrimeVue TabView `v-if` 動態 tab 雷區（FR-047 frontend-overview §3.6）

- TabView 的 `:active-index` 依「渲染出的 TabPanel 順序」計 index。雲端整合是**最後一個** tab，用 `v-if` 移除**末尾** tab **不影響前 5 個 index**（若移的是中間 tab 才會 off-by-one）→ 本案安全。
- 若專案已有把 activeTab 記在 query/store，需確認「隱藏末 tab 後」不會殘留 index=5 指向不存在 tab（保險：`loadDriveStatus` resolve 前 activeTab 若 >4 且 !connected → clamp 到 0）。plan 列為 FE 驗證項。

---

## 5. BE 設計（僅選項 B 時）

若 user 確認走 B：

| 層 | 檔案 | 動作 |
|----|------|------|
| Route | `api/cloud_integration/routes/google_drive_integration_route.py` | 新增 `GoogleDriveConnectedRoute`，`GET /integrations/google-drive/connected`，`jwt_required()` |
| App | `google_drive_integration_service.py` | 加 `is_connected(tenant_id) -> bool`（`get_status(tenant_id).status == "CONNECTED"`，reuse）|
| 註冊 | `api/cloud_integration/__init__.py` | `add_resource(..., "/integrations/google-drive/connected")` |

- 走選項 A：**BE 零改動**，只 FE + 可能補一支 `CloudIntegrationService.getDriveStatus()`（若 FE 尚無）。

---

## 6. 權限 / Error handling
- 狀態查詢：`jwt_required()`，回自己 tenant 狀態（既有 `get_status` 已 tenant-scoped，RLS 不涉）。
- 不新增 error code（純唯讀 gate）。
- fail-closed：查詢失敗 → tab 隱藏。

---

## 7. 不在範圍（YAGNI）
- 不做「未連接時的引導文案 / 導去系統設定連結」（決策 2 = 直接隱藏）。
- 不改雲端空間整合設定頁本身（super_admin 系統層，非本案）。
- 不 gate 其他頁面的雲端相關入口（只本頁 tab；若有其他入口列 follow-up）。
- 不做 project-level（vs tenant-level）drive 連接判斷 —— case 語意是 tenant 層「雲端空間整合設定」。

---

## 8. 風險 / 待查證（plan Step 0）
1. **§2.2 A/B 二選一**：先讓 user 拍板（建議 A，零 BE 改動）。
2. FE `CloudIntegrationService` 是否已有取 tenant drive status 的方法（grep FE）——有就直接用，避免 FE 也重造。
3. TabView activeTab 持久化是否會殘留 index=5（§4.1）——FE 驗。
