Commita governance, user-wizard, operational-feed e catálogo RBAC; adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial; documenta credencial VM112 @betinplace. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.1 KiB
Python
119 lines
3.1 KiB
Python
"""Operational feed API. Spec 041 · OF-API-002."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app import auth
|
|
from app.ops_inbox_store import (
|
|
add_message,
|
|
get_event,
|
|
inbox_stats,
|
|
init_inbox_schema,
|
|
list_events,
|
|
patch_event,
|
|
)
|
|
from app.permissions import can_manage_users
|
|
|
|
router = APIRouter(prefix="/api/v1/ops-inbox", tags=["ops-inbox"])
|
|
|
|
|
|
class InboxMessageBody(BaseModel):
|
|
body: str = Field(min_length=1, max_length=8000)
|
|
note_type: str = Field(default="reply", pattern="^(reply|internal_note)$")
|
|
|
|
|
|
class InboxPatchBody(BaseModel):
|
|
status: str | None = Field(default=None, pattern="^(open|pending|resolved)$")
|
|
assignee: str | None = None
|
|
priority: str | None = Field(default=None, pattern="^(normal|high|critical)$")
|
|
|
|
|
|
def _db():
|
|
conn = auth.db()
|
|
try:
|
|
init_inbox_schema(conn)
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _require_inbox(user: auth.DeskUser) -> None:
|
|
if not can_manage_users(user.role):
|
|
raise HTTPException(403, "insufficient permissions")
|
|
|
|
|
|
@router.get("/stats")
|
|
def inbox_statistics(
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
conn=Depends(_db),
|
|
):
|
|
_require_inbox(user)
|
|
return inbox_stats(conn)
|
|
|
|
|
|
@router.get("/events")
|
|
def inbox_events(
|
|
channel: str | None = Query(default="all"),
|
|
priority: str | None = Query(default=None),
|
|
status: str | None = Query(default=None),
|
|
q: str | None = Query(default=None),
|
|
limit: int = Query(default=128, ge=1, le=500),
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
conn=Depends(_db),
|
|
):
|
|
_require_inbox(user)
|
|
items = list_events(conn, channel=channel, priority=priority, status=status, q=q, limit=limit)
|
|
return {"events": items, "total": len(items)}
|
|
|
|
|
|
@router.get("/events/{event_id}")
|
|
def inbox_event_detail(
|
|
event_id: str,
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
conn=Depends(_db),
|
|
):
|
|
_require_inbox(user)
|
|
ev = get_event(conn, event_id)
|
|
if not ev:
|
|
raise HTTPException(404, "event not found")
|
|
return {"event": ev}
|
|
|
|
|
|
@router.post("/events/{event_id}/messages")
|
|
def inbox_post_message(
|
|
event_id: str,
|
|
body: InboxMessageBody,
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
conn=Depends(_db),
|
|
):
|
|
_require_inbox(user)
|
|
if not get_event(conn, event_id):
|
|
raise HTTPException(404, "event not found")
|
|
author_type = "internal" if body.note_type == "internal_note" else "operator"
|
|
label = user.display_name or user.username
|
|
msg = add_message(conn, event_id, author_type, label, body.body)
|
|
conn.commit()
|
|
return {"message": msg}
|
|
|
|
|
|
@router.patch("/events/{event_id}")
|
|
def inbox_patch(
|
|
event_id: str,
|
|
body: InboxPatchBody,
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
conn=Depends(_db),
|
|
):
|
|
_require_inbox(user)
|
|
ev = patch_event(
|
|
conn,
|
|
event_id,
|
|
status=body.status,
|
|
assignee=body.assignee,
|
|
priority=body.priority,
|
|
)
|
|
if not ev:
|
|
raise HTTPException(404, "event not found")
|
|
conn.commit()
|
|
return {"event": ev}
|