# Menu-Group Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 將系統側邊欄選單從 39 個扁平項目重組為 7 個可展開分組，讓使用者快速找到對應功能，並實現空分組自動隱藏。

**Architecture:** 三層改動，主專案 BE 程式碼零改動：(1) DB migration 新增 9 筆 `public.ui_routes` 分組節點 + 更新 27 筆葉節點的 `pid/sort`；(2) jedi-auth 套件在 `get_user_web_ui_routes()` 末段加剪枝邏輯（無子節點分組自動隱藏）；(3) FE `AppMenu.vue` 移除寫死的 `grcItems` + 補 7 個 i18n 分組 key。

**Tech Stack:** PostgreSQL（migration SQL）、Python / pytest（jedi-auth unit tests）、Vue 3（AppMenu.vue）、JSON（i18n menu.json）

---

## File Map

### Created
- `scripts/sql/2026-05-26-menu-group-restructure.sql` — DB migration（7 分組節點 + 2 搬遷項插入 + 27 葉節點 pid/sort 更新）
- `scripts/sql/backup/` — 新建備份目錄（migration 前手動產生 CSV 備份）

### Modified（jedi-auth 套件）
- `~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/user_service.py` — 加 `_prune_empty_groups` method + 修改 `get_user_web_ui_routes` return 行
- `~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/tests/unittest/test_user_service.py` — 新增 3 個 prune 相關 test case

### Modified（FE）
- `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/layout/AppMenu.vue` — 移除 grcItems 寫死區塊
- `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-tw/menu.json` — 補 7 個 `group-*` key
- `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/en/menu.json` — 補 7 個 `group-*` key（英文版）

### Created（FE）
- `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-cn/menu.json` — 新建（zh-cn 目前無 menu.json），補 7 個 `group-*` key 簡體版

### Not committed（dev-only）
- `~/Projects/Billows/Audit-Manager/compliance-manager-be/pyproject.toml` — path dependency 指向本地 jedi-auth（Task B，不 commit；Task F 收尾時還原 + 更新 pin）

---

## Pre-flight：開工前確認

- [ ] 確認目前 branch 是 `feature/menu-group`

```bash
git branch --show-current
```
Expected: `feature/menu-group`

- [ ] 確認 DB 現況（27 筆待更新葉節點 pid 目前都是 0/NULL）

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "SELECT count(*) FROM public.ui_routes WHERE enable = 1 AND (pid IS NULL OR pid = 0);"
```
Expected: 約 27 筆（目前所有 enable=1 的項目 pid 都是 0）

- [ ] 確認 jedi-auth 當前版本

```bash
grep "^version" ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/pyproject.toml
```
Expected: `version = "0.1.18"`

---

## Task A：jedi-auth — 剪枝邏輯 + Unit Tests（TDD）

**Files:**
- Modify: `~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/user_service.py` (line 527-528 附近)
- Test: `~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/tests/unittest/test_user_service.py`

- [ ] **Step A-1: 確認現有 test 的行數（找到新增位置）**

```bash
grep -n "def test_get_user_web_ui_routes" \
  ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/tests/unittest/test_user_service.py
