메일 알림 시스템 추가
This commit is contained in:
parent
ddf12a851c
commit
3bf38c50df
22 changed files with 1769 additions and 31 deletions
|
|
@ -31,7 +31,7 @@ from ..deps import Principal, require_admin_access
|
|||
from ..engine_client import engine_client
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..services.voice import voice_service
|
||||
from ..services import evaluator, rag
|
||||
from ..services import evaluator, notifications, rag
|
||||
from ..store import store
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
|
@ -49,6 +49,7 @@ TicketCategory = Literal[
|
|||
]
|
||||
TicketPriority = Literal["low", "normal", "high", "urgent"]
|
||||
TicketStatus = Literal["open", "triaged", "in_progress", "resolved", "closed"]
|
||||
NotificationDeliveryStatus = Literal["queued", "sending", "sent", "failed", "skipped"]
|
||||
|
||||
|
||||
class AdminServiceHealth(BaseModel):
|
||||
|
|
@ -258,6 +259,46 @@ class AdminUserPatch(BaseModel):
|
|||
cohort_ids: list[str] | None = None
|
||||
|
||||
|
||||
class AdminNotificationDeliveryResponse(BaseModel):
|
||||
id: str
|
||||
event_id: str
|
||||
kind: str
|
||||
recipient_email: str
|
||||
recipient_name: str
|
||||
recipient_role: str
|
||||
subject: str
|
||||
status: NotificationDeliveryStatus
|
||||
attempts: int
|
||||
last_error: str | None = None
|
||||
provider_message_id: str | None = None
|
||||
created_at: float | None = None
|
||||
updated_at: float | None = None
|
||||
sent_at: float | None = None
|
||||
|
||||
|
||||
class AdminNotificationsResponse(BaseModel):
|
||||
provider: str
|
||||
smtp_configured: bool
|
||||
queued: int
|
||||
failed: int
|
||||
sent: int
|
||||
skipped: int
|
||||
deliveries: list[AdminNotificationDeliveryResponse]
|
||||
|
||||
|
||||
class AdminNotificationProcessResponse(BaseModel):
|
||||
processed: int
|
||||
sent: int
|
||||
failed: int
|
||||
skipped: int
|
||||
|
||||
|
||||
class AdminNotificationTestResponse(AdminNotificationProcessResponse):
|
||||
provider: str
|
||||
smtp_configured: bool
|
||||
recipients: int
|
||||
|
||||
|
||||
class RuntimeHealthMetrics(BaseModel):
|
||||
engine_latency_ms: float | None = None
|
||||
db_pool_size: int = 0
|
||||
|
|
@ -1009,6 +1050,25 @@ def _row_value(row, key: str, default=None):
|
|||
return default
|
||||
|
||||
|
||||
def _notification_delivery_response(row) -> AdminNotificationDeliveryResponse:
|
||||
return AdminNotificationDeliveryResponse(
|
||||
id=str(row["id"]),
|
||||
event_id=str(row["event_id"]),
|
||||
kind=str(row["kind"]),
|
||||
recipient_email=str(row["recipient_email"]),
|
||||
recipient_name=str(row["recipient_name"] or ""),
|
||||
recipient_role=str(row["recipient_role"] or ""),
|
||||
subject=str(row["subject"] or ""),
|
||||
status=row["status"],
|
||||
attempts=int(row["attempts"] or 0),
|
||||
last_error=str(row["last_error"] or "") or None,
|
||||
provider_message_id=str(row["provider_message_id"] or "") or None,
|
||||
created_at=_row_ts(row["created_at"]),
|
||||
updated_at=_row_ts(row["updated_at"]),
|
||||
sent_at=_row_ts(row["sent_at"]),
|
||||
)
|
||||
|
||||
|
||||
def _engine_config_from_row(row) -> AdminEngineConfigResponse:
|
||||
return AdminEngineConfigResponse(
|
||||
engine_mode=_normalize_engine_mode(row["engine_mode"]),
|
||||
|
|
@ -1456,6 +1516,101 @@ async def list_tickets(
|
|||
return _unavailable_tickets()
|
||||
|
||||
|
||||
@router.get("/notifications", response_model=AdminNotificationsResponse)
|
||||
async def list_notifications(
|
||||
principal: AdminPrincipal,
|
||||
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
||||
) -> AdminNotificationsResponse:
|
||||
"""Return recent operational email delivery state."""
|
||||
try:
|
||||
get_pool()
|
||||
async with acquire(role="admin") as conn:
|
||||
status_rows = await conn.fetch(
|
||||
"""
|
||||
SELECT status, COUNT(*) AS count
|
||||
FROM app.notification_delivery
|
||||
GROUP BY status
|
||||
"""
|
||||
)
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT
|
||||
d.id,
|
||||
d.event_id,
|
||||
e.kind,
|
||||
d.recipient_email,
|
||||
d.recipient_name,
|
||||
d.recipient_role,
|
||||
d.subject,
|
||||
d.status,
|
||||
d.attempts,
|
||||
d.last_error,
|
||||
d.provider_message_id,
|
||||
d.created_at,
|
||||
d.updated_at,
|
||||
d.sent_at
|
||||
FROM app.notification_delivery d
|
||||
JOIN app.notification_event e ON e.id = d.event_id
|
||||
ORDER BY d.created_at DESC
|
||||
LIMIT $1
|
||||
""",
|
||||
limit,
|
||||
)
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("admin notifications")
|
||||
status_rows = []
|
||||
rows = []
|
||||
counts = {str(row["status"]): int(row["count"] or 0) for row in status_rows}
|
||||
return AdminNotificationsResponse(
|
||||
provider=settings.notification_email_provider,
|
||||
smtp_configured=bool(settings.smtp_host and settings.smtp_from_email),
|
||||
queued=counts.get("queued", 0),
|
||||
failed=counts.get("failed", 0),
|
||||
sent=counts.get("sent", 0),
|
||||
skipped=counts.get("skipped", 0),
|
||||
deliveries=[_notification_delivery_response(row) for row in rows],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/notifications/process", response_model=AdminNotificationProcessResponse)
|
||||
async def process_notifications(
|
||||
principal: AdminPrincipal,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 25,
|
||||
) -> AdminNotificationProcessResponse:
|
||||
"""Drain queued email notifications once from the admin console/API."""
|
||||
try:
|
||||
result = await notifications.process_queued_email_notifications(limit=limit)
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("admin notification processing")
|
||||
result = {"processed": 0, "sent": 0, "failed": 0, "skipped": 0}
|
||||
return AdminNotificationProcessResponse(**result)
|
||||
|
||||
|
||||
@router.post("/notifications/test", response_model=AdminNotificationTestResponse)
|
||||
async def send_test_notification(
|
||||
principal: AdminPrincipal,
|
||||
) -> AdminNotificationTestResponse:
|
||||
"""Queue and process one explicit admin email test."""
|
||||
recipient_count = 0
|
||||
try:
|
||||
recipient_count = await notifications.enqueue_admin_test_email(
|
||||
actor_user_id=principal.user_id,
|
||||
actor_email=principal.email,
|
||||
)
|
||||
result = await notifications.process_queued_email_notifications(
|
||||
limit=max(1, min(100, recipient_count or 1))
|
||||
)
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("admin notification test")
|
||||
result = {"processed": 0, "sent": 0, "failed": 0, "skipped": 0}
|
||||
return AdminNotificationTestResponse(
|
||||
provider=settings.notification_email_provider,
|
||||
smtp_configured=bool(settings.smtp_host and settings.smtp_from_email),
|
||||
recipients=recipient_count,
|
||||
**result,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/tickets/{ticket_id}", response_model=AdminSupportTicketResponse)
|
||||
async def patch_ticket(
|
||||
ticket_id: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue