Deleting a board or stack left the workflow row pointing at nothing: Deck
keeps the orphaned cards readable until its DeleteCron purges them, so the
run kept failing on the move every few minutes, silently and forever.
- The controller now rejects boards and stacks that do not exist (or are not
the user's) on create and update, so no new broken row can be stored.
- The settings list marks affected workflows in red instead of showing a row
that looks healthy.
- The background job disables such a workflow and mails its owner once. The
mail ignores the notifyEmail flag: that one is about moved cards, this is a
notice that the automation stopped. It stays a one-off because a disabled
workflow is no longer picked up.
The check deliberately goes through StackService::findAll() rather than
BoardService::getUserBoards(): Deck injects the current user into BoardService
as a string frozen at construction, so in the job -- one process, many users,
a cached BoardService -- it would answer for the wrong user or for none, and
every workflow on the instance would have been disabled. findStackIds()
returns null only for a genuinely missing or forbidden board and throws for
anything else, so a Deck outage skips the workflow instead of killing it.
Deck's CardMapper::findAllByStack() filters on stack_id and archived only --
there is no deleted_at condition -- so getActiveCardsInStack() also returned
cards the user had deleted. They stay in the table until Deck's DeleteCron
purges them, which is long enough for an overdue one to get moved out of the
trash and back into a stack.
- Boards: BoardService::getUserBoards() defaults to $includeArchived = true,
and that flag also gates the `deleted_at = 0` condition, so archived and
trashed boards showed up in the dropdown. Request the filtered query
instead, with a fallback to the no-arg call if the signature ever changes.
Stacks need nothing: StackMapper::findAll() always filters deleted_at.
- Each stack dropdown now hides whatever the other one holds, so source and
target can no longer be set to the same stack. The save-time check stays
as the backstop for rows stored before this rule.
- RunWorkflowsJob reads its interval from config.php
('workflow_deck_automation.interval', seconds, default 300, clamped to a
60s minimum). TimedJob re-reads the interval on every cron pass, so a
changed value takes effect without any occ command.
The settings page rendered, but nothing in it worked:
- All NcSelect dropdowns showed "undefined" for every option. In
@nextcloud/vue 9 the `label` prop is vue-select's option display *key*,
not a caption, so `label="Board"` read `option.Board`. Use `input-label`.
- Saving always failed with "Bitte einen Titel angeben". Vue 3 dropped
`.sync`; NcTextField and NcCheckboxRadioSwitch bind via `modelValue`, so
`:value.sync` / `:checked.sync` never wrote back. Use `v-model`.
- NcButton's style prop is now `variant`, `type` is the native button type
and `native-type` is gone. `type="tertiary"` rendered
`<button type="tertiary">`, which HTML falls back to `submit` for, making
the cancel button submit the form.
- The user dropdown was always empty. Deck's RelationalEntity replaces
resolved relations with a RelationalObject once an entity is enriched, so
`$acl->getParticipant()` returns that wrapper and the uid lives in
`getPrimaryKey()` -- probing for `getUID()` yielded null. This also broke
the background job's assigned-user filter, which shares the extractor.
- A board's ACL never contains its owner (a private board has an empty
ACL), so participants are now seeded with the owner, group ACL entries
are expanded via IGroupManager and display names resolved via
IUserManager.
- One board appeared twice: getUserBoards() merges own/group/circle boards
and includes archived and trashed ones. Deduplicate by id and drop those.
Also keep one failing lookup in onBoardChange from taking the other two
dropdowns down with it, and document all of the above in CLAUDE.md.
Ports the standalone due-date cron script into a proper Nextcloud 34
app with a personal-settings UI, per-user workflow configuration
(source/target stack, assigned-user and label filters, email
notification), a TimedJob background runner, and Gitea CI pipelines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>