feat(awooop): surface source flow on overview
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 5m52s
CD Pipeline / build-and-deploy (push) Successful in 4m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m44s

This commit is contained in:
Your Name
2026-05-21 14:43:12 +08:00
parent be585c4071
commit ce3f2fed36
3 changed files with 265 additions and 1 deletions

View File

@@ -84,6 +84,27 @@ type AutomationQualitySummary = {
};
};
type SourceFlowSummary = {
source_event_total: number;
recurrence_group_total: number;
linked_run_total: number;
unlinked_event_total: number;
open_work_item_group_total?: number;
manual_gate_group_total?: number;
automation_gap_group_total?: number;
failed_repair_group_total?: number;
source_correlation_review_group_total?: number;
source_correlation_decision_recorded_group_total?: number;
source_correlation_applied_group_total?: number;
latest_received_at?: string | null;
};
type SourceFlowResponse = {
project_id: string;
limit: number;
summary: SourceFlowSummary;
};
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
const emptySnapshot: Snapshot = {
@@ -304,6 +325,171 @@ function QualityMetric({
);
}
function percentValue(numerator: number, denominator: number): number {
if (denominator <= 0) return 0;
return Math.max(0, Math.min(100, (numerator / denominator) * 100));
}
function ProgressRow({
label,
value,
detail,
percent,
tone,
}: {
label: string;
value: string;
detail: string;
percent: number;
tone: "good" | "warn" | "neutral";
}) {
return (
<div className="border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">{detail}</p>
</div>
<span className="font-mono text-sm font-semibold text-[#141413]">{value}</span>
</div>
<div className="mt-3 h-2 overflow-hidden border border-[#d8d3c7] bg-white">
<div
className={cn(
"h-full",
tone === "good" && "bg-[#6aa879]",
tone === "warn" && "bg-[#d9b36f]",
tone === "neutral" && "bg-[#9bb6d9]"
)}
style={{ width: `${percent}%` }}
/>
</div>
</div>
);
}
function SourceFlowOverviewPanel({
summary,
error,
locale,
}: {
summary: SourceFlowSummary | null;
error: boolean;
locale: string;
}) {
const t = useTranslations("awooop.home.sourceFlow");
const sourceTotal = summary?.source_event_total ?? 0;
const groupTotal = summary?.recurrence_group_total ?? 0;
const linkedRuns = summary?.linked_run_total ?? 0;
const unlinkedEvents = summary?.unlinked_event_total ?? 0;
const openWork = summary?.open_work_item_group_total ?? 0;
const automationGaps = summary?.automation_gap_group_total ?? 0;
const manualGates = summary?.manual_gate_group_total ?? 0;
const failedRepairs = summary?.failed_repair_group_total ?? 0;
const sourceReview = summary?.source_correlation_review_group_total ?? 0;
const sourceReviewRecorded = summary?.source_correlation_decision_recorded_group_total ?? 0;
const sourceApplied = summary?.source_correlation_applied_group_total ?? 0;
const sourceDecisionKnown = sourceReviewRecorded + sourceApplied;
const sourceDecisionTotal = sourceReview + sourceDecisionKnown;
const linkedPercent = percentValue(linkedRuns, sourceTotal);
const workClearPercent = percentValue(Math.max(0, groupTotal - openWork), groupTotal);
const sourceDecisionPercent = sourceDecisionTotal === 0
? 100
: percentValue(sourceDecisionKnown, sourceDecisionTotal);
const latestTime = summary?.latest_received_at
? new Date(summary.latest_received_at).toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US", {
hour: "2-digit",
minute: "2-digit",
})
: "--";
return (
<section className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex items-start gap-2">
<GitBranch className="mt-0.5 h-4 w-4 text-[#d97757]" aria-hidden="true" />
<div>
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<span
className={cn(
"inline-flex border px-2 py-0.5 text-xs font-semibold",
error && "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
!error && sourceTotal > 0 && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
!error && sourceTotal === 0 && "border-[#d8d3c7] bg-white text-[#5f5b52]"
)}
>
{error ? t("unavailable") : t("sourceEvents", { count: sourceTotal })}
</span>
</div>
{error || !summary ? (
<div className="px-4 py-5 text-sm leading-6 text-[#5f5b52]">
{error ? t("loadFailed") : t("empty")}
</div>
) : (
<div className="space-y-4 p-4">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<QualityMetric
label={t("metrics.linkedRuns")}
value={`${linkedRuns}/${sourceTotal}`}
detail={t("metrics.linkedRunsDetail", { unlinked: unlinkedEvents })}
/>
<QualityMetric
label={t("metrics.openWork")}
value={openWork}
detail={t("metrics.openWorkDetail", {
gap: automationGaps,
manual: manualGates,
failed: failedRepairs,
})}
/>
<QualityMetric
label={t("metrics.sourceDecision")}
value={
sourceDecisionTotal === 0
? t("metrics.sourceDecisionNone")
: `${sourceDecisionKnown}/${sourceDecisionTotal}`
}
detail={t("metrics.sourceDecisionDetail", { recorded: sourceReviewRecorded })}
/>
<QualityMetric
label={t("metrics.latest")}
value={latestTime}
detail={t("metrics.latestDetail", { groups: groupTotal })}
/>
</div>
<div className="grid gap-3 xl:grid-cols-3">
<ProgressRow
label={t("progress.linked")}
value={`${linkedPercent.toFixed(0)}%`}
detail={t("progress.linkedDetail")}
percent={linkedPercent}
tone={unlinkedEvents === 0 ? "good" : "warn"}
/>
<ProgressRow
label={t("progress.work")}
value={`${workClearPercent.toFixed(0)}%`}
detail={t("progress.workDetail")}
percent={workClearPercent}
tone={openWork === 0 ? "good" : "warn"}
/>
<ProgressRow
label={t("progress.decision")}
value={`${sourceDecisionPercent.toFixed(0)}%`}
detail={t("progress.decisionDetail")}
percent={sourceDecisionPercent}
tone={sourceReview === 0 ? "good" : "neutral"}
/>
</div>
</div>
)}
</section>
);
}
function AutomationQualityPanel({
summary,
error,
@@ -463,6 +649,8 @@ export default function AwoooPPage() {
const [snapshot, setSnapshot] = useState<Snapshot>(emptySnapshot);
const [qualitySummary, setQualitySummary] = useState<AutomationQualitySummary | null>(null);
const [qualityError, setQualityError] = useState(false);
const [sourceFlowSummary, setSourceFlowSummary] = useState<SourceFlowSummary | null>(null);
const [sourceFlowError, setSourceFlowError] = useState(false);
const [status, setStatus] = useState<SnapshotStatus>("loading");
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -474,13 +662,19 @@ export default function AwoooPPage() {
hours: "24",
limit: "50",
});
const [tenantRes, runRes, approvalRes, contractRes, qualityRes] = await Promise.all([
const sourceFlowParams = new URLSearchParams({
project_id: "awoooi",
limit: "100",
});
const [tenantRes, runRes, approvalRes, contractRes, qualityRes, sourceFlowRes] = await Promise.all([
fetch(`${API_BASE}/api/v1/platform/tenants`),
fetch(`${API_BASE}/api/v1/platform/runs/list?per_page=1`),
fetch(`${API_BASE}/api/v1/platform/approvals`),
fetch(`${API_BASE}/api/v1/platform/contracts?per_page=1`),
fetch(`${API_BASE}/api/v1/platform/truth-chain/quality/summary?${qualityParams.toString()}`)
.catch(() => null),
fetch(`${API_BASE}/api/v1/platform/events/dossier/recurrence?${sourceFlowParams.toString()}`)
.catch(() => null),
]);
if (![tenantRes, runRes, approvalRes, contractRes].every((res) => res.ok)) {
@@ -511,12 +705,22 @@ export default function AwoooPPage() {
setQualitySummary(null);
setQualityError(true);
}
if (sourceFlowRes?.ok) {
const sourceFlowData = await sourceFlowRes.json() as SourceFlowResponse;
setSourceFlowSummary(sourceFlowData.summary);
setSourceFlowError(false);
} else {
setSourceFlowSummary(null);
setSourceFlowError(true);
}
setLastUpdated(new Date());
setStatus("ready");
} catch {
setStatus("degraded");
setQualitySummary(null);
setQualityError(true);
setSourceFlowSummary(null);
setSourceFlowError(true);
setLastUpdated(new Date());
}
}, []);
@@ -631,6 +835,12 @@ export default function AwoooPPage() {
/>
</section>
<SourceFlowOverviewPanel
summary={sourceFlowSummary}
error={sourceFlowError}
locale={locale}
/>
<AutomationQualityPanel summary={qualitySummary} error={qualityError} />
<section className="border border-[#e0ddd4] bg-[#e0ddd4]">