Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v10.45
2026-07-28v10.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 'openid … email'.
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.