```
Expected output 含 3 行（現有 3 個 test）。找最後一個既有 test 的行號，確認要在哪行後面追加新 tests。

- [ ] **Step A-2: 在 test file 的 TestUserService class 末尾追加 3 個 test case**

在 `test_user_service.py` 末尾的 `TestUserService` class 內，緊接在現有最後一個 method 後面插入：

```python
    def test_get_user_web_ui_routes_prune_empty_group(
        self, user_service, mock_user_domain_service, mock_ui_route_domain_service
    ):
        """剪枝：分組節點（無 url）底下無可見子節點時，應從回傳清單移除"""
        # Arrange
        user_uid = "test-uid-001"
        tenant_id = 1
        from unittest.mock import Mock
        mock_user_domain_service.get_user_by_uid.return_value = Mock(roles=[])
        mock_group = Mock()
        mock_group.id = 100
        mock_group.pid = None
        mock_group.name = "group-project-task"
        mock_group.icon = "pi pi-briefcase"
        mock_group.url = None
        mock_group.sort = 10
        mock_group.route_capabilities = []
        mock_ui_route_domain_service.get_viewable_routes_by_user_uid.return_value = [mock_group]

        # Act
        result = user_service.get_user_web_ui_routes(user_uid, tenant_id)

        # Assert
        assert result == []

    def test_get_user_web_ui_routes_keep_group_with_visible_children(
        self, user_service, mock_user_domain_service, mock_ui_route_domain_service
    ):
        """保留：分組節點底下有 2 個可見子節點時，應保留分組並 items 有 2 筆"""
        # Arrange
        user_uid = "test-uid-001"
        tenant_id = 1
        from unittest.mock import Mock
        mock_user_domain_service.get_user_by_uid.return_value = Mock(roles=[])

        mock_group = Mock()
        mock_group.id = 100
        mock_group.pid = None
        mock_group.name = "group-project-task"
        mock_group.icon = "pi pi-briefcase"
        mock_group.url = None
        mock_group.sort = 10
        mock_group.route_capabilities = []

        mock_child1 = Mock()
        mock_child1.id = 1
        mock_child1.pid = 100
        mock_child1.name = "project-dashboard"
        mock_child1.icon = "pi pi-home"
        mock_child1.url = "/project/dashboard"
        mock_child1.sort = 10
        mock_child1.route_capabilities = []

        mock_child2 = Mock()
        mock_child2.id = 2
        mock_child2.pid = 100
        mock_child2.name = "my-tasks"
        mock_child2.icon = "pi pi-check-square"
        mock_child2.url = "/project/task-manage"
        mock_child2.sort = 20
        mock_child2.route_capabilities = []

        mock_ui_route_domain_service.get_viewable_routes_by_user_uid.return_value = [
            mock_group, mock_child1, mock_child2
        ]

        # Act
        result = user_service.get_user_web_ui_routes(user_uid, tenant_id)

        # Assert
        assert len(result) == 1
        assert result[0]["label"] == "group-project-task"
        assert len(result[0]["items"]) == 2

    def test_get_user_web_ui_routes_keep_group_with_partial_visible_children(
        self, user_service, mock_user_domain_service, mock_ui_route_domain_service
    ):
        """保留：分組節點底下有 1 個可見子節點（另一個已被 RBAC 過濾）時，分組仍保留"""
        # Arrange
        user_uid = "test-uid-001"
        tenant_id = 1
        from unittest.mock import Mock
        mock_user_domain_service.get_user_by_uid.return_value = Mock(roles=[])

        mock_group = Mock()
        mock_group.id = 100
        mock_group.pid = None
        mock_group.name = "group-tenant-org"
        mock_group.icon = "pi pi-building"
        mock_group.url = None
        mock_group.sort = 40
        mock_group.route_capabilities = []

        mock_child = Mock()
        mock_child.id = 10
        mock_child.pid = 100
        mock_child.name = "user-manage"
        mock_child.icon = "pi pi-users"
        mock_child.url = "/auth/user-manage"
        mock_child.sort = 30
        mock_child.route_capabilities = []

        mock_ui_route_domain_service.get_viewable_routes_by_user_uid.return_value = [
            mock_group, mock_child
        ]

        # Act
        result = user_service.get_user_web_ui_routes(user_uid, tenant_id)

        # Assert
        assert len(result) == 1
        assert result[0]["label"] == "group-tenant-org"
        assert len(result[0]["items"]) == 1
        assert result[0]["items"][0]["label"] == "user-manage"
```

- [ ] **Step A-3: 跑 3 個新 test，確認 test_prune_empty_group 是 FAIL**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth
pytest tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_prune_empty_group \
       tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_keep_group_with_visible_children \
       tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_keep_group_with_partial_visible_children \
       -v
```

Expected：
- `test_prune_empty_group` → FAIL（`AssertionError: assert [{'label': 'group-project-task', ...}] == []`）
- `test_keep_group_with_visible_children` → PASS（既有邏輯已正確處理）
- `test_keep_group_with_partial_visible_children` → PASS（既有邏輯已正確處理）

