Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v9.96
2026-07-16v9.96 2026-07-16 WeKan ® release
This release adds the following updates:
-
Releases now include the Sandstorm .spk package (
.github/workflows/release-all.yml). The release-all workflow got a newbuild-sandstormjob that runs after the GitHub Release is created: it builds the Sandstorm package from the release tag with the same steps as the standalone sandstorm.yml workflow and attacheswekan-<version>-sandstorm.spkto the Release. The job is non-blocking (continue-on-error), so the experimental Sandstorm build never fails or delays the rest of the release. The standalone sandstorm.yml workflow still exists for building and testing the .spk on its own without doing a full release. Thanks to xet7. -
Bump websocket-driver from 0.7.4 to 0.7.5. Thanks to dependabot.
-
New general
cpu-exec+ bundled qemu-user: every WeKan platform now runs binaries that need missing CPU features through emulation automatically (#6458,snap-src/bin/cpu-exec,snap-src/bin/mongodb-control,snap-src/bin/migration-control,.github/workflows/release-all.yml,.github/workflows/sandstorm.yml,sandstorm-src/build-deps.sh,releases/ferretdb/start-wekan.sh,releases/ferretdb/wekan-entrypoint.sh,docs/Databases/mongodb-avx-qemu.md,docs/Databases/mongodb-raspi4-qemu.md). The #6458 report turned out to run inside a hypervisor that MASKS AVX — and the snap’s old per-tool AVX wrappers (amd64-only, PATH-based) were bypassed by every absolute-pathmongodinvocation. The newcpu-exechelper is one general mechanism for all scripts, sandboxes, platforms and CPUs:cpu-exec --features x86_64=avx,aarch64=atomics <binary> [args]checks/proc/cpuinfoand, when a required feature is missing, transparently re-runs the binary through a same-architecture qemu-user (bundled first, then system); with no declared features it is a plain zero-overhead exec, so every binary can be routed through it (WEKAN_REQUIRED_CPU_FEATURESdeclares requirements externally). The Snap’s mongodb-control and migration-control now run every mongod 7 through it — so MongoDB works (slower) on CPUs without AVX and the migration can READ modern MongoDB data there, with the FerretDB switch/migration fallbacks unchanged;aarch64=atomicscovers MongoDB’s ARMv8.2-A requirement (Raspberry Pi 4 and older lack it). release-all.yml now shipscpu-execplus this-arch’s static qemu-user in every Linux bundle .zip (amd64/arm64/ppc64le/s390x/riscv64 — stripped from the Windows/macOS bundles, where qemu-user does not exist), which flows into the Docker image and the Snap automatically, and the Sandstorm .spk gets both via build-deps.sh; the bundle launcher and Docker entrypoint route node/ferretdb through it. Thanks to a1bert01 and xet7. -
Added regression tests, with negative cases, for all of the fixes below (
tests/htmljsArrayContent.test.cjs,tests/cardDescriptionDraft.test.cjs,tests/commentDraft.test.cjs,tests/attachmentDeleteGuard.test.cjs,tests/ruleMoveAction.test.cjs,tests/snapMigrationRecovery.test.cjs,tests/ferretdbPolling.test.cjs,tests/uiDensity.test.cjs,tests/cpuExec.test.cjs— a BEHAVIORAL test that executes the real cpu-exec against fake /proc/cpuinfo files and a fake qemu-user, covering direct exec, qemu fallback, missing-qemu error, per-arch scoping and env overrides —,tests/cpuExecWiring.test.cjs— pins the cpu-exec DELIVERY pipeline: every Linux bundle in release-all.yml embeds cpu-exec plus its own arch’s qemu-user (arm64/extra arches replace the inherited amd64 one, tolerantly), the Windows and macOS bundles strip both, qemu-user-static is installed in every bundle-building job, the Sandstorm .spk ships both via build-deps.sh, the Docker entrypoint and bundle launcher route ferretdb/node through cpu-exec WITH direct-exec fallbacks for bundles that lack it, and the snap ships it via the snap-src helpers part —,tests/subtasksDefaultBoard.test.cjs(see the #6456 entry below), andtests/ferretdbHasData.test.cjs— a BEHAVIORAL test executing the real snap-src/bin/ferretdb-has-data guard (the check that gates every switch to FerretDB) against crafted directories: non-empty .sqlite passes with no -wal sidecar required, while a 0-byte .sqlite from a failed migration, sidecar-only leftovers, a directory named *.sqlite, and empty/missing directories are all rejected — all wired intotest:unit:nodeinpackage.json; plus Go table tests in the wekan/FerretDB fork’sinternal/backends/sqlite/query_test.goand the fork’s integration-test fixes (OTel exporter skipped with a single log line when no collector is listening, and valid span contexts without a collector soTestOtelCommentpasses — details in the fork’s own CHANGELOG Upcoming). The htmljs test exercises the vendored compiler’s Tag constructor directly (array content vs. attributes, #6459) and the draft tests run the real extractednormalize/normalizeTriggerfunctions; the UI tests guard the #6465 density fixes in the repo’s CSS-guard style (base font 14px, card details docking right of the board instead of over its own card, compact admin table headers, un-clipped zoom pill, one-click board settings cog). The FerretDB Go tests pin the SQLite filter-pushdown semantics: which strings are pushdown-safe, exact WHERE/args for_idand top-level equality filters, and that dotted paths, operators, non-strings and unsafe strings stay with the in-Go filter. Thanks to xet7.
and fixes the following bugs:
-
Subtasks: creating a subtask crashed with “Exception while invoking method ‘addSubtaskCard’” (#6456,
models/boards.js,tests/subtasksDefaultBoard.test.cjs). The lazily-creating getters for the default subtasks helper board and its landing list still used the SYNCBoards.insert/Swimlanes.insert/Lists.insert/Boards.updateAPIs, which Meteor 3 removed from the server — so theaddSubtaskCardmethod’s async path crashed with “insert is not available on the server. Please use insertAsync() instead” the first time a board needed its^Board^helper board created (the list creator additionally readgetDefaultSwimline()._id, which on the Meteor 3 server is a Promise, so it could never have worked). The sync getters are now PURE (no creation — also matching the #3868/#2256 rule that only the server may create these), and the server-side lazy creation lives ingetDefaultSubtasksBoardAsync/getDefaultSubtasksListAsyncusing the async APIs. The never-called date-settings twins, which had the same sync calls and no server-only guard at all, are pure getters now too. With a regression test guarding that no sync collection writes come back tomodels/boards.js. Thanks to sjohnen and xet7. -
GUI: the “More” menu of cards was completely empty, so cards could not be deleted from it (#6459,
npm-packages/meteor-jade-loader/lib/vendor/htmljs.js,client/components/lists/listHeader.jade). A WeKan-local vm-sandbox patch in the vendored jade compiler’s htmljs madeisConstructedObject(Array)returnfalse(upstream returnstrue), so a tag whose inline text compiles to an ARRAY — thelabel {{_ 'source-board'}}:andlabel {{_ 'parent-card'}}:lines in cardMorePopup — got its content array mis-assigned as the tag’s attributes. At runtime Blaze then found a template view object inside the attributes and threw “The basic TransformingVisitor does not support foreign objects in attributes” on every render, killing the whole popup — including the card Delete link. The tag constructor now treats an array first-argument as content, like upstream htmljs. Also fixed the list “More” popup’s copy-link input, which was always empty because it referenced arootUrlhelper that does not exist (nowabsoluteUrl). Board and swimlane menus intentionally offer archive (delete lives in Sidebar → Archive), so only the card menu was actually broken. Thanks to thku and xet7. -
Cards: the “You have an unsaved description” warning could never be cleared by saving (#6455, reincarnation of #1287,
client/components/cards/cardDetails.js,client/components/cards/cardDescription.js). Two bugs: closing the description editor only avoided adding a draft when the text matched the saved description — it never removed a pre-existing draft record, so once the warning appeared, “View it” → Save could not clear it, only Discard could. And the comparison matched a per-line-whitespace-stripped draft against the raw stored description (ornullwhen empty), so descriptions with Markdown" "hard-breaks re-created a phantom draft on every save. Saving now removes the draft record explicitly, and both sides of the comparison are normalized the same way. Thanks to C0rn3j and xet7. -
Comments: a comment being written was lost when the card was closed or a click landed outside the card (#5547,
client/components/activities/comments.js). The comment-draft machinery existed (the form even prefills from it) but had been disarmed since 2019: the escape handler that saved the draft was gated on a “form is open” flag that nothing ever set — the setter was removed back then because the handler also cleared the visible text on every outside click. Now the draft is saved continuously (debounced) while typing and flushed when the form is torn down, submit removes the draft, and the escape handler no longer clears the visible text — so an unfinished comment survives closing the card, and reopening the card restores it into the form. Thanks to Finnlife, webenefits and xet7. -
Attachments: deleting an attachment could log a client-side “Removed nonexistent document” exception even though the delete succeeded (#5282,
client/components/cards/attachments.js). Same class as the fixed #3252 for comments and checklists: under publication churn the attachment document can already be evicted from Minimongo when the confirm handler runs, and removing a missing _id throws. The delete now only runs when the document is still in the local cache — the comment/checklist guards’ missing sibling. Thanks to lupuszr and xet7. -
Rules: the “move card to top/bottom” actions did nothing (#6472,
server/rulesHelper.js,client/components/rules/actions/boardActions.js,client/components/rules/rulesImportExport.js,server/rulesButton.js,models/lists.js). A pile-up of five bugs, all silent because the activity hook swallows rule-action errors: (1) an unresolved destination list (typo’d, renamed, case-mismatched, or on another board) crashed onlist.cardsUnfiltered— now it falls back to the card’s current list; (2) the classic rule wizard’s generic “move to top/bottom” stored the field aslistTitle, which the rule engine never reads (listName) — so every such rule created from the wizard has never worked; (3) an empty destination list madeMath.min()/Math.max()of nothing write a corruptsort: ±Infinity; (4) the rules JSON/CSV import created rules with raw client inserts that the board-admin-only allow rules reject into minimongo limbo — it now uses the samerules.createRuleserver method as the wizard, and defaults missing trigger matching fields (e.g.userId) to the*wildcard so hand-written JSON matches; (5)rules.createRulelet an emptyboardId: ''from a not-yet-loaded board selector override the real board. Also fixed the server-side orphaned-cards fallback inList.cards()/cardsUnfiltered(), which silently never applied because an async lookup was read synchronously. Thanks to jmb26240 and xet7. -
GUI: desktop density regressions from the v7.98–v8.18 mobile UI work (#6465,
client/components/main/layouts.css,client/components/cards/cardDetails.css,client/components/main/header.css,client/components/settings/peopleBody.jade,client/components/settings/settingBody.css,client/components/boards/boardHeader.jade). Restores the WeKan 6.09 desktop look users asked for: the base font is back to 14px (theclamp(...2.5vw...)introduced in v7.98 and raised in v8.02 resolved to 18px on any window wider than 720px — +28.5% on every font, button and input, the “everything is way too big” complaint) and headings back to 22/18/16px; the card details window no longer opens as a huge floating sheet ON TOP of its own card — it docks to the right edge like the classic side panel (still movable by its drag handle), with 6.09’s 20px content padding instead of ~48px white borders, a 3px corner radius, and without the v8.18 rule that forced ALL card text to the title’s size; the All Boards page header band is back to 6.09’s compact padding; the Admin Panel Organizations/Teams tables no longer explode column widths (“Select all / Unselect all” header links are now compact icons and header cells may wrap); the zoom pill no longer renders cut off (fixed-pixel pill inside the 28px quick-access row); and board settings opens with ONE click from a new cog button in the board header (the sidebar path still works). Follow-ups caught by the Playwright suite: archiving or deleting a card now also CLOSES its details window (the card id stayed in the openCards session list, so the right-docked window kept rendering exactly over the archives sidebar and intercepted its Restore/Delete clicks), and an OPEN sidebar now stacks above the card window (z-index 2002 vs 2001) so the sidebar is always usable while a card is open. With Playwright coverage: a new spec proves board settings opens in one click from the header cog (and that the popup closes again), and the archives spec’s board-menu click is scoped to the sidebar instance since the cog made the bare class selector ambiguous. Note: a missing watch “eye” icon is the Admin Panel → Features → Notifications “disable watch” setting, not a regression. Thanks to Mintyt, csonkaoszimt, micha141076 and xet7. -
Snap: WeKan served “502 Bad Gateway” forever when mongod could not start — including on CPUs without AVX (“Illegal instruction”) (#6458, #6466,
snap-src/bin/mongodb-control,snap-src/bin/migration-control). MongoDB 5.0+ x86_64 binaries require AVX; on CPUs without it mongod dies instantly with SIGILL (exit 132). mongodb-control never checked the mongod fork’s exit status: it pinged the dead port for ~10 minutes, snapd restarted the service, and the cycle repeated forever — same limbo as when the data files are still MongoDB 3.x (“This version of MongoDB is too recent”). Now there is an AVX pre-flight and the fork/final-start exit codes are checked: if a COMPLETED FerretDB migration exists the snap switches to it; otherwise the MongoDB → FerretDB migration is (re)run — FerretDB is pure Go + SQLite and the 3.x reader uses the bundled MongoDB 3.2 tools, so neither needs AVX — with a 3-attempt counter so a persistently failing migration cannot ping-pong, and clear log guidance (snap run wekan.migrate). migration-control also skips the pointless mongod 7 probe when AVX is missing. Thanks to kiarn and xet7. -
Snap: a migration that finished with a few per-item errors (“Avatars Errors”) deleted the fully-copied FerretDB database and left the snap serving 502 Bad Gateway (#6466,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs,releases/migrate-mongodb-to-ferretdb.mjs,snap-src/bin/migration-control). Both importers treated ≥10 logged errors of ANY kind as failure — but per-item errors (one document that fails JSON parsing, one avatar that fails to extract) don’t invalidate everything that DID copy. The failure path then discarded the whole migrated SQLite, setmigrate=off, and “fell back” to MongoDB — impossible for a 6.09 upgrade, whose 3.x data files the bundled mongod 7 cannot open, producing the reported endlessdb-eval.mjs pingloop and 502. Per-item errors are now logged but non-fatal (only real failures — disk full, unreachable target/source — still fail), and a failed 3.x-source migration keeps the partial FerretDB SQLite plus its checkpoint and RESUMES on the next start instead of deleting hours of copied data. Thanks to Nissulya, S0QR2, lezioul, usrflo and xet7. -
Snap/Bundle: FerretDB v1 pinned 250–400% CPU and boards took minutes to load after migration (#6467, #6468,
snap-src/bin/wekan-control,releases/ferretdb/start-wekan.sh, and the wekan/FerretDB fork). Two sides. WeKan side: with FerretDB there is no oplog, so Meteor observes every query by POLLING — and its defaults re-run every observed query 50 ms after ANY write and at least every 10 s, which on an active board multiplies into hundreds of full queries per second; with FerretDB the snap and the bundle launcher now default toMETEOR_POLLING_THROTTLE_MS=2000/METEOR_POLLING_INTERVAL_MS=30000(overridable; own changes still appear instantly, other users’ changes may take ~2 s longer). FerretDB side (fork v1.28): real filter pushdown so{boardId: X}uses the SQLite expression indexes instead of decoding the whole 53k-card collection per query, a connection pool cap of 2×CPUs (was 100 — dozens of concurrent full scans thrashing the pure-Go SQLite mutexes were the reported 821k futex calls/30 s), and inserts no longer take the registry’s global write lock. Thanks to anlx-sw, markusst1982 and xet7. -
LDAP: group search filters were double-escaped and broke group filtering (#6460, PR #6469,
packages/wekan-ldap/server/ldap.js). The group filter was post-processed with a global backslash-doubling replace, a leftover workaround from the ldapjs era that predates the properescapedToHexhex escaping. It turned already-correct RFC 4515 escapes like\5cand\28(an AD DN with an escaped comma, a group name with parentheses) into\\5c/\\28, which ldapts’ strict filter parser rejects with “Invalid escaped hex character” — so group filtering, admin-status sync and role sync failed for exactly those directories. The redundant replace is removed; injection protection is unchanged (escapedToHexstill hex-escapes the username). Thanks to ChristianMa97. -
LDAP: enabling org/team sync made every LDAP login fail with ‘forbidden’ (#6461, PR #6470,
packages/wekan-ldap/server/loginHandler.js,packages/wekan-ldap/server/sync.js). On the server, a nestedMeteor.callAsyncinherits the current method invocation’sconnection, so when the login handler called thesetUserOrgsTeamsFromLdapmethod, its admin guard saw the client’sloginconnection with no logged-in user yet and rejected the sync — and the unhandled rejection failed the whole login. (The nightly cron sync runs outside a method invocation, so it was unaffected — which is why this hid.) The login-time sync call now clears the inherited invocation context so it is a true server-to-server call, and org/team sync is additionally wrapped so an optional-enrichment failure is logged instead of blocking login. The same PR also throws the account-creation error fromaddLdapUserat the right point (it was previously used as a user object first), fixes external-avatar localization to useAvatars.writeAsync(the callback-stylewriteno longer exists in ostrio:files 3.x, so localizing avatars silently did nothing), and adds*/?wildcard support with unit tests to theLDAP_SYNC_ORGANIZATIONS_GROUPS/LDAP_SYNC_TEAMS_GROUPSallowlists. Thanks to ChristianMa97 and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.