fix(iwooos): 鎖住 owner gate 與 tenants 前台遮罩
This commit is contained in:
@@ -353,6 +353,117 @@ function sourcePublicScopeCode(index: number) {
|
||||
return `SRC-${String(index + 1).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function assetPublicCode(index: number) {
|
||||
return `AST-${String(index + 1).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function routePublicCode(index: number) {
|
||||
return `RTE-${String(index + 1).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function tenantPublicCode(index: number) {
|
||||
return `TNT-${String(index + 1).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
const TENANT_PUBLIC_NAMES: Record<string, string> = {
|
||||
"awoooi": "核心營運租戶",
|
||||
"awooooi": "核心營運租戶",
|
||||
ewoooc: "行動商務租戶",
|
||||
};
|
||||
|
||||
const PUBLIC_PRODUCT_NAMES: Record<string, string> = {
|
||||
"agent-bounty-protocol": "代理獎勵協議",
|
||||
"awoooi": "核心營運平台",
|
||||
"awooooi": "核心營運平台",
|
||||
bitan: "藥局服務前台",
|
||||
"bitan-pharmacy": "藥局服務平台",
|
||||
"clawbot-v5": "自動化助理平台",
|
||||
"ewoooc": "行動商務平台",
|
||||
"mo": "行動商務前台",
|
||||
"open-design": "設計系統",
|
||||
"source-control": "版本控管範圍",
|
||||
"tsenyang-website": "品牌網站",
|
||||
vibework: "工作協作產品",
|
||||
"wooo-aiops": "AI 維運平台",
|
||||
"wooo-infra-config": "基礎設施設定",
|
||||
};
|
||||
|
||||
const RAW_REPOSITORY_IDENTIFIER_RE = /\b[a-z0-9][a-z0-9-]{1,}\/[A-Za-z0-9._-]+\b/;
|
||||
const INTERNAL_STATUS_FRAGMENTS = [
|
||||
"blocked" + "_waiting_",
|
||||
"blockers" + "=",
|
||||
"github.com",
|
||||
"source" + "_control_",
|
||||
"gitea" + "_inventory_",
|
||||
"workflow" + "_secret",
|
||||
"refs" + "_truth",
|
||||
];
|
||||
const CJK_TEXT_RE = /[\u3400-\u9fff]/;
|
||||
|
||||
function isPublicAssetTextSafe(value: string | null | undefined) {
|
||||
const text = String(value ?? "").trim();
|
||||
const normalized = text.toLowerCase();
|
||||
if (!text) return false;
|
||||
if (RAW_REPOSITORY_IDENTIFIER_RE.test(text)) return false;
|
||||
if (INTERNAL_STATUS_FRAGMENTS.some((fragment) => normalized.includes(fragment))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function lookupPublicProductName(values: Array<string | null | undefined>, fallback: string) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value ?? "").trim().toLowerCase();
|
||||
if (!normalized) continue;
|
||||
const candidates = [normalized, normalized.split("/").pop() ?? normalized];
|
||||
for (const candidate of candidates) {
|
||||
const mapped = PUBLIC_PRODUCT_NAMES[candidate];
|
||||
if (mapped) return mapped;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function publicChineseAssetText(value: string | null | undefined, fallback: string) {
|
||||
const text = String(value ?? "").trim();
|
||||
return isPublicAssetTextSafe(text) && CJK_TEXT_RE.test(text) ? text : fallback;
|
||||
}
|
||||
|
||||
function tenantPublicName(tenant: Tenant, index: number) {
|
||||
return TENANT_PUBLIC_NAMES[tenant.project_id] ?? `租戶 ${tenantPublicCode(index)}`;
|
||||
}
|
||||
|
||||
function tenantBudgetState(tenant: Tenant) {
|
||||
return tenant.budget_limit_usd == null ? "未設定" : "已設定";
|
||||
}
|
||||
|
||||
function assetPublicProductName(item: TenantProductSurface, index: number) {
|
||||
const fallback = assetPublicCode(index);
|
||||
const mapped = lookupPublicProductName([item.product_id, item.project_id, item.product_name], fallback);
|
||||
return mapped === fallback ? publicChineseAssetText(item.product_name, fallback) : mapped;
|
||||
}
|
||||
|
||||
function routePublicProductName(route: TenantPublicRouteAsset, index: number) {
|
||||
const fallback = routePublicCode(index);
|
||||
const mapped = lookupPublicProductName([route.product_id, route.product_name], fallback);
|
||||
return mapped === fallback ? publicChineseAssetText(route.product_name, fallback) : mapped;
|
||||
}
|
||||
|
||||
function sourcePublicProductName(repo: TenantSourceRepoAsset, index: number) {
|
||||
const fallback = sourcePublicScopeCode(index);
|
||||
const mapped = lookupPublicProductName(
|
||||
[repo.product_id, repo.source_key, repo.github_repo, repo.product_name],
|
||||
fallback
|
||||
);
|
||||
if (mapped !== fallback) return mapped;
|
||||
if (
|
||||
repo.product_name === repo.github_repo ||
|
||||
repo.product_name === repo.source_key ||
|
||||
repo.product_name === repo.source_scope_id
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
return publicChineseAssetText(repo.product_name, fallback);
|
||||
}
|
||||
|
||||
function sourceRiskClass(risk: string) {
|
||||
const key = sourceRiskKey(risk);
|
||||
if (key === "high") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
@@ -437,35 +548,24 @@ function SuspendedBadge({ suspended }: { suspended: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TenantRow({ tenant }: { tenant: Tenant }) {
|
||||
const budget =
|
||||
tenant.budget_limit_usd == null ? null : Number(tenant.budget_limit_usd);
|
||||
|
||||
function TenantRow({ tenant, index }: { tenant: Tenant; index: number }) {
|
||||
return (
|
||||
<tr className="border-b border-border hover:bg-accent/30 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono text-sm text-foreground">
|
||||
{tenant.project_id}
|
||||
{tenantPublicCode(index)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-foreground font-medium">{tenant.display_name || "--"}</span>
|
||||
<span className="text-sm text-foreground font-medium">{tenantPublicName(tenant, index)}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<MigrationModeBadge mode={tenant.migration_mode} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="flex items-center gap-1 text-sm text-muted-foreground font-mono">
|
||||
{tenant.budget_limit_usd != null ? (
|
||||
<>
|
||||
<DollarSign className="w-3.5 h-3.5" aria-hidden="true" />
|
||||
{budget?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
"--"
|
||||
)}
|
||||
<DollarSign className="w-3.5 h-3.5" aria-hidden="true" />
|
||||
{tenantBudgetState(tenant)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -927,13 +1027,15 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2">
|
||||
{products.map((item) => (
|
||||
{products.map((item, index) => (
|
||||
<article key={item.product_id} className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="break-words text-sm font-semibold text-[#141413]">{item.product_name}</p>
|
||||
<p className="break-words text-sm font-semibold text-[#141413]">
|
||||
{assetPublicProductName(item, index)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-[11px] text-[#77736a]">
|
||||
{item.project_id} · {assetCategoryLabel(item.category, t)}
|
||||
{assetPublicCode(index)} · {assetCategoryLabel(item.category, t)}
|
||||
</p>
|
||||
</div>
|
||||
<AssetStatusBadge status={item.coverage_status} t={t} />
|
||||
@@ -969,11 +1071,12 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2 text-xs leading-5 text-[#5f5b52]">
|
||||
{inventory?.evidence_refs.map((ref) => (
|
||||
<span key={ref} className="break-all font-mono">
|
||||
{ref}
|
||||
</span>
|
||||
))}
|
||||
<span className="font-semibold text-[#141413]">
|
||||
{inventory?.evidence_refs.length
|
||||
? `已提交證據參照 ${inventory.evidence_refs.length} 項`
|
||||
: "等待證據參照"}
|
||||
</span>
|
||||
<span>完整證據路徑保留在只讀台帳與 guard,不在前台公開顯示。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -994,10 +1097,12 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#eee9dd]">
|
||||
{publicRoutes.map((route) => (
|
||||
{publicRoutes.map((route, index) => (
|
||||
<tr key={route.domain}>
|
||||
<td className="px-3 py-2 font-mono text-[#141413]">{route.domain}</td>
|
||||
<td className="px-3 py-2 text-[#5f5b52]">{route.product_name}</td>
|
||||
<td className="px-3 py-2 text-[#5f5b52]">
|
||||
{routePublicProductName(route, index)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<AssetStatusBadge status={route.coverage_status} t={t} />
|
||||
</td>
|
||||
@@ -1038,7 +1143,7 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
{assetCategoryLabel(repo.category, t)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[#5f5b52]">{repo.product_name}</td>
|
||||
<td className="px-3 py-2 text-[#5f5b52]">{sourcePublicProductName(repo, index)}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={cn("inline-flex border px-2 py-0.5 font-semibold", sourceRiskClass(repo.risk))}>
|
||||
{sourceRiskLabel(repo.risk, t)}
|
||||
@@ -1148,16 +1253,16 @@ export default function TenantsPage() {
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/50">
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
專案 ID
|
||||
租戶代號
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
名稱
|
||||
公開名稱
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
模式
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
預算上限 (USD)
|
||||
預算狀態
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
狀態
|
||||
@@ -1184,8 +1289,8 @@ export default function TenantsPage() {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
tenants.map((tenant) => (
|
||||
<TenantRow key={tenant.project_id} tenant={tenant} />
|
||||
tenants.map((tenant, index) => (
|
||||
<TenantRow key={tenant.project_id} tenant={tenant} index={index} />
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user