mobiledev/taskflow

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

A remix template · 0 remixes · Remix mobiledev/taskflow

DESIGN.md

Design notes

Short notes on the decisions that shape TaskFlow.

One set of use cases, two doors

The web routes and the MCP tools are thin: they read who is calling, call one method on TaskFlow (application/use_cases.py), and translate errors (NotFound → 404, Forbidden → 403, Invalid → 422, or a tool error for MCP). No rule lives in a door, so Claude can't do anything the web page couldn't, and the tests in tests/test_doors.py check that both doors refuse the same things.

Rules live in the domain

Board knows roles (require_view, require_edit, require_owner), who may hold a card, and its lists. Card.edit validates every field and returns a diff of what really changed. The use cases only orchestrate: load, ask the domain, save, log. The domain has no imports beyond the standard library, so its tests run in milliseconds.

A board you can't see doesn't exist

A stranger asking for a board or card gets "not found", never "forbidden", so ids and titles don't leak. Lookups by title or card number only search the caller's own boards. That is what makes "the AI only sees the boards the signed-in person can see" true by construction: every MCP call runs as the gateway's person, through the same lookups.

People talk in numbers and names

Cards get a per-board number (#12); boards can be named by title. A number is claimed with a plain INSERT into card_numbers (key board:number), so two people adding cards at the same moment can't both get #7: one insert wins, the other moves on to #8. An upsert would let both win. Assistants pass card="12" and board="Ops" exactly as the person said it. When a number is ambiguous across boards, the answer says which boards have it instead of guessing.

History records the diff, not just a sentence

Each activity row stores a readable action ("moved #12 from To do to Done") and a JSON diff ({"list_name": ["To do", "Done"]}). Edits that change nothing write nothing, so the history stays meaningful.

Lists, done and overdue

Lists are per board and ordered. The list called Done (or Completed, Finished, Closed) counts as finished wherever it sits; a board without one treats its last list as done. New lists go in before Done, so adding "Review" never turns Review into the finish line (an early version used "the last list", and a tester's Review list broke overdue). That one rule drives overdue (due date passed and not done), "my cards" (open work only), and dependencies (a finished blocker no longer blocks).

Ordering for drag and drop

Cards have a float position. Dropping between two cards takes the midpoint, so a move writes one row instead of renumbering the list.

Dependencies

"Blocked by" links stay on one board. Adding a link that would close a loop is refused (would_cycle walks the blocker's chain). what_blocks follows the chain and reports only open cards.

AI is advice, facts are computed

Dodil Models writes checklist steps, ranks similar cards by embeddings, and words the daily summary. The summary's facts (counts, overdue, due soon, recent activity) are computed by the app and handed to the model, so it can't invent numbers. suggest_checklist changes nothing; checklist_from_ai is the explicit "do it" version.

Data

DataK3 has no sequences, so every key is chosen by the app (short random ids; memberships keyed board:email). Small collections (lists, labels, dependencies, checklists) are JSON in TEXT columns: they're always read with their row, and it keeps the schema to four tables. Writes are upserts, so a retried request is harmless.

No login code, no secrets

The Dodil gateway signs people in through the app's user pool and passes the person in X-Dodil-User; the app never sees a password. The service account's id and secret come from CI secrets via deploy.yaml. Nothing secret is in the repo.

Changes only from the page itself

The page signs in with the gateway's cookie, so a change request must come from the page. Every Ignite app shares *.ignite.dodil.cloud, which makes another organization's app "same-site" for the browser; a SameSite cookie alone is not enough. A middleware refuses any /api write whose Sec-Fetch-Site is not same-origin or whose Origin host is not this app's. Claude and scripts send neither header and use a bearer token, so they are unaffected.

Limits

A board holds at most 12 lists and 1000 cards; a card at most 10 labels and 50 checklist items; titles, descriptions and emails have length caps. Nothing grows without bound.

Safe page

Every value from the API is escaped before it reaches the page, so a card titled <img onerror=...> shows as text.