From fb9b0b3b7caa5c55c4db51a30bd4e83a4200e8eb Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 19 May 2026 00:13:40 +0800
Subject: [PATCH] feat(awooop): record recurrence handoff proposals
---
apps/api/src/api/v1/platform/events.py | 40 ++++
.../services/channel_event_dossier_service.py | 204 ++++++++++++++++++
.../test_channel_event_dossier_service.py | 63 ++++++
apps/web/messages/en.json | 18 +-
apps/web/messages/zh-TW.json | 18 +-
.../app/[locale]/awooop/work-items/page.tsx | 100 +++++++--
6 files changed, 424 insertions(+), 19 deletions(-)
diff --git a/apps/api/src/api/v1/platform/events.py b/apps/api/src/api/v1/platform/events.py
index be23f3ab..5533dfdd 100644
--- a/apps/api/src/api/v1/platform/events.py
+++ b/apps/api/src/api/v1/platform/events.py
@@ -14,12 +14,14 @@ from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from src.services.channel_event_dossier_service import (
+ RecurrenceWorkItemHandoffKind,
RecurrenceWorkItemMode,
RecurrenceWorkItemNotFoundError,
fetch_channel_event_dossier,
fetch_channel_event_dossier_coverage,
fetch_channel_event_dossier_recurrence,
fetch_recurrence_work_item_dry_run,
+ fetch_recurrence_work_item_handoff,
fetch_recurrence_work_item_preview,
)
from src.services.platform_operator_service import list_recent_channel_events
@@ -184,6 +186,17 @@ class RecurrenceWorkItemDryRunRequest(BaseModel):
limit: int = Field(default=300, ge=1, le=300)
+class RecurrenceWorkItemHandoffRequest(BaseModel):
+ """AwoooP recurrence work item handoff request."""
+
+ project_id: str | None = Field(default=None, min_length=1)
+ work_item_id: str = Field(min_length=1)
+ mode: RecurrenceWorkItemMode = "auto"
+ handoff_kind: RecurrenceWorkItemHandoffKind = "ticket_proposal"
+ provider: str | None = Field(default=None, min_length=1)
+ limit: int = Field(default=300, ge=1, le=300)
+
+
@router.get(
"/events/dossier",
response_model=ChannelEventDossierResponse,
@@ -313,6 +326,33 @@ async def dry_run_event_recurrence_work_item(
) from exc
+@router.post(
+ "/events/dossier/recurrence/work-item/handoff",
+ summary="記錄重複告警工作項的交接提案",
+ description=(
+ "依 recurrence read model 與 dry-run 結果記錄 ticket proposal / 人工接手歷史,"
+ "但不修改 incident、auto-repair 或外部 ticket 狀態。"
+ ),
+)
+async def handoff_event_recurrence_work_item(
+ request: RecurrenceWorkItemHandoffRequest,
+) -> dict[str, Any]:
+ try:
+ return await fetch_recurrence_work_item_handoff(
+ project_id=request.project_id,
+ work_item_id=request.work_item_id,
+ mode=request.mode,
+ handoff_kind=request.handoff_kind,
+ provider=request.provider,
+ limit=request.limit,
+ )
+ except RecurrenceWorkItemNotFoundError as exc:
+ raise HTTPException(
+ status_code=404,
+ detail="recurrence_work_item_not_found",
+ ) from exc
+
+
@router.get(
"/events/recent",
response_model=RecentEventsResponse,
diff --git a/apps/api/src/services/channel_event_dossier_service.py b/apps/api/src/services/channel_event_dossier_service.py
index e84328f2..d05c0559 100644
--- a/apps/api/src/services/channel_event_dossier_service.py
+++ b/apps/api/src/services/channel_event_dossier_service.py
@@ -25,6 +25,7 @@ _MAX_RECURRENCE_EVENTS = 300
_MAX_REPAIR_INCIDENTS = 200
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
RecurrenceWorkItemMode = Literal["auto", "ticket", "reverify", "approval_review", "observe"]
+RecurrenceWorkItemHandoffKind = Literal["ticket_proposal", "manual_review"]
class RecurrenceWorkItemNotFoundError(LookupError):
@@ -671,6 +672,64 @@ def build_recurrence_work_item_dry_run(
return payload
+def _recurrence_handoff_next_step(
+ handoff_kind: RecurrenceWorkItemHandoffKind,
+ allowed: bool,
+) -> str:
+ if not allowed:
+ return "fix_preflight_checks"
+ if handoff_kind == "manual_review":
+ return "operator_manual_review"
+ return "operator_review_ticket_preview"
+
+
+def build_recurrence_work_item_handoff(
+ recurrence: dict[str, Any],
+ *,
+ work_item_id: str,
+ mode: RecurrenceWorkItemMode = "auto",
+ handoff_kind: RecurrenceWorkItemHandoffKind = "ticket_proposal",
+) -> dict[str, Any]:
+ """Build a record-only handoff proposal for a recurrence work item."""
+
+ payload = build_recurrence_work_item_dry_run(
+ recurrence,
+ work_item_id=work_item_id,
+ mode=mode,
+ )
+ allowed = bool(payload.get("allowed"))
+ plan = _as_dict(payload.get("plan"))
+ read_model_route = _as_dict(payload.get("read_model_route"))
+ payload.update(
+ {
+ "schema_version": "awooop_recurrence_work_item_handoff_v1",
+ "handoff_kind": handoff_kind,
+ "handoff_status": "ready_to_record" if allowed else "blocked",
+ "handoff_owner": "operator",
+ "safety_level": "handoff_record_only",
+ "writes_incident_state": False,
+ "writes_auto_repair_result": False,
+ "writes_ticket": False,
+ "creates_external_ticket": False,
+ "plan": {
+ **plan,
+ "step": "record_recurrence_work_item_handoff",
+ "flywheel_node": "handoff",
+ "required_scope": "record_history",
+ "writes": ["timeline_events", "alert_operation_log"],
+ },
+ "read_model_route": {
+ **read_model_route,
+ "required_scope": "record_history",
+ "is_shadow": False,
+ "flywheel_node": "handoff",
+ },
+ "next_step": _recurrence_handoff_next_step(handoff_kind, allowed),
+ }
+ )
+ return payload
+
+
def _recurrence_history_context(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema_version": "awooop_recurrence_work_item_dry_run_history_v1",
@@ -696,6 +755,35 @@ def _recurrence_history_context(payload: dict[str, Any]) -> dict[str, Any]:
}
+def _recurrence_handoff_context(payload: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "schema_version": "awooop_recurrence_work_item_handoff_history_v1",
+ "source": payload.get("source"),
+ "project_id": payload.get("project_id"),
+ "work_item_id": payload.get("work_item_id"),
+ "incident_id": payload.get("incident_id"),
+ "auto_repair_id": payload.get("auto_repair_id"),
+ "mode": payload.get("mode"),
+ "requested_mode": payload.get("requested_mode"),
+ "handoff_kind": payload.get("handoff_kind"),
+ "handoff_status": payload.get("handoff_status"),
+ "handoff_owner": payload.get("handoff_owner"),
+ "allowed": payload.get("allowed"),
+ "executed": payload.get("executed"),
+ "safety_level": payload.get("safety_level"),
+ "writes_incident_state": payload.get("writes_incident_state"),
+ "writes_auto_repair_result": payload.get("writes_auto_repair_result"),
+ "writes_ticket": payload.get("writes_ticket"),
+ "creates_external_ticket": payload.get("creates_external_ticket"),
+ "verification_result_preview": payload.get("verification_result_preview"),
+ "current_state_summary": payload.get("current_state_summary"),
+ "ticket_preview": payload.get("ticket_preview"),
+ "read_model_route": payload.get("read_model_route"),
+ "checks": payload.get("checks"),
+ "next_step": payload.get("next_step"),
+ }
+
+
async def _record_recurrence_work_item_dry_run_history(
payload: dict[str, Any],
) -> dict[str, Any]:
@@ -763,6 +851,75 @@ async def _record_recurrence_work_item_dry_run_history(
return history
+async def _record_recurrence_work_item_handoff_history(
+ payload: dict[str, Any],
+) -> dict[str, Any]:
+ incident_id = str(payload.get("incident_id") or "")
+ if not incident_id:
+ return {"recorded": False, "reason": "missing_incident_id"}
+
+ history: dict[str, Any] = {
+ "recorded": False,
+ "alert_operation_id": None,
+ "timeline_event_id": None,
+ }
+ context = _recurrence_handoff_context(payload)
+ allowed = bool(payload.get("allowed"))
+
+ try:
+ from src.repositories.alert_operation_log_repository import (
+ get_alert_operation_log_repository,
+ )
+
+ record = await get_alert_operation_log_repository().append(
+ "ESCALATED",
+ incident_id=incident_id,
+ auto_repair_id=str(payload.get("auto_repair_id") or "") or None,
+ actor="awooop_recurrence_work_item_service",
+ action_detail=(
+ f"recurrence_work_item_handoff:{payload.get('handoff_kind')}"
+ )[:200],
+ success=allowed,
+ context=context,
+ )
+ if record is not None:
+ history["alert_operation_id"] = getattr(record, "id", None)
+ except Exception as exc:
+ logger.warning(
+ "awooop_recurrence_work_item_handoff_alert_operation_history_failed",
+ incident_id=incident_id,
+ error=str(exc),
+ )
+
+ try:
+ from src.services.approval_db import get_timeline_service
+
+ event = await get_timeline_service().add_event(
+ event_type="human",
+ status="warning" if allowed else "error",
+ title="AwoooP recurrence work item handoff",
+ description=_recurrence_handoff_history_description(context),
+ actor="awooop_recurrence_work_item_service",
+ actor_role=str(payload.get("handoff_kind") or "handoff"),
+ incident_id=incident_id,
+ )
+ if event:
+ history["timeline_event_id"] = event.get("id")
+ except Exception as exc:
+ logger.warning(
+ "awooop_recurrence_work_item_handoff_timeline_history_failed",
+ incident_id=incident_id,
+ error=str(exc),
+ )
+
+ history["recorded"] = bool(
+ history.get("alert_operation_id") or history.get("timeline_event_id")
+ )
+ if not history["recorded"]:
+ history["reason"] = "history_sink_unavailable"
+ return history
+
+
def _recurrence_history_description(context: dict[str, Any]) -> str:
state = context.get("current_state_summary") or {}
route = context.get("read_model_route") or {}
@@ -778,6 +935,23 @@ def _recurrence_history_description(context: dict[str, Any]) -> str:
)[:500]
+def _recurrence_handoff_history_description(context: dict[str, Any]) -> str:
+ state = context.get("current_state_summary") or {}
+ route = context.get("read_model_route") or {}
+ return (
+ f"handoff={context.get('handoff_kind')} "
+ f"status={context.get('handoff_status')} "
+ f"mode={context.get('mode')} "
+ f"occurrences={state.get('occurrence_total')} "
+ f"repair_status={state.get('repair_status')} "
+ f"route={route.get('agent_id')}/{route.get('tool_name')} "
+ f"external_ticket={context.get('creates_external_ticket')} "
+ f"writes_incident={context.get('writes_incident_state')} "
+ f"writes_auto_repair={context.get('writes_auto_repair_result')} "
+ f"writes_ticket={context.get('writes_ticket')}"
+ )[:500]
+
+
def build_dossier_coverage(
rows: list[dict[str, Any]],
*,
@@ -1247,3 +1421,33 @@ async def fetch_recurrence_work_item_dry_run(
)
payload["history"] = await _record_recurrence_work_item_dry_run_history(payload)
return payload
+
+
+async def fetch_recurrence_work_item_handoff(
+ *,
+ project_id: str | None,
+ work_item_id: str,
+ mode: RecurrenceWorkItemMode = "auto",
+ handoff_kind: RecurrenceWorkItemHandoffKind = "ticket_proposal",
+ provider: str | None = None,
+ limit: int = _MAX_RECURRENCE_EVENTS,
+) -> dict[str, Any]:
+ """Fetch and record a safe handoff proposal for a recurrence work item."""
+
+ recurrence = await fetch_channel_event_dossier_recurrence(
+ project_id=project_id,
+ provider=provider,
+ limit=limit,
+ )
+ payload = build_recurrence_work_item_handoff(
+ recurrence,
+ work_item_id=work_item_id,
+ mode=mode,
+ handoff_kind=handoff_kind,
+ )
+ payload["history"] = await _record_recurrence_work_item_handoff_history(payload)
+ if payload["history"].get("recorded"):
+ payload["handoff_status"] = "recorded"
+ elif payload.get("allowed"):
+ payload["handoff_status"] = "record_failed"
+ return payload
diff --git a/apps/api/tests/test_channel_event_dossier_service.py b/apps/api/tests/test_channel_event_dossier_service.py
index 977cd314..4f0326bd 100644
--- a/apps/api/tests/test_channel_event_dossier_service.py
+++ b/apps/api/tests/test_channel_event_dossier_service.py
@@ -13,6 +13,7 @@ from src.services.channel_event_dossier_service import (
build_dossier_event,
build_dossier_recurrence,
build_recurrence_work_item_dry_run,
+ build_recurrence_work_item_handoff,
build_recurrence_work_item_preview,
fetch_channel_event_dossier,
fetch_channel_event_dossier_coverage,
@@ -461,6 +462,68 @@ def test_build_recurrence_work_item_dry_run_returns_ticket_preview_without_write
assert dry_run["read_model_route"]["required_scope"] == "read"
+def test_build_recurrence_work_item_handoff_records_ticket_proposal_contract() -> None:
+ recurrence = build_dossier_recurrence(
+ [
+ {
+ "event_id": "event-1",
+ "project_id": "awoooi",
+ "channel_type": "internal",
+ "provider_event_id": "alertmanager:received:1",
+ "content_hash": "a" * 64,
+ "content_preview": "Docker container unhealthy",
+ "content_redacted": "Docker container unhealthy",
+ "redaction_version": "audit_sink_v1",
+ "source_envelope": {
+ "provider": "alertmanager",
+ "source_refs": {
+ "alert_ids": ["alert-1"],
+ "incident_ids": ["INC-20260517-F25B4A"],
+ "fingerprints": ["fp-container-unhealthy"],
+ },
+ "log_correlation": {
+ "alertname": "DockerContainerUnhealthy",
+ "severity": "warning",
+ "namespace": "momo",
+ "target_resource": "bitan-pharmacy-bitan-1",
+ "fingerprint": "fp-container-unhealthy",
+ },
+ },
+ "is_duplicate": True,
+ "provider_ts": None,
+ "received_at": "2026-05-17T23:47:00",
+ "run_id": UUID("33333333-3333-4333-8333-333333333333"),
+ "run_state": "completed",
+ "run_agent_id": "openclaw",
+ }
+ ],
+ project_id="awoooi",
+ limit=20,
+ )
+
+ handoff = build_recurrence_work_item_handoff(
+ recurrence,
+ work_item_id="incident:INC-20260517-F25B4A",
+ )
+
+ assert handoff["schema_version"] == "awooop_recurrence_work_item_handoff_v1"
+ assert handoff["mode"] == "ticket"
+ assert handoff["handoff_kind"] == "ticket_proposal"
+ assert handoff["handoff_status"] == "ready_to_record"
+ assert handoff["handoff_owner"] == "operator"
+ assert handoff["safety_level"] == "handoff_record_only"
+ assert handoff["allowed"] is True
+ assert handoff["creates_external_ticket"] is False
+ assert handoff["writes_incident_state"] is False
+ assert handoff["writes_auto_repair_result"] is False
+ assert handoff["writes_ticket"] is False
+ assert handoff["ticket_preview"]["would_create"] is False
+ assert handoff["next_step"] == "operator_review_ticket_preview"
+ assert handoff["plan"]["step"] == "record_recurrence_work_item_handoff"
+ assert handoff["plan"]["writes"] == ["timeline_events", "alert_operation_log"]
+ assert handoff["read_model_route"]["required_scope"] == "record_history"
+
+
def test_build_recurrence_work_item_preview_raises_for_missing_item() -> None:
recurrence = build_dossier_recurrence(
[],
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index f79e3b5f..9e9ba6a6 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -1846,13 +1846,17 @@
"previewing": "Previewing",
"dryRun": "Dry-run",
"dryRunning": "Dry-running",
- "failed": "The safe preview / dry-run API did not respond, so the next step cannot be claimed.",
+ "handoff": "Handoff",
+ "handoffing": "Handing off",
+ "failed": "The safe preview / dry-run / handoff API did not respond, so the next step cannot be claimed.",
"allowed": "Safety gate passed",
"blocked": "Safety gate blocked",
"mode": "Mode: {mode}",
"previewResult": "Result: {result}",
"writes": "Writes: incident={incident}; autoRepair={autoRepair}; ticket={ticket}",
"history": "Dry-run stored: {recorded}",
+ "handoffStatus": "Handoff: {kind} / {status}",
+ "externalTicket": "External ticket created: {created}",
"ticket": "Ticket preview: {title}",
"modes": {
"auto": "Auto select",
@@ -1862,6 +1866,18 @@
"observe": "Observe",
"unknown": "Unknown"
},
+ "handoffKinds": {
+ "ticket_proposal": "Ticket proposal",
+ "manual_review": "Manual review",
+ "unknown": "Unknown"
+ },
+ "handoffStatuses": {
+ "ready_to_record": "Ready to record",
+ "recorded": "Recorded",
+ "record_failed": "Record failed",
+ "blocked": "Blocked",
+ "unknown": "Unknown"
+ },
"previews": {
"ticket_preview_ready": "Ticket preview ready",
"reverify_preview_ready": "Reverify preview ready",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index e29048f7..afe8f84e 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -1847,13 +1847,17 @@
"previewing": "預覽中",
"dryRun": "乾跑",
"dryRunning": "乾跑中",
- "failed": "安全預覽 / 乾跑 API 未回應,不能判定下一步。",
+ "handoff": "交接",
+ "handoffing": "交接中",
+ "failed": "安全預覽 / 乾跑 / 交接 API 未回應,不能判定下一步。",
"allowed": "安全閘門通過",
"blocked": "安全閘門阻塞",
"mode": "模式:{mode}",
"previewResult": "結果:{result}",
"writes": "寫入:incident={incident};autoRepair={autoRepair};ticket={ticket}",
"history": "試跑入庫:{recorded}",
+ "handoffStatus": "交接:{kind} / {status}",
+ "externalTicket": "外部 Ticket 建立:{created}",
"ticket": "Ticket 預覽:{title}",
"modes": {
"auto": "自動選擇",
@@ -1863,6 +1867,18 @@
"observe": "觀察",
"unknown": "未知"
},
+ "handoffKinds": {
+ "ticket_proposal": "Ticket 提案",
+ "manual_review": "人工覆核",
+ "unknown": "未知"
+ },
+ "handoffStatuses": {
+ "ready_to_record": "待寫入歷史",
+ "recorded": "已寫入歷史",
+ "record_failed": "寫入失敗",
+ "blocked": "已阻塞",
+ "unknown": "未知"
+ },
"previews": {
"ticket_preview_ready": "Ticket 預覽已就緒",
"reverify_preview_ready": "重新驗證預覽已就緒",
diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
index 8ce5b3ea..b5366636 100644
--- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
@@ -110,12 +110,16 @@ type RecurrenceWorkItemActionResult = {
incident_id?: string | null;
mode?: string | null;
requested_mode?: string | null;
+ handoff_kind?: string | null;
+ handoff_status?: string | null;
+ handoff_owner?: string | null;
allowed?: boolean | null;
executed?: boolean | null;
safety_level?: string | null;
writes_incident_state?: boolean | null;
writes_auto_repair_result?: boolean | null;
writes_ticket?: boolean | null;
+ creates_external_ticket?: boolean | null;
verification_result_preview?: string | null;
next_step?: string | null;
checks?: Array<{ name?: string | null; passed?: boolean | null; detail?: string | null }>;
@@ -153,7 +157,7 @@ type RecurrenceWorkItemActionResult = {
};
type RecurrenceWorkItemActionState = {
- loading?: "preview" | "dryRun" | null;
+ loading?: "preview" | "dryRun" | "handoff" | null;
result?: RecurrenceWorkItemActionResult | null;
error?: string | null;
};
@@ -331,6 +335,25 @@ function recurrencePreviewKey(preview?: string | null) {
return "unknown";
}
+function recurrenceHandoffStatusKey(status?: string | null) {
+ if (
+ status === "ready_to_record" ||
+ status === "recorded" ||
+ status === "record_failed" ||
+ status === "blocked"
+ ) {
+ return status;
+ }
+ return "unknown";
+}
+
+function recurrenceHandoffKindKey(kind?: string | null) {
+ if (kind === "ticket_proposal" || kind === "manual_review") {
+ return kind;
+ }
+ return "unknown";
+}
+
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType
@@ -653,7 +676,7 @@ function RecurrenceWorkQueuePanel({
const summary = recurrence?.summary;
const runWorkItemAction = useCallback(async (
workItemId: string,
- action: "preview" | "dryRun"
+ action: "preview" | "dryRun" | "handoff"
) => {
setActionState((current) => ({
...current,
@@ -662,21 +685,36 @@ function RecurrenceWorkQueuePanel({
const encodedProjectId = encodeURIComponent(projectId);
const encodedWorkItemId = encodeURIComponent(workItemId);
- const result = action === "preview"
- ? await fetchJson(
- `${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/preview?project_id=${encodedProjectId}&work_item_id=${encodedWorkItemId}`,
- 12000
- )
- : await postJson(
- `${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/dry-run`,
- {
- project_id: projectId,
- work_item_id: workItemId,
- mode: "auto",
- limit: 300,
- },
- 15000
- );
+ let result: RecurrenceWorkItemActionResult | null = null;
+ if (action === "preview") {
+ result = await fetchJson(
+ `${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/preview?project_id=${encodedProjectId}&work_item_id=${encodedWorkItemId}`,
+ 12000
+ );
+ } else if (action === "dryRun") {
+ result = await postJson(
+ `${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/dry-run`,
+ {
+ project_id: projectId,
+ work_item_id: workItemId,
+ mode: "auto",
+ limit: 300,
+ },
+ 15000
+ );
+ } else {
+ result = await postJson(
+ `${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/handoff`,
+ {
+ project_id: projectId,
+ work_item_id: workItemId,
+ mode: "auto",
+ handoff_kind: "ticket_proposal",
+ limit: 300,
+ },
+ 15000
+ );
+ }
setActionState((current) => ({
...current,
@@ -736,6 +774,8 @@ function RecurrenceWorkQueuePanel({
const actionAllowed = actionResult?.allowed === true;
const actionModeKey = recurrenceActionModeKey(actionResult?.mode);
const previewKey = recurrencePreviewKey(actionResult?.verification_result_preview);
+ const handoffStatusKey = recurrenceHandoffStatusKey(actionResult?.handoff_status);
+ const handoffKindKey = recurrenceHandoffKindKey(actionResult?.handoff_kind);
return (
+
>
) : null}
{runHref ? (
@@ -863,6 +914,21 @@ function RecurrenceWorkQueuePanel({
recorded: String(actionResult.history?.recorded ?? false),
})}
+ {actionResult.handoff_status ? (
+
+ {t("actions.handoffStatus", {
+ status: t(`actions.handoffStatuses.${handoffStatusKey}` as never),
+ kind: t(`actions.handoffKinds.${handoffKindKey}` as never),
+ })}
+
+ ) : null}
+ {actionResult.creates_external_ticket !== undefined ? (
+
+ {t("actions.externalTicket", {
+ created: String(actionResult.creates_external_ticket ?? false),
+ })}
+
+ ) : null}
{actionResult.ticket_preview?.title ? (
{t("actions.ticket", {