60 Commits
Author SHA1 Message Date
Patrick Niebeling 4464e5ad3c Detect trashed and archived Deck entities, skip archived boards
Build package / unit-tests (push) Successful in 3m39s
Build package / package (push) Successful in 3m10s
Build package / xml-lint (push) Successful in 1m55s
Build package / php-lint (8.4) (push) Successful in 3m45s
Build package / php-lint (8.5) (push) Successful in 3m55s
Deck's Entity getters are magic (__call), so method_exists() never saw
getDeletedAt() or getArchived(): trashed cards were moved, trashed boards
were neither dropped from "my boards" nor detected by findStackIds(), and
every board counted as not archived. DeckIntegrationService now checks the
properties with property_exists() via isTrashed()/isArchived().

Workflows on archived boards stay enabled but are skipped by the runner
(isBoardArchived(), read through BoardMapper) until the board is
unarchived, and the settings list marks them. Archived boards are no
longer offered in the board dropdown at all; editing a workflow on one
clears board and stacks and says why.
2026-10-01 13:54:03 +02:00
Patrick Niebeling 9e1a15bab0 Refuse workflows that move cards in a loop
Build package / unit-tests (push) Successful in 4m13s
Build package / package (push) Successful in 1m38s
Build package / xml-lint (push) Successful in 1m4s
Build package / php-lint (8.5) (push) Successful in 4m0s
Build package / php-lint (8.4) (push) Successful in 4m34s
Every enabled workflow is an edge "source stack -> target stack";
LoopDetector finds circles in that graph, a workflow whose source is its
own target being the shortest. Without a check, two workflows A -> B and
B -> A move the same cards back and forth on every run, even within a
single run, with a notice for each move. The trigger date is ignored on
purpose: by start date one way and by due date the other, a card with
both dates passed still circles. Chains stay allowed.

The controller rejects saving an enabled workflow that would close a loop
and names the workflows involved. Workflows stored before this check are
skipped by the runner (logged, not disabled) and flagged in the settings
list via a new `loop` field from index().
2026-10-01 13:47:32 +02:00
Patrick Niebeling 8268f1068d Let workflows move cards by start date instead of due date
Build package / unit-tests (push) Successful in 4m49s
Build package / package (push) Successful in 2m28s
Build package / xml-lint (push) Successful in 3m32s
Build package / php-lint (8.5) (push) Successful in 4m52s
Build package / php-lint (8.4) (push) Successful in 4m56s
Each workflow now has a trigger date (due or start): the runner compares
either the card's due date or its start date against now, read straight
from Deck's Card entity. Mails and Nextcloud notifications say whether
the card was overdue or reached its start date. Cards without the chosen
date stay put.

Deck only has a start date since 1.18 and Entity getters are magic, so
DeckIntegrationService::supportsStartdate() checks the property; without
it the form rejects start-date workflows and the runner skips them
without disabling them.

WorkflowRunner::isOverdue() becomes hasPassed(), as it now covers both
dates. Existing workflows keep the due date (migration Version1005).
Bumps to 35.4.0 so the migration runs.
2026-10-01 13:19:32 +02:00
Patrick Niebeling 5bd3181f10 Add a manual App Store publishing workflow
Build package / unit-tests (push) Successful in 1m51s
Build package / package (push) Successful in 2m39s
Build package / php-lint (8.5) (push) Successful in 1m51s
Build package / xml-lint (push) Successful in 49s
Build package / php-lint (8.4) (push) Successful in 1m47s
appstore.yml signs the tarball of an existing v* release and registers its
Gitea download URL with the App Store; it only runs on workflow_dispatch.
Also adds the AGPL LICENSE and points website, bugs and repository in
info.xml at the Gitea repository.
2026-09-30 12:31:46 +02:00
Patrick Niebeling 7612344726 Send moved-card notices as Nextcloud notifications too
Build package / unit-tests (push) Successful in 1m52s
Build package / package (push) Successful in 1m38s
Build package / xml-lint (push) Successful in 33s
Build package / php-lint (8.5) (push) Successful in 1m48s
Build package / php-lint (8.4) (push) Successful in 1m50s
Each workflow now has a notify channel (mail, notification or both) next
to its recipient mode. Notifications go out per card through
OCP\Notification\IManager, so they show up in the bell and as push on the
mobile and desktop clients. Without the notifications app nothing is sent.
The one-off "workflow disabled" notice is sent as a notification as well.

