""" Kanbante — Kanban project manager integrated with Gitea. FastAPI backend serving HTML + JSON API. """ import os from pathlib import Path from fastapi import FastAPI, Request, Form from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from app.gitea_client import GiteaClient from app.models import ( init_db, get_board_state, move_issue, set_notes, get_notes, KANBAN_COLUMNS, get_stats, ) from fastapi.templating import Jinja2Templates as _Jinja2Templates from jinja2 import Environment, FileSystemLoader, select_autoescape # ── App setup ────────────────────────────────────────────── BASE = Path(__file__).resolve().parent.parent app = FastAPI(title="Kanbante", version="0.1.0") app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static") gitea = GiteaClient() # ── Templates (workaround Starlette 1.x + Jinja2 cache bug) ── _tpl_env = Environment( loader=FileSystemLoader(str(BASE / "app" / "templates")), autoescape=select_autoescape(["html"]), ) def render_template(name: str, **context) -> HTMLResponse: """Render a Jinja2 template directly (bypasses Starlette's buggy caching).""" template = _tpl_env.get_template(name) html = template.render(**context) return HTMLResponse(html) # Column display names COLUMN_LABELS = { "backlog": "📥 Backlog", "todo": "📋 À faire", "in_progress": "🚧 En cours", "review": "🔍 Révision", "done": "✅ Terminé", } COLUMN_COLORS = { "backlog": "#6b7280", "todo": "#3b82f6", "in_progress": "#f59e0b", "review": "#8b5cf6", "done": "#10b981", } @app.on_event("startup") async def startup(): init_db() # ── Helpers ──────────────────────────────────────────────── def _is_active(repo: dict) -> bool: """A repo is active if updated in 2026.""" return repo.get("updated_at", "")[:4] == "2026" def _merge_issues_with_board(issues: list[dict], board: dict[str, list[dict]]) -> dict[str, list[dict]]: """Merge Gitea issues into kanban columns based on board state.""" columns = {col: [] for col in KANBAN_COLUMNS} # Build a lookup of board state by issue number issue_positions: dict[int, str] = {} for col_name, cards in board.items(): for card in cards: issue_positions[card["issue_number"]] = col_name for issue in issues: num = issue["number"] col = issue_positions.get(num, "backlog") columns[col].append(issue) return columns # ── Routes: Pages ───────────────────────────────────────── @app.get("/", response_class=HTMLResponse) async def dashboard(request: Request): """Main dashboard — all projects at a glance.""" repos = gitea.search_repos() active = [r for r in repos if _is_active(r)] inactive = [r for r in repos if not _is_active(r)] stats = get_stats() ctx = { "active_repos": list(active), "inactive_repos": list(inactive), "stats": {k: dict(v) for k, v in stats.items()}, "COLUMN_LABELS": dict(COLUMN_LABELS), "COLUMN_COLORS": dict(COLUMN_COLORS), } return render_template("dashboard.html", **ctx) @app.get("/board/{owner}/{repo}", response_class=HTMLResponse) async def board(request: Request, owner: str, repo: str): """Kanban board for a specific repo.""" full_name = f"{owner}/{repo}" issues = gitea.get_repo_issues(owner, repo, state="open") board_state = get_board_state(full_name) columns = _merge_issues_with_board(issues, board_state) notes = get_notes(full_name) or "" # Get repo info repo_info = {} try: import httpx client = gitea.client r = client.get(f"/repos/{owner}/{repo}") if r.status_code == 200: repo_info = r.json() except Exception: pass return render_template("board.html", owner=owner, repo=repo, full_name=full_name, columns=columns, column_labels=COLUMN_LABELS, column_colors=COLUMN_COLORS, notes=notes, repo_info=repo_info, ) # ── Routes: API ──────────────────────────────────────────── @app.post("/api/move") async def api_move( repo_full_name: str = Form(...), issue_number: int = Form(...), to_column: str = Form(...), ): """Move an issue to a new column.""" if to_column not in KANBAN_COLUMNS: return {"ok": False, "error": "Invalid column"} move_issue(repo_full_name, issue_number, to_column) return {"ok": True} @app.post("/api/notes") async def api_notes( repo_full_name: str = Form(...), note: str = Form(""), ): """Save project notes.""" set_notes(repo_full_name, note) return {"ok": True} @app.get("/api/stats") async def api_stats(): """Get project stats.""" return get_stats() @app.get("/api/projects") async def api_projects(): """Get all projects (JSON).""" repos = gitea.search_repos() return { "active": [r for r in repos if _is_active(r)], "inactive": [r for r in repos if not _is_active(r)], } @app.get("/api/health") async def health(): return {"status": "ok", "version": "0.1.0"}