Files
DevOpsDash/app/routers/tasks.py

229 lines
7.2 KiB
Python
Raw Normal View History

"""Task board router — Taskz CRUD over Redis."""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from app.redis_client import (
get_redis,
get_board,
list_boards,
save_board,
save_task,
BOARD_KEY_PREFIX,
BOARD_INDEX_KEY,
)
router = APIRouter(prefix="/api/v1", tags=["tasks"])
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _slugify(text: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")[:20]
def _make_board_code(project: str, title: str) -> str:
proj = re.sub(r"[^a-z0-9]+", "", project.lower())[:12].upper()
title_word = re.sub(r"[^a-z0-9]+", "", title.lower().split()[0])[:6].upper()
return f"{proj}-{title_word}"
def _next_task_number(board: Dict[str, Any]) -> int:
tasks = board.get("tasks", [])
return max((t.get("number", 0) for t in tasks), default=0) + 1
# ── Schemas ───────────────────────────────────────────────────────────────────
class BoardCreate(BaseModel):
project: str
title: str
description: str = ""
tags: List[str] = []
class TaskCreate(BaseModel):
title: str
description: str = ""
priority: str = "medium"
tags: List[str] = []
scope: Optional[str] = None
class TaskUpdate(BaseModel):
status: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
findings: Optional[str] = None
priority: Optional[str] = None
tags: Optional[List[str]] = None
class BoardStatusUpdate(BaseModel):
new_status: str = "completed"
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("/boards")
def api_list_boards(project: Optional[str] = None, status: Optional[str] = None):
r = get_redis()
boards = list_boards(r, project=project, status=status)
summaries = []
for b in boards:
tasks = b.get("tasks", [])
done = sum(1 for t in tasks if t["status"] == "done")
summaries.append({
"board_id": b["board_id"],
"board_code": b.get("board_code", ""),
"project": b["project"],
"title": b["title"],
"description": b.get("description", ""),
"status": b.get("status", "active"),
"tags": b.get("tags", []),
"task_count": len(tasks),
"done_count": done,
"created_at": b.get("created_at", ""),
"updated_at": b.get("updated_at", ""),
})
return {"boards": summaries, "total": len(summaries)}
@router.post("/boards", status_code=201)
def api_create_board(body: BoardCreate):
r = get_redis()
code = _make_board_code(body.project, body.title)
board_id = f"{_slugify(body.project)}-{_slugify(body.title)}-{_now()[:10].replace('-', '')}"
board = {
"board_id": board_id,
"board_code": code,
"project": body.project,
"title": body.title,
"description": body.description,
"status": "active",
"tags": body.tags,
"tasks": [],
"created_at": _now(),
"updated_at": _now(),
}
save_board(r, board)
return {"board_id": board_id, "board_code": code}
@router.get("/boards/{board_id}")
def api_get_board(board_id: str):
r = get_redis()
board = get_board(r, board_id)
if not board:
raise HTTPException(status_code=404, detail="Board not found")
return board
@router.patch("/boards/{board_id}/status")
def api_close_board(board_id: str, body: BoardStatusUpdate):
r = get_redis()
board = get_board(r, board_id)
if not board:
raise HTTPException(status_code=404, detail="Board not found")
board["status"] = body.new_status
board["updated_at"] = _now()
save_board(r, board)
return {"board_id": board_id, "status": body.new_status}
@router.post("/boards/{board_id}/tasks", status_code=201)
def api_add_task(board_id: str, body: TaskCreate):
r = get_redis()
board = get_board(r, board_id)
if not board:
raise HTTPException(status_code=404, detail="Board not found")
code = board.get("board_code", board_id.upper()[:10])
num = _next_task_number(board)
task_id = f"{code}-{num:03d}"
task: Dict[str, Any] = {
"task_id": task_id,
"board_id": board_id,
"number": num,
"title": body.title,
"description": body.description,
"priority": body.priority,
"status": "pending",
"tags": body.tags,
"scope": body.scope,
"findings": [],
"created_at": _now(),
"updated_at": _now(),
}
save_task(r, task)
return {"task_id": task_id}
@router.patch("/tasks/{task_id}")
def api_update_task(task_id: str, body: TaskUpdate):
r = get_redis()
# find the task inside any board
from app.redis_client import list_boards as _lb, TASK_KEY_PREFIX
raw = r.get(f"{TASK_KEY_PREFIX}{task_id}")
if not raw:
# fallback: scan boards
for board in _lb(r):
task = next((t for t in board.get("tasks", []) if t["task_id"] == task_id), None)
if task:
raw = json.dumps(task)
break
if not raw:
raise HTTPException(status_code=404, detail="Task not found")
task = json.loads(raw)
valid_statuses = {"pending", "in_progress", "done", "blocked", "wont_do"}
valid_priorities = {"low", "medium", "high", "critical"}
if body.status:
if body.status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"Invalid status: {body.status}")
task["status"] = body.status
if body.status == "done":
task["completed_at"] = _now()
if body.title:
task["title"] = body.title
if body.description is not None:
task["description"] = body.description
if body.findings:
findings = task.setdefault("findings", [])
findings.append({"text": body.findings, "at": _now()})
if body.priority:
if body.priority not in valid_priorities:
raise HTTPException(status_code=400, detail=f"Invalid priority: {body.priority}")
task["priority"] = body.priority
if body.tags is not None:
task["tags"] = body.tags
task["updated_at"] = _now()
save_task(r, task)
return {"task_id": task_id, "status": task["status"]}
@router.delete("/tasks/{task_id}")
def api_delete_task(task_id: str):
r = get_redis()
from app.redis_client import TASK_KEY_PREFIX
raw = r.get(f"{TASK_KEY_PREFIX}{task_id}")
if not raw:
raise HTTPException(status_code=404, detail="Task not found")
task = json.loads(raw)
board = get_board(r, task["board_id"])
if board:
board["tasks"] = [t for t in board.get("tasks", []) if t["task_id"] != task_id]
board["updated_at"] = _now()
save_board(r, board)
r.delete(f"{TASK_KEY_PREFIX}{task_id}")
return {"deleted": task_id}