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
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:
@@ -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 受控處置判讀",
|
||||
|
||||
@@ -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 受控處置判讀",
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user