This release includes breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
+2 more
Affected surfaces
Summary
AI summaryUpdates https://github.com/wekan/wekan/issues/6458, https://github.com/wekan/wekan/issues/6466, and https://github.com/wekan/wekan/pull/6462 across a mixed release.
Changes in this release
| Type | Severity | Summary | CVE |
|---|---|---|---|
| Feature | Medium |
Introduces general `cpu-exec` helper with bundled qemu-user for CPU feature emulation. Introduces general `cpu-exec` helper with bundled qemu-user for CPU feature emulation. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Feature | Low |
Adds Sandstorm .spk package to releases (non‑blocking). Adds Sandstorm .spk package to releases (non‑blocking). Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Dependency | Low |
Bumps websocket-driver from 0.7.4 to 0.7.5. Bumps websocket-driver from 0.7.4 to 0.7.5. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Performance | Low |
Reduces CPU usage of FerretDB by throttling Meteor polling intervals and limiting connection pool size. Reduces CPU usage of FerretDB by throttling Meteor polling intervals and limiting connection pool size. Source: granite4.1:30b@2026-07-16-audit Confidence: low |
— |
| Bugfix | High |
Resolves LDAP org/team sync causing all LDAP logins to be forbidden. Resolves LDAP org/team sync causing all LDAP logins to be forbidden. Source: granite4.1:30b@2026-07-16-audit Confidence: low |
— |
| Bugfix | Medium |
Restores “More” menu on cards, allowing deletion and copy‑link actions. Restores “More” menu on cards, allowing deletion and copy‑link actions. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Enables clearing “unsaved description” warning by properly saving drafts. Enables clearing “unsaved description” warning by properly saving drafts. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Prevents loss of in‑progress comment when card is closed or clicked outside. Prevents loss of in‑progress comment when card is closed or clicked outside. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Eliminates “Removed nonexistent document” client error when deleting attachments. Eliminates “Removed nonexistent document” client error when deleting attachments. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Restores rule actions that move cards to top/bottom of lists. Restores rule actions that move cards to top/bottom of lists. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Corrects desktop density regressions from mobile UI changes (font sizes, card layout). Corrects desktop density regressions from mobile UI changes (font sizes, card layout). Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Fixes subtask creation crash caused by removed sync Meteor APIs. Fixes subtask creation crash caused by removed sync Meteor APIs. Source: llm_adapter@2026-07-16 Confidence: low |
— |
| Bugfix | Medium |
Prevents endless 502 Bad Gateway when mongod cannot start due to missing AVX on CPUs. Prevents endless 502 Bad Gateway when mongod cannot start due to missing AVX on CPUs. Source: granite4.1:30b@2026-07-16-audit Confidence: low |
— |
| Bugfix | Medium |
Handles partial FerretDB migration failures without deleting the database, avoiding 502 errors. Handles partial FerretDB migration failures without deleting the database, avoiding 502 errors. Source: granite4.1:30b@2026-07-16-audit Confidence: low |
— |
| Bugfix | Medium |
Fixes double‑escaped LDAP group search filters causing filtering failures. Fixes double‑escaped LDAP group search filters causing filtering failures. Source: granite4.1:30b@2026-07-16-audit Confidence: low |
— |
Full changelog
v9.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's
internal/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 the
addSubtaskCardmethod'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/getDefaultSubtasksListAsync
using 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 made
isConstructedObject(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 as
listTitle, 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 same
rules.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 empty
boardId: ''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 the
setUserOrgsTeamsFromLdapmethod, 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.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About wekan
The Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . New English strings for new features at imports/i18n/data/en.i18n.json . Non-English translations at https://app.transifex.com/wekan/wekan only.
Related context
Related tools
Beta — feedback welcome: [email protected]