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)


§1

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)

§2

Pre-flight:開工前確認

git branch --show-current

Expected: feature/menu-group

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)

grep "^version" ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/pyproject.toml

Expected: version = "0.1.18"


§3

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
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。

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

    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"
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(既有邏輯已正確處理)

4a. 修改 get_user_web_ui_routes 的 return 行(第 528 行):

# 原本:
        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

    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
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

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。

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"

§4

Task B:主專案 path dependency dev 模式 setup(不 commit)

Files:

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

pyproject.toml 找到(約在 dependencies 區段):

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

改為:

    # "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 看得到這個改動。

請使用者重啟 BE(BE 不支援 hot reload,path dep 改動需重啟才生效)。

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

grep "_prune_empty_groups" ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/user_service.py

Expected:找到該 method 定義。


§5

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

Files:

  • Create: scripts/sql/2026-05-26-menu-group-restructure.sql
  • Create: scripts/sql/backup/ (directory)
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)

建立 scripts/sql/2026-05-26-menu-group-restructure.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;

BEGIN; ... ROLLBACK; 模式跑確認沒有 SQL 錯誤:

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 後不存在。

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

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)

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 才能正確排除舊根節點。

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"

§6

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
head -50 ~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md

AppMenu.vue 找到 onMounted 區塊(第 10~19 行),把:

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)]
})

改為:

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

~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/config/locales/i18n/zh-tw/menu.json"menu" 物件末尾(在 "flow-template-create" 那行後面),新增:

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

(注意:最後一個既有 key 要加逗號,新增的最後一個 key 依 JSON 格式不加逗號)

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

        "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"

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,內容:

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

§7

Task E:跨 repo Smoke Test

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

# 先取 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)

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)

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

# 操作 預期結果
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: [](空陣列),genMenuNodeelse 分支會賦值 tree_node.to = null 並刪除 items,使其成為無法點擊的葉節點。jedi-auth 剪枝邏輯保證空分組不會出現在 API 回傳中,但若 BE/DB 部署順序錯誤(FE 先上、BE 後上),此情況可能暫時發生。部署順序必須嚴格遵守 BE → DB migration → FE。


§8

Task F:收尾(等 user 明確指示後才執行)

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

# 在 jedi-auth pyproject.toml 把 version = "0.1.18" 改為 "0.1.19"
# (patch bump — 剪枝邏輯屬 bug-fix-style 增量改進)
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>

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

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

    "jedi-auth==0.1.19",
#jedi-auth = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-auth", develop = true}
poetry update jedi-auth
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)"

§9

附錄:Rollback 快速參考

若 migration 出問題需還原:

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。