feat(web): surface AI automation production proof
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 37s
CD Pipeline / build-and-deploy (push) Successful in 4m41s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s

This commit is contained in:
Your Name
2026-07-01 23:07:32 +08:00
parent 230ad3ef9c
commit ec498e457d
3 changed files with 158 additions and 5 deletions

View File

@@ -3878,8 +3878,8 @@
"riskTitle": "Controlled risk lanes",
"on": "ON",
"off": "OFF",
"ownerRequired": "owner review required",
"ownerNotRequired": "owner review not required",
"ownerRequired": "Controlled evidence review required",
"ownerNotRequired": "Controlled evidence review cleared",
"criticalBreakGlass": "critical break-glass",
"criticalReview": "critical review"
},
@@ -11585,6 +11585,20 @@
"playbook": "PlayBook",
"telegram": "Telegram"
},
"proof": {
"deploy": "Production deploy",
"deployDetail": "CD / readback: {status}",
"runtime": "Runtime DB",
"runtimeDetail": "marker: {marker}",
"workItems": "Work complete",
"workItemsDetail": "{percent}% complete",
"sources": "Log sources",
"sourcesDetail": "Project, product, site, service, package, and tool",
"events": "Classified events",
"eventsDetail": "24h {recent}",
"ok": "ok",
"degraded": "degraded"
},
"recent": "24h {count}",
"missing": "{count} missing",
"closedDetail": "required stages ok",

View File

@@ -3878,8 +3878,8 @@
"riskTitle": "Controlled risk lanes",
"on": "ON",
"off": "OFF",
"ownerRequired": "owner review required",
"ownerNotRequired": "owner review not required",
"ownerRequired": "受控證據複核需要補齊",
"ownerNotRequired": "受控證據複核已免除",
"criticalBreakGlass": "critical break-glass",
"criticalReview": "critical review"
},
@@ -11585,6 +11585,20 @@
"playbook": "PlayBook",
"telegram": "Telegram"
},
"proof": {
"deploy": "正式部署",
"deployDetail": "CD / readback{status}",
"runtime": "Runtime DB",
"runtimeDetail": "marker{marker}",
"workItems": "工作完成",
"workItemsDetail": "完成度 {percent}%",
"sources": "Log 來源",
"sourcesDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具",
"events": "分類事件",
"eventsDetail": "近 24h {recent}",
"ok": "ok",
"degraded": "degraded"
},
"recent": "近 24h {count}",
"missing": "缺 {count} 節點",
"closedDetail": "required stages ok",

View File

