feat(awooop): expose ai automation log taxonomy
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
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 15:21:17 +08:00
parent 8f0c6d3002
commit fe42ed1b43
10 changed files with 1389 additions and 2 deletions

View File

@@ -0,0 +1,454 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import {
Activity,
Bot,
BookOpenCheck,
CheckCircle2,
RefreshCw,
Send,
ShieldCheck,
TriangleAlert,
Wrench,
} from "lucide-react";
import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base";
import { cn } from "@/lib/utils";
type RuntimeReceiptReadback = {
db_read_status?: string | null;
ansible_apply_executed?: {
total?: number | null;
recent?: number | null;
} | null;
auto_repair_execution_receipt?: {
total?: number | null;
recent?: number | null;
} | null;
post_apply_verifier?: {
total?: number | null;
recent?: number | null;
} | null;
km_writeback?: {
total?: number | null;
recent?: number | null;
} | null;
telegram_receipt?: {
total?: number | null;
recent?: number | null;
} | null;
mcp_context?: {
total?: number | null;
recent?: number | null;
} | null;
service_log_evidence?: {
total?: number | null;
recent?: number | null;
} | null;
executor_log_projection?: {
total?: number | null;
recent?: number | null;
} | null;
playbook_trust?: {
total?: number | null;
recent?: number | null;
} | null;
trace_ledger?: {
stage_count?: number | null;
recorded_stage_count?: number | null;
missing_required_stage_ids?: string[] | null;
learning_source_stage_ids?: string[] | null;
public_safety?: {
reads_raw_sessions?: boolean | null;
stores_secret_values?: boolean | null;
stores_unredacted_telegram_payload?: boolean | null;
stores_internal_reasoning?: boolean | null;
} | null;
stages?: Array<{
stage_id?: string | null;
display_name?: string | null;
recorded?: boolean | null;
total?: number | null;
recent?: number | null;
required_for_closed_loop?: boolean | null;
feeds_learning?: boolean | null;
next_action_if_missing?: string | null;
}> | null;
} | null;
log_integration_taxonomy?: {
label_dimensions?: string[] | null;
rollups?: {
source_family_count?: number | null;
active_source_family_count?: number | null;
label_dimension_count?: number | null;
classified_event_total?: number | null;
recent_classified_event_total?: number | null;
learning_source_family_count?: number | null;
} | null;
} | null;
latest_flow_closure?: {
apply_op_id?: string | null;
incident_id?: string | null;
has_post_apply_verifier?: boolean | null;
has_km_writeback?: boolean | null;
has_telegram_receipt?: boolean | null;
closed?: boolean | null;
missing?: string[] | null;
} | null;
autonomous_execution_loop_ledger?: {
execution_state?: string | null;
closed?: boolean | null;
missing_stage_ids?: string[] | null;
next_executor_action?: string | null;
operation_id?: string | null;
incident_id?: string | null;
catalog_id?: string | null;
} | null;
};
type RuntimeControlPayload = {
program_status?: {
implementation_completion_percent?: number | null;
deploy_readback_marker?: string | null;
} | null;
current_policy?: {
low_risk_controlled_apply_allowed?: boolean | null;
medium_risk_controlled_apply_allowed?: boolean | null;
high_risk_controlled_apply_allowed?: boolean | null;
critical_break_glass_required?: boolean | null;
} | null;
runtime_receipt_readback?: RuntimeReceiptReadback | null;
rollups?: Record<string, number | string | boolean | null | undefined> | null;
};
type PanelMode = "full" | "compact";
type Tone = "ok" | "warn" | "neutral";
const API_BASE = getRuntimeApiBaseUrl();
function numberValue(value: unknown): string {
const numeric = Number(value ?? 0);
if (!Number.isFinite(numeric)) return "0";
return new Intl.NumberFormat("zh-TW").format(numeric);
}
function toNumber(value: unknown): number {
const numeric = Number(value ?? 0);
return Number.isFinite(numeric) ? numeric : 0;
}
function shortRef(value?: string | null): string {
if (!value) return "--";
return value.length > 12 ? `${value.slice(0, 8)}...` : value;
}
function toneClass(tone: Tone): string {
if (tone === "ok") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
if (tone === "warn") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
}
function formatTime(value: Date | null, locale: string): string {
if (!value) return "--";
return value.toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
async function fetchRuntimeControl(): Promise<RuntimeControlPayload | null> {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 12_000);
try {
const response = await fetch(`${API_BASE}/api/v1/agents/agent-autonomous-runtime-control`, {
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) return null;
return (await response.json()) as RuntimeControlPayload;
} catch {
return null;
} finally {
window.clearTimeout(timeout);
}
}
export function AutonomousRuntimeReceiptPanel({
mode = "full",
}: {
mode?: PanelMode;
}) {
const t = useTranslations("awooop.autonomousRuntime");
const locale = useLocale();
const [payload, setPayload] = useState<RuntimeControlPayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
const next = await fetchRuntimeControl();
setPayload(next);
setError(next === null);
setUpdatedAt(next ? new Date() : null);
setLoading(false);
}, []);
useEffect(() => {
refresh();
const timer = window.setInterval(refresh, 30_000);
return () => window.clearInterval(timer);
}, [refresh]);
const readback = payload?.runtime_receipt_readback;
const ledger = readback?.autonomous_execution_loop_ledger;
const traceLedger = readback?.trace_ledger;
const logTaxonomy = readback?.log_integration_taxonomy;
const logRollups = logTaxonomy?.rollups ?? {};
const latestFlow = readback?.latest_flow_closure;
const rollups = payload?.rollups ?? {};
const closed = ledger?.closed === true || latestFlow?.closed === true;
const dbOk = readback?.db_read_status === "ok";
const missingStages = traceLedger?.missing_required_stage_ids
?? ledger?.missing_stage_ids
?? latestFlow?.missing
?? [];
const tone: Tone = error || !dbOk ? "warn" : closed ? "ok" : "neutral";
const stateLabel = closed
? t("states.closed")
: error
? t("states.unavailable")
: dbOk
? t("states.open")
: t("states.degraded");
const metrics = useMemo(
() => [
{
key: "trace",
label: t("metrics.trace"),
value: toNumber(rollups.live_trace_recorded_stage_count ?? traceLedger?.recorded_stage_count),
recent: toNumber(traceLedger?.stage_count),
icon: CheckCircle2,
caption: t("traceCaption", {
count: numberValue(traceLedger?.stage_count ?? 0),
missing: numberValue(missingStages.length),
}),
},
{
key: "mcp",
label: t("metrics.mcp"),
value: toNumber(rollups.live_mcp_context_count ?? readback?.mcp_context?.total),
recent: toNumber(readback?.mcp_context?.recent),
icon: Bot,
},
{
key: "logs",
label: t("metrics.logs"),
value: toNumber(
rollups.live_log_classified_event_total
?? logRollups.classified_event_total
?? (
toNumber(rollups.live_service_log_evidence_count ?? readback?.service_log_evidence?.total)
+ toNumber(rollups.live_executor_log_projection_count ?? readback?.executor_log_projection?.total)
)
),
recent: toNumber(readback?.service_log_evidence?.recent)
+ toNumber(readback?.executor_log_projection?.recent),
icon: Activity,
},
{
key: "apply",
label: t("metrics.apply"),
value: toNumber(rollups.live_ansible_apply_executed_count ?? readback?.ansible_apply_executed?.total),
recent: toNumber(readback?.ansible_apply_executed?.recent),
icon: Wrench,
},
{
key: "receipt",
label: t("metrics.receipt"),
value: toNumber(rollups.live_auto_repair_execution_receipt_count ?? readback?.auto_repair_execution_receipt?.total),
recent: toNumber(readback?.auto_repair_execution_receipt?.recent),
icon: Activity,
},
{
key: "verifier",
label: t("metrics.verifier"),
value: toNumber(rollups.live_post_apply_verifier_count ?? readback?.post_apply_verifier?.total),
recent: toNumber(readback?.post_apply_verifier?.recent),
icon: ShieldCheck,
},
{
key: "km",
label: t("metrics.km"),
value: toNumber(rollups.live_km_writeback_count ?? readback?.km_writeback?.total),
recent: toNumber(readback?.km_writeback?.recent),
icon: BookOpenCheck,
},
{
key: "playbook",
label: t("metrics.playbook"),
value: toNumber(rollups.live_playbook_trust_signal_count ?? readback?.playbook_trust?.total),
recent: toNumber(readback?.playbook_trust?.recent),
icon: ShieldCheck,
},
{
key: "telegram",
label: t("metrics.telegram"),
value: toNumber(rollups.live_telegram_receipt_count ?? readback?.telegram_receipt?.total),
recent: toNumber(readback?.telegram_receipt?.recent),
icon: Send,
},
],
[logRollups.classified_event_total, missingStages.length, readback, rollups, t, traceLedger]
);
const policy = payload?.current_policy;
const riskText = [
policy?.low_risk_controlled_apply_allowed ? t("risk.low") : null,
policy?.medium_risk_controlled_apply_allowed ? t("risk.medium") : null,
policy?.high_risk_controlled_apply_allowed ? t("risk.high") : null,
].filter(Boolean).join(" / ");
return (
<section className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<span className={cn("flex h-9 w-9 shrink-0 items-center justify-center border", toneClass(tone))}>
<Bot className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", toneClass(tone))}>
{stateLabel}
</span>
<span className="font-mono text-xs text-[#77736a]">
{t("completion", {
percent: numberValue(payload?.program_status?.implementation_completion_percent ?? 0),
})}
</span>
</div>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("detail", {
incident: ledger?.incident_id ?? latestFlow?.incident_id ?? "--",
op: shortRef(ledger?.operation_id ?? latestFlow?.apply_op_id),
catalog: ledger?.catalog_id ?? "--",
})}
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-xs text-[#77736a]">
{formatTime(updatedAt, locale)}
</span>
<button
type="button"
onClick={refresh}
disabled={loading}
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-2 text-xs font-semibold text-[#141413] hover:border-[#d97757] disabled:opacity-60"
aria-label={t("refresh")}
>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} aria-hidden="true" />
{t("refresh")}
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-px bg-[#e0ddd4] md:grid-cols-5 xl:grid-cols-10">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("metrics.loop")}</p>
<div className="mt-2 flex items-center gap-2">
{closed ? (
<CheckCircle2 className="h-4 w-4 text-[#17602a]" aria-hidden="true" />
) : (
<TriangleAlert className="h-4 w-4 text-[#8a5a08]" aria-hidden="true" />
)}
<span className="font-mono text-lg font-semibold text-[#141413]">
{closed ? "1" : "0"}
</span>
</div>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{missingStages.length > 0
? t("missing", { count: missingStages.length })
: t("closedDetail")}
</p>
</div>
{metrics.map((metric) => {
const Icon = metric.icon;
return (
<div key={metric.key} className="bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold text-[#77736a]">{metric.label}</p>
<div className="mt-2 font-mono text-lg font-semibold text-[#141413]">
{numberValue(metric.value)}
</div>
</div>
<Icon className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
</div>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{"caption" in metric && metric.caption
? metric.caption
: t("recent", { count: numberValue(metric.recent) })}
</p>
</div>
);
})}
</div>
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-4">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("taxonomy.sources")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{numberValue(rollups.live_log_active_source_family_count ?? logRollups.active_source_family_count)}
{" / "}
{numberValue(rollups.live_log_source_family_count ?? logRollups.source_family_count)}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("taxonomy.labels")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{numberValue(rollups.live_log_label_dimension_count ?? logRollups.label_dimension_count)}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("taxonomy.events")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{numberValue(rollups.live_log_classified_event_total ?? logRollups.classified_event_total)}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("taxonomy.learning")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{numberValue(logRollups.learning_source_family_count)}
</p>
</div>
</div>
{mode === "full" ? (
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.label")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">{riskText || "--"}</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.critical")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("nextAction")}</p>
<p className="mt-1 truncate text-sm font-semibold text-[#141413]">
{ledger?.next_executor_action ?? "--"}
</p>
</div>
</div>
) : null}
</section>
);
}