Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v9.97
2026-07-17v9.97 2026-07-17 WeKan ® release
This release adds the following new features:
-
Snap: SELF-HEALING attachments — the fixes below apply themselves automatically on upgrade, no commands needed (#6473,
snap-src/bin/attachment-repair(new),snap-src/bin/wekan-control,snap-src/bin/migration-control,snap-src/bin/wekan-force-migrate,releases/migrate-mongodb-to-ferretdb.mjs,snap-src/bin/migrate-mongo3-to-ferretdb.mjs,snapcraft.yaml,snapcraft-core26.yaml). The snap auto-refreshes on ~15k servers, so a fix that needssnap run wekan.migratetyped by hand does not reach most of them — and a full re-migration would be WRONG anyway, because it rebuilds FerretDB from the frozen MongoDB source and would lose boards/cards users created on FerretDB since migrating. Instead, on every start on FerretDB,wekan-controlnow launchesattachment-repairin the background: it starts a temporary source mongod (7.x or the bundled 3.2 reader — the MongoDB data was never modified, so everything is recoverable) and runs the migration importer in a new incremental FILES_ONLY mode that checks what is already migrated and migrates only what is missing: text collections are never touched, every attachment/ avatar whose target record is onfswith the file actually on disk is verified and skipped, records deleted by users since the migration are never resurrected, and only the missing binaries/records are extracted — so on healthy servers the repair is a fast no-op and on #6473-affected servers the attachments simply appear, live, while WeKan runs. It runs once (marker$SNAP_COMMON/.attachments-files-v2-done; a fresh successful migration pre-writes it,snap run wekan.migrateclears it, failures retry on the next start without ever blocking WeKan from starting), and a manualsnap run wekan.repair-attachmentscommand is registered too. The importer itself now stamps the migration marker withfilesVersion(currently 2) and, when it finds a marker with an olderfilesVersion— orFILES_ONLY=truein the environment — automatically switches to this incremental repair, so Docker/source installs get the same self-healing by simply re-running the same importer command they migrated with. Verified end-to-end against two live FerretDB instances with the real importer: a broken-migration target (missing CollectionFS record, gridFsFileId-only record, marker without filesVersion) was repaired — record re-created with its card linkage, binary extracted, record repointed — while a board renamed on FerretDB after the migration stayed untouched, and the second run exited immediately as already-migrated. Behavioral tests (positive + negative) drive the real bash script with stubbed snap tooling:tests/attachmentRepair.test.cjs. Thanks to mueschel and xet7. -
All platforms: startup schema upgrade — WeKan now CHECKS on start that data from EVERY old WeKan version has been migrated to the newest database structure, and migrates only what is missing (#6473 follow-up, #1959, #1971,
server/lib/schemaUpgradeSteps.js(new),server/startupSchemaUpgrade.js(new),server/migrations/ensureValidSwimlaneIds.js,server/imports.js). WeKan v0.9–v8.00 ran startup migrations; v8.01 disabled them (large databases meant long downtime) in favour of read-time compatibility — which covers the swimlane era but not everything, so text data from old versions could sit invisible in the database. The new startup upgrade reinstates the safety net without the downtime: version-gated (the_wekan_migrationmarker stores the WeKan version and datetime of the previous successful re-check, so while the version is unchanged a boot costs ONEfindOne— a full re-check is mandatory only after a new WeKan release, orWEKAN_FORCE_SCHEMA_UPGRADE=true; opt out withWEKAN_SKIP_SCHEMA_UPGRADE=true), non-blocking (runs in the background, WeKan serves immediately), with a live migration dashboard at/schema-upgrade-statuson every platform that shows the Admin Panel product name when one is set (not “WeKan”) plus per-step progress, and fast on big databases (bounded existence probes,distinct()set-joins instead of card scans, and server-sideupdateManybatches instead of per-document round trips — thousands of cards never mean thousands of queries). Steps, each verified againstgit show v8.00:server/migrations.jsand each idempotent:archived-flag-backfill(docs missingarchivednever match thearchived: falseview queries — whole boards/lists/cards were invisible in the Swimlanes and Lists views),swimlane-structure(every board gets a visible swimlane, every list/card aswimlaneId, cards with a danglinglistIdare rescued to a visible list — and, fixing #1959 and #1971, unarchived cards whose swimlane was DELETED, ARCHIVED or belongs to another board are reassigned to the board’s first visible swimlane, so everything is visible in both the Swimlanes view and the Lists view; the card-insert hook now also validates client-supplied swimlaneIds so new cards can never land under a deleted/archived swimlane again),checklist-items-embedded(pre-v0.79 embeddedchecklist.items[]extracted to theChecklistItemscollection — the text was in the database but never shown),customfields-boardIds(pre-v2.49 scalarboardId→boardIdsarray — old custom field definitions and their card values were orphaned),board-allows-defaults(the ~33 defaultValue-trueallows*flags backfilled — a missing flag rendered as false and HID existing descriptions/checklists/comments/attachments),board-members-isactive(members withoutisActivewere denied board access),board-permission-lowercase(‘PUBLIC’ boards had silently become member-only), andfs-path-heal(filesystem attachments/avatars whose recorded path predates the current WRITABLE_PATH layout — v6.10-18uploads/<coll>, v6.19-v8.4xWRITABLE_PATH/<coll>, CFS→ostrio temp files — are located and repointed/copied into the current layout). A step that fails or leaves unresolved work never blocks WeKan from starting and keeps the version un-stamped so the next boot re-checks. 36+ unit tests with negative cases (tests/schemaUpgradeSteps.test.cjs), plus verified end-to-end against a live FerretDB (SQLite) with old-shape seed data: all 8 steps migrate correctly and the second boot is gated to a no-op. Thanks to mueschel and xet7. -
Migration speed: batched writes and preloaded lookups instead of per-document round trips (5-hour migrations reported;
releases/migrate-mongodb-to-ferretdb.mjs). The text phase now copies each batch with ONE unorderedinsertManyround trip (falling back to per-documentreplaceOneupserts only for batches that hit duplicates on resumed/re-run migrations), and the attachment/avatar phases preload the target’s metadata records once per bucket instead of onefindOneper file (bounded: collections over 100k records fall back to per-file lookups). The startup schema upgrade uses the same philosophy (distinct()/updateMany). Thanks to mueschel and xet7.
and fixes the following bugs:
-
OAuth2/OIDC:
OAUTH2_LOGIN_STYLE=redirectwas ignored — a popup always opened (#5695,packages/wekan-oidc/oidc_client.js,server/authentication.js,server/models/settings.js,client/components/main/layouts.js). The setting was stored correctly in the OIDC service configuration, but the login button always passedloginStyle: 'popup', and Meteor’sOAuth._loginStylegives the caller’s option precedence — so the admin’s redirect setting silently lost on every login (and the Meteor-internalloginStyleoption even leaked into the provider’s authorization URL). The client now honors a configuredloginStyle: 'redirect'over the button’s generic popup default (explicit caller choices still win; Safari-private-mode popup fallback kept) and no longer leaks the option to the provider. Also repaired the existingOIDC_REDIRECTION_ENABLED=true“go straight to the provider” feature, which was doubly broken since the Meteor 3 port:isOidcRedirectionEnabledinspected a Promise (always false), and the client handler assigned an undeclared variable (strict-mode ReferenceError). Behavioral tests drive the real client code in a VM against Meteor’s loginStyle precedence:tests/oauth2LoginStyle.test.cjs(12 tests, positive + negative). Thanks to ArturRuta and xet7. -
Deleting a user left “ghost users” on boards and cards (#1289,
models/users.js, newmodels/lib/userDeletionCleanup.js): every deletion path (admin method, self-service delete,DELETE /api/users/:userId) removed only the user document, leaving dangling references — empty-avatar board members that could not be removed, stale card members/assignees/watchers, orphaned avatar files — reproducible for 8 years. A server-sideUsers.after.removehook now prunesboards.members/watchers,cards.members/assignees/watchers,lists.watchersand the user’s avatar files on every deletion path; activities and comments are deliberately kept for history (their rendering is already null-guarded). Tests:tests/userDeletionCleanup.test.cjs(6). Thanks to chotaire and xet7. -
Swimlanes jumped up and down when starting/ending a card drag (#2877,
client/components/boards/boardBody.css): drag start hid every list’s ”+ Add Card” composer link withdisplay: none, collapsing its row — lists shrank, auto-height swimlanes shrank, and every swimlane below jumped up ~23px (and back down on drop), shifting the drop target under the cursor mid-drag (root cause proven by frame-diffing the issue’s own GIF). The composer now hides withvisibility: hidden, keeping its layout box, so nothing moves; collapsed multi-selection cards stay collapsed intentionally (they preview the post-drop list). Tests:tests/swimlaneDragJump.test.cjs(8). Thanks to xet7. -
Dragging a card toward an off-screen list never auto-scrolled the board (#443,
client/components/lists/list.js, newimports/lib/boardAutoScroll.js): the horizontal auto-scroll targeted.board-canvas, which only overflows vertically since the swimlane layout — its scrollLeftMax was always 0, so the guard never fired and users had to drop on an intermediate list and scroll by hand. Edge-proximity auto-scroll now drives the.js-listslane actually under the pointer (clamped, overshoot-safe), with vertical scrolling kept on the canvas. Tests:tests/boardAutoScroll.test.cjs(15, incl. a proof the old no-overflow target could never scroll). Thanks to anhenghuang, AlexanderS and xet7. -
Board Rules: the Card Title Filter did nothing on several triggers — and rules created via the REST API never fired at all (#2345, #2674,
client/components/rules/triggers/boardTriggers.js,.jade,server/rulesHelper.js,server/models/rules.js, newmodels/lib/ruleCardTitleFilter.js,docs/API/Rules.md,docs/API/REST-API.md,api.py): the generic moved/archive trigger builders never saved the filter (and a trigger doc MISSING the field can never satisfy the matcher’s$in, so those rules fired for nothing); a set filter never showed in the rule details; archive activities carry no card title so their filters compared against undefined; and REST-created rules skipped the wildcard defaulting entirely — the exact “remove user when moved away” rule from #2674 silently never ran. Filters are now stored (empty →*), shown in the rule description, matched with the title resolved from the card when the activity lacks it, legacy field-less triggers keep matching, the API normalizes missing matching fields to wildcards and validates types, and the rule actions no longer crash on unresolvable usernames or member-less cards. The Rules REST API is now documented (docs/API/Rules.md) and listed in api.py’s help with the #2674 two-rule example. Tests:tests/rulesCardTitleFilter.test.cjs(12) andtests/rulesApiTriggerNormalize.test.cjs(19). Thanks to InfoSec812, sfahrenholz and xet7. -
Sandstorm: username uniqueness probe was case-sensitive and raced concurrent logins (#574,
sandstorm.js, newmodels/lib/sandstormUsername.js): derivingmax,max1, … from the preferred handle matched exact case only (an existingMaxdid not stop a newmax) and the check-then-set window let a concurrent insert claim the name first, aborting the hook with E11000. The probe is now an anchored, escaped, case-insensitive regex and the claim retries the next number on a duplicate-key loss. Tests:tests/sandstormUsername.test.cjs(11). Thanks to mitar and xet7. -
Inviting a second user whose email shares a local part failed with a bare “403” (#619,
server/models/users.js, newmodels/lib/inviteeUsername.js): invitingcats@foo.comcreates user “cats”; invitingcats@facebook.comthen crashed into Meteor’s raw403 Username already exists. The invitee’s username now probescats,cats1,cats2, … to the first free variant, and exhaustion raises the translatederror-username-takeninstead of a number. Tests:tests/inviteeUsername.test.cjs(11, incl. the literal reported scenario). Thanks to lemoer and xet7. -
Date pickers ignored the configured default time and stored 12:00 (#1502,
client/lib/datepicker.js, newimports/lib/datePickerTime.js): the due-date picker configures a 17:00 default (and now() for received/start/end), but an inverted guard applied it only when the card ALREADY had a date — exactly when it is unnecessary — so empty time fields fell back to a hard-coded 12:00 on save. The default now pre-fills empty pickers and backs the submit fallback; existing dates keep their own time. The issue’s original AM/PM parse mismatch was already resolved by the native date/time inputs (79b94824e). Tests:tests/datePickerDefaultTime.test.cjs(10). Thanks to Vlasterx, saschafoerster, suncobran and xet7. -
Dragging a card while someone added a card to the target list dropped it into the WRONG swimlane (#2769, new
client/lib/cardDragGeometry.js,client/components/lists/listBody.js): jQuery UI sortable snapshots container geometry at drag start; a mid-drag DOM insertion (another user’s new card, or the drag’s own composer auto-close) shifted every swimlane below while the cached rectangles stayed put — the drop landed in the neighbouring swimlane with no visible placeholder. A MutationObserver now refreshes the active drag’s geometry on real mid-drag layout changes (sortable’s own churn filtered out). Tests:tests/cardDragGeometry.test.cjs(13). Thanks to hever and xet7. -
Board import lost card dates (#1992,
models/wekanCreator.js, newmodels/lib/importedCardDates.js): the importer derivedcreatedAtonly from acreateCardactivity (absent in Sandstorm/pruned exports — dates silently reset to import time) and never importedreceivedAt/endAtat all. All five date fields now restore with sane fallbacks (activity → the card’s own exported date → import time), and the card creator falls back to the exported userId. The missing-cards half of the report was already fixed by 68e0032c6. Tests:tests/importedCardDates.test.cjs(13). Thanks to xet7. -
Archiving a swimlane made its cards disappear — and restore brought back an empty swimlane (#2292,
models/swimlanes.js, newmodels/lib/swimlaneArchive.js): only the swimlane document was flagged; its unarchived cards became invisible everywhere (board views render unarchived swimlanes, Archive lists archived docs). Archiving a swimlane now archives its cards (mirroring lists), and restore brings back exactly the cards archived WITH it — individually archived cards stay archived. Tests:tests/archiveSwimlaneCards.test.cjs(11). Thanks to Cactusbone and xet7. -
“Move/Copy selection to board” wrote
sort: NaNto every card (#2494,imports/reactiveCache.js): the client-sidenoCachecard lookup returned a PROMISE since the Meteor 3 port, so the max-sort read wasundefinedand every moved card got NaN — cards appeared and disappeared and could not be reordered. The uncached client path is synchronous minimongo again. Tests:tests/reactiveCacheNoCacheCard.test.cjs(8, incl. a proof the pre-fix routing yields NaN). Thanks to Vermeille and xet7. -
Subtask “View it” did nothing when the subtask lives on another board (#1853,
client/components/cards/subtaskViewHelpers.js,subtasks.js): the original crash (board._idof undefined) had become a silent no-op guard — when the deposit board is not in minimongo the button just did nothing. Navigation now falls back to the subtask’s own boardId (the route loads the board), and truly broken subtasks warn instead of dying. Tests:tests/subtaskViewNavigation.test.cjs(11). Thanks to Vanclief and xet7. -
Labels/members could not be dragged onto cards added after the board rendered (#1554,
client/components/lists/list.js): the droppable-initializing autorun lost its reactive dependency in 7673c77c5 (2023), so it ran once per list render and later-added minicards silently rejected sidebar drags until the board was re-entered (“works after search-and-back”). Dependency restored via the ReactiveCache. Tests:tests/labelDragDroppable.test.cjs(3). Thanks to Miffe and xet7. -
“Add filtered cards to selection” swept cards from OTHER boards into bulk actions (#2306,
client/lib/filter.js,client/lib/multiSelection.js, newmodels/lib/boardScopedSelection.js): the filter selector carried no boardId, and minimongo legitimately holds foreign-board cards (linked boards, dialogs, notifications) — a bulk archive could silently mutate other boards. The selection and its bulk-action selector are now board-scoped, and foreign ids are rejected at insertion. Tests:tests/boardScopedSelection.test.cjs(17, incl. the exact reported repro). Thanks to IcedQuinn and xet7. -
REST API: login tokens could never be revoked (#1437,
server/apiAuthRoutes.js, newmodels/lib/apiLogout.js): everyPOST /users/loginminted another ~90-day resume token with no way to invalidate any of them. NewPOST /users/logoutrevokes the presented token (or all of the user’s tokens with{"all": true}), always scoped to the authenticated user. Tests:tests/apiLogout.test.cjs(12). Thanks to ppouliot and xet7. -
Editing one checklist item and clicking another left BOTH edit forms open — and submitting overwrote the new item’s title with the previous item’s text (#2418,
client/lib/inlinedform.js, newclient/lib/inlinedFormManager.js): since a 2021 change, the “close the previously opened inline form” call was a silent no-op (the escape action is disabled for click execution), and the submit handlers grab the template’s FIRST textarea — with two forms open the wrong form’s text was saved. Subtasks reproduced the full bug; checklists’ workaround corrupted the open-form tracker so Escape closed the whole card pane. A small state manager restores the single-open-form invariant (opening a form closes the previous one, without closing popups — preserving the 2021 intent). Tests:tests/inlinedFormSingleOpen.test.cjs(9, incl. the exact reported repro chain). Thanks to Beebo89 and xet7. -
Advanced Filter never matched date custom fields (#2989,
client/lib/filter.js, newimports/lib/advancedFilter.js): the tokenizer treated every/as a regex delimiter even inside quotes, so'Date de fin' == '06/04/2020'broke tokenizing and the filter silently did nothing; and date custom fields store Date OBJECTS while the selectors compared strings/parseInt —==/</>could never match and!=matched everything. Dates typed in the user’s date format (day-first respected) now build half-open Date-range selectors (==means “that day”), with the legacy behavior untouched for non-date fields. Tests:tests/advancedFilterDate.test.cjs(14, incl. a proof the old selector shape never matched a stored Date). Thanks to k1ng440 and xet7. -
Cards could not be reordered by drag in lists full of subtask cards — with silent data loss on multi-selection drops (#3826,
server/models/cards.js,client/components/lists/list.js, newmodels/lib/cardSortRepair.js):addSubtaskCardinserted EVERY subtask card with the constantsort: -1, so such lists contained only tied sorts; dropping between two equal sorts computes a zero increment, the move modifier came out empty and the card snapped back — and a multi-selection drop wrote the SAME sort to every selected card, permanently destroying their order. Subtask cards now append with a unique sort, and the drop handler detects degenerate (tied/inverted) gaps and repairs the siblings’ sorts to a strict order before recomputing the drop index. Tests:tests/subtaskCardReorder.test.cjs(14). Thanks to jayki and xet7. -
Cross-board subtask full path disappeared after refresh (#3453,
server/publications/boards.js, newserver/lib/subtaskAncestors.js): the board publication shipped only the DIRECT parent cards, while the full-path label walks the whole ancestor chain client-side — after F5 the grandparents were missing from minimongo and the path truncated/vanished. The publication now walks and publishes the full ancestor chain (batched per level, cycle-safe, tolerant of deleted ancestors). Tests:tests/subtaskAncestors.test.cjs(11). Thanks to MPeti1 and xet7. -
Linked cards: phantom empty custom-field rows and a template TypeError on every render (from #3748,
models/cards.js, newmodels/lib/customFieldsWD.js): a linked card keeps the ORIGINAL board’s custom-field snapshot; unresolvable definitions rendered as empty{}placeholders — a phantom row per entry andCannot read properties of undefined (reading 'type')from the card details template (also reachable on normal cards with deleted definitions). Unmatched entries are now skipped. The rest of #3748 is by design: linked cards are pointers that mirror the original; label/custom-field ids are board-scoped, and name-based inheritance is the COPY feature. Tests:tests/customFieldsWD.test.cjs(9). Thanks to peterbecich and xet7. -
Archive sidebar: Restore/Delete links floated ambiguously between two archived cards (#3199,
client/components/sidebar/sidebarArchives.jade,sidebar.css): each card and its links were loose siblings with near-equal spacing above and below, so the links seemed to belong to the card underneath. Each archived card is now grouped with its own links in one container with a clear separator gap below (RTL-safe logical properties, theme-neutral). Tests:tests/archiveLinkGrouping.test.cjs(9). Thanks to fxkr and xet7. -
Attachments uploaded inside card comments: listing in the card’s Attachments section is now guaranteed and regression-locked (#3843, new
models/lib/attachmentMeta.js,client/lib/utils.js): the rich comment editor already uploads into the same Attachments collection with the same card meta as the Attachments popup, so they DO list — but nothing pinned that invariant and the meta was built in two places. One shared, null-safe builder now feeds both paths, with tests pinning that gallery queries key onmeta.cardIdwith no source filter (and that board backgrounds stay excluded). Tests:tests/commentAttachmentsList.test.cjs(12). Thanks to jghaanstra and xet7. -
Maximized card rendered at the wrong place after scrolling in Swimlanes view (#4822,
client/components/cards/cardDetails.css): the legacy maximized-pane CSS had nopositionof its own, so the pane stayed an in-flow item inside the scrolled board canvas (off-screen after scrolling down); the desktop-mode floating-window rules also out-specified every maximize geometry rule, and inline drag offsets survived maximizing. The maximized pane is now viewport-fixedwith explicit insets that beat both the floating-window rules and stale drag offsets (drag position is restored on minimize), RTL-safe via logical properties. A cascade-resolver regression test pins the behavior against the real stylesheet and fails on the pre-fix CSS:tests/maximizedCardPosition.test.cjs(11 tests). Thanks to pravdomil and xet7. -
LDAP group filter locked out admins and rejected multiple groups (#4036,
packages/wekan-ldap/server/ldap.js): withLDAP_GROUP_FILTER_ENABLE=true, only members of the singleLDAP_GROUP_FILTER_GROUP_NAMEgroup could log in — an admin who was only inLDAP_SYNC_ADMIN_GROUPScould not log in at all, and a comma-separated group list produced the literal filter(cn=A,B)that matches nothing. The filter now ORs across every comma-separated group name and, when admin sync is enabled, also admits the admin-sync groups. Thanks to zeisss-mercedes and xet7. -
Board invitation emails could carry a dead invitation code — and signup then failed with “The invitation code doesn’t exist” (#4043,
server/models/settings.js,server/models/users.js, newmodels/lib/invitationCodeEmail.js): re-inviting an unregistered user re-sent the SAME stale code (invalidated by an earlier OAuth2 signup or deleted account) instead of regenerating it; a failed SMTP send deleted a previously delivered, still-valid code; codes were mailed without checking they exist and are valid; the invitee address was only lowercased client-side; and the signup hook deleted the code BEFORE the account insert was committed, so a failed insert burned the code for every retry. Re-invites now regenerate stale codes (still-valid ones are kept so earlier emails keep working), sends fail loudly on unusable codes, rollback only removes codes the failed send itself created, and consumption happens only after a successful signup. Tests:tests/invitationCodeEmail.test.cjs(14). Thanks to jkoenig134 and xet7. -
Japanese/Chinese UI: the add-card button and footer links wrapped mid-word (#4023,
client/components/forms/forms.css): CJK text has no spaces, so the narrow add-card composer footer broke 追加 / リンク / 検索 / テンプレート between any two characters. The composer/edit footers now useword-break: keep-allwithflex-wrap: wrap(wrapping between links, never inside a word) andwhite-space: nowrapon the button and each link group; Latin wrapping is unchanged and the negative tests pin that no global word-break was introduced. Tests:tests/cjkLabelWrap.test.cjs(9). Thanks to yuki-snow1823, Sylvain2703 and xet7. -
Users added to a team AFTER the team was assigned to a board never became board members — and the Admin Panel bulk team add/remove silently did nothing (#4593,
server/models/users.js,client/components/settings/peopleBody.js, newmodels/lib/teamBoardMemberSync.js): assigning a team to a board snapshotted its then-current members, so later joiners could see the board via publications but every authority gate (hasMember, card/list mutations, attachment downloads, export) denied them; and the Admin Panel “Add/Remove team to selected users” used a direct client-sideUsers.updatethat server permissions silently deny.editUser/createUsernow add new team members to all boards their teams are assigned to (never touching existing member entries, skipping template boards), and the bulk actions go through the admineditUsermethod. Tests:tests/teamBoardMemberSync.test.cjs(13). Thanks to szymonsztuka and xet7. -
LDAP background sync only worked once per user (#4654,
packages/wekan-ldap/server/ldap.js,sync.js, newuserIdFilter.js):getUserByIdcrashed or built the invalid filter(|(=user))whenLDAP_UNIQUE_IDENTIFIER_FIELDwas unset/empty (it never consultedLDAP_USER_SEARCH_FIELD, where the stored id actually comes from), and the username sync passed$setas query OPTIONS tofindOneAsync— logging “Syncing user username” while writing nothing. New sharedbuildUserIdFilter()ORs across both configured fields,idAttributeis persisted on new LDAP users, and the username sync actually updates. Tests:tests/ldapUserIdFilter.test.cjs(11). Thanks to fabianrbz and xet7. -
All Boards page: per-list card counts and member avatars never showed (#5174, #4825, new
models/lib/boardTileData.js,server/publications/boards.js,client/components/boards/boardsList.js,.jade): the helpers were stubbed to[]to stop the #4214 reactive “icons dance”, and the gating flags were never published. New non-reactivegetAllBoardsTileDatamethod (one boards query, one lists query, one grouped card count) fetched once per page visit; per-board “Show card count per list”/“Show Board members avatars” settings enforced strictly both ways. Tests:tests/boardTileData.test.cjs(17). Thanks to mueschel, Miuler and xet7. -
Lists rendered with different widths by default (#5659, new
models/lib/listWidth.js,client/components/lists/list.js,listHeader.js,models/users.js): the default width was duplicated in four resolution paths that disagreed (270 vs 272), so lists on the same (public) board could differ with no customization. Single source of truth (272), all paths normalize out-of-range values the same way; customized widths still win. Tests:tests/listWidthDefaults.test.cjs(12). Thanks to butteredCat-2021 and xet7. -
Declining a board invitation kept the board active in the member’s overview (#4730,
server/models/boards.js, newmodels/lib/boardInvites.js): the decline flow calledquitBoard(deactivate) thenacceptInvite, which unconditionally REACTIVATED membership — it also let any removed member re-add themselves.quitBoardnow clears the pending invitation (and works for stale-invite-only users);acceptInviteonly activates when an invitation actually exists. Tests:tests/boardInvites.test.cjs(11). Thanks to Griiimm and xet7. -
After migrating a user from password to LDAP, the old local password still logged in (#4419, new
server/lib/ldapPasswordLoginGuard.js,server/authentication.js): avalidateLoginAttempthook now rejects password-service logins forauthenticationMethod: 'ldap'users while LDAP is enabled — respecting theLDAP_LOGIN_FALLBACK=truefeature, never touching other services or session resumes, and opt-out-able withLDAP_MIGRATION_ALLOW_PASSWORD_LOGIN=trueso no deployment is hard-locked. Tests:tests/ldapPasswordLoginGuard.test.cjs(12). Thanks to preciousamorc and xet7. -
LDAP_ENCRYPTION=true(the documented value) silently connected WITHOUT encryption (#4158, newpackages/wekan-ldap/server/encryptionSetting.js,ldap.js,docs/Login/LDAP.md): only the undocumentedssl/tlsvalues did anything, and any other value (includingtrue, which JSON-parses to a boolean) meant silent plaintext. Nowtrue→LDAPS,starttls→STARTTLS, legacyssl/tlskeep their historical meanings with a deprecation notice, and unknown values log a clear warning listing the accepted ones. Tests:tests/ldapEncryptionSetting.test.cjs(22). Thanks to farwayer and xet7. -
OIDC login onto an existing account wiped the profile — avatars and templates disappeared (#4560,
server/models/users.js): theOAUTH2_MERGE_EXISTING_USERSmerge path replaced the wholeprofilewith the OIDC-derived one, losingavatarUrl,templatesBoardId(+ template swimlanes), language and preferences. The merge now preserves the stored profile and only fills gaps/updates the asserted fullname; the fail-closed linking rules (GHSA-mp7g-hj5q-gxhq) are untouched. Tests:tests/oidcProfileMerge.test.cjs(7, fails on pre-fix code). Thanks to LeoLu-eng and xet7. -
OAuth2/OIDC: concurrent logins could contaminate each other’s user data — users saw stale or missing emails/username/teams, and the database could disagree with the UI (#4897,
packages/wekan-oidc/oidc_server.js,packages/wekan-oidc/loginHandler.js). The OIDC server flow keptprofile,serviceDataanduserinfoas MODULE-SCOPE variables shared by every login of every user: fields the current login did not overwrite leaked from the previous user’s login (refreshToken, whitelisted id-token claims, branch-dependent email), and because the handler awaits the token/userinfo requests, two interleaved logins wrote into the SAME objects — a login could complete carrying another user’s id/email/username, updating the wrong user document. ThePROPAGATE_OIDC_DATAgroup/attribute path additionally ran on implicit GLOBALS (teamArray,isAdmin,user_email, …) with awaits between assignment and use, so concurrent logins could write one user’s email/teams/admin flag onto another user’s document — real database corruption, matching the “web interface shows different data vs mongodb” report. All login state is now per-login locals, the login handler compares actual values (the old username/fullname comparisons compared a string to an object, always true), and a regression test proves isolation under concurrent logins and fails against the pre-fix code:tests/oidcLoginStateIsolation.test.cjs(11 tests, positive + negative). Thanks to gerardo-junior and xet7. -
Snap/Docker: after the MongoDB → FerretDB migration ALL CollectionFS-era attachments were missing (#6473,
releases/migrate-mongodb-to-ferretdb.mjs,snap-src/bin/migrate-mongo3-to-ferretdb.mjs,snap-src/bin/migrate-gridfs-to-fs.mjs). Real CollectionFS (old WeKan’sFS.Store.GridFS('attachments')) stores each file’s GridFS id atcopies.<bucket>.key(copies.attachments.key/copies.avatars.key) — but the importers only looked atoriginal.gridFsFileId,gridFsFileIdandcopies.gridfs.key, none of which exist in that layout. So the modern importer “skipped” every file silently and the MongoDB 3.x importer extracted the binaries but never created an attachment record (and the records it did create lost theirmeta.cardId/boardId, which CollectionFS keeps at the record’s TOP level — an attachment withoutmeta.cardIdshows on no card). Either way the migration reported success with zero attachments visible. A new sharedresolveCfsGridFsId()resolves the id from all four layouts (original.gridFsFileId,gridFsFileId,copies.<bucket>.key,copies.gridfs.key, then anycopies.*.key), the modern importer now drives extraction fromcfs_gridfs.<bucket>.filesitself (so a missing/emptycfs.<bucket>.filerecordcollection no longer skips everything — binaries without a filerecord are still extracted to disk), the mongo3 importer copies the top-levelboardId/cardId/listId/swimlaneId/userIdintometa, and filerecords whose binary cannot be located are reported as errors on the migration dashboard instead of being silently dropped. If you already migrated and attachments are missing, run the migration again:snap run wekan.migrate(the source MongoDB data was never modified). Behavioral positive/negative tests:tests/migrationAttachmentExtraction.test.cjs. Thanks to mueschel and xet7. -
Snap/Docker: Meteor-Files attachments stored in GridFS without a
storage: 'gridfs'flag were left pointing at a GridFS that no longer exists after migration (#6473,releases/migrate-mongodb-to-ferretdb.mjs,snap-src/bin/migrate-gridfs-to-fs.mjs). WeKan’s owngetFileStrategyserves a version from GridFS when its storage flag says'gridfs'or it carries aversions.*.meta.gridFsFileIdreference — those reference-only records worked fine on MongoDB, but the migration’s file phase only matchedversions.original.storage: 'gridfs', so their binaries were never extracted and the record kept pointing into the void (404 after the switch). The record scan now matches both forms (and every version, not justoriginal), and a second, bucket-driven sweep walks<bucket>.filesby itsmetadata.fileIdback-reference (the way WeKan writes GridFS uploads), recovering binaries even when the record’s flags say nothing about GridFS. Any GridFS-flagged version whose binary genuinely cannot be located is reported on the dashboard. Thanks to mueschel and xet7. -
Admin Panel > Attachments showed “/data” as the Filesystem Storage path on every platform (#6473,
client/components/settings/attachments.js,client/components/settings/settingBody.js,server/models/attachmentStorageSettings.js,models/lib/attachmentStoragePath.js). The Blaze helpers computed the path fromprocess.env.WRITABLE_PATHin the browser, whereprocess.envnever has it, so the page always fell back to “/data” — a path that does not exist on a Snap install (the real path is/var/snap/wekan/common/files/attachments), sending admins hunting for a directory that was never there. The client now asks the server via a new admin-onlygetAttachmentStoragePathsmethod, whose Snap-aware computation (shared, dependency-freemodels/lib/attachmentStoragePath.js— WRITABLE_PATH already ends in/fileson Snap,/filesis appended elsewhere) is also used for the settings document’s default filesystem path, which pointed at/data/attachmentsinstead of/data/files/attachmentson Docker. Unit tests with negative cases:tests/attachmentStoragePath.test.cjs. Thanks to mueschel and xet7. -
Snap:
snap run wekan.database ferretdblooked like it re-ran the migration — it only switches databases (#6473,snap-src/bin/wekan-database). Running it while already on FerretDB printed “WeKan now uses FerretDB (SQLite).” and users reasonably read that as “migration done” while their attachments stayed missing. It now says when nothing was switched, states that the command does NOT migrate data, and names the command that does:snap run wekan.migrate. Thanks to mueschel and xet7. -
FerretDB (SQLite) rejected documents with literal dotted field names, silently dropping them during migration (#6473, wekan/FerretDB
internal/types/document_validation.go). MongoDB has accepted documents with literal.in field names since 3.6, and data migrated from a real MongoDB can legitimately contain them — but FerretDB v1’s document validation rejected every such document (“invalid key: … (key must not contain ’.’ sign)”), and since per-item migration errors are deliberately non-fatal (#6466), those documents simply went missing. Fixed in the bundled wekan/FerretDB fork: dotted keys are stored and round-tripped literally with MongoDB’s own semantics (query/update paths still treat.as a path separator), while the other key rules ($prefix, duplicates, UTF-8) still reject. Verified end-to-end against a live FerretDB (SQLite): insert, nested$set, round-trip, and the negative cases. Thanks to mueschel and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.