diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py
index 9c87c391..e283a4e4 100644
--- a/apps/api/src/services/platform_operator_service.py
+++ b/apps/api/src/services/platform_operator_service.py
@@ -1300,6 +1300,76 @@ def _status_chain_execution_section(truth_chain: dict[str, Any] | None) -> dict[
}
+def _source_ref_values(envelope: Any, key: str) -> list[str]:
+ if not isinstance(envelope, dict):
+ return []
+ source_refs = envelope.get("source_refs")
+ if not isinstance(source_refs, dict):
+ return []
+ raw_values = source_refs.get(key)
+ if isinstance(raw_values, list):
+ return [str(item) for item in raw_values if str(item or "").strip()]
+ if raw_values not in (None, ""):
+ return [str(raw_values)]
+ return []
+
+
+def _status_chain_source_section(truth_chain: dict[str, Any] | None) -> dict[str, Any]:
+ channel = truth_chain.get("channel") if isinstance(truth_chain, dict) else {}
+ if not isinstance(channel, dict):
+ channel = {}
+ inbound_events = channel.get("inbound_events")
+ outbound_messages = channel.get("outbound_messages")
+ if not isinstance(inbound_events, list):
+ inbound_events = []
+ if not isinstance(outbound_messages, list):
+ outbound_messages = []
+
+ source_refs: dict[str, list[str]] = {
+ "alert_ids": [],
+ "sentry_issue_ids": [],
+ "signoz_alerts": [],
+ "fingerprints": [],
+ "incident_ids": [],
+ }
+ inbound_channels: list[str] = []
+ for row in inbound_events:
+ if not isinstance(row, dict):
+ continue
+ _append_unique(inbound_channels, row.get("channel_type"))
+ envelope = row.get("source_envelope")
+ for key in source_refs:
+ for value in _source_ref_values(envelope, key):
+ _append_unique(source_refs[key], value)
+
+ latest_inbound = inbound_events[0] if inbound_events and isinstance(inbound_events[0], dict) else {}
+ latest_outbound = (
+ outbound_messages[0]
+ if outbound_messages and isinstance(outbound_messages[0], dict)
+ else {}
+ )
+
+ return {
+ "inbound_total": len(inbound_events),
+ "outbound_total": len(outbound_messages),
+ "inbound_channels": inbound_channels[:5],
+ "refs": {key: values[:5] for key, values in source_refs.items()},
+ "latest_inbound": {
+ "channel_type": latest_inbound.get("channel_type"),
+ "provider_event_id": latest_inbound.get("provider_event_id"),
+ "content_type": latest_inbound.get("content_type"),
+ "is_duplicate": latest_inbound.get("is_duplicate"),
+ "received_at": latest_inbound.get("received_at"),
+ },
+ "latest_outbound": {
+ "channel_type": latest_outbound.get("channel_type"),
+ "message_type": latest_outbound.get("message_type"),
+ "send_status": latest_outbound.get("send_status"),
+ "sent_at": latest_outbound.get("sent_at"),
+ },
+ }
+
+
def _build_awooop_status_chain(
*,
incident_ids: list[str],
@@ -1379,6 +1449,7 @@ def _build_awooop_status_chain(
mcp_section = _status_chain_mcp_section(truth_chain)
execution_section = _status_chain_execution_section(truth_chain)
+ source_section = _status_chain_source_section(truth_chain)
blockers = [
str(item)
for item in [
@@ -1422,6 +1493,7 @@ def _build_awooop_status_chain(
},
"mcp": mcp_section,
"execution": execution_section,
+ "source_refs": source_section,
}
diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py
index 72b27d98..36bd8a10 100644
--- a/apps/api/tests/test_awooop_operator_timeline_labels.py
+++ b/apps/api/tests/test_awooop_operator_timeline_labels.py
@@ -539,6 +539,33 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
"not_used_reason": None,
},
},
+ "channel": {
+ "inbound_events": [
+ {
+ "channel_type": "alertmanager",
+ "provider_event_id": "alert-1",
+ "content_type": "application/json",
+ "is_duplicate": False,
+ "received_at": "2026-05-20T00:00:00Z",
+ "source_envelope": {
+ "source_refs": {
+ "alert_ids": ["alert-1"],
+ "sentry_issue_ids": ["SENTRY-1"],
+ "signoz_alerts": ["signoz:abc"],
+ "fingerprints": ["fp-1"],
+ }
+ },
+ }
+ ],
+ "outbound_messages": [
+ {
+ "channel_type": "telegram",
+ "message_type": "incident_detail",
+ "send_status": "sent",
+ "sent_at": "2026-05-20T00:01:00Z",
+ }
+ ],
+ },
},
remediation_history={
"total": 1,
@@ -571,6 +598,10 @@ def test_awooop_status_chain_marks_verified_repair() -> None:
assert chain["execution"]["playbook_ids"] == ["pb-host-restart"]
assert chain["execution"]["ansible"]["considered"] is True
assert chain["execution"]["ansible"]["candidate_count"] == 1
+ assert chain["source_refs"]["inbound_total"] == 1
+ assert chain["source_refs"]["outbound_total"] == 1
+ assert chain["source_refs"]["refs"]["sentry_issue_ids"] == ["SENTRY-1"]
+ assert chain["source_refs"]["refs"]["signoz_alerts"] == ["signoz:abc"]
def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index da4daa38..dac34236 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -436,6 +436,7 @@
"flowExecutionAnsibleConsidered": "considered ({records} records / {candidates} candidates)",
"flowExecutionAnsibleNotUsed": "not used: {reason}",
"flowExecutionAnsibleEmpty": "--",
+ "flowSourceRefsDetail": "Source detail: Inbound {inbound} / Outbound {outbound}; Alert {alert}; Sentry {sentry}; SigNoz {signoz}; latest {latest}",
"flowTruthChainCurrent": "{stage} / {status}",
"flowComplete": "Complete",
"flowStages": {
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 46a08f58..3a93632a 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -437,6 +437,7 @@
"flowExecutionAnsibleConsidered": "已納入 ({records} records / {candidates} candidates)",
"flowExecutionAnsibleNotUsed": "未使用:{reason}",
"flowExecutionAnsibleEmpty": "--",
+ "flowSourceRefsDetail": "來源明細:Inbound {inbound} / Outbound {outbound};Alert {alert};Sentry {sentry};SigNoz {signoz};最新 {latest}",
"flowTruthChainCurrent": "{stage} / {status}",
"flowComplete": "已完成",
"flowStages": {
diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx
index f09adb5c..87c5ceb5 100644
--- a/apps/web/src/components/awooop/status-chain.tsx
+++ b/apps/web/src/components/awooop/status-chain.tsx
@@ -88,6 +88,31 @@ export interface AwoooPStatusChain {
}>;
};
};
+ source_refs?: {
+ inbound_total?: number | null;
+ outbound_total?: number | null;
+ inbound_channels?: string[];
+ refs?: {
+ alert_ids?: string[];
+ sentry_issue_ids?: string[];
+ signoz_alerts?: string[];
+ fingerprints?: string[];
+ incident_ids?: string[];
+ };
+ latest_inbound?: {
+ channel_type?: string | null;
+ provider_event_id?: string | null;
+ content_type?: string | null;
+ is_duplicate?: boolean | null;
+ received_at?: string | null;
+ };
+ latest_outbound?: {
+ channel_type?: string | null;
+ message_type?: string | null;
+ send_status?: string | null;
+ sent_at?: string | null;
+ };
+ };
}
function toneClass(chain?: AwoooPStatusChain | null) {
diff --git a/apps/web/src/components/incident/incident-card.tsx b/apps/web/src/components/incident/incident-card.tsx
index 262a9e6b..6190105d 100644
--- a/apps/web/src/components/incident/incident-card.tsx
+++ b/apps/web/src/components/incident/incident-card.tsx
@@ -380,6 +380,22 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
playbook: executionPlaybook,
})
: null
+ const sourceRefs = statusChain?.source_refs
+ const latestSource = sourceRefs?.latest_inbound?.channel_type
+ ? `${sourceRefs.latest_inbound.channel_type}/${chainValue(sourceRefs.latest_inbound.provider_event_id)}`
+ : sourceRefs?.latest_outbound?.channel_type
+ ? `${sourceRefs.latest_outbound.channel_type}/${chainValue(sourceRefs.latest_outbound.message_type)}`
+ : '--'
+ const statusChainSourceRefDetail = hasTruthChain
+ ? t('flowSourceRefsDetail', {
+ inbound: sourceRefs?.inbound_total ?? 0,
+ outbound: sourceRefs?.outbound_total ?? 0,
+ alert: sourceRefs?.refs?.alert_ids?.length ?? 0,
+ sentry: sourceRefs?.refs?.sentry_issue_ids?.length ?? 0,
+ signoz: sourceRefs?.refs?.signoz_alerts?.length ?? 0,
+ latest: latestSource,
+ })
+ : null
const serviceName = incident.affected_services?.[0] ?? '--'
const duration = formatDuration(incident.created_at)
@@ -659,6 +675,10 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
{statusChainExecutionDetail}
+ /
+
+ {statusChainSourceRefDetail}
+
>
)}