mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
Boards opened under the versions before #6515 had every per-swimlane list
un-bound automatically: the board data-repair treated any list with a
swimlaneId as #6484 corruption and cleared it, and a per-swimlane list is
indistinguishable from a corrupted board-wide one at the data level. #6515
stopped it. Nothing put the bindings back, so those lists still render under
every swimlane, and deleting one from a swimlane deletes the only list document
there is - which is what #6670 reports.
The old value is recoverable, not guessable. That clearing went through
Lists.direct.updateAsync, which bypasses collection hooks, so it only ever
touched the list document - while the binding each list was CREATED with is
recorded in a different collection:
// models/lists.js - Lists.after.insert -> trackOriginalPosition()
originalSwimlaneId: this.swimlaneId || null,
if (!existingHistory) { PositionHistory.insertAsync(document); }
That insert is insert-ONLY, written once at creation and never overwritten, so
it survived untouched for every list created since list position tracking
landed (2025-10-16, 2543df942).
Admin Panel / Problems / Summary now detects this the way it detects broken
cards - "Lists missing their swimlane N", with a Restore button beside it -
and restoring puts each list back in the swimlane its own record names.
Every rule is a reason to SKIP, because the failure mode here is worse than
doing nothing:
- a list that already has a swimlaneId is never touched, so the repair is
idempotent and cannot undo a binding an admin has since set by hand;
- a list with no record, or one recorded as board-wide (null), stays
board-wide;
- a swimlane that has since been deleted is not resurrected, and one on
another board is refused - either would hide the list in every swimlane,
which is worse than leaving it shared.
Nothing is inferred from the cards, and a test pins that the planner cannot
grow a use for them. Inference is the obvious idea and it is wrong: on a board
whose second swimlane is new every card is still in the first one, so it would
bind every list to swimlane 1 and hide them from the others - #6484 again,
the bug the clearing existed to fix.
Detection is read-only and swallows its own errors, because the Problems page
polls it every 30 seconds and a detection that throws would take the other
problems on that page with it. The repair writes swimlaneId and nothing else,
through .direct, one update per swimlane rather than one per list.
One new string, translated into all 197 locales that were complete, each
following that file's own "Repaired __fixed__ card(s)." sibling so the count
style and script match what the language already uses here. Low confidence on
the long tail - Bislama, Buryat, Cherokee, Ewe, Fijian, Greenlandic, Inuktitut,
Klingon, Nahuatl, Sakha, Tamazight, Venda, Volapük, Wolof - which a speaker
should correct. Noted while writing them: the `lists` value is wrong-language in
mn (Russian), sk (Czech), sr and wa-RR (Tagalog); not touched here.
Also fixed a guard that the parallel worktree exposed:
tests/noIdentityReplacement.test.cjs walked into .claude/worktrees/, a full
second checkout, and reported the deliberate samples in ITS copy of that test
file as real findings.
Thanks to TawsTm and xet7 !
76 lines
2.8 KiB
JavaScript
76 lines
2.8 KiB
JavaScript
'use strict';
|
|
|
|
// Pure builder for the Admin Panel / Problems "Status" overview and the
|
|
// `snap run wekan.problems` overview: given a snapshot of what is in progress and
|
|
// what problems were detected, it returns a single normalized overview object.
|
|
// Pure + unit-testable; the server (server/lib/systemStatus.js) and the snap
|
|
// command both gather the snapshot and render this same shape.
|
|
//
|
|
// input = {
|
|
// inProgress: [{ kind, active, message }], // migrations / repairs running
|
|
// brokenCards: <number>, // broken/orphaned cards count
|
|
// unboundLists: <number>, // lists whose swimlane can be restored
|
|
// loginProblems: [{ id, title, detail, severity, ok }],
|
|
// extraProblems: [{ id, severity, title, detail, count? }], // optional
|
|
// }
|
|
|
|
function buildProblemsOverview(input) {
|
|
const i = input || {};
|
|
|
|
const inProgress = (Array.isArray(i.inProgress) ? i.inProgress : [])
|
|
.filter(x => x && x.active);
|
|
|
|
const problems = [];
|
|
|
|
const brokenCards = Number.isFinite(i.brokenCards) ? i.brokenCards : 0;
|
|
if (brokenCards > 0) {
|
|
problems.push({
|
|
id: 'broken-cards',
|
|
severity: 'warning',
|
|
count: brokenCards,
|
|
title: 'Broken cards',
|
|
detail: `${brokenCards} card(s) with a missing board/list/swimlane or an invalid type. Open the board to auto-repair, or run the repair migration.`,
|
|
});
|
|
}
|
|
|
|
// #6670: lists that were un-bound from their swimlane by the pre-#6515
|
|
// automatic repair, and whose original swimlane is still recorded in
|
|
// positionHistory so it can be put back exactly. Only counted when something
|
|
// CAN be restored, so the admin is never shown a problem with no remedy.
|
|
const unboundLists = Number.isFinite(i.unboundLists) ? i.unboundLists : 0;
|
|
if (unboundLists > 0) {
|
|
problems.push({
|
|
id: 'unbound-lists',
|
|
severity: 'warning',
|
|
count: unboundLists,
|
|
title: 'Lists missing their swimlane',
|
|
detail: `${unboundLists} list(s) lost the swimlane they belonged to and now appear under every swimlane, so deleting one from a swimlane deletes it from all of them. Restore puts each list back in the swimlane it was CREATED in, which is still recorded; lists with no record, or whose swimlane has since been deleted, are left board-wide.`,
|
|
});
|
|
}
|
|
|
|
// Login-page causes that currently look wrong (ok === false).
|
|
for (const c of Array.isArray(i.loginProblems) ? i.loginProblems : []) {
|
|
if (c && c.ok === false) {
|
|
problems.push({
|
|
id: c.id,
|
|
severity: c.severity || 'warning',
|
|
title: c.title,
|
|
detail: c.detail,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const p of Array.isArray(i.extraProblems) ? i.extraProblems : []) {
|
|
if (p) problems.push(p);
|
|
}
|
|
|
|
return {
|
|
anyInProgress: inProgress.length > 0,
|
|
inProgress,
|
|
anyProblems: problems.length > 0,
|
|
problems,
|
|
};
|
|
}
|
|
|
|
module.exports = { buildProblemsOverview };
|