Commit Graph
10 Commits
Author SHA1 Message Date
gnilebein 28b4edc811 revert 0e66319a06
Build package / php-lint (8.4) (push) Successful in 43s
Build package / php-lint (8.5) (push) Successful in 40s
Build package / xml-lint (push) Successful in 12s
Build package / unit-tests (push) Successful in 38s
Build package / package (push) Successful in 52s
revert Let each workflow watch the due date or the start date

Deck 1.18 added a start date to cards (Card::$startdate), so a workflow
no longer has to mean "overdue". Each rule now carries a date_field
('due' | 'start'), chosen in the settings form next to board and stacks.

Existing rules keep the due date: the new column's default supplies it
for every stored row, so there is no backfill step, and
Workflow::getDateFieldOrDue() covers entities that were never near the
database as well as hand-edited values. The controller is the one place
that rejects an unknown value instead of normalising it - a client
asking for a rule it would not get should hear about it.

The probe for the field is property_exists(), not method_exists():
Deck's entities declare no getter as real code - getStartdate(),
getDuedate(), even getId() all go through Entity::__call(), which
method_exists() ignores by definition. A method_exists() guard would
have been false on every Deck version and would have turned every
start-date rule into a silent no-op.

assertDeckAvailable() now also refuses Deck older than 1.18.0. That
floor cannot live in appinfo/info.xml - neither the server's nor the app
store's schema has an app-to-app dependency element - but
<nextcloud min-version="34"> already implies it, since 1.18.x is the
only Deck release line published for Nextcloud 34.

isOverdue() becomes hasPassed(): one function for both dates, since the
comparison and the argument against Deck's day-truncating
getDaysUntilDue() are identical for either.

Also carries a pending NcSelect icon-alignment fix that was already in
the working tree.

Bump to 34.1.0.
2026-08-26 07:10:55 +02:00
Patrick Niebeling 0e66319a06 Let each workflow watch the due date or the start date
Build package / php-lint (8.4) (push) Successful in 39s
Build package / php-lint (8.5) (push) Successful in 43s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Failing after 39s
Build package / package (push) Skipped
Deck 1.18 added a start date to cards (Card::$startdate), so a workflow
no longer has to mean "overdue". Each rule now carries a date_field
('due' | 'start'), chosen in the settings form next to board and stacks.

Existing rules keep the due date: the new column's default supplies it
for every stored row, so there is no backfill step, and
Workflow::getDateFieldOrDue() covers entities that were never near the
database as well as hand-edited values. The controller is the one place
that rejects an unknown value instead of normalising it - a client
asking for a rule it would not get should hear about it.

The probe for the field is property_exists(), not method_exists():
Deck's entities declare no getter as real code - getStartdate(),
getDuedate(), even getId() all go through Entity::__call(), which
method_exists() ignores by definition. A method_exists() guard would
have been false on every Deck version and would have turned every
start-date rule into a silent no-op.

assertDeckAvailable() now also refuses Deck older than 1.18.0. That
floor cannot live in appinfo/info.xml - neither the server's nor the app
store's schema has an app-to-app dependency element - but
<nextcloud min-version="34"> already implies it, since 1.18.x is the
only Deck release line published for Nextcloud 34.

isOverdue() becomes hasPassed(): one function for both dates, since the
comparison and the argument against Deck's day-truncating
getDaysUntilDue() are identical for either.

Also carries a pending NcSelect icon-alignment fix that was already in
the working tree.

Bump to 34.1.0.
2026-08-25 15:28:30 +02:00
Patrick Niebeling 131ef2e938 Surface dead filters, and name board and stacks in the moved-card mail
Build package / package (push) Successful in 55s
Build package / php-lint (8.2) (push) Successful in 41s
Build package / php-lint (8.3) (push) Successful in 34s
Build package / php-lint (8.4) (push) Successful in 40s
Build package / xml-lint (push) Successful in 11s
Build package / unit-tests (push) Successful in 42s
A filter whose entries have all been deleted from the board was the one
failure mode of this app that was completely invisible: the workflow kept
running, kept updating last_run and matched nothing, forever. It happens
with deleted labels and just as easily when a filtered user loses board
access, because Deck's BoardService::deleteAcl() calls
assignedUsersMapper->deleteByParticipantOnBoard() and wipes that user's
card assignments.

