sawera/sawera-boards

Sawera Boards - a team task board for the Dodil Boards Challenge. Boards, lists, cards, roles, activity log, MCP actions and AI extras. FastAPI + DataK3 + appid + Models + MCP.

A remix template · 0 remixes · Remix sawera/sawera-boards

README.md

Sawera Boards — a team task board on Dodil

A Trello-style team task board that anyone can remix into their own Dodil organization and have running in minutes. Boards → lists → cards, real roles, an activity log, and the same actions from a web page and from Claude or ChatGPT over MCP — because both doors call one shared set of use cases.

Built for the Dodil Boards Challenge. MIT licensed.

What it does

The core (all of it, working):

  • People and roles. The owner invites members. Roles: owner, member, viewer. A viewer can't change anything; only the owner manages members. Nobody can see a board they are not on — it doesn't exist for them.
  • Boards → lists → cards. Every board starts with To do / Doing / Done, and lists can be added, renamed and removed. A card has a title, description, assignee, due date and labels, and moves between lists by drag and drop on the web page (or by name over MCP). The owner can also delete a board — every list, card and comment goes with it.
  • Activity log. Every change is recorded — who, when, what changed — and readable on the web page and over MCP.
  • Card comments. Each card carries its own discussion: members comment on the card detail (web) or through MCP (add_comment / card_comments), viewers can read but not write, and a 💬 count on the card face shows where the conversation is.
  • MCP actions, not table access. my_cards ("what's on my plate this week?"), add_card, move_card, assign_card, overdue_cards ("what's overdue?"), what_is_blocking, and more — always scoped to the boards the signed-in person can see.

Extras:

  • Overdue flags — computed on read, never stored, so they can't go stale. Cards in a done-like list (Done, Complete, Shipped…) are never overdue.
  • Blocked-by links — a card can be blocked by other cards on its board; circles are refused by the domain rules, and "what is blocking the launch?" walks the chain. Deleting a blocker frees the cards behind it.
  • AI (Dodil Models) — suggest_checklist turns a card title into a checklist; daily_summary writes a short stand-up summary of a board.
  • Search across your boards: word matches first, then semantic search — Dodil embeddings rank cards by meaning, so "money owed" finds "Send the invoice". A card's vector is computed when the card is saved (against a hash of its text, recomputed only when the text changes), so a search only has to embed the question and answers in seconds; if the AI is unavailable the word matches still come back.

Run it locally (no Dodil account needed)

cd app
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt pytest
python -m pytest -q ../tests      # the rules, through the use cases and both doors

To run the server against your remixed bucket, set BUCKET, DODIL_SERVICE_ACCOUNT_ID, DODIL_SERVICE_ACCOUNT_SECRET and DEV_USER=you@example.com (acts as that person without the gateway; local only), then uvicorn boards.main:app --port 8080.

Remix it into your own organization

dodil update                                   # CLI 0.1.89 or later
dodil git remix <org>/sawera-boards --to-org <your-org> --name <your-repo>

Keep <your-repo> to 18 characters or less (the data bucket is <repo>-data, max 25). The remix creates the repo, the DataK3 bucket with the tables from schema.sql, the <repo>-users pool, and a service account with data and Models access; CI then tests, builds and deploys it to Ignite.

After the remix — the one manual step: let people sign in. Add the app's callback URL to the new pool, exactly as dodil guide remix describes under "After the remix: let people sign in". Until that step is done, the app is up but sign-in can't complete.

Check any copy before or after remixing:

dodil git remix check <your-org>/<your-repo>   # must say: valid

Connect Claude or ChatGPT

The app serves MCP at https://<your-app-host>/mcp. Add it as a custom connector in Claude (or ChatGPT): it asks you to sign in through your pool, then approve the client once with dodil appid client list <repo>-users and dodil appid client approve <repo>-users <client-id>. Try: "What's on my plate this week?", "Add a card 'Send invoice' to Doing on the Ops board", "Move the invoice card to Done", "What's overdue?", "What is blocking the launch card?"

How it is built

Light Domain-Driven Design, one rule in one place:

app/boards/
  domain/model.py            Board, BoardList, Card, Role — the rules
                             (a viewer can't edit, only the owner manages
                             members, blocked-by circles are refused)
  application/use_cases.py   every action: create_board, add_list, add_card,
                             move_card, assign_card, my_cards, overdue_cards,
                             what_is_blocking, daily_summary …
  application/ports.py       what the use cases need (repository, AI assistant)
  infrastructure/dodil.py    service-account token, DataK3, Dodil Models
  infrastructure/repository.py  boards, lists, cards, members, activity (DataK3)
  doors/web.py               JSON routes + the board page  -> the use cases
  doors/page.py              the page itself (one HTML file, no build step)
  doors/mcp.py               MCP tools                     -> the same use cases
  doors/identity.py          who is calling (the gateway's X-Dodil-User header)
  main.py                    wires it together, /mcp, sign-in metadata, /healthz,
                             and a POST / self-check that proves the app can
                             reach its own data with its own credential
tests/test_boards.py         the rules, through the use cases and both doors
schema.sql                   the tables, created by the remix
.dodil/remix.yaml            what a remix creates
.dodil/deploy.yaml           how CI deploys every copy

Design decisions, briefly

  • Fractional positions order cards in a list: a drag is one UPDATE, never a renumbering.
  • Overdue and blocked are derived, not stored: a card's flags are computed from its due date, its list and its blockers on every read.
  • Done is a name, not a flag: a list whose name reads as finished (done / complete / completed / shipped / finished / closed) ends overdue and blocking. Rename the list and the meaning follows.
  • Labels and blocked-by are stored comma-joined in one column each, cleaned and validated by the domain. At board scale this keeps the schema flat and the queries trivial; a join table would be the first change at 100× the data.
  • Visibility is enforced in the use cases (_board() turns "not yours" into "not found"), so no door can leak a board by accident.

Honest notes — what was hard

  • The MCP server in the Python SDK returns a list result as one JSON text item per element, not one JSON array; the door tests assert against that real behaviour.
  • Validation must happen before mutation: an early version of set_blockers assigned the new links before checking them for circles, and a refused call still poisoned the in-memory card. It now validates the proposed links first, and a test pins that ordering down.
  • DataK3 wants natural keys and no sequences, so ids are short random hex chosen by the app; every write is an upsert, which makes CI retries safe.
  • A remix creates the bucket's tables from the template's schema at remix time, so a live copy can hold an older table shape than the code expects. The app upgrades its own bucket at startup with idempotent statements (ensure_schema in the SQL repository): on a fresh bucket they are no-ops. One starter column could not be retired that way — cards.list_name is NOT NULL in the starter shape and DataK3 does not support dropping a NOT NULL constraint — so the app writes the denormalized list name on every card save, and schema.sql keeps the column for new buckets too.

What I would improve next

  • File attachments (Dodil files) on the card detail.
  • Due-date reminders in the daily summary ("due tomorrow" section).
  • "Find cards like this one" (card-to-card similarity) on top of the shipped semantic search.
  • An archive for done cards older than a chosen date.

Licence

MIT. See LICENSE.