Existing workflows keep mail (migration Version1004). Bumps to 35.3.0 so
the migration runs.
2026-09-30 12:31:01 +02:00
Patrick Niebeling 35903772eb Say in the README intro that only board owners set up workflows
Build package / unit-tests (push) Successful in 3m11s
Build package / package (push) Successful in 2m49s
Build package / php-lint (8.4) (push) Successful in 1m40s
Build package / xml-lint (push) Successful in 38s
Build package / php-lint (8.5) (push) Successful in 1m53s
2026-09-30 11:48:25 +02:00
Patrick Niebeling 5083607ac4 Keep the docs independent of a specific Nextcloud version
Build package / unit-tests (push) Successful in 1m53s
Build package / package (push) Successful in 2m48s
Build package / xml-lint (push) Successful in 16s
Build package / php-lint (8.4) (push) Successful in 56s
Build package / php-lint (8.5) (push) Successful in 1m21s
2026-09-30 11:47:13 +02:00
Patrick Niebeling c7baa8c67f Drop the no-filters remark from the README
Build package / unit-tests (push) Successful in 1m38s
Build package / package (push) Successful in 2m57s
Build package / xml-lint (push) Successful in 40s
Build package / php-lint (8.4) (push) Successful in 2m5s
Build package / php-lint (8.5) (push) Successful in 2m6s
2026-09-30 11:46:44 +02:00
Patrick Niebeling e5b5f25748 Split the docs: README for users, DEVELOPMENT.md for developers
Build package / unit-tests (push) Successful in 1m51s
Build package / package (push) Successful in 1m58s
Build package / xml-lint (push) Successful in 1m1s
Build package / php-lint (8.4) (push) Successful in 1m47s
Build package / php-lint (8.5) (push) Successful in 1m49s
The README now explains setup, workflows, the mail options with Deck's
real menu names, and what happens when boards or stacks disappear -
without class names. Architecture, data model, tests, CI, release and
the time_sensitive troubleshooting move to DEVELOPMENT.md, fixing the
stale CI table, PHP version and Nextcloud version on the way.
DEVELOPMENT.md is excluded from the app package.
2026-09-30 11:43:53 +02:00
Patrick Niebeling ebf48b0707 Link the card title instead of an extra button in the mail
Build package / unit-tests (push) Successful in 2m17s
Build package / package (push) Successful in 59s
Build package / xml-lint (push) Successful in 1m14s
Build package / php-lint (8.4) (push) Successful in 2m14s
Build package / php-lint (8.5) (push) Successful in 2m15s
The plain-text part gets the card URL on its own line. Bumps to 35.2.2.
2026-09-30 11:05:22 +02:00
Patrick Niebeling 30e644a001 Condense each moved card to one sentence in the mail
Build package / unit-tests (push) Successful in 3m28s
Build package / package (push) Canceled after 21s
Build package / xml-lint (push) Successful in 1m36s
Build package / php-lint (8.5) (push) Successful in 3m20s
Build package / php-lint (8.4) (push) Successful in 3m26s
Drops the workflow name; the mail only says which card moved where.
Bumps to 35.2.1.
2026-09-30 11:02:04 +02:00
Patrick Niebeling 860e8b7bbb Leave the card description out of the moved-cards mail
Build package / unit-tests (push) Successful in 1m14s
Build package / package (push) Successful in 1m17s
Build package / xml-lint (push) Successful in 30s
Build package / php-lint (8.4) (push) Successful in 55s
Build package / php-lint (8.5) (push) Successful in 56s
2026-09-30 10:41:20 +02:00
Patrick Niebeling 1ffc0765ea Switch the app to German as its only language
The app is used internally only and its settings page was German
already. Mails, controller error messages (shown in the form), the
settings section name and the mail sender are now written in German
directly instead of going through IL10N without any translation files.
Nextcloud's own mail footer is requested in German as well.
2026-09-30 10:39:12 +02:00
Patrick Niebeling 578b235f08 Notify board members about moved cards according to their Deck setting
Build package / unit-tests (push) Failing after 20s
Build package / package (push) Skipped
Build package / xml-lint (push) Successful in 46s
Build package / php-lint (8.4) (push) Successful in 1m30s
Build package / php-lint (8.5) (push) Successful in 1m36s
Replaces the yes/no notify_email flag with notify_mode:
- off:   no mail
- owner: only the board owner, as before (migrated from notify_email=1)
- board: everyone with access to the board, filtered by their own Deck
         "Notifications" setting for it (board:<id>:notify-due: off /
         assigned / all), mirroring Deck's NotificationHelper rules

