Skip to content

DRU-390 - Issues: UI pages: board, list, ticket - #412

Draft
druks-operator-treadstone[bot] wants to merge 3 commits into
integration/issuesfrom
agent/DRU-390
Draft

DRU-390 - Issues: UI pages: board, list, ticket#412
druks-operator-treadstone[bot] wants to merge 3 commits into
integration/issuesfrom
agent/DRU-390

Conversation

@druks-operator-treadstone

Copy link
Copy Markdown
Contributor

Linear ticket: DRU-390

Plan

DRU-390 — Issues app UI: board, list, ticket

Target repo is czpython/druks, not the druks-apps checkout in this sandbox. The rewritten ticket and operator note put this work in backend/druks/contrib/issues/, stacked on PR #411 (branch agent/DRU-389, still open). The issues app, models, enums, schemas, and the issues_* write doors all live there; none exist in druks-apps. See Q1 — the sandbox must be re-pointed at druks and branched from agent/DRU-389 before implementation.

Scope: add backend/druks/contrib/issues/pages.py (new) and set navigation = ["board", "list"] in backend/druks/contrib/issues/app.py. Pages only — no dist/, no JavaScript, no new druks.ui block. No new routes: every write already exists as an operation_id on routes.py from #411.

How pages work here (verified against druks/ui and backend/tests/druks-field_notes/.../pages.py): @ui.page("/") registers a top-level page auto-discovered from the package; App.navigation entries must be static top-level pages (is_static and not parent, base.py:239). So board (/) and list (/list) are top-level @ui.page; ticket (/tickets/{identifier}) is a top-level parameterized page reached by ui.Link(page="ticket", arguments={"identifier": ...}). Exactly one page at /. ui.Page accepts follows=<subject>; a named ui.Section + refresh="region" ui.Action is the live comment region (blocks.py). ui.Action.fields exists, so creation is an action, not a /new page.

Three pages (read models directly; call doors only for writes):

  • board / — one ui.Columns, a ui.Section per status Backlog, Todo, Ready for Agent, In Progress, In Review, Done (Cancelled omitted), each holding ui.Cards grouped in Python from a single Ticket.list_board(); empty status → ui.EmptyState (e.g. ui.Cards(empty=...)). Card title = title, description = identifier + priority/assignee when set, control ui.Links to ticket. Page-level New ticket / New project controls, shown on an empty install.
  • list /list — one ui.Table (identifier, title, priority, assignee, project, updated) per status in order In Progress, In Review, Ready for Agent, Todo, Backlog, Done, Cancelled, rows from Ticket.list_for_status(status) (already updated_at desc).
  • ticket /tickets/{identifier}ui.Page(follows=<ticket>); title, ui.Markdown description, ui.Facts (status/priority/assignee/project/identifier); edit ui.Formissues_update_ticket; move ui.Action with a status ui.SelectFieldissues_set_status; a named comments ui.Section whose comment ui.Form/ui.Action (required body, issues_add_comment, refresh="region") appends in place; empty thread → ui.EmptyState. Unknown identifier → a ui.Page with ui.EmptyState.

Options / lookups: project SelectField from Project.list() (label=name, value=str(id)); assignee from Account.list_non_system() usernames (value=account id) + one Unassigned choice; resolve project_id/assignee_id to names for cards & table via those two lists. New-ticket status select defaults to Status.TODO. After a create, the action refreshes the board (refresh="page") — the new identifier is minted server-side and an Action.link is static, so a link-to-new-ticket isn't expressible.

Risk — Unassigned wiring: SelectField.value is a plain string; create_ticket/update_ticket on #411 treat assignee_id="" as a real id (require_assignee → 404). Implementer must confirm the shell drops empty select values before the POST; if it does not, the minimal blank→None coercion belongs on those two doors (a stacked-branch tweak, not a page hack). Do not invent a sentinel.

