old-CHANGELOG/2026/06.md
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-06 | Releases |
|---|---|
| 06 | 1 |
| 10 | 1 |
| 11 | 4 |
| 12 | 1 |
| 13 | 2 |
| 14 | 2 |
| 15 | 1 |
| 17 | 5 |
| 18 | 5 |
| 19 | 7 |
| 20 | 2 |
| 21 | 1 |
| 23 | 4 |
| 26 | 2 |
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
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
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
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.
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
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
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
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
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
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
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
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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.
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
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
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.)
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.)
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)
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
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.)
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.)
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)
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)
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)
#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)
#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
#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
#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
#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)
Thanks to above GitHub users for their contributions and translators for their translations.
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
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
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
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
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
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
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.)
Thanks to above GitHub users for their contributions and translators for their translations.
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
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)
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
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 build → node 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.
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.
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).
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).
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
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
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
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
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
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
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
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
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
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
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
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
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
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
#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
#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
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
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:
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).docs/Features/Lists/Lists.md.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.
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.
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)
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 )
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 (SimpleSchemaValidationContext → SimpleSchema → …), 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 )
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 )
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 )
Thanks to GitHub users Atry, mueller-ma and liferadioat for reporting.
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.
Feature:Area,
Targets:, Severity:, etc.).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:
Feature:User-accounts:*): ADFS, Azure, B2C, Google,
Header-Login, Nextcloud, Oracle, Zitadel, Autologin, Accounts-Lockout,
Forgot-Password.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.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)
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
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)
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
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
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)
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
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
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
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
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>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>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
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
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
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
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)
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
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
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
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)
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
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)
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
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
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
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
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
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
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)
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.
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
Thanks to above GitHub users for their contributions and translators for their translations.
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)
Thanks to above GitHub users for their contributions and translators for their translations.
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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>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.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)
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)
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)
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)
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)
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)
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)
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)
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
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)
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)
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)
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)
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)
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following updates:
Thanks to above GitHub users for their contributions and translators for their translations.
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
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 releaseand 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
Thanks to above GitHub users for their contributions and translators for their translations.
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
and fixes the following bugs:
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
Thanks to above GitHub users for their contributions and translators for their translations.
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)
Thanks to above GitHub users for their contributions and translators for their translations.
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
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.
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
Thanks to above GitHub users for their contributions and translators for their translations.
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.
Thanks to above GitHub users for their contributions and translators for their translations.
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
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
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
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
Thanks to above GitHub users for their contributions and translators for their translations.
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
pgrep -f 'meteor run --port 3000') and sends it a normal kill, which
also tears down the node child it spawned.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.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><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.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.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
.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.--workers=3 on Windows).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.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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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)
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
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
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.Thanks to above GitHub users for their contributions and translators for their translations.
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
and adds the following updates:
: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).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).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).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).aria-hidden="true" so they are not announced redundantly..sr-only helper class.docs/README.md (verified with a link
check).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.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>/b/:id/:slug/rules.When … → Then … builder to create a rule, drag an
action onto an existing rule to change it, and delete rules from the graph.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
/api/boards/:boardId/rules), documented in the OpenAPI docs and api.py
(addrule / editrule / removerule / listrules / getrule).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).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
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
.xlsx (the
matching Excel export already existed).GET /api/boards/:boardId/exportPDF): exports the board title, lists and
cards to a PDF (reusing the card PDF builder). Added api.py exportboardpdf.GET /api/boards/:boardId/export/kanboard), which
round-trips with the importer.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.api.py — no external script needed).tests/playwright/specs/25-excel-pdf.e2e.js (Excel import +
board PDF export). Documented in
Excel-and-VBA
and
Kanboard.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)
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
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
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)
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
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
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:
TAPi18n is not defined
(TAPi18n was not imported in server/rulesHelper.js). Fixes #5822.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>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)
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)
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
alt text to the user avatar
image
(the surrounding link already carries the accessible name).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:
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)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)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)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).length of a missing cell); date columns are now treated as optional.
(models/csvCreator.js)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).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)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)<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.)
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following updates:
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.
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-left→margin-inline-start, left:→inset-inline-start:,
text-align: left→text-align: start, float: left→float: 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
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.
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.
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.
Thanks to above GitHub users for their contributions and translators for their translations.
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
and fixes the following bugs:
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
.. 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).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.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).
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.
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.
and fixes the following bugs:
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
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).
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.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
Thanks to above GitHub users for their contributions and translators for their translations.
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
and adds the following updates:
Thanks to above GitHub users for their contributions and translators for their translations.
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
and adds the following updates:
and fixes the following bugs:
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
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
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.
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
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)
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
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
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.
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
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)
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
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
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:
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.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.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.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).
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).
Thanks to above GitHub users for their contributions and translators for their translations.
TLDR:
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)
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:
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:
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.
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
** 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.
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>eu-west-1, an example bucket name / endpoint / connection string),
which is intentionally not translated;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.
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.
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.
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).
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.
and adds the following updates:
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.
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
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.
** 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
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.
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>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.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.ObjectID → ObjectId).
Thanks to xet7.(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.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.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)
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)
** 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:
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.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.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.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)
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.
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.FilesCollection#findOne() not available in server — ReactiveCache.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.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.
Thanks to above GitHub users for their contributions and translators for their translations.