akif/arclet-boards

Arclet Boards — private Kanban workspaces with AppID, DataK3, shared web and MCP actions.

A remix template · 1 remixes · Remix akif/arclet-boards

app/web/routes.py

"""HTTP adapter: validated action contracts and permissions live in application/."""
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, ConfigDict, Field

from app.application.contracts import READS, WRITES
from app.domain.model import Invalid, Person

STATIC = Path(__file__).parent / "static"


class ActionBody(BaseModel):
    model_config = ConfigDict(extra="forbid")
    arguments: dict = Field(default_factory=dict)


def build_router(boards):
    router = APIRouter()

    def call(request, action, payload=None):
        return boards.call(action, request.state.person, payload,
                           request.headers.get("idempotency-key"))

    @router.get("/api/session")
    def session(request: Request):
        person: Person = request.state.person
        return {"sub": person.sub, "email": person.email, "product": "Arclet Boards"}

    @router.get("/api/boards")
    def list_boards(request: Request):
        return call(request, "list_boards")

    @router.get("/api/boards/{board_id}")
    def get_board(board_id: str, request: Request):
        return call(request, "get_board", {"board_id": board_id})

    @router.get("/api/cards/{card_id}")
    def get_card(card_id: str, request: Request):
        return call(request, "get_card", {"card_id": card_id})

    @router.get("/api/workspaces")
    def list_workspaces(request: Request):
        return call(request, "list_workspaces")

    @router.get("/api/tasks/{kind}")
    def tasks(kind: str, request: Request):
        if kind not in ("mine", "overdue"):
            raise Invalid("Choose mine or overdue")
        return call(request, "list_my_cards" if kind == "mine" else "list_overdue")

    @router.post("/api/actions/{action}")
    def action(action: str, body: ActionBody, request: Request):
        if action not in READS and action not in WRITES:
            raise Invalid("Unknown action")
        return call(request, action, body.arguments)

    @router.get("/")
    def index():
        return FileResponse(STATIC / "index.html", media_type="text/html")

    return router