Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (218)

v10.45

2026-07-28

v10.45 2026-07-28 WeKan ® release

This release fixes the following bugs:

Every LDAP user became an admin, and searching all boards failed. Thanks to karvox, ahlgrimma, frantzstaboeeg and xet7.

With ldap-sync-admin-groups set to ONE group, every user that logged in became a WeKan administrator (#6540). Two independent causes.

The group query builds (&(objectclass=group)(member=<the user>)). The member clause — the one that says WHOSE groups these are — was left out whenever the user entry had no value for the configured member format, and the search then ran as (&(objectclass=group)), which answers with EVERY group in the directory. So every user “was in” the admin group, and a login restricted by group let everyone in for the same reason. It answers with NO groups now, names the misconfigured setting in the log, and first tries the other usual spellings of the same value.

The comparison was split(',') matched exactly: “ti, admins” produced ” admins” and matched nothing, an unset value produced one empty string that matched a group whose name the directory did not return, and Active Directory’s case-insensitive names did not match at all. It is one shared rule now — trimmed, case-insensitive, and an EMPTY configured list can never grant admin — used by both the login path and the background sync.

“Search All Boards” answered “Server Error”, with $nin needs an array in the log (#6537): two calls in the global-search publication were not awaited, so a PROMISE was handed to Mongo where an array belongs, and both id helpers ignored the userId their callers passed, so the archived half of a search asked for the boards of nobody.

The REST route that creates a checklist item gave every item sort: 0, so a checklist filled over the API came out in an order nobody chose (#6544). It appends now, like the UI does.

OAuth2 scopes, a bind address, an IPv6 database, and two TLS switches. Thanks to lukasjelonek, sysblade, scoopex, 1977er, GuiGuiSoft, marioschulz93 and xet7.

A Keycloak login opened its popup and closed it again immediately (#6545): the snap’s default for the OAuth2 scopes was "'openid profile email'" — the quotes are part of the VALUE — so the scope sent to the provider was 'openidemail'. The default has no quotes now, and WeKan strips them anyway, so an install still carrying the old value keeps working.

There was a mongodb-bind-ip but no way to say where WeKan itself should listen, so IPv6 was unreachable (#6546, #6555): snap set wekan bind-ip='::'. And an IPv6 DATABASE could not be reached at all (#6550), because an IPv6 literal has to be bracketed in a MongoDB URI — mongodb://::1:27019/ is not a URL.

A mail server whose certificate does not match the name it is reached by (“Hostname/IP doesn’t match certificate’s altnames”) could not be used (#6551), and neither could a webhook endpoint with a self-signed certificate (#6553). MAIL_TLS_REJECT_UNAUTHORIZED=false and WEBHOOK_TLS_REJECT_UNAUTHORIZED=false say “connect anyway”: off by default, one per purpose, and never NODE_TLS_REJECT_UNAUTHORIZED, which would drop certificate checking for everything WeKan connects to. The webhook switch changes nothing else — the connection is still pinned to the resolved address and private ranges are still refused.

The snap has an application-menu entry that opens WeKan. Thanks to COOKIE-1816 and xet7.

“Wekan installed but is not visible in menu” (#6539) — because the snap installs a SERVER: it starts a daemon and shipped no .desktop file, so nothing appeared in the menu and it looked like nothing had been installed.

wekan.open is that entry. It reads the snap’s own settings and opens the address WeKan is actually serving — ROOT_URL when there is one, otherwise localhost with the configured port — so snap set wekan port=… is followed without anyone editing a desktop file. On a headless install, where there is no session to hand the URL to, it prints the address instead of failing silently.

Impersonate said "Match failed", and a localhost ROOT_URL now says what it will do to your email. Thanks to ahlgrimma, BastienGraziani and xet7.

Impersonating a user did nothing, and the log said “Match error: Expected string, got null” (#6536). The popup called the server with the id from its data context, and when there was none it called with undefined. The client does not call at all without an id now, and the method answers a missing one with “impersonate: a user id is required” instead of “Match failed”, which named neither the method nor what was missing.

And an invitation mail arrived with http://127.0.0.1/b/... in it (#6538). Every link WeKan sends is built from ROOT_URL, so when that is left at localhost the mail goes out with the sender’s own machine in it — unusable for everyone who receives it, with nothing failing and no error to look at. WeKan says so once at startup now, naming the setting and what to set it to. A warning, not a refusal: a single-machine install where localhost IS the address is perfectly valid.

One missing SWC helper stopped WeKan from starting in an older browser. Thanks to zubzhaaaw and xet7.

WeKan 10.44 in Yandex Browser died at load with Cannot find module '@swc/helpers/_/_possible_constructor_return' and nothing rendered.

An older browser is served web.browser.legacy, where SWC compiles classes down to ES5 and emits imports of its own runtime helpers. The built legacy bundle contains link("@swc/helpers/_/_possible_constructor_return", …) — the app asks for it — while the module tree beside it holds 22 helper directories and not that one, so the module system cannot resolve what the code imports.

It is the order of the build: Meteor’s scanner includes an npm package’s files from the imports it can SEE, and these imports are written by the transform afterwards. _call_super came in through another helper’s relative require and _possible_constructor_return, which nothing else requires, did not — exactly one was missing, and it was enough to stop the app.

client/lib/swcHelpers.js imports the ES5 class, iteration and async helper set from ordinary client code, which the scanner does see, and it is loaded first. Having the whole set removes the class of failure rather than this one instance: the next class written slightly differently would otherwise pull in the next helper nobody imported. The modern bundle was never affected, which is why this showed in one browser only.

A busy database no longer costs WeKan its boot, and the boot no longer keeps it busy. Thanks to Nissulya and xet7.

A snap upgraded from 6.09 to 10.44 was in a systemd restart loop at restart 72, with 8 CPUs at load 7 and SQLITE_BUSY everywhere:

error on boot.js Error [ValidationError]: Failed validation
[collection.go:191 sqlite.(*collection).UpdateAll]
database is locked (5) (SQLITE_BUSY)

Three things, each making the others worse.

A transient database error ended the boot. Meteor’s boot.js exits the process when a startup callback rejects; systemd restarts it, and the restart re-runs the same startup work against a database that is busy because the previous boot was doing it. SQLITE_BUSY means another writer had the lock — nothing is wrong with the data — so every startup callback is wrapped now: a transient database error is logged, recorded for Admin Panel / Problems, and swallowed, and that step runs again on the next start. A full disk, a refused login or a syntax error is still fatal, because none of those fix themselves.

The board-id backfill scanned every card on every boot. It streamed the whole Cards collection and issued one multi-update per card — 130,947 of them on that instance, for each of two collections — and its “anything left to do?” guard could never go quiet, because a checklist whose card was deleted has no board id to copy. It is driven by the rows that are MISSING the board id now (normally none), in bounded chunks, and version-gated like the schema upgrade beside it, so an unchanged version costs one findOne.

FerretDB answered SQLITE_BUSY where the busy timeout could not help. The driver’s default transaction is DEFERRED: it takes the write lock at its first write, and if another connection has written since its read snapshot, SQLite fails it immediately without calling the busy handler. The fork’s SQLite DSN defaults to _txlock=immediate now, so BEGIN asks for the write lock — which the 30-second busy handler does cover — and a contended writer waits its turn.

The WIP limit could not be switched off, the Attachments checkboxes were not ticks, and the sidebar rows did not line up. Thanks to Alishara and xet7.

Three UI bugs from one report.

The WIP-limit popup: “the checkbox can not be unchecked and the counter always falls back to 1” — one cause for both. getWipLimit() read the list back through ReactiveCache.getList(this._id), and on the SERVER that getter is async: it returns a promise, and a promise has no wipLimit, so the helper answered 0 for every option. enableWipLimit therefore saw a value of 0 and reset the limit to 1 on every click, and toggled !enabled where enabled was always 0, so every click turned the limit ON. The document is this — the server needs no lookup, and a toggle wants the state as it was when the click happened. The client keeps the lookup, which is what makes the popup follow the change. Apply also refuses a limit that is not a usable number instead of sending NaN, which passes check(limit, Number) and then dies in the schema with nothing to show the user.

Admin Panel / Attachments: every checkbox on those panes — Backup’s three, each storage’s Enabled and Read, the S3 path-style flag, the avatar-upload block — drew as a grey rotated rectangle instead of a green tick. They were native <input type="checkbox"> styled into WeKan’s material checkbox, which needs the browser to drop its own rendering for appearance: none; where it does not, the geometry applies and the colours do not. They are .materialCheckBox divs now, the same markup as the rest of WeKan. Two of them could not be unchecked for a second reason: their state was written as checked="{{filesystemRead}}", a quoted STRING — and checked="false" is checked in HTML.

The sidebar checkbox rows (“checkboxes and text don’t fit well”): a row is a.flex > i.fa + span, and .flex is only display: flex — no alignment and no gap — so the box glyph touched the first letter of its label and sat on a different line from it.

and fixes the following release-tooling bugs:

Two release failures that were the workflow's own fault. Thanks to xet7.

The v10.43 run failed five jobs; two of them were the workflow’s.

build-win64 failed with “wekan-10.43-win64.zip has no bundle/main.js” — four lines after 7-Zip reported writing a 283 MiB archive of 46014 files. The zip was fine; the check added the night before was not. 7-Zip lists Windows paths with BACKSLASHES, and it searched for them as a regular expression, where bundle\main.js means bundlemain.js — which nothing is called. It would have failed on every release. The listing is taken once now and searched as a fixed string, for either separator, and both forms were replayed in bash to be sure the old one matches nothing and the new one matches 7-Zip’s own output.

snap-variants did all its work and died on the last push: “remote: Permission to wekan/wekan-gantt-gpl.git denied”. Its guard checked that WEKAN_REPO_TOKEN was SET, and set is not the same as allowed — so a whole snap build burned before the token was found wanting. It asks GitHub whether the token can push to that variant repository now, and skips with a named reason if it cannot. That does not grant the rights; widening the token is a maintainer action, written up with the rest of the run’s failures in ../log/workflow/TODO.txt.

The other three failures are not the workflow’s: ppc64el and s390x cannot be built by an unmaintained QEMU action that caps at core22 (Snap-Core.md) — fixed by the next entry — and the variant pushes need a token that may write those repositories.

The ppc64el and s390x snaps build on Launchpad now, and the dead QEMU job is gone. Thanks to xet7.

Both legs failed on every release, and not because of a secret or a flake: “Your build requires a base that this tool does not support (core24)”. They were built by a snap-qemu job using diddlesnaps/snapcraft-multiarch-action, whose compiled dist/index.js caps at core22 in three independent places, whose build image has no :core24 tag, and which is unmaintained. WeKan’s snapcraft.yaml is base: core24, so the build died the instant snapcraft read it — after the version gate passed and QEMU had set up. There is no maintained QEMU multi-arch snap action that does core24; Canonical’s answer for an architecture with no native runner IS Launchpad remote-build (Snap-Core.md reads the evidence out of the action’s source rather than off the error string).

snap-launchpad’s matrix is now [ppc64el, s390x, riscv64], and snap-qemu is deleted rather than disabled.

These two arches once left Launchpad FOR QEMU, because the old remote-build legs ended in Launchpad state “Stopped” with no snap and then failed at snapcraft upload (“is not a valid file”, exit 64). Today’s job is what those legs were not: it retries the remote build 3×, requires the .snap file to exist, and uploads only when it is there. It stays continue-on-error, fail-fast: false and timeout-minutes: 180, so the price of this path — a Launchpad queue that can last hours — can neither fail the release nor cancel another architecture, and each arch publishes the moment it finishes.

It needs LP_CREDENTIALS as well as SNAP_AUTH, which these two arches did not need before. The first step names either secret when it is unset and decodes LP_CREDENTIALS, and the remote-build step says by name when Launchpad answers unauthorized, so a credential problem is one named line rather than an ordinary-looking build failure.

and improves FerretDB v1, which WeKan runs on:

MySQL and MariaDB, found by running the query catalogue against them. Thanks to xet7.

The conformance run is a live client against a live engine, and it took the mysql backend from answering nothing to answering nearly everything, in two passes.

First pass: every pushed-down filter was built as col->$.?, which is not MySQL — the -> operator takes a LITERAL path, and a placeholder there is a syntax error — so any find, update or aggregation carrying a filter failed with Error 1064. Paths are bound through JSON_EXTRACT(col, ?) now. 55 identical answers became 65, and 44 errors became 33.

Second pass, the rest of them: JSON_CONTAINS wants a JSON document as its candidate, so $eq, $ne and $in answered Error 3146 until the candidate became CAST(? AS JSON); createIndexes on a field that already had an index built either a trailing comma or the bare ALTER TABLE db.t; and the statistics query behind collStats / dbStats never aliased information_schema.tables.

MariaDB could not create a collection at all: it does not have MySQL’s -> and ->> JSON operators, so every statement carrying one failed there. All of them are JSON_EXTRACT / JSON_UNQUOTE(JSON_EXTRACT(...)) now, which both engines understand.

Third pass, after MariaDB could run at all: CAST(? AS JSON) is a syntax error on MariaDB, which has no JSON type, so the candidate goes through JSON_EXTRACT(?, '$') — one statement both engines accept. And reading the paths those fixes had just made reachable found four more: DeleteAll could never delete a document (its branch was inverted and crossed), DROP INDEX was PostgreSQL’s spelling, a boolean candidate bound as 1 would have matched nothing — silently, which is worse than the error it replaced, since a pushdown that is too narrow returns rows the in-Go filter never sees — and the per-index size query behind collStats was not valid SQL.

Date and BSON-timestamp RANGES are no longer pushed down on this backend: one answered with no documents where every other backend answered with two, and until a live EXPLAIN shows the expression MySQL needs, the Go filter is the honest answer.

The fixes are in the WeKan fork of FerretDB v1 (wekan/FerretDB, main-v1), which is what docker-compose-ferretdb-v1-mysql.yml and -mariadb.yml run.

The conformance run found five FerretDB gaps and two that stopped MySQL and MariaDB dead. Thanks to xet7.

The new “All databases (sequential)” test — one query catalogue, every backend that has an image for this CPU — was run for the first time, and it earned its keep. Everything is fixed in wekan/FerretDB, which WeKan’s default database is built from.

SQLite and PostgreSQL agree on 98 of 100 cases, and the two they do not are $slice and $elemMatch projections, which neither implements — agreement about a limitation rather than a difference between them.

Seven $group accumulators answered only “not implemented yet”, on every backend: $avg, $min, $max, $first, $last, $push and $stdDevPop. Only $sum and $count existed. All of them are implemented now, plus $addToSet and $stdDevSamp.

MySQL and MariaDB could not store anything at all. MySQL rejected every statement — the backend quoted identifiers with double quotes, which MySQL reads as string literals, so every INSERT was a syntax error. MariaDB never got that far: the driver was configured with a struct literal whose zero value refuses the native password handshake, which is what every default MariaDB root account asks for. Both fixed, with tests.

and fixes the following test-harness bugs:

The login helper logged every browser test out again, so the whole suite failed. Thanks to xet7.

The Chromium run failed 16 of its first 17 tests, each burning the full 60-second timeout inside the fixture — “Test timeout of 60000ms exceeded while setting up boardPage” — with the page showing “Board not found”.

loginWithToken installed a page.addInitScript that removes Meteor’s three Accounts keys from localStorage, so a previous session cannot resume and race the new login. But an init script runs on EVERY navigation of that page: the goto right after the login, the one that opens the board the test just logged in FOR, also started with the token removed. The client was anonymous, the seeded board is private, and the router answered “Board not found” — five times, 20 seconds each, until openBoard gave up. The one test that passed in that stretch is “user NOT added to board cannot see it”, which is what being logged out looks like too.

The clear is one-shot now: the init script does nothing unless a flag is armed, and it consumes the flag; the login arms it and reloads, so exactly the one page load the login happens on has nothing to resume, and every later navigation keeps the session. It still touches only those three keys.

Every node suite runs and is reported, instead of the run stopping at the first failure. Thanks to xet7.

test:unit:node was node tests/a.cjs && node tests/b.cjs && …, 260 suites long, and npm’s && stops at the first failing suite — so everything after it never ran, and nothing said so. One run printed “tests:508 fail:1” having skipped about 200 suites: one stale guard hid the next, one full test run at a time.

tests/run-node-suites.cjs replaces the chain. It DISCOVERS the suites (tests/*.test.cjs|js, tests/unit/*), so writing the file is registering it; it runs every suite even when an earlier one failed and lists the failures together at the end; each suite still runs in its own node process; a per-suite timeout means a hanging suite fails that suite instead of the run; and --list, --bail and substring filters are there for working on one. Discovery was compared against the old chain before switching — the same 260 suites, nothing gained or lost.

build.sh now reads the runner’s own ===== node suites: N run, M failed line for the count, instead of guessing from error text.

The 20 suites the chain had been hiding, and the four real defects among them. Thanks to xet7.

With the runner in place, all 260 suites ran for the first time: 20 failed.

Four were defects in the app. accentOf('constructor') returned Object’s constructor — a FUNCTION — because a plain lookup answers from Object.prototype, so a board colour named after any prototype member would have written a function into a stylesheet. And in RTL: the dependency overlay and its connect handle each set a physical left/right AFTER the logical property, so the physical one won and both stayed on the LTR side in an Arabic or Hebrew layout; the skip link, the avatar’s account badge, the stats value column and the theme-category label were physical too. All logical now.

The other sixteen were guards pinning a spelling or a design that had since changed — a fixed-size slice a grown comment pushed the subject out of, the Grey Icons feature that is gone, a separator count the site-theme picker made four, the board-member restriction that moved into Organizations and Teams, the hamburger that is now the last button in the flow, the report page the SERVER names. Each was corrected to what the code does now, with the reason written down. Two of them could never have passed: the “no hand-written table” regex matched the +tablePage include the same test requires, and the RTL scanner read CSS COMMENTS as declarations — the comment explaining why left: 50% needs no RTL variant was reported as a violation of the rule it documents.

A guard about ordering failed because of how a process is started. Thanks to xet7.

tests/sandstormMigrationBridge.test.cjs failed the whole node suite with “bridge is released BEFORE the importer binds the port”. The ordering in sandstorm-src/start.js is correct and unchanged; the guard was anchored on spawnSync(NODE, [IMPORTER], and every spawn in that file goes through cpuExec() now — spawnSync(...cpuExec(NODE, [IMPORTER]), …). indexOf returned -1, and “stopBridge is before -1” is false. It anchors on WHAT is spawned now, and asserts each anchor was found.

The summary was also counting that one failure twice, matching both the AssertionError line and the throw err line of the same dump.

and adds the following test menu entries:

Run everything, or all FerretDB tests, sequentially. Thanks to xet7.

Two entries in ./build.sh → Tests, and in build.bat.

Run all FerretDB tests - SEQUENTIAL runs the FerretDB subdirectory’s own build.sh test-all: unit, vet and the integration suite, one at a time. FerretDB is expected inside this repo — the “All databases” entry clones wekan/FerretDB when it is not there — and if it is missing this says so, with the clone command, rather than failing obscurely.

EVERYTHING (sequential) runs the three suites one stage at a time: WeKan’s own tests (which build a fresh bundle and start a server), then the database conformance run (which builds FerretDB from source and runs one query catalogue against every database with a Docker image for this CPU), then all of FerretDB’s tests. They share ONE ../log/<datetime>/ directory, so a run that touches three test systems still leaves its logs in one place, and it ends with a three-line verdict. Nothing runs concurrently, and the menu text says so: this takes a long time, and the reason to run it is to find out what is broken, which needs readable output more than speed.

build.bat runs releases/run-everything.sh rather than reimplementing any of it — the WeKan stage needs a POSIX shell throughout, and a second implementation would drift. That script is also the non-interactive way to run it anywhere.

Tests menu runs everything first, and says what each option really tests. Thanks to xet7.

The menu offered “ALL tests, parallel”, “ALL tests, sequential” and, eleven entries below them, “EVERYTHING (sequential)”, and nothing said what the difference was. “ALL tests” is WeKan’s own suite only — Mocha, the node unit suites, the import regression, the node E2E harness and the three browsers. “EVERYTHING” is that, then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB’s own tests.

The everything-run is now entry 1 in ./build.sh and in build.bat, the other two are named WeKan’s own tests only, and their descriptions end with what they do NOT cover.

Every option now also writes its log to ../log/<datetime>/, beside the whole-suite runs — Mocha, the import regression, node E2E, each Playwright browser, the floating-promises guard and the test counts. The new helper reuses WEKAN_LOGDIR when a larger run set one, so a whole run stays in ONE directory. Until now, running a single option left nothing to read afterwards.

The guard test pins both: entry 1 must be the everything-run, entries 2 and 3 must not claim to be ALL tests, and both scripts must log every option.

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