rahulpawar/taskdeck

Lanebook, a team task board (boards, lists, cards, roles, history) you run from the web or from Claude/ChatGPT over MCP. FastAPI + DataK3 + appid + Models.

A remix template · 0 remixes · Remix rahulpawar/taskdeck

app/boards/doors/web.py

"""The web door: JSON routes plus the board page. Every route calls a use case and nothing else."""
from typing import List, Optional

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from pydantic import BaseModel

from boards.application.use_cases import Boards, NotFound
from boards.doors.identity import person_from
from boards.doors.page import PAGE
from boards.domain.model import Forbidden, Invalid, Person


def build_router(boards: Boards) -> APIRouter:
    r = APIRouter()

    def me(request: Request) -> Person:
        p = person_from(request.headers)
        if p is None:
            raise HTTPException(401, "sign in first: /.dodil/auth/login")
        return p

    def run(fn, *args, **kw):
        try:
            return fn(*args, **kw)
        except NotFound as e:
            raise HTTPException(404, str(e))
        except Forbidden as e:
            raise HTTPException(403, str(e))
        except Invalid as e:
            raise HTTPException(422, str(e))

    class Title(BaseModel):
        title: str

    class Name(BaseModel):
        name: str

    class Member(BaseModel):
        email: str
        role: str = "member"

    class CardIn(BaseModel):
        title: str
        list: str = ""
        description: str = ""
        assignee: Optional[str] = None
        due_date: Optional[str] = None
        labels: Optional[List[str]] = None

    class CardPatch(BaseModel):
        title: Optional[str] = None
        description: Optional[str] = None
        assignee: Optional[str] = None
        due_date: Optional[str] = None
        labels: Optional[List[str]] = None

    class Move(BaseModel):
        list: str
        position: Optional[float] = None

    class Blockers(BaseModel):
        blockers: List[str] = []

    class Apply(BaseModel):
        apply: bool = False

    @r.get("/api/me")
    def who(request: Request):
        return {"email": me(request).email}

    @r.get("/api/boards")
    def my_boards(request: Request):
        return run(boards.my_boards, me(request))

    @r.post("/api/boards", status_code=201)
    def create_board(body: Title, request: Request):
        return run(boards.create_board, me(request), body.title)

    @r.get("/api/boards/{board_id}")
    def show_board(board_id: str, request: Request):
        return run(boards.show_board, me(request), board_id)

    @r.patch("/api/boards/{board_id}")
    def rename_board(board_id: str, body: Title, request: Request):
        return run(boards.rename_board, me(request), board_id, body.title)

    @r.post("/api/boards/{board_id}/members", status_code=201)
    def add_member(board_id: str, body: Member, request: Request):
        return run(boards.add_member, me(request), board_id, body.email, body.role)

    @r.delete("/api/boards/{board_id}/members/{email}")
    def remove_member(board_id: str, email: str, request: Request):
        return run(boards.remove_member, me(request), board_id, email)

    @r.post("/api/boards/{board_id}/lists", status_code=201)
    def add_list(board_id: str, body: Name, request: Request):
        return run(boards.add_list, me(request), board_id, body.name)

    @r.patch("/api/boards/{board_id}/lists/{list_id}")
    def rename_list(board_id: str, list_id: str, body: Name, request: Request):
        return run(boards.rename_list, me(request), board_id, list_id, body.name)

    @r.delete("/api/boards/{board_id}/lists/{list_id}")
    def delete_list(board_id: str, list_id: str, request: Request):
        return run(boards.delete_list, me(request), board_id, list_id)

    @r.get("/api/boards/{board_id}/activity")
    def activity(board_id: str, request: Request, limit: int = 40):
        return run(boards.history, me(request), board_id, limit)

    @r.get("/api/boards/{board_id}/summary")
    def summary(board_id: str, request: Request):
        return run(boards.board_summary, me(request), board_id)

    @r.post("/api/boards/{board_id}/cards", status_code=201)
    def add_card(board_id: str, body: CardIn, request: Request):
        return run(boards.add_card, me(request), board_id, body.title, body.list,
                   body.description, body.assignee, body.due_date, body.labels)

    @r.get("/api/cards/{card_id}")
    def show_card(card_id: str, request: Request):
        return run(boards.show_card, me(request), card_id)

    @r.patch("/api/cards/{card_id}")
    def update_card(card_id: str, body: CardPatch, request: Request):
        sent = body.model_fields_set
        clear = tuple(f for f in ("assignee", "due_date", "labels", "description")
                      if f in sent and getattr(body, f) in (None, "", []))
        val = lambda f: getattr(body, f) if f in sent and f not in clear else None
        return run(boards.update_card, me(request), card_id, None, title=val("title"),
                   description=val("description"), assignee=val("assignee"),
                   due_date=val("due_date"), labels=val("labels"), clear=clear)

    @r.post("/api/cards/{card_id}/move")
    def move_card(card_id: str, body: Move, request: Request):
        return run(boards.move_card, me(request), card_id, body.list, None, body.position)

    @r.delete("/api/cards/{card_id}")
    def delete_card(card_id: str, request: Request):
        return run(boards.delete_card, me(request), card_id)

    @r.put("/api/cards/{card_id}/blocked-by")
    def set_blocked_by(card_id: str, body: Blockers, request: Request):
        return run(boards.set_blocked_by, me(request), card_id, body.blockers)

    @r.get("/api/cards/{card_id}/blocking")
    def blocking(card_id: str, request: Request):
        return run(boards.what_is_blocking, me(request), card_id)

    @r.post("/api/cards/{card_id}/checklist")
    def checklist(card_id: str, body: Apply, request: Request):
        return run(boards.suggest_checklist, me(request), card_id, body.apply)

    @r.get("/api/cards/{card_id}/similar")
    def similar(card_id: str, request: Request):
        return run(boards.similar_cards, me(request), card_id)

    @r.get("/api/plate")
    def plate(request: Request, days: int = 7):
        return run(boards.my_plate, me(request), days)

    @r.get("/api/overdue")
    def overdue(request: Request):
        return run(boards.overdue, me(request))

    @r.get("/api/search")
    def search(q: str, request: Request):
        return run(boards.search_cards, me(request), q)

    @r.get("/", response_class=HTMLResponse)
    def page(request: Request):
        if person_from(request.headers) is None:
            return RedirectResponse("/.dodil/auth/login?return_to=/", status_code=303)
        return PAGE

    return r