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.
This commit is contained in:
Patrick Niebeling
2026-08-13 15:47:55 +02:00
parent ee72d6fcf9
commit 0eff367268
7 changed files with 279 additions and 5 deletions
+46 -1
View File
@@ -25,7 +25,12 @@
</thead>
<tbody>
<tr v-for="workflow in workflows" :key="workflow.id">
<td>{{ workflow.title }}</td>
<td>
{{ workflow.title }}
<span v-if="workflowIssues[workflow.id]" class="wfda-issue">
{{ workflowIssues[workflow.id] }}
</span>
</td>
<td>{{ stackLabel(workflow.boardId, workflow.sourceStackId) }}</td>
<td>{{ stackLabel(workflow.boardId, workflow.targetStackId) }}</td>
<td>
@@ -175,6 +180,40 @@ const boardOptions = computed(() => boards.value.map((board) => ({ value: board.
const sourceStackOptions = computed(() => stackOptions.value.filter((option) => option.value !== form.targetStackId))
const targetStackOptions = computed(() => stackOptions.value.filter((option) => option.value !== form.sourceStackId))
/**
* Workflows whose board or stacks were deleted in Deck, keyed by id. The
* background job switches these off by itself, but a row that just stopped
* working needs to say so instead of looking healthy.
*/
const workflowIssues = computed(() => {
const issues = {}
for (const workflow of workflows.value) {
if (!boards.value.some((board) => board.id === workflow.boardId)) {
issues[workflow.id] = 'Board nicht mehr vorhanden'
continue
}
const stacks = stacksByBoard[workflow.boardId]
if (!stacks) {
continue
}
const missing = []
if (!stacks.some((stack) => stack.id === workflow.sourceStackId)) {
missing.push('Quell-Stapel')
}
if (!stacks.some((stack) => stack.id === workflow.targetStackId)) {
missing.push('Ziel-Stapel')
}
if (missing.length) {
issues[workflow.id] = `${missing.join(' und ')} nicht mehr vorhanden`
}
}
return issues
})
const form = reactive(emptyForm())
function emptyForm() {
@@ -370,6 +409,12 @@ onMounted(loadAll)
justify-content: flex-end;
}
.wfda-issue {
display: block;
color: var(--color-error);
font-size: 0.9em;
}
.wfda-empty {
color: var(--color-text-maxcontrast);
margin-bottom: 16px;