feat(awooop): summarize callback capture in runs list
This commit is contained in:
@@ -1078,6 +1078,24 @@ def _callback_reply_evidence_capture_status(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _callback_reply_capture_status_from_outbound(
|
||||||
|
row: AwoooPOutboundMessage,
|
||||||
|
callback_reply: Mapping[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build capture status directly from one outbound source envelope."""
|
||||||
|
source_envelope = _as_dict(row.source_envelope)
|
||||||
|
return _callback_reply_evidence_capture_status(
|
||||||
|
callback_reply=callback_reply,
|
||||||
|
persisted_awooop_status_chain=(
|
||||||
|
_as_dict(source_envelope.get("awooop_status_chain")) or None
|
||||||
|
),
|
||||||
|
persisted_km_stale_completion_summary=(
|
||||||
|
_as_dict(source_envelope.get("km_stale_completion_summary")) or None
|
||||||
|
),
|
||||||
|
event_at=row.sent_at or row.queued_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
"""Convert one callback reply outbound row into a read-only evidence item."""
|
"""Convert one callback reply outbound row into a read-only evidence item."""
|
||||||
callback_reply = _as_dict(row.get("callback_reply"))
|
callback_reply = _as_dict(row.get("callback_reply"))
|
||||||
@@ -1481,6 +1499,13 @@ def _run_callback_reply_summary(
|
|||||||
"latest_incident_id": None,
|
"latest_incident_id": None,
|
||||||
"latest_at": None,
|
"latest_at": None,
|
||||||
"latest_provider_message_id": None,
|
"latest_provider_message_id": None,
|
||||||
|
"capture_status": "no_callback",
|
||||||
|
"capture_captured": 0,
|
||||||
|
"capture_partial": 0,
|
||||||
|
"capture_not_captured": 0,
|
||||||
|
"latest_capture_status": None,
|
||||||
|
"latest_capture_missing": [],
|
||||||
|
"latest_capture_next_action": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted_rows = sorted(
|
sorted_rows = sorted(
|
||||||
@@ -1496,6 +1521,26 @@ def _run_callback_reply_summary(
|
|||||||
failed = statuses.count("callback_reply_failed")
|
failed = statuses.count("callback_reply_failed")
|
||||||
latest_status = str(latest_callback.get("status") or "")
|
latest_status = str(latest_callback.get("status") or "")
|
||||||
summary_status = _callback_reply_public_status(latest_callback)
|
summary_status = _callback_reply_public_status(latest_callback)
|
||||||
|
capture_rows = [
|
||||||
|
_callback_reply_capture_status_from_outbound(row, callback)
|
||||||
|
for row, callback in sorted_rows
|
||||||
|
]
|
||||||
|
capture_statuses = [
|
||||||
|
str(capture.get("status") or "observed")
|
||||||
|
for capture in capture_rows
|
||||||
|
]
|
||||||
|
capture_not_captured = capture_statuses.count("not_captured")
|
||||||
|
capture_partial = capture_statuses.count("partial")
|
||||||
|
capture_captured = capture_statuses.count("captured")
|
||||||
|
latest_capture = capture_rows[0] if capture_rows else {}
|
||||||
|
if capture_not_captured > 0:
|
||||||
|
capture_status = "not_captured"
|
||||||
|
elif capture_partial > 0:
|
||||||
|
capture_status = "partial"
|
||||||
|
elif capture_captured > 0 and capture_captured == len(capture_rows):
|
||||||
|
capture_status = "captured"
|
||||||
|
else:
|
||||||
|
capture_status = "observed"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"schema_version": "awooop_run_callback_reply_summary_v1",
|
"schema_version": "awooop_run_callback_reply_summary_v1",
|
||||||
@@ -1511,6 +1556,13 @@ def _run_callback_reply_summary(
|
|||||||
"latest_incident_id": latest_callback.get("incident_id"),
|
"latest_incident_id": latest_callback.get("incident_id"),
|
||||||
"latest_at": latest_row.sent_at or latest_row.queued_at,
|
"latest_at": latest_row.sent_at or latest_row.queued_at,
|
||||||
"latest_provider_message_id": latest_row.provider_message_id,
|
"latest_provider_message_id": latest_row.provider_message_id,
|
||||||
|
"capture_status": capture_status,
|
||||||
|
"capture_captured": capture_captured,
|
||||||
|
"capture_partial": capture_partial,
|
||||||
|
"capture_not_captured": capture_not_captured,
|
||||||
|
"latest_capture_status": latest_capture.get("status"),
|
||||||
|
"latest_capture_missing": latest_capture.get("missing") or [],
|
||||||
|
"latest_capture_next_action": latest_capture.get("next_action"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,12 @@ def test_run_callback_reply_summary_marks_latest_fallback() -> None:
|
|||||||
assert summary["latest_incident_id"] == "INC-20260513-79ED5E"
|
assert summary["latest_incident_id"] == "INC-20260513-79ED5E"
|
||||||
assert summary["latest_provider_message_id"] == "101"
|
assert summary["latest_provider_message_id"] == "101"
|
||||||
assert summary["needs_human"] is False
|
assert summary["needs_human"] is False
|
||||||
|
assert summary["capture_status"] == "not_captured"
|
||||||
|
assert summary["capture_not_captured"] == 2
|
||||||
|
assert summary["latest_capture_missing"] == [
|
||||||
|
"awooop_status_chain",
|
||||||
|
"km_stale_completion_summary",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_run_callback_reply_summary_marks_failed_as_human_attention() -> None:
|
def test_run_callback_reply_summary_marks_failed_as_human_attention() -> None:
|
||||||
@@ -291,6 +297,54 @@ def test_run_callback_reply_summary_marks_failed_as_human_attention() -> None:
|
|||||||
assert summary["status"] == "failed"
|
assert summary["status"] == "failed"
|
||||||
assert summary["failed"] == 1
|
assert summary["failed"] == 1
|
||||||
assert summary["needs_human"] is True
|
assert summary["needs_human"] is True
|
||||||
|
assert summary["capture_status"] == "not_captured"
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_callback_reply_summary_counts_capture_statuses() -> None:
|
||||||
|
summary = _run_callback_reply_summary([
|
||||||
|
SimpleNamespace(
|
||||||
|
source_envelope={
|
||||||
|
"callback_reply": {
|
||||||
|
"status": "callback_reply_sent",
|
||||||
|
"action": "detail",
|
||||||
|
"incident_id": "INC-20260513-79ED5E",
|
||||||
|
},
|
||||||
|
"awooop_status_chain": {
|
||||||
|
"schema_version": "awooop_status_chain_callback_reply_snapshot_v1",
|
||||||
|
},
|
||||||
|
"km_stale_completion_summary": {
|
||||||
|
"schema_version": (
|
||||||
|
"km_stale_owner_review_callback_reply_snapshot_v1"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sent_at=datetime(2026, 5, 18, 6, 3, 0),
|
||||||
|
queued_at=datetime(2026, 5, 18, 6, 3, 0),
|
||||||
|
provider_message_id="102",
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
source_envelope={
|
||||||
|
"callback_reply": {
|
||||||
|
"status": "callback_reply_sent",
|
||||||
|
"action": "history",
|
||||||
|
"incident_id": "INC-20260513-79ED5E",
|
||||||
|
},
|
||||||
|
"awooop_status_chain": {
|
||||||
|
"schema_version": "awooop_status_chain_callback_reply_snapshot_v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sent_at=datetime(2026, 5, 18, 6, 4, 0),
|
||||||
|
queued_at=datetime(2026, 5, 18, 6, 4, 0),
|
||||||
|
provider_message_id="103",
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
assert summary["capture_status"] == "partial"
|
||||||
|
assert summary["capture_captured"] == 1
|
||||||
|
assert summary["capture_partial"] == 1
|
||||||
|
assert summary["capture_not_captured"] == 0
|
||||||
|
assert summary["latest_capture_status"] == "partial"
|
||||||
|
assert summary["latest_capture_missing"] == ["km_stale_completion_summary"]
|
||||||
|
|
||||||
|
|
||||||
def test_run_callback_reply_summary_marks_no_callback() -> None:
|
def test_run_callback_reply_summary_marks_no_callback() -> None:
|
||||||
@@ -305,6 +359,7 @@ def test_run_callback_reply_summary_marks_no_callback() -> None:
|
|||||||
|
|
||||||
assert summary["status"] == "no_callback"
|
assert summary["status"] == "no_callback"
|
||||||
assert summary["total"] == 0
|
assert summary["total"] == 0
|
||||||
|
assert summary["capture_status"] == "no_callback"
|
||||||
|
|
||||||
|
|
||||||
def test_list_runs_response_preserves_callback_reply_summary() -> None:
|
def test_list_runs_response_preserves_callback_reply_summary() -> None:
|
||||||
@@ -336,6 +391,18 @@ def test_list_runs_response_preserves_callback_reply_summary() -> None:
|
|||||||
"latest_incident_id": "INC-20260513-79ED5E",
|
"latest_incident_id": "INC-20260513-79ED5E",
|
||||||
"latest_at": "2026-05-18T07:31:37",
|
"latest_at": "2026-05-18T07:31:37",
|
||||||
"latest_provider_message_id": "telegram_callback_reply:failed",
|
"latest_provider_message_id": "telegram_callback_reply:failed",
|
||||||
|
"capture_status": "not_captured",
|
||||||
|
"capture_captured": 0,
|
||||||
|
"capture_partial": 0,
|
||||||
|
"capture_not_captured": 1,
|
||||||
|
"latest_capture_status": "not_captured",
|
||||||
|
"latest_capture_missing": [
|
||||||
|
"awooop_status_chain",
|
||||||
|
"km_stale_completion_summary",
|
||||||
|
],
|
||||||
|
"latest_capture_next_action": (
|
||||||
|
"press_telegram_detail_or_history_after_rollout"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -347,6 +414,9 @@ def test_list_runs_response_preserves_callback_reply_summary() -> None:
|
|||||||
dumped = response.model_dump(mode="json")
|
dumped = response.model_dump(mode="json")
|
||||||
assert dumped["runs"][0]["callback_reply_summary"]["status"] == "failed"
|
assert dumped["runs"][0]["callback_reply_summary"]["status"] == "failed"
|
||||||
assert dumped["runs"][0]["callback_reply_summary"]["needs_human"] is True
|
assert dumped["runs"][0]["callback_reply_summary"]["needs_human"] is True
|
||||||
|
assert dumped["runs"][0]["callback_reply_summary"]["capture_status"] == (
|
||||||
|
"not_captured"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None:
|
def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None:
|
||||||
|
|||||||
@@ -2764,6 +2764,18 @@
|
|||||||
"emptyShort": "No detail / history callback yet",
|
"emptyShort": "No detail / history callback yet",
|
||||||
"latest": "{action} · {incidentId}",
|
"latest": "{action} · {incidentId}",
|
||||||
"needsHuman": "Callback failure needs human review",
|
"needsHuman": "Callback failure needs human review",
|
||||||
|
"captureLine": "Snapshot: {status}; captured {captured} / partial {partial} / not captured {notCaptured}",
|
||||||
|
"captureMissing": "Missing: {items}",
|
||||||
|
"captureStatuses": {
|
||||||
|
"captured": "Captured",
|
||||||
|
"partial": "Partially captured",
|
||||||
|
"not_captured": "Not captured",
|
||||||
|
"observed": "Recorded"
|
||||||
|
},
|
||||||
|
"captureItems": {
|
||||||
|
"awooopStatusChain": "AwoooP status chain",
|
||||||
|
"kmCompletionSummary": "KM owner-review snapshot"
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"label": "TG Callback filter",
|
"label": "TG Callback filter",
|
||||||
"all": "All TG callbacks"
|
"all": "All TG callbacks"
|
||||||
|
|||||||
@@ -2765,6 +2765,18 @@
|
|||||||
"emptyShort": "尚無詳情 / 歷史 callback",
|
"emptyShort": "尚無詳情 / 歷史 callback",
|
||||||
"latest": "{action} · {incidentId}",
|
"latest": "{action} · {incidentId}",
|
||||||
"needsHuman": "Callback 失敗需人工確認",
|
"needsHuman": "Callback 失敗需人工確認",
|
||||||
|
"captureLine": "Snapshot:{status};已捕捉 {captured} / 部分 {partial} / 未捕捉 {notCaptured}",
|
||||||
|
"captureMissing": "尚缺:{items}",
|
||||||
|
"captureStatuses": {
|
||||||
|
"captured": "已捕捉",
|
||||||
|
"partial": "部分捕捉",
|
||||||
|
"not_captured": "未捕捉",
|
||||||
|
"observed": "已記錄"
|
||||||
|
},
|
||||||
|
"captureItems": {
|
||||||
|
"awooopStatusChain": "AwoooP 狀態鏈",
|
||||||
|
"kmCompletionSummary": "KM owner-review snapshot"
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"label": "TG Callback 篩選",
|
"label": "TG Callback 篩選",
|
||||||
"all": "所有 TG Callback"
|
"all": "所有 TG Callback"
|
||||||
|
|||||||
@@ -117,6 +117,13 @@ interface CallbackReplySummary {
|
|||||||
latest_incident_id?: string | null;
|
latest_incident_id?: string | null;
|
||||||
latest_at?: string | null;
|
latest_at?: string | null;
|
||||||
latest_provider_message_id?: string | null;
|
latest_provider_message_id?: string | null;
|
||||||
|
capture_status?: CallbackEvidenceCaptureState | "no_callback" | string | null;
|
||||||
|
capture_captured?: number;
|
||||||
|
capture_partial?: number;
|
||||||
|
capture_not_captured?: number;
|
||||||
|
latest_capture_status?: CallbackEvidenceCaptureState | string | null;
|
||||||
|
latest_capture_missing?: string[];
|
||||||
|
latest_capture_next_action?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallbackEvidenceCaptureStatus {
|
interface CallbackEvidenceCaptureStatus {
|
||||||
@@ -908,8 +915,29 @@ function CallbackReplyCell({ summary }: { summary?: CallbackReplySummary | null
|
|||||||
const status = normalizeCallbackReplyStatus(summary);
|
const status = normalizeCallbackReplyStatus(summary);
|
||||||
const config = CALLBACK_REPLY_CONFIG[status];
|
const config = CALLBACK_REPLY_CONFIG[status];
|
||||||
const total = summary?.total ?? 0;
|
const total = summary?.total ?? 0;
|
||||||
|
const captureStatus = total > 0
|
||||||
|
? normalizeCallbackEvidenceCaptureStatus(summary?.capture_status)
|
||||||
|
: null;
|
||||||
|
const captureConfig = captureStatus
|
||||||
|
? {
|
||||||
|
captured: "text-[#17602a]",
|
||||||
|
partial: "text-[#8a5a08]",
|
||||||
|
not_captured: "text-[#9f2f25]",
|
||||||
|
observed: "text-[#1f5b9b]",
|
||||||
|
}[captureStatus]
|
||||||
|
: null;
|
||||||
const latestAction = summary?.latest_action ? String(summary.latest_action) : null;
|
const latestAction = summary?.latest_action ? String(summary.latest_action) : null;
|
||||||
const latestIncidentId = summary?.latest_incident_id ? String(summary.latest_incident_id) : null;
|
const latestIncidentId = summary?.latest_incident_id ? String(summary.latest_incident_id) : null;
|
||||||
|
const captureItemLabels: Record<string, string> = {
|
||||||
|
awooop_status_chain: t("captureItems.awooopStatusChain"),
|
||||||
|
km_stale_completion_summary: t("captureItems.kmCompletionSummary"),
|
||||||
|
};
|
||||||
|
const latestCaptureMissing = Array.isArray(summary?.latest_capture_missing)
|
||||||
|
? summary.latest_capture_missing
|
||||||
|
: [];
|
||||||
|
const latestCaptureMissingText = latestCaptureMissing.length > 0
|
||||||
|
? latestCaptureMissing.map((item) => captureItemLabels[item] ?? item).join(" / ")
|
||||||
|
: null;
|
||||||
const countText = total > 0
|
const countText = total > 0
|
||||||
? t("count", {
|
? t("count", {
|
||||||
total,
|
total,
|
||||||
@@ -944,6 +972,19 @@ function CallbackReplyCell({ summary }: { summary?: CallbackReplySummary | null
|
|||||||
{latestText}
|
{latestText}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{captureStatus && (
|
||||||
|
<span
|
||||||
|
className={cn("text-xs leading-5", captureConfig)}
|
||||||
|
title={latestCaptureMissingText ? t("captureMissing", { items: latestCaptureMissingText }) : undefined}
|
||||||
|
>
|
||||||
|
{t("captureLine", {
|
||||||
|
status: t(`captureStatuses.${captureStatus}` as never),
|
||||||
|
captured: summary?.capture_captured ?? 0,
|
||||||
|
partial: summary?.capture_partial ?? 0,
|
||||||
|
notCaptured: summary?.capture_not_captured ?? 0,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{summary?.needs_human && (
|
{summary?.needs_human && (
|
||||||
<span className="text-xs font-semibold text-[#9f2f25]">
|
<span className="text-xs font-semibold text-[#9f2f25]">
|
||||||
{t("needsHuman")}
|
{t("needsHuman")}
|
||||||
|
|||||||
Reference in New Issue
Block a user