stubborn-otter/otter-boards

Otter Boards, a team task board you can run from the web or from Claude and ChatGPT. FastAPI + DataK3 + appid + Models + MCP.

A remix template · 1 remixes · Remix stubborn-otter/otter-boards

README.md

Otter Boards

A team task board you can remix into your own Dodil organization and run from the web page or from Claude and ChatGPT. Boards hold lists, lists hold cards, and an owner decides who is on each board. Say "move card 12 to Done" to your assistant, and the card moves on your teammate's screen.

Built for the Dodil Boards Challenge (rules). MIT licence.

What it does

  • People: an owner invites members by email. Roles are owner, member and viewer. A viewer can read but not change anything, only the owner manages people, and a board you aren't on doesn't exist for you, on the web or through MCP.
  • Boards, lists, cards: real lists you can add, rename, reorder and archive. A card has a title, description, assignee, due date and labels, and a short number on its board. Drag cards between lists on the page.
  • History: every change to a card records who, when and what changed.
  • MCP: 30 tools that are actions, never table access. They take the names people use: a board by name, a list by name, a card by its number or title.
  • Extras: an AI checklist you review before saving, "find cards like this one" by meaning, today's summary of a board, "blocked by" links with "what is blocking the launch?", and overdue cards flagged automatically. If Dodil Models is down the board keeps working: similar cards fall back to shared words, and the summary falls back to the plain facts.

Remix it

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

The remix creates, in your org: the repo, a DataK3 bucket <repo>-data with the tables from schema.sql, a user pool <repo>-users, and a service account with access to the bucket and to Dodil Models. CI then tests, builds and deploys it. Keep <your-repo> to 18 characters or less, so the bucket name fits.

After the remix: let people sign in

Add the app's callback URL to the pool, as dodil guide remix describes under "After the remix: let people sign in". The app's host is on its page under Apps in the console:

dodil appid settings set <your-repo>-users \
  --redirect-uris "https://<your-app-host>/.dodil/auth/callback"

On our own remix Dodil had already allowed that URL, so sign-in may work before you run this. If the sign-in page says redirect_uri is not in the pool's allowlist, this is the fix. Public sign-up is on, so anyone with the link can make an account, but they see no boards until invited.

Then open the app, create a board, and invite people from Manage people. There is no email service, so send them the link yourself: a person sees a board once they sign in with the email you invited.

Connect Claude or ChatGPT

The app serves MCP at https://<your-app-host>/mcp. Add it as a custom connector: 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>.

You sayTool
What's on my plate this week?my_cards
What's overdue?overdue_cards
Add a card 'Send invoice' to Doing on the Ops boardadd_card
Move card 12 to Donemove_card
Give it to Ana, due Fridayassign_card, update_card
What is blocking the launch?what_is_blocking
Turn this card into a checklistsuggest_checklist, then save_checklist
Find cards like this onefind_similar_cards
Summarise the Ops board for todayboard_summary

Card numbers are per board, so if 12 exists on two of your boards the tool asks which one.

Run it locally

Tests need no Dodil account: fakes stand in for the data and the AI.

cd app
python3.12 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt pytest
python -m pytest -q ../tests

The end-to-end run needs Docker. It starts a throwaway postgres:16-alpine, loads schema.sql, runs the rules on the real repository, then starts the real server and drives it over HTTP and with a real MCP client. A small local stand-in plays Dodil Models, so nothing calls Dodil.

tests/e2e/run.sh                # run everything, then remove the container
tests/e2e/run.sh serve          # leave it running to try at http://127.0.0.1:8765
AS=bo@example.com tests/e2e/run.sh serve     # the same, as somebody else

To run against a real bucket instead, 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 from app/.

What's inside

app/boards/
  domain/model.py            Board, BoardList, Card, roles and the rules. No database, no HTTP.
  domain/similarity.py       how alike two cards are: by meaning or by shared words
  application/use_cases.py   every action. Web and MCP call these and nothing else.
  application/ports.py       what the use cases need: a repository and an assistant
  infrastructure/repository.py  the tables in DataK3
  infrastructure/dodil.py    service-account token, DataK3 connection, Dodil Models
  doors/web.py               JSON routes and the page   -> call the use cases
  doors/mcp.py               the MCP tools              -> call the same use cases
  doors/identity.py          who is calling (the gateway's X-Dodil-User header)
  doors/static/              the page: plain HTML, CSS and JavaScript, no build step
  main.py                    wires it together: /mcp, sign-in metadata, /healthz
tests/                       rules, roles through both doors, the Models client, the schema
tests/e2e/                   the real run, with local stand-ins. Never in the production image.
schema.sql                   the tables, created by the remix
docs/DECISIONS.md            why it's built this way

What I'd improve

  • Finish proving it on Dodil. See "Not proven yet" in docs/DECISIONS.md.
  • Edits that can't collide. Two people saving the same card at the same moment: the last save wins the whole row. A version column would catch that, if DataK3 allows a conditional write.
  • Writes that land together. A card and its history line are two writes. If the second fails the change is saved but not logged. A transaction fixes it, once DataK3's support is known.
  • Vector search in DataK3. Similar cards are scored in Python today. DataK3 advertises vector search over the same rows, which would scale better than loading a board's embeddings.
  • Smaller things: restoring archived cards, moving ownership to someone else, closing a board, time zones (overdue uses UTC days today), a live feed instead of a 15-second refresh, and touch drag and drop (on phones, the card's List menu moves it).

Licence

MIT. See LICENSE.