- [ ] **Step A-4: 在 user_service.py 加入剪枝邏輯**

**4a. 修改 `get_user_web_ui_routes` 的 return 行（第 528 行）：**

```python
# 原本：
        sort_nodes(roots)
        return roots

# 改成：
        sort_nodes(roots)
        return self._prune_empty_groups(roots)
```

**4b. 在 `get_user_web_ui_routes` 同一個 class 內、method 後面新增 `_prune_empty_groups`：**

```python
    def _prune_empty_groups(self, nodes: list[dict]) -> list[dict]:
        """
        遞迴剪枝：移除「無 url 且 items 為空」的分組節點。
        葉節點（有 url）永遠保留；分組節點若還有可見子項也保留。
        """
        pruned = []
        for n in nodes:
            if n.get("items"):
                n["items"] = self._prune_empty_groups(n["items"])
            is_group = not n.get("url")
            if is_group and not n.get("items"):
                continue
            pruned.append(n)
        return pruned
```

- [ ] **Step A-5: 跑 3 個新 test，確認全部 PASS**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth
pytest tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_prune_empty_group \
       tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_keep_group_with_visible_children \
       tests/unittest/test_user_service.py::TestUserService::test_get_user_web_ui_routes_keep_group_with_partial_visible_children \
       -v
```
Expected：3 PASSED

- [ ] **Step A-6: 跑 jedi-auth 完整 unit test suite，確認無 regression**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth
pytest tests/unittest/ -v --tb=short 2>&1 | tail -20
```
Expected：所有既有 tests PASS，3 個新 tests PASS，無 FAILED。

- [ ] **Step A-7: 在 jedi-auth repo 建立 commit（不 bump version）**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth
git add jedi_auth/app/service/user_service.py tests/unittest/test_user_service.py
git commit -m "feat(user-service): add _prune_empty_groups to hide empty menu group nodes"
```

---

## Task B：主專案 path dependency dev 模式 setup（不 commit）

**Files:**
- Modify (not committed): `~/Projects/Billows/Audit-Manager/compliance-manager-be/pyproject.toml`

- [ ] **Step B-1: 取消 pyproject.toml 內的 jedi-auth path dependency 註解**

在 `pyproject.toml` 找到（約在 dependencies 區段）：

```toml
    "jedi-auth==0.1.18",
#jedi-auth = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-auth", develop = true}
```

改為：

```toml
    # "jedi-auth==0.1.18",
jedi-auth = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-auth", develop = true}
```

⚠️ **注意：此改動不 commit**。確保 working tree 是 dirty 狀態、`git status` 看得到這個改動。

- [ ] **Step B-2: 讓使用者重啟 BE，確認剪枝邏輯生效**

請使用者重啟 BE（BE 不支援 hot reload，path dep 改動需重啟才生效）。

重啟後驗證新 jedi-auth 已載入：

```bash
grep "_prune_empty_groups" ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/user_service.py
```
Expected：找到該 method 定義。

---

## Task C：DB migration SQL 撰寫 + dry-run verify

**Files:**
- Create: `scripts/sql/2026-05-26-menu-group-restructure.sql`
- Create: `scripts/sql/backup/` (directory)

- [ ] **Step C-1: 建立備份目錄，dump 現有 ui_routes 備份**

```bash
mkdir -p /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/backup

PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -At -F',' \
  -c "SELECT id, name, pid, sort, enable FROM public.ui_routes ORDER BY id;" \
  > /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/backup/2026-05-26-ui_routes-pre-menu-group.csv

wc -l /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/backup/2026-05-26-ui_routes-pre-menu-group.csv
```
Expected：39 行（含所有現有 ui_routes）

- [ ] **Step C-2: 建立 migration SQL 檔案**

建立 `scripts/sql/2026-05-26-menu-group-restructure.sql`，內容如下：

```sql
-- Date: 2026-05-26
-- Menu group restructure: 7 group nodes + 2 migrated items + 27 leaf pid/sort updates
-- Run as: cmmgr (not cm_app — RLS blocks system-level writes)

