From 3351b07aa414dd5b84d3a84d7a3d486a6519d337 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Jun 2026 20:25:49 +0800 Subject: [PATCH] fix(web): condense tenants asset cockpit --- apps/web/messages/en.json | 25 +++ apps/web/messages/zh-TW.json | 25 +++ .../src/app/[locale]/awooop/tenants/page.tsx | 162 +++++++++++------- 3 files changed, 149 insertions(+), 63 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 808189f8..2c24bfae 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -10749,6 +10749,9 @@ "loading": "讀取中", "heatmapTitle": "產品納管熱力圖", "heatmapDetail": "每個產品以入口、來源與 owner 狀態呈現納管成熟度。", + "matrixTitle": "決策支援矩陣", + "matrixDetail": "先判斷全域資產是否納入、哪些還缺證據、下一步該往哪個工作區下鑽。", + "matrixBadge": "首屏判讀", "remainingProducts": "+{count} 個產品", "drilldownTitle": "關聯工作區", "boundaryTitle": "目前不可誤讀", @@ -10778,6 +10781,23 @@ "sources": "來源", "owner": "owner" }, + "matrix": { + "products": "產品 / 專案", + "productsStatus": "已進同一張資產地圖", + "productsDetail": "所有前後台產品、公開網站與平台工具先收斂到單一納管視圖。", + "routes": "網站入口", + "routesStatus": "待驗證 {pending}", + "routesDetail": "有入口就必須能追到可觀測性、smoke 與 owner 證據。", + "sources": "來源範圍", + "sourcesStatus": "缺口 {blockers}", + "sourcesDetail": "原始碼範圍只顯示脫敏代號,未完成證據前不得切主來源。", + "owner": "Owner gate", + "ownerStatus": "待回覆 {waiting}", + "ownerDetail": "沒有負責人接受紀錄,就不把候選範圍當正式核准。", + "runtime": "Runtime gate", + "runtimeStatus": "操作入口 {actions}", + "runtimeDetail": "這頁只作納管判讀,不提供掃描、部署、修復或主機操作入口。" + }, "drilldowns": { "observability": "可觀測性", "observabilityDetail": "主機、服務、網站入口、告警與接收證據。", @@ -10855,6 +10875,11 @@ "runtimeGate": "執行閘門", "actionButtons": "操作入口" }, + "details": { + "productsSummary": "{count} 個產品 / 專案完整明細,預設收合避免干擾首屏判讀。", + "routesSummary": "{count} 個網站 / 服務入口;待驗證 {pending} 個。", + "reposSummary": "{count} 個脫敏來源範圍;待補證據 {blockers} 個。" + }, "columns": { "domain": "網站 / 服務入口", "product": "產品 / 專案", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 808189f8..2c24bfae 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -10749,6 +10749,9 @@ "loading": "讀取中", "heatmapTitle": "產品納管熱力圖", "heatmapDetail": "每個產品以入口、來源與 owner 狀態呈現納管成熟度。", + "matrixTitle": "決策支援矩陣", + "matrixDetail": "先判斷全域資產是否納入、哪些還缺證據、下一步該往哪個工作區下鑽。", + "matrixBadge": "首屏判讀", "remainingProducts": "+{count} 個產品", "drilldownTitle": "關聯工作區", "boundaryTitle": "目前不可誤讀", @@ -10778,6 +10781,23 @@ "sources": "來源", "owner": "owner" }, + "matrix": { + "products": "產品 / 專案", + "productsStatus": "已進同一張資產地圖", + "productsDetail": "所有前後台產品、公開網站與平台工具先收斂到單一納管視圖。", + "routes": "網站入口", + "routesStatus": "待驗證 {pending}", + "routesDetail": "有入口就必須能追到可觀測性、smoke 與 owner 證據。", + "sources": "來源範圍", + "sourcesStatus": "缺口 {blockers}", + "sourcesDetail": "原始碼範圍只顯示脫敏代號,未完成證據前不得切主來源。", + "owner": "Owner gate", + "ownerStatus": "待回覆 {waiting}", + "ownerDetail": "沒有負責人接受紀錄,就不把候選範圍當正式核准。", + "runtime": "Runtime gate", + "runtimeStatus": "操作入口 {actions}", + "runtimeDetail": "這頁只作納管判讀,不提供掃描、部署、修復或主機操作入口。" + }, "drilldowns": { "observability": "可觀測性", "observabilityDetail": "主機、服務、網站入口、告警與接收證據。", @@ -10855,6 +10875,11 @@ "runtimeGate": "執行閘門", "actionButtons": "操作入口" }, + "details": { + "productsSummary": "{count} 個產品 / 專案完整明細,預設收合避免干擾首屏判讀。", + "routesSummary": "{count} 個網站 / 服務入口;待驗證 {pending} 個。", + "reposSummary": "{count} 個脫敏來源範圍;待補證據 {blockers} 個。" + }, "columns": { "domain": "網站 / 服務入口", "product": "產品 / 專案", diff --git a/apps/web/src/app/[locale]/awooop/tenants/page.tsx b/apps/web/src/app/[locale]/awooop/tenants/page.tsx index d037d068..d0f60f6e 100644 --- a/apps/web/src/app/[locale]/awooop/tenants/page.tsx +++ b/apps/web/src/app/[locale]/awooop/tenants/page.tsx @@ -16,6 +16,7 @@ import { AlertCircle, DollarSign, Ban, + ChevronDown, CheckCircle2, GitBranch, Globe2, @@ -588,7 +589,6 @@ function ProductCommandMap({ loading: boolean; }) { const t = useTranslations("awooop.tenants.productCommandMap"); - const assetT = useTranslations("awooop.tenants.globalAssets"); const products = inventory?.products ?? []; const publicRoutes = inventory?.public_routes ?? []; const sourceRepos = inventory?.source_repos ?? []; @@ -604,6 +604,9 @@ function ProductCommandMap({ const acceptedOwnerCount = summary?.owner_response_accepted_count ?? 0; const runtimeGateCount = summary?.runtime_gate_count ?? 0; const actionButtonCount = summary?.action_button_count ?? 0; + const routeReadyCount = publicRoutes.filter((item) => item.route_smoke_accepted).length; + const sourcePrimaryReadyCount = summary?.source_primary_ready_count ?? 0; + const sourceCandidateCount = summary?.source_candidate_repo_count ?? sourceRepos.length; const topologyRows = [ { key: "products", @@ -668,8 +671,43 @@ function ProductCommandMap({ tone: runtimeGateCount === 0 ? "locked" as const : "warn" as const, }, ]; - const productPreview = products.slice(0, 12); - const remainingProducts = Math.max(products.length - productPreview.length, 0); + const decisionRows = [ + { + key: "products", + value: `${products.length}`, + status: t("matrix.productsStatus"), + detail: t("matrix.productsDetail"), + tone: "steady" as const, + }, + { + key: "routes", + value: `${routeReadyCount}/${publicRoutes.length}`, + status: t("matrix.routesStatus", { pending: routePendingCount }), + detail: t("matrix.routesDetail"), + tone: routePendingCount > 0 ? "warn" as const : "steady" as const, + }, + { + key: "sources", + value: `${sourcePrimaryReadyCount}/${sourceCandidateCount}`, + status: t("matrix.sourcesStatus", { blockers: sourceBlockerCount }), + detail: t("matrix.sourcesDetail"), + tone: sourceBlockerCount > 0 ? "warn" as const : "steady" as const, + }, + { + key: "owner", + value: `${acceptedOwnerCount}/${products.length}`, + status: t("matrix.ownerStatus", { waiting: ownerWaitingCount }), + detail: t("matrix.ownerDetail"), + tone: "warn" as const, + }, + { + key: "runtime", + value: `${runtimeGateCount}`, + status: t("matrix.runtimeStatus", { actions: actionButtonCount }), + detail: t("matrix.runtimeDetail"), + tone: "locked" as const, + }, + ]; const drilldowns = [ { href: "/observability", @@ -754,52 +792,32 @@ function ProductCommandMap({
-

{t("heatmapTitle")}

-

{t("heatmapDetail")}

+

{t("matrixTitle")}

+

{t("matrixDetail")}

- {remainingProducts > 0 && ( - - {t("remainingProducts", { count: remainingProducts })} - - )} + + {t("matrixBadge")} +
-
- {productPreview.map((item, index) => { - const statusTone = ASSET_STATUS_TONES[item.coverage_status] ?? "locked"; - return ( -
-
-
-

- {assetPublicProductName(item, index)} -

-

- {assetPublicCode(index)} · {assetCategoryLabel(item.category, assetT)} -

-
-
-
-
-

{t("mini.routes")}

-

{item.public_route_count}

-
-
-

{t("mini.sources")}

-

{item.source_repo_count}

-
-
-

{t("mini.owner")}

-

{item.owner_response_accepted_count}

-
-
-
- ); - })} +
+ {decisionRows.map(({ key, value, status, detail, tone }) => ( +
+
+

+ {t(`matrix.${key}` as never)} +

+

{value}

+
+
+ {status} +
+

{detail}

+
+ ))}
@@ -1438,13 +1456,19 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
-
-
-

{t("productsTitle")}

- - {products.length} {t("itemsUnit")} +
+ + + {t("productsTitle")} + + {t("details.productsSummary", { count: products.length })} + -
+ + {products.length} {t("itemsUnit")} + +
{products.map((item, index) => (
@@ -1479,7 +1503,7 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
))}
-
+

{t("boundaryTitle")}

@@ -1501,10 +1525,16 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
-
-
- {t("routesTitle")} -
+
+ + + {t("routesTitle")} + + {t("details.routesSummary", { count: publicRoutes.length, pending: pendingRouteSmokeCount })} + + +
{publicRoutes.map((route, index) => (
@@ -1568,12 +1598,18 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
-
+ -
-
- {t("reposTitle")} -
+
+ + + {t("reposTitle")} + + {t("details.reposSummary", { count: sourceRepos.length, blockers: sourceBlockerCount })} + + +
{sourceRepos.map((repo, index) => (
-
+
);