feat(web): show GitHub readiness in AwoooP tenants

This commit is contained in:
Your Name
2026-05-21 10:04:00 +08:00
parent 56d725268d
commit 77e9f45db7
8 changed files with 339 additions and 3 deletions

View File

@@ -3955,6 +3955,40 @@
"hostCoverage": "The three named hosts are included only for security visibility and evidence readiness; no SSH, updates, credentialed scans, or blocking controls are performed.",
"ownerResponse": "S4.9-S4.12 owner response received / accepted remain 0. Tenant scope display is not approval."
}
},
"githubTenantReadinessScope": {
"title": "GitHub Primary Readiness Tenant Scope",
"subtitle": "Tenant management mirrors the source-control owner scope gap between the AWOOOI first tenant and the Gitea-to-GitHub path. This is not tenant policy, repo creation, or primary switch authorization.",
"badge": "Read-only scope",
"openIwooos": "Open IwoooS",
"scopeRefsTitle": "Owner Scope Refs",
"boundaryLabel": "Tenant / GitHub Boundary",
"boundaryTitle": "Tenant scope still waits for owner response",
"boundaryDetail": "This panel only displays the relation between tenant scope and source-control readiness. It does not change tenant migration mode, modify tenant policy, create GitHub repos, mutate refs, collect secret values, switch primary, or disable Gitea.",
"metrics": {
"candidateRepos": {
"label": "Candidate Repos",
"detail": "8 candidate repos are scope visibility only and do not authorize GitHub repo creation."
},
"inScopeRepos": {
"label": "In-scope Repos",
"detail": "7 repos still require owner scope decision and source-control response."
},
"ownerResponses": {
"label": "Owner Responses",
"detail": "22 response templates remain 0 received / 0 accepted."
},
"tenantScopeChanges": {
"label": "Tenant Scope Changes",
"detail": "Currently 0. Readiness display must not change tenant policy or migration mode."
}
},
"scopeRefs": {
"tenantSourceScope": "The AWOOOI first tenant only maps to source-control readiness scope; it does not mean the whole platform or GitHub primary is accepted.",
"giteaInventoryOwner": "S4.9 still waits for Gitea inventory owner attestation. Repo scope must not be filled as accepted before coverage is accepted.",
"githubTargetOwner": "S4.10 still waits for GitHub target owner decision. Do not create repos or change visibility before the target owner accepts.",
"workflowSecretOwner": "S4.12 only waits for workflow / secret name owner response. Secret values must not be collected and GitHub secrets must not be changed."
}
}
},
"runs": {

View File

@@ -3956,6 +3956,40 @@
"hostCoverage": "三台指定主機目前只納入資安視野與 evidence readiness不做 SSH、更新、credentialed scan 或 blocking control。",
"ownerResponse": "S4.9-S4.12 owner response received / accepted 仍為 0tenant scope 顯示不等於批准。"
}
},
"githubTenantReadinessScope": {
"title": "GitHub Primary Readiness 租戶範圍",
"subtitle": "租戶管理同步顯示 AWOOOI first tenant 與 Gitea 轉 GitHub 的 source-control owner scope 缺口;這不是租戶 policy、repo 建立或 primary 切換授權。",
"badge": "只讀 scope",
"openIwooos": "開啟 IwoooS",
"scopeRefsTitle": "Owner scope refs",
"boundaryLabel": "租戶 / GitHub 邊界",
"boundaryTitle": "租戶範圍仍等待 owner response",
"boundaryDetail": "這個面板只呈現租戶與 source-control readiness 的對應關係;不變更 tenant migration mode、不改 tenant policy、不建立 GitHub repo、不改 refs、不收 secret value、不切 primary也不停用 Gitea。",
"metrics": {
"candidateRepos": {
"label": "Candidate repos",
"detail": "8 個候選 repo 只作為 scope visibility不代表可建立 GitHub repo。"
},
"inScopeRepos": {
"label": "In-scope repos",
"detail": "7 個 repo 仍需 owner scope decision 與 source-control response。"
},
"ownerResponses": {
"label": "Owner responses",
"detail": "22 個 response templates 仍為 0 received / 0 accepted。"
},
"tenantScopeChanges": {
"label": "Tenant scope changes",
"detail": "目前為 0不得因顯示 readiness 而改 tenant policy 或 migration mode。"
}
},
"scopeRefs": {
"tenantSourceScope": "AWOOOI first tenant 只對應 source-control readiness 範圍,不代表全平台或 GitHub primary 已接受。",
"giteaInventoryOwner": "S4.9 仍等待 Gitea inventory owner attestationcoverage accepted 前不得補寫 repo scope。",
"githubTargetOwner": "S4.10 仍等待 GitHub target owner decisiontarget owner 未接受前不得建立 repo 或改 visibility。",
"workflowSecretOwner": "S4.12 只等待 workflow / secret name owner response仍不得收 secret value 或修改 GitHub secrets。"
}
}
},
"runs": {

View File

@@ -16,6 +16,10 @@ import {
DollarSign,
Ban,
CheckCircle2,
GitBranch,
ListChecks,
Lock,
SearchCheck,
ShieldCheck,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -46,12 +50,67 @@ interface ApiResponse {
total: number;
}
type GitHubTenantReadinessMetric = {
key: string;
value: string;
icon: typeof ShieldCheck;
tone: "neutral" | "warn";
};
type GitHubTenantReadinessScope = {
key: string;
name: string;
status: string;
};
// =============================================================================
// 常數
// =============================================================================
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
const githubTenantReadinessMetrics: GitHubTenantReadinessMetric[] = [
{ key: "candidateRepos", value: "8", icon: GitBranch, tone: "neutral" },
{ key: "inScopeRepos", value: "7", icon: ListChecks, tone: "warn" },
{ key: "ownerResponses", value: "0/22", icon: ShieldCheck, tone: "warn" },
{ key: "tenantScopeChanges", value: "0", icon: Lock, tone: "warn" },
];
const githubTenantReadinessScopes: GitHubTenantReadinessScope[] = [
{
key: "tenantSourceScope",
name: "AWOOOI first tenant source-control scope",
status: "waiting",
},
{
key: "giteaInventoryOwner",
name: "S4.9 Gitea inventory owner attestation",
status: "0/5",
},
{
key: "githubTargetOwner",
name: "S4.10 GitHub target owner decision",
status: "0/7",
},
{
key: "workflowSecretOwner",
name: "S4.12 workflow / secret name owner response",
status: "0/5",
},
];
const githubTenantReadinessBoundaries = [
"tenant_source_control_scope_accepted=false",
"repo_owner_response_accepted=false",
"repo_creation_authorized=false",
"refs_mutation_authorized=false",
"github_primary_switch_authorized=false",
"gitea_disablement_authorized=false",
"tenant_policy_mutation_authorized=false",
"runtime_execution_authorized=false",
"action_buttons_allowed=false",
];
const MIGRATION_MODE_CONFIG: Record<
MigrationMode,
{ label: string; bg: string; text: string; border: string }
@@ -312,6 +371,106 @@ function SecurityTenantScopeCandidatePanel() {
);
}
function GitHubTenantReadinessScopePanel() {
const t = useTranslations("awooop.tenants.githubTenantReadinessScope");
return (
<section className="overflow-hidden border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex min-w-0 items-start gap-2">
<GitBranch className="mt-0.5 h-4 w-4 shrink-0 text-[#d97757]" aria-hidden="true" />
<div className="min-w-0">
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 max-w-3xl text-xs leading-5 text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 text-xs font-semibold text-[#8a5a08]">
{t("badge")}
</span>
<Link
href="/iwooos"
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-1.5 text-xs font-semibold text-[#141413] hover:border-[#d97757]"
>
{t("openIwooos")}
<ArrowRight className="h-3.5 w-3.5 text-[#77736a]" aria-hidden="true" />
</Link>
</div>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
{githubTenantReadinessMetrics.map((metric) => {
const Icon = metric.icon;
return (
<div key={metric.key} className="min-h-[122px] bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-[#77736a]">
{t(`metrics.${metric.key}.label` as never)}
</p>
<p className="mt-2 break-words font-mono text-2xl font-semibold text-[#141413]">
{metric.value}
</p>
</div>
<span
className={cn(
"flex h-8 w-8 shrink-0 items-center justify-center border",
metric.tone === "warn" && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
metric.tone === "neutral" && "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"
)}
>
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-3 text-xs leading-5 text-[#5f5b52]">
{t(`metrics.${metric.key}.detail` as never)}
</p>
</div>
);
})}
</div>
<div className="grid min-w-0 gap-4 p-4 xl:grid-cols-[1.1fr_0.9fr]">
<div className="min-w-0 border border-[#e0ddd4] bg-white">
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 text-xs font-semibold text-[#141413]">
{t("scopeRefsTitle")}
</div>
<div className="divide-y divide-[#eee9dd]">
{githubTenantReadinessScopes.map((scope) => (
<div key={scope.key} className="grid gap-3 px-4 py-3 md:grid-cols-[minmax(0,1fr)_96px]">
<div className="min-w-0">
<p className="break-words font-mono text-xs font-semibold text-[#141413]">
{scope.name}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t(`scopeRefs.${scope.key}` as never)}
</p>
</div>
<div>
<span className="inline-flex border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono text-xs font-semibold text-[#8a5a08]">
{scope.status}
</span>
</div>
</div>
))}
</div>
</div>
<div className="min-w-0 border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("boundaryLabel")}</p>
<p className="mt-2 text-sm font-semibold text-[#141413]">{t("boundaryTitle")}</p>
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{t("boundaryDetail")}</p>
<div className="mt-4 grid gap-2 break-all font-mono text-xs text-[#141413]">
{githubTenantReadinessBoundaries.map((boundary) => (
<span key={boundary}>{boundary}</span>
))}
</div>
</div>
</div>
</section>
);
}
// =============================================================================
// Main Component
// =============================================================================
@@ -366,6 +525,8 @@ export default function TenantsPage() {
<SecurityTenantScopeCandidatePanel />
<GitHubTenantReadinessScopePanel />
{/* Error State */}
{error && (
<div className="flex items-start gap-3 p-4 bg-red-900/20 border border-red-800/40 rounded-lg">