BEGIN;

-- ============================================================
-- Step 1: 新增 7 個分組節點（pid=NULL，無 url，僅作為展開父節點）
-- ============================================================

-- 1. group-project-task (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-project-task', NULL, 'pi pi-briefcase', 1, '專案與任務分組', 10
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-project-task');

-- 2. group-compliance-audit (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-compliance-audit', NULL, 'pi pi-shield', 1, '合規與稽核分組', 20
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-compliance-audit');

-- 3. group-communication (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-communication', NULL, 'pi pi-comments', 1, '溝通與互動分組', 30
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-communication');

-- 4. group-tenant-org (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-tenant-org', NULL, 'pi pi-building', 1, '租戶與組織分組', 40
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-tenant-org');

-- 5. group-system-admin (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-system-admin', NULL, 'pi pi-cog', 1, '系統管理分組', 50
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-system-admin');

-- 6. group-system-config (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-system-config', NULL, 'pi pi-sliders-h', 1, '系統設定分組', 60
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-system-config');

-- 7. group-ai-analytics (2026-05-26)
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), NULL, 'group-ai-analytics', NULL, 'pi pi-chart-line', 1, 'AI 與分析分組', 70
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-ai-analytics');

-- ============================================================
-- Step 2: 新增 2 個搬遷項（AppMenu.vue 原本寫死，搬進 DB）(2026-05-26)
-- pid 指向 group-project-task，不掛 capability（所有 user 可見）
-- ============================================================

-- 8. project-dashboard (2026-05-26)
WITH g AS (SELECT id FROM public.ui_routes WHERE name = 'group-project-task')
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), g.id, 'project-dashboard', '/project/dashboard', 'pi pi-home', 1, '專案儀表板', 10 FROM g
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'project-dashboard');

-- 9. my-tasks (2026-05-26)
WITH g AS (SELECT id FROM public.ui_routes WHERE name = 'group-project-task')
INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort)
SELECT gen_random_uuid(), g.id, 'my-tasks', '/project/task-manage', 'pi pi-check-square', 1, '待辦任務', 20 FROM g
WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'my-tasks');

-- ============================================================
-- Step 3: 更新 27 筆既有 ui_routes 的 pid / sort (2026-05-26)
-- ============================================================

-- Group 1 (project-task): sort 30/40/50（10/20 保留給 project-dashboard/my-tasks）
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-project-task'), sort = 30 WHERE name = 'project-list';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-project-task'), sort = 40 WHERE name = 'audit-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-project-task'), sort = 50 WHERE name = 'report-manage';

-- Group 2 (compliance-audit): sort 10/20/30/40
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-compliance-audit'), sort = 10 WHERE name = 'module-frame';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-compliance-audit'), sort = 20 WHERE name = 'compliance-framework-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-compliance-audit'), sort = 30 WHERE name = 'tool-plugin-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-compliance-audit'), sort = 40 WHERE name = 'flow-template-manage';

-- Group 3 (communication): sort 10/20/30/40/50/60
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 10 WHERE name = 'bulletin-list';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 20 WHERE name = 'bulletin-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 30 WHERE name = 'survey-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 40 WHERE name = 'system-feedback-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 50 WHERE name = 'system-feedback-view';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-communication'), sort = 60 WHERE name = 'issue-integrate-config';

-- Group 4 (tenant-org): sort 10/20/30/40
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-tenant-org'), sort = 10 WHERE name = 'tenant-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-tenant-org'), sort = 20 WHERE name = 'department-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-tenant-org'), sort = 30 WHERE name = 'user-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-tenant-org'), sort = 40 WHERE name = 'role-manage';

-- Group 5 (system-admin): sort 10/20/30/40/50
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-admin'), sort = 10 WHERE name = 'system-menu-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-admin'), sort = 20 WHERE name = 'information-system-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-admin'), sort = 30 WHERE name = 'device-manage';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-admin'), sort = 40 WHERE name = 'storage-config';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-admin'), sort = 50 WHERE name = 'cloud-integrations';

