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
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user