dmslanes/lanes
Lanes - team task boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix dmslanes/lanes
app/web/routes.py
"""HTTP door: thin FastAPI routes over the same use cases the MCP tools call."""
from __future__ import annotations
from datetime import date
from pathlib import Path
from fastapi import APIRouter, Depends, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from app.application.use_cases import BoardService
from app.application.views import (
activity_view, board_view, card_view, cards_with_context, full_board_view, list_view, member_view,
)
from app.infrastructure.identity import email_from_headers
STATIC = Path(__file__).parent / "static"
class NewWorkspace(BaseModel):
name: str = Field(max_length=200)
class Invite(BaseModel):
email: str
role: str = "member"
class NewBoard(BaseModel):
workspace_id: str
name: str = Field(max_length=200)
class NewList(BaseModel):
name: str = Field(max_length=200)
class NewCard(BaseModel):
list: str
title: str = Field(max_length=200)
description: str = Field("", max_length=5000)
assignee: str | None = None
due_date: date | None = None
labels: list[str] = []
class CardEdit(BaseModel):
title: str | None = Field(None, max_length=200)
description: str | None = Field(None, max_length=5000)
due_date: date | None = None
labels: list[str] | None = None
clear_due_date: bool = False
version: int | None = None
class Move(BaseModel):
to_list: str
position: int | None = None
class Assign(BaseModel):
assignee: str | None = None
class Block(BaseModel):
blocked_by: str
def build_router(svc: BoardService) -> APIRouter:
r = APIRouter()
def me(request: Request) -> str:
return email_from_headers(request.headers)
@r.get("/", include_in_schema=False)
def index():
return FileResponse(STATIC / "index.html")
@r.get("/api/me")
def whoami(actor: str = Depends(me)):
return {"email": actor, "workspaces": [{"id": ws.id, "name": ws.name, "role": role.value}
for ws, role in svc.list_workspaces(actor)]}
@r.post("/api/workspaces", status_code=201)
def create_workspace(body: NewWorkspace, actor: str = Depends(me)):
ws = svc.create_workspace(actor, body.name)
return {"id": ws.id, "name": ws.name, "role": "owner"}
@r.get("/api/workspaces/{workspace_id}/members")
def members(workspace_id: str, actor: str = Depends(me)):
return [member_view(m) for m in svc.list_members(actor, workspace_id)]
@r.post("/api/workspaces/{workspace_id}/members", status_code=201)
def invite(workspace_id: str, body: Invite, actor: str = Depends(me)):
return member_view(svc.invite_member(actor, workspace_id, body.email, body.role))
@r.delete("/api/workspaces/{workspace_id}/members/{email}", status_code=204)
def remove(workspace_id: str, email: str, actor: str = Depends(me)):
svc.remove_member(actor, workspace_id, email)
@r.get("/api/boards")
def boards(actor: str = Depends(me)):
return [board_view(b) for b in svc.list_boards(actor)]
@r.post("/api/boards", status_code=201)
def create_board(body: NewBoard, actor: str = Depends(me)):
return board_view(svc.create_board(actor, body.workspace_id, body.name))
@r.get("/api/boards/{board_id}")
def board(board_id: str, actor: str = Depends(me)):
return full_board_view(svc.get_board(actor, board_id))
@r.post("/api/boards/{board_id}/lists", status_code=201)
def add_list(board_id: str, body: NewList, actor: str = Depends(me)):
return list_view(svc.add_list(actor, board_id, body.name))
@r.get("/api/boards/{board_id}/activity")
def board_activity(board_id: str, actor: str = Depends(me)):
return [activity_view(a) for a in svc.board_activity(actor, board_id)]
@r.post("/api/boards/{board_id}/cards", status_code=201)
def add_card(board_id: str, body: NewCard, actor: str = Depends(me)):
return card_view(svc.add_card(actor, board_id, body.list, body.title, body.description,
body.assignee, body.due_date, body.labels))
@r.get("/api/cards/{ref}")
def card(ref: str, actor: str = Depends(me)):
return cards_with_context(svc, [svc.get_card(actor, ref)])[0]
@r.patch("/api/cards/{ref}")
def edit_card(ref: str, body: CardEdit, actor: str = Depends(me)):
fields = {k: v for k, v in body.model_dump(exclude={"version", "clear_due_date"}).items() if v is not None}
if body.clear_due_date:
fields["due_date"] = None
return card_view(svc.update_card(actor, ref, expected_version=body.version, **fields))
@r.post("/api/cards/{ref}/move")
def move(ref: str, body: Move, actor: str = Depends(me)):
return card_view(svc.move_card(actor, ref, body.to_list, body.position))
@r.post("/api/cards/{ref}/assign")
def assign(ref: str, body: Assign, actor: str = Depends(me)):
return card_view(svc.assign_card(actor, ref, body.assignee))
@r.delete("/api/cards/{ref}", status_code=204)
def delete(ref: str, actor: str = Depends(me)):
svc.delete_card(actor, ref)
@r.get("/api/cards/{ref}/history")
def history(ref: str, actor: str = Depends(me)):
return [activity_view(a) for a in svc.card_history(actor, ref)]
@r.post("/api/cards/{ref}/blocked-by")
def block(ref: str, body: Block, actor: str = Depends(me)):
return card_view(svc.block_card(actor, ref, body.blocked_by))
@r.delete("/api/cards/{ref}/blocked-by/{blocker}")
def unblock(ref: str, blocker: str, actor: str = Depends(me)):
return card_view(svc.unblock_card(actor, ref, blocker))
@r.get("/api/cards/{ref}/blockers")
def blockers(ref: str, actor: str = Depends(me)):
return cards_with_context(svc, svc.blockers(actor, ref))
@r.post("/api/cards/{ref}/checklist")
def checklist(ref: str, actor: str = Depends(me)):
return card_view(svc.card_to_checklist(actor, ref))
@r.get("/api/cards/{ref}/similar")
def similar(ref: str, actor: str = Depends(me)):
pairs = svc.similar_cards(actor, ref)
return [{**v, "similarity": round(score, 3)}
for v, (_, score) in zip(cards_with_context(svc, [c for c, _ in pairs]), pairs)]
@r.get("/api/boards/{board_id}/summary")
def summary(board_id: str, actor: str = Depends(me)):
return {"summary": svc.board_summary(actor, board_id)}
@r.get("/api/my-cards")
def my_cards(days: int | None = None, actor: str = Depends(me)):
return cards_with_context(svc, svc.my_cards(actor, days))
@r.get("/api/overdue")
def overdue(board_id: str | None = None, actor: str = Depends(me)):
return cards_with_context(svc, svc.overdue(actor, board_id))
@r.get("/api/search")
def search(q: str, actor: str = Depends(me)):
return cards_with_context(svc, svc.search_cards(actor, q))
return r