Back to Wekan

WeKan ® 2026-06 releases

old-CHANGELOG/2026/06.md

10.99272.0 KB
Original Source

WeKan ® 2026-06 releases

Moved out of CHANGELOG.md to keep that file small enough to open (wekan/wekan#6580). Nothing here has been changed: a release section is a record, and it reads the same as it did there.

Releases per day:

2026-06Releases
061
101
114
121
132
142
151
175
185
197
202
211
234
262

v9.73 2026-06-26 WeKan ® release

This release fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/219ee659e">Mobile UI still too large at All Boards / iPhone 12 mini, #6426: the first revert only cleaned…</a></summary>

Mobile UI still too large at All Boards / iPhone 12 mini, #6426: the first revert only cleaned boardsList.css, leaving the same forced "2x/3x bigger on mobile" rules in header.css (#header-quick-access logo/quick-access bar) and boardBody.css (#content — which the All Boards list and popups render inside) and the iPhone card-details body text. Those are now neutralized to normal size too, and the iPhone-12-mini header blocks that had grown tall and wrapped (to fit the 3x text) are back to a compact single row. The #6419 mobile touch-target icon sizes in the card-details header are intentionally kept. Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/219ee659e">Drag-to-scroll (dragscroll) not working on the Login, Register and All Boards pages: the page…</a></summary>

Drag-to-scroll (dragscroll) not working on the Login, Register and All Boards pages: the page helper tagged <body> with the dragscroll class, but on phones body.mobile-mode is position: fixed and the real scroll container becomes #content, so tagging only <body> could never scroll on those pages (Login/Register have no #content and correctly use <body>). enablePageDragscroll/disablePageDragscroll now tag whichever element actually scrolls — both <body> and #content when present — so only the overflowing one scrolls (no double-scroll). Touch one-finger scrolling (dragscrollTouch.js) already picks the nearest scrollable .dragscroll ancestor, so #content wins over <body> when both are tagged. Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88f353dc1">Drag-to-scroll now works on every whole-page layout in both mobile and desktop modes, not just All…</a></summary>

Drag-to-scroll now works on every whole-page layout in both mobile and desktop modes, not just All Boards / My Cards / Login / Register. It was wired into only those few templates, so Due Cards, Global Search, Public, Bookmarks, Broken Cards, Settings, People, Admin Reports, Attachments, Translation, Import, board Rules and the Not Found page had no page drag-scroll at all. The per-template enable/disable calls (which also fought each other when navigating between two non-board pages) were replaced by a single route-aware autorun in defaultLayout (the persistent shell that owns #content): every non-board route tags the real scroll container (<body> + #content), and the board-canvas routes (board / board-short / card) leave it off so their own .board-canvas.dragscroll keeps handling the drag. Login / Register keep their own enable/disable because they use userFormsLayout (no #content). Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4a7df3745">Login / Register pages not scrollable on phones — the sign-up / sign-in link, legal notice and…</a></summary>

Login / Register pages not scrollable on phones — the sign-up / sign-in link, legal notice and language selector below the form were unreachable (and hidden behind the on-screen keyboard). On mobile, body.mobile-mode pins <body> to position: fixed + height: 100vh (an iOS board-view anti-bounce fix), but the auth pages have no inner #content scroller and their .auth-layout/.auth-dialog were sized to the viewport height, so the page was trapped at 100vh. userFormsLayout now tags <body> with .userform-layout while mounted, and CSS lets those pages scroll as a normal document on mobile (position: static; height: auto; min-height: 100vh) with the layout/dialog growing to their content. Scoped to .userform-layout so the board-view mobile lock is unchanged. Fixed in this commit

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.72 2026-06-26 WeKan ® release

This release fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/487cf3f9632d747665bd14e4a0e43159760a49c6">Fixed api.py addcustomfieldtoboard crashing on an empty settings argument</a>.</summary>

Fixed api.py addcustomfieldtoboard crashing on an empty settings argument. The CLI did settings = str(json.loads(sys.argv[6])), so json.loads('') raised JSONDecodeError and the command aborted before sending the request (a bug api.py's own header comment flagged). Empty settings now default to {} and are sent as valid JSON via json.dumps instead of str(dict) (which produced invalid JSON with single quotes). Verified that both empty and object settings (e.g. {"currencyCode":"EUR"}) create the custom field. Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7a2a3ace5611cbffa94c2a8d1cedae29b7cf347">Mobile UI too large at All Boards and top bars, #6426: reverted the recent forced mobile 2x/3x UI…</a></summary>

Mobile UI too large at All Boards and top bars, #6426: reverted the recent forced mobile 2x/3x UI scaling rules that made All Boards and quick-access/header controls render oversized, while keeping the small-screen All Boards layout collapse so the left menu still stacks above the board grid on narrow screens. Fixed in this commit

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.71 2026-06-23 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/c1da7a4a332cd10bbd2ed3c93122fe0ea8864336">Sort, search and paginate the All Boards page, #5799: the All Boards page only offered the per-user…</a></summary>

Sort, search and paginate the All Boards page, #5799: the All Boards page only offered the per-user manual drag order with no obvious way to sort by name. Added a Sort button (Custom drag order / Title A→Z / Title Z→A, stored per user), a board-name search box that spans every category (Starred, Templates, Remaining and all (sub)workspaces), and pagination of the board icons — in the sorted modes the current page is computed on the server (getAllBoardsPage) so only that page is rendered and the page scales to thousands of boards. The search box and pagination controls are placed like Admin Panel > People. Custom (manual drag order) stays unpaginated so drag-reordering keeps working. All server-side filtering/sorting resolves against the effective current user, so it also works when a GlobalAdmin impersonates a user; added Playwright impersonation tests (tests/playwright/specs/38-impersonation.e2e.js). Note: "Stop impersonating" is still a follow-up — impersonation is connection-scoped and currently reverts on a full page reload. Fixed in this commit and this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5729">Fixed (same) width for all lists</a>.</summary>

Fixed (same) width for all lists, #5729 : the Set width popup now has a "Same width for all lists" toggle. When enabled, every list on the board renders at one shared width for the current viewer, and dragging the resize handle of ANY list updates that single value so ALL lists change together. The setting is per-viewer/per-board and works for both logged-in users (stored in profile.fixedListWidthBoards / profile.fixedListWidths) and anonymous public-board users (stored in localStorage keys wekan-fixed-list-width-enabled / wekan-fixed-list-width). Enabling fixed width turns off auto-width (the two modes are mutually exclusive); widths below 270px are rejected

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/issues/5752">Due date does not work when the language uses non-Latin (e.g. Persian/Farsi) digits</a>.</summary>

Due date does not work when the language uses non-Latin (e.g. Persian/Farsi) digits, #5752 : in locales such as Persian/Farsi or Arabic, dates and times can be rendered with non-Latin digits (Persian/Extended Arabic-Indic ۰۱۲۳۴۵۶۷۸۹, U+06F0–U+06F9, or Arabic-Indic ٠١٢٣٤٥٦٧٨٩, U+0660–U+0669). The native JavaScript Date constructor only understands ASCII digits, so any such string fails to parse and produces an Invalid Date when setting/updating or comparing dates. Added a normalizeDigits() helper in imports/lib/dateUtils.js that converts those non-Latin digits to ASCII, applied it at every date-string parse boundary in dateUtils.js (via an internal toDate() wrapper) and in the date/time picker submit/change handlers in client/lib/datepicker.js. The normal ASCII path is unchanged

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5715">Impossible to create a Link to a whole BOARD if that board already has cards</a>.</summary>

Impossible to create a Link to a whole BOARD if that board already has cards, #5715 : in the link-card popup, choosing a board used to auto-populate the List/Card sub-selects, with no way to clear them, so once the chosen board had cards you could only link to a card and no longer to the whole board. The Swimlane/List/Card selects now default to a blank "(none)" option and are no longer auto-selected when a board is chosen, and the popup's confirm ("link") button now falls back to creating a board-level link when a board is selected but the Card field is left blank (matching the previously-only-empty-board behaviour). Added tests/linkCardPopup.test.js covering the positive and negative cases

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5706">Internal Server Error (500) when attempting to reset a password</a>.</summary>

Internal Server Error (500) when attempting to reset a password, #5706 : on the Forgot Password page, clicking "Email reset link" could return a raw HTTP 500 instead of sending the reset email / showing success. This happens when the server's SMTP is not configured (no MAIL_URL / MAIL_FROM, or a bad mail server): Meteor's Email.sendAsync throws, and the exception propagated unhandled out of the forgotPassword method as an opaque 500. The reset-password / verify-email / enroll-account email-template builders are now hardened so they never throw (guarded user name/language lookups and a safe fallback if translation fails), and Accounts.sendResetPasswordEmail is wrapped so a send failure surfaces as a clean Meteor.Error('email-fail', ...) instead of a 500. Note: this makes the failure graceful, but to actually receive reset emails you must still configure SMTP (Admin Panel mail server, or MAIL_URL and MAIL_FROM). Logic extracted to server/lib/resetPasswordEmail.js and unit-tested in tests/unit/resetPasswordEmail.test.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5756">Changing the UI language had no effect for some languages (e.g. Chinese zh-CN, zh-Hans/zh-Hant…</a></summary>

Changing the UI language had no effect for some languages (e.g. Chinese zh-CN, zh-Hans/zh-Hant, Arabic ar-*) while others (de, fr) worked, #5756 : the custom tap:i18n reimplementation registered each language's resource bundle under the raw Wekan tag, but i18next (with cleanCode: true) resolves and looks translations up under a normalised code. For region/script-tagged and legacy underscore tags (af_ZA, en_AU, …) the stored code and the lookup code disagreed, so the bundle was never found and the UI silently fell back to English. The loader now normalises every i18next call (supportedLngs, addResourceBundle, changeLanguage, t) through the same code — underscores converted to hyphens and run through i18next's formatLanguageCode — so storage and lookup always agree for ALL supported languages. Dynamic JSON imports that resolve to an ES-module namespace are now unwrapped safely (without mistaking the data's own "default" translation key for the module's default export), and the language-switch handlers in the user header and login form now surface a failed load instead of leaving the UI silently in English. Added positive and negative regression tests in imports/i18n/i18n.test.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/afbabccd2685af77ffddc1038f62823882615b74">Card labels took two lines / double height on minicards, wasting vertical space, #6424: each…</a></summary>

Card labels took two lines / double height on minicards, wasting vertical space, #6424: each label's name is rendered inside a .viewer, whose global min-height: 2.5vh (intended for the full content editor) forced every minicard label to roughly double height. The minicard text labels now reset that min-height, render the viewer/paragraph inline and stay on a single compact line. Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/69c899f1f99b0ea984d3b36f5d2a2aac67c4ff9a">REST API: moving a card to another list (PUT .../cards/:cardId with listId) returned HTTP 500…</a></summary>

REST API: moving a card to another list (PUT .../cards/:cardId with listId) returned HTTP 500 — "fieldNames.includes is not a function", #6423: the list-move path called the cardMove() activity helper with { fieldName: 'listId' } (a plain object) as its fieldNames argument, but cardMove() does fieldNames.includes('boardId'), so the object's missing .includes threw and the endpoint returned HTTP 500. The changed field names are now passed as an array (['listId']), matching the cross-board move path. Fixed in this commit

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/22f996903b09d35e3aa959a9aa65b30204e12202">Notification emails were sent in English despite the user's language setting, #5875: on the server…</a></summary>

Notification emails were sent in English despite the user's language setting, #5875: on the server only the default (English) translation bundle is loaded at startup, so translating a notification to the user's language fell back to English because i18next never had that language's bundle. Added TAPi18n.ensureLanguageLoaded() (loads the bundle on demand) and await it before translating in the email notification, EmailLocalization.sendEmail and outgoing webhook paths. Fixed in this commit

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.70 2026-06-23 WeKan ® release

This release fixes the following bugs:

<details> <summary>Copying a card to or from a board with no labels threw.</summary>

Copying a card to or from a board with no labels threw. Card.copy()'s cross-board label-remap did oldBoard.labels.filter(...) / filterCopiedLabelIds(newBoard.labels, …) without guarding a missing labels array. Most boards always have default labels, but a board created via the REST API (POST /api/boards) has no labels array — so copying a card to/from such a board (or copying the board itself, which copies its cards) threw Cannot read properties of undefined (reading 'filter') and returned HTTP 500. Both label lookups now fall back to []. Cross-board copy regressions in tests/playwright/specs/17-rest-api.e2e.js.

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5592">Copying a board did not copy its webhooks</a>.</summary>

Copying a board did not copy its webhooks, #5592 : board.copy() duplicated swimlanes/lists/cards, custom fields and rules/triggers/actions, but had no loop for Integrations (outgoing webhooks), so a copied board lost all of them (even though they are per-board children — boardRemover deletes them by boardId). copy() now also copies the board's Integrations, remapping boardId (URL/token/activities carry over — the copying user is a board admin with access to them). Regression in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/1894">Disabled user accounts could be added to boards</a>.</summary>

Disabled user accounts could be added to boards, #1894 : none of the add-member paths checked the target account's loginDisabled flag, so a deactivated user could be invited/added and assigned to cards. inviteUserToBoard now throws error-user-disabled, and the REST POST /api/boards/:boardId/members/:userId/add endpoint returns HTTP 400, when the target account is disabled (re-enabling the account allows the add again). Regression in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5122">Admin Panel boards report listed removed members as current members</a>.</summary>

Admin Panel boards report listed removed members as current members, #5122 : removing a member from a board marks the member entry isActive: false (it is kept in board.members for role history / re-activation), but the Admin Panel → Reports → Boards member column listed all member entries, so removed users still appeared as members. The report now filters to active members (isActive !== false). (The raw GET /api/boards/:boardId response intentionally still returns the full members array with each entry's isActive flag, so API consumers can filter as they need.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/4847">Could not remove a deleted user from a card's members</a>.</summary>

Could not remove a deleted user from a card's members, #4847 (card side): when a user account is deleted, its entry stays in a card's members, rendering as a blank avatar. Clicking it opened the member popup, whose template dereferenced the now-missing user document (user.profile.fullname / user.username) and failed to render — so there was no way to remove the orphaned member. The popup now detects a missing user, shows a "Deleted user" entry with the raw id, and keeps the Remove from Card control (the remove handler keys off the member's userId, not the user document), so orphaned card members can be removed. (No automated regression — a self-evident Blaze template guard; the underlying unassignMember removal already worked. The board-members list still hides such entries because activeMembers() also intentionally filters members whose user doc is merely not-yet-loaded, so surfacing board-side orphans cleanly is a separate follow-up.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/4965">A newly added board member was missing from the card members popup</a>.</summary>

A newly added board member was missing from the card members popup, #4965 : the card "add members" popup snapshotted the board's member list once when it opened, so a member added to the board afterwards (or whose user document finished loading just after the popup opened) did not appear until the popup was reopened. The popup now derives its candidate list reactively (it stores only the filter term and re-reads board.activeMembers() on each render), so newly-added members show up without reopening. No change to activeMembers() itself (its deleted-user filtering is unchanged)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5809">Editing a linked card you cannot write to failed silently</a>.</summary>

Editing a linked card you cannot write to failed silently, #5809 : a linked card whose target lives on a board the user cannot write to (e.g. a private board) rejected title/description edits at the server allow rule, but the card-detail edit handlers had no error handling, so the edit just vanished with no feedback. The title and description submit handlers now catch the failure and show the error (mirroring the existing label-color handler), so the user sees why the edit did not save. (No automated regression — surfacing a permission denial across a private linked board is a UX/error path that is not cleanly reproducible in the test harness.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5688">Performance: copying a card with many checklist items took minutes</a>.</summary>

Performance: copying a card with many checklist items took minutes ( #5688 ). Checklist.copy() duplicated each checklist item with the hooked insertAsync, so collection-hooks fired each item's after.insert — every one doing a getCard and inserting an addChecklistItem activity. Copying a card with ~100 items meant ~100+ activity inserts (plus per-item DB round-trips), taking minutes and spiking CPU. The copy now uses .direct for the checklist and its items (skipping the per-item activity hooks — pure churn for a wholesale copy) and sets boardId itself, the same approach as the cardRemover delete path. Copy regression (checklist + all items duplicated) in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5347">Copying a card to another board orphaned its subtasks on the old board</a>.</summary>

Copying a card to another board orphaned its subtasks on the old board ( #5347 data symptom): Card.copy() re-pointed each subtask's parentId to the copied card but left its boardId/swimlaneId/listId pointing at the source board — so a cross-board copy created subtasks stranded on the original board whose parent lives on another board (and it mutated the cached source docs in place). Copied subtasks are now re-homed onto the destination board/swimlane/list alongside the copied parent, via a fresh object (no cache mutation). Cross-board copy regression in tests/playwright/specs/17-rest-api.e2e.js. (This fixes the orphaned-cross-board-subtask data symptom noted in #5347; the separate "Maximum call stack" error there is not yet root-caused — see the issue.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5325">Error when clicking the notification icon</a>.</summary>

Error when clicking the notification icon, #5325 : a notification whose referenced activity no longer existed (its card/board was deleted) left an entry whose activityObj was null, and the notifications drawer dereferences it (activity.user, activity._id, …) — so one orphaned notification threw and broke the whole popup. user.notifications() now drops entries whose activity can't be resolved. (No automated regression — an orphaned-notification state is not cleanly reproducible in the harness; the fix is a self-evident filter.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5390">Deleting a custom field from a board could throw</a>.</summary>

Deleting a custom field from a board could throw, #5390 : removing a (multi-board) custom field from one board runs a before.update hook that logged a setCustomField activity by reading (await getActivity({customFieldId})).value — with no null guard, so a field that never had a value set (no such activity) threw Cannot read properties of undefined and aborted the removal. The lookup is now guarded. (server/models/customFields.js)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5565">Board "show checklists on minicard" setting had no effect</a>.</summary>

Board "show checklists on minicard" setting had no effect, #5565 : the sidebar toggle writes board.allowsChecklistsOnMinicard, but the minicard render checked a different, UI-less field (board.allowsChecklistAtMinicard), so enabling the board-wide setting never showed checklists on minicards. The minicard now reads the field the toggle actually sets. (client/components/cards/minicard.js)

</details>

and these issues are verified resolved in current code (could not reproduce / no error observed here; re-test on the reporter's data requested):

<details> <summary><a href="https://github.com/wekan/wekan/issues/5388">#5388</a>.</summary>

#5388 (collapsing a list affected all users): list collapse state is now stored per user (profile.collapsedLists[boardId][listId], or a cookie for logged-out users) instead of a shared collapsed field on the list document — so one user collapsing a list no longer changes it for everyone (resolved by commit 414b8dbf4, which postdates the report; mirrors the per-user swimlane-collapse handling)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/3894">#3894</a>.</summary>

#3894 (board import failed when the JSON's members referenced a user not present in users): the importers already guard a missing user entry (skip the dangling member instead of dereferencing undefined) in client/components/import/wekanMembersMapper.js, models/wekanmapper.js and models/wekanCreator.js, with a dedicated test (tests/wekanCreator.inconsistent.test.js)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5411">#5411</a>.</summary>

#5411 (non-super-admin board admins could not see the add-member "+"): the sidebar add-member button is gated on currentUser.canInviteToBoard, which returns true for any board admin (board.hasAdmin), and the secure default invite roles include board-admin. So board admins (not just site admins) see the "+". This was resolved by the "Allow Invite to Board" roles feature (commit c956ab5a4), which postdates the report; a site admin can additionally let other board roles invite via Admin Panel → People → Roles

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5627">#5627</a>.</summary>

#5627 (rules not copied when creating a board from a template): board.copy() already copies the board's rules + triggers + actions (remapping boardId and the rule's triggerId/actionId); the report predates that code. Now covered by the #5592/#5627 copy regression in tests/playwright/specs/17-rest-api.e2e.js so it cannot silently regress

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5630">#5630</a>.</summary>

#5630 (cannot save Admin Panel Layout settings): already fixed — the Layout save handler had referenced form fields that were moved to the separate Accessibility settings template, throwing before the save; the current js-save-layout handler no longer reads those fields

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5117">#5117</a>.</summary>

#5117 (a TeX formula rendered both an SVG and a <math> tag): already fixed by migrating math rendering from markdown-it-mathjax3 (which emitted both SVG and MathML) to Temml, which outputs MathML only (packages/markdown/src/template-integration.js)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.69 2026-06-23 WeKan ® release

This release fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/issues/3697">Can't edit a card's members in the UI after removing them via the REST API</a>.</summary>

Can't edit a card's members in the UI after removing them via the REST API, #3697 : clearing a card's members (or assignees) over REST left the field uneditable in the UI. The card PUT handler guarded with if (req.body.members) — falsy for the two natural clear payloads (null and "") — so "remove the last member" was a silent no-op, and its string branch was written to store null rather than []; a card with members: null then breaks UI code that treats it as an array. The handler now uses an !== undefined guard plus a shared coercion helper so any clear payload (null / "" / []) stores a clean String[] (never null), a single id is wrapped into an array, and stray non-string entries are dropped; getMembers()/getAssignees() also coerce a legacy null to [] on read so existing corrupted documents edit cleanly. Pure logic in models/lib/restArrayParam.js with a Meteor-free Node unit test (tests/restArrayParam.test.cjs, npm run test:unit:node, incl. a negative test reproducing the old null-writing logic), plus a REST regression in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/2875">Can't create a card with no member via the REST API</a>.</summary>

Can't create a card with no member via the REST API, #2875 : the card-create endpoints (POST .../cards and POST .../cards/bulk) wrote req.body.members/assignees straight to the insert with no normalization — the create-side twin of #3697 — so a null/"" payload persisted as null (breaking later UI editing). Both handlers now run the same coerceRestArrayParam helper: the field is omitted when not provided (so the schema default [] applies), any clear payload becomes [] (never null), and a single id is wrapped into an array. REST regression in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5650">Board created through the REST API shows in the API but not in the browser UI</a>.</summary>

Board created through the REST API shows in the API but not in the browser UI, #5650 : POST /api/boards set the board's sole member's userId from req.body.owner with no fallback, so a request that omits owner created a board whose only member had userId: undefined. The board-list publication matches members.$elemMatch: { userId, isActive: true }, which can never match an undefined id — so the board was returned by the REST API but invisible in the browser. owner now falls back to the authenticated caller (req.body.owner || req.userId), mirroring the Meteor create method. REST + DB regression in tests/playwright/specs/17-rest-api.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5166">Copying a card to another board left its comments on the wrong board</a>.</summary>

Copying a card to another board left its comments on the wrong board, #5166 : CardComments.copy() cloned a comment but only changed its cardId, so a card copied to another board produced comments that kept the source board's boardId. Comment permission checks key off the comment's boardId, so edit/delete on a copied comment was validated against the wrong board, and any board-scoped query saw it on the old board. copy() now also sets the destination boardId (the author userId is intentionally preserved). Cross-board copy regression in tests/playwright/specs/17-rest-api.e2e.js. Note: the related "wrong author shown" symptom is a separate display issue — when a copied comment's author is not a member of the destination board, their user document isn't published there, so the UI can't resolve the name; adding those users to the board resolves it. A broader fix (publishing comment authors' minimal profile on boards where their comments appear) is left as a follow-up

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/3252">Exception "Removed nonexistent document" when deleting a card detail</a>.</summary>

Exception "Removed nonexistent document" when deleting a card detail, #3252 (partial): deleting a comment or checklist/checklist-item could throw Removed nonexistent document on the client. The delete handlers called Collection.remove(_id) directly, but under heavy archive/delete churn the target doc can already have been evicted from the client's Minimongo cache, and removing a missing _id throws. The comment / checklist / checklist-item delete handlers now check the doc still exists in the local cache before removing it (client/components/activities/comments.js, client/components/cards/checklists.js). The server-side before.remove hooks were already hardened to guard + log instead of throwing. (No automated regression for the thrown exception — the eviction race is not deterministically reproducible; the guard itself is a self-evident findOne-then-remove.) The high CPU on bulk delete the issue also reports is reduced by the cascade change below; the remaining cost is on the archive path (a bulk update, not a delete) and is a separate follow-up

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5322">Performance: deleting a card no longer fans out per-child activity churn</a>.</summary>

Performance: deleting a card no longer fans out per-child activity churn (#3252, #5322 ). cardRemover removed a card's checklist items, checklists and comments with the hooked removeAsync, so collection-hooks fired each child's before.remove once per document — every one doing a getCard and inserting an activity (e.g. a removedChecklistItem per item), which then triggered the notification + publication observers. Deleting (or bulk-deleting) a card with many children produced an activity-insert storm and pegged the CPU. cardRemover now removes those children with .direct (skipping the per-child hooks — they only logged activities that are unviewable once the card is gone) and clears all of the card's activities in a single bulk op, which also removes the activities that a delete previously left orphaned. The deleteCard activity is still logged afterward (so the outgoing webhook fires), and attachments still go through the normal remove so their files are deleted. Cascade + activity-cleanup regression (also a negative test) in tests/playwright/specs/17-rest-api.e2e.js

</details>

and this issue is verified resolved in current code (could not reproduce / no error observed here; re-test on the reporter's data requested):

<details> <summary><a href="https://github.com/wekan/wekan/issues/2292">#2292</a>.</summary>

#2292 (archiving a swimlane appeared to delete all its cards): Swimlane.archive() (models/swimlanes.js) only sets archived: true on the swimlane — it does not touch or delete the cards, and restore() brings the swimlane and its cards back. While a swimlane is archived its cards are merely hidden from the board view (board queries filter to archived: false swimlanes), not lost — the v2.27-era cascade described in the report no longer happens. (A dedicated way to view an archived swimlane's cards before restoring would be a separate UX improvement.)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.68 2026-06-23 WeKan ® release

This release adds the following updates and developer tooling:

<details> <summary><a href="https://github.com/git-pkgs/forge">build.sh / build.bat: multi-forge mirroring</a>.</summary>

build.sh / build.bat: multi-forge mirroring. Two new menu options: Install forge CLI tools (installs the gh-like CLIs gh, glab, tea, git-bug, and the unified forge via the detected package manager / go install), and Mirror repo GitHub → GitLab/Codeberg/Forgejo/Gitea. The mirror flow selects source + target by number (e.g. 1 3 = GitHub → Codeberg), pushes all branches/tags with git push --mirror, then runs a cross-platform Node engine (tools/forge-mirror.js) that syncs only the issues and pull requests missing at the target (driving the authenticated CLIs; de-duplicated by title; dry-run by default) and converts the GitHub Actions workflow syntax for the target: an annotated .forgejo/workflows/ copy that flags the known Forgejo/Gitea incompatibilities (hashFiles(), permissions:, continue-on-error:, complex runs-on:), or for GitLab a .gitlab-ci.yml scaffold plus a guide pointing at GitLab's official converter skill

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6414">Dependency updates</a>.</summary>

Dependency updates (merged the low-risk Dependabot PRs that pass build + unit tests): @swc/helpers 0.5.22 → 0.5.23 (#6416) and the test-only sinon 21 → 22 (#6418). The major production bumps were held back for individual testing rather than merged: jquery 3 → 4 (#6417, risks Blaze / jQuery-UI compatibility), @babel/runtime 7 → 8 (#6415), and @tweedegolf/sab-adapter-amazon-s3 1 → 3 ( #6414 , S3 storage is not exercised by CI)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e28b3bc21">build.sh: run the Chromium / Firefox / WebKit Playwright matrix with or without Docker</a>.</summary>

build.sh: run the Chromium / Firefox / WebKit Playwright matrix with or without Docker. The WebKit-only Docker support is generalized so any browser can run natively or inside the official Playwright Docker image, selectable via WEKAN_PLAYWRIGHT_DOCKER=1/0 (whole matrix) or per-browser WEKAN_CHROMIUM_DOCKER / WEKAN_FIREFOX_DOCKER / WEKAN_WEBKIT_DOCKER. Defaults are unchanged (Chromium/Firefox native, WebKit via Docker on Linux arm64). Adds an "Install Playwright browsers" menu item that does playwright install --with-deps and/or pulls the Playwright Docker image. commit e28b3bc21

</details> <details> <summary>E2E reliability.</summary>

E2E reliability: the Playwright suite was red on main because the rspack client JS bundle was not being served by meteor run (dev mode) in headless CI — browser requests for the bundle returned the SPA HTML fallback (Unexpected token '<'), so Meteor never initialised and all ~212 specs timed out in waitForMeteor (confirmed: a CI readiness probe got an empty response from the bundle URL for 10 minutes straight). The CI E2E job now runs against a production build (meteor buildnode bundle/main.js), where the client JS is baked into the bundle and served statically by the Meteor server — no rspack dev server, no chunk-serving race. The production run also needed two env vars the dev runner sets but CI did not: WRITABLE_PATH (without it server/00checkStartup.js exits before listening) and WITH_API=true (without it every REST-API spec gets HTML instead of JSON). The suite is also sharded 2× per browser (6 parallel jobs) to cut wall-clock time, and the per-spec waitForMeteor timeout was raised 30s → 60s. Confirmed: the suite went from 0 tests running (all timed out) to ~119/120 passing per shard. The handful of residual specs were then triaged locally: two were real product bugs (below — the sort-cards button and Template Container deletion); the RTL/LTR i18n-text spec was CI-only timing on the production bundle, made robust by waiting for the async result instead of reading it immediately. Two specs are quarantined (test.fixme) for focused follow-up: a pre-existing "control" assertion (a plain click outside the card closing it — the actual #5686 guard it backs still passes), and the #5798 template-card end-to-end flow, which is unstable only on the CI production bundle (it passes locally) — the #5798 product fix itself is committed and verified (see below); the flaky part is the multi-step template-search-and-instantiate UI under polling reactivity.

</details> <details> <summary>Sort-cards button stayed clickable after sorting.</summary>

Sort-cards button stayed clickable after sorting. In the board header the js-sort-cards class (which carries the handler that opens the sort popup) was replaced by emphasis once a sort was active, so after sorting once the button was dead — the popup could not be reopened to change or clear the sort. It now keeps js-sort-cards and only adds emphasis when active.

</details> <details> <summary>Deleting a Template Container board threw and aborted.</summary>

Deleting a Template Container board threw and aborted. boardRemover cleared the user's template profile pointers with $unset, but those four profile.* fields were non-optional String in the schema, so SimpleSchema rejected the update ("Templates board ID is required") and the whole board removal failed. The fields are now optional, so the container deletes and its pointers clear cleanly (#2339/#5850).

</details> <details> <summary>Regression tests.</summary>

Regression tests for several of the fixes below, each negative-tested (verified to fail on the pre-fix code): the attachment filename truncation (#6412) has a Meteor-free Node unit test (tests/filenameSanitizer.test.cjs, npm run test:unit:node) plus the meteor test mocha test, and Playwright specs (tests/playwright/specs/36-fixed-bug-regressions.e2e.js) cover #3907, #5886, #5892, #3897 and #5798 (the #5798 spec passes locally but is quarantined on CI — see the E2E reliability note above).

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/issues/4255">Cannot delete some boards from Archive</a>.</summary>

Cannot delete some boards from Archive, #4255 : the Archive listed boards the user could not actually delete — clicking Delete did nothing and the console showed remove failed: Access denied. The archivedBoards publication scoped its list to admin members (isAdmin: true) but ignored the active flag, whereas the Boards.remove permission (hasAdmin()) requires an active admin (isActive: true && isAdmin: true). A user who was an admin on a board but no longer active still saw it in the Archive, then hit "Access denied" on delete. The publication now matches the remove permission exactly (isActive: true && isAdmin: true), so the Archive only lists boards the user can really delete

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5874">Weird moving card bug — cross-board move could silently lose a card</a>.</summary>

Weird moving card bug — cross-board move could silently lose a card, #5874 (data loss): rarely, moving a card from board A to board B left it in a corrupt half-moved state — the card's boardId became B but its listId/swimlaneId still belonged to board A. The reporter saw exactly this: the card existed in board B's JSON but with board A's list/swimlane, so it was invisible in every normal view on both boards and clicking its link reopened board A. Root cause: the Move/Copy-card dialog resolves the destination board's swimlanes and lists from the client Minimongo cache, which can still be empty at the moment the user clicks "Done" — the destination board's publishComposite('board') data has not finished merging — so the dialog keeps the source board's swimlaneId/listId while the boardId is already the destination. (setFirstSwimlaneId()/setFirstListId() swallow the lookup miss in a silent try/catch, leaving the stale ids in place.) Several other callers — drag reorder, multi-card move in the filter sidebar, board-action rules, the REST API — can in principle produce the same mismatch. Fix: a new server-side Cards.before.update guard (enforceCardBoardConsistency, registered first so the corrected modifier is what every later hook and the persisted write see) runs whenever a card's boardId changes and rewrites the pending update so the swimlane/list always belong to the destination board, falling back to that board's default swimlane and first list when they don't. It runs on the server, where the destination board's swimlanes/lists are always present regardless of client cache state, and is corrective only — a cross-board move whose targets already belong to the destination board is left untouched, and a same-board reorder is ignored. The decision logic was extracted into a pure, dependency-injected module (models/lib/cardBoardConsistency.js) with a Meteor-free Node unit test (tests/cardBoardConsistency.test.cjs, npm run test:unit:node), including a negative test asserting that the raw unguarded modifier is exactly the #5874 corrupt state

</details> <details> <summary><a href="https://github.com/meteor/meteor/issues/12421">Responsive views still seem broken, #6419 (partial): the most concrete, "stops-work" part is fixed…</a></summary>

Responsive views still seem broken, #6419 (partial): the most concrete, "stops-work" part is fixed — on mobile an open card could not be closed because the card-details overlay (z-index 100) sat below the app header bar (#header-main-bar, z-index 1000), so the header covered the close (X) button. The full-screen mobile card now uses z-index 1100 — above the header, still below popups (2000+) so card menus/date pickers open over it. Mobile header action icons were also enlarged and verified on a real iPhone profile via Playwright screenshots (tests/playwright/mobile-shot.js): the top bar and the board action bar buttons were only ~28–32px tall with a 13–15px font (below the comfortable ~44px touch-target / 16px readable minimum). They are now ~44px tall with a 16px font, applied by viewport width (@media (max-width: 800px)) so they work on any phone without toggling Desktop/Mobile mode — the board header renders as a clean row of large, icon-only buttons. Two deeper root causes were then found and fixed (verified on an iPhone profile via Playwright screenshots): (1) No viewport meta tag. WeKan's server-rendered <head> (server/lib/customHeadRender.js) had no <meta name="viewport">, so mobile browsers laid the page out at their default ~980px virtual width and scaled it down — making the whole UI tiny, reporting window.innerWidth === 980 on a 390px phone, and preventing every @media (max-width: 800px) rule (and the width-based mobile detection) from ever matching. Added width=device-width, initial-scale=1, viewport-fit=cover (user zoom left enabled for accessibility). (2) profile.mobileMode defaulted to false. The user schema set mobileMode: false on every user, so Utils.getMobileMode() always returned the profile value and the auto-detection below it was dead code — every user was locked to desktop-mode even on a phone. The field is now optional (no default), so it stays unset until the user explicitly toggles; auto-detection was also rewritten to use reliable matchMedia width/pointer queries instead of fragile user-agent sniffing (cf. **Meteor #12421 **, where Mobile Safari UA version parsing was wrong — this instance reports isModern: true, so #12421 is not the cause here, but it is the same class of UA-detection fragility this rewrite avoids), and a matchMedia listener now re-applies mobile-mode on resize/orientation when the user has no explicit preference. Net: phones auto-detect mobile mode at the correct device width, with large tappable icons, no manual toggle. Also fixed the Admin Panel's secondary top bar in small-width mode: those tab buttons (Settings / People / Reports / Attachments / Translation / Info) are rendered inside #header-quick-access, whose mobile rules scale all text/icons 2× — so the admin tabs rendered at 28px text / 56px icons, huge and overlapping the "Version" label. A scoped override caps them at a normal touch size (15px text / 18px icons). And fixed overlapping text in the Admin Panel settings body on phones: the layout forced a side-by-side menu+content row "even on narrow windows", so the ~127px side menu and the content were crammed together and the long (e.g. Finnish) section labels overflowed the menu box rightward, visually overlapping the content. On small screens the layout now stacks — a full-width compact section menu on top, full-width settings content below — verified via iPhone-profile screenshots

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/6420">Missing voting buttons</a>.</summary>

Missing voting buttons, #6420 : the showVotingButtons (and showPlanningPokerButtons) helpers in cardDetails.js referenced an undefined currentUser variable, so every card render threw ReferenceError: currentUser is not defined and the vote / planning-poker buttons silently disappeared. The helpers now resolve currentUser via ReactiveCache.getCurrentUser() and null-guard the board-member check. Regression test in tests/playwright/specs/36-fixed-bug-regressions.e2e.js

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5e26cf004">ENAMETOOLONG: very long attachment filenames could not be migrated to filesystem storage, #6412…</a></summary>

ENAMETOOLONG: very long attachment filenames could not be migrated to filesystem storage, #6412: attachment filenames were sanitized for path traversal but never length-limited, so a very long name (worse with multibyte UTF-8 like German umlauts) produced an on-disk <id>-<version>-<name> component exceeding the filesystem's 255-byte limit and failed with ENAMETOOLONG. sanitizeFilename now truncates to 200 UTF-8 bytes (measured in bytes, never splitting a codepoint) while preserving the file extension. Done: commit 5e26cf004

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6382ad6a8">Card "added label" history was deleted whenever the card was moved, #3907: updateActivities removed…</a></summary>

Card "added label" history was deleted whenever the card was moved, #3907: updateActivities removed all addedLabel activities whenever boardId appeared in a card update, but Card.move() always re-sets boardId (even moving within the same board), so every move wiped the card's label history (data loss in the activities collection). The removal/remap now only runs on an actual board change (comparing the pre-update boardId with the new value). Done: commit 6382ad6a8

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc0fe0b61">Public boards did not fully load longer lists when viewed as a guest, #3897: a guest (no logged-in…</a></summary>

Public boards did not fully load longer lists when viewed as a guest, #3897: a guest (no logged-in user) hit getCurrentUser().isBoardAdmin() in template helpers, throwing Cannot read property 'isBoardAdmin' of null and aborting the Tracker render so lists stopped loading partway. The helpers now use optional chaining (getCurrentUser()?.isBoardAdmin() / ?.isWorker()), which is falsy for guests instead of throwing. Done: commit fc0fe0b61

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1165c9b6d">Changed order of lists is not persisted, #5997: the server side was verified working end-to-end (a…</a></summary>

Changed order of lists is not persisted, #5997: the server side was verified working end-to-end (a DDP call to updateListSort reorders the lists collection and the sort:1 query returns the new order), so the regression was client-side. saveSorting read the neighbouring lists with .prev('.js-list') / .next('.js-list'), which jQuery only matches when the sibling is immediately adjacent; the lists container also renders the add-list composer and a +cardDetails element (when a card is open) between lists, so an interspersed non-list sibling made calculateIndex mis-detect the first/last position and compute a wrong sort. Now uses prevAll/nextAll('.js-list').first(). Done: commit 1165c9b6d

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/097806984">Cards made from a template link to the template itself, #5798: a card instantiated from a template…</a></summary>

Cards made from a template link to the template itself, #5798: a card instantiated from a template was copied with the templates board id (the template-search source), so it had boardId = templates board. It still showed in the target list (the list renders cards by listId and the templates board is subscribed), but clicking it navigated to the templates board instead of opening the card. It is now copied into the current board. Done: commit 097806984

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/612ed3e51">Creating a card from a template threw "There is no current view" and created no card</a>.</summary>

Creating a card from a template threw "There is no current view" and created no card. Found while adding the #5798 regression test: the searchElement popup's minicard-click handler called tpl.getSortIndex() (which reads Template.currentData()) after await tpl.board.getNextCardNumber(), and Blaze's synchronous current-view context is lost across an await. The sort index is now computed before the await. Done: commit 612ed3e51

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5e9b2bccd">Lists do not collapse correctly with the Modern theme, #5892: the list-width rework (#6409) added a…</a></summary>

Lists do not collapse correctly with the Modern theme, #5892: the list-width rework (#6409) added a persistent .list[style*="--list-width"] { width: … !important } rule that overrode the 30px collapsed width, so a list with a custom width stayed full width when collapsed. Collapsed lists are now excluded from that rule and the 30px width is !important. Done: commit 5e9b2bccd

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6aad946bc">Sort by due date is not remembered as the default view, #5886: the card sort was kept only in an…</a></summary>

Sort by due date is not remembered as the default view, #5886: the card sort was kept only in an in-memory Meteor Session variable, which resets on page reload, so the chosen sort reverted to the default. The sort is now persisted to localStorage and restored on load (sorting and the sort icon). Done: commit 6aad946bc

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f999b9e74">Change card's parent shows no cards the first time you select a board, #3745…</a></summary>

Change card's parent shows no cards the first time you select a board, #3745: Template.cardMorePopup's cards() helper queried ReactiveCache.getCards({ boardId }) from client minimongo the instant a board was picked, before that board's card subscription had loaded, so the parent-card list was empty the first time (and only worked on reopen, once the data had arrived). It now subscribes with an onReady callback and a parentBoardReady reactive flag, and the list only renders once the subscription is ready — the same subscription-readiness pattern as the #5798 fix. Done: commit f999b9e74

</details>

and these issues are verified resolved in current code (could not reproduce / no error observed here; re-test on the reporter's data requested):

<details> <summary><a href="https://github.com/wekan/wekan/issues/3826">#3826</a>.</summary>

#3826 (cannot reorder cards in a list whose cards have parents): built a drag-sort reproduction harness (tests/playwright/helpers/dragSort.js) that drives jQuery-UI sortable with a realistic stepped mouse gesture (Playwright's dragTo() does not trigger it), plus a regression spec (tests/playwright/specs/37-card-drag-sort.e2e.js). Dragging a sub-task card (one with a parentId) to a new position in its list persists the new order both with a few cards and at 15 cards — it does not revert

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/1289">#1289</a>.</summary>

#1289 (card with a deleted member user): verified via the Playwright harness — a card whose members/assignees reference a non-existent user renders its board minicard and opens the card detail with zero console errors, so userAvatar null-guards missing users correctly

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/1389">#1389</a>.</summary>

#1389 (edge-to-edge URL makes the description uneditable): the card detail has an explicit edit (pencil) control rather than relying on clicking the rendered text, so a full-width link no longer blocks editing

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5808">#5808</a>.</summary>

#5808 (bidirectional cross-board card link makes both cards hang on open): a Playwright repro that mutually links two cards across two boards opens the card detail in ~1s with no errors and no redirect loop — the link only navigates on an explicit click, not on open, so there is no auto-bounce

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5757">#5757</a>.</summary>

#5757 (card activities jump to a recent date after changing the due date): activity createdAt is set once in the Activities.before.insert hook, nothing bulk-updates it, and the UI renders activity.createdAt directly — so changing a due date inserts one new a-dueAt activity and cannot re-date existing ones

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.67 2026-06-21 WeKan ® release

This release fixes the following bugs:

  • Updated to Meteor 3.5-rc.2.
<details> <summary><a href="https://github.com/wekan/wekan/issues/6409">Reworked confusing and unreliable list widths</a>.</summary>

Reworked confusing and unreliable list widths, #6409 : a list now has one width instead of the old "min width / max width / automatic" trio, and it reliably persists across reloads (the render now drives the --list-width CSS variable the styles actually use, so a width no longer reverts to auto after reload). A new board setting Personal list widths chooses the scope:

</details>
  • Off (default) — Shared: the width lives on the list (lists.width), is the same for everyone on the board, and only members with write access can change it (read-only/comment-only members no longer see the resize handle). Shared widths are included in board export/import (the importer previously dropped lists.width; it now preserves width, color and collapsed state, and the board's list-width scope).
  • On — Personal: each user keeps their own widths (profile, or localStorage when not logged in), falling back to the shared width then the default. The per-list popup is simplified to a single width value plus an Auto list width toggle (fit lists to content); the advanced per-list min/max pixel options were removed. Auto-width follows the same Shared/Personal scope (per-board for everyone, or per-user) and is carried through export/import. Documented in docs/Features/Lists/Lists.md.
<details> <summary>build.sh / build.bat menu option 2 ("Build WeKan") now also clears the rspack dev-build caches…</summary>

build.sh / build.bat menu option 2 ("Build WeKan") now also clears the rspack dev-build caches (_build and node_modules/.cache) in addition to node_modules / .meteor/local / .build, so the next meteor run recompiles from scratch instead of occasionally serving stale modules after a git checkout/merge.

</details> <details> <summary>build.sh / build.bat now give the Meteor build tool and Node a larger heap by default…</summary>

build.sh / build.bat now give the Meteor build tool and Node a larger heap by default (TOOL_NODE_FLAGS and NODE_OPTIONS = --max-old-space-size=8192) for every dev-run, test and build option, so long development sessions and test runs no longer crash with "FATAL ERROR: ... JavaScript heap out of memory". Both honor an existing value, so you can lower it on machines with less RAM.

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/6411">Fixed editing the 2nd/3rd organization or team in Admin Panel › People always showing the FIRST one</a>.</summary>

Fixed editing the 2nd/3rd organization or team in Admin Panel › People always showing the FIRST one, #6411 : on /people, clicking Edit on any organization or team filled the form with the first one's values (so you could never edit the others). The edit/settings popups are opened from the row with data context { org } / { team }, but their helpers read this.orgId / this.teamId (undefined there) and called getOrg(undefined) / getTeam(undefined), which findOne({}) resolves to the first document. The popup helpers, the save handlers and the delete (settings) handlers now resolve the clicked row's id from the { org } / { team } context. Verified against a running instance (each org/team now edits its own values)

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6410">Fixed boards not rendering at all (blank board view) after the mongodb/bson 7.3.0 dependency bump</a>.</summary>

Fixed boards not rendering at all (blank board view) after the mongodb/bson 7.3.0 dependency bump. bson 7.x runs const { startupSnapshot } = globalThis?.process?.getBuiltinModule('v8') ?? {} at module-load time; the optional chaining stops before the call, so in the browser — where a partial process polyfill exists but has no getBuiltinModule — it threw TypeError: getBuiltinModule is not a function while evaluating client/components/cards/attachments.js (import { ObjectId } from 'bson'). That aborted the client bundle bootstrap part-way through client/imports.js, so every feature imported after it (notifications, swimlanes, rules, …) was never registered and the board view died with No such template: notifications. Added a tiny browser shim (client/lib/bsonBrowserShim.js, imported first in client/main.js) that gives the browser process a no-op getBuiltinModule, so bson takes its intended ?? {} fallback. New unit tests (client/lib/tests/bsonBrowserShim.tests.js); also hardened the #5686 Playwright spec to run against a rendering board. ( PR #6410 )

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6406">Fixed REST API returning HTTP 500 with a stack trace for an invalid request, #5804: posting a…</a></summary>

Fixed REST API returning HTTP 500 with a stack trace for an invalid request, #5804: posting a comment without the required comment parameter (or to a board that does not exist) returned an HTTP 500 error page. The schema-validation error thrown on insert is a circular object (SimpleSchemaValidationContextSimpleSchema → …), and serializing it crashed the response writer (Converting circular structure to JSON). Now: the comment parameter is validated and a missing/empty one returns HTTP 400; an unknown board returns HTTP 404 (the board-access checks no longer dereference board.members of a non-existent board); the JSON response writer is crash-proof (falls back to a safe { "error": … } payload instead of throwing); and REST comment errors now use their real status code instead of 200. New unit tests in server/lib/tests/apiResponseHelpers.tests.js. ( PR #6406 )

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6407">Fixed selecting text in a checklist closing the card, #5686: selecting the text of a checklist item…</a></summary>

Fixed selecting text in a checklist closing the card, #5686: selecting the text of a checklist item and releasing the mouse outside the card detail pane closed the card. The checklist items template stops mousedown propagation (for item sorting), so the existing cardDetailsIsDragging guard never engaged and the document-level "click outside to close" handler closed the card. The close handler now also keeps the card open whenever a live text selection is anchored inside the card pane (new propagation-independent guard client/lib/cardCloseGuard.js), so a deliberate click on empty board space still closes the card. New unit tests in client/lib/tests/cardCloseGuard.tests.js and a Playwright regression test in tests/playwright/specs/34-checklist-text-selection.e2e.js. ( PR #6407 )

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6408">Fixed list reordering throwing 403 Access denied for read-only members, #5462: read-only /…</a></summary>

Fixed list reordering throwing 403 Access denied for read-only members, #5462: read-only / comment-only board members could still drag-reorder lists, which fired a server write that allow/deny rejected with 403 Access denied (the list then snapped back). Of the three list jQuery-UI sortables in client/components/swimlanes/swimlanes.js, one was not gated on Utils.canModifyBoard(); it now is, consistent with the other two, plus a defense-in-depth guard so a logged-in user without write access can never persist a reorder (anonymous public-board reordering via localStorage is unaffected). The server already enforced this; the fix stops the unauthorized drag and the console error. New Playwright regression test in tests/playwright/specs/35-list-sort-permissions.e2e.js. ( PR #6408 )

</details>

Thanks to GitHub users Atry, mueller-ma and liferadioat for reporting.

v9.65 2026-06-20 WeKan ® release

This release adds the following updates:

<details> <summary>Issue triage: closed 13 already-fixed Bug issues (with evidence), relabeled ~25 mislabeled feature…</summary>

Issue triage: closed 13 already-fixed Bug issues (with evidence), relabeled ~25 mislabeled feature requests to Feature with a "Feature Request:" title prefix, and prefixed ~35 environment-specific reports "Environment specific:" and gave them the Bug:Environment-specific label.

</details>
  • Audited labels on all 533 open issues for correctness (type, Feature:Area, Targets:, Severity:, etc.).
<details> <summary>Added 23 missing GitHub labels found by auditing docs/Login and docs/Features against the issue…</summary>

Added 23 missing GitHub labels found by auditing docs/Login and docs/Features against the issue labels, matching the existing label style and colours (Feature:* = #0052cc, Targets:* = #fbca04), and applied them across open and closed issues:

</details>
  • Login methods (Feature:User-accounts:*): ADFS, Azure, B2C, Google, Header-Login, Nextcloud, Oracle, Zitadel, Autologin, Accounts-Lockout, Forgot-Password.
  • Features: Feature:LaTeX, Feature:Mermaid-Diagram, Feature:Emoji, Feature:Python, Feature:Cards:Cover, Feature:Cards:Location, Feature:Custom-Logo, Feature:RTL, Feature:Members, Feature:Multitenancy, Feature:Allow-private-boards-only.
  • Platform: Targets:Apache.

and adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/bdb8e6254">Threaded comment replies</a>.</summary>

Threaded comment replies : card comments gain an optional parentId; a "Reply" link links a new comment to its parent, rendered with an "in reply to" quote. Initial MVP (single-level visual threading)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bdb8e6254">Restrict board admins from editing/deleting other users' comments</a>.</summary>

Restrict board admins from editing/deleting other users' comments : new board setting restrictCommentEditing (default off). When on, only a comment's author may edit/delete it; enforced server-side via collection hooks

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d7ae93bb2">Visible status of sub-tasks</a>.</summary>

Visible status of sub-tasks : each subtask now shows its current list (prefixed with the board title when on a different board) read-only next to its title

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b998246c3">Drag-and-drop search results into board columns</a>.</summary>

Drag-and-drop search results into board columns : cards in the search-results list can be dragged onto board lists, reusing the existing card.move(). MVP: drops append to the end of the target list (no pixel-precise insertion index yet)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bf75efaef">Per-user permanent dismissal of the Announcement banner</a>.</summary>

Per-user permanent dismissal of the Announcement banner : a user can permanently close the current announcement so it does not reappear on reload/board-switch, until the admin edits the announcement text (which makes it reappear for everyone)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a8ed326a5">Show how many times a card's due date was changed</a>.</summary>

Show how many times a card's due date was changed : the card detail now displays a "due date changed N times" count (derived from existing a-dueAt activities) for deadline accountability

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/371258a6d">Restrict adding board members to the same Organization or Team</a>.</summary>

Restrict adding board members to the same Organization or Team : new global admin setting boardMembersFromSameOrgOrTeamOnly (default off). When on, a user can only be added to a board if they share an Organization or Team with the inviter or an active board member; enforced server-side in the invite/search paths. Site admins bypass

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0a43d8ac3">Import Google Calendar .ics files into board cards</a>.</summary>

Import Google Calendar .ics files into board cards : MVP, import-only. New dependency-free iCalendar parser (server/lib/icsImport.js) maps each VEVENT to a card with startAt/dueAt so events appear on Calendar/Gantt views, plus an importIcsToBoard Meteor method and a REST endpoint POST /api/boards/:boardId/swimlanes/:swimlaneId/lists/:listId/ics (documented in the OpenAPI spec, with an importics example in api.py). Two-way Google Calendar sync is not included (see wekan-ical-server for read-only WeKan→calendar export)

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/f569eb9c0c0a654d39b927322eb606e473ad7ef4">Fixed OIDC/OAuth2 "Log Out" redirecting to the identity provider home page instead of back to Wekan</a>. Thanks to zambalee and xet7.</summary>

Fixed OIDC/OAuth2 "Log Out" redirecting to the identity provider home page instead of back to Wekan With autologin (OIDC_REDIRECTION_ENABLED=true), clicking Log Out redirected to the OAuth2 server URL (for example the Keycloak base URL https://id.company.com), which shows an error page for non-admin users. Added the new optional OAUTH2_LOGOUT_ENDPOINT setting: when set to the provider's end_session_endpoint (Keycloak example /realms/<realm>/protocol/openid-connect/logout), Wekan now performs an OIDC RP-initiated logout that ends the identity provider session and returns the user to Wekan (ROOT_URL) via post_logout_redirect_uri. When unset, logout behaviour is unchanged, so this is backward compatible. For Keycloak 18+, add your Wekan ROOT_URL to the client's Valid post logout redirect URIs. See docs/Login/Keycloak/Keycloak.md

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/086254e10">Fixed due dates not correctly colour coded</a>.</summary>

Fixed due dates not correctly colour coded : future due dates more than 48 hours away are now shaded grey (not-due) instead of amber (due-soon). Root cause was a call to diff(theDate, now, 'days') where 'days' is not a valid unit, so the threshold compared raw milliseconds; replaced with a precise hours-based comparison in a single shared helper

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/086254e10">Fixed due date colour mismatch between list and card detail</a>.</summary>

Fixed due date colour mismatch between list and card detail : an overdue card now shows red in both the minicard/list and the opened card detail. The card-detail status colours now use !important so overdue red overrides the due-date yellow base (matching the minicard), and the colour-decision logic is unified into one shared helper used by both views

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b3acbd692">Fixed unable to view all cards by due date</a>.</summary>

Fixed unable to view all cards by due date : removed the limit: 100 cap in the dueCards publication so all of a user's due cards across boards are shown

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b3acbd692">Fixed unable to scroll past the first cards in the Due Cards view on mobile</a>.</summary>

Fixed unable to scroll past the first cards in the Due Cards view on mobile : the due-cards list now has a scoped scroll container with a viewport-relative max height

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5c890baca">Fixed card-detail sub-popups disappearing on mobile</a>.</summary>

Fixed card-detail sub-popups disappearing on mobile : assigning a user or setting the due date on touch devices no longer closes the popup (touch events inside the popup no longer bubble to the click-outside close handler on mobile viewports)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5c890baca">Fixed mobile board layout and tiny Home button</a>.</summary>

Fixed mobile board layout and tiny Home button : minicards now render one per row (full width) on narrow screens and the header Home / All Boards button is a proper tap target

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5c890baca">Fixed oversized padding/margins and stray ➕ emoji from recent UI changes</a>.</summary>

Fixed oversized padding/margins and stray ➕ emoji from recent UI changes : trimmed excessive padding/margins on mobile; remaining stray plus emojis are tracked for replacement with a Font Awesome icon

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c18ae388b">Fixed "create list" not available in Lists board-view mode</a>.</summary>

Fixed "create list" not available in Lists board-view mode : the "Add list" composer now appears in Lists mode (using the board's default swimlane), not only in Swimlanes mode

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fe4d2a56f">Fixed board/list/swimlane numbering breaking first-letter keyboard navigation</a>.</summary>

Fixed board/list/swimlane numbering breaking first-letter keyboard navigation : the move/copy card popups no longer prefix a number to each option, so options start with their name again and digit-named boards are readable

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f59273508">Fixed missing notification and card-history entry when a new attachment is uploaded</a>.</summary>

Fixed missing notification and card-history entry when a new attachment is uploaded : uploading an attachment now creates an addAttachment activity, so card members and subscribers are notified (consistent with attachment removal); previously the activity was never created because the store-strategy upload hook was dead code

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d26d117e3">Fixed copying a card selecting all/unnamed labels on the destination board</a>.</summary>

Fixed copying a card selecting all/unnamed labels on the destination board : Cards.copy() now applies the same unnamed-label guard as Cards.move() and persists the remapped labels onto the inserted card

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a0e701708">Fixed copied card losing its cover ("show as thumb")</a>.</summary>

Fixed copied card losing its cover ("show as thumb") : coverId is now remapped to the newly copied attachment instead of pointing at the original (now-unresolvable) attachment id

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09e9c993e">Fixed deleting a date on a linked card not taking effect</a>.</summary>

Fixed deleting a date on a linked card not taking effect : unsetReceived/unsetStart/unsetDue/unsetEnd now resolve the real card id via getRealId() (consistent with the set* methods) so they update the underlying linked card, not the link placeholder

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aa412b2a1">Fixed comment-only members being able to archive cards from the UI</a>.</summary>

Fixed comment-only members being able to archive cards from the UI : the archive action now respects Utils.canModifyCard() like every other mutating card action (the server allow-rule already rejected the write; this closes the client-side UX gap)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/45bd7dcc8">Fixed sub-task board being inaccessible until a reload</a>.</summary>

Fixed sub-task board being inaccessible until a reload : the "view subtask" navigation now guards against a not-yet-loaded board, mirroring the sibling handler

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/da7c06ec6">Fixed the "When a card is moved to Archive" rule trigger not being activatable</a>.</summary>

Fixed the "When a card is moved to Archive" rule trigger not being activatable : a CSS class-name mismatch (js-add-arc-trigger vs js-add-arch-trigger) between the board-triggers template and its click handler is fixed, with a regression test

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/300a9751e">Fixed "select all in list" crossing swimlanes</a>.</summary>

Fixed "select all in list" crossing swimlanes : list select-all is now scoped to the current swimlane in swimlanes view (allCards() gained an optional swimlane scope); list-wide behaviour is preserved where there is no swimlane context

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9643dcded">Fixed copying a swimlane to another board losing card labels</a>.</summary>

Fixed copying a swimlane to another board losing card labels : missing board-level labels are now recreated on the destination board (preserving colour) before the per-card copy so label assignments survive

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7eec78c04">Fixed Calendar View ignoring the start-day-of-week setting</a>.</summary>

Fixed Calendar View ignoring the start-day-of-week setting : FullCalendar's firstDay is now derived from getStartDayOfWeek() instead of the locale default

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ba8882d8">Fixed deleted-attachment notification crediting the uploader instead of the deleter</a>.</summary>

Fixed deleted-attachment notification crediting the uploader instead of the deleter : the deleteAttachment activity now records the acting user (falling back to the uploader for server/system removals)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9aa97ab8f">Fixed updating a card title not firing the outgoing webhook</a>.</summary>

Fixed updating a card title not firing the outgoing webhook : a title change now logs an a-changedTitle activity (rendered in the activity feed) so the existing outgoing-webhook hook fires, consistent with description/date changes

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3ce75dd87">Fixed @mention: pressing Enter to pick a user closed the card / submitted the comment</a>.</summary>

Fixed @mention: pressing Enter to pick a user closed the card / submitted the comment , also #4172 and #5457: when the @mention autocomplete dropdown is open, Enter now selects the highlighted user instead of submitting/closing (shared textcomplete keydown guard hardened)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/39e12035e">Fixed REST card API move/sort/date/archive bugs</a>.</summary>

Fixed REST card API move/sort/date/archive bugs : consolidated duplicated board-move variable names (#5398); moving a card to another list via the API now puts it on top of the destination list like the Move Card dialog (#5399); due/received/start/end dates set via the API now persist instead of being stripped (#5537); and archived cards can be inspected via the single-card GET and de-archived without needing a list_id (#5546)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e98aae278">Fixed the per-checklist "Hide checked items" toggle being inverted and affecting all checklists</a>.</summary>

Fixed the per-checklist "Hide checked items" toggle being inverted and affecting all checklists : it is now read per checklist and hides an item exactly when it is checked and that checklist's toggle is on

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6f8543322">Fixed setting a list/swimlane colour to silver saving it as None</a>.</summary>

Fixed setting a list/swimlane colour to silver saving it as None : list/swimlane colours are normalized through a shared canonical allowed-colour helper, so silver (and every offered colour) is accepted and rendered

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f819054bb">Fixed internal caret ^board^ helper boards appearing in board lists and the REST API</a>.</summary>

Fixed internal caret ^board^ helper boards appearing in board lists and the REST API : caret-wrapped titles and non-board types are now filtered from /api/users/:userId/boards, /api/boards and the Boards.userBoards helper, consistent with the UI

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/beac9e3d7">Fixed being unable to remove a deleted (non-existent) user from a board's members</a>.</summary>

Fixed being unable to remove a deleted (non-existent) user from a board's members : orphaned member entries are now hard-removed by userId even when the user account no longer exists

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/98315c26c">Fixed a custom number field displaying as NaN when cleared after being set</a>.</summary>

Fixed a custom number field displaying as NaN when cleared after being set : an empty number value is stored as '' and rendered as empty via a shared formatNumberValue helper

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b27313c7">Fixed subtask creation producing extra swimlanes/columns and only allowing one subtask</a>.</summary>

Fixed subtask creation producing extra swimlanes/columns and only allowing one subtask , also #5788, #2256 and #4782: subtask creation is now server-authoritative (addSubtaskCard Meteor method) and client-side auto-creation of the default subtasks board/list is guarded to the server, so the client can no longer create duplicate subtasks boards/swimlanes and multiple subtasks can be created reliably

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b27313c7">Fixed board "always on card" custom fields not being applied to new subtask cards</a>.</summary>

Fixed board "always on card" custom fields not being applied to new subtask cards , also #3562: the destination board's automatic custom fields are now added to a new subtask

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b27313c7">Fixed a circular subtask/parent reference hanging the whole board</a>.</summary>

Fixed a circular subtask/parent reference hanging the whole board : the parent-chain guard compared array indices instead of ids; it now compares by value and setParentId refuses a cyclic re-parent

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/774289f92">Fixed the board Subtasks "Landing list for subtasks deposited here" setting not saving / showing…</a></summary>

Fixed the board Subtasks "Landing list for subtasks deposited here" setting not saving / showing the wrong list , also #3876, #4849 and #4947: the settings popup now reads the deposit board's lists and matches the stored subtasksDefaultListId

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7bb7540ba">Fixed deleting a card not firing the outgoing webhook</a>.</summary>

Fixed deleting a card not firing the outgoing webhook : card deletion now creates a deleteCard activity (in the before-remove hook and the REST delete endpoints) so the outgoing webhook fires; board/list/swimlane deletion already emitted their delete activities (#2950)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8863ec24a">Fixed a configured outgoing webhook making it impossible to set card members</a>.</summary>

Fixed a configured outgoing webhook making it impossible to set card members : outgoing webhook delivery is now fire-and-forget and error-isolated, so a slow/unreachable/failing webhook endpoint can no longer abort the member update

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.64 2026-06-20 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUE of ChecklistBleed:

<details> <summary><a href="https://github.com/wekan/wekan/commit/b1ca76007">Fixed ChecklistBleed: any authenticated user can write checklist data into a private board they are…</a> Thanks to DavidCarliez and xet7.</summary>

Fixed ChecklistBleed: any authenticated user can write checklist data into a private board they are not a member of (cross-board write via collection allow rule) (GHSA-gv8h-5p3p-6hx7, CWE-863 Incorrect Authorization). This is the same class as BoardBleed (GHSA-gm7v-pc38-53jr), but for the card-attached Checklist and ChecklistItem documents that the boardId-only denyCrossBoardMove fix did not cover. Checklists and checklist items are attached to a card and carry a denormalized boardId; they are MOVED between cards by $set-ting a new cardId (and, for items, a new checklistId) in a direct DDP collection update, after which the Checklists.before.update hook re-derives boardId from the destination card. The collection allow rules (server/permissions/checklists.js, server/permissions/checklistItems.js) authorized an update by checking only the document's CURRENT (source) cardId — i.e. the attacker's own board — and never inspected the new destination cardId/checklistId/boardId. Because every logged-in user can create a board where they are a write-capable member, a low-privileged user with write access to one board could create a checklist/item on their own card and then, in a single /checklists/update or /checklistItems/update DDP call, set its cardId to a card in a private board where they are not a member (if they know the target card id): the allow rule saw the attacker's source card, approved the write, and the before-update hook attached the attacker-controlled document to the victim's private board. The protected moveChecklist Meteor method correctly checks both source and destination board membership, but a DDP client can bypass that method and update the collections directly. CVSS:3.1 Moderate (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N). Fixed by adding denyCrossBoardMoveByCard and denyCrossBoardMoveByChecklistItem helpers in server/lib/utils.js and a Checklists.deny/ ChecklistItems.deny update rule on each collection that rejects any update whose destination board — resolved from a new boardId, cardId, or checklistId in the modifier — the caller has no write access to. Legitimate moves into boards the user belongs to and same-card edits keep working, and the server-side moveChecklist method (which bypasses allow/deny) is unchanged. A regression test (server/lib/tests/checklistbleed.security.tests.js) was added. Affected Wekan v9.62 and earlier

</details>

and adds the following updates:

<details> <summary><a href="https://github.com/wekan/wekan/commit/069dbc11f">Fix the Docker pre-build version guard false-failing the release</a>. Thanks to xet7.</summary>

Fix the Docker pre-build version guard false-failing the release The bundle-version guard added for the Admin-Panel-version fix assumed the WeKan app package.json ships as a standalone file with a v-prefixed version, and made "not found" fatal. Meteor does not ship it that way — it inlines the app package.json into the compiled bundle/programs/server/app/app.js as a JSON module ({"name":"wekan","version":"v9.63.0",...}), so the guard found no v-prefixed package.json and aborted the v9.63 Docker build. The guard now reads the version from that inlined module (anchored on "name":"wekan", exactly what require('/package.json') resolves to), and a detection miss is now a warning that continues rather than a hard failure — only a confirmed version MISMATCH blocks the release, since --build-arg VERSION is the actual guarantee

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.63 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/40c3ee098">Fix the wekan.fi install page version never updating from a stale value</a>. Thanks to xet7.</summary>

Fix the wekan.fi install page version never updating from a stale value The release-all.yml website job ran every release but the install page's version stayed frozen at v9.57: releases/release-website.sh updated it with a sed anchored on >v$OLD</span>, which silently no-op'd once the published page's value no longer matched the old_version passed for a release. Anchor on the stable <span class="version-number"> instead and re-normalize whatever version is there to the new one, with an assert so a miss fails loudly. Same self-healing fix applied to the local-flow copy in releases/version.sh. The live wekan.fi install page was also corrected to the current version

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1e5ae8f64">Make the remaining release version substitutions self-healing</a>. Thanks to xet7.</summary>

Make the remaining release version substitutions self-healing Hardened the last $OLD_VERSION-anchored seds in releases/version.sh — the same fragile pattern that froze the Docker ARG VERSION and the install page. The snapcraft.yaml bundle download was release-critical (same class as the Docker bug: the snap downloads wekan-<v>-<arch>.zip, so a stale value ships the wrong bundle under the right name) and was stuck at v9.57; it now anchors on the wekan-<v>- / releases/download/v<v>/ shapes and asserts. The sandstorm appVersion rewrite is now global and self-healing (the redundant $OLD_NO_DOTS-anchored fixup is dropped), and the Windows Offline.md doc links self-heal with a soft warning (cosmetic, so they must not fail the release). snapcraft.yaml and Offline.md were also corrected from their stale v9.57

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.62 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/b30f4967c">Release All Platforms: Fix Docker image showing a stale Admin Panel version (image tagged vX…</a> Thanks to xet7.</summary>

Release All Platforms: Fix Docker image showing a stale Admin Panel version (image tagged vX reported v9.57) The Admin Panel reads the WeKan version from the bundled package.json, which comes verbatim from the wekan-<version>-<arch>.zip the Dockerfile downloads and unzips into /build. The docker job never passed --build-arg VERSION, so every image was built against the Dockerfile's hardcoded ARG VERSION default — and that default was stuck at 9.57 because releases/version.sh rewrote it with a sed anchored on the old version number, which silently no-op'd whenever old_version did not match (e.g. the skipped 9.58 numbering). The result: images tagged v9.59/v9.60/v9.61 shipped the v9.57 bundle and reported 9.57 in the Admin Panel. Fixed three ways: the docker job now passes --build-arg VERSION=${VERSION} (the release version is authoritative); the version.sh Dockerfile rewrite now anchors on the ^ARG VERSION= prefix and asserts the result, so the default can never go stale again; and a new pre-build guard downloads the release bundle and fails fast if its app package.json version does not match the release tag, before pushing a mislabeled image to the registries. Already-pushed v9.58–v9.61 images need a rebuild to carry their correct contents

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.61 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/c41ad5fd68ae4ed23b5414dffabfea4b3ac1dd8d">Release All Platforms: Set GH_REPO on the bundle-attach steps so gh finds the repository</a>. Thanks to xet7.</summary>

Release All Platforms: Set GH_REPO on the bundle-attach steps so gh finds the repository The win64 / mac-arm64 / s390x / ppc64le bundles each finished building but then failed on gh release upload with failed to run git: fatal: not a git repository. gh tries to detect the target repo from a git remote, but build-win64 checks the repo out into src/ (so the workspace root is not a git repository) and build-mac-arm64 / build-extra-arches do not check it out at all. Set GH_REPO=${{ github.repository }} on all three attach steps so gh targets wekan/wekan directly without git remote detection. The snap job is unaffected (it does a full checkout into the workspace root)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.60 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/863883a7f71983d371d28b18db7fcf410e0720ce">Release All Platforms: Drop armv7l from extra-arch bundles, because Node.js 24 ships no armv7l…</a> Thanks to xet7.</summary>

Release All Platforms: Drop armv7l from extra-arch bundles, because Node.js 24 ships no armv7l binaries The build-extra-arches armv7l matrix entry failed with no matching manifest for linux/arm/v7 when pulling node:24-slim. Root cause: Node.js 24 publishes no armv7l (32-bit ARM) binaries at all — neither the official dist nor unofficial builds — so there is no node:24 arm/v7 image to rebuild native modules against, and no Node 24 runtime to run such a bundle on an armv7 device. The armv7l matrix entry is removed; extra-arch bundles are now s390x + ppc64le. armv7 was already excluded from the Docker image and snap, so those are unaffected

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/227d7877c8fe97253249ada61c24fd991f9e0085">Release All Platforms: Move win64 and mac-arm64 into the post-release extra-platform phase</a>. Thanks to xet7.</summary>

Release All Platforms: Move win64 and mac-arm64 into the post-release extra-platform phase build-win64 and build-mac-arm64 were prerequisites of the release job, so a slow or flaky Windows/macOS runner blocked creation of the GitHub Release (and thus the Docker and snap jobs, which depend on it). They now depend on release instead — alongside build-extra-arches — and each attaches its own wekan-<version>-<platform>.zip to the already-created Release via gh release upload --clobber. The core release now waits only on the amd64

  • arm64 bundles. No build steps changed; only the dependency wiring, upload mechanism, and section grouping
</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.59 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/69236a90620b792bbe95b9e60caef2ecd172b327">Release All Platforms: Fix duplicate mapping keys in generated OpenAPI spec that broke API docs…</a> Thanks to xet7.</summary>

Release All Platforms: Fix duplicate mapping keys in generated OpenAPI spec that broke API docs rendering The Release All Platforms bump job regenerates public/api/wekan.yml via openapi/generate_openapi.py and then renders it with @redocly/cli, whose strict YAML parser rejects duplicate mapping keys. The 3-level nested SimpleSchema in models/attachmentStorageSettings.js (storageConfig.filesystem.enabled, storageConfig.gridfs.enabled, …) exposed two generator bugs: the sub-schema name was derived from only the first dotted path segment, collapsing filesystem.* and gridfs.* leaf keys (enabled/read/write) into one mapping; and the linear emitter reopened the parent schema header for each interleaved nested object. The generator now builds sub-schema names from all leading path segments and groups each sub-schema's fields contiguously, so deeply nested objects emit distinct, valid sub-schemas. Output is unchanged for existing 1- and 2-level schemas

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.58 2026-06-19 WeKan ® release

This release adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/7c8ef380f54a6f1fe45ea834ee5927996631fe6c">Release: bump both WeKan version fields in package-lock.json</a>. Thanks to xet7.</summary>

Release: bump both WeKan version fields in package-lock.json releases/version.sh updated only the top-level version in package-lock.json, leaving the nested packages."" version stale (lockfileVersion 3 stores the root version twice). The local release.sh flow hid this because rebuild-release.sh re-runs meteor npm install before committing, but the remote Release All Platforms flow (release-all.yml bump job) commits version.sh's output directly, so a stale package-lock.json was pushed to main. The bump now anchors on the v-prefixed WeKan version and replaces both root fields, never touching any dependency version (those are plain semver, no v prefix). Node.js (24.x) and MongoDB (7.x) bumps are unchanged — the pinned major is intentional and the minor/patch already updates any older 24.x/7.x to the newest

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2a713a60b8620fe2b1d20968f2cd1ed88abf18a9">Don't auto-create an empty Template Container, and make user-created ones functional</a>. Thanks to xet7.</summary>

Don't auto-create an empty Template Container, and make user-created ones functional Opening All Boards / Templates no longer auto-creates an empty Template Container board (the board-list autorun that called ensureTemplatesBoard on view open is removed); the container is created only on demand via the Add Template Container button. To make a user-created container actually usable, createBoardWithInitialSwimlanes now wires the user's profile template pointers (templatesBoardId and the card/list/board template swimlane ids, keyed by a new per-swimlane role) when the board type is template-container — without this the container looked right but stayed inert, since swimlane.isCardTemplatesSwimlane()/… compare against those pointers to decide that an added card/list/swimlane/board becomes a template. Deleting a Template Container now clears those pointers for any user referencing it, so no save/insert-from-template path is left pointing at a dead board and a fresh container can be created. Adds Playwright e2e coverage (#5850, #2339)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/422e70329">Make the All Boards / Templates redesign and org/team/domain board sharing work end-to-end</a>. Thanks to xet7.</summary>

Make the All Boards / Templates redesign and org/team/domain board sharing work end-to-end Fixes bugs the new Playwright e2e tests surfaced in the recently added features: the new server method modules are now actually loaded (they use an explicit server mainModule, so unimported files never registered their methods); the per-org/team feature-toggle methods and setUserOrgsTeamsFromLdap check their arguments before the admin guard (an unchecked-argument rejection was being turned into a full app crash by SyncedCron's global handler) and authorize from this.userId; the boards publication now publishes template-container and domain-shared boards (not only type:'board'); and the /templates and /remaining All Boards routes apply the same membership filtering as the home route (#5850, #4737, #2339)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ddb88af15">Enforce the per-org/team "Shared Templates" flag for drag-to-share</a>. Thanks to xet7.</summary>

Enforce the per-org/team "Shared Templates" flag for drag-to-share The All Boards / Templates drag-to-share now offers only the Organizations and Teams whose Shared Templates flag is enabled (previously the flag had no effect). A new non-admin getMyShareableGroups method returns the user's flagged orgs/teams (plus their email domains), since the org/team publications are admin-only (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f5cca8cea12fac0cdffae5293463c090def22538">Enforce the per-org/team "Propagate Members To Boards" flag</a>. Thanks to xet7.</summary>

Enforce the per-org/team "Propagate Members To Boards" flag When Propagate Members To Boards is enabled for an Organization or Team, its member users are now added as members of the regular boards that list that group (new admin- and server-callable propagateOrgTeamMembersToBoards method, also run at the end of the LDAP background sync). It is strictly add-only and skips template boards (which stay group-only) (#5850, #4737)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4aa8c5eed">Drag a Template Board onto an Organization, Team or Domain to share it</a>. Thanks to xet7.</summary>

Drag a Template Board onto an Organization, Team or Domain to share it On the All Boards / Templates view, the left menu shows the user's Organizations, Teams and Domains as drop targets (gated by the admin's Shared Templates scopes). Dragging a Template Board onto one shares the board with that org/team/domain (via setBoardOrgs/setBoardTeams/setBoardDomains) — add-only and adding no individual members (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dbaa27dd6">GlobalAdmin REST API for the Admin Panel global settings</a>. Thanks to xet7.</summary>

GlobalAdmin REST API for the Admin Panel global settings Adds GET /api/settings and PUT /api/settings, restricted to the global admin, to read and update the Admin Panel global settings (registration, product name, logos, custom head/manifest, accessibility and support pages, etc.). A whitelist of fields is exposed; mailServer/SMTP credentials are never returned or writable over REST. Documented with @operation JSDoc (OpenAPI) and api.py examples (getsettings, editsettings)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/46982a3efd437429cef26a48da812bfd0da60602">REST API for board domain sharing</a>. Thanks to xet7.</summary>

REST API for board domain sharing GET / POST / DELETE /api/boards/:boardId/domains list, add and remove the email domains a board is shared with, reusing the setBoardDomains validation and requiring board-admin (or site-admin) rights for changes. Documented in the generated OpenAPI spec with matching api.py examples (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8849b015dd3debdca4445fbf097e289c5be08128">Admin Panel / People / Domains: list domains with per-domain user counts</a>. Thanks to xet7.</summary>

Admin Panel / People / Domains: list domains with per-domain user counts A new Domains tab in Admin Panel > People lists every email-address domain across all users with the count of users in each (each user counted once by their primary email's domain), backed by an admin-only getDomainsWithUserCounts method. This is the per-domain piece of the Template Boards sharing feature (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cf4494721">Template boards: members tab is group-only and shows only the creator</a>. Thanks to xet7.</summary>

Template boards: members tab is group-only and shows only the creator On template boards (template-board / template-container) the board members People tab now shows only the original creator and hides the add-individual-member button, so these shared boards are shared only with groups (Organizations, Teams, Domains) — not individual users. Regular boards are unchanged (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/50c5d0738213d41bbae35fe8c25ce5e03d2e82cc">Share a board with an email domain from the board members sidebar</a>. Thanks to xet7.</summary>

Share a board with an email domain from the board members sidebar A new Domains tab in the board members sidebar lets a board admin type a domain (e.g. example.com) and add or remove it, backed by a board-admin-gated setBoardDomains method that trims, lowercases, validates and de-duplicates domains. Combined with the board.domains access wiring, every user whose primary email is in an active domain on the board gets access (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/50c5d0738">Domain-based board sharing: board can be shared with an email domain</a>. Thanks to xet7.</summary>

Domain-based board sharing: board can be shared with an email domain Adds a board.domains field (mirroring board.orgs/board.teams) so a board can be shared with an email-address domain — every user whose primary email is in an active domain on the board gets access. Adds a User.emailDomains() helper and wires the new dimension into the board visibility selector, the board publications and the All Boards membership query (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/04b26f470">Auth sync marks the orgs/teams it manages as "Sync Members From Auth Provider"</a>. Thanks to xet7.</summary>

Auth sync marks the orgs/teams it manages as "Sync Members From Auth Provider" When the LDAP/OIDC group sync creates or assigns an Organization or Team, it now sets that org/team's orgSyncMembersFromAuth / teamSyncMembersFromAuth flag, so the Sync Members From Auth Provider column in Admin Panel > People reflects which orgs/teams are auth-managed (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1ee73a0cfb51fb72f635f04f8e659b0d873f1713">All Boards / Templates: URL routes, link redirect, no auto-created templates board</a>. Thanks to xet7.</summary>

All Boards / Templates: URL routes, link redirect, no auto-created templates board The All Boards page's Templates and Remaining sub-views are now addressable by their own URLs (/templates, /remaining), so they can be linked and redirected to and switch the view live. The Member Settings → Templates menu link now opens the All Boards / Templates page instead of opening or creating a specific board. New users no longer get an auto-created "Templates" board at signup; the templates-container board (with its Card / List / Board Templates swimlanes) is instead created lazily on first use of the Templates view via a new ensureTemplatesBoard server method. This works for any authentication method (password, LDAP, OAuth2), existing users keep their templates board and all save-as-template functionality, and any user can still create additional template boards through Create Board → Add template board (#2339, #5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/82f028cbea6b0fd0c83d1b0e30c4d047eb93fdee">Optional setting to set admin status from OAuth2/OIDC groups</a>. Thanks to xet7.</summary>

Optional setting to set admin status from OAuth2/OIDC groups Optional and off by default: when OAUTH2_ADMIN_GROUPS is set to a comma- or whitespace-separated list of group names, a user logging in via OAuth2/OIDC whose OIDC groups claim intersects that list is granted Wekan admin (isAdmin), applied both at login and at first-time account creation; the group data is accepted as either plain strings or objects with a display name. This mirrors the existing LDAP LDAP_SYNC_ADMIN_GROUPS behaviour. When OAUTH2_ADMIN_GROUPS is empty/unset (the default), admin status is left completely unchanged (#5876)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c4cc17c10d07a670a3f58c2d8e736aaf13337ab6">Per-organization and per-team feature toggle columns in Admin Panel &gt; People</a>. Thanks to xet7.</summary>

Per-organization and per-team feature toggle columns in Admin Panel > People Admin Panel > People > Organizations and Teams each gain three per-record checkbox columns, all disabled by default: Shared Templates (allow members to drag personal Template Boards onto this org/team), Propagate Members To Boards (add this org/team's members to the boards that list it), and Sync Members From Auth Provider (this org/team's membership is maintained by the authentication provider's group/membership sync — LDAP, OAuth2/OIDC, SAML, etc.). Each column header has select-all / unselect-all, and the per-tab search box filters the rows. All three flags are enforced: Shared Templates gates which orgs/teams are offered as drag-to-share targets, Propagate Members To Boards adds a flagged group's members to the regular boards that list it, and Sync Members From Auth Provider is set by the LDAP/OIDC group sync for the orgs/teams it manages (#4737, #5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8e2dacd856cef46728f0341eeafcb03fd0f453fa">Added Admin Panel setting to block avatar uploads</a>. Thanks to xet7.</summary>

Added Admin Panel setting to block avatar uploads Admin Panel > Attachments > Transfer limits now has an "Avatars" option that blocks users from uploading new avatars (board setting limitSettings.avatarsUploadBlocked, unchecked by default so avatars stay enabled). When blocked, the avatar upload option is hidden in the user's avatar popup and new uploads are rejected server-side — useful when avatars are synced from another source (#4740)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/100357d953fe891eda8c3d44b9c1986ef09e6a6b">Improvements to Template Boards and sharing them with Organizations, Teams and Domains</a>.</summary>

Improvements to Template Boards and sharing them with Organizations, Teams and Domains Card and board templates already exist in WeKan; these are improvements to how Template Boards are managed and shared on the All Boards page (#5850):

</details>
  • At All Boards / Templates, all of the user's own Board Templates are shown, with a dedicated Add Template Board button that creates a new Template Board there.
  • The + Add board button at All Boards / Remaining no longer has an "Add template board" checkbox; creating a Template Board is now done only with the Add Template Board button under All Boards / Templates.
  • A Template Board can contain Board, Swimlane, List and Card templates.
  • Members of a Template Board can, on any board where they are a member, add those templates to their current board.
  • If a user belongs to an Organization, Team or Domain, and the Admin Panel has Shared Templates for Organizations / Teams / Domains enabled, then everyone in that Organization, Team or Domain can drag a personal Template Board at All Boards / Templates onto that Organization, Team or Domain board to share it.
  • Domain means the domain part of a user's email address (for example example.com): users that belong to that domain can drag a Template Board to that domain so it is shared with everyone in the domain.
  • New Admin Panel / People / Domains page that lists the domains and the count of users per domain. Thanks to xet7.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d822c1a89961b167808104c6870176d26b90f1b6">Optional setting to refuse unknown OAuth2/OIDC logins</a>. Thanks to xet7.</summary>

Optional setting to refuse unknown OAuth2/OIDC logins Optional and off by default: when OAUTH2_AUTO_REGISTRATION=false, a first-time OAuth2/OIDC login that does not already match a Wekan account (by verified email — the same secure key the existing account-merge logic uses) is rejected instead of silently creating a new account. This lets an instance allow only already-provisioned users (for example synced from LDAP) to sign in via OAuth2. The default true keeps the previous behaviour, so existing deployments are unaffected (#4736)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f094f7e8677e1fb3c1823b17605df20943159f4c">Notify the assigned card member/assignee directly</a>. Thanks to xet7.</summary>

Notify the assigned card member/assignee directly When a user is added as a card member or assignee, that user is now notified directly, so an assignment reaches the assignee themselves — instead of only board watchers, or (with BIGEVENTS_PATTERN) every active board member. The user who performed the assignment is still skipped, so self-assignment does not self-notify. Optional: opt out with NOTIFY_ON_ASSIGN=false; on by default (#5833)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c1752c5fbaf1f0b7640f9797c187fe6ba2dcc85f">Export big boards: optional JSON export without attachments</a>. Thanks to xet7.</summary>

Export big boards: optional JSON export without attachments Very large boards could fail to export as JSON because every attachment is base64-encoded inline into a single object, which overflows the JSON serializer's maximum string length (and loads every attachment into memory at once). A new optional ?attachments=false query parameter on GET /api/boards/:boardId/export exports the board structure and attachment metadata while skipping the base64 file data, so big boards export successfully; a matching Export / JSON (without attachments) entry is added to the board export menu. The default export is unchanged (#5870)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6a89da85e614793edd9e1f6f30abea02281a8eed">Fix LDAP sign-in failing when a DN or cn contains parentheses</a>. Thanks to xet7.</summary>

Fix LDAP sign-in failing when a DN or cn contains parentheses The user's group-member value (e.g. a member DN or cn) was interpolated into the LDAP group search filter unescaped, so a value containing ( or ) produced illegal unescaped char: ( and broke sign-in when LDAP_SYNC_ADMIN_STATUS (or any group sync) was enabled. Adds RFC 4515 escaping for ( ) * \ and NUL in the group filter (also prevents filter injection) (#5236)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9ddf28845">Fix snap build failing to download Node.js (404)</a>. Thanks to xet7.</summary>

Fix snap build failing to download Node.js (404) The snapcraft Node.js download used the floating latest-v24.x/ path with a pinned node-v24.16.0 filename, which 404'd once upstream Node advanced past that release — failing the amd64 and arm64 snap builds. Pinned to the explicit, stable https://nodejs.org/dist/v24.17.0/ path (matching the Dockerfile)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d28106dd532a5a31bdd52a68bb397ac8249956c">Keep LDAP admin status updated during background sync</a>. Thanks to xet7.</summary>

Keep LDAP admin status updated during background sync LDAP_SYNC_ADMIN_STATUS previously only updated a user's admin status at login, so an admin-group change in LDAP was never reflected for existing users who did not log in. The background sync now applies the same admin-status logic to each existing LDAP user it syncs. Gated by the existing LDAP_SYNC_ADMIN_STATUS flag (default off), so default behaviour is unchanged (#4739)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9bfd370aa34ef2ca6d3c64a9cfa18bdc76c8429">Optionally make LDAP authoritative for user active status</a>. Thanks to xet7.</summary>

Optionally make LDAP authoritative for user active status. New optional LDAP_BACKGROUND_SYNC_DISABLE_NONEXISTANT_USERS (default false): when enabled, the background sync disables (loginDisabled) LDAP-sourced users that are no longer found in the directory, and re-enables users that are present in LDAP again — so LDAP is the authoritative source of active status (matching the external docs/Login/ldap-sync/ldap-sync.py). Off by default; only LDAP-sourced users are considered. With the flag on, a manual disable of an LDAP user is overridden on the next sync (#4738)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bc8167166dab343d194f3f84e47b5fc3a1a02f9d">Optionally sync LDAP groups as Organizations or Teams</a>. Thanks to xet7.</summary>

Optionally sync LDAP groups as Organizations or Teams. New optional, default-off settings sync a user's LDAP groups into Wekan Organizations and/or Teams during background sync: LDAP_SYNC_ORGANIZATIONS and LDAP_SYNC_TEAMS, with optional comma-separated allowlists LDAP_SYNC_ORGANIZATIONS_GROUPS / LDAP_SYNC_TEAMS_GROUPS restricting which of the user's groups become orgs/teams (empty = all). The user's groups are read with the existing LDAP_GROUP_FILTER_* machinery; matching orgs/teams are created (active) if missing and added to the user's membership (add-only — no existing membership is ever removed). The sync runs both during background sync and at login ( commit ). Off by default, so existing deployments are unaffected (#4737)

</details>

and updates the documentation:

<details> <summary><a href="https://github.com/wekan/wekan/commit/6b093bd13">Documented the new LDAP/OAuth2 env vars and template-sharing features</a>. Thanks to xet7.</summary>

Documented the new LDAP/OAuth2 env vars and template-sharing features Documents LDAP_SYNC_ORGANIZATIONS/LDAP_SYNC_TEAMS (+ group allowlists), LDAP_BACKGROUND_SYNC_DISABLE_NONEXISTANT_USERS, the LDAP admin-status and group-filter-escaping fixes, OAUTH2_AUTO_REGISTRATION, OAUTH2_ADMIN_GROUPS, NOTIFY_ON_ASSIGN, the Admin Panel Domains tab and org/team toggle columns, sharing a board with a domain, and group-only template boards

</details>

and adds the following tests:

<details> <summary><a href="https://github.com/wekan/wekan/commit/62bd6f2b20da23f0d89480dfb9947c8583c11773">Added e2e tests for domain-based board sharing</a>. Thanks to xet7.</summary>

Added e2e tests for domain-based board sharing Verifies setBoardDomains normalizes, validates and de-duplicates board domains (rejecting malformed entries), that the admin-only getDomainsWithUserCounts returns correct per-domain counts and denies non-admins, and that a board shared with a user's email domain becomes visible to that user (#5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b41dc111f9a98cabb1c52d8e15f447ad3672c569">Added e2e tests for the All Boards / Templates redesign</a>. Thanks to xet7.</summary>

Added e2e tests for the All Boards / Templates redesign Verifies the ensureTemplatesBoard method creates the templates container and is idempotent, that the /templates and /remaining routes show the right boards (template-container vs regular), and that the Member Settings → Templates link navigates to /templates (#2339, #5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7c63ec6205b83ce8743617690d1062064a56637">Added e2e tests for the per-org/team feature toggle methods</a>. Thanks to xet7.</summary>

Added e2e tests for the per-org/team feature toggle methods Verifies the admin-only per-org and per-team toggles (Shared Templates, Propagate Members To Boards, Sync Members From Auth Provider), the select-all / unselect-all bulk methods, and that a non-admin call is a silent no-op (#4737, #5850)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/79847e9450e69da13b8b9adbfe47ed14d711d2d8">Added a unit test for the OAuth2 admin-from-groups logic</a>. Thanks to xet7.</summary>

Added a unit test for the OAuth2 admin-from-groups logic A runnable Node unit test for oauth2AdminStatusFromGroups: string and object group forms, comma/whitespace parsing, intersection with OAUTH2_ADMIN_GROUPS, and the default-off behaviour (no admin change when unset) (#5876)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9370ddfb34e93d20ff11382352d0786e5d68c4f3">Test harness: db.insertOne / db.insertMany now return the driver result</a>. Thanks to xet7.</summary>

Test harness: db.insertOne / db.insertMany now return the driver result The Playwright mongo-runner discarded the insert result, so db.insertOne(...) returned null and specs could not read insertedId; it now returns the result (empty insertMany yields insertedCount 0)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e65f61d36b45b512565467c9941d20c36ae64037">Added e2e test for LDAP group → Organization/Team sync</a>. Thanks to xet7.</summary>

Added e2e test for LDAP group → Organization/Team sync Verifies a group is created as a Team/Organization (active), added to the user, add-only (existing memberships preserved), idempotent, and that a non-admin call is rejected (#4737)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/963e6588c0ae1eb04ff63594a49385a7fde9dbbb">Added more tests</a>. Thanks to xet7.</summary>

Added more tests Playwright e2e tests for the "Mark as complete" Show at Minicard option (allowsDueCompleteOnMinicard — hidden by default, shown when enabled) and the Admin Panel avatar-upload block (isAvatarUploadBlocked / limitSettings.avatarsUploadBlocked — disabled by default)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.57 2026-06-19 WeKan ® release

This release fixes the following bugs:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.56 2026-06-18 WeKan ® release

This release adds the following updates:

  • Updated dependencies. Merged Dependabot update: dompurify 3.4.9 → 3.4.11 (#6395), a patch update of the HTML sanitizer used for XSS protection. Thanks to xet7.

Thanks to above GitHub users for their contributions and translators for their translations.

v9.55 2026-06-18 WeKan ® release

This release fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/ddfe351968f629b619cd757c410a222381d11f98">Fixed "Release All Platforms" snap build failing at upload with exit 64</a>. Thanks to xet7.</summary>

Fixed "Release All Platforms" snap build failing at upload with exit 64 The Launchpad snap build produced no .snap (only build logs), but snapcraft remote-build exited 0 so the workflow reported success; the upload step then globbed wekan_<version>_*.snap, matched nothing, and passed the literal pattern to snapcraft upload ("is not a valid file", exit 64). Hardened the workflow to verify a .snap was actually produced (instead of trusting the exit code alone), print the Launchpad build logs when it was not, and use nullglob

  • existence checks in the upload/attach steps so a no-match fails clearly instead of cryptically. Also made releases/version.sh set snapcraft.yaml's version: robustly — matching ^version: rather than a hard-coded line number and tolerating any quoting, then verifying the change applied — so the snap can no longer be built with a stale version that the upload glob cannot match, and the snap job fails fast if the tagged version does not match the release
</details>

and adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/61c8b2fb244dcdfbd097122de6f0250b10a8f151">Build WeKan server bundles for s390x, ppc64le and armv7l</a>. Thanks to xet7.</summary>

Build WeKan server bundles for s390x, ppc64le and armv7l The "Release All Platforms" workflow now also builds the WeKan server bundle for every other architecture Node.js 24.x publishes binaries for — s390x, ppc64le and armv7l — by rebuilding the bundle's native modules under QEMU emulation, and attaches each wekan-<version>-<arch>.zip to the GitHub Release as an extra download. These extra-architecture bundles are not part of the Docker image or snap (run a separate MongoDB on those platforms), and are built independently so a slow or failing emulated build never blocks the main amd64/arm64/win64/mac release, Docker or snap

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.54 2026-06-18 WeKan ® release

This release adds the following updates:

<details> <summary><a href="https://github.com/wekan/wekan/commit/ebc0f017bf2d0a6183a35cdd2b829a0cfff436ed">Updated dependencies</a>. Thanks to xet7.</summary>

Updated dependencies Merged Dependabot dependency updates: mongodb 6.21.0 → 7.3.0 in the tests/playwright e2e harness (#6393), @aws-sdk/client-s3 3.1054.0 → 3.1071.0 (#6390), @google-cloud/storage 7.19.0 → 7.21.0 (#6391), @typescript-eslint/eslint-plugin 8.60.1 → 8.61.1 (#6392), and @tweedegolf/sab-adapter-google-cloud 1.0.10 → 3.0.2 (#6389)

</details>

and adds the following features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/d6a8f4386671630468fa61a8e98c4435c2c97803">Added a "Show at Minicard" option for the Card Settings "Mark as complete" toggle</a>. Thanks to xet7.</summary>

Added a "Show at Minicard" option for the Card Settings "Mark as complete" toggle The "Mark as complete" row in Board Settings → Card Settings now has its own "Show at Minicard" checkbox (new board setting allowsDueCompleteOnMinicard), unchecked by default — so the complete toggle is no longer shown on the current board's minicards unless it is explicitly enabled

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/issues/6385">Fixed Card Settings toggles (e.g. "Mark as complete") could not be reversed without refreshing the…</a> Thanks to xet7.</summary>

Fixed Card Settings toggles (e.g. "Mark as complete") could not be reversed without refreshing the page The Board Settings → Card Settings popup (boardCardSettingsPopup) cached the board once in onCreated as a non-reactive snapshot, but its toggle handlers computed the new value from that snapshot (!tpl.currentBoard.allowsX). So the first toggle worked, but reversing it recomputed !oldValue from the stale snapshot and set the same value again — it only "reset" after a page refresh re-took the snapshot. Fixed by re-reading the board reactively in an autorun, so every Card Settings toggle works both ways without a refresh. The same fix was applied to the Subtask Settings popup, which had the identical pattern

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.53 2026-06-18 WeKan ® release

This release fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/b65ae77fbdb3649d8d743933e6c8778b465578cd">Fixed Playwright e2e tests all failing because Meteor was not a browser global</a>. Thanks to xet7.</summary>

Fixed Playwright e2e tests all failing because Meteor was not a browser global Every Playwright end-to-end test (all 183, across Chromium, Firefox and WebKit) failed at the waitForMeteor step with TimeoutError: page.waitForFunction, because typeof Meteor was undefined in the browser window scope. Under the Meteor 3.5 + rspack build, bare Meteor references in app code are rewritten by rspack's ProvidePlugin into per-module imports, so Meteor is no longer placed on window the way the classic Meteor linker did. The tests call Meteor.loginWithToken / Meteor.userId / Meteor.subscribe etc. via page.evaluate (which runs in window scope), so they could never proceed past login. Fixed by re-exposing window.Meteor = Meteor early in client/00-startup.js, restoring the long-standing classic-Meteor behaviour where Meteor is a global (also handy in the browser console)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.52 2026-06-18 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUE of InputBleed:

<details> <summary><a href="https://github.com/wekan/wekan/commit/8dfb54d9d5d68f120e3aa710c3521a9e9ac9670c">Fixed InputBleed</a>. Thanks to GitHub CodeQL and xet7.</summary>

Fixed InputBleed : incomplete multi-character HTML sanitization in card dependency import allowed HTML/script element injection](https://github.com/wekan/wekan/blob/main/client/lib/importDependencies.js) (CWE-79 Cross-site Scripting, CWE-80 Improper Neutralization of Script-Related HTML Tags, CWE-116 Improper Encoding or Escaping of Output; GitHub CodeQL code scanning alert #421, rule js/incomplete-multi-character-sanitization, severity High). stripHtml() in client/lib/importDependencies.js removed HTML tags with a single pass of /<[^>]*>/g. That is an incomplete multi-character sanitization in two ways: removing one match can splice surrounding text into a new match (so the replacement must be looped to a fixed point), and a dangling, unclosed tag that has no closing > (for example a trailing <script or <svg/onload=...) is never matched by the regex at all and survives untouched — leaving <script in the output, exactly as the scanner warned. A crafted card-dependency ("Red Strings") import file (the WeKan/generic JSON or Miro item titles and connector captions that pass through stripHtml) could therefore smuggle an HTML/script fragment past the sanitizer. Fixed by looping the tag-stripping replacement to a fixed point and then removing any remaining stray </> characters, so neither a complete nor a partial tag can remain

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/0094fd21a0d59bd9f88df17f7dfcf13286f8d65c">Card Details popup</a>. Thanks to xet7.</summary>

Card Details popup : removed the redundant empty second popup that appeared at the top of the page when a card was opened as a popup (for example from the Board Table view Edit link or from search results). The card details content is always position:fixed, so it renders as its own framed box and escapes the generic popup wrapper, leaving that wrapper (with its "Card Details" title header, border and background) visible as an empty box. The wrapper is now collapsed and made invisible so only the card itself shows. The card's own close button now closes the popup (in addition to clicking outside or pressing Escape)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.51 2026-06-17 WeKan ® release

This release adds the following improvements:

<details> <summary><a href="https://github.com/wekan/wekan/commit/2b9eab766726f3953c708878d2deba0f66df2edb">Board Table view</a>. Thanks to xet7.</summary>

Board Table view : the table now has the columns Card, List, Swimlane, Assignees, Members, Labels, Received, Start, Due and End. The Received, Start, Due and End dates are formatted and styled the same way as on the opened card details and the Gantt view (reusing the cardReceivedDate / cardStartDate / cardDueDate / cardEndDate badge templates), and can be sorted by clicking their column headers. Clicking a date opens the date-select popup so it can be changed, and an add (+) button is shown for empty dates — both only for board roles that have permission to change the card (the same canModifyCard check used on card details). A Received, Start, Due or End column is hidden when both its "Show at Card" and "Show at Minicard" Card Settings are unchecked for the board. The Labels cell now word-wraps so long label names no longer overflow across the other columns. Each row also has a leftmost "Edit" link (pencil icon

  • text) styled like the one on the Admin Panel / People page; clicking it opens the Card Details popup on top of the Board Table view
</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.50 2026-06-17 WeKan ® release

This release adds the following improvements:

<details> <summary><a href="https://github.com/wekan/wekan/commit/5dfe6c3deb725d1a7143b84c8872725525f812af">Board Table view</a>. Thanks to xet7.</summary>

Board Table view : render markdown with +viewer in the Card, List, Swimlane and Labels cells, so emoji shortcodes and markdown (for example :thumbsup:) display rendered instead of as literal text, and word-wrap the Card, List and Swimlane cells so long text no longer overflows across the other columns

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.49 2026-06-17 WeKan ® release

This release fixes the following bugs:

<details> <summary>Fix Board Table view not loading with "No such template: tableView" error. Thanks to xet7.</summary>

Fix Board Table view not loading with "No such template: tableView" error. The tableView.jade, tableView.js and tableView.css files of the new Board Table view were never imported in client/features/boards.js, so the tableView template was not bundled and selecting the Table view threw a Tracker recompute exception. Added the missing imports.

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.48 2026-06-17 WeKan ® release

This release adds the following updates:

and adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/86bf48a626e2947836ae7dc5dda65c08d0174552">Board Table view. Part 1</a>. Thanks to xet7.</summary>

Board Table view. Part 1, Part 2 The board view menu (Swimlanes, Lists, Calendar, Gantt) gains a new "Table" entry below Gantt. It shows every card of the current board in a table — Card, List, Swimlane, Members, Labels and Due Date — using the same styling as the My Cards table view (the .my-cards-board-table CSS classes). Whereas My Cards' table spans all of the user's cards across boards, this is the per-board equivalent showing all of the current board's cards. The view is stored like the others as the user's board-view-table boardView. It also has a search box and previous/next pagination styled like the Admin Panel / People page, and Excel-like column sorting: click the Card, List, Swimlane or Due Date header to sort by it, click again to reverse, with an arrow showing the active column and direction. Search, sort and pagination run client-side over the board's already-loaded cards

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/a1281acec09281f7679efa651c0c36e5d7927f1f">List scrollbar disappeared after resizing a list width</a>. Thanks to xet7.</summary>

List scrollbar disappeared after resizing a list width : Fixes #6386. When changing the width of a list by dragging the resize handle between lists, the list's vertical scrollbar (used to scroll up and down through the cards in a list) disappeared. Cause: dragging the resize handle sets an inline --list-width property on the list, which made the resize CSS rules force display: block on the list permanently. That collapsed the flex-column layout so .list-body was no longer height-bounded and its overflow-y: auto scrollbar never appeared. Fixed by keeping the flex column layout (display: flex; flex-direction: column) instead of display: block during and after resize; the explicit inline width still pins the list width

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ad2b969eb5aeea535e0392c9cf81d32a79f11657">Login and register pages are now scrollable to the bottom of the form</a>. Thanks to xet7.</summary>

Login and register pages are now scrollable to the bottom of the form : Tall authentication forms (many login methods, legal notice, language selector, etc.) could overflow the viewport without a reliable way to scroll to all fields. The auth pages render directly into <body>, which already has overflow-y: auto, so the vertical scrollbar now appears whenever the form is taller than the viewport, with a real draggable thumb, placed on the right for LTR and on the left for RTL (since <body> inherits direction from the html dir attribute). Two are added below the dialog so the bottom of the form scrolls fully into view. An earlier attempt forced overflow-y: scroll, which left a non-draggable empty scrollbar track in Chromium and no scrollbar at all in Firefox when the form fit the viewport; using the default overflow-y: auto shows the scrollbar only when there is something to scroll

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/da93bfbca687e4ca9b1ed8798e9f07ec0d31bab7">Drag-to-scroll (dragscroll) now works on the All Boards, My Cards, Login and Register pages and in…</a> Thanks to xet7.</summary>

Drag-to-scroll (dragscroll) now works on the All Boards, My Cards, Login and Register pages and in the board Lists view : Dragging empty space to scroll already worked on the board Swimlanes view but not on these pages. The dragscroll library scrolls whichever element carries the dragscroll class, and that element must be the actual scroll container. On All Boards, My Cards, Login and Register the page scroll container is <body> (the previous dragscroll class on ul.board-list only scrolled in mobile view), so a small shared helper (client/lib/pageDragscroll.js) now toggles the dragscroll class on <body> while those templates are mounted and calls dragscroll.reset(); the library has a dedicated el == document.body branch for whole-page scrolling. In the board Lists view the .board-canvas only received dragscroll when the board had swimlanes, so vertical drag-scroll failed on swimlane-less boards; the canvas now always carries the class. One-finger touch scrolling (client/lib/dragscrollTouch.js) covers these too

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.47 2026-06-17 WeKan ® release

This release adds the following updates:

<details> <summary><a href="https://github.com/wekan/wekan/commit/64aa784b2b2b9f96040aad066101aef3f8444da0">Developer test tooling: "Run ALL tests" now stops an existing dev server on port 3000 instead of…</a> Thanks to xet7.</summary>

Developer test tooling: "Run ALL tests" now stops an existing dev server on port 3000 instead of aborting : build.sh menu option 9 ("Run ALL tests") previously errored out with "Port 3000 is already in use" when a dev server was already running, forcing the user to stop it manually. It now detects and stops the existing Meteor dev server before starting its own

</details>
  • When port 3000 is busy, option 9 finds the existing Meteor dev server (pgrep -f 'meteor run --port 3000') and sends it a normal kill, which also tears down the node child it spawned.
  • It then polls the port for up to 30s, escalating to SIGKILL at the 15s mark and falling back to lsof -ti tcp:3000 (or fuser -k 3000/tcp) to catch anything still holding the port whose command line does not match.
  • Only if the port is still occupied after all that does it print an error and abort; otherwise it reports "Port 3000 is now free" and starts its own server.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7db02489202c560d227a476db24cd761c66a0a00">Fix moving/copying a card silently failing with a 403 validation error</a>. Thanks to xet7.</summary>

Fix moving/copying a card silently failing with a 403 validation error : the move, copy, copy-many and convert-checklist-item card dialogs could leave a card in its original list instead of moving it

</details>
  • Root cause: the dialog's "Done" handler read the target board/swimlane/list by scraping the DOM <select> elements. The reactive boards() helper can transiently return [] while a board subscription re-resolves, leaving the board <select> momentarily option-less, so selectedIndex was -1 and the scraped boardId was undefined. card.move(undefined, …) then failed server-side with ValidationError: Not permitted. Untrusted code may only updateAsync documents by ID [403], and the card silently stayed put. This was a gap in the earlier dialog fix, which bound only the swimlane and list options to the live selection but left the board <select> on the (empty) last-confirmed option.
  • Fix: the Done handler now reads boardId/swimlaneId/listId from the dialog's live reactive selection (selectedBoardId/selectedSwimlaneId/ selectedListId), which stays correct across re-renders, and the board <option selected> attribute is bound to the live selection in all four dialogs for UI consistency.
  • Test hardening: three Playwright/Node E2E tests that flaked under the all-parallel run (move-list-right, add-to-top/add-to-bottom, and the Node E2E second-session list-order check) now wait for the board subscription to populate before acting, instead of reading once after a fixed delay.
<details> <summary><a href="https://github.com/wekan/wekan/commit/19fe2e2b6f21b5206e29dcd568576f001abbf37a">Developer test tooling: run all tests in parallel against a single dev server</a>. Thanks to xet7.</summary>

Developer test tooling: run all tests in parallel against a single dev server : build.sh and build.bat now run all tests in parallel against a single dev server, and fix the WebKit/Docker permission fallout

</details>
  • "Run ALL tests" (menu option 9) now starts one WeKan server on http://localhost:3000 (using .meteor/local) and runs every test job concurrently with a live, refreshing progress display: import regression, Node E2E, and Playwright Chromium + Firefox + WebKit all run against that one server, while the Mocha server-side suite runs at the same time in its own isolated Meteor build dir (.meteor/local-test, via METEOR_LOCAL_DIR) so the two Meteor builds never share .meteor/local. A per-job PASS/FAIL summary and per-job logs (../wekan-alltests-<job>.log) are written at the end. Previously these ran strictly one after another.
  • New menu option 16, "Test Playwright ALL browsers in parallel", runs Chromium + Firefox + WebKit at the same time against a server that is already running on :3000 (WebKit via the Playwright Docker image on Linux arm64, native elsewhere; the others run with --workers=3 on Windows).
  • Each Playwright browser now writes to its own test-results/<browser> output dir so parallel runs do not clobber each other's artifacts, and the WebKit Docker run executes as the host user (--user) so it no longer leaves root-owned files behind. A guard repairs an already root-owned test-results/ that caused EACCES: permission denied, mkdir .../.playwright-artifacts-N.
<details> <summary><a href="https://github.com/wekan/wekan/commit/83963dce80909ae0c6dde512c297619e1213c502">Run ALL tests: start the :3000 server before Mocha so it boots fast again</a>. Thanks to xet7.</summary>

Run ALL tests: start the :3000 server before Mocha so it boots fast again : the parallel "Run ALL tests" flow launched Mocha (in its own .meteor/local-test build) before the :3000 dev server, so two full Meteor builds competed for CPU/disk and the server took a long time to become ready — shown as a long line of dots during the readiness wait. Mocha and the import regression do not need the server, so they are now launched only after the server build is underway; the server builds alone and boots fast again, while they still run in parallel with the E2E and browser jobs. Applied to both build.sh and build.bat

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8e70a2b6a95373125be222534cc2fd4da6c278e8">Fix #6380: login page missing username/password fields after upgrade</a>. Thanks to xet7.</summary>

Fix #6380: login page missing username/password fields after upgrade : the password form is hidden by default in CSS and only revealed by JS when isPasswordLoginEnabled returns truthy; a slow/failed method call or a not-yet-rendered accounts form left the login without username and password fields. It now shows the form unless password login is explicitly disabled, and waits for the form element to appear before showing it

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/67d9de32db8829ae11ad654fee621162368eed36">Fix #6381: make the card "Mark as complete" toggle configurable, hidden by default</a>. Thanks to xet7.</summary>

Fix #6381: make the card "Mark as complete" toggle configurable, hidden by default : a new board setting allowsDueComplete (off by default) controls whether the "Mark as complete" toggle is shown on cards, with a checkbox in the board Card Settings popup to enable it per board

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dd7306d8de1f8c49d2f80b6cb6b49994e7a5a94d">Fix #6382: stop the client auto-creating thousands of empty swimlanes</a>. Thanks to xet7.</summary>

Fix #6382: stop the client auto-creating thousands of empty swimlanes : getDefaultSwimline() inserted a swimlane whenever none was found, but on the client it runs inside reactive renders — for a board whose swimlanes were not yet loaded (e.g. the default subtasks board viewed via "All boards") every re-render inserted another empty swimlane (2008 in the report), freezing the browser. The default swimlane is now auto-created only on the server

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/758f320969ccdbe2026cf1acb9d361906da3f76a">Move/Copy/Convert card dialogs: bind swimlane and list select to live selection</a>. Thanks to xet7.</summary>

Move/Copy/Convert card dialogs: bind swimlane and list select to live selection : the swimlane and list <select> selected option in the move, copy, copy-many and convert-checklist-item card dialogs now follows the live selection instead of the last-confirmed option, so a Blaze reactive re-render can no longer silently revert the user's in-progress choice

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/be44f9c3114fcef82847510058f8ccd014e32109">Playwright: probe browsers and skip ones that cannot launch on the host</a>. Thanks to xet7.</summary>

Playwright: probe browsers and skip ones that cannot launch on the host : the test runner now probes each browser and skips any that cannot launch (e.g. the bundled WebKit needs old system libraries that newer Linux arm64 distros like Ubuntu 26.04 no longer ship), removing false WebKit failures locally while still running every browser on CI. Override with WEKAN_PLAYWRIGHT_PROBE=1/0

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dac356ed061167cdf994bd9f82a849514922a92e">build.sh: platform detection, Docker WebKit on Linux arm64, all browsers in ALL tests</a>. Thanks to xet7.</summary>

build.sh: platform detection, Docker WebKit on Linux arm64, all browsers in ALL tests : detect OS/arch (Linux amd64/arm64, macOS arm64); run the WebKit Playwright specs via the official Playwright Docker image on Linux arm64 where the bundled WebKit cannot launch natively; and run Chromium, Firefox and WebKit in the "Run ALL tests" option

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d5e5df6549f0496e4eaa54675eee2392cbebdd8d">build.bat: Windows menu parity for building, running and testing WeKan</a>. Thanks to xet7.</summary>

build.bat: Windows menu parity for building, running and testing WeKan : the Windows batch script now mirrors build.sh's interactive menu so building, running and testing WeKan (Mocha, import regression, Node E2E and Playwright Chromium/Firefox/WebKit) works on Windows amd64/arm64 too

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6375">Bumped form-data from 2.5.5 to 2.5.6</a>. Thanks to Dependabot and xet7.</summary>

Bumped form-data from 2.5.5 to 2.5.6 : security fix for the CRLF-injection issue (CVE-2026-12143, GHSA-hmw2-7cc7-3qxx) where CR/LF/" in multipart field names and filenames were not escaped. It is a transitive dependency (pulled in via @google-cloud/storage); lockfile-only change

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6376">Bumped launch-editor from 2.13.2 to 2.14.1</a>. Thanks to Dependabot and xet7.</summary>

Bumped launch-editor from 2.13.2 to 2.14.1 : a dev-only dependency (used by webpack-dev-server / @rsdoctor/sdk, not in the production bundle); lockfile-only change

</details> <details> <summary><a href="https://github.com/wekan/wekan/pull/6379">Bumped dompurify from 3.4.6 to 3.4.9</a>. Thanks to Dependabot and xet7.</summary>

Bumped dompurify from 3.4.6 to 3.4.9 : update of the HTML sanitizer used to sanitize card descriptions, comments and other rendered markdown (XSS protection)

</details>

and adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/b909f82f890661103364e591553e85a1edd2bf87">Added card dependency "Red Strings" / PI program board: visualize card-to-card dependencies as red…</a> Thanks to CodeFreezr, dbt4u, helioguardabaxo and xet7.</summary>

Added card dependency "Red Strings" / PI program board: visualize card-to-card dependencies as red, arrow-headed connection lines drawn on top of the board (for SAFe PI-planning program boards). A card now has a cardDependencies list edited from a new "Dependencies" section in the card detail (pick or remove other cards on the same board), and a board header toggle (showDependencies) renders an SVG overlay that draws a red curve from each card to each of its dependencies, following the live card positions on scroll/resize. The overlay is non-interactive (pointer-events: none) so cards stay clickable. Each dependency is now typed and customizable: a relation type (related-to, blocks, is-blocked-by, fixes, is-fixed-by — the type sets the arrow direction; related-to is undirected), a per-line color (any color, via a color picker, not just red) and an icon (FontAwesome). The card detail "Dependencies" section edits all three (relation type, color, icon picker), with a search-by-title picker to add one; a colored icon+count badge is shown on the minicard; and the board Filter sidebar can filter cards by dependency relation type. A REST API was added (tag Dependencies, documented in the OpenAPI docs and api.py): GET /api/boards/:boardId/dependencies, GET/POST /api/boards/:boardId/cards/:cardId/dependencies and PUT/DELETE /api/boards/:boardId/cards/:cardId/dependencies/:targetId, each accepting type/color/icon. Dependency lines can be exported (Board Settings → Export → Dependencies / JSON and / SVG; the SVG is a standalone, round-trippable diagram) and imported (All Boards → New → Import → Dependencies (JSON/SVG)) into a chosen board, matching cards by id, then card number, then title. Importing a Jira board now maps Jira issuelinks best-effort to dependency relations. Card dependencies and the board's showDependencies toggle are preserved through card/board copy and WeKan board export/import/migrate (target ids are remapped, dangling ones dropped), and a card moved to another board drops its now cross-board dependencies and cleans inbound references. Covered by tests (Part 1, Part 2, Part 3): e2e specs 27-red-strings (overlay, toggle, typed lines, minicard badge, copyCard preservation, import matching) and 28-dependencies-rest (REST CRUD + schema validation), plus mocha unit tests for the metadata helpers, the REST OpenAPI annotations, the filter selector, the cross-board move cleanup and the Jira issue-link mapping. Fixed editing an existing dependency from the card detail throwing a client 403 ("Untrusted code may only updateAsync documents by ID") — changing a relation's type/color/icon or removing it now rewrites the cardDependencies array and updates by _id instead of using a forbidden positional-$ selector update, and fixed the dependency icon picker not applying the chosen icon (the popup now edits the source card, not the dependency row), with an e2e test for editing a dependency's type/color/icon. Added a piplanning.io / Kendis / Miro-style drag-to-connect (Part 1, Part 2 ): when the overlay is on, each minicard shows a small connect handle (right edge, on hover) — drag it onto another card to create a dependency (a dashed guide line follows the cursor) — and a connection line is clickable to change its type/color/icon or delete it. It is not a mode: cards stay clickable and the rest of the overlay is click-through. The Dependencies (JSON/SVG) importer now also best-effort maps Miro REST API data (items + connectors, resolved to card titles; "block"/"fix" captions → relation type); Kendis/piplanning.io (and GitHub/GitLab) have no public dependency format, so a generic { "lines": [...] } JSON interchange is documented for them. Documented in Features/RedStrings. Fixes #3392

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc9e8674d89ec5b045f2e2c1b14fade9e92baf0d">Added an Admin Panel "Shared templates" view grouped by Organization / Team / email Domain</a>. Thanks to xet7.</summary>

Added an Admin Panel "Shared templates" view grouped by Organization / Team / email Domain : a new admin-only "Shared templates" tab under Admin Panel → People lists users' shareable template boards, grouped by Organization, Team or email Domain. The three scope checkboxes are live view filters (default unchecked); checking one or more shows the matching groups, and only users whose Templates board is non-empty are listed. A new admin-only adminSharedTemplates method enumerates each user's linked template boards (the cardType-linkedBoard cards in their Board Templates swimlane) and returns them with the user's orgs/teams/email domains; the boards are shown as links into each template board. Covered by an e2e suite (tests/playwright/specs/26-shared-templates.e2e.js). Documented in Features/Templates. Fixes #3313

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/charts/commit/d1662fc3b91a6ce28d5ea92ec2ad06d0df3e0755">Fixed duplicate MONGO_URL environment variable generated by the Helm chart</a>. Thanks to the reporter and xet7.</summary>

Fixed duplicate MONGO_URL environment variable generated by the Helm chart , which made helm install/upgrade fail with duplicate entries for key [name="MONGO_URL"] when the default env list (which already includes MONGO_URL) was used. The chart now emits its computed MONGO_URL only when one is not already provided via env or secretEnv. Fixes #6289

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/92b0f96387c831839eb299a8feb00fe614419783">Fixed GFM strikethrough (~~text~~) no longer rendering in card descriptions</a>. Thanks to Buo-ren Lin and xet7.</summary>

Fixed GFM strikethrough (~~text~~) no longer rendering in card descriptions : markdown-it renders ~~text~~ to <s>…</s>, but the DOMPurify allow-list did not include s/del/strike, so the sanitizer stripped the tag (keeping the bare text). Those inline tags are now allowed in both DOMPurify configs. Fixes #6008

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/17bb7c1264969a50f778452b37335599e3af0518">Fixed the release pipeline's OpenAPI docs generator crashing on template-literal route paths</a>. Thanks to xet7.</summary>

Fixed the release pipeline's OpenAPI docs generator crashing on template-literal route paths , which failed the GitHub Actions "release-all" bump job (AttributeError: 'NoneType' object has no attribute 'rstrip') when a REST route is registered with a backtick path such as `/api/boards/:boardId/export/${format}`. The generator now resolves such paths (a ${identifier} becomes a {identifier} path parameter) and skips any route whose path cannot be resolved statically instead of aborting the whole release

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d241f9d8f6b20fb9bdc0f829a9ffb5f595c3b8bd">Fixed the Member Settings "Change Avatar" entry rendering in a different (uppercase-looking) style…</a> Thanks to xet7.</summary>

Fixed the Member Settings "Change Avatar" entry rendering in a different (uppercase-looking) style than the other menu items : its label was mis-nested inside the <i class="fa fa-picture-o"> icon element instead of being a sibling of it, so it inherited the FontAwesome icon font styling. The label now sits directly under the menu link like every other entry

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/82192d04703c3b4bc09b7f28b8923d979c3b8831">Hardened the reactive DataCache teardown to re-check for dependents before stopping a still-used…</a> Thanks to xet7.</summary>

Hardened the reactive DataCache teardown to re-check for dependents before stopping a still-used entry : the 60s teardown timeout could stop the computation and delete a value that a dependent re-attached to during the window, surfacing as a transient undefined (a contributor to the "Board not found" flicker)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/309425545ffa91747f1f8d7d8585ec04795e13bf">Fixed board export error responses returning HTTP 200 with an empty body</a>. Thanks to xet7.</summary>

Fixed board export error responses returning HTTP 200 with an empty body : the export endpoints passed a bare number to sendJsonResult, which treats its argument as an options object, so 404/400/403/auth failures returned 200 with no body. They now return the correct status code and a JSON error body

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/29631f6f4b61fcf19ec1517a2ce335f3de7ac4b0">Fixed the board create/delete REST handlers masking errors as success</a>. Thanks to xet7.</summary>

Fixed the board create/delete REST handlers masking errors as success : POST /api/boards and DELETE /api/boards/:boardId caught errors and returned code: 200 with the error as data; they now report the real status code (so e.g. an unauthorized delete returns a 4xx)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5eeedf8744ce344e5258a2c26698b9af66c78142">Hardened board import against out-of-range swimlane/card colors</a>. Thanks to xet7.</summary>

Hardened board import against out-of-range swimlane/card colors : like the earlier board-color fix, a card or swimlane color is now applied only when it is a recognized color value, so a foreign/old export carrying an unknown color can no longer fail collection2 validation and abort the import

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a3d4848c88956e588ce46672c103f266d072f969">Fixed the GitHub Actions Playwright E2E workflow so the Firefox and WebKit browsers can actually…</a> Thanks to xet7.</summary>

Fixed the GitHub Actions Playwright E2E workflow so the Firefox and WebKit browsers can actually run (Firefox/WebKit, mongosh ): the test step now sets WEKAN_PLAYWRIGHT_ALL=1 (so --project=firefox/webkit resolve instead of failing with "Project not found"), WebKit was added to the CI matrix (Playwright's bundled WebKit runs headless on the Linux runner), and mongosh is now installed in the Playwright and Puppeteer-regression jobs (the e2e DB helpers shell out to it, which was failing with spawnSync mongosh ENOENT). npm run test:playwright:all got the same WEKAN_PLAYWRIGHT_ALL=1 fix

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/599687f507b5279156fe93c47a546ec74ba92fef">Translated the remaining untranslated English strings in the Finnish translation</a>. Thanks to xet7.</summary>

Translated the remaining untranslated English strings in the Finnish translation (fi.i18n.json) — the Shared Templates, card-dependency ("Red Strings") and dependency import/export strings — using the existing Finnish terminology

</details>
  • Fix flaky Playwright card/board tests under the parallel run: the new 3-browser parallel run surfaced three load-induced (not product) failures that took DOM snapshots before the UI had settled. Thanks to xet7. Details:
  • 03-cards-operations "move does not create duplicate cards" read the card titles immediately after the move and could catch the card mid-flight (already removed from the source list, not yet rendered in the target). It now waits for the card to be visible in the target list and gone from the source list before snapshotting.
  • 03-cards-operations "add-to-bottom places the card last" polls until the reactive re-sort places the new card last, since submitNewCard only waits for the card to exist, not for its final sort position.
  • helpers/auth.js openBoard now retries up to 5 times at 20s each so the slowest browser (WebKit) survives the contention of the 3-browser parallel run against one shared dev server, instead of failing in test setup.
  • Build scripts: At tests option 9, run option 2 build if .build or node_modules missing. Thanks to xet7.

Thanks to above GitHub users for their contributions and translators for their translations.

v9.46 2026-06-15 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUES of ProxyBleed:

<details> <summary><a href="https://github.com/wekan/wekan/commit/b181889a565254bc9bf79379a34fc7f617ccda28">Fixed ProxyBleed: Header-login IP allowlist bypass via X-Forwarded-For spoofing allows…</a> Thanks to rz1027 and xet7.</summary>

Fixed ProxyBleed: Header-login IP allowlist bypass via X-Forwarded-For spoofing allows unauthenticated full account takeover (incl. admin) (GHSA-jggc-qvfc-jr6x, CWE-290 Authentication Bypass by Spoofing, CWE-348 Use of Less Trusted Source). WeKan's header-login (reverse-proxy SSO) feature gates passwordless login on a source-IP allowlist (HEADER_LOGIN_TRUSTED_IPS), but getRequestIp() in server/lib/headerLoginAuth.js derived the source IP from the client-supplied X-Forwarded-For request header (its left-most hop), falling back to the real TCP socket peer only when that header was absent. Because X-Forwarded-For is fully attacker-controlled, an unauthenticated attacker who can reach the app port directly (a second ingress, a published container port, a k8s NodePort, SSRF — i.e. any path that does not traverse the trusted reverse proxy) could send a single GET request with X-Forwarded-For: <an-allowlisted-ip> plus the username header (e.g. X-Auth-User: admin) and be minted a full passwordless login session (meteor_login_token) for any existing user, including admin — complete account takeover and admin impersonation, with no password, token or shared secret. An empty or unset allowlist additionally failed OPEN (it trusted every source). The minted session is a real, persisted resume token that also authenticates the REST API as the spoofed user. Fixed by deriving the source IP from the real TCP socket peer (req.socket.remoteAddress/req.connection.remoteAddress, normalizing IPv4-mapped IPv6) and never from X-Forwarded-For, and by making an empty/unset HEADER_LOGIN_TRUSTED_IPS fail CLOSED so header-login authenticates no one until the trusted reverse-proxy IP(s) are configured (a startup warning is logged in that case). The same findOrCreateHeaderLoginUser / isTrustedHeaderLoginSource helpers back the attachment API, so that path is fixed by the same change. OPERATOR ACTION REQUIRED: if you use header-login you must set HEADER_LOGIN_TRUSTED_IPS to your reverse proxy's IP address(es) — after this change an unset allowlist disables header-login instead of trusting everyone, and the source IP compared against the allowlist is now the real proxy connection, not a forgeable header. For multi-hop proxy deployments an OPT-IN HEADER_LOGIN_TRUSTED_PROXIES list was added: X-Forwarded-For is honored only when the immediate TCP peer is one of those explicitly trusted proxies, and then only the right-most hop that is not itself a trusted proxy (the real client) is matched against HEADER_LOGIN_TRUSTED_IPS — a direct attacker's forged header is still ignored. Covered by regression tests (server/lib/tests/proxybleed.security.tests.js and server/lib/tests/headerLoginAuth.tests.js), documented in Header-Login, and the new env vars were added to docker-compose.yml, start-wekan.sh, start-wekan.bat, Dockerfile, .devcontainer/Dockerfile, releases/virtualbox/start-wekan.sh and the Snap config. Affected Wekan v9.44 and earlier

</details>

and adds the following updates:

  • Improved accessibility across all pages following WCAG 2.1 AA guidelines:
    • Added a visible keyboard focus indicator (:focus-visible outline) for links, buttons and form controls. The global CSS reset previously stripped all focus outlines, leaving keyboard users with no indication of focus (WCAG 2.4.7).
    • Added a "Skip to main content" link that lets keyboard and screen-reader users bypass the header chrome (WCAG 2.4.1 Bypass Blocks).
    • Added landmark roles so assistive technology can navigate page regions: role="main" on the content area, role="navigation" on the header quick-access bar, and role="search" on the global search form (WCAG 1.3.1).
    • Marked modals and popups as role="dialog" with accessible names, set aria-modal="true" on modals, and moved keyboard focus into a modal when it opens and back to the triggering element when it closes (WCAG 2.4.3 Focus Order).
    • Added accessible names (aria-label) to icon-only controls that previously had none: modal/popup close and back buttons, the announcement close button, the sidebar close/back buttons and the global search input and clear button (WCAG 4.1.2 Name, Role, Value).
    • Marked decorative Font Awesome icons inside labelled controls as aria-hidden="true" so they are not announced redundantly.
    • Added a screen-reader-only .sr-only helper class.
  • Reorganized and updated the documentation:
    • Fixed 14 broken relative links in docs/README.md (verified with a link check).
    • Split the monolithic docs/Features/Features.md into focused topic pages under docs/Features/ subdirectories: Boards, Lists, Cards, Members, Keyboard Shortcuts, Admin Panel, WIP Limits, Cleanup and Stats; Features.md is now an index linking to them, and docs/README.md links the new pages.
    • Added documentation for previously-undocumented features: Right-to-Left (RTL) UI, Stickers, Card Locations, Board Background Images, Attachments and File Storage, the card "complete" checkbox, and a new Accessibility feature page (covering both the Admin Panel / Settings / Accessibility info page and the built-in accessibility features above).
    • Updated outdated information based on the CHANGELOG: the Meteor/Node versions (now Meteor 3.x / Node.js 24.x) and the obsolete feature wishlist (Custom Fields, Subtasks, Swimlanes, Gantt, WIP limits, voting and templates are now implemented).

and adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/9c39226a59f8fc156559315b85a1769e109c43e8">Added an accessibility end-to-end test suite</a>.</summary>

Added an accessibility end-to-end test suite that checks the page language, skip link, landmark roles, visible focus, dialog roles, accessible names on icon controls, and the absence of duplicate element ids

</details>
  • Greatly expanded board automation Rules:
    • The Rules page is now a fullscreen page below the top bar (board sidebar → Rules), instead of a cramped popup. It has its own route /b/:id/:slug/rules.
    • Scheduled rules: run rules on a schedule (once on a date, or every day / weekday / week / month at a chosen time), on due dates (set / approaching / overdue), or by card aging ("a card has been in a list for N days"). A SyncedCron job evaluates these every minute. This makes processes such as "archive cards that have been in the Completed column for 90 days" work out of the box.
    • These long-standing automation feature requests are implemented by the above rules work and can be closed: Fixes #1160, Fixes #2476, Fixes #4372, Fixes #5775, Fixes #5825 — IFTTT-style rules including recurring cards (#1160), Trello-Butler- like Scheduled Rules (#2476), rules that set a due date 1 day/week/month ahead via the "set a date relative to now (+N days)" action (#4372), and automated recurring cards/tasks on a daily/weekday/weekly/monthly schedule from the GUI (#5775, #5825).
    • More Trello-Butler-style automations: card buttons (shown on the card) and board buttons (shown in the board header) that run an action on demand, and new actions — sort a list (by due/name/created/modified), move all cards in a list, mark card complete/incomplete, and set a date relative to now.
    • Manage rules: select all / unselect all, delete selected, edit (rename), and a drag-and-drop visual Workflow editor (Jira-like): drag a trigger and an action into a When … → Then … builder to create a rule, drag an action onto an existing rule to change it, and delete rules from the graph.
    • Import / Export rules to JSON (lossless) and CSV (round-trippable); export only the selected rules; a best-effort importer for Trello Butler commands (Trello board exports do not contain Butler rules, so they are pasted in and the supported subset is mapped, with unmapped lines reported).
    • Import visual workflows from n8n and Node-RED (best-effort: the workflow graph's trigger→action edges are mapped to WeKan rules, unmapped nodes reported), with an Import target selector to choose which personal workspace and board the imported rules go into (applies to all importers in the dialog).
<details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added a whole-board import REST API</a>.</summary>

Added a whole-board import REST API (POST /api/boards/import) that recreates a board — including its rules/triggers/actions (workflows) and other data — from a WeKan board export. With the existing export endpoint this enables migrating all boards + workflows + rules from another WeKan over the API; api.py adds importboard and migratefromwekan REMOTE_URL REMOTE_USER REMOTE_PASS. The remote fetch is done client-side, so the server never fetches arbitrary URLs. Covered by an e2e test (export a board with a rule → import it → assert the rule, trigger and action are recreated on the new board) in tests/playwright/specs/23-rest-api-more.e2e.js

</details>
  • Rules REST API to add/edit/remove/list rules (/api/boards/:boardId/rules), documented in the OpenAPI docs and api.py (addrule / editrule / removerule / listrules / getrule).
  • Added a Rules e2e test suite (tests/playwright/specs/20-rules.e2e.js, covering the fullscreen page, creating event rules, import/export, selecting rules, the workflow view and board buttons) and docs (Features/Rules, updated IFTTT page).
<details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added Jira import</a>.</summary>

Added Jira import ("All Boards → New → Import → From Jira"): import boards from a Jira Cloud REST issue-search JSON, similar to the Trello importer. Jira statuses become lists, issues become cards (with labels, due dates and assignees mapped), and an optional automationRules array is imported as WeKan rules

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Import from WeKan, Trello and Jira</a>.</summary>

Import from WeKan, Trello and Jira can now be done with or without mapping members: an "Import without mapping members (map later)" button skips the member mapping step and imports immediately, so members can be mapped afterwards. Covered by tests/playwright/specs/21-import-without-mapping.e2e.js

</details>
  • Rounded out board import/export menus:
    • Excel (.xlsx) board import ("All Boards → New → Import → From Excel"): the spreadsheet is parsed on the server (exceljs) into rows and imported through the CSV creator, so boards can round-trip through .xlsx (the matching Excel export already existed).
    • Board PDF export ("Board Settings → Export → Export board to PDF", and GET /api/boards/:boardId/exportPDF): exports the board title, lists and cards to a PDF (reusing the card PDF builder). Added api.py exportboardpdf.
    • Kanboard import and export: "From Kanboard" in the import menu (columns → lists, tasks → cards, tags → labels) and "Export board to Kanboard JSON" in the export menu (GET /api/boards/:boardId/export/kanboard), which round-trips with the importer.
    • Import and export to/from NextCloud Deck, OpenProject, GitHub, GitLab, Gitea and Forgejo. Implemented generically: each tool has a small parser that normalizes its export/API JSON to a common shape (reusing one import engine) and a formatter that emits the tool's JSON (reusing one export collector). Added to the import and export menus, with a generalized REST API (POST /api/boards/import/:source, GET /api/boards/:boardId/export/:format) and api.py helpers (importboardfrom, exportboardformat) so all boards can be migrated in bulk. Issue trackers (GitHub/GitLab/Gitea/Forgejo) map issues to cards grouped into Open/Closed lists; Deck stacks and OpenProject statuses become lists. Documented in External-Tools.
    • Asana and ZenKit are now fully built-in (import + export menus, REST API and api.py — no external script needed).
    • Together with the existing CSV/Excel import+export and the card PDF export, this completes CSV / Excel / PDF import & export. Fixes #395.
    • Covered by tests/playwright/specs/25-excel-pdf.e2e.js (Excel import + board PDF export). Documented in Excel-and-VBA and Kanboard.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Extended the REST API</a>.</summary>

Extended the REST API so the newer card features are scriptable: the card edit endpoint (PUT /api/boards/:boardId/lists/:listId/cards/:cardId) now accepts stickers, locations and dueComplete (the complete checkbox), documented with api.py examples (setcardstickers / setcardlocations / setcardcomplete)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added e2e tests for previously-untested documented features</a>.</summary>

Added e2e tests for previously-untested documented features (tests/playwright/specs/22-card-features.e2e.js): stickers, card locations, the complete checkbox and WIP limits

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Expanded REST API test coverage</a>.</summary>

Expanded REST API test coverage (tests/playwright/specs/23-rest-api-more.e2e.js): the Rules API (create/list/get/edit/delete), the new card stickers / locations / dueComplete fields, and core CRUD for swimlanes, lists, custom fields, checklists + items and comments

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added a board background image upload/download API</a>.</summary>

Added a board background image upload/download API (the background counterpart of the card-attachment upload API): POST /api/attachment/upload-background and GET /api/attachment/download-background/:boardId (plus the DDP methods api.board.uploadBackground / api.board.downloadBackground). Uploads use the current Admin Panel / Attachments / Default Storage backend and set the image as the board's active background (board-admin gated). Documented in the OpenAPI docs and api.py (uploadbackground / downloadbackground)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added Trello-Butler-style rule variables</a>.</summary>

Added Trello-Butler-style rule variables{cardname}, {cardnumber}, {listname}, {boardname}, {duedate}, {username}, {date} / {time} / {datetime}, etc. — substituted in rule action text (email subject/body, created card/checklist/swimlane names). Fixes #2475

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added visual card aging</a>.</summary>

Added visual card aging : when enabled per board (board settings → "Card aging"), cards that have not been touched for a while are progressively faded based on their last activity, Trello-style. The three fade-tier day thresholds are board-configurable (default 7 / 14 / 28 days). Toggleable and configurable in the board settings and over the card-settings REST API (cardAging, cardAgingDays1/2/3). Fixes #3984

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added accessible reordering</a>.</summary>

Added accessible reordering without drag-and-drop: visually hidden, keyboard-focusable "Move card up/down" buttons on minicards and "Move list left/right" buttons on list headers, for screen-reader and keyboard users. Fixes #459

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">The board's background image is now also shown as the board tile background</a>.</summary>

The board's background image is now also shown as the board tile background on the All Boards list page (reusing the existing board background, with a dark overlay for readability). Fixes #5157

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Fixed the WIP limit counting</a>.</summary>

Fixed the WIP limit counting only the currently visible cards when a filter is active, which let lists exceed their hard WIP limit; it now counts all cards in the list. Fixes #2095

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Fixed the "check all / uncheck all / (un)check item" rule actions crashing</a>.</summary>

Fixed the "check all / uncheck all / (un)check item" rule actions crashing with "Cannot read property 'uncheckAllItems' of undefined" when the named checklist or item does not exist on the card; they now no-op safely. Fixes #5283

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0bce67a3bf301c21ded3eb97e84a3be0010290f">Added e2e tests for the above features and fixes</a>.</summary>

Added e2e tests for the above features and fixes in tests/playwright/specs/24-feature-issues.e2e.js (accessible card/list reordering, visual card aging with default and configurable thresholds, board-delete rule cleanup, and the board background on the All Boards tile)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c8c0438d662426aaf246a6164d3c3b18bd3462f6">Fixed duplicate id="header" attributes rendered inside loops on the My Cards page</a>.</summary>

Fixed duplicate id="header" attributes rendered inside loops on the My Cards page , which produced invalid HTML and broke assistive-technology navigation (WCAG 4.1.1)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/255de2062ff9b54393fd603bfc6cabddb8ede66a">Fixed the My Cards table markup</a>.</summary>

Fixed the My Cards table markup : header cells (<th>) are now wrapped in a <tr> with scope="col", and a caption was added, so the table is announced correctly

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/073842b3c54f07205c5b0a9c519a665f920c768e">Fixed several bugs in the new features above</a>.</summary>

Fixed several bugs in the new features above that were surfaced by running the full test suite (./build.sh → "Run ALL tests"), so the Playwright suite is green again:

</details>
  • Board JSON export returned empty lists, swimlanes and rules. A previous fix that made attachment export use meta.boardId accidentally applied the same selector to lists/swimlanes/rules, which store a flat boardId, so those came back empty — and the whole-board import API then had nothing to recreate. Attachments now use their own selector. (models/exporter.js)
  • Export, PDF export and board-delete REST endpoints required a global site admin (Authentication.checkUserId) instead of board-level access, so a board owner using their own API token got 403/empty responses and board deletion silently did nothing (its rule/trigger/action cleanup never ran). They now require only a logged-in user and rely on the existing per-board canExport / board-admin checks. (models/export.js, models/exportPDF.js, server/models/boards.js)
  • Rules did not appear on the fullscreen Rules page. The boardRules publication passed the user id string to Board.isVisibleBy(), which expects a user object, so a board's rules/triggers/actions were never published. (server/publications/rules.js)
  • Card edit and checklist REST endpoints returned 500. PUT /api/boards/:boardId/lists/:listId/cards/:cardId with locations stored entries without the schema-required _id (and without numeric coordinates), and the checklist POST referenced a permission helper it never imported. (server/models/cards.js, server/models/checklists.js)
  • Excel/CSV import crashed on spreadsheets without date columns (it read .length of a missing cell); date columns are now treated as optional. (models/csvCreator.js)
  • Importing a board whose export carried a non-WeKan board color aborted with a 400. A WeKan/Trello export can contain color: "bgnone", which is not an allowed board color, so collection2 rejected the board insert and the import never navigated to the new board. The imported color now falls back to the default when it is not a recognized WeKan color. The post-import username bookkeeping was also hardened to only record usernames on users that actually exist. (models/wekanCreator.js)
  • Accessible "move card up/down" and "move list left/right" buttons did nothing. The buttons are visually hidden with an .sr-only container that clips them to a 1px box (clip: rect(0,0,0,0)), so a click resolved (via hit-testing) to the element behind them and the Blaze handler never fired. They are now kept in normal flow and hit-testable while still visually hidden (a transparent, focusable control), so the reorder handlers actually run. (client/components/main/layouts.css)
  • Board/card automation buttons never rendered. The board-button/card-button helpers joined a published rule to its triggers document to read the button type/label, but the schemaless triggers collection's documents do not reach the client over the board subscription in this Meteor 3 setup, so the lookup found nothing. The button type/label are now denormalised onto the (schema-backed, reliably-published) rule document by a new board-admin-gated rules.createRule server method, and the header/card read them straight from the rule. (server/rulesButton.js, models/rules.js, client/components/rules/actions/boardActions.js, client/components/rules/boardButtons.js, client/components/rules/cardButtons.js)
  • Hardened several tests that were checking the wrong state rather than a real product bug: the accessibility test now polls for the <html lang> attribute (set by client JS on startup) instead of reading it once before the page settles; the board-background tile test selects the "Remaining" boards menu where the unstarred seeded board appears; and the list-menu helper and a few card/list reads now poll/retry through a transient, pre-existing "Board not found" re-render that can briefly tear down the board view while its subscription settles. Thanks to xet7.

Known issues / possible later fixes (not addressed in this release):

<details> <summary>Transient "Board not found" flicker on an open board.</summary>

Transient "Board not found" flicker on an open board. While a board's subscription re-settles, the reactive board read (Utils.getCurrentBoard()ReactiveCache.getBoard → the memoizing DataCache) can momentarily return undefined while the subscription is still ready(), so client/components/ boards/boardBody.jade briefly shows board-not-found and tears down the lists/ cards before the board re-renders. Real users on slow/reconnecting links can see a flash and lose scroll/popup state. The test suite was made resilient to it, but the underlying flicker is unfixed. A safe fix needs care because the obvious options have trade-offs: a "was-ever-present" latch in the board template would replace the flash with a spinner but still unmount the lists; making getCurrentBoard() sticky would keep the board mounted but return a stale board after a genuine delete/archive (global behavior change); and hardening DataCache (not storing a transient undefined over a present value) risks masking legitimate removals across every reactive read. Same root cause as the board/card automation-button flicker fixed above (worked around there by reading Minimongo directly). (The DataCache 60s-teardown contributor to this flicker has since been hardened — see the Upcoming release fixes above.)

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.45 2026-06-14 WeKan ® release

This release adds the following updates:

  • Made the "Release All Platforms" snap build resilient to transient Launchpad failures. snapcraft remote-build intermittently dies with a transient TLS drop (SSLEOFError: EOF occurred in violation of protocol, exit 70) while talking to Launchpad after pushing the repo — unrelated to the build itself — which failed the whole release. The step now retries the remote build up to three times before giving up. Thanks to xet7.

Thanks to above GitHub users for their contributions and translators for their translations.

v9.44 2026-06-14 WeKan ® release

This release adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/2536861b797fa971edff53b22420f744f8347118">Full right-to-left (RTL) UI for every page when an RTL language is selected</a>. Thanks to xet7.</summary>

Full right-to-left (RTL) UI for every page when an RTL language is selected (Arabic and its variants, Persian/Farsi, Hebrew, Uyghur, Uzbek-Arabic and Yiddish — the languages flagged rtl: true in imports/i18n/languages.js). Previously only some pages flipped. The root <html> element now gets a reactive dir="rtl"/dir="ltr" attribute that follows the chosen language, so the whole UI mirrors at once. To make this work on every page rather than a handful, all directional component CSS was converted to CSS logical properties (margin-leftmargin-inline-start, left:inset-inline-start:, text-align: lefttext-align: start, float: leftfloat: inline-start, and the mirror-image right/end variants) across 48 stylesheets, which flip automatically with dir. Horizontal-centering rules using left: 50% + transform: translate(-50%, …) were intentionally kept physical, since those already center correctly in both directions. The calendar view also renders RTL

</details> <details> <summary>Tests for the RTL UI. Thanks to xet7.</summary>

Tests for the RTL UI. A fast, server-less tests/rtl.test.js (run with node tests/rtl.test.js) checks that exactly the expected languages are flagged rtl: true, that the direction helper maps each language to the right dir value, that the root <html> and the client keep dir in sync, and — as a regression guard — that component CSS keeps using logical properties (no physical margin-left/float: left/bare left: offsets sneak back in). A Playwright browser spec tests/playwright/specs/18-rtl-layout.e2e.js drives the app in both English (LTR) and Arabic (RTL) and asserts, on the boards list, board view, card details, my-cards / due-cards / global-search, the admin settings page and the login page, that the direction is correct, the translated text is visible, and leading-edge content (the boards menu, the first board list) sits on the correct side.

</details> <details> <summary>Fixed the "Meteor unit tests" CI job hanging until its 45-minute timeout. Thanks to xet7.</summary>

Fixed the "Meteor unit tests" CI job hanging until its 45-minute timeout. The workflow set TEST_WATCH: '0', but meteortesting:mocha computes testWatch: TEST_WATCH || … and the string '0' is truthy in JavaScript, so it turned on watch mode and the process never exited. Removing the env var lets meteor test --once finish and exit with the correct status.

</details> <details> <summary>Fixed the GitHub Pages (docs) build failing with a Liquid syntax error. Thanks to xet7.</summary>

Fixed the GitHub Pages (docs) build failing with a Liquid syntax error. The webhook reference page docs/Webhooks/Webhook-data.md documents payloads full of {{placeholder}} tokens, and two had a typo ({{swimlane-id} with one closing brace) that aborted Jekyll's Liquid parser. The placeholder-heavy doc pages (Webhook-data, Translations, Troubleshooting-Mail, Original-Positions) are now wrapped in {% raw %}/{% endraw %} so Jekyll emits the template tokens literally instead of trying to evaluate them — fixing the build and making the placeholders render correctly.

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.43 2026-06-13 WeKan ® release

This release adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/e63c5b112d517b259fdc5347444914b9d86fd71f">Add/Edit location popup can also</a>. Thanks to xet7.</summary>

Add/Edit location popup can also detect a location from a map link from many providers, grouped by region — USA: Google Maps, Bing Maps, Apple Maps, Waze; Europe: OpenStreetMap, HERE WeGo, Yandex Maps, Mapy.cz, 2GIS; Asia: Baidu Maps, Amap (Gaode) — plus generic ?q=/?ll= links: paste the link, press "Detect", and the latitude, longitude and (when present) the place name/address are filled in automatically. Detection handles each provider's real-world URL shapes, including percent-encoded commas (e.g. Waze/Yandex ll=...%2C...) and alternate forms (HERE share.here.com/l/, Yandex pt=, 2GIS m=), and the coordinate order (several non-US providers use lon,lat). For the Chinese providers a datum conversion is applied — Baidu uses BD-09 and Amap uses GCJ-02, both offset from WGS-84 — so pins land in the right place on the way in and out. The popup also has an "Open map links at" setting (OpenStreetMap by default) saved to the user profile, with the same region-grouped provider list, controlling which map service the location "Open in map" links use

</details>

and fixes the following bugs:

  • Fix incomplete URL substring sanitization.

Thanks to above GitHub users for their contributions and translators for their translations.

v9.42 2026-06-13 WeKan ® release

This release adds the following new features:

  • Greatly improved import from Trello to WeKan:
    • Attachments now import. Trello attachment URLs require OAuth to download, so WeKan could not fetch them. The Trello import page now has two separate file fields: a .json field for a single Trello board, and a .zip field for a package produced by the Trello Card Attachments Downloader. The .zip may contain one or more board .json files together with each board's attachments in a board-name subfolder. The .zip is uploaded over HTTP and processed entirely on the server (the attachment bytes never travel over the realtime WebSocket, which previously overflowed the connection and made the page flicker): WeKan detects every board .json, matches each board to its attachment subfolder by name, streams each file straight into the Default file storage, imports all boards, then opens All Boards. The upload is guarded against abuse — it rejects oversized uploads, zip bombs (too many files or too much uncompressed data, per-file size caps) and unsafe entry paths (absolute paths or .. traversal), and never overwrites existing files (stored under generated ids). The importer also reads attachments from card.attachments (newer Trello exports), not only from the action log, and uses the correct Meteor-Files insert API (the previous calls were no-ops on Meteor 3). Single board imports (the .json file or the pasted JSON, after the map-members step) now run through the same server-side HTTP path rather than the realtime importBoard method, so a heavy import can no longer drop the WebSocket and get retried in an endless loop (the page flickering with "Invalid frame header" / repeated "logged out" errors). When importing only a single board .json and you have saved Trello API key and token, WeKan uses them on the server to download that board's attachments, background image, member avatars and card stickers from Trello (a .json export often omits stickers; avatars only fill in mapped users who have none — an existing avatar is never overwritten); each download is best-effort and a failure never aborts the import. After a single-board import the new board's data is subscribed and loaded before opening it, so it shows its swimlanes, lists and cards immediately instead of needing a browser page reload (the HTTP import, unlike the old realtime method, does not push the written documents to the client by itself).
    • Import Trello-only card features: stickers (shown as Font Awesome icons on minicards and the card detail), card cover (color or attachment), card location (name, address, coordinates with an OpenStreetMap link), and the due-date "complete" checkbox shown on the minicard. Trello's two named sticker packs are renamed and highlighted (the icon keeps its normal colour) so they stay distinct: the "taco" pack becomes "mascot" with an underlined icon, and the "pete" pack becomes "computer" with a ring around the icon, e.g. "pete-ghost" imports as "computer ghost" with a Snapchat-ghost icon. Several named stickers that used to share an icon now get their own Font Awesome 4.7 icon (mascot active => heartbeat, pixel => qrcode, proto => flask, embarrassed => meh-o, clean => bath, computer shipped => truck).
    • Live Trello API import: paste your Trello API key and token on the Trello import page to list all your Trello workspaces and boards, then import all or selected boards together with their attachments (downloaded server-side). Imported boards are placed under a personal workspace named after their Trello workspace, created by name if it does not exist, under an optional parent workspace you choose. The API client respects Trello's rate limits: it spaces out requests, honours Retry-After on HTTP 429, retries transient 5xx and network errors with capped exponential backoff, and reports a clear error on an invalid key/token (401). After a successful API import you are taken to the All Boards page where the new boards appear.
    • The live API import runs as a background job on the server: you can navigate away and come back to the Trello import page to watch progress, see per-board results, and read the full error log. Errors are shown in a selectable text box with a one-click "Copy to clipboard" button (and you can still select and copy just part of the text) so they are easy to share when fixing issues. If the import stops on a fatal API error (invalid token, or rate limit still failing after retries) you can fix the cause and Resume from where it left off, and a server restart leaves the job paused for resuming rather than lost. You can also Cancel the import, or Cancel and delete the boards already imported, to start the whole process over cleanly.
    • You can optionally save your Trello API key and token with the Save and Delete buttons on the import page. When saved, they are stored only on the server, never sent back to the browser, and are reused automatically when you list workspaces or import (so you do not have to paste them again); a "saved" indicator is shown, Save can overwrite them with new values, and Delete clears them from the database. If you do not save them, they are kept only in server memory for the running import and re-supplied when you resume.
    • Larger Trello JSON exports can be selected as a file instead of pasted.
    • Keep imported usernames for later mapping. Imported members that match an existing WeKan user (by username, or by a previously recorded imported username) are mapped automatically, and the imported username is remembered on that user so future imports map too. Members with no matching WeKan user are no longer lost: their usernames are kept on the board, so an admin can map them to real users later in Admin Panel / People (Imported Usernames), without having to map everyone up front at import time. Thanks to xet7.
<details> <summary>Board background images can now be stored in WeKan. Thanks to xet7.</summary>

Board background images can now be stored in WeKan. A new backgrounds file storage directory is created alongside attachments and avatars (using the current default storage backend), and Board menu → Board backgrounds lets a board admin upload background images, set one as the active board background, download, and delete them. Board export now includes the board's background images and re-imports them, and a Trello board's background image is downloaded and stored on import (so it keeps working even if the original Trello URL later changes).

</details> <details> <summary>Stickers can be added to and removed from cards directly in WeKan, chosen from a set of Font… Thanks to xet7.</summary>

Stickers can be added to and removed from cards directly in WeKan, chosen from a set of Font Awesome icons similar to Trello's stickers (previously stickers only arrived via Trello import). The picker also includes every "mascot" (underlined) and "computer" (ringed) highlighted sticker that a Trello import can produce — generated from the same icon mapping the importer uses — so any imported sticker can also be added by hand. Stickers show on the minicard and in the card detail.

</details> <details> <summary>The Trello-style "complete" checkbox (mark a card complete/incomplete, independent of the due date)… Thanks to xet7.</summary>

The Trello-style "complete" checkbox (mark a card complete/incomplete, independent of the due date) is shown as an animated green checkbox to the left of the card title, both on the minicard and in the opened card, with "Mark as complete" / "Mark as incomplete" tooltips. It uses the same animated checkbox style as Admin Panel / Settings / Announcements and is vertically centered with the title text, and the two stay in sync. The minicard and the opened-card checkboxes use the same checked style (the board theme's colour, or green when no theme is set), so they always look identical. Subtask checkboxes and the card-detail custom-field checkbox now use the same animated checkbox as checklist items (they previously used static square icons), so every checkbox on a card animates consistently.

</details> <details> <summary>Cards can now have multiple locations, similar to multiple members. Thanks to xet7.</summary>

Cards can now have multiple locations, similar to multiple members. The card detail shows a Location section (after Labels and Stickers) listing each location with its name, address and an OpenStreetMap link, with an "Add location" button to add more and a button to edit or remove each one. A single location imported from Trello keeps working and is shown in the same list. The Add/Edit location popup can also detect a location from any map link (Google Maps, OpenStreetMap, Bing Maps, Apple Maps, or generic ?q=/?ll= links): paste the link, press "Detect", and the latitude, longitude and (when present) the place name/address are filled in automatically. The popup also has an "Open map links at" setting (OpenStreetMap by default, or Google/Bing/Apple Maps) saved to the user profile, controlling which map service the location "Open in map" links use.

</details> <details> <summary>The opened card now docks to the top of the window, overlaying the global and board header bars…</summary>

The opened card now docks to the top of the window, overlaying the global and board header bars, instead of opening downward from the clicked minicard, and it can be dragged all the way to the top without its top hiding behind those bars. (At 100% zoom the board wrapper no longer sets a transform: scale(1), which had made it the containing block for the fixed card and trapped it below the headers.) Thanks to xet7.

</details>

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/68e0032c6d3b23a195b4051e2853465965e073dc">Fix Board Export/Import error</a>. Thanks to titver968 and xet7.</summary>

Fix Board Export/Import error Fix import/clone of inconsistent board JSON so the newest WeKan can import board exports from any newer or older WeKan version. A syntactically valid export can still be internally inconsistent (a board member whose user account was deleted is no longer in the export's users, cards pointing at a missing listId, orphaned checklists, or a missing lists array). Such exports previously failed import as "error-json-malformed" or silently dropped data (cards with an undefined list never rendered). The importer now skips dangling user references in the member mapper (client wekanMembersMapper, server wekanmapper used by board cloning, and WekanCreator), falls back to the first imported list — creating one default list when the export has none — for cards with a dangling listId, and skips orphaned checklists whose card is missing. The importer remains version-agnostic: it never reads _format, normalizes old id vs _id fields, and ignores the source boardId. Added tests/wekanCreator.inconsistent.test.js covering these cases

</details> <details> <summary>Fix Trello import returning HTTP 500 on any card that has an attachment. Thanks to xet7.</summary>

Fix Trello import returning HTTP 500 on any card that has an attachment. A past refactor removed the links variable declaration but left its consumer in models/trelloCreator.js, so importing an attachment card threw "ReferenceError: links is not defined". Also fixed a stray return that silently aborted importing the rest of the cards when one attachment URL was blocked (now continue), and Trello cards no longer all import as card number #0 (the Trello short number idShort is preserved).

</details> <details> <summary>Fix attachments not importing from WeKan board JSON exports. Thanks to xet7.</summary>

Fix attachments not importing from WeKan board JSON exports. The importer called the old Meteor-Files Attachments.load/insert(..., cb, true) API, which is a no-op on Meteor 3, so exported base64 attachments were silently dropped. It now inserts them with Attachments.insertAsync, and one failed attachment no longer aborts importing the rest of the cards.

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.41 2026-06-12 WeKan ® release

This release adds the following new features:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.40 2026-06-11 WeKan ® release

This release fixes the following bugs:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.39 2026-06-11 WeKan ® release

This release fixes the following bugs:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.38 2026-06-11 WeKan ® release

This release fixes the following bugs:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.37 2026-06-11 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUES of BoardBleed:

<details> <summary><a href="https://github.com/wekan/wekan/commit/d369a3614a4737c29d48a6345a790edf2506ddae">Fixed BoardBleed</a>. Thanks to 0xzap and xet7.</summary>

Fixed BoardBleed : Broken access control lets any authenticated user move their Cards/Lists/Swimlanes into a private board they are not a member of (cross-board write via collection allow rule)](https://github.com/wekan/wekan/security/advisories/GHSA-gm7v-pc38-53jr) (CWE-284, CWE-639). WeKan boards are membership-scoped, but the DDP collection write policies for Cards, Lists and Swimlanes (server/permissions/cards.js, server/permissions/lists.js, server/permissions/swimlanes.js) authorized an update by checking only the CURRENT (pre-update) boardId of the document — i.e. the attacker's own source board — and never validated the NEW boardId supplied in the update modifier. Because every logged-in user can create a board where they are admin, an attacker could take a document they own and, in a single /cards/update, /lists/update or /swimlanes/update DDP call, $set its boardId (plus swimlaneId/listId) to a victim's private board: the allow rule saw the attacker's own source board, approved the write, and the document was relocated into a board the attacker is not a member of and cannot even read. This let an unprivileged user inject arbitrary cards/lists/swimlanes (attacker-controlled titles, descriptions, assignees, etc.) into any private board by id, defeating board-level access control. The REST API for the same operation (PUT /api/boards/:boardId/lists/:listId/cards/:cardId with newBoardId) was not affected because it correctly calls Authentication.checkBoardWriteAccess(req.userId, newBoardId) on the destination board; only the DDP allow/deny layer was vulnerable. Fixed by adding a denyCrossBoardMove helper in server/lib/utils.js and a Cards.deny/Lists.deny/ Swimlanes.deny update rule on each collection that rejects any update whose modifier $sets a boardId on which the caller does not have write access, so a cross-board move is only allowed into a destination board where the user is an active write-capable member. Affected Wekan v9.35 and earlier

</details>

and adds the following updates:

Thanks to above GitHub users for their contributions and translators for their translations.

v9.36 2026-06-10 WeKan ® release

This release fixes the following CRITICAL SECURITY ISSUES of TokenBleed:

<details> <summary><a href="https://github.com/wekan/wekan/commit/08ae61161cd9602f79f441e3922ffe890b3f11de">Fixed TokenBleed: unauthenticated login-token minting via un-awaited auth check in POST…</a> Thanks to Zion Boggan and xet7.</summary>

Fixed TokenBleed: unauthenticated login-token minting via un-awaited auth check in POST /api/createtoken/:userId (CWE-863, CWE-287). Authentication.checkUserId in server/authentication.js is an async function, so its 401 (undefined userId) and 403 (not an admin) throws become rejected promises rather than synchronous exceptions. The REST handlers in server/models/users.js and server/models/boards.js called it without await inside a plain synchronous try/catch, which cannot catch a rejected promise, so the failed check never stopped execution. POST /api/createtoken/:userId then went on to mint and return a usable login token for any user ID in the URL — including an admin — with no credentials at all (unauthenticated account takeover). The same detached-rejection bypass also affected GET /api/users, GET /api/users/:userId, PUT /api/users/:userId, POST /api/users/, DELETE /api/users/:userId, POST /api/deletetoken, GET /api/boards, GET /api/boards_count, DELETE /api/boards/:boardId, GET /api/users/:userId/boards and POST /api/boards/:boardId/copy. Fixed by awaiting every async Authentication check (and making the two non-async handlers async) so a failed check rejects before any privileged code runs. The same un-awaited pattern in the board/card/Excel/PDF export handlers (models/export.js, models/exportExcel.js, models/exportExcelCard.js, models/exportPDF.js, which were backstopped by exporter.canExport()) and in the checklist-create handler (server/models/checklists.js) was given the same await pass. Affected Wekan v9.35 and earlier

</details>

and adds the following updates:

and fixes the following bugs:

<details> <summary><a href="https://github.com/wekan/wekan/commit/3fe8f602c8c5ade128f5d7e439bff47f51a28045">Fix LDAP_SYNC_ADMIN_GROUPS</a>.</summary>

Fix LDAP_SYNC_ADMIN_GROUPS so that admin status sync and group/role sync no longer require LDAP_GROUP_FILTER_ENABLE=true, which only controls the login restriction filter Thanks to xet7

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2931cc990f99a831e424870fb56a7a09d12f95b2">Expand and fix REST API</a>.</summary>

Expand and fix REST API (Admin API, board member, card field, card copy/move). REST routes live in server/models/*.js (using WebApp.handlers.get/post/put/delete), their schemas in models/*.js, OpenAPI docs are generated by openapi/generate_openapi.py into public/api/wekan.yml, and api.py is the Python CLI wrapper. api.py already gained CLI wrappers for the endpoints that exist today: addboardmember, removeboardmember, setboardmemberrole, setcardmembers, setcardassignees, setcarddate, setcardlabels, movecard

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5998">#5998</a>. Thanks to xet7.</summary>

Issue #5998 : Add/remove user to board with a role (BoardAdmin, Normal, etc). Already exists server-side: POST /api/boards/:boardId/members/:userId/add and POST /api/boards/:boardId/members/:userId/remove (action=add/remove), and POST /api/boards/:boardId/members/:memberId to change an existing member's permission flags (isAdmin, isCommentOnly, isWorker, isNoComments, isNormalAssignedOnly, isCommentAssignedOnly, isReadOnly, isReadAssignedOnly). api.py now wraps these with a friendly ROLE name → flags mapping. Implemented: the API itself now also accepts a single named role (admin/normal/commentOnly/worker/readOnly/normalAssignedOnly/ commentAssignedOnly/readAssignedOnly/noComments), mapped to the permission booleans server-side by boardMemberRoleToFlags (server/lib/utils.js). When role is present it wins over the individual flags; an unknown role returns 400. Members are identified by userId only. Wired into both POST .../members/:userId/add and POST .../members/:memberId

</details> <details> <summary>Add/Remove board member to card as card member or card assignee. Thanks to xet7.</summary>

Add/Remove board member to card as card member or card assignee. Already exists via PUT /api/boards/:boardId/lists/:listId/cards/:cardId with members / assignees (array, or '' to clear). api.py now wraps these as setcardmembers / setcardassignees. These REPLACE the list. Implemented: merge-style endpoints to add/remove a single member or assignee (POST/DELETE /api/boards/:boardId/lists/:listId/cards/:cardId/members/:memberId and .../assignees/:assigneeId), using $addToSet/$pull so callers don't read-modify-write the whole array. Adding validates board membership (canAssignCardMember) and rejects non-members with 400; removing a stale id is allowed. Wrapped in api.py as addcardmember/removecardmember/ addcardassignee/removecardassignee.

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5897">#5897</a>. Thanks to xet7.</summary>

Issue #5897 : Create Linked Card. Linked cards reference another card; the data model uses linkedId / type: 'cardType-linkedCard'. Implemented by reusing the existing POST /api/boards/:boardId/lists/:listId/cards route: when linkedId is present in the body the new card is created via Card.link (type cardType-linkedCard) instead of holding its own content. Linking across boards is allowed; the caller must have write access to the destination board and read access to the linked card's board (checkBoardAccess). Wrapped in api.py as linkcard

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5846">#5846</a>. Thanks to xet7.</summary>

Fix Issue #5846 : Add/Remove card dates (Received/Start/Due/End). Setting dates already worked via PUT card with receivedAt, startAt, dueAt, endAt, but the handler only wrote a date when the value was truthy (if (req.body.receivedAt)), so an empty string could NOT clear a date — removing a date via the API did not work. Fixed in server/models/cards.js: whenever a date field is present in the request body, an empty string / null / "null" now $unsets the date and any other value sets it. api.py wraps this as setcarddate ... DATETYPE [DATEVALUE] (omit DATEVALUE, or pass an empty string, to clear)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5819">#5819</a>. Thanks to xet7.</summary>

Issue #5819 : Bulk add/remove labels with API, and BoardAdmin label management in the right sidebar Labels hamburger/trigram menu. PUT card labelIds REPLACES the whole label set (wrapped as setcardlabels). Implemented: a multi-card bulk endpoint POST /api/boards/:boardId/cards/labels taking {cardIds, addLabelIds, removeLabelIds} that MERGES — existing labels are kept, removeLabelIds are dropped, addLabelIds are added, de-duplicated, across all listed cards in one request. addLabelIds are validated against the board's labels (request rejected with the offending ids otherwise); cards not on the board are reported in notFound. Wrapped in api.py as bulkcardlabels. Board label creation via PUT /api/boards/:boardId/labels is now gated to BoardAdmin (allowIsBoardAdmin); normal members can still apply existing labels to cards (including via the bulk endpoint). Still planned: the right-sidebar Labels hamburger/trigram menu BoardAdmin-only UI

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/5813">#5813</a>. Thanks to xet7.</summary>

Fix Issue #5813 : Card number is not unique when concurrently creating multiple cards via REST API; many cards got the same card number. Root cause: Board.getNextCardNumber() (models/boards.js) read the current max cardNumber and returned max + 1; two concurrent card creations read the same max and both got the same number — a classic read-then-increment race. Fixed by allocating card numbers from an atomic per-board counter (Counters.incrementCounterAsync('cardNumber-<boardId>') in models/counters.js), a single atomic findOneAndUpdate($inc) that is safe under concurrency. Existing boards (and imported boards with existing cards) are lazy-seeded on first use: the counter is initialized to the board's current max cardNumber via an idempotent $setOnInsert upsert, so no migration is needed and no number an existing card already has is reissued. Card numbers may have gaps (deleting a card does not decrement the counter), which is intended. Numbering stays per-board. The client keeps the old max + 1 read (card numbers are not authoritative there; the server insert recomputes). See also #4743 below

</details> <details> <summary>Copy/Move Swimlane/List/Card to the same or a different board, before/after a position counted… Thanks to xet7.</summary>

Copy/Move Swimlane/List/Card to the same or a different board, before/after a position counted (number of swimlanes/lists/cards) from the top-left. Implemented: target position is a 0-based index counted from the top-left ("after N items"); the server converts it to a sort value between siblings via computeSortForIndex (server/lib/utils.js). New endpoints: POST /api/boards/:boardId/lists/:listId/cards/:cardId/copy, POST /api/boards/:boardId/swimlanes/:swimlaneId/copy and .../move, POST /api/boards/:boardId/lists/:listId/copy and .../move. Copy is a FULL deep copy via the existing model copy() methods (cards, checklists, attachments, custom-field values). Destination board write access is required (may differ from source). Wrapped in api.py as copycard, copyswimlane/moveswimlane, copylist/movelist. NOTE: List.move merges into an existing same-titled list on the destination board when one exists (a pre-existing model behavior); same-board list move is a pure reposition.

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/4815">#4815</a>.</summary>

Issue #4815 : API to get My Cards and Due Cards (also needs a user-scoped API). The web UI already has My Cards and Due Cards views. Implemented: a single GET /api/user/cards endpoint returning the current user's cards (where they are a member or assignee), with a ?due=true filter for cards that have a due date and an optional ?from=/?to= (ISO 8601) due-date range. Returns a compact minicard-like field set, sorted by due date. Wrapped in api.py as mycards. (Cross-user/admin querying, board filter and pagination were not requested for the first version; revisit if needed for users in very many boards.) Thanks to xet7

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/4811">#4811</a>.</summary>

Issue #4811 : After adding a card via the API, does the card count update properly? To verify: GET /api/boards/:boardId/cards_count and GET /api/boards/:boardId/lists/:listId/cards_count after POST card creation. Card counts are computed from the cards collection, so they should reflect API-created cards; this needs a regression test confirming counts update immediately after API create/delete (and are not stale due to caching)

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/3062">#3062</a>. Thanks to xet7.</summary>

Issue #3062 : API for the Card Settings that live under Board Settings. Board Settings card defaults (which fields/badges show on cards and minicards) are the board's allows* toggles. Implemented: GET/PUT /api/boards/:boardId/cardSettings exposing those board-level allows* settings (read requires board access; write requires board write access). PUT accepts any subset of the recognized keys and ignores unknown ones. Per-user card presentation settings are intentionally out of scope. Wrapped in api.py as getcardsettings/ setcardsetting

</details> <details> <summary><a href="https://github.com/wekan/wekan/issues/4743">#4743</a>. Thanks to xet7.</summary>

Fix Issue #4743 : Using the REST API to manipulate many cards crashes WeKan (100% CPU, server becomes unreachable/unusable, REST calls time out with HTTP 408). Reported workflow: every night delete all cards in a board and recreate them (create card → edit with custom fields/dates/labels → list all → delete all), done in a tight loop with no delay; WeKan eventually pegs CPU and stops responding. Same root area as #5813 (concurrent card creation), plus the cost of per-card cascade work (activities, server-side reactivity, custom-field defaults, before/after hooks) under bursts. Implemented so far: (1) atomic card numbering (see #5813) removes the hot read-modify-write contention; (2) bulk create/delete endpoints POST /api/boards/:boardId/lists/:listId/cards/bulk (body {authorId, swimlaneId, cards:[...]}) and DELETE /api/boards/:boardId/cards/bulk (body {cardIds:[...]}), each capped at 500 items per request, so a sync job sends one request instead of hundreds — directly addressing the reporter's nightly delete-all/recreate workflow. Wrapped in api.py as bulkaddcards / bulkdeletecards. Bulk create returns a per-card result/error array; bulk delete returns {deleted, notFound} and only touches cards on the given board. Still planned/optional: per-token rate limiting and batched/optional activity logging were considered but not chosen for now; the bulk endpoints plus the atomic card numbering are expected to remove the hot contention. Revisit rate limiting if storms persist

</details> <details> <summary>Fix REST API auth bugs found by the new E2E tests: awaited the membership/admin checks and return…</summary>

Fix REST API auth bugs found by the new E2E tests: awaited the membership/admin checks and return explicit status codes (CWE-862). Several auth helpers (checkAdminOrCondition, checkUserId) are async, so calling them without await let a denied caller's rejection slip past while the handler kept running: on card create that performed the write anyway (auth bypass), and on the board member endpoints the un-awaited rejection surfaced to the client as an HTTP 503. Fixes:

</details>
  • The pre-existing single-card create (POST .../cards) and the new bulk-create endpoint now await the membership check, so a logged-in non-board-member can no longer create cards.
  • The board member add/remove/permission endpoints (POST .../members/:userId/add, .../remove, POST .../members/:memberId) now require board admin (or site admin), awaited, returning a clean 401/403 — instead of the previous un-awaited checkUserId (nominally site-admin-only, but effectively bypassed) that 503'd on denial.
  • Board-label creation (PUT .../labels) is gated to board admin / site admin, and it plus GET /api/user/cards return explicit 401/403 instead of letting an auth helper throw (which, under Express 4, would otherwise leave the request hanging). These were all found by the new REST API E2E test below. Thanks to xet7.
<details> <summary>End-to-end tests for the new REST API behavior and permissions.</summary>

End-to-end tests for the new REST API behavior and permissions. A new Playwright spec tests/playwright/specs/17-rest-api.e2e.js exercises the new/changed endpoints against a running server (real Bearer-token auth → real HTTP request → MongoDB change verified directly), asserting both correct data AND correct permissions: bulk create/delete (#4743), unique card numbers under bulk create (#5813), bulk label merge + rejection of off-board labels + BoardAdmin-gated label creation (#5819), add board member by named role + invalid-role rejection and card-member add validated against board membership (#5998), add/clear card date (#5846), linked-card creation (#5897), copy-card-to-position deep copy, board card settings GET/PUT (#3062), GET /api/user/cards with due filter (#4815), and that an unauthenticated request is denied. These use Playwright's HTTP request client (no browser is launched, so they run fast). The pure data/permission helpers they rely on live in server/lib/utils.js (boardMemberRoleToFlags, computeSortForIndex, mergeLabelIds, canAssignCardMember, isCardDateClear).

</details> <details> <summary>build.sh test menu reorganized. Thanks to xet7.</summary>

build.sh test menu reorganized. There is now a menu option per test type so each can be run on its own: "Run ALL tests on http://localhost:3000 (start server, progress + summary)" (starts the server and runs import regression + Mocha + Node E2E + Playwright Chromium, streaming progress and printing a per-suite PASS/FAIL summary), plus standalone "Test Mocha unit + security + API-logic tests", "Test import regression", "Test Node E2E regressions", and the existing per-browser Playwright options. Playwright holds the browser-UI specs (01–16) and the API E2E spec (17); the fast unit/security/policy tests stay in the Mocha suite (meteor test --once --driver-package meteortesting:mocha).

</details>

Thanks to above GitHub users for their contributions and translators for their translations.

v9.35 2026-06-06 WeKan ® release

TLDR:

  • Admin Panel/Attachments/Move Files works: CollectionFS/Meteor-Files/S3/Azure Blob/Google Cloud Storage.
  • To have attachments and avatars visible, move them from CollectionFS to any other Storage
<details> <summary><a href="https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md#backup-wekan-snap-to-directory-dump">To upgrade, mongodump/mongorestore (older with</a>.</summary>

To upgrade, mongodump/mongorestore (older with LD_LIBRARY_PATH ) to MongoDB 7.x, and copy WRITABLE_PATH files/attachments/avatars if exists, at Snap /var/snap/wekan/common/files/, at Docker /data/files etc

</details>

This release adds the following CRITICAL SECURITY FIXES:

<details> <summary><a href="https://github.com/wekan/wekan/security/advisories/GHSA-qfqv-42qw-vvwh">Fix GHSA-qfqv-42qw-vvwh: cloneBoard Meteor method has no authorization check — any user can clone…</a> Thanks to dizconnectz for the coordinated disclosure and xet7.</summary>

Fix GHSA-qfqv-42qw-vvwh: cloneBoard Meteor method has no authorization check — any user can clone (read) any private board by ID (CWE-639, CWE-862) The cloneBoard Meteor method in models/import.js copied an entire board — including all cards, comments, attachments, member info and activities — identified solely by a caller-supplied sourceBoardId, and performed no authorization check: it never verified that the calling user was a member of (or otherwise permitted to read) the source board. Any authenticated Wekan user who knew a board's ID (board IDs appear in board URLs and remain known to removed members) could call Meteor.call('cloneBoard', '<targetBoardId>') over DDP and obtain a permanent, fully-readable copy of that board's contents, even for private boards they had no access to. The method called exporter.build() directly, skipping the canExport() guard (models/exporter.js) that the REST export route correctly enforces. Fixed by requiring this.userId and running the same exporter.canExport(user) check the export route uses before building/cloning the board, so cloning a board now requires the same read authorization as exporting it. CVSS 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

</details>

and, while fixing the above, the following similar authorization issues found by code review were fixed (the CloneBleed group):

<details> <summary><a href="https://github.com/wekan/wekan/blob/main/docs/hall-of-fame/clonebleed/">Fix authorization guards that silently never ran in attachment, list, checklist, migration and…</a></summary>

Fix authorization guards that silently never ran in attachment, list, checklist, migration and webhook server methods (CWE-862, CWE-639) A review for the same class of bug as cloneBoard found several server-side methods whose access checks were present in the source but never actually enforced, plus a few methods missing a check entirely:

</details>
  • server/models/checklists.js moveChecklist: the membership guard called allowIsBoardMemberByCard(...), but that helper is async and was both unimported and un-awaited, so !allowIsBoardMemberByCard(...) evaluated !Promise (always false) and the check never ran — any logged-in user could move a checklist between cards on boards they cannot access. Now imported and awaited, and login is required.
  • server/models/lists.js updateListSort: the guard read typeof allowIsBoardMember === 'function' && !allowIsBoardMember(...), but allowIsBoardMember was never imported, so the typeof was always false and the guard never ran — any caller could reorder/re-assign any board's lists by ID. Replaced with a real hasBoardWriteAccess check, plus a check that the list actually belongs to the named board, and a login check.
  • server/attachmentApi.js and server/routes/attachmentApi.js: the attachment Meteor methods and REST handlers called board.isBoardMember(...), which is not a method on the board model, so the permission check never behaved as intended. Replaced with the real board.hasMember(...), and the upload path now also verifies the target card actually belongs to the named board (so boardId cannot be spoofed to a board the caller is a member of).
  • server/models/users.js applyListWidth: stored per-board list width with no authorization. Now requires the caller to be a member of the board and requires the list to belong to that board.
  • server/models/boards.js getBackgroundImageURL: returned any board's background image URL by ID. Now requires the board to be visible to the caller.
  • server/migrations/fixMissingListsMigration.js and server/migrations/migrateAttachments.js: the migration status/execute methods only checked that the caller was logged in. Reading status now requires board visibility, executing a board-rewriting migration now requires board admin (or instance admin), and the attachment migration methods now require board access (global status reads require instance admin).
  • server/notifications/outgoing.js: the webhook delivery method trusted the caller-supplied integration object and only checked that some integration with that URL existed. It now verifies a matching integration exists on its own board and that the caller is a member of that board, closing a path where any authenticated user could drive webhooks (and, via the two-way response path, overwrite comments) on boards they cannot access.
  • server/models/userPositionHistory.js: the userPositionHistory.createCheckpoint, .getRecent, .getCheckpoints and .restoreToCheckpoint methods only checked that the caller was logged in, even though the sibling positionHistory.track* methods (server/methods/positionHistory.js) already require the board to be visible to the caller — the same PositionHistoryBleed class fixed in v8.20/v8.21. All four now require board visibility (via the shared isVisibleBy guard) before reading or restoring position history scoped to a board. Thanks to xet7.

and, while auditing that client-side permission checks are also enforced server-side, the following gaps where the server did not re-verify a UI-gated permission were fixed:

<details> <summary><a href="https://github.com/wekan/wekan/blob/main/docs/hall-of-fame/clonebleed/">Fix client-side permission gates not re-verified server-side (CWE-862, CWE-269)</a>.</summary>

Fix client-side permission gates not re-verified server-side (CWE-862, CWE-269) The browser UI hides certain actions from read-only members and from non-admins, but those are cosmetic — the server-side allow rules and Meteor methods must enforce the same role. An audit found four places that did not:

</details>
  • server/permissions/customFields.js: the Custom Field allow insert/update/remove rules used allowIsAnyBoardMember (mere membership), so read-only/comment-only/worker members could create/modify/delete Custom Fields (board-wide schema) via direct DDP collection writes — the same read-only-write class as GHSA-6733, which had only fixed the REST path. Now uses the new allowIsAnyBoardMemberWithWriteAccess write-access helper.
  • server/permissions/cardCommentReactions.js: the reaction allow rules used allowIsBoardMember, letting read-only members add/remove comment reactions. Reacting is a form of commenting, so it now uses allowIsBoardMemberCommentOnly (Normal/Comment-only allowed, Read-only/No-comments denied), matching CardComments.insert.
  • server/models/boards.js archiveBoard: only required board membership, so any member (including read-only) could archive a board (hiding it for everyone) over DDP, although the UI gates archiving behind board admin. Now requires board admin (or global admin), matching the Boards.allow update/remove rules.
  • server/models/settings.js sendSMTPTestEmail: only required login, so any authenticated user could trigger the server to send an SMTP test (and have the server's SMTP error messages surfaced to them), although the UI gates it behind global admin. Now requires global admin. Thanks to xet7.

and, in the same access-control audit, the attachment write API was tightened:

<details> <summary>Security: attachment write operations in the REST/DDP API now require board write access, not just… Thanks to xet7.</summary>

Security: attachment write operations in the REST/DDP API now require board write access, not just membership (CWE-862, CWE-639). Both attachment API implementations (server/routes/attachmentApi.js HTTP routes and server/attachmentApi.js Meteor methods) gated upload / copy / move / delete on board.hasMember(), which is true for any active member regardless of role. This let read-only, comment-only, no-comments, worker and assigned-only board members add, copy, move and delete attachments through the API — actions the UI forbids for those roles. They now require board write access (global site admins still allowed); read operations (download / list / info) keep membership-level access. Added security tests in server/lib/tests/attachmentApi.tests.js.

</details>

and adds the following new features:

<details> <summary><a href="https://github.com/wekan/wekan/commit/a86a087915aac9d204e157b1a698091c079e04e6">Admin Panel / Attachments</a>. Thanks to xet7.</summary>

** Admin Panel / Attachments : "Calculate file counts" on every storage backend, shown right below the "Read" toggle.** Azure Blob Storage and Google Cloud Storage now have a "Calculate file counts" button (new getAzureStorageStats / getGcsStorageStats methods) that reports how many attachments and avatars are stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already offer. The count is read from the stored file metadata (fast, no cloud API calls). The S3 button was also moved up to sit directly under its "Read" checkbox, so all backends are consistent

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dca9427bfb58532778bc705e4c8c8014ba4b01ae">Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy files visible</a>.</summary>

** Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy files visible ** A translatable line under the "MongoDB GridFS Storage" title reminds admins: "To have attachments and avatars visible, move them from CollectionFS to any other Storage." Thanks to xet7

</details> <details> <summary>Admin Panel / Attachments: the Google Cloud Storage page now documents the required bucket… Thanks to xet7.</summary>

Admin Panel / Attachments: the Google Cloud Storage page now documents the required bucket permission. A note under the "GCS Storage" title explains that the service account needs the Storage Object Admin role (roles/storage.objectAdmin) on the bucket (Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access → add the service account → role "Storage Object Admin"), and that the bucket must exist in the same project — otherwise "Test connection" fails with storage.objects.list denied.

</details> <details> <summary>Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles at the top of each…</summary>

Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles at the top of each cloud storage. S3, Azure and Google Cloud Storage now have a Save button directly below the "[ ] Enabled [ ] Read" row, so those toggles can be saved without scrolling to the bottom of the (now quite long) storage page. It saves the same way as the existing bottom Save button — the server merges that provider's config and preserves secrets left blank — and the bottom Save button is kept. (Filesystem and GridFS have only a Read toggle, which already saves the instant it is toggled, so they need no button.) Thanks to xet7.

</details> <details> <summary>Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields (S3, Azure and Google…</summary>

Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields (S3, Azure and Google Cloud Storage). Each cloud-storage setting now guides the admin from top to bottom with both English and translated text, so it is easy to follow along while clicking through the provider's Cloud Console:

</details>
  • the field label uses the provider's own console wording in English (e.g. Access key ID, Secret access key, Storage account key, Service account key (JSON)), with the translated label shown below it;
  • a literal example of what the value should look like (e.g. eu-west-1, an example bucket name / endpoint / connection string), which is intentionally not translated;
  • a short description of the value, in English and then translated;
  • the Cloud Console menu path showing exactly where to find or create the value (e.g. AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key; Azure Portal → Storage accounts → your account → Access keys → key1; Google Cloud Console → IAM & Admin → Service accounts → … → Keys → Add key → Create new key → JSON), again in English and then translated. The provider field names, examples and menu paths are kept in English so they match what the Cloud Console actually shows, while the descriptions and menu paths are also translatable; section titles, the enable/read/force-path-style checkboxes and the Test/Save buttons remain fully translated. Thanks to xet7.
<details> <summary>Admin Panel / Attachments / Move Attachment: "Repair file locations" button and a persistent "last… Thanks to xet7.</summary>

Admin Panel / Attachments / Move Attachment: "Repair file locations" button and a persistent "last move" message. The new Repair file locations button scans all attachments and avatars and finds any whose recorded storage (versions.<v>.storage / path / meta.gridFsFileId) no longer matches where the binary actually is — left inconsistent by an interrupted or failed move — and fixes the database to point at the real location: it detects the binary in GridFS (by the metadata.fileId stamped on upload, recovering files whose GridFS id reference was lost) or on the filesystem (using the storage strategy's own thorough path resolution, so the repair agrees with what a real download/move would find even when versions.<v>.path is stale), then corrects storage, path and meta.gridFsFileId accordingly. Cloud-stored files are left untouched, and files found nowhere are reported as "Not found". The scan is streamed (memory-safe) and shows a per-scope searched / repaired / not-found summary. Separately, after a move finishes the page now keeps showing the last move operation — source → destination (scope) and the date/time as YYYY-MM-DD HH:MM:SS — so it is clear what was last done.

</details> <details> <summary>SVG image uploads are now sanitized instead of rejected. Thanks to xet7.</summary>

SVG image uploads are now sanitized instead of rejected. Uploaded SVGs (attachments and avatars) are cleaned in place in onAfterUpload via the new models/lib/sanitizeSvg.js, which removes JavaScript (<script>, inline on*= event handlers, javascript:/vbscript: URIs, <foreignObject> / <iframe> / <object> / <embed> and similar active content) and XML loops (<!DOCTYPE> / <!ENTITY> entity-expansion / XXE constructs and <?xml-stylesheet?>), so SVG images can be uploaded safely.

</details> <details> <summary>Unified attachment/avatar storage migration: move any → any. Thanks to xet7.</summary>

Unified attachment/avatar storage migration: move any → any. Admin Panel / Attachments / Move Attachment can now move Attachments, Avatars, or both, from any source to any destination across Filesystem, Meteor-Files GridFS, Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is "All Read-enabled storages" (every backend whose Read flag is enabled and whose settings work), so everything can be consolidated into one destination in a single run. All metadata is preserved (board / swimlane / list / card / user / uploaded date / name / type / size), attachment cover references (cards.coverId) are remapped when an id changes, and the legacy source is deleted only after the new copy is verified.

</details> <details> <summary>Legacy CollectionFS GridFS is now a first-class storage backend. Thanks to xet7.</summary>

Legacy CollectionFS GridFS is now a first-class storage backend (read, migrate-from, and export-to) for both attachments and avatars, via the new models/lib/collectionFsStore.js (binary keyed by copies.<coll>.key in the cfs_gridfs.<coll> bucket, metadata in cfs.<coll>.filerecord).

</details> <details> <summary>The storage strategy layer is now collection-aware. Thanks to xet7.</summary>

The storage strategy layer is now collection-aware. moveToStorage and the GridFS/Cloud strategies previously hard-coded the Attachments collection, so moving avatars to GridFS/cloud would have updated the wrong collection. The factory now carries its collection (Attachments or Avatars), so avatars can be stored in Meteor-Files GridFS and cloud, not only on the filesystem. Attachment behavior is unchanged.

</details>

and adds the following updates:

<details> <summary>Documented the attachment / file REST API in the OpenAPI docs. Thanks to xet7.</summary>

Documented the attachment / file REST API in the OpenAPI docs — file upload, download, info, listing a board's files, copy, move and delete (the /api/attachment/* endpoints registered via WebApp.handlers.use(), which the generator cannot auto-discover) are now documented in openapi/extra_paths.yml and injected into the generated docs, including their authentication and permission requirements.

</details>

and adds the following fixes:

<details> <summary><a href="https://github.com/wekan/wekan/commit/dca9427bfb58532778bc705e4c8c8014ba4b01ae">Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the database</a>. Thanks to xet7.</summary>

** Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the database ** MongoDB refuses compact on an active replica-set primary unless force: true is given, so every collection failed with "will not run compact on an active replica set primary … use force:true to force" — and because the usual Meteor setup is a single-node replica set (only a primary, no secondaries), nothing was compacted at all and no disk space was reclaimed. The primary is now compacted with force: true (secondaries are still compacted without it, so the primary stays available while they run), so the GridFS collections are actually rewritten and freed space is returned to the filesystem

</details> <details> <summary>Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud config inputs are… Thanks to xet7.</summary>

Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud config inputs are trimmed. Testing an Azure/GCS/S3 connection used to report a single generic Incomplete configuration or adapter not installed even when the real problem was specific — e.g. Azure rejecting the config with Invalid URL (a stray space/newline in the account name, or a bad endpoint / connection string). testCloudConnection now reports the actual cause: adapter not installed, required fields missing, the adapter's own configuration error (such as Invalid URL), or the real listFiles error (auth failure, container not found, …). It also pre-validates the config and gives an actionable message before the adapter turns it into a cryptic error — e.g. for Azure it explains that the Storage account name must be just the account name (3–24 lowercase letters/numbers, e.g. wekanstorage, not a full https://… URL and with no spaces), or that the Connection string is malformed; and when Azure still returns Invalid URL the message appends which field to check. In addition, all cloud-storage text fields are now trimmed when read from the form, so leading/trailing whitespace from copy-paste no longer produces an invalid URL.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a86a087915aac9d204e157b1a698091c079e04e6">Fixed moving attachments to S3</a>.</summary>

** Fixed moving attachments to S3 (and other cloud storage) failing and crashing the server.** Uploading to S3 failed two ways, each of which crashed the whole server because the rejection was unhandled and SyncedCron treats those as fatal. The @tweedegolf/sab-adapter-amazon-s3 adapter uploaded with a PutObjectCommand whose Body was a live stream: (1) with no ContentLength the AWS SDK fell back to a chunked signed upload that needs a decoded content length, which was undefined for a stream of unknown size — Invalid value "undefined" for header "x-amz-decoded-content-length"; and (2) if the socket dropped mid-upload (socket hang up), the SDK's body-stream promise rejected unhandled. Fixed by uploading the file as a complete in-memory buffer (addFileFromBuffer) instead of a live stream: a buffer has a known length (no content-length error) and no socket-bound stream to re-reject. The cloud upload promise now never rejects — any failure is captured and exposed via waitUntilStored(), which moveToStorage checks inside a try/catch so a failed upload leaves the source file intact and is logged cleanly. (Files are moved one at a time, so peak memory is one file.) Thanks to xet7

</details> <details> <summary>Fixed TypeError: Cannot read properties of undefined (reading 'on') when moving attachments between… Thanks to xet7.</summary>

Fixed TypeError: Cannot read properties of undefined (reading 'on') when moving attachments between Meteor-Files/GridFS and Filesystem. If a file's source binary was missing at its recorded location (e.g. a file left half-moved by an earlier interrupted run — storage says "gridfs" but the GridFS id reference is gone, or a filesystem path that no longer exists), getReadStream()/getWriteStream() returned undefined and moveToStorage (and copyFile) called .on() on it, throwing. Both now detect a missing read/write stream, log a clear message (which file, version, from→to storage, and why), skip that file and leave the source intact so no data is lost, and continue with the rest. Use the new "Repair file locations" button to fix the underlying inconsistent records.

</details> <details> <summary>Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing the server, hanging the…</summary>

Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing the server, hanging the Admin Panel move at "File 1 / N", and leaving broken avatars. Three problems in Admin Panel / Attachments / Move Attachment:

</details>
  • Server crash on the first file (uncaughtException: TypeError: Cannot read properties of undefined (reading '_id') at AttachmentStoreStrategyGridFs.writeStreamFinished). The current mongodb driver's GridFS GridFSBucketWriteStream 'finish' event no longer passes the stored file document, so finishedData._id threw and killed the process (which is also why the move appeared stuck at "File 1 / 4" — the background job died mid-file and its persisted status was frozen). The GridFS strategy now reads the uploaded file id from the write stream itself (gridFSFile._id / id), the 'finish' handler is wrapped so it can never crash the process, and a startup reconciliation clears a stale "running" move status left by a crashed run so the UI un-sticks and a new move can be started.
  • Broken avatar after migrating avatars. The bulk move only repointed card cover references (cards.coverId); a user's profile.avatarUrl still pointed at the deleted legacy /cfs/files/avatars/<oldId> URL, so the avatar rendered broken. The move now repoints profile.avatarUrl to the migrated avatar (remapReferences handles avatars), and migrated files stamp meta.migratedFromId so references can be repaired to the exact new file. The startup repair in server/models/users.js (previously a no-op: it string-replaced the URL prefix while keeping the now-deleted id and never saved the document) now points each affected user's avatar at their migrated Meteor-Files avatar — matched precisely by meta.migratedFromId, otherwise strictly by userId (newest migrated avatar first), so a user can only ever be given their own avatar, never another user's.
  • ObjectID deprecation warning from models/lib/grid/createObjectId.js (MongoInternals.NpmModule.ObjectIDObjectId). Thanks to xet7.
  • Fix Dropdown list cannot be created with values.
    • Fixed Dropdown custom field options not being addable / saving as empty. Creating a "Dropdown" custom field showed the "List Options" box, but pressing Enter (or typing and saving) never added any option, and on the card the only selectable value was (none). The custom-fields sidebar component was migrated from BlazeComponent to a plain Template, but the template still iterates the options with {{#each dropdownItems.get}} — under BlazeComponent that resolved to the instance's dropdownItems ReactiveVar, whereas a plain Template does not expose instance variables to the template, so the list rendered nothing. Because the options were never rendered as inputs, getDropdownItems() then overwrote the ReactiveVar with the empty DOM on save and every entered value was dropped. Fixed by re-adding the missing dropdownItems helper (returning the ReactiveVar), matching the pattern the other migrated templates already use. Thanks to xet7.
    • Fixed Exception in global helper _ when opening the Create Custom Field popup (and any translation containing a literal %). i18n is configured with a global sprintf post-processor (postProcess: ["sprintf"]), so every translation is run through i18next-sprintf-postprocessor. The help text custom-field-stringtemplate-format ("Format (use %{value} as placeholder)") contains a literal %{value} that sprintf cannot parse, so vsprintf threw and crashed the global Blaze _ translation helper — breaking that popup and any string (in any language, including user translation overrides) that contains a stray %. TAPi18n.__ now retries without the sprintf post-processor when it throws, returning the raw string (so %{value} is shown literally) instead of crashing. Thanks to rouceto1 and xet7.
<details> <summary><a href="https://github.com/wekan/wekan/commit/075e86b00e1f0dc0dea519acd37cece4d0a1fad3">Fixed new checklists (and checklist items) on a newly added card not being visible until…</a> Thanks to ahlgrimma and xet7.</summary>

Fixed new checklists (and checklist items) on a newly added card not being visible until logout/login The board publication batched checklists, checklist items, comments and attachments into board-level cursors filtered by cardId: { $in: cardIds }, where cardIds was a one-time snapshot. In reywood:publish-composite a child cursor only re-runs when its parent (the board) document changes, so the snapshot never refreshed when a card was added — a checklist on a card created after subscribing matched no published card and only appeared on the next subscribe (logout/login). This was a regression from the "Optimized board loading" change, which had replaced the original reactive per-card child cursors with these batched snapshots. Fixed by denormalizing a boardId field onto Checklists and ChecklistItems so they can be published with a single board-level cursor filtered by boardId — one cursor per collection (keeping the load optimization) that still reacts to checklists on newly added cards, because a new checklist is created already carrying the board's id. boardId is set on insert (server before.insert hooks, plus explicitly in the Trello/WeKan board importers, which use direct.insert and bypass hooks), re-derived when a checklist/item or its card moves to another card (before.update on cardId) or the card moves to another board (Cards.after.update cascade), and backfilled for existing data by an idempotent startup migration. New { boardId: 1 } indexes were added on both collections. Comments and attachments instead remain reactive as per-card children of the cards cursor. Assigned-only board members still only receive checklists for cards assigned to them (the board-level cursor falls back to the assigned cards' ids for those roles)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/72767ad9a769fee2548f93bbd7174edd69995e97">Fixed SyncedCron crash</a>. Thanks to titver968 and xet7.</summary>

Fixed SyncedCron crash Fixed deleting archived lists (or many cards) crashing the server with SyncedCron: Fatal error encountered (unhandledRejection): TypeError: Cannot read properties of undefined (reading 'boardId') at server/models/checklistItems.js. Deleting a list cascades into removing its cards, and each card's checklists, checklist items, comments and attachments (cardRemover in models/cards.js). Cards.before.remove called cardRemover without await (and was not async), so the card document was deleted first and the cascade then ran with the parent card already gone; the ChecklistItems.before.remove / Checklists.before.remove hooks dereferenced the now-undefined card (card.boardId) and threw, and because the promise was unhandled, SyncedCron caught the rejection and tore down all running cron jobs. Fixed by making Cards.before.remove async and awaiting cardRemover (so sub-items are removed while the card still exists), and the REST card-delete handler now runs cardRemover before removing the card. As defense in depth, every card-activity hook and helper that looked up a card and used its boardId / listId / swimlaneId now skips (with a warning) when the parent card — or, for checklist-completion activities, the parent checklist — is missing, instead of throwing: before.remove on checklist items and checklists, Checklists.after.insert, the Cards.before.update timing activity, and the shared itemCreation / publishCheckActivity / publishChekListCompleted / publishChekListUncompleted / commentCreation helpers (matching the guards already present in server/models/cardComments.js)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/39f3c89b0a11c5671b77d3a0b95200ac7256a4f1">Fixed upgrade crash</a>.</summary>

** Fixed upgrade crash An error occurred when creating an index for collection "users": Topology is closed / MongoServerSelectionError: Server selection timed out after 30000 ms.** When WeKan started before MongoDB was reachable and had an elected replica-set primary (common right after an upgrade, while MongoDB replays its WiredTiger journal, or when the app container starts at the same instant as the database), the first index creation threw and Node exited. Now:

</details>
  • WeKan waits until MongoDB is ready (reachable, with an elected primary) before it starts, in every launch path: start-wekan.sh, start-wekan.bat, the Snap (snap-src/bin/wekan-control), and the app itself (server/00waitForMongo.js / server/lib/mongoStartup.js, which blocks the first Meteor.startup so it also protects the plain Docker image). The Docker docker-compose.yml and docker-compose-multitenancy.yml now give wekandb a healthcheck (primary elected) and the WeKan/tenant services depends_on: condition: service_healthy.
  • If MongoDB stays unreachable for too long (default 120s, configurable via WEKAN_DB_WAIT_TIMEOUT), a clear English-only message is printed to the Node.js console / docker logs / snap logs explaining that a database upgrade with mongodump (old MongoDB) and mongorestore --drop (new MongoDB) may be needed, and reminding that attachments and avatars live on disk under WRITABLE_PATH (files, attachments, avatars; on Snap /var/snap/wekan/common/files, on Docker the wekan-files volume at /data) and must be copied too. WeKan keeps retrying after printing it.
  • Index creation is now idempotent and crash-safe. A new ensureIndex helper checks the existing indexes and only creates the ones that are missing, and never throws — a single index problem is logged in English instead of taking the whole server down. All startup index creation across the model files was switched to it. Thanks to xet7.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c71a97cba20247ce227a288fa74ef23cb3c4c83e">Fixed OpenAPI REST API documentation generation</a>. Thanks to xet7.</summary>

Fixed OpenAPI REST API documentation generation , which had been broken since after WeKan v7.93 and only generated docs for the login/register endpoints (2 operations) instead of the full API. The Meteor 3 migration moved the REST routes from models/*.js (JsonRoutes.add(...)) into server/models/*.js (WebApp.handlers.get/post/put/delete(...)) and introduced optional chaining (?.) that the esprima Python parser cannot read, so openapi/generate_openapi.py silently skipped every route file. The generator now understands both routing styles, scans both models/ and server/models/, downlevels modern JS syntax so files parse, handles the type: Array SimpleSchema idiom, and releases/rebuild-docs.sh works directly with Python 3.12.x (PEP 668). The generated public/api/wekan.yml / wekan.html now cover the full API again (89 operations / 61 paths)

</details> <details> <summary>The attachment copy API now honours the admin "Admin Panel / Attachments" API transfer limits. Thanks to xet7.</summary>

The attachment copy API now honours the admin "Admin Panel / Attachments" API transfer limits. Copying an attachment creates a new attachment but skipped the apiUploadBlocked / apiUploadMaxBytes checks that upload enforces; copy now respects them in both API implementations.

</details>
  • Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing. Several issues in the bulk move:
    • The GridFS source matcher required meta.gridFsFileId to be absent, but Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero files and "nothing happened". The matcher now recognizes real GridFS files (versions.*.storage === 'gridfs' or a meta.gridFsFileId reference), and a "nothing to move" message is shown when a source is empty instead of silently doing nothing.
    • Moving files crashed the whole server with FilesCollection#findOne() not available in serverReactiveCache.getAttachment used a synchronous findOne(); it now uses findOneAsync(). The background job is also hardened so a single failing file is skipped instead of crashing the server via an unhandled rejection.
    • The attachment copy API now honours the admin API upload limits (it previously skipped them).
<details> <summary>Fixed the "MongoDB Meteor-Files" file-count statistic. Thanks to xet7.</summary>

Fixed the "MongoDB Meteor-Files" file-count statistic in Admin Panel / Attachments, which counted every attachment metadata document (so files on the Filesystem were wrongly reported as being in GridFS). It now counts only attachments actually stored in GridFS, consistent with the move tool. Also renamed the mislabeled "Mongo-Files" column to "Meteor-Files".

</details> <details> <summary>Read legacy CollectionFS attachments and avatars in place. Thanks to xet7.</summary>

Read legacy CollectionFS attachments and avatars in place (without migrating). The backward-compatibility layer (models/lib/attachmentBackwardCompatibility.js) was broken — it looked up the GridFS binary by the filerecord _id and by filename instead of by ObjectId(copies.<coll>.key), so legacy files were never found. It is fixed and generalized for attachments and avatars. Legacy attachments now appear in the card attachment gallery (new legacyBoardAttachments publication) and stream from the cfs_gridfs.attachments bucket, and legacy avatars (/cfs/files/avatars/<id> URLs) are served from the cfs_gridfs.avatars bucket instead of redirecting to a 404.

</details>

Thanks to above GitHub users for their contributions and translators for their translations.