-- Group 6 (system-config): sort 10/20/30/40
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-config'), sort = 10 WHERE name = 'smtp-config';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-config'), sort = 20 WHERE name = 'ldap-config';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-config'), sort = 30 WHERE name = 'notify-config';
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-system-config'), sort = 40 WHERE name = 'user-log';

-- Group 7 (ai-analytics): sort 10
UPDATE public.ui_routes SET pid = (SELECT id FROM public.ui_routes WHERE name = 'group-ai-analytics'), sort = 10 WHERE name = 'ai-dashboard';

COMMIT;
```

- [ ] **Step C-3: dry-run 驗證（不 apply）**

用 `BEGIN; ... ROLLBACK;` 模式跑確認沒有 SQL 錯誤：

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "BEGIN; \
      INSERT INTO public.ui_routes (uid, pid, name, url, icon, enable, description, sort) \
        SELECT gen_random_uuid(), NULL, 'group-project-task', NULL, 'pi pi-briefcase', 1, '測試', 10 \
        WHERE NOT EXISTS (SELECT 1 FROM public.ui_routes WHERE name = 'group-project-task'); \
      SELECT name FROM public.ui_routes WHERE name = 'group-project-task'; \
      ROLLBACK;"
```
Expected：在 ROLLBACK 前能查到 `group-project-task`，ROLLBACK 後不存在。

- [ ] **Step C-4: 正式執行 migration（cmmgr 帳號）**

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -f /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-05-26-menu-group-restructure.sql
```
Expected：無 ERROR，最後印 `COMMIT`。

- [ ] **Step C-5: 驗證 migration 結果**

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "SELECT name, pid, sort FROM public.ui_routes WHERE name LIKE 'group-%' ORDER BY sort;"
```
Expected：7 行（group-project-task 10, group-compliance-audit 20, ... group-ai-analytics 70）

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "SELECT count(*) FROM public.ui_routes WHERE pid IS NOT NULL AND pid != 0 AND enable = 1;"
```
Expected：29 筆（27 既有更新 + 2 新增搬遷項）

> **注意**：現有 DB 的根節點用 `pid=0`（整數 0）而非 NULL，所以改用 `pid != 0` 才能正確排除舊根節點。

- [ ] **Step C-6: commit migration SQL + 備份 CSV**

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git add scripts/sql/2026-05-26-menu-group-restructure.sql scripts/sql/backup/2026-05-26-ui_routes-pre-menu-group.csv
git commit -m "feat(db): menu-group restructure migration — 7 groups + 29 leaf nodes assigned"
```

---

## Task D：FE AppMenu.vue + i18n 補 key

**Files:**
- Modify: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/layout/AppMenu.vue`
- Modify: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-tw/menu.json`
- Modify: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/en/menu.json`

- [ ] **Step D-1: 讀 FE CLAUDE.md 了解 FE commit 規範**

```bash
head -50 ~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md
```

- [ ] **Step D-2: 修改 AppMenu.vue — 移除寫死 grcItems**

在 `AppMenu.vue` 找到 `onMounted` 區塊（第 10~19 行），把：

```javascript
onMounted(()=> {
    // GRC / Project module (static — no permission gate needed for demo)
    const grcItems = [
        { label: 'project-dashboard',  icon: 'pi pi-home',         to: '/project/dashboard' },
        // { label: 'project-manage',     icon: 'pi pi-folder-open',  to: '/project/projects' },
        { label: 'my-tasks',           icon: 'pi pi-check-square', to: '/project/task-manage' },
    ]

    menu.value = [...grcItems, ...genMenuNode(permissions.value)]
})
```

改為：

```javascript
onMounted(()=> {
    menu.value = genMenuNode(permissions.value)
})
```

- [ ] **Step D-3: 補 zh-tw/menu.json 的 7 個 group-* key**

在 `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-tw/menu.json` 的 `"menu"` 物件末尾（在 `"flow-template-create"` 那行後面），新增：

```json
        "group-project-task":     "專案與任務",
        "group-compliance-audit": "合規與稽核",
        "group-communication":    "溝通與互動",
        "group-tenant-org":       "租戶與組織",
        "group-system-admin":     "系統管理",
        "group-system-config":    "系統設定",
        "group-ai-analytics":     "AI 與分析"