Two low-stakes signals, no new column and no migration:

- WorkflowRunner::warnAboutDeadFilters() logs a warning on every run.
- workflowIssues in PersonalSettings.vue marks the row red.

Deliberately no mail and no `enabled = false`. One deleted label out of
three is harmless, and even a fully dead filter can be one board edit away
from being live again.

Both obey the rule the disable path already follows: only positive
knowledge. DeckIntegrationService::findFilterOptions() therefore throws
instead of degrading to [], unlike listLabels()/listParticipants(), where
an empty list only means an empty dropdown.

That also fixes an existing false positive of the same family:
loadStacksFor() stored [] when the request failed, so a single failed
fetch reported "Quell-Stapel und Ziel-Stapel nicht mehr vorhanden" for an
untouched board. Failed requests now store null and are read as "unknown".

The comparison itself lives in WorkflowRunner::findDeadFilters(), static
and side-effect free like cardMatchesFilters(), with unit tests -- notably
that an empty filter is never dead, which would otherwise flag every
unfiltered workflow.

Separately, the moved-card mail now names the board and both stacks.
DeckIntegrationService::describeTargets() resolves the titles at most once
per workflow run, and only when a card actually moved and the workflow
wants a mail; unreadable titles degrade to #<id> rather than costing the
user their notification.
2026-08-13 23:02:13 +02:00
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 0eff367268 Handle workflows whose board or stack was deleted
Build package / php-lint (8.2) (push) Successful in 49s
Build package / php-lint (8.3) (push) Successful in 45s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 45s
Build package / package (push) Successful in 1m3s
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.
2026-08-13 15:47:55 +02:00
Patrick Niebeling 184c39d829 Document that the time_sensitive column has to be reset once
Build package / php-lint (8.2) (push) Successful in 50s
Build package / php-lint (8.3) (push) Successful in 36s
Build package / php-lint (8.4) (push) Successful in 40s
Build package / xml-lint (push) Successful in 11s
Build package / unit-tests (push) Successful in 44s
Build package / package (push) Successful in 50s
Shipping the TIME_SENSITIVE change is not enough for instances that already
ran the job: getNext() filters on the time_sensitive column of oc_jobs, and
JobList::setLastRun() only ever sets it to TIME_INSENSITIVE -- there is no
else branch, and JobList::add() writes the column only when inserting, so
re-registering an existing job on app update or app:enable leaves the stamp
in place.

Document the one-off UPDATE (and the occ-only alternative) in the README
troubleshooting section and in CLAUDE.md.
2026-08-13 15:18:22 +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 Niebeling e06e382147 Merge release workflow into the main build workflow
Build package / package (push) Successful in 58s
Lint info.xml / xml-lint (push) Successful in 13s
Lint PHP / php-lint (8.2) (push) Successful in 45s
Lint PHP / php-lint (8.3) (push) Successful in 41s
Lint PHP / php-lint (8.4) (push) Successful in 34s
PHPUnit / unit-tests (push) Successful in 42s
build-release.yml and build-main.yml ran identical steps (checkout, node,
npm install, npm run build, make appstore) and differed only in where the
package landed. Fold the v* tag trigger into build-main.yml and branch on
gitea.ref in the publishing step: main keeps the rolling latest-main
pre-release, tags get a normal release on the tag that was just pushed.

Tag deletion is now guarded - only latest-main is recreated, since deleting
a version tag would destroy the ref that triggered the run. A stale release
is still removed in both cases so moved tags republish cleanly.

Side effects: version builds are published as durable release assets with a
stable URL instead of expiring workflow artifacts, which drops the last
actions/upload-artifact usage and with it the @v3 pin forced by the GHES
restriction; and the setup-php step goes away, since make appstore only
shells out to mkdir/tar/rm.
2026-08-13 13:46:34 +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