Recipients are resolved via PermissionService::findUsers() (owner, ACL,
groups and circles expanded), AssignmentMapper::isUserAssigned() and
IConfig, none of which reads Deck's frozen $userId. If they cannot be
resolved, no mail is sent rather than one to the wrong people.

Moved cards are collected over the whole run and each recipient gets a
single digest mail afterwards, in their own language.

Bumps to 35.2.0 so the migrations run.
2026-09-30 10:31:45 +02:00
Patrick Niebeling 27717205cd Read Deck due dates as UTC regardless of PHP's default timezone
Deck stores duedate as a zoneless DATETIME holding UTC wall-clock time,
and Entity hydrates it in PHP's default zone. That is only correct
because Nextcloud forces the default to UTC; pin the wall clock to UTC
explicitly so the overdue check compares real instants either way.
2026-09-30 10:27:13 +02:00
Patrick Niebeling ed684cca4f Translate CLAUDE.md to German and condense it
Drops history and duplicated explanations, fixes the stale branch
description (nextcloud-35 was merged into main; the 34.x line only
survives as tags) and the miscounted frontend gotchas.
2026-09-30 10:22:52 +02:00
Patrick Niebeling 0904292c73 Remove the assigned-user and label filters
Build package / unit-tests (push) Successful in 38s
Build package / package (push) Successful in 1m44s
Build package / php-lint (8.4) (push) Successful in 1m33s
Build package / php-lint (8.5) (push) Successful in 49s
Build package / xml-lint (push) Successful in 13s
Workflows now move every overdue card of their source stack. A new
migration drops filter_user_ids and filter_label_ids; workflows that
had a filter keep running unfiltered. The version goes to 35.1.0 so
the migration runs on update.

Removed along with the filters: the labels and participants endpoints
and their Deck lookups, the dead-filter warnings and markers, and
CardService::enrichCards() in the card read. enrichCards() only added
labels, assignees and comment counts, and ran a permission check
against Deck's frozen uid.
2026-09-30 09:55:54 +02:00
Patrick Niebeling e84123654e Tie workflows to the board owner instead of their creator
Build package / unit-tests (push) Successful in 1m5s
Build package / package (push) Successful in 1m21s
Build package / php-lint (8.4) (push) Successful in 2m22s
Build package / php-lint (8.5) (push) Successful in 57s
Build package / xml-lint (push) Successful in 18s
A workflow now belongs to whoever owns its board. The settings page
lists, edits and deletes workflows by board ownership alone, using
Deck's BoardMapper::findAllByOwner(), a single owner query that never
returns shared boards and carries no frozen uid. A transferred board's
workflows therefore show up for the new owner immediately.

The background job still needs one user to run as and mail, so it
looks up the board owner before every run and stores it in user_id
when it changed. Archived boards stay listed so their workflows keep
a name, but are no longer offered or accepted as a target.
2026-09-30 09:38:11 +02:00
Patrick Niebeling ada16cb101 Only allow workflows on owned boards, and show the board in the table
Build package / unit-tests (push) Successful in 42s
Build package / package (push) Successful in 1m4s
Build package / php-lint (8.4) (push) Successful in 1m7s
Build package / php-lint (8.5) (push) Successful in 56s
Build package / xml-lint (push) Successful in 17s
The board list now only contains boards the current user owns, which
also makes the controller's board validation enforce the owner rule.

