"""FlowDeck — API REST v1.""" from __future__ import annotations import json import logging from datetime import datetime from fastapi import APIRouter, HTTPException, Query from app.config import settings from app.db import get_conn from app.services.gitea_client import gitea logger = logging.getLogger(__name__) router = APIRouter(tags=["api"], prefix="/api") @router.get("/health") async def health(): """Health check: DB + Gitea connectivity.""" db_ok = False gitea_ok = False try: with get_conn() as conn: conn.execute("SELECT 1") db_ok = True except Exception: pass try: await gitea.get_user_repos(page=1, limit=1) gitea_ok = True except Exception: pass return { "status": "ok" if (db_ok and gitea_ok) else "degraded", "version": "0.2.0", "db": db_ok, "gitea": gitea_ok, } @router.get("/stats") async def stats(): """Global stats for dashboard.""" with get_conn() as conn: board_count = conn.execute("SELECT COUNT(*) as c FROM boards").fetchone()["c"] card_count = conn.execute("SELECT COUNT(*) as c FROM cards").fetchone()["c"] note_count = conn.execute("SELECT COUNT(*) as c FROM notes").fetchone()["c"] return { "boards": board_count, "cards": card_count, "notes": note_count, } @router.get("/projects") async def projects(search: str = Query(default=""), show_archived: bool = Query(default=False)): """List Gitea projects (JSON).""" try: repos = await gitea.get_user_repos(page=1, limit=50) orgs = await gitea.get_user_orgs() for org in orgs: try: org_repos = await gitea.get_org_repos(org["username"]) for r in org_repos: r["_org"] = org["username"] repos.extend(org_repos) except Exception: pass if search: q = search.lower() repos = [r for r in repos if q in r.get("full_name", "").lower() or q in (r.get("description") or "").lower()] if not show_archived: repos = [r for r in repos if not r.get("archived", False)] repos.sort(key=lambda r: r.get("updated_at", ""), reverse=True) except Exception: repos = [] return {"projects": repos} @router.post("/move") async def move_card( owner: str = Query(...), repo: str = Query(...), issue_id: int = Query(...), column: str = Query(...), ): """Move a card to a column. Updates DB position + Gitea labels if mapped.""" with get_conn() as conn: board = conn.execute( "SELECT id FROM boards WHERE project_owner=? AND project_name=?", (owner, repo), ).fetchone() if not board: raise HTTPException(status_code=404, detail="Board not found") board_id = board["id"] # Upsert card existing = conn.execute( "SELECT id FROM cards WHERE board_id=? AND gitea_issue_id=?", (board_id, issue_id), ).fetchone() if existing: conn.execute( "UPDATE cards SET column_name=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (column, existing["id"]), ) else: conn.execute( "INSERT INTO cards (board_id, gitea_issue_id, column_name, position) VALUES (?, ?, ?, 0)", (board_id, issue_id, column), ) # Check label mapping for sync to Gitea mapping = conn.execute( "SELECT gitea_label, close_issue FROM col_mapping WHERE board_id=? AND column_name=?", (board_id, column), ).fetchone() conn.commit() # Sync to Gitea if mapping: try: if mapping["close_issue"]: await gitea.close_issue(owner, repo, issue_id) else: # Get current labels, add mapped label issue = await gitea.get_issue(owner, repo, issue_id) current_labels = [lbl["name"] for lbl in issue.get("labels", [])] # Remove other status labels, add new one status_labels = await _get_status_labels(owner, repo, board_id) filtered = [l for l in current_labels if l not in status_labels] filtered.append(mapping["gitea_label"]) await gitea.update_issue_labels(owner, repo, issue_id, filtered) except Exception as e: logger.warning("Gitea sync failed: %s", e) return {"status": "ok", "issue_id": issue_id, "column": column} async def _get_status_labels(owner: str, repo: str, board_id: int) -> list[str]: """Get all status labels configured for this board.""" with get_conn() as conn: rows = conn.execute( "SELECT gitea_label FROM col_mapping WHERE board_id=?", (board_id,), ).fetchall() return [r["gitea_label"] for r in rows] @router.post("/col-mapping") async def set_col_mapping( owner: str = Query(...), repo: str = Query(...), column: str = Query(...), gitea_label: str = Query(...), close_issue: bool = Query(default=False), ): """Set column-to-label mapping.""" with get_conn() as conn: board = conn.execute( "SELECT id FROM boards WHERE project_owner=? AND project_name=?", (owner, repo), ).fetchone() if not board: raise HTTPException(status_code=404, detail="Board not found") board_id = board["id"] conn.execute( """INSERT INTO col_mapping (board_id, column_name, gitea_label, close_issue) VALUES (?, ?, ?, ?) ON CONFLICT(board_id, column_name) DO UPDATE SET gitea_label=excluded.gitea_label, close_issue=excluded.close_issue""", (board_id, column, gitea_label, int(close_issue)), ) conn.commit() return {"status": "ok", "column": column, "label": gitea_label} @router.get("/board-config/{owner}/{repo}") async def get_board_config(owner: str, repo: str): """Get board columns and WIP limits.""" with get_conn() as conn: board = conn.execute( "SELECT * FROM boards WHERE project_owner=? AND project_name=?", (owner, repo), ).fetchone() if not board: return {"columns": ["Backlog", "À faire", "En cours", "Révision", "Terminé"], "wip_limits": {}} mappings = conn.execute( "SELECT * FROM col_mapping WHERE board_id=?", (board["id"],) ).fetchall() return { "columns": json.loads(board["columns_json"]), "wip_limits": json.loads(board["wip_limits_json"] or "{}"), "col_mappings": [dict(m) for m in mappings], } @router.post("/board-config/{owner}/{repo}") async def update_board_config( owner: str, repo: str, columns: str = Query(default=""), wip_limits: str = Query(default=""), ): """Update board columns and WIP limits.""" with get_conn() as conn: board = conn.execute( "SELECT id FROM boards WHERE project_owner=? AND project_name=?", (owner, repo), ).fetchone() if not board: conn.execute( "INSERT INTO boards (project_owner, project_name, columns_json, wip_limits_json) VALUES (?, ?, ?, ?)", (owner, repo, columns, wip_limits), ) else: conn.execute( "UPDATE boards SET columns_json=?, wip_limits_json=? WHERE id=?", (columns, wip_limits, board["id"]), ) conn.commit() return {"status": "ok"}