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.
Job::start() calls setLastRun() before run() and clears `reserved_at` only
afterwards through setExecutionTime(), which is not in a finally block. Any
uncaught throwable therefore leaves the job reserved, and JobList::getNext()
skips reserved jobs until the reservation is 12 hours stale -- so a single
failing workflow takes the whole app offline for half a day, while last_run
still shows the start of that failed attempt.
Wrap both loops so a failure is logged with its exception and the remaining
users and workflows still run.
The mail named the card and the workflow but not what the card is about,
which meant opening Deck to know whether the move mattered.
The description is escaped explicitly: addBodyText() only calls
htmlspecialchars() when it has to derive the plain-text part itself, and we
pass both parts to get line breaks in the HTML body -- so escaping user
input is on us. Long descriptions are cut at 2000 characters.
Deck stores the description as Markdown; it is sent unrendered rather than
pulling in a parser for one mail.
The job was registered but never executed: `background-job:list` kept
showing last_run = 1970-01-01. It was marked TIME_INSENSITIVE, and
OC\Core\Service\CronService::runCli() reads `maintenance_window_start` and
calls jobList->getNext($onlyTimeSensitive = true) whenever the current UTC
hour is outside [start, start+4] -- so with a maintenance window configured,
time-insensitive jobs are skipped for the other 20 hours of the day.
A job whose entire purpose is a 5-minute (now configurable down to 60s)
reaction time is time-sensitive by definition.
Also fix the occ invocation in README.md and CLAUDE.md: background-job:worker
takes job classes, not an app id, so the documented
`background-job:worker workflow_deck_automation` matched nothing.
- 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.
@nextcloud/vite-config prefixes every entry name with the app id, so the
entry "workflow-deck-automation-personal-settings" was emitted as
js/workflow_deck_automation-workflow-deck-automation-personal-settings.mjs
while Util::addScript() was still asking for the unprefixed name. Nextcloud
found neither the script nor the style, so only the empty mount div rendered
and the settings page stayed blank.
Shorten the entry to "personal-settings" and pass the full on-disk basename
(APP_ID . '-personal-settings') to addScript()/addStyle(). Document the
prefix rule and the intentionally near-empty CSS entry stub 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>