Files
Project_Velocity/backend/api/routes_admin_surface.py
sayan 7ee51543d9
Some checks failed
Production Readiness / backend-contracts (push) Failing after 1m47s
Production Readiness / webos-typecheck (push) Successful in 1m57s
Production Readiness / ipad-parse (push) Successful in 1m32s
Merge Conflicts (#41)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #41
2026-04-28 11:32:56 +05:30

530 lines
20 KiB
Python

"""
routes_admin_surface.py
───────────────────────
Admin Control Plane API
Roles: Only 'admin' or 'superadmin' may access these endpoints.
Endpoints:
GET /admin-surface/health — system health overview
GET /admin-surface/queues — queue depth snapshot
GET /admin-surface/installs — surface session / install overview
POST /admin-surface/actions — submit an admin action
GET /admin-surface/actions — list admin action history
GET /admin-surface/actions/{id} — get a specific action
GET /admin-surface/logs — recent audit event log
GET /admin-surface/templates — template catalog summary (admin view)
POST /admin-surface/templates/{id}/publish — publish a template
POST /admin-surface/templates/{id}/archive — archive a template
GET /admin-surface/synthetic-jobs — list synthetic generation jobs
POST /admin-surface/synthetic-jobs/{id}/cancel — cancel a synthetic job
"""
from __future__ import annotations
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import get_current_user
logger = logging.getLogger("velocity.admin_surface")
router = APIRouter()
# ── RBAC guard ────────────────────────────────────────────────────────────────
ADMIN_ROLES = {"admin", "superadmin", "ADMIN", "SUPERADMIN"}
def require_admin(user=Depends(get_current_user)):
normalized_role = user.role.upper()
if normalized_role not in {"ADMIN", "SUPERADMIN"}:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required.",
)
return user
# ── Helpers ───────────────────────────────────────────────────────────────────
def _pool(request: Request):
pool = request.app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
return pool
# ── Pydantic Models ───────────────────────────────────────────────────────────
VALID_ACTION_TYPES = {
"user_create", "user_deactivate", "user_role_change",
"tenant_config_update", "inventory_batch_approve", "inventory_batch_reject",
"template_publish", "template_archive",
"synthetic_job_trigger", "synthetic_job_cancel",
"system_health_check", "queue_drain", "debug_event_export",
"install_register", "install_deregister",
}
class AdminActionRequest(BaseModel):
action_type: str
target_type: str
target_id: str
payload: dict = Field(default_factory=dict)
idempotency_key: Optional[str] = None
# ── System Health ─────────────────────────────────────────────────────────────
@router.get("/health", summary="System health overview")
async def get_health(
request: Request,
admin=Depends(require_admin),
):
"""
Returns an aggregated health snapshot covering DB pool, queue depths,
and basic surface session counts.
"""
pool = _pool(request)
async with pool.acquire() as conn:
# DB round-trip latency
import time
t0 = time.monotonic()
await conn.fetchval("SELECT 1")
db_latency_ms = round((time.monotonic() - t0) * 1000, 2)
# Pending jobs
pending_transcriptions = await conn.fetchval(
"SELECT COUNT(*) FROM edge_transcription_jobs WHERE status='pending'"
)
pending_synthetic_jobs = await conn.fetchval(
"SELECT COUNT(*) FROM oracle_synthetic_generation_jobs WHERE status IN ('pending','running')"
)
pending_admin_actions = await conn.fetchval(
"SELECT COUNT(*) FROM admin_action_events WHERE status='pending'"
)
pending_inventory_batches = await conn.fetchval(
"SELECT COUNT(*) FROM inventory_import_batches WHERE status IN ('pending','validating','processing')"
)
# Active surface sessions (last 30 min)
active_sessions = await conn.fetchval(
"SELECT COUNT(*) FROM surface_sessions WHERE last_active_at > NOW() - INTERVAL '30 minutes'"
)
# Surface breakdown
surface_breakdown = await conn.fetch(
"""
SELECT surface_type, COUNT(*) as count
FROM surface_sessions
WHERE last_active_at > NOW() - INTERVAL '30 minutes'
GROUP BY surface_type
"""
)
return {
"status": "ok",
"timestamp": datetime.now(timezone.utc).isoformat(),
"database": {
"connected": True,
"latency_ms": db_latency_ms,
},
"queues": {
"pending_transcriptions": pending_transcriptions,
"pending_synthetic_jobs": pending_synthetic_jobs,
"pending_admin_actions": pending_admin_actions,
"pending_inventory_batches": pending_inventory_batches,
},
"active_sessions": {
"total": active_sessions,
"by_surface": {r["surface_type"]: r["count"] for r in surface_breakdown},
},
}
# ── Queue Visibility ──────────────────────────────────────────────────────────
@router.get("/queues", summary="Queue depth snapshot")
async def get_queues(
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
transcription_queue = await conn.fetch(
"""
SELECT status, COUNT(*) as count
FROM edge_transcription_jobs
GROUP BY status ORDER BY status
"""
)
synthetic_queue = await conn.fetch(
"""
SELECT status, COUNT(*) as count
FROM oracle_synthetic_generation_jobs
GROUP BY status ORDER BY status
"""
)
inventory_queue = await conn.fetch(
"""
SELECT status, COUNT(*) as count
FROM inventory_import_batches
GROUP BY status ORDER BY status
"""
)
admin_queue = await conn.fetch(
"""
SELECT status, COUNT(*) as count
FROM admin_action_events
GROUP BY status ORDER BY status
"""
)
return {
"transcription_jobs": {r["status"]: r["count"] for r in transcription_queue},
"synthetic_jobs": {r["status"]: r["count"] for r in synthetic_queue},
"inventory_batches": {r["status"]: r["count"] for r in inventory_queue},
"admin_actions": {r["status"]: r["count"] for r in admin_queue},
"timestamp": datetime.now(timezone.utc).isoformat(),
}
# ── Install / Surface Overview ────────────────────────────────────────────────
@router.get("/installs", summary="Surface session and install overview")
async def get_installs(
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT surface_type, app_version, COUNT(*) as session_count,
MAX(last_active_at) as last_seen
FROM surface_sessions
GROUP BY surface_type, app_version
ORDER BY surface_type, app_version
"""
)
return {
"installs": [dict(r) for r in rows],
"timestamp": datetime.now(timezone.utc).isoformat(),
}
# ── Admin Actions ─────────────────────────────────────────────────────────────
@router.post("/actions", status_code=status.HTTP_201_CREATED, summary="Submit an admin action")
async def submit_action(
request: Request,
body: AdminActionRequest,
admin=Depends(require_admin),
):
"""
Submit a bounded admin action. All actions are persisted with full audit trail.
Supported action_types are enumerated in VALID_ACTION_TYPES.
Actions are not auto-executed — they transition to 'pending' and must be
processed by the appropriate backend job or confirmed by a second admin.
(This prevents destructive mass-actions from running unreviewed.)
"""
if body.action_type not in VALID_ACTION_TYPES:
raise HTTPException(400, f"Invalid action_type. Valid: {sorted(VALID_ACTION_TYPES)}")
action_id = body.idempotency_key or str(uuid.uuid4())
pool = _pool(request)
async with pool.acquire() as conn:
try:
row = await conn.fetchrow(
"""
INSERT INTO admin_action_events (
tenant_id, action_id, action_type, target_type, target_id,
requested_by, payload
) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)
RETURNING action_event_id, status, created_at
""",
admin.role, action_id, body.action_type, body.target_type,
body.target_id, admin.user_id, json.dumps(body.payload),
)
except Exception as exc:
if "unique" in str(exc).lower():
raise HTTPException(409, "Action with this idempotency key already exists")
raise
logger.info(
"Admin action submitted: %s by %s%s/%s",
body.action_type, admin.user_id, body.target_type, body.target_id,
)
return {
"action_event_id": str(row["action_event_id"]),
"action_id": action_id,
"status": row["status"],
"created_at": str(row["created_at"]),
}
@router.get("/actions", summary="List admin action history")
async def list_actions(
request: Request,
action_type: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
admin=Depends(require_admin),
):
pool = _pool(request)
where = "WHERE tenant_id = $1"
params: list[Any] = [admin.role]
idx = 2
if action_type:
where += f" AND action_type = ${idx}"; params.append(action_type); idx += 1
if status_filter:
where += f" AND status = ${idx}"; params.append(status_filter); idx += 1
async with pool.acquire() as conn:
rows = await conn.fetch(
f"""
SELECT action_event_id, action_id, action_type, target_type, target_id,
requested_by, status, result_message, executed_at, created_at
FROM admin_action_events
{where}
ORDER BY created_at DESC
LIMIT ${idx} OFFSET ${idx+1}
""",
*params, limit, offset,
)
total = await conn.fetchval(
f"SELECT COUNT(*) FROM admin_action_events {where}", *params,
)
return {"total": total, "limit": limit, "offset": offset, "actions": [dict(r) for r in rows]}
@router.get("/actions/{action_event_id}", summary="Get a specific admin action")
async def get_action(
action_event_id: str,
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT * FROM admin_action_events WHERE action_event_id=$1 AND tenant_id=$2",
action_event_id, admin.role,
)
if not row:
raise HTTPException(404, "Admin action not found")
return dict(row)
# ── Audit Log ─────────────────────────────────────────────────────────────────
@router.get("/logs", summary="Recent Oracle audit events")
async def get_audit_logs(
request: Request,
entity_type: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
if entity_type:
rows = await conn.fetch(
"""
SELECT audit_event_id, entity_type, entity_id, action, actor_id,
actor_type, correlation_id, details, created_at
FROM oracle_audit_events
WHERE tenant_id=$1 AND entity_type=$2
ORDER BY created_at DESC
LIMIT $3 OFFSET $4
""",
admin.role, entity_type, limit, offset,
)
else:
rows = await conn.fetch(
"""
SELECT audit_event_id, entity_type, entity_id, action, actor_id,
actor_type, correlation_id, details, created_at
FROM oracle_audit_events
WHERE tenant_id=$1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
""",
admin.role, limit, offset,
)
return {"logs": [dict(r) for r in rows]}
# ── Template Administration ───────────────────────────────────────────────────
@router.get("/templates", summary="Template catalog admin view")
async def get_templates_admin(
request: Request,
status_filter: Optional[str] = Query(None, alias="status"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
admin=Depends(require_admin),
):
pool = _pool(request)
where = "WHERE tenant_id = $1"
params: list[Any] = [admin.role]
idx = 2
if status_filter:
where += f" AND status = ${idx}"; params.append(status_filter); idx += 1
async with pool.acquire() as conn:
rows = await conn.fetch(
f"""
SELECT t.template_id, t.name, t.category, t.status, t.origin,
t.version, t.use_count, t.chapter_id, t.subchapter_id,
ch.name as chapter_name, sub.name as subchapter_name,
t.created_at, t.updated_at
FROM oracle_component_templates t
LEFT JOIN oracle_template_chapters ch ON ch.chapter_id = t.chapter_id
LEFT JOIN oracle_template_subchapters sub ON sub.subchapter_id = t.subchapter_id
{where}
ORDER BY t.updated_at DESC
LIMIT ${idx} OFFSET ${idx+1}
""",
*params, limit, offset,
)
total = await conn.fetchval(
f"SELECT COUNT(*) FROM oracle_component_templates {where}", *params,
)
return {"total": total, "limit": limit, "offset": offset, "templates": [dict(r) for r in rows]}
@router.post("/templates/{template_id}/publish", summary="Publish a template")
async def publish_template(
template_id: str,
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
result = await conn.execute(
"""
UPDATE oracle_component_templates
SET status='catalog_active', updated_at=NOW()
WHERE template_id=$1 AND tenant_id=$2
""",
template_id, admin.role,
)
if result == "UPDATE 0":
raise HTTPException(404, "Template not found")
logger.info("Template %s published by admin %s", template_id, admin.user_id)
return {"status": "published"}
@router.post("/templates/{template_id}/archive", summary="Archive a template")
async def archive_template(
template_id: str,
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
result = await conn.execute(
"""
UPDATE oracle_component_templates
SET status='archived', updated_at=NOW()
WHERE template_id=$1 AND tenant_id=$2
""",
template_id, admin.role,
)
if result == "UPDATE 0":
raise HTTPException(404, "Template not found")
logger.info("Template %s archived by admin %s", template_id, admin.user_id)
return {"status": "archived"}
# ── Template Chapter Admin ────────────────────────────────────────────────────
@router.get("/template-chapters", summary="List template chapters (admin view)")
async def list_chapters_admin(
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT ch.chapter_id, ch.name, ch.description, ch.sort_order, ch.is_active,
COUNT(sub.subchapter_id) as subchapter_count
FROM oracle_template_chapters ch
LEFT JOIN oracle_template_subchapters sub ON sub.chapter_id = ch.chapter_id
WHERE ch.tenant_id=$1
GROUP BY ch.chapter_id
ORDER BY ch.sort_order ASC
""",
admin.role,
)
return {"chapters": [dict(r) for r in rows]}
# ── Synthetic Jobs Admin ──────────────────────────────────────────────────────
@router.get("/synthetic-jobs", summary="List synthetic generation jobs")
async def list_synthetic_jobs(
request: Request,
status_filter: Optional[str] = Query(None, alias="status"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
if status_filter:
rows = await conn.fetch(
"""
SELECT job_id, template_id, model, status, requested_count,
accepted_count, created_by, started_at, completed_at, created_at
FROM oracle_synthetic_generation_jobs
WHERE tenant_id=$1 AND status=$2
ORDER BY created_at DESC LIMIT $3 OFFSET $4
""",
admin.role, status_filter, limit, offset,
)
else:
rows = await conn.fetch(
"""
SELECT job_id, template_id, model, status, requested_count,
accepted_count, created_by, started_at, completed_at, created_at
FROM oracle_synthetic_generation_jobs
WHERE tenant_id=$1
ORDER BY created_at DESC LIMIT $2 OFFSET $3
""",
admin.role, limit, offset,
)
return {"jobs": [dict(r) for r in rows]}
@router.post("/synthetic-jobs/{job_id}/cancel", summary="Cancel a synthetic generation job")
async def cancel_synthetic_job(
job_id: str,
request: Request,
admin=Depends(require_admin),
):
pool = _pool(request)
async with pool.acquire() as conn:
result = await conn.execute(
"""
UPDATE oracle_synthetic_generation_jobs
SET status='cancelled', updated_at=NOW()
WHERE job_id=$1 AND tenant_id=$2 AND status IN ('pending','running')
""",
job_id, admin.role,
)
if result == "UPDATE 0":
raise HTTPException(404, "Job not found or already in terminal state")
return {"status": "cancelled"}