diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9822e0f1..b6626b5b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -525,6 +525,57 @@ } } }, + "liveEvidence": { + "title": "Live Evidence", + "realtime": "Live read", + "fields": { + "metric": "Metric", + "detail": "Detail", + "source": "Read Source" + }, + "sources": { + "dossierCoverage": "/api/v1/platform/events/dossier/coverage", + "runsAndCicd": "/api/v1/platform/runs/list + /api/v1/platform/cicd/events", + "aiRouteStatus": "/api/v1/platform/ai-route-status", + "runsAndStatusChain": "/api/v1/platform/runs/list + /api/v1/platform/status-chain", + "qualityAndRecurrence": "/api/v1/platform/truth-chain/quality/summary + /api/v1/platform/events/dossier/recurrence", + "truthChainQuality": "/api/v1/platform/truth-chain/quality/summary", + "approvalsAndQuality": "approval store + /api/v1/platform/truth-chain/quality/summary", + "kmBurndown": "/api/v1/ai/governance/km-stale-owner-review-burndown" + }, + "signal": { + "metric": "sources {sources} / refs {refs}", + "detail": "missing refs {missing}, duplicates {duplicates}; Alert {alert} / Sentry {sentry} / SigNoz {signoz}" + }, + "intake": { + "metric": "Runs {runs} / linked {linked}", + "detail": "latest CI/CD {stage}:{status}, commit {commit}, needs attention {attention}" + }, + "ai": { + "metric": "{lane} / {provider}", + "detail": "skipped lanes {skipped}, operator action={action}, reason={reason}" + }, + "mcp": { + "metric": "MCP observations {observations} / gateway {gateway}", + "detail": "success {success}, failed {failed}, server={server}, route={route}" + }, + "playbook": { + "metric": "gate {gate} / automation gaps {gaps}", + "detail": "open work items {workItems}, verified groups {verifiedGroups}, auto-repair linked {linkedAutoRepair}" + }, + "ansible": { + "metric": "check-mode {checkMode} / pending {pending}", + "detail": "blocker={blocker}, candidates={candidates}, operations={operations}" + }, + "approval": { + "metric": "pending {pending} / verified {verified}/{evaluated}", + "detail": "human gates {humanGates}, auto-repair records {autoRepairRecords}, operation records {operations}" + }, + "verify": { + "metric": "stale {stale} / ratio {ratio}", + "detail": "owner review pending {pending}, completed {completed}, remaining to threshold {remaining}" + } + }, "values": { "verified": "verified {verified}/{evaluated}", "topGate": "{gate} missing {count}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7f4c294c..b700c27a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -526,6 +526,57 @@ } } }, + "liveEvidence": { + "title": "Live Evidence", + "realtime": "即時讀取", + "fields": { + "metric": "指標", + "detail": "細節", + "source": "讀取來源" + }, + "sources": { + "dossierCoverage": "/api/v1/platform/events/dossier/coverage", + "runsAndCicd": "/api/v1/platform/runs/list + /api/v1/platform/cicd/events", + "aiRouteStatus": "/api/v1/platform/ai-route-status", + "runsAndStatusChain": "/api/v1/platform/runs/list + /api/v1/platform/status-chain", + "qualityAndRecurrence": "/api/v1/platform/truth-chain/quality/summary + /api/v1/platform/events/dossier/recurrence", + "truthChainQuality": "/api/v1/platform/truth-chain/quality/summary", + "approvalsAndQuality": "approval store + /api/v1/platform/truth-chain/quality/summary", + "kmBurndown": "/api/v1/ai/governance/km-stale-owner-review-burndown" + }, + "signal": { + "metric": "來源 {sources} / refs {refs}", + "detail": "missing refs {missing},duplicates {duplicates};Alert {alert} / Sentry {sentry} / SigNoz {signoz}" + }, + "intake": { + "metric": "Runs {runs} / linked {linked}", + "detail": "最新 CI/CD {stage}:{status},commit {commit},需注意 {attention}" + }, + "ai": { + "metric": "{lane} / {provider}", + "detail": "skipped lanes {skipped},operator action={action},reason={reason}" + }, + "mcp": { + "metric": "MCP observations {observations} / gateway {gateway}", + "detail": "success {success},failed {failed},server={server},route={route}" + }, + "playbook": { + "metric": "gate {gate} / automation gaps {gaps}", + "detail": "open work items {workItems},verified groups {verifiedGroups},auto-repair linked {linkedAutoRepair}" + }, + "ansible": { + "metric": "check-mode {checkMode} / pending {pending}", + "detail": "blocker={blocker},candidates={candidates},operations={operations}" + }, + "approval": { + "metric": "pending {pending} / verified {verified}/{evaluated}", + "detail": "human gates {humanGates},auto-repair records {autoRepairRecords},operation records {operations}" + }, + "verify": { + "metric": "stale {stale} / ratio {ratio}", + "detail": "owner review pending {pending},completed {completed},距離門檻剩 {remaining}" + } + }, "values": { "verified": "verified {verified}/{evaluated}", "topGate": "{gate} 缺 {count}", diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index ea5e5d76..9e305b63 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -104,10 +104,97 @@ interface HomepageKmStaleCandidatesResponse { threshold_days?: number } +interface HomepageDossierCoverageResponse { + summary?: { + source_count?: number + source_ref_total?: number + missing_source_refs_total?: number + duplicate_total?: number + alert_ref_total?: number + sentry_ref_total?: number + signoz_ref_total?: number + latest_received_at?: string | null + } +} + +interface HomepageEventRecurrenceResponse { + summary?: { + source_event_total?: number + linked_run_total?: number + unlinked_event_total?: number + auto_repair_linked_total?: number + verified_repair_group_total?: number + open_work_item_group_total?: number + automation_gap_group_total?: number + recurrent_group_total?: number + recurrence_group_total?: number + duplicate_event_total?: number + latest_received_at?: string | null + } +} + +interface HomepageRunSummary { + run_id?: string + state?: string + created_at?: string | null + remediation_summary?: { + status?: string | null + evidence_total?: number | null + mcp_observation_total?: number | null + mcp_observation_success?: number | null + mcp_observation_failed?: number | null + latest_mcp_server?: string | null + latest_route?: string | null + has_mcp_investigation?: boolean | null + has_dry_run?: boolean | null + human_gate_open?: boolean | null + } | null +} + +interface HomepageRunsListResponse { + runs?: HomepageRunSummary[] + total?: number +} + +interface HomepageCicdEventsResponse { + total?: number + items?: Array<{ + stage?: string | null + status?: string | null + summary?: string | null + commit_sha?: string | null + needs_attention?: boolean | null + created_at?: string | null + }> +} + +interface HomepageKmOwnerReviewBurndownResponse { + burn_down_status?: string | null + current_snapshot?: { + stale_count?: number + total_count?: number + stale_ratio?: number + threshold?: number + stale_days?: number + } | null + entries_to_threshold?: number | null + pending_owner_reviews?: number | null + completed_owner_reviews?: number | null + completion_audit_total?: number | null + stale_ratio_recheck_total?: number | null + manual_review_required?: boolean | null + generated_at?: string | null +} + interface HomepageAutomationBriefSnapshot { callbackReplies?: HomepageCallbackRepliesResponse | null aiRouteStatus?: HomepageAiRouteStatusResponse | null kmStaleCandidates?: HomepageKmStaleCandidatesResponse | null + dossierCoverage?: HomepageDossierCoverageResponse | null + eventRecurrence?: HomepageEventRecurrenceResponse | null + runsList?: HomepageRunsListResponse | null + cicdEvents?: HomepageCicdEventsResponse | null + kmOwnerReviewBurndown?: HomepageKmOwnerReviewBurndownResponse | null } type HomepageWorkTone = 'live' | 'progress' | 'blocked' | 'watching' @@ -125,6 +212,12 @@ interface HomepageBlueprintStage extends HomepageWorkItemSummary { owner: string evidence: string nextAction: string + liveEvidence: { + metric: string + detail: string + source: string + updated: string + } } async function fetchHomepageJson(url: string, signal?: AbortSignal): Promise { @@ -689,8 +782,46 @@ export default function Home({ params }: { params: { locale: string } }) { `${API_BASE}/api/v1/ai/governance/km-stale-candidates?project_id=${encodedProjectId}&limit=5`, controller.signal ), - ]).then(([callbackReplies, aiRouteStatus, kmStaleCandidates]) => { - setAutomationBrief({ callbackReplies, aiRouteStatus, kmStaleCandidates }) + fetchHomepageJson( + `${API_BASE}/api/v1/platform/events/dossier/coverage?project_id=${encodedProjectId}&limit=100`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/platform/events/dossier/recurrence?project_id=${encodedProjectId}&limit=100`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/platform/runs/list?project_id=${encodedProjectId}&per_page=25`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/platform/cicd/events?project_id=${encodedProjectId}&limit=12`, + controller.signal + ), + fetchHomepageJson( + `${API_BASE}/api/v1/ai/governance/km-stale-owner-review-burndown?project_id=${encodedProjectId}&limit=5`, + controller.signal + ), + ]).then(([ + callbackReplies, + aiRouteStatus, + kmStaleCandidates, + dossierCoverage, + eventRecurrence, + runsList, + cicdEvents, + kmOwnerReviewBurndown, + ]) => { + setAutomationBrief({ + callbackReplies, + aiRouteStatus, + kmStaleCandidates, + dossierCoverage, + eventRecurrence, + runsList, + cicdEvents, + kmOwnerReviewBurndown, + }) }).catch(() => {}) .finally(() => setAutomationBriefLoaded(true)) return () => controller.abort() @@ -701,6 +832,17 @@ export default function Home({ params }: { params: { locale: string } }) { const unavailableValue = tDashboard('automationDelivery.unavailableValue') const loadingStatus = tDashboard('automationDelivery.status.loading') const unavailableStatus = tDashboard('automationDelivery.status.unavailable') + const formatLiveEvidenceTime = (value: string | null | undefined) => { + if (!value) return automationBriefLoaded ? unavailableValue : loadingStatus + const date = new Date(value) + if (Number.isNaN(date.getTime())) return unavailableValue + return date.toLocaleString(locale === 'en' ? 'en-US' : 'zh-TW', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } const evaluatedAutomationTotal = automationQuality?.evaluated_total const verifiedAutomationTotal = automationQuality?.verified_auto_repair_total const formatAutomationNumber = (value: number | null | undefined, loaded: boolean) => { @@ -753,6 +895,66 @@ export default function Home({ params }: { params: { locale: string } }) { const hasAiRouteStatus = automationBrief.aiRouteStatus !== undefined && automationBrief.aiRouteStatus !== null const hasKmStaleCandidates = automationBrief.kmStaleCandidates !== undefined && automationBrief.kmStaleCandidates !== null const canClaimFullAutoRepair = automationQualityAvailable && Boolean(automationQuality?.production_claim?.can_claim_full_auto_repair) + const dossierCoverageSummary = automationBrief.dossierCoverage?.summary ?? null + const recurrenceSummary = automationBrief.eventRecurrence?.summary ?? null + const recentRuns = automationBrief.runsList?.runs ?? [] + const latestRun = recentRuns[0] + const runEvidenceSummary = recentRuns.reduce( + (acc, run) => { + const summary = run.remediation_summary + acc.mcpTotal += summary?.mcp_observation_total ?? 0 + acc.mcpSuccess += summary?.mcp_observation_success ?? 0 + acc.mcpFailed += summary?.mcp_observation_failed ?? 0 + acc.evidenceTotal += summary?.evidence_total ?? 0 + if (summary?.has_mcp_investigation) acc.mcpInvestigatedRuns += 1 + if (summary?.human_gate_open) acc.humanGateRuns += 1 + if (!acc.latestMcpServer && summary?.latest_mcp_server) acc.latestMcpServer = summary.latest_mcp_server + if (!acc.latestRoute && summary?.latest_route) acc.latestRoute = summary.latest_route + return acc + }, + { + mcpTotal: 0, + mcpSuccess: 0, + mcpFailed: 0, + evidenceTotal: 0, + mcpInvestigatedRuns: 0, + humanGateRuns: 0, + latestMcpServer: null as string | null, + latestRoute: null as string | null, + } + ) + const statusChainValues = Object.values(statusChains).filter((chain): chain is NonNullable => Boolean(chain && !chain.fetch_error)) + const statusChainEvidenceSummary = statusChainValues.reduce( + (acc, chain) => { + acc.mcpGateway += chain.mcp?.gateway?.total ?? chain.evidence?.mcp_gateway_total ?? 0 + acc.mcpGatewaySuccess += chain.mcp?.gateway?.success ?? 0 + acc.mcpGatewayFailed += chain.mcp?.gateway?.failed ?? 0 + acc.operationRecords += chain.evidence?.operation_records ?? chain.execution?.operation_total ?? 0 + acc.autoRepairRecords += chain.evidence?.auto_repair_records ?? 0 + acc.knowledgeEntries += chain.evidence?.knowledge_entries ?? 0 + acc.inboundRefs += chain.source_refs?.inbound_total ?? 0 + acc.outboundRefs += chain.source_refs?.outbound_total ?? 0 + return acc + }, + { + mcpGateway: 0, + mcpGatewaySuccess: 0, + mcpGatewayFailed: 0, + operationRecords: 0, + autoRepairRecords: 0, + knowledgeEntries: 0, + inboundRefs: 0, + outboundRefs: 0, + } + ) + const cicdItems = automationBrief.cicdEvents?.items ?? [] + const latestCicdEvent = cicdItems[0] + const cicdNeedsAttention = cicdItems.filter(item => item.needs_attention).length + const kmOwnerReviewBurndown = automationBrief.kmOwnerReviewBurndown ?? null + const kmBurndownSnapshot = kmOwnerReviewBurndown?.current_snapshot ?? null + const staleRatioLabel = typeof kmBurndownSnapshot?.stale_ratio === 'number' + ? `${(kmBurndownSnapshot.stale_ratio * 100).toFixed(1)}%` + : automationBriefLoaded ? unavailableValue : loadingStatus const automationWorkToneStyle: Record = { live: { bg: '#f0faf2', border: '#9bc7a4', color: '#17602a' }, progress: { bg: '#fff7e8', border: '#d9b36f', color: '#8a5a08' }, @@ -986,6 +1188,21 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.signal.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.signal.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.signal.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.signal.metric', { + sources: formatAutomationNumber(dossierCoverageSummary?.source_count, automationBriefLoaded), + refs: formatAutomationNumber(dossierCoverageSummary?.source_ref_total, automationBriefLoaded), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.signal.detail', { + missing: formatAutomationNumber(dossierCoverageSummary?.missing_source_refs_total, automationBriefLoaded), + duplicates: formatAutomationNumber(dossierCoverageSummary?.duplicate_total, automationBriefLoaded), + alert: formatAutomationNumber(dossierCoverageSummary?.alert_ref_total, automationBriefLoaded), + sentry: formatAutomationNumber(dossierCoverageSummary?.sentry_ref_total, automationBriefLoaded), + signoz: formatAutomationNumber(dossierCoverageSummary?.signoz_ref_total, automationBriefLoaded), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.dossierCoverage'), + updated: formatLiveEvidenceTime(dossierCoverageSummary?.latest_received_at), + }, }, { key: 'intake', @@ -997,6 +1214,20 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.intake.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.intake.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.intake.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.intake.metric', { + runs: formatAutomationNumber(automationBrief.runsList?.total, automationBriefLoaded), + linked: formatAutomationNumber(recurrenceSummary?.linked_run_total, automationBriefLoaded), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.intake.detail', { + stage: latestCicdEvent?.stage ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + status: latestCicdEvent?.status ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + commit: latestCicdEvent?.commit_sha?.slice(0, 7) ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + attention: formatAutomationNumber(cicdNeedsAttention, automationBriefLoaded), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.runsAndCicd'), + updated: formatLiveEvidenceTime(latestCicdEvent?.created_at ?? latestRun?.created_at), + }, }, { key: 'ai', @@ -1011,17 +1242,49 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.ai.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.ai.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.ai.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.ai.metric', { + lane: aiRouteLaneMode, + provider: aiRouteSelectedProvider, + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.ai.detail', { + skipped: formatAutomationNumber(automationBrief.aiRouteStatus?.skipped_lanes?.length, automationBriefLoaded), + action: automationBrief.aiRouteStatus?.operator_action?.action ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + reason: automationBrief.aiRouteStatus?.operator_action?.reason ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.aiRouteStatus'), + updated: automationBriefLoaded ? tDashboard('automationDiagrams.workspace.liveEvidence.realtime') : loadingStatus, + }, }, { key: 'mcp', title: tDashboard('automationDiagrams.workspace.flow.stages.mcp'), - status: tDashboard('automationDelivery.status.watching'), - detail: tDashboard('automationDiagrams.cards.incidentFlow.detail'), + status: runEvidenceSummary.mcpTotal > 0 || statusChainEvidenceSummary.mcpGateway > 0 + ? tDashboard('automationDelivery.status.live') + : tDashboard('automationDelivery.status.watching'), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.metric', { + observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, automationBriefLoaded), + gateway: formatAutomationNumber(statusChainEvidenceSummary.mcpGateway, true), + }), href: `/${locale}/awooop/runs?project_id=awoooi`, - tone: 'watching', + tone: runEvidenceSummary.mcpFailed > 0 ? 'progress' : (runEvidenceSummary.mcpTotal > 0 || statusChainEvidenceSummary.mcpGateway > 0) ? 'live' : 'watching', owner: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.metric', { + observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, automationBriefLoaded), + gateway: formatAutomationNumber(statusChainEvidenceSummary.mcpGateway, true), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.detail', { + success: formatAutomationNumber(runEvidenceSummary.mcpSuccess + statusChainEvidenceSummary.mcpGatewaySuccess, automationBriefLoaded), + failed: formatAutomationNumber(runEvidenceSummary.mcpFailed + statusChainEvidenceSummary.mcpGatewayFailed, automationBriefLoaded), + server: runEvidenceSummary.latestMcpServer ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + route: runEvidenceSummary.latestRoute ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.runsAndStatusChain'), + updated: formatLiveEvidenceTime(latestRun?.created_at), + }, }, { key: 'playbook', @@ -1036,6 +1299,19 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.playbook.metric', { + gate: topAutomationGate?.gate ?? (automationQualityLoaded ? unavailableValue : loadingStatus), + gaps: formatAutomationNumber(recurrenceSummary?.automation_gap_group_total, automationBriefLoaded), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.playbook.detail', { + workItems: formatAutomationNumber(recurrenceSummary?.open_work_item_group_total, automationBriefLoaded), + verifiedGroups: formatAutomationNumber(recurrenceSummary?.verified_repair_group_total, automationBriefLoaded), + linkedAutoRepair: formatAutomationNumber(recurrenceSummary?.auto_repair_linked_total, automationBriefLoaded), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.qualityAndRecurrence'), + updated: formatLiveEvidenceTime(recurrenceSummary?.latest_received_at), + }, }, { key: 'ansible', @@ -1051,6 +1327,19 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.ansible.metric', { + checkMode: formatAutomationNumber(executionBackend?.ansible_check_mode_total, automationQualityLoaded), + pending: formatAutomationNumber(executionBackend?.ansible_pending_check_mode_total, automationQualityLoaded), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.ansible.detail', { + blocker: ansibleRuntime?.blockers?.join(' / ') || (automationQualityLoaded ? unavailableValue : loadingStatus), + candidates: formatAutomationNumber(executionBackend?.ansible_candidate_total, automationQualityLoaded), + operations: formatAutomationNumber(executionBackend?.operation_records_total, automationQualityLoaded), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.truthChainQuality'), + updated: automationQualityLoaded ? tDashboard('automationDiagrams.workspace.liveEvidence.realtime') : loadingStatus, + }, }, { key: 'approval', @@ -1065,6 +1354,20 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.approval.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.approval.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.approval.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.approval.metric', { + pending: formatAutomationNumber(pendingApprovals, true), + verified: formatAutomationNumber(verifiedAutomationTotal, automationQualityLoaded), + evaluated: formatAutomationNumber(evaluatedAutomationTotal, automationQualityLoaded), + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.approval.detail', { + humanGates: formatAutomationNumber(runEvidenceSummary.humanGateRuns, automationBriefLoaded), + autoRepairRecords: formatAutomationNumber(statusChainEvidenceSummary.autoRepairRecords, true), + operations: formatAutomationNumber(statusChainEvidenceSummary.operationRecords, true), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.approvalsAndQuality'), + updated: automationQualityLoaded ? tDashboard('automationDiagrams.workspace.liveEvidence.realtime') : loadingStatus, + }, }, { key: 'verify', @@ -1079,6 +1382,19 @@ export default function Home({ params }: { params: { locale: string } }) { owner: tDashboard('automationDiagrams.workspace.inspector.stages.verify.owner'), evidence: tDashboard('automationDiagrams.workspace.inspector.stages.verify.evidence'), nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.verify.nextAction'), + liveEvidence: { + metric: tDashboard('automationDiagrams.workspace.liveEvidence.verify.metric', { + stale: formatAutomationNumber(kmBurndownSnapshot?.stale_count ?? kmStaleTotal, automationBriefLoaded), + ratio: staleRatioLabel, + }), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.verify.detail', { + pending: formatAutomationNumber(kmOwnerReviewBurndown?.pending_owner_reviews, automationBriefLoaded), + completed: formatAutomationNumber(kmOwnerReviewBurndown?.completed_owner_reviews, automationBriefLoaded), + remaining: formatAutomationNumber(kmOwnerReviewBurndown?.entries_to_threshold, automationBriefLoaded), + }), + source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.kmBurndown'), + updated: formatLiveEvidenceTime(kmOwnerReviewBurndown?.generated_at), + }, }, ] const runtimeTopologyLayers = [ @@ -1714,6 +2030,18 @@ export default function Home({ params }: { params: { locale: string } }) {
{stage.detail}
+
+ {stage.liveEvidence.metric} +
) })} @@ -1806,6 +2134,56 @@ export default function Home({ params }: { params: { locale: string } }) { ))} +
+
+
+ {tDashboard('automationDiagrams.workspace.liveEvidence.title')} +
+ + {selectedBlueprintStage.liveEvidence.updated} + +
+
+ {[ + [tDashboard('automationDiagrams.workspace.liveEvidence.fields.metric'), selectedBlueprintStage.liveEvidence.metric], + [tDashboard('automationDiagrams.workspace.liveEvidence.fields.detail'), selectedBlueprintStage.liveEvidence.detail], + [tDashboard('automationDiagrams.workspace.liveEvidence.fields.source'), selectedBlueprintStage.liveEvidence.source], + ].map(([label, value]) => ( +
+
+ {label} +
+
+ {value} +
+
+ ))} +
+