The workflow table gains a Board column (Board, Titel, Von Stapel,
Nach Stapel, Filter, E-Mail, Aktiv) and is sorted alphabetically by
those columns in that order.
2026-09-30 09:16:09 +02:00
Patrick Niebeling dfec88d396 Start a permanent Nextcloud 35 version line at 35.0.0
Build package / unit-tests (push) Successful in 45s
Build package / xml-lint (pull_request) Successful in 13s
Build package / unit-tests (pull_request) Successful in 49s
Build package / package (push) Successful in 57s
Build package / php-lint (8.4) (push) Successful in 52s
Build package / php-lint (8.5) (push) Successful in 49s
Build package / xml-lint (push) Successful in 15s
Build package / php-lint (8.4) (pull_request) Successful in 47s
Build package / php-lint (8.5) (pull_request) Successful in 45s
Build package / package (pull_request) Successful in 49s
Nextcloud Hub Summer 26 (server 35.x) is declared as the sole
supported major on this branch, per the existing versioning scheme
(leading version number tracks the declared NC major). main keeps
tracking Nextcloud 34 (Hub 26 Spring) at the 34.x.x line; the two are
maintained in parallel rather than one replacing the other.
2026-09-22 07:36:15 +02:00
Patrick Niebeling 0e66319a06 Let each workflow watch the due date or the start date
Build package / unit-tests (push) Failing after 39s
Build package / package (push) Skipped
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
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 43648f3728 Use app.svg for the personal settings section icon
Was already pending in the working tree; committed unchanged.
2026-08-25 15:28:21 +02:00
Patrick Niebeling ce749e63a4 Fix overdue check to compare real timestamps, not whole calendar days
Build package / package (push) Successful in 57s
Build package / unit-tests (push) Successful in 37s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / php-lint (8.5) (push) Successful in 35s
Build package / xml-lint (push) Successful in 11s
WorkflowRunner::isOverdue() took Card::getDaysUntilDue(), which Deck
computes by truncating both "now" and the due date to midnight before
diffing. A card due earlier today therefore reported 0 days until due -
not overdue - until the calendar date rolled over to the next day, so
a card due "this morning" sat unmoved for up to 24h while one due
"yesterday morning" moved immediately. isOverdue() now takes the due
date and $now directly and compares them as real timestamps, matching
what Deck's own UI shows.

Bump to 34.0.2.
2026-08-25 13:48:02 +02:00
Patrick Niebeling b203ba35f8 Align declared PHP compatibility with CI: require 8.4-8.5
Build package / unit-tests (push) Successful in 37s
Build package / package (push) Successful in 52s
Build package / php-lint (8.4) (push) Successful in 36s
Build package / php-lint (8.5) (push) Successful in 35s
Build package / xml-lint (push) Successful in 14s
Bump to 34.0.1.
2026-08-25 13:28:52 +02:00
Patrick Niebeling ef973af4b5 Drop PHP 8.2/8.3 from CI, test against 8.4/8.5 only
Build package / unit-tests (push) Successful in 39s
Build package / package (push) Successful in 52s
Build package / php-lint (8.4) (push) Successful in 36s
Build package / php-lint (8.5) (push) Successful in 33s
Build package / xml-lint (push) Successful in 11s
2026-08-25 13:14:02 +02:00
Patrick Niebeling a95d5e2d8a Switch to Nextcloud-major-aligned versioning starting at 34.0.0
Build package / package (push) Successful in 55s
Build package / php-lint (8.2) (push) Successful in 40s
Build package / unit-tests (push) Successful in 36s
Build package / php-lint (8.3) (push) Successful in 43s
Build package / php-lint (8.4) (push) Successful in 35s
Build package / php-lint (8.5) (push) Successful in 35s
Build package / xml-lint (push) Successful in 11s
Leading version number now tracks the supported Nextcloud major
(currently 34, i.e. Hub 26 Spring) instead of an independent 0.x
count, so compatibility is visible at a glance. Documents the scheme
in CLAUDE.md's release process section.
2026-08-25 13:06:47 +02:00
Patrick Niebeling 2d2f3ca19c Fix overdue detection: stop discarding enriched cards for CardDetails wrappers
Build package / php-lint (8.2) (push) Successful in 45s
Build package / unit-tests (push) Successful in 44s
Build package / package (push) Successful in 1m1s
Build package / php-lint (8.3) (push) Successful in 39s
Build package / php-lint (8.4) (push) Successful in 42s
Build package / php-lint (8.5) (push) Successful in 39s
Build package / xml-lint (push) Successful in 12s
getActiveCardsInStack() reassigned $cards to CardService::enrichCards()'s
return value. That return value is an array of CardDetails wrappers whose
own entity fields (duedate, stackId, ...) are never populated from the
wrapped card - only its overridden jsonSerialize() reads through to them.
So every card's getDuedate()/getDaysUntilDue() resolved to null, and
WorkflowRunner::isOverdue() was always false: no workflow could ever match
a card, regardless of its actual due date.

enrichCards() already mutates the original $cards entries in place (labels,
assigned users, comment counts, ...), so the fix is to keep using that
array instead of capturing the CardDetails return value.

