4 Commits
Author SHA1 Message Date
Patrick Niebeling b4057809ed Run Deck as the workflow owner, and drop the job interval
Build package / php-lint (8.2) (push) Successful in 49s
Build package / php-lint (8.3) (push) Successful in 42s
Build package / php-lint (8.4) (push) Successful in 37s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 44s
Build package / package (push) Successful in 59s
Workflows were being switched off with "its board no longer exists or is
no longer available to you" for boards that were perfectly intact.

Deck does not read the session for permissions. PermissionService -- the
class behind every check, including the ones inside CardService::reorder()
-- takes the current user as a plain `private ?string $userId`, filled
from the app container's `userId` service (ISession::get('user_id'),
registered shared). Pimple resolves that once per process and caches it,
and ServerContainer caches Deck's app container just as long. In cron the
value is null whenever a Deck-owned job ran earlier in the same pass, and
otherwise the first workflow owner touched -- never the user being
impersonated. null fails every check, so Deck answered NoPermissionException
for an untouched board and findStackIds() read that as "the board is gone".

IUserSession::setUser() never had any effect on this path.

- DeckIntegrationService::beginUserContext()/endUserContext() pin
  PermissionService (mandatory), CardService, BoardService and
  ActivityManager (best effort) to the workflow owner, and restore them
  afterwards. If PermissionService cannot be pinned, the runner skips that
  user instead of acting under someone else's permissions.
- findStackIds() now takes the uid as an argument and is assembled from
  pieces that cannot answer for the wrong user: BoardMapper and StackMapper
  carry no user state, and getPermissions() is handed the uid explicitly.
  It no longer goes through StackService::findAll().
- NoPermissionException is no longer treated as "board missing". Unknown
  failures throw, which leaves the workflow enabled.

This also fixes the second half of the same defect: card moves silently
failed for every user except the first one processed in a cron pass.

Separately, RunWorkflowsJob is now a plain Job instead of a TimedJob and
runs on every cron pass. The workflow_deck_automation.interval config key
is gone. Time sensitivity is no longer merely declared but structurally
unreachable: JobList::add() leaves the column at its TIME_SENSITIVE
default, and the ratchet in setLastRun() only fires for TimedJob.
2026-08-13 22:40:57 +02:00
Patrick Niebeling b37a0db123 Make the background job time-sensitive so cron actually runs it
Build package / php-lint (8.2) (push) Successful in 48s
Build package / php-lint (8.3) (push) Successful in 45s
Build package / php-lint (8.4) (push) Successful in 47s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 43s
Build package / package (push) Canceled after 0s
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.
2026-08-13 14:48:13 +02:00
Patrick Niebeling 13903f8137 Hide inactive boards, keep source and target stack apart, make the interval configurable
Build package / php-lint (8.2) (push) Successful in 46s
Build package / php-lint (8.3) (push) Successful in 40s
Build package / php-lint (8.4) (push) Successful in 35s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 46s
Build package / package (push) Successful in 1m3s
- 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.
2026-08-13 14:33:28 +02:00
Patrick NiebelingandClaude Sonnet 5 300acde27e Add Deck workflow automation Nextcloud app
Lint info.xml / xml-lint (push) Successful in 30s
Lint PHP / php-lint (8.2) (push) Successful in 55s
Lint PHP / php-lint (8.3) (push) Successful in 44s
Lint PHP / php-lint (8.4) (push) Successful in 41s
PHPUnit / unit-tests (push) Failing after 41s
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>
2026-08-13 10:13:05 +02:00