Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 27. októbra 2026

Prechádzať dokumentáciu · Kanban backend

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Plugin development

Kanban backend

Composition

The entry point creates the runtime, then calls createKanbanService(ctx, runtime) from src/backend/service.ts. That identical KanbanService instance is passed to agent registration, registerKanbanApi(ctx, runtime, service, agent) and registerKanbanLifecycle(ctx, runtime, service, agent).

API registration binds the agent participant synchronizer before requests are served. Lifecycle registration uses the same binding. Membership changes from HTTP and tools therefore both await syncBoardParticipants after commit. A synchronization failure propagates: the membership is already saved, but conversation access must not be reported as synchronized. No detached promise, polling or timeout substitutes for that acknowledgement.

All complete routes come from kanbanRoutes; handlers parse their path, query and body contracts, and validate success responses. Human and agent transports use the same access owner and mutation implementation. HTTP identities come only from verified request authentication. Fixed error message IDs correspond to the plugin's browser strings; SQL and untrusted exception text stay out of responses.

Transactions and order

Reads and writes acquire the board row lock before access admission. Card and child IDs must belong to that board. Access reads use the transaction-bound tx.host.usersRead, never a captured root host inside a transaction. Project links and handoff reservations use the core's active-Project and account-access seams. Relinking a board does not retarget an existing card conversation.

Mutation bodies are validated again by the service, including calls from agent tools. Human assignees must be current board members; selecting Chetty does not start paid work and is refused outside handoff. Removing a member clears their assignments in that same board transaction. Switching from whole team to explicit membership retains the current human member set. A whole-team board cannot exclude one individual while remaining whole-team.

Free integer slots update only the moved item. An occupied requested position inserts before its occupant and shifts subsequent positions by one, retaining their order. Bulk moves allocate a 1024-gap block in supplied card-ID order, shifting the existing suffix beyond that block if needed. Columns and checklists use the same insertion rule. Safe-integer exhaustion raises a conflict; no ID-based tie decides where a drop lands. All cards of a bulk mutation are admitted before any change, so a cross-board ID rolls it back.

Every column DTO has required nonnegative integer cardCount, calculated from all persisted cards in that column, including archived cards. It is independent of snapshot visibility, search filters and pagination. Creation/update responses, board snapshots and card details (including agent reads) share that one SQL projection. Only cardCount === 0 describes an empty column for deletion; an active-card list alone cannot establish emptiness. This is a read projection, not a stored counter, and requires no migration.

Board creation accepts a four-name initialColumnNames tuple supplied by the browser; absent names keep the English defaults for existing name-only callers. Roles and order remain fixed for those four initial columns. Names are request data, not a locale heuristic.

The listMemberCandidates read is admitted under the board lock through KanbanAccess, then projects current human accounts not already in the board. Any member or administrator may use it, in person or through their agent. Label update/delete similarly require membership, not board management.

Search uses SQL predicates, an exact matching count and bounded SQL pagination. Explicit archived: 'only' excludes active rows before both count and page selection, regardless of includeArchived. Omitting that mode retains the existing active/all filter. Card detail adds checklist, comments, attachment metadata and activity; storage keys never cross the wire. Descriptions remain HTML/plain-text data and do not authorize rendering untrusted HTML in the host DOM.

Notifications

Each committed board mutation captures the current human recipients and sends one account-scoped board-changed plugin event per recipient. Membership removal also invalidates the removed account's former board view. Activity, assignment notices, mention notices and member invitations originate from the same backend owner, including mutations made through tools. Notifications run after the SQL transaction has committed. A post-commit failure rejects the operation rather than claiming that all effects succeeded; callers must refresh before retrying a write.

Card alerts pass only the card title as a template parameter and link to the card. Refusal reasons remain stored as existing browser message IDs on the card and are localized by the detail UI; alerts never interpolate those IDs as natural-language text. Native interrupted outcomes supply no reason detail.