@@ -9,9 +9,11 @@ import {
Bot,
BookOpenCheck,
CheckCircle2,
Database,
Gauge,
ListChecks,
RefreshCw,
Rocket,
Send,
ShieldCheck,
SlidersHorizontal,
@@ -182,6 +184,22 @@ type RuntimeControlPayload = {
rollups?: Record<string, number | string | boolean | null | undefined> | null;
};
type PriorityWorkOrderPayload = {
status?: string | null;
summary?: {
latest_successful_deploy_marker?: string | null;
latest_successful_deployed_source_sha?: string | null;
latest_successful_deployed_source_short_sha?: string | null;
ai_loop_current_blocker_deploy_marker_readback_required?: boolean | null;
ai_loop_current_blocker_deploy_marker_resolved_by_production_readback?: boolean | null;
} | null;
mainline_execution_state?: {
current_main_cd_run_id?: string | null;
current_main_cd_run_status?: string | null;
current_main_latest_source_sha?: string | null;
} | null;
};
type PanelMode = "full" | "compact";
type Tone = "ok" | "warn" | "neutral";
type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked";
@@ -262,6 +280,23 @@ async function fetchRuntimeControl(): Promise<RuntimeControlPayload | null> {
}
}
async function fetchPriorityWorkOrder(): Promise<PriorityWorkOrderPayload | null> {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 12_000);
try {
const response = await fetch(`${API_BASE}/api/v1/agents/awoooi-priority-work-order-readback`, {
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) return null;
return (await response.json()) as PriorityWorkOrderPayload;
} catch {
return null;
} finally {
window.clearTimeout(timeout);
}
}
export function AutonomousRuntimeReceiptPanel({
mode = "full",
}: {
@@ -270,6 +305,7 @@ export function AutonomousRuntimeReceiptPanel({
const t = useTranslations("awooop.autonomousRuntime");
const locale = useLocale();
const [payload, setPayload] = useState<RuntimeControlPayload | null>(null);
const [priorityPayload, setPriorityPayload] = useState<PriorityWorkOrderPayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
@@ -277,8 +313,12 @@ export function AutonomousRuntimeReceiptPanel({
const refresh = useCallback(async () => {
setLoading(true);
const next = await fetchRuntimeControl();
const [next, priority] = await Promise.all([
fetchRuntimeControl(),
fetchPriorityWorkOrder(),
]);
setPayload(next);
setPriorityPayload(priority);
setError(next === null);
setUpdatedAt(next ? new Date() : null);
setLoading(false);
@@ -510,6 +550,64 @@ export function AutonomousRuntimeReceiptPanel({
const workCompletionPercent = workTotal > 0
? Math.min(100, Math.round((workCompleted / workTotal) * 100))
: 0;
const prioritySummary = priorityPayload?.summary;
const mainlineState = priorityPayload?.mainline_execution_state;
const deployedSourceSha = prioritySummary?.latest_successful_deployed_source_sha
?? mainlineState?.current_main_latest_source_sha
?? null;
const deployedSourceShortSha = prioritySummary?.latest_successful_deployed_source_short_sha
?? (deployedSourceSha ? deployedSourceSha.slice(0, 10) : null);
const deployReadbackResolved = prioritySummary?.ai_loop_current_blocker_deploy_marker_resolved_by_production_readback === true;
const proofCards = [
{
key: "deploy",
label: t("proof.deploy"),
value: deployedSourceShortSha ?? "--",
detail: t("proof.deployDetail", {
status: mainlineState?.current_main_cd_run_status ?? priorityPayload?.status ?? "--",
}),
icon: Rocket,
tone: deployReadbackResolved ? "ok" as Tone : "warn" as Tone,
},
{
key: "runtime",
label: t("proof.runtime"),
value: dbOk ? t("proof.ok") : t("proof.degraded"),
detail: t("proof.runtimeDetail", {
marker: shortRef(payload?.program_status?.deploy_readback_marker),
}),
icon: Database,
tone: dbOk ? "ok" as Tone : "warn" as Tone,
},
{
key: "work",
label: t("proof.workItems"),
value: `${numberValue(workCompleted)}/${numberValue(workTotal)}`,
detail: t("proof.workItemsDetail", {
percent: numberValue(workCompletionPercent),
}),
icon: ListChecks,
tone: workTotal > 0 && workCompleted === workTotal ? "ok" as Tone : "warn" as Tone,
},
{
key: "sources",
label: t("proof.sources"),
value: `${numberValue(rollups.live_log_active_source_family_count ?? logRollups.active_source_family_count)}/${numberValue(rollups.live_log_source_family_count ?? logRollups.source_family_count)}`,
detail: t("proof.sourcesDetail"),
icon: Bot,
tone: toNumber(rollups.live_log_active_source_family_count ?? logRollups.active_source_family_count) > 0 ? "ok" as Tone : "warn" as Tone,
},
{
key: "events",
label: t("proof.events"),
value: numberValue(rollups.live_log_classified_event_total ?? logRollups.classified_event_total),
detail: t("proof.eventsDetail", {
recent: numberValue(rollups.live_log_recent_classified_event_total ?? logRollups.recent_classified_event_total),
}),
icon: Activity,
tone: toNumber(rollups.live_log_classified_event_total ?? logRollups.classified_event_total) > 0 ? "ok" as Tone : "neutral" as Tone,
},
];
const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter));
const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length;
const orderedActive = orderedWorkItems.filter((item) => (
@@ -574,6 +672,33 @@ export function AutonomousRuntimeReceiptPanel({
</div>
</div>
<div
data-testid="ai-automation-production-proof"
className="grid gap-px border-b border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-5"
>
{proofCards.map((card) => {
const Icon = card.icon;
return (
<div key={card.key} className="bg-white px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-[#77736a]">{card.label}</p>
<p className="mt-2 truncate font-mono text-xl font-semibold text-[#141413]">
{card.value}
</p>
</div>
<span className={cn("flex h-8 w-8 shrink-0 items-center justify-center border", toneClass(card.tone))}>
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-2 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
{card.detail}
</p>
</div>
);
})}
</div>
<div className="grid grid-cols-2 gap-px bg-[#e0ddd4] md:grid-cols-5 xl:grid-cols-12">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("metrics.loop")}</p>