Acceptance Criteria

  • AC1: backend/druks/contrib/issues/pages.py is added and declares exactly three top-level pages via @ui.page: board at /, list at /list, and ticket at /tickets/{identifier}; backend/druks/contrib/issues/app.py sets navigation = ["board", "list"].
    • Verification: Grep pages.py for the three @ui.page(...) decorators and their function names; grep app.py for the navigation assignment.
  • AC2: board returns a single ui.Columns holding one ui.Section per status in order Backlog, Todo, Ready for Agent, In Progress, In Review, Done (Cancelled absent); cards are built from a Ticket.list_board() read grouped by status, each card's title = the ticket title and description carries the identifier plus priority/assignee when set, and the card control ui.Links to the ticket page with arguments={"identifier": ...}; an empty status renders a ui.EmptyState rather than nothing.
    • Verification: Read board in pages.py: confirm the status-order list excludes Cancelled, cards derive from list_board(), each card links to the ticket page by identifier, and the empty branch yields an EmptyState.
  • AC3: list renders one ui.Table per status in order In Progress, In Review, Ready for Agent, Todo, Backlog, Done, Cancelled, each table with columns identifier, title, priority, assignee, project, updated and rows sourced from Ticket.list_for_status(status); Cancelled is present as the last section.
    • Verification: Read list in pages.py: confirm the seven-section order, the six columns, list_for_status per section, and Cancelled last.
  • AC4: ticket returns ui.Page(..., follows=<the Ticket>) showing the title, a ui.Markdown description, a ui.Facts of status/priority/assignee/project/identifier, an edit ui.Form (fields title, description, priority, assignee, project) whose action targets operation issues_update_ticket, a move ui.Action carrying a status ui.SelectField targeting issues_set_status, and a named ui.Section for comments whose comment ui.Form/ui.Action has a required body field, operation issues_add_comment, and refresh="region"; an empty thread renders a ui.EmptyState, and an unknown identifier returns a page with a ui.EmptyState rather than raising.
    • Verification: Read ticket in pages.py: confirm follows= is the ticket, the three operations are named, the SelectField sits on the move action, refresh="region" is inside a named Section, and the not-found branch returns an EmptyState page.
  • AC5: The board exposes page-level New ticket and New project controls unconditionally (present on an empty install, built outside any has-tickets guard). New ticket collects title (required), project (required select), description, status (select defaulting to Status.TODO), priority, and assignee and targets issues_create_ticket; New project collects name and prefix and targets issues_create_project; after a successful create each action refreshes the board (refresh="page").
    • Verification: Read pages.py: confirm the two create controls are constructed outside any ticket-count guard, their field sets, the Todo default on the status select, the two target operations, and the board refresh.
  • AC6: Project select options come from Project.list() with label=name and value=str(project.id); assignee select options come from Account.list_non_system() usernames (value=account id) plus one explicit Unassigned choice; no no-project option is offered, and card/table assignee and project columns resolve ids to names via those same two lists.
    • Verification: Read pages.py: confirm option construction from Project.list() and Account.list_non_system(), a single Unassigned entry, no empty-project option, and id→name resolution reused for cards and the table.
  • AC7: No JavaScript or SPA is shipped and no kanban/Board primitive is introduced: the diff adds no file under any dist/ directory and no new druks.ui block class; the board is composed only from ui.Columns/ui.Section/ui.Card(/ui.Cards).
    • Verification: Inspect the diff: no dist/ paths, no new class in backend/druks/ui/, and board uses only Columns/Section/Card/Cards.

chaosk and others added 3 commits September 4, 2026 03:28
Co-authored-by: Cursor <cursoragent@cursor.com>
routes.py carries the operations pages, the dashboard, and the sandbox all
call. set_status is the one door that moves a ticket and the only one that
publishes ticket.transitioned — skipped when the row is already there, so a
repeat does not open a second build. Content edits and creates stay quiet.
Status now says on the enum whether it is completed and whether it is
terminal, which is what the funnel reads.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chaosk
chaosk force-pushed the agent/DRU-389 branch 2 times, most recently from 3611ea5 to 4f7aad3 Compare September 8, 2026 03:49
Base automatically changed from agent/DRU-389 to integration/issues September 8, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants