"""FlowDeck — Board Kanban.""" from __future__ import annotations import json import logging from datetime import datetime from fastapi import APIRouter, Request, HTTPException from fastapi.responses import HTMLResponse from app.db import get_conn from app.services.gitea_client import gitea logger = logging.getLogger(__name__) router = APIRouter(tags=["board"], prefix="/board") def _ensure_board(owner: str, repo: str) -> dict: """Get or create a board row, return columns config.""" with get_conn() as conn: row = conn.execute( "SELECT id, columns_json FROM boards WHERE project_owner=? AND project_name=?", (owner, repo), ).fetchone() if not row: cur = conn.execute( "INSERT INTO boards (project_owner, project_name) VALUES (?, ?)", (owner, repo), ) conn.commit() return {"id": cur.lastrowid, "columns": ["Backlog", "À faire", "En cours", "Révision", "Terminé"]} return {"id": row["id"], "columns": json.loads(row["columns_json"])} def _get_cards(board_id: int) -> list[dict]: """Get all cards for a board, ordered by column + position.""" with get_conn() as conn: rows = conn.execute( "SELECT * FROM cards WHERE board_id=? ORDER BY column_name, position", (board_id,), ).fetchall() return [dict(r) for r in rows] @router.get("/{owner}/{repo}", response_class=HTMLResponse) async def board( request: Request, owner: str, repo: str, milestone: str = "", label: str = "", assignee: str = "", fragment: bool = False, ): """Kanban board for a Gitea project.""" try: issues = await gitea.get_issues(owner, repo, state="all") labels_list = await gitea.get_labels(owner, repo) milestones = await gitea.get_milestones(owner, repo) if milestone: issues = [i for i in issues if i.get("milestone", {}).get("title") == milestone] if label: issues = [i for i in issues if any(lbl["name"] == label for lbl in i.get("labels", []))] if assignee: issues = [i for i in issues if i.get("assignee", {}).get("login") == assignee] prs = [i for i in issues if i.get("pull_request")] issues_only = [i for i in issues if not i.get("pull_request")] board_info = _ensure_board(owner, repo) columns = board_info["columns"] cards = _get_cards(board_info["id"]) column_map: dict[str, list[dict]] = {col: [] for col in columns} card_by_issue = {c["gitea_issue_id"]: c for c in cards if c["gitea_issue_id"]} for issue in issues_only: col = _issue_column(issue, columns, board_info["id"]) issue["_col"] = col issue["_card"] = card_by_issue.get(issue["number"]) column_map[col].append(issue) except Exception as e: from httpx import HTTPStatusError if isinstance(e, HTTPStatusError) and e.response is not None and e.response.status_code == 404: return HTMLResponse( '
' f'

Projet introuvable

' f'

{owner}/{repo} n\'existe pas sur Gitea.

' 'Retour aux projets
', status_code=404) logger.error("Board error: %s", e) return HTMLResponse(f"

Erreur: {e}

", status_code=500) from jinja2 import Environment, FileSystemLoader from datetime import datetime from app.config import settings from app.auth.session import SessionManager env = Environment(loader=FileSystemLoader("app/templates")) now_str = datetime.now().isoformat()[:10] user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) ctx = dict( request=request, owner=owner, repo=repo, columns=columns, column_map=column_map, labels=labels_list, milestones=milestones, current_milestone=milestone, current_label=label, current_assignee=assignee, prs=prs, settings=settings, now=now_str, user=user, ) if fragment: template = env.get_template("board_fragment.html") else: template = env.get_template("board.html") return template.render(**ctx) def _issue_column(issue: dict, columns: list[str], board_id: int) -> str: """Determine which column an issue belongs to.""" # ponytail: closed = Terminé, else check label mapping if issue.get("state") == "closed": return "Terminé" if "Terminé" in columns else columns[-1] # Check label-to-column mapping in DB issue_labels = [lbl["name"] for lbl in issue.get("labels", [])] with get_conn() as conn: for lbl in issue_labels: row = conn.execute( "SELECT column_name FROM col_mapping WHERE board_id=? AND gitea_label=?", (board_id, lbl), ).fetchone() if row and row["column_name"] in columns: return row["column_name"] # Default: Backlog return columns[0] if columns else "Backlog"