```

（注意：最後一個既有 key 要加逗號，新增的最後一個 key 依 JSON 格式不加逗號）

- [ ] **Step D-4: 補 en/menu.json 的 7 個 group-* key**

在 `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/en/menu.json` 的 `"menu"` 物件末尾同樣新增：

```json
        "group-project-task":     "Projects & Tasks",
        "group-compliance-audit": "Compliance & Audit",
        "group-communication":    "Communication",
        "group-tenant-org":       "Tenant & Organization",
        "group-system-admin":     "System Administration",
        "group-system-config":    "System Configuration",
        "group-ai-analytics":     "AI & Analytics"
```

- [ ] **Step D-5: 新建 zh-cn/menu.json（該檔案目前不存在）**

`zh-cn` 目錄只有 `common.json` 等其他檔，無 `menu.json`。若不建此檔，zh-cn 切換時分組標題會顯示裸 i18n key（如 `lang.menu.group-project-task`）。

建立 `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-cn/menu.json`，內容：

```json
{
    "menu": {
        "group-project-task":     "项目与任务",
        "group-compliance-audit": "合规与稽核",
        "group-communication":    "沟通与互动",
        "group-tenant-org":       "租户与组织",
        "group-system-admin":     "系统管理",
        "group-system-config":    "系统设置",
        "group-ai-analytics":     "AI 与分析"
    }
}
```

- [ ] **Step D-6: Commit FE changes**

```bash
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git add src/layout/AppMenu.vue \
        src/config/locales/i18n/zh-tw/menu.json \
        src/config/locales/i18n/en/menu.json \
        src/config/locales/i18n/zh-cn/menu.json
git commit -m "feat(menu): replace hardcoded grcItems with DB-driven groups + i18n group keys (zh-tw/zh-cn/en)"
```

---

## Task E：跨 repo Smoke Test

- [ ] **Step E-1: BE 重啟（請使用者重啟 BE）**

請使用者重啟 BE（確保 path dep 的 jedi-auth + 剪枝邏輯已載入）。

- [ ] **Step E-2: 呼叫 /user/web-menu API，驗證回傳有 7 個分組**

```bash
# 先取 JWT token（用 blsadmin 登入）
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"login_name":"blsadmin","password":"Billows@123!"}' \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))")
echo "Token: ${TOKEN:0:20}..."

# 呼叫 web-menu
curl -s http://localhost:8000/user/web-menu \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Tenant-ID: 102" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); [print(n['label']) for n in d.get('data',[])]"
```
Expected：7 行輸出（group-project-task / group-compliance-audit / ... / group-ai-analytics）

- [ ] **Step E-3: DB 驗證分組樹狀結構**

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "
SELECT g.name as group_name, count(c.id) as child_count
FROM public.ui_routes g
LEFT JOIN public.ui_routes c ON c.pid = g.id AND c.enable = 1
WHERE g.name LIKE 'group-%' AND g.enable = 1
GROUP BY g.name, g.sort
ORDER BY g.sort;
"
```
Expected：7 行，每行顯示分組名稱 + 子節點數（group-project-task: 5, group-compliance-audit: 4, group-communication: 6, group-tenant-org: 4, group-system-admin: 5, group-system-config: 4, group-ai-analytics: 1）

- [ ] **Step E-4: FE manual E2E checklist（請使用者在瀏覽器確認）**

請使用者在瀏覽器依序驗證：