The authoritative process registers the due-alert interval. Active, unfinished cards with a current human assignee receive due-tomorrow and overdue notices. Claims include card, deadline, state and recipient. SQL conflict admission allows one sender; a five-minute lease can be reclaimed after interruption. Only successful delivery marks the matching token delivered. Retrying a send-before-mark uses the same host alert key, preserving host deduplication. Date comparisons use PostgreSQL's current calendar date. Archived cards, archived boards and done columns are excluded.

Attachments and removal

Migration 1 owns attachment_cleanup as well as attachment metadata. There is no child foreign key on cleanup rows: SQL cascades must not erase pending filesystem work.

An upload first reserves a generated UUID cleanup key durably. Its normal READ COMMITTED transaction locks that key, admits the board/card, writes the file with exclusive creation, inserts metadata and deletes the reservation. This callback is deliberately not SERIALIZABLE and must never acquire automatic callback retries. On failure the remaining reservation becomes deletable. If a commit acknowledgement is uncertain, committed metadata and reservation removal still share one transaction, so recovery cannot remove a committed file.

Card, board and attachment deletion queue storage keys in the deleting transaction. User removal queues and removes files uploaded by that account, but retains comments/history with null authors. Periodic cleanup locks queued keys with SKIP LOCKED, unlinks files and removes completed queue rows. Missing files are the explicit idempotent success case; other filesystem failures propagate and retain the durable queue. Authoritative service startup also reaps upload reservations left by the previous process; ordinary ticks never reap in-flight uploads. The host owns service/interval lifetime.

The decoded cap is 5 MiB per file, enforced by the request schema, attachment metadata schema and database size constraint. File names are metadata only; paths use generated keys under the plugin data directory. Downloads always use attachment disposition, nosniff and private, no-store. Only PNG, JPEG, GIF, WebP and PDF receive preview MIME types; all other content is octet-stream. Browser previews consume fetched bytes rather than embedding executable content.

User removal clears membership, assignee, founder, creator, mention and actor references while preserving authored prose. It synchronizes participants after commit. Project removal unlinks boards idempotently without deleting cards.

Card agent state

reserveHandoff admits only a human with the plugin grant and linked Project access, then writes one durable token per card before any external paid work. The backend stores typed previous/started card projections for rollback. transitionAgent owns binding, start, refusal, settlement and finish transactions; external integrations never issue parallel card SQL mutations.

Binding records the prepared session and first owner before participant sync or paid work; it does not move or assign the card. Start rechecks live membership, plugin grant and linked Project, sets working status and Chetty assignment and moves an initial-column card to the next ordinary column. Refusal retains the prepared session/owner and restores handoff-owned fields only while that same start remains current; independent edits survive. A removed original column cannot be restored, and a removed assignee cannot regain membership through rollback. Newer settlement/finish transitions win.

Finish requires the acting agent's exact persisted card session and a nonempty summary. It writes an agent comment, moves to review when present, marks done and alerts current member recipients. Native answered outcomes change working/interrupted to waiting; stopped/failed change working to interrupted, with no reason detail. Done is never downgraded. Shared paused input and restart parking are not settled outcomes and do not independently change this projection.

Integration limits

The handoff route, called by a person or by their agent through ElowenApi, delegates to the agent's prepare/bind/participants/start/send sequence. Core's owner-only settlement hook is observational, with no durable admission receipt or missed-event replay. A hard crash may leave card status stale until a later settled conversation turn, or leave a prepared link without a new run. The plugin never automatically resends paid work, guesses an interruption or uses assistant-text presence as admission evidence. The stored conversation is the place to inspect the result and continue manually.

Membership changes still await participant synchronization after commit. Core's native same-turn participant lock seam permits that call from an existing card turn without waiting for its own send lock; foreign callers retain native serialization. The plugin uses no fire-and-forget participant update or parallel access authority.

The host's authenticated plugin API retains its 8 MiB JSON body limit. A 5 MiB file expands to 6,990,508 base64 characters; the bounded filename and MIME metadata leave the complete upload request below that limit. The plugin needs no host limit change or alternate upload transport.