Bump to 0.2.3.
2026-08-25 13:01:35 +02:00
Patrick Niebeling a6dde35249 Bump version to 0.2.2 for the WorkflowRunner logging change
Build package / package (push) Successful in 51s
Build package / php-lint (8.2) (push) Successful in 40s
Build package / unit-tests (push) Successful in 38s
Build package / php-lint (8.3) (push) Successful in 38s
Build package / php-lint (8.4) (push) Successful in 35s
Build package / php-lint (8.5) (push) Successful in 35s
Build package / xml-lint (push) Successful in 13s
2026-08-25 12:22:20 +02:00
Patrick Niebeling 7ca728e79c Log matched and moved overdue cards in WorkflowRunner
Build package / php-lint (8.2) (push) Successful in 48s
Build package / unit-tests (push) Successful in 37s
Build package / package (push) Successful in 1m0s
Build package / php-lint (8.3) (push) Successful in 43s
Build package / php-lint (8.4) (push) Successful in 34s
Build package / php-lint (8.5) (push) Successful in 34s
Build package / xml-lint (push) Successful in 12s
Adds an info-level log line when a card passes a workflow's overdue/
filter checks, and another once the move to the target stack
succeeds - so a run can be traced per card/workflow in the log,
filterable by the app id.
2026-08-25 12:20:28 +02:00
Patrick Niebeling be6faa7b42 Allow PHP 8.5
Build package / package (push) Successful in 58s
Build package / php-lint (8.2) (push) Successful in 43s
Build package / unit-tests (push) Successful in 40s
Build package / php-lint (8.3) (push) Successful in 39s
Build package / php-lint (8.4) (push) Successful in 37s
Build package / php-lint (8.5) (push) Successful in 34s
Build package / xml-lint (push) Successful in 11s
occ upgrade refused to install the app on a PHP 8.5 instance:

  Exception: App "Array" cannot be installed because the following
  dependencies are not fulfilled: PHP wird in einer frueheren Version
  als 8.4 benoetigt.

That is the max-version="8.4" in appinfo/info.xml, not a real
incompatibility -- nothing in this app is bounded above by 8.4.