| # | 操作 | 預期結果 |
|---|------|---------|
| 1 | admin user 登入 | 看到 7 個分組標題 |
| 2 | 點開「專案與任務」 | 展開顯示：專案儀表板 / 待辦任務 / 專案管理 / 我的稽核 / 專案調查結果 |
| 3 | 點葉節點（如「專案管理」）| 正常跳轉到對應路由 |
| 4 | 點分組標題本身（無 url）| 不跳轉，僅 toggle 展開/收合 |
| 5 | zh-tw 切換到 zh-cn | 分組標題顯示中文（注：zh-cn/menu.json 未建，若有 fallback 顯示 zh-tw 中文亦可） |
| 6 | 切換到 en locale | 顯示英文分組標題（Projects & Tasks 等） |
| 7 | 新進無 role user 登入 | 只看到「專案儀表板 / 待辦任務」兩項（無分組標題） |

> **Edge case 注意**：若某個分組節點回傳時 `items: []`（空陣列），`genMenuNode` 的 `else` 分支會賦值 `tree_node.to = null` 並刪除 `items`，使其成為無法點擊的葉節點。jedi-auth 剪枝邏輯保證空分組不會出現在 API 回傳中，但若 BE/DB 部署順序錯誤（FE 先上、BE 後上），此情況可能暫時發生。部署順序必須嚴格遵守 BE → DB migration → FE。

---

## Task F：收尾（等 user 明確指示後才執行）

⚠️ **此 Task 等 user 說「smoke 通過，可以收尾」或「準備進版」才執行。**

- [ ] **Step F-1: jedi-auth bump version**

```bash
# 在 jedi-auth pyproject.toml 把 version = "0.1.18" 改為 "0.1.19"
# （patch bump — 剪枝邏輯屬 bug-fix-style 增量改進）
```

- [ ] **Step F-2: jedi-auth commit + push origin**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth
git add pyproject.toml
git commit -m "chore: bump version 0.1.18 -> 0.1.19 (prune empty menu groups)"
git push origin <current-branch>
```

- [ ] **Step F-3: 推 Nexus（請使用者執行）**

jedi-auth 推 Nexus 需使用者自行執行，Claude 不代為操作。

- [ ] **Step F-4: 主專案 pyproject.toml 還原 pin 新版本**

在 `compliance-manager-be/pyproject.toml` 把 Task B 的 path dep 還原：

```toml
    "jedi-auth==0.1.19",
#jedi-auth = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-auth", develop = true}
```

- [ ] **Step F-5: poetry update jedi-auth（請使用者執行）**

```bash
poetry update jedi-auth
```

- [ ] **Step F-6: 主專案 commit（含 migration 已在 Task C commit，這裡只 commit pyproject 還原）**

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git add pyproject.toml poetry.lock
git commit -m "chore(deps): bump jedi-auth 0.1.18 -> 0.1.19 (menu group prune logic)"
```

---

## 附錄：Rollback 快速參考

若 migration 出問題需還原：

```sql
BEGIN;

-- 1. 移除本期新增 9 筆（2 搬遷項 + 7 分組節點）
DELETE FROM public.ui_routes WHERE name IN (
    'project-dashboard', 'my-tasks',
    'group-project-task', 'group-compliance-audit', 'group-communication',
    'group-tenant-org', 'group-system-admin', 'group-system-config', 'group-ai-analytics'
);

-- 2. 還原 27 筆葉節點 pid 為 NULL（sort 用備份 CSV 還原，或接受現有數值）
UPDATE public.ui_routes SET pid = NULL WHERE name IN (
    'project-list', 'audit-manage', 'report-manage',
    'module-frame', 'compliance-framework-manage', 'tool-plugin-manage', 'flow-template-manage',
    'bulletin-list', 'bulletin-manage', 'survey-manage',
    'system-feedback-manage', 'system-feedback-view', 'issue-integrate-config',
    'tenant-manage', 'department-manage', 'user-manage', 'role-manage',
    'system-menu-manage', 'information-system-manage', 'device-manage', 'storage-config', 'cloud-integrations',
    'smtp-config', 'ldap-config', 'notify-config', 'user-log',
    'ai-dashboard'
);

COMMIT;
```

jedi-auth 剪枝邏輯對扁平 tree（無分組節點）是 no-op，Rollback DB 後舊行為自然恢復，不需另外 rollback jedi-auth。
