FR-052 實作計畫 — 雲端整合 Tab 顯示 Gate

docs/features/FR-052-2607-cloud-tab-gate/design.md。 跨 repo:FE 為主(compliance-manager-fe);BE 視 Step 0 選項(零或極小)。 只規劃不實作——user approve 後才進 Phase 5。


§1

CLAUDE.md 規範檢查


§2

Step 0 — A/B 二選一(動手前必拍板,design §2.2)

先讓 user 二選一(design §2.2 表):

選項 BE 改動 說明
A(建議) FE 直接打既有 GET /integrations/google-drive,判 status==='CONNECTED'
B(user Phase 0 原選) 新一支 /connected route 語意最窄但與既有 GET 重疊

user Phase 0 選 B,但當時未知既有 GET 已非 admin gated。開工前一句話確認——若改選 A,跳過所有 BE 任務。

FE 前置 grepCloudIntegrationServicesrc/service/)是否已有取 tenant drive status 的方法?有 → 直接用,FE 也不重造。

輸出:Step 0 結論寫本檔末,再開工。


§3

BE 任務(僅選項 B;選 A 則全跳過)

Task B1 — is_connected app method〔0.1d〕

  • app/cloud_integration/service/google_drive_integration_service.py:加
    @transaction
    def is_connected(self, tenant_id: int) -> bool:
        return self.get_status(tenant_id).status == "CONNECTED"

Task B2 — connected route〔0.15d〕

  • api/cloud_integration/routes/google_drive_integration_route.py:新 GoogleDriveConnectedRoute(MethodResource)GETjwt_required(),回 return_response(True, {"connected": service.is_connected(user.tenant_id)})
  • api/cloud_integration/__init__.pyadd_resource(GoogleDriveConnectedRoute, "/integrations/google-drive/connected")
  • :pytest — CONNECTED tenant 回 true;DISCONNECTED/EXPIRED/REVOKED 回 false。

§4

FE 任務(compliance-manager-fe,主體)

Task F1 — service 取狀態方法〔選 A:0;選 B:0.15d〕

  • 選項 A(review 已證零改動)CloudIntegrationService.getGoogleDriveStatus() 已存在src/service/CloudIntegrationService.js:14-17,打 GET /integrations/google-drive,已含 BaseService 返回值歸一化)→ F1 零改動,直接用
  • 選項 B:新增 getDriveConnected()GET /integrations/google-drive/connected + src/config/api/api.js 補常數。

Task F2 — ProjectPlanningView tab gate〔0.3d〕

  • src/views/project/ProjectPlanningView.vue
    • const driveConnected = ref(false)onMountedloadDriveStatus()(A: s.status==='CONNECTED';B: s.connected===true;catch → false,fail-closed)。
    • 雲端整合 <TabPanel>(:2110)加 v-if="driveConnected"
  • ⚠️ 深連結 clamp 必做(review 風險升級:從「保險」改「必做」)ProjectPlanningView.vue:289-296 TAB_INDEX_BY_QUERYcloud:5activeTabIndex 在 setup 同步初始化、早於 loadDriveStatus() resolve。未連接 tenant 帶 ?tab=cloud 進來 → activeIndex=5 指向不存在的 tab。F2 必做 clamploadDriveStatus resolve 後 if (!connected && activeTabIndex.value >= 5) activeTabIndex.value = 0
    • review 全 FE grep 確認無其他寫死 tab=cloud 來源,但 query 是 user 可自由帶的 → clamp 不可省。
  • 驗(FR-047 frontend-overview §3.6 雷區):雲端 tab 是末 tab(index 5),v-if 移除末 tab 不影響前 5 index。連 / 未連 / ?tab=cloud 深連結三情境驗。

Task F3 — i18n〔0〕

  • 無新文案(決策 2 = 直接隱藏,不加引導)。既有 tab_cloud_integrations 譯名不動。

§5

執行順序 / 依賴

Step 0(A/B 拍板 + FE grep 既有 service)
   ↓
[若 B] B1 → B2(BE pytest 綠)
   ↓
F1(service)→ F2(tab v-if)→ 驗 TabView index
   ↓
manual smoke(連 / 未連兩 tenant)→ Phase 4 test-plan → E2E
  • 選 A:無 BE 任務,直接 F1→F2。
  • 回滾:F2 一 commit(移 v-if 即恢復恆顯);B1/B2 各一 commit。

§6

完成標準

  • [ ]〔若 B〕BE pytest:is_connected 四狀態正確。

§7

Step 0 查證結果

2026-07-21 首腦 review 已做完 FE grep + BE route 複核。A/B 最終仍等 user 一句話拍板(勿自行定案),但證據強烈指向 A。

✅ FE grep(已做完)

  • CloudIntegrationService.getGoogleDriveStatus() 已存在src/service/CloudIntegrationService.js:14-17,打 GET /integrations/google-drive,已含 BaseService 返回值歸一化)→ 選 A 連 F1 都零改動,FE 只剩 F2 一個 task

✅ BE route 複核(已做完)

  • GET /integrations/google-drive 只有 jwt_required()DELETEviewer_is_super_admingoogle_drive_integration_route.py:22-50)→ 選 A 的 reuse 端點對全登入者開放,可行。

判定:選 B 的唯一理由「不外洩 email/folder」不成立——既有 GET 本來就對全 JWT 開放,B 收不回任何暴露。選 A 則整個 FR-052 = ProjectPlanningView.vue 單檔改動(F1 零改、只做 F2)。首腦強烈建議 A。

✅ 決策:走 A(reuse 既有 GET,零 BE 改動)(2026-07-21 首腦「往下做」授權下定案;B 唯一理由已證不成立)。→ BE 任務全跳過,FR-052 = ProjectPlanningView.vue 單檔 F2(F1 用既有 getGoogleDriveStatus())。

✅ 深連結風險已證實(見 F2)?tab=cloud 殘留 index=5 為真(ProjectPlanningView.vue:289-296),F2 clamp 已從「保險」升為「必做」。