feat(web): surface log automation mainline
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 21s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-29 23:53:18 +08:00
parent 01d9176142
commit ed32c7123d
3 changed files with 397 additions and 2 deletions

View File

@@ -11765,6 +11765,64 @@
"expired": "已逾時",
"expiredDetail": "改排 AI retry / rollback / verifier不預設人工結案"
},
"logAutomation": {
"eyebrow": "LOG / KM / RAG / MCP / PlayBook Mainline",
"title": "AI Automation Writeback And Executor Queue",
"subtitle": "Reads the production executor readback directly and connects service log labels, feedback receipts, post-write verifier, controlled plan, and AI Agent next action queue into one trackable mainline.",
"refresh": "Refresh mainline",
"loadFailed": "AI automation mainline readback failed to load",
"emptyBatches": "No executor batches returned yet",
"boundary": "This panel shows the low / medium / high metadata writeback queue available to AI controlled apply. Runtime dispatch truth still comes from production readback; critical remains break-glass.",
"errorDetail": "AI automation mainline read failed: {error}",
"status": {
"loading": "Loading mainline",
"ready": "executor ready",
"waiting": "waiting readback",
"error": "readback failed"
},
"metrics": {
"plans": "Writeback plans",
"plansDetail": "Verified KM / RAG / MCP / PlayBook / Agent plans.",
"batches": "Executor batches",
"batchesDetail": "ready / total batches consumable by AI Agent controlled apply.",
"selectors": "Target selectors",
"selectorsDetail": "Every writeback binds project, service, package, and tool.",
"verifiers": "Post-apply verifiers",
"verifiersDetail": "Every target batch must carry a verifier ref."
},
"stages": {
"logs": "LOG labels",
"receipts": "Feedback receipts",
"verifier": "Post-write verifier",
"plan": "Controlled plan",
"executor": "Executor queue"
},
"targets": {
"km": "KM",
"rag": "RAG",
"playbook": "PlayBook",
"mcp": "MCP",
"verifier": "Verifier",
"ai_agent": "AI Agent",
"unknown": "Unknown"
},
"policy": {
"controlledApply": "L/M/H controlled apply",
"ownerReview": "L/M/H owner review",
"critical": "Critical",
"runtime": "Runtime dispatch",
"queue": "Next action queue",
"evidence": "Evidence chain"
},
"values": {
"on": "on",
"off": "off",
"required": "required",
"notRequired": "not required",
"performed": "performed",
"notPerformed": "not performed"
}
},
"decisionRail": {
"eyebrow": "審批決策 Rail",
"title": "卡住的批准與 AI 受控處置判讀",

View File

@@ -11765,6 +11765,64 @@
"expired": "已逾時",
"expiredDetail": "改排 AI retry / rollback / verifier不預設人工結案"
},
"logAutomation": {
"eyebrow": "LOG / KM / RAG / MCP / PlayBook 主線",
"title": "AI 自動化回寫與執行器隊列",
"subtitle": "直接讀取正式 API 的 executor readback把服務日誌貼標、feedback receipt、post-write verifier、controlled plan 與 AI Agent next action queue 串成同一條可追蹤主線。",
"refresh": "刷新主線",
"loadFailed": "AI 自動化主線 readback 載入失敗",
"emptyBatches": "尚未取得 executor batch",
"boundary": "此面板顯示 AI controlled apply 已具備的低 / 中 / 高風險 metadata writeback 隊列runtime dispatch 是否已執行仍以正式 readback 為準critical 維持 break-glass。",
"errorDetail": "AI 自動化主線讀取失敗:{error}",
"status": {
"loading": "讀取主線",
"ready": "executor ready",
"waiting": "等待 readback",
"error": "readback failed"
},
"metrics": {
"plans": "回寫計畫",
"plansDetail": "已驗證的 KM / RAG / MCP / PlayBook / Agent plan。",
"batches": "Executor 批次",
"batchesDetail": "ready / total供 AI Agent controlled apply 消費。",
"selectors": "目標 selector",
"selectorsDetail": "每筆回寫都要綁 project、service、package、tool。",
"verifiers": "Post-apply verifier",
"verifiersDetail": "每個 target batch 都必須有 verifier ref。"
},
"stages": {
"logs": "LOG 貼標",
"receipts": "Feedback receipts",
"verifier": "Post-write verifier",
"plan": "Controlled plan",
"executor": "Executor queue"
},
"targets": {
"km": "KM",
"rag": "RAG",
"playbook": "PlayBook",
"mcp": "MCP",
"verifier": "Verifier",
"ai_agent": "AI Agent",
"unknown": "Unknown"
},
"policy": {
"controlledApply": "低中高風險 controlled apply",
"ownerReview": "低中高 owner review",
"critical": "Critical",
"runtime": "Runtime dispatch",
"queue": "Next action queue",
"evidence": "Evidence chain"
},
"values": {
"on": "開啟",
"off": "關閉",
"required": "必要",
"notRequired": "不必要",
"performed": "已執行",
"notPerformed": "未執行"
}
},
"decisionRail": {
"eyebrow": "審批決策 Rail",
"title": "卡住的批准與 AI 受控處置判讀",

View File

@@ -108,6 +108,76 @@ interface LegacyApproval {
required_signatures?: number | null;
}
interface LogWritebackExecutionBatch {
batch_id?: string | null;
target?: string | null;
target_surface?: string | null;
risk_tier?: string | null;
executor_route?: string | null;
status?: string | null;
apply_mode?: string | null;
dispatch_enabled_by_policy?: boolean | null;
plan_count?: number | null;
target_selector_count?: number | null;
source_of_truth_diff_count?: number | null;
runtime_dispatch_performed?: boolean | null;
check_mode?: {
enabled?: boolean | null;
required?: boolean | null;
} | null;
rollback?: {
required?: boolean | null;
rollback_refs?: string[] | null;
} | null;
post_apply_verifier?: {
required?: boolean | null;
verifier_refs?: string[] | null;
} | null;
}
interface LogWritebackExecutorReadback {
schema_version?: string | null;
status?: string | null;
active_blockers?: string[] | null;
readback?: {
source_status?: string | null;
safe_next_step?: string | null;
} | null;
executor_policy?: {
executor_route?: string | null;
low_medium_high_controlled_apply_enabled?: boolean | null;
owner_review_required_for_low_medium_high?: boolean | null;
critical_break_glass_required?: boolean | null;
} | null;
execution_batches?: LogWritebackExecutionBatch[] | null;
agent_consumption_context?: {
context_id?: string | null;
consumable_by?: string[] | null;
evidence_chain?: string[] | null;
next_action_queue?: Array<{
batch_id?: string | null;
target?: string | null;
plan_count?: number | null;
}> | null;
learning_feedback_targets?: string[] | null;
raw_payload_required?: boolean | null;
} | null;
rollups?: {
source_writeback_plan_count?: number | null;
execution_batch_count?: number | null;
ready_execution_batch_count?: number | null;
target_count?: number | null;
target_selector_count?: number | null;
source_of_truth_diff_count?: number | null;
check_mode_ready_count?: number | null;
rollback_ready_count?: number | null;
post_apply_verifier_ready_count?: number | null;
controlled_executor_dispatch_ready?: boolean | null;
controlled_apply_enabled_by_policy?: boolean | null;
runtime_dispatch_performed?: boolean | null;
} | null;
}
// =============================================================================
// 常數
// =============================================================================
@@ -388,6 +458,181 @@ function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | n
);
}
function LogAutomationMainlinePanel({
readback,
loading,
error,
onRefresh,
}: {
readback: LogWritebackExecutorReadback | null;
loading: boolean;
error: string | null;
onRefresh: () => void;
}) {
const t = useTranslations("awooop.approvals.logAutomation");
const rollups = readback?.rollups ?? {};
const policy = readback?.executor_policy ?? {};
const context = readback?.agent_consumption_context ?? {};
const batches = readback?.execution_batches ?? [];
const blockers = readback?.active_blockers ?? [];
const ready = readback?.status === "controlled_writeback_executor_ready" && blockers.length === 0;
const statusText = loading
? t("status.loading")
: error
? t("status.error")
: ready
? t("status.ready")
: t("status.waiting");
const statusClass = ready
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
: error
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
const metrics = [
{
label: t("metrics.plans"),
value: rollups.source_writeback_plan_count ?? "--",
detail: t("metrics.plansDetail"),
icon: GitBranch,
},
{
label: t("metrics.batches"),
value: `${rollups.ready_execution_batch_count ?? 0}/${rollups.execution_batch_count ?? 0}`,
detail: t("metrics.batchesDetail"),
icon: ListChecks,
},
{
label: t("metrics.selectors"),
value: rollups.target_selector_count ?? "--",
detail: t("metrics.selectorsDetail"),
icon: SearchCheck,
},
{
label: t("metrics.verifiers"),
value: rollups.post_apply_verifier_ready_count ?? "--",
detail: t("metrics.verifiersDetail"),
icon: ShieldCheck,
},
];
const stages = [
{ key: "logs", value: t("stages.logs") },
{ key: "receipts", value: t("stages.receipts") },
{ key: "verifier", value: t("stages.verifier") },
{ key: "plan", value: t("stages.plan") },
{ key: "executor", value: t("stages.executor") },
];
return (
<section className="border border-[#d8d3c7] bg-white" data-testid="log-automation-mainline-panel">
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#e6e2d8] bg-[#fbfaf5] px-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<span className="flex h-9 w-9 flex-shrink-0 items-center justify-center border border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]">
<GitBranch className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-[#5f5b52]">{t("eyebrow")}</p>
<h3 className="mt-1 text-base font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 max-w-4xl text-xs leading-5 text-[#5f5b52]">{t("subtitle")}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className={cn("inline-flex items-center border px-2.5 py-1 text-xs font-semibold", statusClass)}>
{statusText}
</span>
<button
type="button"
onClick={onRefresh}
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-1.5 text-xs font-semibold text-[#3f3b34] transition hover:bg-[#f4f1e8]"
>
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} aria-hidden="true" />
{t("refresh")}
</button>
</div>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
{metrics.map((item) => {
const Icon = item.icon;
return (
<div key={item.label} className="min-w-0 bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 font-mono text-2xl font-semibold text-[#141413]">{item.value}</p>
</div>
<Icon className="h-4 w-4 text-[#1f5b9b]" aria-hidden="true" />
</div>
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
</div>
);
})}
</div>
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-[1.1fr_1fr]">
<div className="bg-white p-4">
<div className="flex flex-wrap gap-2">
{stages.map((stage, index) => (
<span
key={stage.key}
className={cn(
"inline-flex items-center gap-2 border px-2.5 py-1 text-xs font-semibold",
ready ? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]" : "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"
)}
>
<span className="font-mono text-[11px]">{index + 1}</span>
{stage.value}
</span>
))}
</div>
<div className="mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
{(batches.length ? batches : []).map((batch) => (
<div key={batch.batch_id ?? batch.target ?? "batch"} className="border border-[#e0ddd4] bg-[#fbfaf5] px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-semibold text-[#141413]">
{t(`targets.${batch.target ?? "unknown"}`)}
</p>
<span className="font-mono text-[11px] text-[#5f5b52]">{batch.plan_count ?? 0}</span>
</div>
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]" title={batch.status ?? "--"}>
{batch.status ?? "--"}
</p>
</div>
))}
{!loading && batches.length === 0 ? (
<div className="border border-[#e0ddd4] bg-[#fbfaf5] px-3 py-2 text-xs text-[#5f5b52]">
{t("emptyBatches")}
</div>
) : null}
</div>
</div>
<div className="bg-white p-4">
<div className="grid gap-2 sm:grid-cols-2">
{[
[t("policy.controlledApply"), policy.low_medium_high_controlled_apply_enabled ? t("values.on") : t("values.off")],
[t("policy.ownerReview"), policy.owner_review_required_for_low_medium_high ? t("values.required") : t("values.notRequired")],
[t("policy.critical"), policy.critical_break_glass_required ? t("values.required") : t("values.off")],
[t("policy.runtime"), rollups.runtime_dispatch_performed ? t("values.performed") : t("values.notPerformed")],
[t("policy.queue"), String(context.next_action_queue?.length ?? 0)],
[t("policy.evidence"), String(context.evidence_chain?.length ?? 0)],
].map(([label, value]) => (
<div key={label} className="min-w-0 border border-[#e0ddd4] bg-[#fbfaf5] px-3 py-2">
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
<p className="mt-1 truncate font-mono text-xs text-[#141413]" title={String(value)}>
{value}
</p>
</div>
))}
</div>
<p className="mt-3 text-xs leading-5 text-[#5f5b52]">
{error ? t("errorDetail", { error }) : t("boundary")}
</p>
</div>
</div>
</section>
);
}
function LegacyHitlBacklogPanel({
approvals,
loading,
@@ -1675,10 +1920,28 @@ export default function ApprovalsPage() {
const [incidentTimeline, setIncidentTimeline] = useState<IncidentTimelineResponse | null>(null);
const [incidentLoading, setIncidentLoading] = useState(false);
const [incidentError, setIncidentError] = useState<string | null>(null);
const [logAutomationReadback, setLogAutomationReadback] = useState<LogWritebackExecutorReadback | null>(null);
const [logAutomationLoading, setLogAutomationLoading] = useState(true);
const [logAutomationError, setLogAutomationError] = useState<string | null>(null);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
const [evidenceFilter, setEvidenceFilter] = useState<"" | RemediationStatus>("");
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fetchLogAutomationMainline = useCallback(async () => {
setLogAutomationLoading(true);
setLogAutomationError(null);
const payload = await fetchJson<LogWritebackExecutorReadback>(
`${API_BASE}/api/v1/agents/agent-log-controlled-writeback-executor-readback`
);
if (payload) {
setLogAutomationReadback(payload);
} else {
setLogAutomationReadback(null);
setLogAutomationError(t("logAutomation.loadFailed"));
}
setLogAutomationLoading(false);
}, [t]);
const fetchApprovals = useCallback(async () => {
const fetchJson = async (url: string) => {
const res = await fetch(url);
@@ -1727,6 +1990,10 @@ export default function ApprovalsPage() {
fetchApprovals();
}, [fetchApprovals]);
useEffect(() => {
fetchLogAutomationMainline();
}, [fetchLogAutomationMainline]);
useEffect(() => {
let cancelled = false;
if (!queryIncidentId) {
@@ -1772,11 +2039,12 @@ export default function ApprovalsPage() {
useEffect(() => {
intervalRef.current = setInterval(() => {
fetchApprovals();
fetchLogAutomationMainline();
}, AUTO_REFRESH_INTERVAL);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [fetchApprovals]);
}, [fetchApprovals, fetchLogAutomationMainline]);
const legacyPendingCount = legacyApprovals.length;
const totalPendingCount = approvals.length + legacyPendingCount;
@@ -1887,7 +2155,11 @@ export default function ApprovalsPage() {
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t("page.autoRefresh")}</span>
<button
onClick={() => { setLoading(true); fetchApprovals(); }}
onClick={() => {
setLoading(true);
fetchApprovals();
fetchLogAutomationMainline();
}}
disabled={loading}
className="flex items-center gap-2 rounded-button px-3 py-2 text-sm text-muted-foreground transition hover:bg-accent hover:text-foreground disabled:opacity-50"
aria-label={t("page.refreshNow")}
@@ -1927,6 +2199,13 @@ export default function ApprovalsPage() {
<AutonomousRuntimeReceiptPanel mode="compact" />
<LogAutomationMainlinePanel
readback={logAutomationReadback}
loading={logAutomationLoading}
error={logAutomationError}
onRefresh={fetchLogAutomationMainline}
/>
<ApprovalDecisionRail
approvals={approvals}
legacyApprovals={legacyApprovals}