"""IRM: задачи по инцидентам (или вне привязки).""" from __future__ import annotations import html from uuid import UUID import asyncpg from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from onguard24.deps import get_pool from onguard24.domain.events import EventBus from onguard24.modules.ui_support import wrap_module_html_page router = APIRouter(tags=["module-tasks"]) ui_router = APIRouter(tags=["web-tasks"], include_in_schema=False) class TaskCreate(BaseModel): title: str = Field(..., min_length=1, max_length=500) incident_id: UUID | None = None class TaskPatch(BaseModel): title: str | None = Field(default=None, min_length=1, max_length=500) status: str | None = Field(default=None, max_length=64) def register_events(_bus: EventBus, _pool: asyncpg.Pool | None = None) -> None: pass async def render_home_fragment(request: Request) -> str: pool = get_pool(request) if pool is None: return '

Нужна БД для задач.

' try: async with pool.acquire() as conn: n = await conn.fetchval("SELECT count(*)::int FROM tasks") except Exception: return '

Таблица задач недоступна (миграции?).

' return f'

Задач: {int(n)}

' @router.get("/") async def list_tasks_api( pool: asyncpg.Pool | None = Depends(get_pool), incident_id: UUID | None = None, limit: int = 100, ): if pool is None: return {"items": [], "database": "disabled"} limit = min(max(limit, 1), 200) async with pool.acquire() as conn: if incident_id: rows = await conn.fetch( """ SELECT id, incident_id, title, status, created_at FROM tasks WHERE incident_id = $1::uuid ORDER BY created_at DESC LIMIT $2 """, incident_id, limit, ) else: rows = await conn.fetch( """ SELECT id, incident_id, title, status, created_at FROM tasks ORDER BY created_at DESC LIMIT $1 """, limit, ) items = [] for r in rows: items.append( { "id": str(r["id"]), "incident_id": str(r["incident_id"]) if r["incident_id"] else None, "title": r["title"], "status": r["status"], "created_at": r["created_at"].isoformat() if r["created_at"] else None, } ) return {"items": items} @router.post("/", status_code=201) async def create_task_api(body: TaskCreate, pool: asyncpg.Pool | None = Depends(get_pool)): if pool is None: raise HTTPException(status_code=503, detail="database disabled") if body.incident_id: async with pool.acquire() as conn: ok = await conn.fetchval( "SELECT 1 FROM incidents WHERE id = $1::uuid", body.incident_id, ) if not ok: raise HTTPException(status_code=400, detail="incident not found") async with pool.acquire() as conn: row = await conn.fetchrow( """ INSERT INTO tasks (title, incident_id, status) VALUES ($1, $2::uuid, 'open') RETURNING id, incident_id, title, status, created_at """, body.title.strip(), body.incident_id, ) return { "id": str(row["id"]), "incident_id": str(row["incident_id"]) if row["incident_id"] else None, "title": row["title"], "status": row["status"], "created_at": row["created_at"].isoformat() if row["created_at"] else None, } @router.get("/{task_id}") async def get_task_api(task_id: UUID, pool: asyncpg.Pool | None = Depends(get_pool)): if pool is None: raise HTTPException(status_code=503, detail="database disabled") async with pool.acquire() as conn: row = await conn.fetchrow( """ SELECT id, incident_id, title, status, created_at FROM tasks WHERE id = $1::uuid """, task_id, ) if not row: raise HTTPException(status_code=404, detail="not found") return { "id": str(row["id"]), "incident_id": str(row["incident_id"]) if row["incident_id"] else None, "title": row["title"], "status": row["status"], "created_at": row["created_at"].isoformat() if row["created_at"] else None, } @router.patch("/{task_id}") async def patch_task_api( task_id: UUID, body: TaskPatch, pool: asyncpg.Pool | None = Depends(get_pool), ): if pool is None: raise HTTPException(status_code=503, detail="database disabled") if body.title is None and body.status is None: raise HTTPException(status_code=400, detail="no fields to update") async with pool.acquire() as conn: row = await conn.fetchrow( """ UPDATE tasks SET title = COALESCE($2, title), status = COALESCE($3, status) WHERE id = $1::uuid RETURNING id, incident_id, title, status, created_at """, task_id, body.title.strip() if body.title is not None else None, body.status, ) if not row: raise HTTPException(status_code=404, detail="not found") return { "id": str(row["id"]), "incident_id": str(row["incident_id"]) if row["incident_id"] else None, "title": row["title"], "status": row["status"], "created_at": row["created_at"].isoformat() if row["created_at"] else None, } @ui_router.get("/", response_class=HTMLResponse) async def tasks_ui_home(request: Request): pool = get_pool(request) rows_html = "" err = "" if pool is None: err = "

База данных не настроена.

" else: try: async with pool.acquire() as conn: rows = await conn.fetch( """ SELECT t.id, t.title, t.status, t.incident_id, t.created_at FROM tasks t ORDER BY t.created_at DESC LIMIT 100 """ ) for r in rows: iid = str(r["incident_id"])[:8] + "…" if r["incident_id"] else "—" rows_html += ( "" f"{html.escape(str(r['id']))[:8]}…" f"{html.escape(r['title'])}" f"{html.escape(r['status'])}" f"{html.escape(iid)}" "" ) except Exception as e: err = f"

{html.escape(str(e))}

" inner = f"""

Задачи

{err} {rows_html or ''}
IDЗаголовокСтатусИнцидент
Пока нет задач
""" return HTMLResponse( wrap_module_html_page( document_title="Задачи — onGuard24", current_slug="tasks", main_inner_html=inner, ) )