Raise it to 8.5 and add 8.5 to the php-lint matrix, so CI covers the
version the server actually runs instead of stopping one below it.
2026-08-13 23:15:43 +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 / unit-tests (push) Successful in 42s
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
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 / unit-tests (push) Successful in 44s
Build package / package (push) Successful in 59s
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
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 / unit-tests (push) Successful in 45s
Build package / package (push) Successful in 1m3s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / xml-lint (push) Successful in 13s
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 ee72d6fcf9 Do not move cards that are in the trash
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.
2026-08-13 15:36:39 +02:00
Patrick Niebeling 80371d48fb Never let a throwable escape the workflow run
Build package / php-lint (8.2) (push) Successful in 58s
Build package / unit-tests (push) Successful in 38s
Build package / package (push) Successful in 58s
Build package / php-lint (8.3) (push) Successful in 41s
Build package / php-lint (8.4) (push) Successful in 36s
Build package / xml-lint (push) Successful in 12s
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.
2026-08-13 15:24:16 +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 / unit-tests (push) Successful in 44s
Build package / package (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
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 058eac253e Keep the dev-only root files out of the appstore package
Build package / php-lint (8.2) (push) Successful in 49s
Build package / php-lint (8.3) (push) Successful in 39s
Build package / unit-tests (push) Successful in 45s
Build package / package (push) Successful in 57s
Build package / php-lint (8.4) (push) Successful in 40s
Build package / xml-lint (push) Successful in 12s
CLAUDE.md was shipped inside workflow_deck_automation.tar.gz. Exclude it
along with two files in the same category: CLAUDE.local.md, which is
gitignored private environment notes and must never end up in a downloadable
package, and tsconfig.json, which exists purely so vite-plugin-dts has a
config to resolve at build time and is dead weight on the server.
2026-08-13 15:04:37 +02:00
Patrick Niebeling 68d88e952b Include the card description in the notification mail
Build package / php-lint (8.2) (push) Successful in 1m46s
Build package / unit-tests (push) Successful in 45s
Build package / package (push) Successful in 1m4s
Build package / php-lint (8.3) (push) Successful in 47s
Build package / php-lint (8.4) (push) Successful in 38s
Build package / xml-lint (push) Successful in 17s
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.
2026-08-13 15:01:53 +02:00
Patrick Niebeling 01fdd51b5a Keep the stored stacks and filters when editing a workflow
Build package / php-lint (8.2) (push) Successful in 43s
Build package / php-lint (8.3) (push) Successful in 40s
Build package / unit-tests (push) Successful in 37s
Build package / package (push) Successful in 58s
Build package / php-lint (8.4) (push) Successful in 39s
Build package / xml-lint (push) Successful in 11s
editWorkflow() filled the form and then called onBoardChange() to populate
the dependent dropdowns -- but that function exists to *reset* them, so it
immediately nulled sourceStackId, targetStackId and both filter lists again.
The board stayed selected because it is not one of the fields it clears.

Split the two responsibilities: loadBoardOptions() only fetches the
stack/label/user lists, onBoardChange() clears the dependent selection and
then delegates to it. Editing now calls loadBoardOptions() directly.

onBoardChange() additionally ignores re-picking the board that is already
loaded, so selecting the same entry again no longer wipes the form.
2026-08-13 14:51:28 +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 / unit-tests (push) Successful in 43s
Build package / package (push) Canceled after 0s
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
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 / unit-tests (push) Successful in 46s
Build package / package (push) Successful in 1m3s
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
- 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 8f3e3e694a Gate the release on the lint and test jobs
Build package / php-lint (8.2) (push) Successful in 47s
Build package / unit-tests (push) Successful in 40s
Build package / package (push) Successful in 1m1s
Build package / php-lint (8.3) (push) Successful in 36s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / xml-lint (push) Successful in 14s
The four workflows all triggered on the same push to main and ran fully
independently, so build-main.yml published a latest-main release even when
PHPUnit or the linters were red. `needs:` only works between jobs of one
workflow, so the checks move into build-main.yml and the package job now
depends on them.

Two gaps close along the way: the check workflows only ran on main pushes
and pull requests, so a v* tag was published entirely unverified, and
`npm run build` only ran on main, so no pull request ever exercised the
frontend build -- the workflow now runs on pull_request too, with the
publish step skipped so the job acts as a build check.

Build and publish deliberately stay in a single job; moving the tarball
between jobs would require actions/upload-artifact (unusable on this Gitea,
see CLAUDE.md) or actions/cache.
2026-08-13 14:17:00 +02:00
Patrick Niebeling 85b42d84c9 Fix unusable personal settings form: v8 Vue idioms and Deck relations
Build package / package (push) Successful in 50s
Lint info.xml / xml-lint (push) Successful in 20s
Lint PHP / php-lint (8.2) (push) Successful in 58s
Lint PHP / php-lint (8.3) (push) Successful in 57s
Lint PHP / php-lint (8.4) (push) Successful in 55s
PHPUnit / unit-tests (push) Successful in 1m7s
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.
2026-08-13 14:13:11 +02:00
Patrick Niebeling 9a8e4e47f0 Use the built-in GITEA_TOKEN instead of a hand-made RELEASE_TOKEN secret
Build package / package (push) Successful in 59s
Lint info.xml / xml-lint (push) Successful in 13s
Lint PHP / php-lint (8.2) (push) Successful in 43s
Lint PHP / php-lint (8.3) (push) Successful in 36s
Lint PHP / php-lint (8.4) (push) Successful in 34s
PHPUnit / unit-tests (push) Successful in 40s
Gitea injects a token into every Actions job as secrets.GITEA_TOKEN, so the
manually created PAT was never necessary. Declare permissions: contents:
write on the job, which Gitea maps to Code: write (deleting the latest-main
tag) and Releases: write (creating the release, uploading the asset) - the
full set the publish step needs.

Removes a secret that had to be created by hand, could expire, and was a
single point of failure the workflow had no fallback for.
2026-08-13 13:54:38 +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 Niebeling d2f6640d3d Fix blank personal settings page from mismatched bundle names
Build main artifact / package (push) Successful in 1m11s
Lint info.xml / xml-lint (push) Successful in 14s
Lint PHP / php-lint (8.2) (push) Successful in 48s
Lint PHP / php-lint (8.3) (push) Successful in 39s
Lint PHP / php-lint (8.4) (push) Successful in 34s
PHPUnit / unit-tests (push) Successful in 37s
@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.
2026-08-13 13:38:38 +02:00
Patrick Niebeling c5bf6f8f0d Add rolling main-branch build workflow for manual server deploys
Build main artifact / package (push) Successful in 1m2s
Lint info.xml / xml-lint (push) Successful in 15s
Lint PHP / php-lint (8.2) (push) Successful in 46s
Lint PHP / php-lint (8.3) (push) Successful in 45s
Lint PHP / php-lint (8.4) (push) Successful in 37s
PHPUnit / unit-tests (push) Successful in 42s
Publishes the appstore package as a Gitea release asset on every push
to main, so a deployable build can be grabbed without cutting a
version tag first.
2026-08-13 12:52:29 +02:00
Patrick Niebeling 086c8c06f3 Pin actions/upload-artifact back to v3
Lint info.xml / xml-lint (push) Successful in 15s
Lint PHP / php-lint (8.2) (push) Successful in 42s
Lint PHP / php-lint (8.3) (push) Successful in 43s
Lint PHP / php-lint (8.4) (push) Successful in 41s
PHPUnit / unit-tests (push) Successful in 48s
Build release artifact / package (push) Successful in 1m49s
v4+ uses the @actions/artifact v2 backend, which this self-hosted
Gitea instance rejects as unsupported since it identifies as GHES to
the action's GHES-detection check. checkout/setup-node don't have
this issue and stay on v7.
2026-08-13 12:13:52 +02:00
Patrick Niebeling d4c75c4942 Bump pipeline actions to latest major versions
Lint info.xml / xml-lint (push) Successful in 28s
Lint PHP / php-lint (8.2) (push) Successful in 51s
Lint PHP / php-lint (8.3) (push) Successful in 40s
Lint PHP / php-lint (8.4) (push) Successful in 47s
PHPUnit / unit-tests (push) Successful in 49s
Build release artifact / package (push) Failing after 2m19s
actions/checkout v4 -> v7, actions/setup-node v4 -> v7,
actions/upload-artifact v4 -> v7 across all workflows.
shivammathur/setup-php stays on the floating @v2 tag, which already
resolves to the latest 2.x release.
2026-08-13 12:06:26 +02:00
Patrick Niebeling e761c2c369 Replace rsync with tar-pipe copy in appstore packaging
Lint info.xml / xml-lint (push) Successful in 44s
Lint PHP / php-lint (8.2) (push) Successful in 2m1s
Lint PHP / php-lint (8.3) (push) Successful in 39s
Lint PHP / php-lint (8.4) (push) Successful in 38s
PHPUnit / unit-tests (push) Successful in 43s
Build release artifact / package (push) Failing after 1m32s
The self-hosted Gitea runner's image has no rsync installed. tar is
a core utility present on essentially any Linux base image, and its
--exclude on a directory prevents descending into it at all, which
also avoids the build/ output dir (itself inside the source tree)
being copied into itself.
2026-08-13 11:49:08 +02:00
Patrick Niebeling d5ce1b3017 Fix @nextcloud/vue component imports for v9 exports map
Lint info.xml / xml-lint (push) Successful in 22s
Lint PHP / php-lint (8.2) (push) Successful in 52s
Lint PHP / php-lint (8.4) (push) Successful in 1m1s
PHPUnit / unit-tests (push) Successful in 42s
Build release artifact / package (push) Failing after 1m46s
Lint PHP / php-lint (8.3) (push) Successful in 45s
v9 restricts subpath imports via package.json "exports" to
@nextcloud/vue/components/Xxx; the old dist/Components/Xxx.js deep
import path used throughout PersonalSettings.vue is no longer
resolvable under Node's ESM export map and failed the build.
2026-08-13 11:39:47 +02:00
Patrick Niebeling 09d59d936d Add missing typescript devDependency to fix vite-plugin-dts crash
Lint info.xml / xml-lint (push) Successful in 12s
Lint PHP / php-lint (8.2) (push) Successful in 45s
Lint PHP / php-lint (8.3) (push) Successful in 39s
Lint PHP / php-lint (8.4) (push) Successful in 42s
PHPUnit / unit-tests (push) Successful in 39s
Build release artifact / package (push) Failing after 1m34s
vite-plugin-dts peer-depends on typescript: "*" but nothing installed
it, so it crashed at module-load time before tsconfig.json was ever
consulted. The earlier tsconfig.json fix alone was insufficient.
2026-08-13 11:26:48 +02:00
Patrick Niebeling f530b8e487 Track CLAUDE.md, keep only CLAUDE.local.md ignored
Lint info.xml / xml-lint (push) Successful in 14s
Lint PHP / php-lint (8.2) (push) Successful in 46s
Lint PHP / php-lint (8.3) (push) Successful in 1m0s
Lint PHP / php-lint (8.4) (push) Successful in 50s
PHPUnit / unit-tests (push) Successful in 49s
CLAUDE.md is now committed project guidance for future Claude Code
sessions; CLAUDE.local.md stays gitignored for machine-local notes.
2026-08-13 11:08:21 +02:00
Patrick Niebeling e18df52792 Add tsconfig.json to fix vite-plugin-dts crash during config load
Lint info.xml / xml-lint (push) Successful in 14s
Lint PHP / php-lint (8.2) (push) Successful in 47s
Lint PHP / php-lint (8.3) (push) Successful in 49s
Lint PHP / php-lint (8.4) (push) Successful in 40s
PHPUnit / unit-tests (push) Successful in 43s
Build release artifact / package (push) Failing after 1m30s
@nextcloud/vite-config's index.js barrel statically re-exports
createLibConfig from libConfig.js, which imports vite-plugin-dts at
module scope. That import chain runs even though we only use
createAppConfig, and vite-plugin-dts crashes while resolving a
tsconfig that did not exist in this (plain JS/Vue) project.
2026-08-13 11:05:37 +02:00
Patrick Niebeling c081b00205 Declare package as ESM (type: module)
Lint info.xml / xml-lint (push) Successful in 20s
Lint PHP / php-lint (8.2) (push) Successful in 48s
Lint PHP / php-lint (8.3) (push) Successful in 40s
Lint PHP / php-lint (8.4) (push) Successful in 36s
PHPUnit / unit-tests (push) Successful in 41s
Build release artifact / package (push) Failing after 1m28s
vite.config.js was being loaded as CommonJS by default, so Vite's
config bundler tried to require() the ESM-only @nextcloud/vite-config
and failed. All our JS already uses import/export syntax.
2026-08-13 10:50:16 +02:00
Patrick Niebeling 431879cba4 Fix vite version to satisfy @nextcloud/vite-config peer requirement
Lint info.xml / xml-lint (push) Successful in 14s
Lint PHP / php-lint (8.2) (push) Successful in 1m17s
Lint PHP / php-lint (8.3) (push) Successful in 38s
Lint PHP / php-lint (8.4) (push) Successful in 36s
PHPUnit / unit-tests (push) Successful in 41s
Build release artifact / package (push) Failing after 1m30s
@nextcloud/vite-config@2.5.4 (resolved from our ^2.2.0 range) requires
vite@^7.3.6 as a peer; we had vite pinned to ^6.0.0, causing an
ERESOLVE conflict during npm install.
2026-08-13 10:41:24 +02:00
Patrick Niebeling e5f02f3bcc Fix build-release.yml: use npm install, not npm ci
Lint info.xml / xml-lint (push) Successful in 19s
Lint PHP / php-lint (8.2) (push) Successful in 47s
Lint PHP / php-lint (8.3) (push) Successful in 38s
Lint PHP / php-lint (8.4) (push) Successful in 35s
PHPUnit / unit-tests (push) Successful in 37s
Build release artifact / package (push) Failing after 55s
No package-lock.json is committed (intentionally gitignored), so npm ci
has nothing to install from and fails with EUSAGE.
2026-08-13 10:34:55 +02:00
Patrick Niebeling fe0dc909d0 Bump version to 0.1.0 for release, ignore local Claude notes
Lint info.xml / xml-lint (push) Successful in 13s
Lint PHP / php-lint (8.2) (push) Successful in 47s
Lint PHP / php-lint (8.3) (push) Successful in 38s
Lint PHP / php-lint (8.4) (push) Successful in 42s
PHPUnit / unit-tests (push) Successful in 38s
Build release artifact / package (push) Failing after 1m38s
.gitignore now excludes CLAUDE.md/CLAUDE.local.md and common
IDE/OS/env cruft so local assistant notes never get committed.
2026-08-13 10:26:09 +02:00
Patrick NiebelingandClaude Sonnet 5 6da1a442a6 Fix composer platform override blocking CI dependency resolution
Lint info.xml / xml-lint (push) Successful in 14s
Lint PHP / php-lint (8.2) (push) Successful in 44s
Lint PHP / php-lint (8.3) (push) Successful in 35s
Lint PHP / php-lint (8.4) (push) Successful in 38s
PHPUnit / unit-tests (push) Successful in 41s
config.platform.php was pinned to 8.2, but nextcloud/ocp:dev-master
requires PHP 8.3+, so Composer failed to resolve even though the
runner's actual PHP (8.3.33) satisfies it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:17:58 +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