fix(api): record approval gate timeline events
This commit is contained in:
@@ -175,6 +175,149 @@ def approval_request_to_record_data(
|
||||
}
|
||||
|
||||
|
||||
def _record_value(value: Any) -> str:
|
||||
if hasattr(value, "value"):
|
||||
value = value.value
|
||||
text = str(value or "").strip()
|
||||
if "." in text:
|
||||
text = text.rsplit(".", 1)[-1]
|
||||
return text.lower()
|
||||
|
||||
|
||||
def _record_int(value: Any) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def _approval_gate_stage(record: ApprovalRecord) -> str:
|
||||
status = _record_value(record.status)
|
||||
current = _record_int(getattr(record, "current_signatures", 0))
|
||||
required = _record_int(getattr(record, "required_signatures", 0))
|
||||
|
||||
if status == "pending":
|
||||
return "approval_required"
|
||||
if status == "approved" and current == 0 and required == 0:
|
||||
return "approval_auto_approved"
|
||||
if status == "approved":
|
||||
return "approval_approved"
|
||||
if status == "execution_success":
|
||||
return "execution_verified"
|
||||
if status == "execution_failed":
|
||||
return "execution_failed"
|
||||
if status in {"rejected", "expired"}:
|
||||
return f"approval_{status}"
|
||||
return "approval_status_recorded"
|
||||
|
||||
|
||||
def _approval_gate_status(record: ApprovalRecord) -> str:
|
||||
status = _record_value(record.status)
|
||||
if status == "pending":
|
||||
return "warning"
|
||||
if status in {"approved", "execution_success"}:
|
||||
return "success"
|
||||
if status in {"rejected", "expired", "execution_failed"}:
|
||||
return "error"
|
||||
return "info"
|
||||
|
||||
|
||||
def _approval_needs_human(record: ApprovalRecord) -> bool:
|
||||
status = _record_value(record.status)
|
||||
current = _record_int(getattr(record, "current_signatures", 0))
|
||||
required = _record_int(getattr(record, "required_signatures", 0))
|
||||
return status == "pending" and current < required
|
||||
|
||||
|
||||
def _approval_next_action(record: ApprovalRecord) -> str:
|
||||
status = _record_value(record.status)
|
||||
if status == "pending":
|
||||
return (
|
||||
"operator_approve_or_reject"
|
||||
if _approval_needs_human(record)
|
||||
else "execute_or_verify"
|
||||
)
|
||||
if status == "approved":
|
||||
return "execute_or_verify"
|
||||
if status == "execution_success":
|
||||
return "verify_or_close"
|
||||
if status == "execution_failed":
|
||||
return "manual_fix_or_rollback"
|
||||
if status in {"rejected", "expired"}:
|
||||
return "review_or_close"
|
||||
return "review_status_chain"
|
||||
|
||||
|
||||
def _approval_blocked_reason(record: ApprovalRecord) -> str:
|
||||
status = _record_value(record.status)
|
||||
if status == "pending" and _approval_needs_human(record):
|
||||
return "waiting_for_required_signatures"
|
||||
if status == "execution_failed":
|
||||
return "execution_failed"
|
||||
if status == "rejected":
|
||||
return "operator_rejected"
|
||||
if status == "expired":
|
||||
return "approval_expired"
|
||||
return "none"
|
||||
|
||||
|
||||
def _approval_decision_mode(record: ApprovalRecord) -> str:
|
||||
current = _record_int(getattr(record, "current_signatures", 0))
|
||||
required = _record_int(getattr(record, "required_signatures", 0))
|
||||
risk_level = _record_value(record.risk_level)
|
||||
if _approval_needs_human(record) or current > 0:
|
||||
return "manual"
|
||||
if risk_level == "low" and required == 0:
|
||||
return "auto"
|
||||
return "manual"
|
||||
|
||||
|
||||
def build_approval_created_timeline_event(record: ApprovalRecord) -> TimelineEvent:
|
||||
"""Create the raw audit rail event that mirrors a newly-created approval gate."""
|
||||
current = _record_int(getattr(record, "current_signatures", 0))
|
||||
required = _record_int(getattr(record, "required_signatures", 0))
|
||||
risk_level = _record_value(record.risk_level)
|
||||
needs_human = _approval_needs_human(record)
|
||||
stage = _approval_gate_stage(record)
|
||||
next_action = _approval_next_action(record)
|
||||
mode = _approval_decision_mode(record)
|
||||
description = "; ".join(
|
||||
[
|
||||
f"stage={stage}",
|
||||
f"next_action={next_action}",
|
||||
f"blocked_reason={_approval_blocked_reason(record)}",
|
||||
f"auto_or_manual={mode}",
|
||||
f"needs_human={'yes' if needs_human else 'no'}",
|
||||
f"risk_level={risk_level}",
|
||||
f"signatures={current}/{required}",
|
||||
f"action={str(getattr(record, 'action', '') or '')[:240]}",
|
||||
]
|
||||
)
|
||||
|
||||
title = (
|
||||
"Approval gate waiting for human decision"
|
||||
if needs_human
|
||||
else "Approval gate passed"
|
||||
)
|
||||
return TimelineEvent(
|
||||
event_type="human",
|
||||
status=_approval_gate_status(record),
|
||||
title=title,
|
||||
description=description,
|
||||
actor=getattr(record, "requested_by", None),
|
||||
actor_role="approval_gate",
|
||||
risk_level=risk_level,
|
||||
approval_id=str(record.id),
|
||||
incident_id=getattr(record, "incident_id", None),
|
||||
)
|
||||
|
||||
|
||||
def add_approval_created_timeline_event(db: Any, record: ApprovalRecord) -> TimelineEvent:
|
||||
event = build_approval_created_timeline_event(record)
|
||||
db.add(event)
|
||||
return event
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Database Approval Service
|
||||
# =============================================================================
|
||||
@@ -224,6 +367,7 @@ class ApprovalDBService:
|
||||
db.add(record)
|
||||
await db.flush()
|
||||
await db.refresh(record)
|
||||
add_approval_created_timeline_event(db, record)
|
||||
|
||||
logger.info(
|
||||
"approval_created_db",
|
||||
@@ -275,6 +419,7 @@ class ApprovalDBService:
|
||||
db.add(record)
|
||||
await db.flush()
|
||||
await db.refresh(record)
|
||||
add_approval_created_timeline_event(db, record)
|
||||
|
||||
logger.info(
|
||||
"approval_created_with_fingerprint",
|
||||
|
||||
@@ -36,6 +36,7 @@ from src.core.unit_of_work import UnitOfWork
|
||||
from src.db.models import ApprovalRecord, IncidentRecord
|
||||
from src.models.approval import ApprovalRequestCreate, ApprovalStatus
|
||||
from src.models.incident import IncidentStatus
|
||||
from src.services.approval_db import add_approval_created_timeline_event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from redis.asyncio import Redis
|
||||
@@ -167,6 +168,7 @@ class IncidentApprovalService:
|
||||
)
|
||||
uow.session.add(approval_record)
|
||||
await uow.flush() # 取得 ID
|
||||
add_approval_created_timeline_event(uow.session, approval_record)
|
||||
|
||||
# 更新 Incident metadata 連結 Approval
|
||||
if link_metadata:
|
||||
|
||||
Reference in New Issue
Block a user