old-CHANGELOG/2026/07.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-07 | Releases |
|---|---|
| 05 | 2 |
| 06 | 5 |
| 09 | 3 |
| 11 | 4 |
| 13 | 2 |
| 15 | 6 |
| 16 | 1 |
| 17 | 3 |
| 18 | 2 |
| 19 | 2 |
| 20 | 4 |
| 21 | 5 |
| 22 | 14 |
| 23 | 7 |
| 24 | 2 |
| 25 | 2 |
| 26 | 2 |
| 27 | 3 |
| 28 | 6 |
| 29 | 3 |
| 30 | 1 |
| 31 | 1 |
This release fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/35e3c8c66">The card window opens beside its card again, on whichever side has more room</a>. Thanks to csonkaoszimt, Mintyt and xet7.</summary>"notice how the card info docks neatly right next to the card" — the 6.09 behaviour asked for in #6465. The window had been moved off the middle of the board and docked to the end edge instead, which stopped it covering the board but left it nowhere near the card it belongs to: open a card in the FIRST list of a wide board and its details are a screen away, with the whole board between them.
It now opens beside the minicard it was opened from, on whichever side has more room, and always entirely inside the visible area. A card on the left of the board opens to its right; a card on the right opens to its left.
This is horizontal only. The vertical geometry — the staggered top and the
bottom: 8px that makes the window full height — is already right, so nothing
here writes top, bottom or height, and a test fails if it starts to.
The width is passed through unchanged unless the viewport itself is narrower than the window. Shrinking the window to fit the gap beside the minicard was tried and dropped: on a 600px-wide desktop it turned a 520px window into a 240px one, and this is about where the window is, not how big it is. When neither side has room it overlaps the card instead, pushed as far towards the roomier side as the viewport allows — the card is behind it for a moment, which is recoverable; a window off the edge of the screen is not.
Nothing is placed unless there is something to place it against: a card opened
from a URL, from search, or whose list is scrolled out of view has no minicard
on screen and keeps the previous dock. Only the desktop floating window is
touched — the popup form (Board Table, search results) and the mini-screen card
have their own geometry, and the maximized window's insets are !important. A
window the user has dragged keeps where they put it; the viewport clamp still
applies to it, so a browser window that shrinks cannot leave it hanging off the
edge.
One vertical side effect had to be paid for. The stylesheet's default-position
rule is :not([style*="left"]):not([style*="top"]), so writing an inline left
switches it off — and it carried the top as well as the width. Without a
replacement, a sixth open window (past the five staggered rules, reachable with
"Open many cards at once") would have had no top at all. The replacement is
vertical-only and written BEFORE the stagger rules, so cards 1–5 still take
their staggered top from those and no vertical position changes anywhere; the
test pins that order, because the same rule written after them would win the
specificity tie and flatten cards 2–5 onto card 1.
Ticking "Propagate Members To Boards" for a team in Admin Panel > People > Teams stored the flag and added nobody. The members appeared later, when the LDAP sync cron ran the same propagation by its other, correct route — which is why the feature looked half-working rather than broken.
setTeamPropagateMembersToBoards(team, value) receives team as the selector
the client sent — { _id: … }, the same value it hands to Team.updateAsync on
the line above — and passed it whole to propagateGroupMembersToBoards, which
put it straight into the member lookup. So it ran
Meteor.users.find({ 'teams.teamId': { _id: 'abc' } })
comparing a string field against an object. It matched nobody, the function returned "0 boards updated, 0 members added", and nothing threw: the checkbox went green and did nothing. The org column beside it was identical and is fixed with it, though the report was about teams.
Both call sites pass the id now, and propagateGroupMembersToBoards normalises
whatever it is given at the one point every caller goes through — a plain id, or
a document or selector carrying one. Anything it cannot turn into an id is
refused and logged, rather than quietly treated as a group that happens to
have no members, because that silence is exactly how this survived a release. A
missing group stays quiet, since the propagate-everything pass may legitimately
have nothing to pass.
The select-all checkbox in the column header makes the same promise to the
admin, and it did not propagate at all — not even wrongly: it set the flag on
every row and stopped. It propagates now when that field is the one being turned
on, and only for its own kind, so ticking the team column does not act on the
org column beside it. The propagate-everything pass takes an optional kind for
this; its default is unchanged, so the LDAP cron and the
propagateOrgTeamMembersToBoards method still do both.
The two wiring tests required the buggy calls — they asserted
propagateGroupMembersToBoards('team', team) verbatim, pinning that a call
happened without asking whether its argument was right, and so held the bug in
place. They require the id now and fail if the whole selector comes back, and
the normaliser is exercised for real rather than read: every shape a caller
might hold an id in resolves to it, a selector with no id resolves to nothing
and warns, and an absent group is quiet.
The values survived the move; their keys did not. Every entry came out keyed by
an empty _id, which no board can match to a field definition, so the card
arrived on the destination board with every custom field showing empty.
Drag-and-drop, the Move card popup and the REST API newBoardId move all go
through the same helper, and a cross-board card copy had it too.
mapCustomFieldsToBoard() was synchronous and called
ReactiveCache.getCustomField(), which is async on the server — it awaits
findOneAsync — and synchronous only on the client. So on the server both
lookups returned Promises. A Promise is truthy, so the "field not found" guard
never fired and the "the destination board has its own definition" branch always
did, assigning newCf._id: undefined on a Promise. The schema declares
customFields.$._id as optional: true, defaultValue: '', so collection2
cleaned that undefined to '' on the way to the database. Nothing threw and
nothing was logged.
Both lookups are awaited now, and so is addBoard() — also async, and until the
two above were awaited its branch was unreachable, so its own missing await had
never been exercised.
Two more things in the same function. The entries are rebuilt rather than
mutated in place: a card copy deliberately works on a shallow copy of the card
"to avoid mutating the source card in ReactiveCache", and cf._id = newCf._id
reached straight through that copy into the source card's own entry objects,
re-keying the card being copied from. And an entry with no id is passed
through instead of looked up — getCustomField() defaults its selector to {},
so a lookup of an empty id returns an arbitrary custom field, and a blank
entry (including one blanked by this very bug) would have been re-keyed to
whichever field came first.
This is the same defect as the fix for
#6504 — an unawaited
ReactiveCache.getBoard in this same cross-board branch, which gave
"newBoard.getNextCardNumber is not a function" — one line further down. The
guard added then named the two getBoard calls and stopped there, so the third
call shipped broken for twenty more releases. That guard now checks the branch
as a whole and fails on any unawaited async call in it; run against the pre-fix
file it reports exactly this one.
The new test lifts the real function out of the model and runs it against both worlds — an async cache (the server, where this broke) and a synchronous one (the client, where it always worked) — which must give the same answer: a value whose field exists on the destination board by name and type is re-keyed to that board's definition, and one whose field does not is left pointing at the definition it had, which gains the destination board so the value still resolves.
Cards moved or copied between boards on an affected release are not repaired by this. The entry kept its value but lost the only reference to which field it belonged to, so there is nothing left to match it back with, and those values have to be set again.
</details>This release fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/91bd63107">Fix WeKan not starting in Yandex Browser: Meteor's own transpile asked for a module that was not there</a>. Thanks to zubzhaaaw and xet7.</summary>Three attempts at this had added imports to client/lib/swcHelpers.js, and
10.51 still failed in Yandex Browser with the same line:
Uncaught Error: Cannot find module
'@swc/helpers/_/_possible_constructor_return'
at a.s [as link] (…) at client-rspack.js (…:285:985)
Read the stack outward: client-meteor.js links client-rspack.js, and the
failing link() is inside client-rspack.js. But the bundle rspack writes
contains no @swc/helpers specifier at all — the built
_build/main-prod/client-rspack.js has the helper bodies inlined and the string
nowhere. The import is in the copy of that file that Meteor compiled, and
Meteor put it there: package.json says "meteor": { "modern": true }, which
turns on Meteor 3.3+'s SWC transpiler for every file; babel-compiler sets
jsc.externalHelpers: true whenever node_modules/@swc/helpers exists — and
WeKan depends on it — so the output imports its helpers instead of inlining
them; and for web.browser.legacy that transpile passes no jsc.target and an
env.targets down to IE 11, so class is lowered to ES5 and the output gains
import { _ } from "@swc/helpers/_/_possible_constructor_return". That
specifier is not in the legacy bundle's module tree, so the whole app fails
before anything runs. The modern bundle was never affected: nothing there is
lowered that far, and where a helper is needed the modern module runtime
resolves module (the esm/ file) while the legacy one prefers main
(../../cjs/_x.cjs).
That is also why the earlier fixes could not work, whatever they did.
client/lib/swcHelpers.js is in the rspack graph; rspack resolves its
imports and inlines the helper bodies, so they never become entries in Meteor's
module tree — which is the tree the failing link() searches. No import written
in app code can satisfy a link that Meteor's transpiler adds afterwards.
So /.swcrc sets jsc.externalHelpers: false, and SWC inlines each helper into
the file that needs it: the import is never emitted, so it cannot go missing.
Both readers of that file merge it over their own defaults and neither preserves
this key — Meteor keeps only jsc.target, env.targets and module.type, and
@meteorjs/rspack only jsc.target — so it reaches the Meteor transpile and
the rspack build alike. It costs one copy of a helper per file that uses one,
which is the right trade for a bundle that otherwise does not load.
.gitignore's *.sw* is there for vim's .swp/.swo and matched .swcrc
too, so adding the file was silently a no-op; !.swcrc now follows it, and the
test fails if that exception is removed or moved above the rule it corrects.
Only a browser served the legacy bundle could hit any of this, which is why it
read as a Yandex bug. useragent-ng reports Yandex Browser as the family
"Yandex Browser", webapp camel-cases that to yandexBrowser, and
modern-browsers has neither a minimum nor a chrome alias for that name —
isModern() is false for a name nobody declared — so every Yandex Browser user
was served ES5. server/modernBrowsers.js declares yandexBrowser: 18: its
major version is a year, and 18.x is already Chromium 64, past full ES2015 (51)
and dynamic import() (63). Samsung Internet, Vivaldi, Opera Mobile, Whale,
MIUI, UC Browser and QQ Browser are in the same position and are left alone on
purpose — their version numbers do not map onto a Chromium version — and the
file says so rather than looking as though they were forgotten.
The test reproduces Meteor's decision — webapp's camelCase, modern-browsers'
alias table and lookup — against real user agent strings, so it fails if the key
stops matching the family name or the minimum stops covering the versions in the
reports.
The restart loop is gone — a transient database error no longer ends the boot or the process — and the reporter's 10.49 log shows the same contention arriving in front of a user instead:
Exception while invoking method '/users/updateAsync' MongoServerError:
… [collection.go:191 sqlite.(*collection).UpdateAll]
[db.go:151 fsql.(*DB).InTransaction] database is locked (5) (SQLITE_BUSY)
That error goes to the client: the edit fails, and the user reloads to find
out what happened — which is the complaint that they only see their cards after
a reload. And it was not supposed to. models/lib/databaseErrors.js has
classified a locked SQLite as { id: 'deadlock', act: 'retry' } with "Retried
automatically." since it was written, and server/00processErrors.js says "the
write is retried by whatever issued it". Nothing read act. No code in WeKan
retried anything, anywhere.
SQLITE_BUSY is the one database error a client is meant to handle itself.
SQLite has a single writer; a second writer that arrives while the lock is held
is told to come back, and nothing was applied — FerretDB takes the lock before
the transaction, so a BUSY answer means the write did not happen. Retrying the
same write is the documented behaviour and stays idempotent: Meteor has already
chosen the _id, so a retry that raced a duplicate is refused by the unique
index rather than inserted twice.
server/00retryBusyWrites.js wraps insertAsync, updateAsync, upsertAsync
and removeAsync: a transient database error is retried with exponential
backoff and full jitter, bounded at five attempts and two seconds in total
(WEKAN_DB_RETRY_*). Bounded on purpose — retrying for longer holds a method
invocation and its slot in the connection pool while the contention it is
waiting for gets worse. When it still fails, the original error is thrown
unchanged, so a database that is genuinely stuck looks exactly as it did before
and nothing is hidden; anything that is not transient is rethrown on the first
attempt, so a full disk is not waited on four times before being reported.
It is loaded from server/imports.js, after the Meteor packages, so it sits
outside the wrappers collection2 and collection-hooks put on the same prototype
— which is how it sees the error at all, because by then the busy error is
collection2's ValidationError with the database's message inside it, the form
in the reporter's SyncedCron traces.
Writes only: every error in the reports is a write, which is what contends for
the one writer, and SQLite in WAL mode does not block a reader against a writer.
Retries are counted and summarised at most once a minute, so a contended
database is visible without the log becoming the new problem, and a write
finally given up on is recorded for Admin Panel / Problems — except an
eventlog write, which is how the recorder writes, and reporting its failure
would call the recorder from inside itself.
The reporter asked outright whether PostgreSQL is an option for a snap
installation. It is, and it has been: the advice for constant contention now
says how — snap set wekan wekan-ferretdb-handler=postgresql wekan-ferretdb-url=…, or docker-compose-ferretdb-v1-postgresql.yml in Docker
— instead of only saying that SQLite has one writer.
The test loads the real module against a stub collection prototype and exercises
the behaviour rather than reading the source: a busy write is retried and
succeeds with its arguments and its collection unchanged, the ValidationError
form is recognised too, a full disk is thrown at once and never retried, an
endlessly busy database gets the original error after a bounded number of
attempts and a bounded wall-clock time, the backoff grows and is jittered and
capped on the shipped numbers, reads are left unwrapped, wrapping twice is a
no-op, and the eventlog write is retried without reporting on itself.
A database problem arrived in Admin Panel / Problems / Database problems with an empty Category, Name and Action and a detail that said "The database returned an error WeKan has no rule for. Read the message below" — with no message below, or anywhere else on the page. The row said that something had happened and nothing about what.
Nothing was wrong with the classifier or the recorder. EventLog has a
SimpleSchema attached, and collection2 cleans every insert against it with
filter: true, so a field the schema does not declare is dropped silently on
the way to the database. The four fields the database stream uses — type
(the rule id), db (which database said it), kind and message — were never
added to that schema, so they were the four that were thrown away, and they are
exactly the four the page shows in Category, Name, Action and the message it
told the admin to read. The other four event streams were unaffected: they only
ever write fields the schema declares.
The schema declares them now, and the test pins the general rule rather than
this one instance — for each of the five event loggers, every key of the
document it inserts must be declared in the EventLog schema — so the next
field added to a logger cannot vanish the same way.
The message is shown as well: the Detail cell carries WeKan's reading of the
error and then the database's own sentence, which is the one thing an admin can
search for or paste into an issue. The stream search looks at message, db,
kind and type, so searching for "postgresql" or for a phrase out of the
message finds the row that displays it, and the unclassified advice no longer
promises a message "below" — it follows in the same cell.
Because that message is now stored and displayed, it loses its credentials
first. A database that refuses a login or cannot be reached quotes the
connection URL back, password and all, so classifyDatabaseError redacts the
userinfo of any URL in the message before returning it — keeping the host, the
port and the database name, which are what makes the message useful — and it is
done there rather than in the recorder so no future caller can forget it. The
message is also sanitized like every other stream's detail: one line, control
characters out, capped.
This release publishes the following packages that were not being published:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f3f3d21a3">One release now publishes all three snaps: wekan, wekan-ondra and wekan-gantt-gpl</a>. Thanks to xet7.</summary>People are still on the older snap names, so a release that publishes only
wekan leaves them on a stale package. Two things stood in the way, both in the
snap-variants job.
It gated EVERYTHING on WEKAN_REPO_TOKEN being able to push to
wekan/<variant>. Building and publishing a snap needs SNAP_AUTH and nothing
else; keeping the variant GitHub repositories in step with wekan/wekan is a
separate, optional thing. Because the two were tied together, a token without
push rights meant no variant snaps at all — which is why v10.48 and v10.49
published none. The guard answers two questions now: one decides the build and
the publish, the other decides only whether the repository is updated, and every
"cannot push" warning ends with "The snap is still built and published".
The variant tree is also no longer taken from a clone of the variant repository:
it is copied from the release tag the job checked out, with the snap name and
title written into BOTH snapcraft.yaml and snapcraft-core26.yaml — renaming
only the first left the core26 file saying name: wekan. So the snap is built
from what was just released, whatever state the variant repository is in.
Both variants build on amd64 and arm64 now, the two native runners the default
snap uses, and publish to stable, candidate, beta and edge like the default
snap: base: core24 with grade: stable, which is what those channels accept.
A last check before the upload refuses a build whose file name does not start
with the variant's snap name — publishing a wekan_*.snap from there would
overwrite the default snap in the store.
SNAP_AUTH has to carry the ACL for all three names, which is one
snapcraft export-login --snaps wekan,wekan-ondra,wekan-gantt-gpl.
snapcraft.yaml is what the variant job builds; snapcraft-core26.yaml is the
same snap on the next base. The sync step renamed only the first, so the core26
file in wekan-ondra and wekan-gantt-gpl kept saying name: wekan — and a
core26 build from either repository would have published itself as the DEFAULT
WeKan snap. Both files are renamed now, and a guard pins it.
It also ignores /wekan-ondra/ and /wekan-gantt-gpl/, which is where those
two repositories are cloned when they are synced by hand from this working copy.
wekanteam/wekan-gantt-gpl and wekanteam/wekan-ondra are the same WeKan as
wekanteam/wekan — the variant repositories are identical to wekan/wekan
apart from the snap name — so nothing is rebuilt: a new workflow, "Publish
variant Docker image (manual)", points the variant tag at the multi-arch
manifest the release already built and verified. Same digests, all five
architectures, seconds instead of a half-hour emulated build, and no way for the
variant image to claim a release it was not built from. releases/docker-publish-variant.sh
does the same thing from a terminal.
It is workflow_dispatch only, on purpose: the release publishes
wekanteam/wekan every time, and the variant images are published when the
maintainer decides to. The credential is checked and logged in with before
anything is pushed, and both the source manifest and the pushed tag are asked
which architectures they carry.
and fixes the following release-workflow mistake:
<details> <summary><a href="https://github.com/wekan/wekan/commit/4f872a68c">A credential the release cannot read is not the Snap Store refusing it</a>. Thanks to xet7.</summary>v10.50 built the amd64 snap, uploaded it, and died on
Publishing snap "wekan_10.50_amd64.snap"...
Unsquashing snap file 'wekan_10.50_amd64.snap'.
Credentials could not be parsed. Expected valid Ubuntu One credentials.
and the job then printed "SNAP_AUTH did not work: the Snap Store refused the
upload". The store refused nothing — snapcraft could not READ the secret. The
commonest way that happens is storing what snapcraft export-login PRINTED
instead of the file it WROTE.
The secrets check now rejects what is unambiguously not a credential — under
100 characters, or carrying snapcraft's own "Exported login" banner — in one
line at the TOP of the job instead of after a full build on every architecture.
The message after a failed upload no longer claims to know which failure it was:
it names both, and how to tell them apart from the snapcraft line above it
("could not be parsed" is an unreadable secret, 401/403 is a valid one without
the ACL for that snap name). And every re-export command in the workflow now
names all three snaps, since one release publishes wekan, wekan-ondra and
wekan-gantt-gpl.
and has the following developer-tooling changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b7d66e97a">Every maintainer script in releases/ is now a menu entry in build.sh and build.bat</a>. Thanks to xet7.</summary>releases/ holds about ninety scripts and FOUR of them were reachable from a
menu — the test run, the database conformance run, run-everything and the
CHANGELOG hash repair. Everything else existed only for whoever remembered the
file name: the bundle builds per architecture, the whole snap flow, the
Transifex scripts, the Sandstorm packaging, the tag and release helpers, the
Docker image publishing, the VM helpers.
They are one list now, in eight groups — Release, Snap, Bundles, Docker images, Sandstorm, Translations, Git and repo, Server and VM. Pick a group, pick a script; one that needs a version, a language code, a branch or a file is ASKED for it and gets it passed through. build.bat offers the same entries in the same order through Git Bash, and says so by name when bash is not on PATH.
Nine scripts are deliberately absent, each with its reason recorded in the guard: the four the Tests and Setup menus already run, the helper the others source, the three that run inside the built snap, bundle or Docker image, and the superseded ones.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74a8f931f">Twelve one-line wrapper scripts became commands, and both menus gained a command line</a>. Thanks to xet7.</summary>releases/ufw-enable.sh was one line — sudo ufw enable — and so were eleven
others. Nothing called them except the menus, so they are the command itself
now, in the same menu entries. Checked one at a time before deleting:
docker-build-deps.sh looked like the same kind of thing and is still here,
because releases/docker-build.sh runs it. The ten that are systemd, ufw, snap
or multipass are Linux-only and build.bat does not offer them, because Windows
cannot do them at all.
And both scripts run any entry without the menu, which is what makes them usable from a script or a cron entry:
./build.sh --list every name, with what it does
./build.sh release-snap 10.50
./build.sh push-translation ja
./build.sh ufw-enable what releases/ufw-enable.sh used to do
The name is the file name without its extension, or — for the twelve commands — the name of the wrapper file it replaced, so anything anybody had in a script keeps working. An unknown name says so and exits 2. Guards keep the two menus offering the same entries in the same order, and pin that every example in the help text is a real command name.
</details>and has the following documentation fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/af2683d56">Say why the snap is built on core24: it is the base that may publish to stable</a>. Thanks to xet7.</summary>The header of snapcraft.yaml still described an older policy — "the workflow
publishes it to the candidate + beta + edge channels; the stable channel is
published MANUALLY later" — which has not been true for a while: every snap job
publishes stable, candidate, beta and edge.
Both snapcraft headers now say the actual reason for the base. core24 is a
released base, so the snap can be grade: stable and the stable and candidate
channels accept it. core26 is still experimental: build-base: devel forces
grade: devel, and a devel-grade snap is refused by stable and candidate, so it
could only reach beta and edge. That is why the release builds core24 and keeps
the core26 file for testing only. A guard pins all of it, including that
snapcraft-core26.yaml is only ever renamed by the variant sync, never built.
Thanks to above GitHub users for their contributions and translators for their translations.
This release has the following release-workflow fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/9b57dfd8b">The Windows zip was never broken: a matched file was reported as missing, because of SIGPIPE</a>. Thanks to xet7.</summary>v10.49 printed the archive listing that the previous release added, and it ends
two releases of guessing about wekan-*-win64.zip:
7z t: archive is intact.
7z l -ba listed 51850 entries; the first five:
2026-07-28 21:58:39 D.... 0 0 bundle
2026-07-28 21:52:07 ..... 9 9 bundle\.node_version.txt
2026-07-28 21:58:36 ....A 8828928 3365469 bundle\bsondump.exe
2026-07-28 21:58:39 ....A 55313920 26791675 bundle\ferretdb.exe
2026-07-28 21:52:07 ..... 243 168 bundle\main.js
##[error]Process completed with exit code 141
bundle\main.js is in the zip, the archive passes its integrity test, and the
step still failed — with 141, which is 128+13, SIGPIPE. printf … | head -5:
head takes its five lines and exits, printf is killed writing into a closed
pipe, set -o pipefail (which GitHub's bash sets) makes that the pipeline's
status, and set -e ends the step. The evidence printed, and then killed the
job.
The same mechanism is the ORIGINAL failure, and that is the part worth keeping:
printf … | grep -qF -e "bundle\main.js" — grep -q exits the instant it
MATCHES, printf dies of SIGPIPE, pipefail reports 141, and if ! reads that as
"not found". So "has no bundle/main.js" was printed BECAUSE the file was found.
Both forms were reproduced exactly in bash before the fix was written.
Every listing now goes through a file and every reader reads the file: the
win64 verify, the four unzip -l "$zip" | grep -q checks on the other
platforms — the same trap, only luckier so far about the size of the pipe
buffer — and the five gh release view … | grep -qxF asset checks. A guard
pins that nothing in the workflow pipes a listing into grep or head.
and has the following test fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/18a2dbda8">The card-drag test measured the board's edge auto-scroll, which is a feature</a>. Thanks to xet7.</summary>The browser test added for #6558
ran for the first time and failed on the lane's scroll position, while
everything it exists for passed: the drag was really in progress, and neither
the canvas nor the lane carried dragscroll while it was.
It grabbed the first card of the first list, which sits within 40px of the lane's left edge. Inside that zone the drag's own auto-scroll takes over by design and moves the lane 15px per mouse event — eight moves, 120 − 8×15 = exactly the 0 the test read. That is the off-screen-list auto-scroll working, not the panning the test is about.
Panning follows the pointer 1:1 and happens anywhere; edge auto-scroll happens only at an edge. So the test drags in the middle now: it measures the lane and canvas rectangles, picks the minicard with the most room inside a 70px margin on every side, and moves it along a short diagonal clamped to that box. A viewport too small for such a drag skips the test with that reason, rather than measuring the wrong mechanism.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release has the following release-workflow fixes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/89e6189cc">Release jobs now say what actually failed: no snap, no evidence hidden, no wrong secret blamed</a>. Thanks to xet7.</summary>Four jobs of release-all.yml reported the wrong cause in v10.48, which is
worse than the failure itself — two of them sent a whole round of work after the
wrong problem.
snap-launchpad never checked whether a .snap existed. Its test was
snaps=( wekan_<version>_<arch>.snap ) followed by a count, in three steps; that
string has no wildcard, so it is a literal filename, nullglob cannot empty a
literal, and the count was always 1. s390x therefore logged an empty
Artifacts: list, then "Remote build s390x succeeded", then 'wekan_10.48_s390x.snap' is not a valid file — and the error text blamed SNAP_AUTH, although the Snap
Store never saw an upload. It is a real glob now, the match must be non-empty,
and a missing artifact and a refusal by the store are two different messages.
The Launchpad build log — the only thing that says why a build ends as "Stopped" — was downloaded and then printed only when snapcraft exited non-zero, which after the above it did not. It is printed whenever there is no snap.
build-win64 verified the zip by listing it into a variable and grepping that,
so a failure printed the verdict and none of the evidence, and a bad zip and a
bad grep looked alike. The bundle's main.js, node.exe and start-wekan.bat
are now checked in the directory before zipping, the archive must pass 7z t,
and the listing is printed with a match count — a zero count is a warning that
names the two facts contradicting it, not a failed release.
The snap-variants pre-flight asked GitHub for .permissions.push, which
describes the authenticated user's role rather than what the token may do: it
answered true one step before remote: Permission to wekan/wekan-gantt-gpl.git denied to xet7. It now asks the receive-pack advertisement, which is what
GitHub refuses for a token without Contents:write.
Finally, the one cause that leaves no trace in a build log is named in the job's
error output: snapcraft remote-build files every build under an
auto-generated Launchpad project, and a project with no licence does not qualify
for free hosting, so its builds are stopped.
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c185f5e98">Moving a card no longer pans the board at the same time</a>. Thanks to mueschel and xet7.</summary>On a board large enough to have scrollbars in both directions, dragging a card sometimes moved the card, sometimes scrolled the list, sometimes scrolled the board, and often several of those at once — so a card could not be dropped where it was aimed.
A board runs THREE drag-scroll implementations over the same pointer: the
dragscroll library, bound separately to .board-canvas, to every swimlane and
to every lane; the lane pan in swimlanes.js; and jQuery UI sortable, the one
that is supposed to move the card. Nothing stood any of them down while a drag
was in progress. swimlanes.js did try, for list drags, but reset the library
FIRST and removed the class afterwards — a reset binds to whatever carries the
class at that moment, so every listener stayed — and it left the canvas tagged
anyway. The lane pan ignored the nodragscroll marker the library honours, so
pressing a minicard, which carries that marker exactly because it is the card's
drag handle, started a horizontal pan that ran alongside the card drag.
Drag-scrolling is now suspended for the whole drag — class off, THEN reset — and
exactly the elements it was taken from get it back, so a board route does not
gain panning it deliberately turned off. Every sortable on the board (cards,
lists, swimlanes) suspends when a drag starts and resumes when it stops, a
window mouseup / touchend / dragend restores in case a drag never reaches its
stop handler, and a swimlane re-rendered mid-drag does not re-arm panning under
the pointer. The lane pan refuses to start on a drag source, and gives way if a
drag begins after the press. The drag handles themselves are marked
nodragscroll: a press on a handle moves the item, it never pans the board
underneath it.
A guard pins the order of the two steps, the restore-what-was-suspended rule, the safety net, the suspend/resume in every sortable and the two markers; a browser test drags a card sideways on a 12-list board and pins that neither the lane nor the canvas scrolls while it moves, and that panning is back afterwards.
</details>and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/9d045d3a3">Every CHANGELOG commit link points at a commit that exists, and the repair no longer breaks good ones</a>. Thanks to xet7.</summary>106 of the 3014 commit links in this file pointed at nothing: a rebase, amend or squash after the entry was written, or a 40-character hash whose first nine characters were right and whose tail was invented. Clicking one gave GitHub's 404 with no way to tell which change it had meant. Each was resolved against the history — by subject and author date, by prefix, or by reading the entry's own text against the commit messages of the release it sits in — and every link now resolves to a commit in the repository.
releases/fix-changelog-hashes.sh, which build.sh runs after a rebase and
release-all.sh runs before a release, decided a link was stale when its commit
was not an ancestor of HEAD. A commit that lives only in an old release tag is
not on this branch, yet GitHub serves it fine, so that rule repointed working
links to a DIFFERENT change — two 2019-era links were repaired that way and had
to be put back. Staleness is now "no ref in this clone reaches it", the
replacement is matched on subject AND author date (a rebase keeps the author
date), then on the patch itself, then by prefix, and a subject shared by several
commits is never used to pick one. --all-sections checks the whole file rather
than the section being released.
The 18:10 run: 267 node suites with 2 failures, all three browsers clean, Mocha 500 passing, FerretDB unit and integration green, and the conformance run has all four backends — SQLite, PostgreSQL, MySQL and MariaDB — answering identically.
changelogFormat was right, and about the entries written that day: two lines
START with #, which markdown renders as a heading and which splits the version
list. The issue numbers are escaped and the lines rewrapped so none begins with
one. boardsVisibilitySettings pinned a save handler that no longer saves the
display toggle — that toggle writes itself from its own click handler — so the
guard now checks what the code actually does.
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/032722fcc">Trust a certificate instead of disabling verification, and escape every metacharacter</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>Alert #430, js/disabling-certificate-validation, High:
reqOptions.rejectUnauthorized = false in the outgoing-webhook path. It is
right, and the switch is gone.
rejectUnauthorized: false accepts ANY certificate — including the one a man in
the middle presents — which is the attack the TLS handshake exists to stop. What
the reports behind it actually need is not "verify nothing", it is "this
certificate is legitimate", so that is what WeKan takes now:
WEBHOOK_TLS_CA_CERT (the certificate or CA to TRUST for outgoing webhooks),
MAIL_TLS_CA_CERT (the same for the mail server) and MAIL_TLS_SERVERNAME (the
name to verify the mail certificate AGAINST, for a wildcard that covers one level
fewer than the host has).
Each is the PEM itself or a path to a file holding it. A self-signed certificate is its own issuer, so naming it here is exactly what makes it valid — and verification stays ON, the chain is still checked, the hostname is still checked. A path that cannot be read is not fatal: it says which setting failed and keeps the system trust store. The SSRF protections are untouched — the address is still resolved once and pinned, private ranges are still refused, redirects are still blocked.
Alert #429, js/incomplete-sanitization, High: a test escaped dots only when
building a regular expression, so a backslash in the value could change the
meaning of the pattern it was spliced into. It escapes every metacharacter now,
the backslash first, with the helper the other test files already use.
and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/edd290433">A rejected promise no longer ends the whole server when the database is busy</a>. Thanks to Nissulya and xet7.</summary>The snap that was restarting in a loop (#6533) showed the earlier fixes working. It also showed the server dying regardless:
SyncedCron: Fatal error encountered (unhandledRejection): ValidationError:
... sqlite.(*collection).UpdateAll ... database is locked (5) (SQLITE_BUSY)
SyncedCron: Received UNHANDLED_REJECTION signal - cleaning up running jobs
systemd: Main process exited, code=exited, status=1/FAILURE
quave:synced-cron installs a process-wide unhandledRejection handler that
calls process.exit(1). So ANY unhandled rejection — including one write losing
a race for the SQLite write lock, which is transient by definition — killed the
whole server, and systemd restarted it into a database the restart had made
busier. The restart counter reached 73.
WeKan takes that decision back. The package is deliberately polite about it: it
only cleans up and exits if (process.listenerCount('unhandledRejection') === 1)
— when nothing else has an opinion — so having one is the documented way to stop
it. A transient database error is logged and recorded for Admin Panel / Problems
and WeKan keeps serving; anything else is logged with its full stack and WeKan
still keeps serving, because ending everybody's session over one rejected promise
is a larger failure than the one being reported. An uncaught EXCEPTION still
exits unless it is a transient database error: a process that threw out of a
place nobody handled can be holding half-applied state.
Nothing is silenced — every rejection is logged, and the database ones are
counted in the database event stream where Admin Panel / Problems shows them.
and has the following test-harness fixes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/9978219b2">Four guards that pinned an older shape, and a browser test that named the wrong half</a>. Thanks to xet7.</summary>The Visibility saves share one "is this input on screen" check now, so the guard
looks at the helper rather than at each call site. /information redirects the
FlowRouter way, like /translation before it. The phone rules carry
!important, because the desktop rule they override is more specific. And
"the newest release" in the changelog guard means the newest RELEASE — an
Upcoming section may sit above it, it is checked by its own test, and a release
needs at least ONE entry, since "more than five" measured the day's workload
rather than the format.
The background-image tile test waited only for
.board-list-item.has-background-image, which cannot tell "the board has not
arrived in minimongo yet" from "it arrived without the class"; under a
three-browser run WebKit reported the second when it was the first. It waits for
that board's own tile first, so a future failure names the actual problem.
and updates the backlog:
<details> <summary><a href="https://github.com/wekan/wekan/commit/11c946301">TODO Later: ten issues closed since it was written, and what testing FerretDB on MySQL answered</a>. Thanks to xet7.</summary>Every issue in # TODO Later was checked against GitHub. Ten are no longer open
and are gone from the list: issues #3138, #3252, #3276, #3378, #3748,
#3828, #4055, #4774, #5149 and #6511. The "already correct in the current
code" category went with them, because it held only the two that are now closed.
Issue #6509 — "please test FerretDB v1 with MySQL, MariaDB and SAP HANA" — is
mostly answered rather than pending: the conformance harness runs one catalogue of 100
queries against every backend with an image for this machine, and MariaDB now
answers identically to SQLite on 98 of them, the two exceptions being the
$slice / $elemMatch projections that no backend implements. Getting there
took a dozen fixes in the fork. MySQL's confirming run is still pending and SAP
HANA is untested — its image needs a licence acceptance — and the entry says so
instead of implying the whole request is done.
Finnish gained databaseReportTitle ("Tietokantaongelmat"). The other 40
placeholders in that language are numbers, symbols, product names and a font
name, which are the same in Finnish — the count of "untranslated" strings is
mostly that. The search-operator abbreviations (b:, l:, s:) are left in
English on purpose: translating them changes how a search is TYPED in that
language, which is a decision for the maintainer, not a wording fix.
and updates translations:
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c1bb2a6a5">The SWC helper fix shipped in 10.45 was deleted by the bundler, because the package says it may be</a>. Thanks to zubzhaaaw and xet7.</summary>WeKan 10.45 still failed in Yandex Browser with Cannot find module '@swc/helpers/_/_possible_constructor_return' (#6556), and the fix for it was in
that build.
@swc/helpers declares "sideEffects": false — a promise to the bundler that
importing one of its modules changes nothing observable, so an import whose
bindings are never READ may be removed entirely. import '@swc/helpers/_/_possible_constructor_return'; is exactly that: every line of
the new file was dropped, the package subdirectory was never pulled into the
bundle, and the legacy module tree came out as before. The fix compiled to
nothing and looked like it had been applied.
Every helper is imported BY NAME and read now — collected into an array whose
length is written to window.__wekanSwcHelpers. That is an observable effect,
so no optimizer may remove the imports that feed it, and the global says in one
word whether the helpers reached the bundle at all if this ever happens again.
and fixes the following test-harness faults:
<details> <summary><a href="https://github.com/wekan/wekan/commit/64971491a">A flaky WebKit navigation and a leftover container were reported as failures they were not</a>. Thanks to xet7.</summary>Two things in the test harness answered the wrong question, and four guards had drifted.
WebKit: page.goto answered "WebKit encountered an internal error" — the
browser under the load of a three-browser run, not WeKan. openBoard retries
five times for exactly that, but a THROW from the navigation escaped the loop, so
one flaky goto failed a test with four attempts left. The navigation is inside
the try now, and the final message says which of the two happened: a board that
never rendered, or a navigation that never succeeded.
Database conformance: PostgreSQL and MySQL were reported as "container did
not start" when the log said failed to bind host port 127.0.0.1:35432: address already in use — a container left behind by an interrupted earlier run, not a
broken image. The script removes containers named wekan-conformance-db-* (its
own; a docker compose stack is named differently and is never touched) before
it starts, and a docker run that hits a taken port moves to the next free one
and retries.
The guards: a template lookup that knew only one of the two quote styles the file
uses; a changelog summary one character over the limit, and two summaries linking
the FerretDB commit they describe, which the guard now accepts because that fork
is where the code lives; a redirect asserted as FlowRouter.go when a
triggersEnter redirect is FlowRouter's own way to do it; and "every rule
mentioning the bell or the avatar", which is three rules, not the two that place
them.
This release fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/e901ebd3f">Every LDAP user became an admin, and searching all boards failed</a>. Thanks to karvox, ahlgrimma, frantzstaboeeg and xet7.</summary>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.
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.
"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.
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.
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 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.
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:
<details> <summary><a href="https://github.com/wekan/wekan/commit/520254768">Two release failures that were the workflow's own fault</a>. Thanks to xet7.</summary>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.
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:
<details> <summary><a href="https://github.com/wekan/FerretDB/commit/f4b1450e">MySQL and MariaDB, found by running the query catalogue against them</a>. Thanks to xet7.</summary>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 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.
</details>and fixes the following test-harness bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/7b017c56a">The login helper logged every browser test out again, so the whole suite failed</a>. Thanks to xet7.</summary>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.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9eedab13b">Every node suite runs and is reported, instead of the run stopping at the first failure</a>. Thanks to xet7.</summary>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.
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.
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:
<details> <summary><a href="https://github.com/wekan/wekan/commit/24ffa4bd3">Run everything, or all FerretDB tests, sequentially</a>. Thanks to xet7.</summary>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.
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.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following test:
<details> <summary><a href="https://github.com/wekan/wekan/commit/7a3783fbe">Tests menu runs every database this CPU can, and checks they answer the same</a>. Thanks to xet7.</summary>FerretDB v1 translates one MongoDB query into five different SQL dialects, so
"WeKan starts on MariaDB" says very little. The question that decides whether a
backend can be trusted with a board is whether {n: {$gt: 5}} returns the same
documents, in the same order, as it does on SQLite — and nothing was asking it.
./build.sh → Tests → All databases (sequential) now does, and build.bat
has the same entry, running the same script rather than a second implementation
that would drift.
It builds FerretDB v1 from source first: the FerretDB subdirectory is cloned
from [email protected]:wekan/FerretDB if it is not there, updated if it is, and
built through its own build.sh, which installs the Go toolchain and the module
dependencies when they are missing — so the tests run against the newest code,
not a downloaded release. Then, for each backend whose database image has a
build for THIS CPU — asked of the registry with docker manifest inspect, so no
table can go stale — it starts that database, runs the freshly built FerretDB
against it, runs the whole catalogue and stops everything. Sequentially, because
they all use the same FerretDB port and a database under test should not be
competing with three others.
The catalogue is 100 cases in 15 groups, taken from FerretDB v1's own handler sources rather than from MongoDB's manual: every query, update and bitwise operator, the aggregation stages and accumulators, projection, sorting, paging, count, distinct, indexes and uniqueness, capped collections — which is how the OpLog exists at all — and the commands whose answers may legitimately differ. The seed data is deliberately awkward, because tidy data lets a broken translation pass. Answers are normalised and compared byte for byte against SQLite, so document ORDER counts; two backends failing the same way is agreement about a limitation, one answering where another fails is a difference.
Everything lands in ../log/<datetime>/ with every other test run, including
db-conformance-report.md. SAP HANA is opt-in behind WEKAN_CONFORMANCE_HANA=1
— amd64-only, ~16 GB of RAM, SAP's licence — so a menu choice cannot start it by
accident.
Three things the first real run, on arm64, found.
The FerretDB it built could not start: panic: commit.txt value ... != vcs.revision value .... Go stamps the VCS revision into the binary and
FerretDB's build/version panics when it disagrees with the committed
commit.txt, which only its generator refreshes — so every commit made after
the last refresh built a binary that panicked, whatever the change was. Fixed in
the fork: its build.sh build regenerates them now, as the release build always
did.
Ports: FerretDB listened on 27017, which is where a dev server's database lives
and where the compose files publish FerretDB. So this could not run beside
anything else and, worse, could have pointed the tests at somebody else's
database and rewritten it. It listens on 37017 now and publishes the database
server on 35432, both moved on if something is already listening, both settable
with WEKAN_CONFORMANCE_PORT and WEKAN_CONFORMANCE_DB_PORT, and its
containers are named per run so a stack started with docker compose up is
never reused or stopped.
And Ctrl-C only killed whatever was in the foreground — a registry lookup, a sleep — after which the loop carried on and reported the interrupted lookup as "NO linux/arm64", which is a lie about the image. An interrupt ends the run now, and the image check has three outcomes rather than two: has it, does not have it, could not ask. A FerretDB that will not start also prints the last lines of its log, because the reason is usually one line.
</details>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/fbf88fa19">A Docker Compose file for every FerretDB v1 backend, generated from one source</a>. Thanks to xet7.</summary>docker-compose.yml runs FerretDB v1 on its embedded SQLite. The same FerretDB
can store into PostgreSQL, MySQL, MariaDB or SAP HANA instead, and there was no
compose file for any of them — those backends were reachable only by editing
the default file by hand. There is one per backend now:
docker-compose-ferretdb-v1-postgresql.yml, -mysql.yml, -mariadb.yml and
-sap-hana.yml.
They differ ONLY in the database. The same WeKan image, the same ~700 lines of
environment and the same comments explaining them — they are generated from
docker-compose.yml rather than copied, and
tests/dockerComposeBackends.test.cjs compares each file's WeKan service
against the default one line for line. A
copy drifts on the first setting somebody adds to one file, and then a user
following the PostgreSQL file gets a differently configured WeKan than the
SQLite file gives them.
Each names its own database service, runs FerretDB with the matching
--handler, and says at the top how to start, follow and stop THAT file with
docker compose -f. The images are the newest published — postgres:18,
mysql:9, mariadb:12 and SAP's saplabs/hanaexpress — with the LTS
alternative named in a comment where there is one. PostgreSQL is confirmed
working with Meteor 3; MySQL, MariaDB and SAP HANA say in their own headers
that they are experimental, because they are. SAP HANA also needed the
wekan/FerretDB release binaries to be
built with the ferretdb_hana build tag — without it --handler=hana is an
unknown handler — so that build now passes the tag.
build.sh and build.bat offer every docker-compose*.yml in the repo now,
not four of the eight, and the test fails if a file exists that neither menu
can start, or if a menu offers a file that does not exist.
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c1d62cde">Admin Panel reports show the whole instance, not the admin's own boards</a>. Thanks to xet7.</summary>Boards Report was empty while the Cards report beside it listed cards from
thousands of boards. It published userBoardIds(this.userId) — the boards the
ADMIN is personally a member of — and on an instance whose admin is not a board
member that is nothing at all. Until the pagination fix below, the pane hid it:
it rendered every board in minimongo, which the All Boards page had already put
there. The Files report was scoped the same way, to the cards that admin can
access.
Both cover the whole instance now, as Cards, Broken cards, Rules, Impersonation
and Recovery already did. That membership selector was also what kept those two
publications honest, so both gained an isAdmin guard at the top —
boardsReport asked only for a logged-in user before — and each count method
counts the same set its publication pages. Files and Rules name their page now
too: opening one card puts its attachments in minimongo, opening a rules editor
puts that board's rules there, and neither belongs in a report page. The Files
page is sorted by name, because paging with no sort is paging over natural
order — where a document can appear on two pages or on none — and that sort has
an index.
Admin Panel / People showed the admin on every one of its 578 pages, and Admin
Panel / Problems / Broken cards was one endless page under a pager that said
"1 / 1". The same bug in two shapes: the pane subscribes to a publication that
sends exactly one page, then reads that page back out of minimongo — which
holds far more than the page. The logged-in user's own record is always there,
because accounts publishes it, and so is every card of every board the admin
has opened; a plain find(query) cannot tell those apart from the page. So
the server names the page now: getPeoplePageIds returns the ids of the
People page with the publication's own selector, sort and window, and the
Broken cards, Cards and Boards report publications send the ids of the page
they just sent alongside it. Each pane renders that list, in that order, and
nothing else.
Every paginated page in WeKan loads ten rows at a time, from one constant, including the pages that draw a pager of their own — the reports and the event streams, the four People panes, Translation, the search pages and the archive. Table.md records the rule and its two deliberate exceptions.
And the counting behind those pagers is index-backed. Every count already
asked the database for a count rather than fetching the rows, but several
counted and paged on unindexed fields: People sorts users newest-first, so on
an instance with 14000 users every page of ten sorted all 14000 first, and
Broken cards asks an $or over boardId/swimlaneId/listId/type, which
uses no index at all unless every branch has one. The missing indexes are
created at startup by the idempotent ensureIndex, so an upgrade adds them by
itself. tests/adminPageRows.test.cjs and tests/paginationIndexes.test.cjs
pin all of it.
and has the following release-tooling changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/ec8f706f7">The release workflow says what worked, what failed, and why</a>. Thanks to xet7.</summary>release-all.yml could fail in ways that left no explanation, or no failure at
all: a version bump that committed nothing, a release published with empty
notes, a bundle zip that exists but holds no bundle/main.js, an upload that
reports success and leaves no asset behind, and a multi-arch image whose
manifest is missing a CPU — which nobody notices until a user on that CPU is
told there is no matching manifest. Each of those is checked now, and each
check says the same three things when it fails: what was expected, what was
actually there, and what to do about it. On success it prints one OK: ...
line naming what it verified, so the log shows what worked and not only what
did not. Every job also ends with a "Job result" step that runs whatever
happened, so the run summary is one line per job — OK, FAILED or CANCELLED —
instead of eighteen logs to open.
Only three jobs said anything about their secrets. Everywhere else a missing
one surfaced deep inside a tool — a login that was refused, a checkout that
404'd — with nothing naming the secret, and the docker job did it only after
the multi-arch build had already spent half an hour on an image it could not
push. Every job that needs a secret now checks it in its FIRST step and names
what is missing. Where the answer is cheap it also checks that the secret
WORKS, which is the other half of the question: WEKAN_REPO_TOKEN is asked
whether it can push to the repository it is for, the three registry credentials
are decoded and logged in with, and the base64 ones must decode. What only the
far end can answer is answered where it is used — the Snap Store uploads name
SNAP_AUTH when they are refused, and a Launchpad build that fails with an
unauthorized in its log says LP_CREDENTIALS may be why.
snap-qemu (ppc64el, s390x), snap-launchpad (riscv64), snap-variants,
ucs and nextcloud were hard-disabled with if: ${{ false }}, so a release
never said anything about them at all. They all carry continue-on-error: true,
so running them cannot fail a release or hold up another job — the point of
running them is to SEE what they report. The three that skip their work while
their secrets are unset — snap-variants, ucs, nextcloud — do it with a
::warning:: now instead of a quiet ::notice::, so the missing secrets show
in the run summary rather than only in a log.
and has the following developer-facing change:
<details> <summary><a href="https://github.com/wekan/wekan/commit/7f63f0422">A test pins that a per-tenant admin sees no report data</a>. Thanks to xet7.</summary>Admin Panel / Problems is instance-wide, so every report publication and count
method asks for the SITE admin flag user.isAdmin — not canOpenAdminPanel,
which a per-tenant Global Admin passes — and problems is not one of a tenant
admin's tabs. That is what keeps the Boards and Files reports honest now that
they cover the whole instance instead of the admin's own boards, so it is
pinned rather than left to be re-derived. The one admin list a tenant admin
does get, People, is tenant-scoped in all three places that decide what it
shows: the publication, the page-ids method and the count all go through
peopleScopeSelector, so its pager cannot count rows its page may not show.
and improves the documentation:
<details> <summary><a href="https://github.com/wekan/wekan/commit/fea3c432e">docs/Databases is one directory per database, each with a README</a>. Thanks to xet7.</summary>It was a flat list in which the database was a filename prefix, in three
spellings — MongoDB-Driver-System.md, mongodb-avx-qemu.md,
MongoDB_OpLog_Enablement.md — plus FerretDB2-PostgreSQL.md and a
ToroDB-PostgreSQL directory. Somebody asking "how do I run WeKan on
PostgreSQL" had to read the prefixes to work out which files were even about
their database.
Now there is a directory each for Migrations, MongoDB, FerretDB — with
1 and 2 inside it, because v1 and v2 are different products with different
backends — and ToroDB, with PostgreSQL inside it. Each file keeps its name
minus the redundant prefix, and the old PostgreSQL.md, which was an index of
FerretDB and ToroDB rather than a document about PostgreSQL, becomes the
directory's README.md.
Every directory has a README.md saying what is in it and where to go next —
including FerretDB/1, which had no documentation at all although it is
WeKan's default database and had just gained four more backends. A link to a
directory points at the DIRECTORY, not at its README.md, because that file is
what is opened by default anyway. Every link into the old paths is updated: in
the docs, in docker-compose-ferretdb-v2-postgresql.yml and on the wekan.fi
website, along with the relative links inside the moved files, which gained a
directory level. tests/docsDatabases.test.cjs pins the layout, the READMEs,
the directory links, that every relative link resolves, and that nothing
outside CHANGELOG.md still names an old path.
WeKan runs on every CPU Node.js runs on, and this fork of FerretDB v1 exists
because MongoDB publishes no server for most of them. Alternatives.md in
docs/Databases/FerretDB/1
answers the two questions that keep coming back: which databases even have
images for those CPUs, and what would be missing in FerretDB v1 before it could
store into one.
The architecture table is read from each registry's own manifest rather than from documentation, and it says plainly what that data shows: PostgreSQL is the only widely-portable database server — the only one publishing ppc64le, s390x and riscv64. MariaDB covers ppc64le and s390x, MySQL neither, and MongoDB itself and upstream FerretDB 2 are amd64 + arm64 only. This fork's own image covers nine platforms.
What a new backend needs is taken from the code: a pure-Go driver, because the
binaries are built CGO_ENABLED=0 and that is what makes one build serve nine
architectures — which is why Oracle and IBM Db2 are out despite Db2's ppc64le
and s390x images; the three internal/backends interfaces, none of them
stubbable; a metadata registry; the SQL features the translation actually uses,
including the record-id column that capped collections — and therefore the
OpLog — are built on; correct MongoDB semantics on top of that; and a live
integration run, which is the whole difference between the confirmed and the
experimental rows. Plus the shortcut: a database that speaks the PostgreSQL or
MySQL wire protocol needs no new backend, only an existing one that survives
its dialect — CockroachDB brings s390x that way, for the price of a test run.
A follow-up groups them, so the reader is not left to find the pattern: they are not fifteen questions but seven families — PostgreSQL-wire, MySQL-wire, enterprise SQL, embedded, columnar, key-value, and what already speaks MongoDB — and inside a family the answer is one answer. The page opens with all of it in five sentences and one table, because the conclusion (verify the three backends that already exist rather than write a fourth) was previously visible only to whoever read to the end.
</details>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/0c9b7b7fa">Every popup had 10px of empty space above it that nothing asked for</a>. Thanks to xet7.</summary>The space above the first row of Board Settings, Board View, Member Settings,
Sort Cards, Change Watch, Change Visibility and Swimlane Actions — and above
the Title field of Rename Board — was much larger than the space beside it.
None of it came from popup.css: peopleBody.css and translationBody.css
each carried a bare .content-wrapper { margin-top: 10px }, and
.content-wrapper is not a settings class at all — the only element in WeKan
with that class is the scrolling body of a POPUP, so those two strays pushed
the content of every popup in the app down by 10px. They are gone, and the gap
that is left is 6px under the header plus the content's 12px of padding = the
same 18px the content has beside it. Change Visibility and Change Watch also
lay their rows out as three left-aligned columns now — icon, name, description
— with the check mark of the active row in a track of its own, so it can no
longer push that row's description out of line with the others.
tests/popupSpacing.test.cjs pins both, and fails again if any stylesheet
styles a popup-internal class without saying which popup.
The second header bar centred its items inside a content box that had 7px of
padding above and none below, so everything sat half that distance high — most
visibly the board title, the tallest item in the row. The button groups then
carried a 3px top margin, which is a shift and not centring, so they hung below
the title's centre line. The padding is symmetric now, wrapped rows are
separated by row-gap instead of that margin, and the title centres on its own
text rather than on a line box far taller than it. The sidebar hamburger had
2px of margin on one side and 8px on the other, which left an empty strip
between it and the end of the bar; equal margins put it in the middle of the
space between its divider and that edge.
From the dev-server log, on a plain attachment upload: [onAfterUpload] filename hardening failed: TypeError: Cannot read properties of undefined (reading 'gridFsFileId'). getFileStrategy read
fileObj.versions[versionName].meta.gridFsFileId to decide the storage, and a
freshly uploaded file has no meta on its version — only a GridFS one does —
so that read threw on EVERY upload. onAfterUpload catches and logs, so the
upload succeeded and looked fine while the two things that hook does were
silently skipped for every new file: detecting the real mime type and
correcting the stored filename from it. All three optional places are read
defensively now, and the decision is unchanged where the data is there.
Reported after upgrading from WeKan 6: an attached image is visible in board
view, downloads correctly from the card, and yet the card view shows an empty
white box that cannot be maximized — while cards created after the upgrade are
fine. The card view does not look at the file, it looks at the flags
Meteor-Files writes at upload time (if(isImage) img … else span= extension),
and an attachment that came through a migration can arrive without them, and
often without extension or type either. The download button worked because
a download needs no flag. models/lib/attachmentKind.js is now the one place
that derives the kind — from the mime type where the document has one, in any
of the fields that have carried it, and from the file name where it does not; a
flag the document states is believed, and the name is consulted only when no
type is stated. The gallery, the "add cover" menu and the viewer all ask it,
and a startup repair writes the same answer back to the documents so the REST
API and the exports agree with the screen. tests/attachmentKind.test.cjs pins
the rules.
and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1013c87f7">Two test-harness fixes the newest run found</a>. Thanks to xet7.</summary>The node suites got four assertions further and stopped in the same file: two
more cpuExec expectations still spelled the qemu-wrapped command as a bare
name, which cpu-exec resolves to an absolute path now. And WebKit failed
33-board-domains with "Unexpected userId after login" — the admin's id, when
the test had just switched to the non-admin: loginWithToken logged the new
user in over a session that was still someone else's, so the poll could not
tell "not landed yet" from "landed on the wrong user". It logs the previous
user out first and waits for an empty session, with a 15s settle deadline for a
loaded three-browser run.
First real run of the node suites — they had never run anywhere until the "Run
ALL tests" change — and cpuExec.test.cjs failed, which is what that change
was for. Two real things behind it. qemu-user does not search PATH: it opens
the file it is given, so a bare command name (cpu-exec --features x86_64=avx mongod …) reached qemu as a relative path that does not exist and qemu exited
1 having run nothing — on exactly the CPU this helper exists for. cpu-exec
resolves the name with type -P first and hands over an absolute path; an
absolute path is passed through as before. And the test asked for "no qemu"
with PATH=/usr/bin:/bin, which is only true on a machine without qemu-user
installed; it now builds a PATH holding exactly the tools cpu-exec uses and
nothing else, so the case is the same everywhere.
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/302bd9041">A removed DDP session no longer crashes the server on its 101st message</a>. Thanks to bluetopaz1204, Nissulya and xet7.</summary>From the comments today, on 10.40 with real users: TypeError: self._pendingRemoveFunction is not a function at Session.send, then Main process exited, code=exited, status=1/FAILURE and Scheduled restart job, restart counter is at 4. It is a state-machine hole in Meteor's ddp-server:
_removeSession sets messageQueue = [] and a remove function; the remove
function clears itself and deletes the session but leaves the queue, which is
truthy — so Session.send keeps queueing for a session that no longer exists
and the message past maxMessageQueueLength (100) calls null. It runs from an
Immediate with no try/catch above it, so it is an uncaught exception, and
synced-cron exits the process on one: every user disconnected, new cards only
after a reload, systemd restarting — which reads as "WeKan is slow and the CPU
is high". A queue with no remove function is now dropped instead of pushed to,
so send() takes its ordinary path; the grace-period queue for a real
reconnect and a legitimate overflow both behave as before.
tests/ddpSessionSendGuard.test.cjs replays the upstream state machine,
including that the unguarded version really does throw on the 101st message.
and has one icon set:
<details> <summary><a href="https://github.com/wekan/wekan/commit/3487d2519">One icon set: Font Awesome, and the Grey Icons feature is removed</a>. Thanks to xet7.</summary>WeKan used colourful Unicode emoji as its icon set in 8.00–8.24 only; before
and after, the icons are Font Awesome 4.7. A handful had survived that change —
every notification type, the board star, the vote thumb, the spent-time badge,
the list width toggles, the bookmarks star, the back arrow, the sidebar's plus
and hash, the four date badges, the mobile drag handle, the multi-selection
tick, the gantt day markers — each a different picture on every platform, at a
size and colour the stylesheet does not control. They are Font Awesome glyphs
now, and "Grey Icons" in Member Settings, which existed to grey exactly those
emoji with a MutationObserver over every rendered subtree, is removed whole:
menu entry, handler, method, schema field, publication, stylesheet, the string
in all 147 language files, the API-spec field and the docs mentions. The
language-picker flags stay: Font Awesome 4.7 has no flags.
tests/fontAwesomeIcons.test.cjs fails on an emoji in any template or CSS
content:, and on any leftover of the removed feature.
and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/fa831e982">"Run ALL tests" now runs all the tests, in build.sh and in build.bat</a>. Thanks to xet7.</summary>Two gaps that together meant most of the suite never ran anywhere. The flow ran
six jobs — mocha, the import regression, the Node E2E harness and the three
browsers — and no npm unit script at all, so everything in test:unit:node and
test:unit:all (the ~165 .cjs guards plus the sticker, Trello and OAuth2
suites) was never executed by "Run ALL tests". Both scripts now have a unit
job that runs meteor npm run test:unit:all, counted, waited for and reported
with its own log. And the scripts themselves were incomplete: 72 suite files
existed that no npm script mentioned — they are all registered now, and
tests/testsAreRegistered.test.cjs fails when a plain-node suite is in no
script or when the flow stops running it. Not verified here: with no node in
this environment those 72 suites have never been executed, so some may fail on
the first real run — that is the information this buys, not a regression it
introduces.
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/64c1f426a">The right sidebar starts below the header, in both modes</a>. Thanks to xet7.</summary>Its upper part — the panel title, the tabs, the members row — was underneath
the two header bars, so the sidebar appeared to start in the middle of itself.
On a phone it is position: fixed, and it has to be: the board behind it is
wider than the screen, so an absolutely-positioned sidebar pins to the far
right of the BOARD and shows as a narrow strip. But fixed means against the
viewport, and it was pinned at top: 0 — where the header bars are. There is
no number to put there: the header is the quick-access bar plus a board bar
whose buttons wrap to one, two or three rows depending on language and window
width. So the header measures itself into --wekan-header-height and the
sidebar starts at that, kept current by a ResizeObserver — the buttons
re-wrapping does not fire a window resize.
publishComposite('board') starts with check(boardId, String). A
subscription arrives with a null board id, check() throws, and a throw inside
an ASYNC publisher escapes as an unhandled promise rejection — which this app
treats as fatal: SyncedCron: Fatal error encountered (unhandledRejection) and
Exited with code: 1. One bad subscription stopped the server for everyone,
and subscription arguments come from the client, so any client could send it;
the app was sending it itself, from a popup that read
Session.get('currentBoard') on a page that has no current board. The
publisher TESTS its arguments now instead of checking them — a subscription
that names no board publishes nothing and readies — and the same guard is on
boardCardsWindow and boardCardsLoadingMode. The client no longer sends a
subscription for a board id it does not have. Each guarded publisher still
marks its arguments with check(x, Match.Any)
(the follow-up that adds it),
because this app runs with audit-argument-checks and Match.test alone is not
checking — without that, every subscription failed with "Did not check() all
arguments".
tests/publicationArgumentGuard.test.cjs replays the guard and fails on an
unguarded board subscription.
The board bar's icons still took four rows - title, seven, three, hamburger -
because the rule that makes each button a flex item of the bar was in
boardHeader.css, and the display: flex for those groups in header.css has
the same specificity and loads later; it is written in the file that wins now.
And lists still stacked in desktop mode: boardBody.css lays the whole canvas
out for a phone by WIDTH — display: block on the swimlane, overflow-x: hidden on the wrapper — which makes every list a full-width row and drops the
add-list form under the last list. That is mobile mode's layout now; desktop
mode gets a swimlane that is a flex ROW scrolling sideways, so the lists run
left to right and the add-list form is the last item of the row — the right in
LTR and the left in RTL, by writing direction rather than a hard-coded side.
Five things, one theme: what belongs to the MODE the user chose and what
belongs to the width of the window. The board bar's buttons still took three
rows in desktop mode — the metrics were written in boardHeader.css, and
Meteor loads components/boards/ before components/main/, so header.css's
margin: 0 6px won at equal specificity; they are written in the file that
wins now, and eleven icons take two rows in both modes. The sidebar hamburger
is the last button at the right of the last row instead of pinned beside the
title. The avatar was cut in half in mobile mode only: the drag-handle toggle
in the top bar is also a .board-header-btn, so mobile mode gave it 42px of
padding for one icon — the width the avatar needed. Lists stacked one per row
in desktop mode, because that layout is chosen by width; it is
body.mobile-mode now. And the minicard's full-height thumb handle, chosen by
pointer: coarse, is mobile mode's too — desktop mode keeps the compact handle
in the corner under the menu button.
Eleven icons took three rows under the title: each button was ~20px of icon
inside 10px of icon margin inside 6px of button margin — 52px of a 375px bar
for 20px of icon — so five fitted a row. A 44px touch target with 2px between
buttons is 48px each, so seven fit a row and the icons take two, the first of
them sharing the title's row. 44px is the minimum comfortable touch target, so
this is as tight as it goes: one row would need 34px each, smaller than a
fingertip. tests/narrowWindowLayout.test.cjs does the arithmetic and fails if
the metrics stop fitting eleven icons in two rows.
The avatar sat above the bell and against the right edge: its wrapper carries
top: -5px from the desktop bar, and #header-user-bar carries 10px of
padding on each side — 20px of a 375px bar spent on nothing. Neither applies on
a phone now, and the chips behind the mode-toggle icons are trimmed to match.
"My Boards" had moved to the middle of the second header bar, which was a
scoping mistake: the phone rule that makes the All Boards page a flex column
was written for a bare .wrapper, and header.jade puts wrapper on
#header-main-bar on every page that is not a board — so the bar became a flex
column and centred its items. It is scoped to #content .wrapper now.
Four things on a phone, all the same in mobile mode and desktop mode. The
board's buttons started on a row of their own under the title, because every
group is one flex item and moves as a block; on a phone the groups are
display: contents, so each button is a flex item of the bar and they start
right of the title. The sidebar hamburger sat on a row below them although two
rules were meant to pin it to the top right corner — they were scoped to
body.board-view, a class written in 67 CSS rules and set by NO code, so they
had never applied; the width-based copy asks
:has(.board-header-sidebar-toggle) instead. The mobile/desktop toggle did not
say which mode was on: #000 against #666 at 14px is no difference, so the
current side is a filled chip in the active theme with a white glyph and the
other is faded. And the avatar sat higher than the bell — a bottom margin lifts
an item by half of it in a centred row — while its initials sat low in the
circle, at y="11" of a 15-unit viewBox with a font-size taller than the box.
Reported against 10.10, again against 10.37, and again against 10.38 after two
fixes that made the numbers more accurate instead of removing them. An inner
scroller only works when every ancestor between it and the viewport has a
definite height, and this chain was built out of viewport arithmetic: the
wrapper was 100dvh although it begins below the two header bars, so its
bottom sat about two bars below the screen; the layout between had no height at
all, so height: 100% under it resolved to auto; and the list was `calc(100dvh
— a guess at everything above it, which on a phone is ~226px, so the list box reached ~100px below the screen and its last rows were under the fold where no gesture could bring them. There is no viewport arithmetic belowbodynow: the page is 100dvh and everything under it is a flex chain, so the list ends exactly where the screen does, whatever the bars above it are. Both modes use the same mechanism.tests/boardListScrollChain.test.cjs` fails if
"viewport minus a guess" comes back. Not verified on a device: there is no
phone here, and Playwright's fixed viewport has no browser toolbar.and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/a84cfa6be">build.sh and build.bat always build WeKan before running the tests</a>. Thanks to xet7.</summary>Both built the bundle only when .build/bundle was MISSING — exactly the case
where a bundle exists and is stale. The :3000 test server runs that precompiled
bundle, so Node E2E and every Playwright browser were testing whatever was built
last time, and the run passed or failed on code that is no longer in the working
tree. Both delete .build and build every time now, and stop with an error if
the bundle is missing afterwards. build.bat was also two menu entries behind
build.sh — the dev server's custom port + ROOT_URL host, and "Install
Playwright browsers" — and both are added, renumbered and dispatched.
tests/buildScriptParity.test.cjs fails if either script stops rebuilding, if a
menu entry has no counterpart, if a Docker compose file one can start and the
other cannot, or if a .bat menu prints a number it does not dispatch.
and fills the new strings in every language:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b1e4c3b18">"Open many cards at once" is translated into every language</a>. Thanks to xet7.</summary>The two strings the "Open many cards at once" setting added were the English
source in every language, which is what a pull leaves behind for a string that
is untranslated everywhere. They are translated directly — written here from
each language's own existing translations, with no external service — using the
word for "card" that each file already uses, so they read like the rest of the
file. 142 languages; English and its eleven en-* variants keep the source by
design. Only placeholders were touched, so no human translation could be
overwritten, and filled strings stay local: they are never pushed to Transifex.
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/f6c83495b">The notification bell and the avatar sit the same way in both modes</a>. Thanks to xet7.</summary>They were placed by whatever margin happened to win: one auto margin on the zoom
pill pushed the pill, the bell and the avatar to the end of the row as a packed
group, so the bell sat against the pill with the empty space beyond it, and the
avatar's distance from the edge was whatever was left — different in mobile mode
and in desktop mode on the same phone. Below 800px the free space is shared
around the bell instead: an auto margin on each side puts it midway between the
zoom pill and the avatar, and the avatar keeps a fixed 12px gap to the right
edge. Both .iphone-device variants are named in the selector list, since that
fallback sets these margins with !important and two classes more, and the
placement would otherwise apply everywhere except the phone it was reported
from.
Two things differed between mobile mode and desktop mode on the same phone. The
board tile's drag handle was a 26px circle in one and a 40px circle in the
other, and on a ~97px tile the big one covered a good part of the tile it sits
on; both are the smaller one now, still well over the ~24px a finger needs. And
a workspace had no readable name in EITHER mode: the row's rule said the name
"must give way to the fixed items around it", and it gave way completely — the
drag handle, folder icon, menu button and count chip come to ~130px at their
desktop paddings, the menu column on a 375px phone is ~145px, and the name was
laid out in the 15px left over. Those items are trimmed to ~90px and the name
has a 3.5em floor it may not shrink below, with an ellipsis when it is long.
tests/allBoardsPhoneRow.test.cjs reads the sizes out of the cascade.
Both old URLs are panes of Admin Panel / Settings now, and both redirected with
FlowRouter.go('setting') called from INSIDE triggersEnter. A trigger runs
while its own route is still entering, and a go() from there is swallowed - so
nothing was rendered at all and whatever page the browser was showing simply
stayed. Playwright caught it on every browser: /information showed All Boards. A
trigger now redirects with the redirect it is handed, and each URL asks for
the pane it used to be a page of, so the bookmark lands where it pointed.
tests/adminOldUrlRedirect.test.cjs pins the redirect form and the pane.
The board titles were laid out two characters per line. Space for the drag
handle - one absolutely-positioned circle - was reserved three times on the way
down: on the list item, on the tile and on the text container, which is more
than a ~97px phone tile has. It is reserved once now, and not at all when drag
handles are off and no handle is rendered. The page could also be dragged
sideways, with the avatar past the right edge: the quick-access bar is nowrap
with overflow: visible and every item flex-shrink: 0, so a row wider than
the screen spilled - and visible overflow is scrollable overflow. The zoom pill,
by far the widest item, gives way instead, and html { overflow-x: hidden } on
a phone is the guarantee. tests/mobileAllBoardsFit.test.cjs pins both.
On a phone the pill was empty and "100" sat to the right of it in tiny type,
half under the notification bell. The pill was allowed to shrink below its own
contents, and a flex item that shrinks past its content does not clip it - the
white background ends where the width says and the text carries on outside it.
The pill is sized by what is in it now, and what is in it is made small instead.
The tiny type was font-size: 0.7em of a 12px bar - about 8px, the smallest
text on the page - now a plain 14px, which is also why the pill stays narrow.
The base rule's 24px height cap, shorter than that text, is lifted so the number
is centred. tests/mobileAllBoardsFit.test.cjs pins all of it.
The phone/desktop toggle writes an explicit choice, and Utils.isMiniScreen()
returns it as-is, so a phone whose user picked DESKTOP mode is not a mini screen
and its body carries no .mobile-mode — while the viewport is still 375px wide.
Three fixes written for one of those two therefore did nothing there. "Create
board" opened 160px in with its right half off the screen, because the geometry
laid it out as a floating box anchored to the button while the CSS made it the
full width; the board bar's hamburger was pushed to a third row of its own; and
the top bar was still wider than the screen, so the avatar was cut off. The
popup is a sheet pinned to the corner at any viewport that narrow, the hamburger
leaves the flow by width as well as by mode, and the drag-handle toggle, the
mode toggle and the logo give back the ~60px the avatar needed.
tests/narrowWindowLayout.test.cjs pins all three.
and improves the changelog and the documentation:
<details> <summary><a href="https://github.com/wekan/wekan/commit/5f9c32521">The changelog shows a short description, and hides the long one behind it</a>. Thanks to xet7.</summary>Every entry is a <details> now: the <summary> is a short description of what
was done and IS the link to the commit — the hash is in the href, never on the
page — and clicking it reveals the long description, wrapped at 80 columns. 977
entries across every release were converted, the ones the old prose format had
left malformed were repaired rather than carried over, and no URL or heading was
lost. The top of the file became # Platforms (with its collapsible Version
list) and # TODO Later, whose blocks carry no Thanks to because nothing
there is done yet. The rules are written down in
CLAUDE.md, and
tests/changelogFormat.test.cjs checks the whole file against them.
The wekan.fi Hall of Fame page grew a row of eight cells, one thing in each — CVE, Icon beside it, Vulnerability name, Date, Responsible Security Disclosure by, Stars, Process, Vulnerabilities — with the Process and Vulnerabilities cells collapsed behind their own summary, the stars of a row on one line and the icon beside its red drop on one, the reporter's nickname as the link to their GitHub — checked to exist before it is linked — and no role note after a name. All of it is stated in CLAUDE.md so the next security entry is written to fit rather than reconstructed from the file.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUES:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f1c89548e">ZipBleed: arbitrary file write when restoring a backup archive (zip-slip)</a>. Thanks to xet7.</summary>ZipBleed: arbitrary file write when
restoring a backup archive (zip-slip) (CWE-22 Improper Limitation of a
Pathname to a Restricted Directory). A zip entry carries its own path, chosen by
whoever built the archive, and path.join() RESOLVES .. segments instead of
rejecting them. The restore in server/methods/backup.js checked only that an
entry's first path segment was attachments or avatars, then joined the rest
onto the target directory. An entry named
2026-07-25_12-00-00/attachments/../../../../etc/cron.d/wekan therefore passed
that check — its first segment really is attachments — and joined its way
clean out of the files directory, and the entry's contents were streamed to
whatever path came out. A crafted backup.zip could drop or overwrite a file
anywhere the WeKan process could write. Restoring a backup is exactly the moment
nobody inspects the file they were handed, and restoreBackup is behind
requireAdmin(), so the archive arrives the normal way: offered to an admin as
a backup to restore. Fixed by resolving each entry to an absolute path and
requiring it to sit under the directory it belongs in — compared against the
base plus a separator, so a sibling directory whose name merely starts with the
same letters (/data/files/attachments-evil) is not accepted either. The data
half of an archive names the Mongo collection to restore into, so that is now
constrained to a plain name, which also keeps a restore out of the database's
internal system.* collections. A refused entry is skipped and reported rather
than thrown on, so one hostile entry cannot abort a genuine restore. Found while
reviewing the open dependency pull requests — it is WeKan's own code, not any
dependency. tests/zipbleed.test.cjs asserts the exact traversal that motivated
the fix is refused, that a plain entry still restores where it belongs, and that
the restore really calls both guards
GHSA-3gcg-g6rf-w2rx:
an invalid authToken on a board export endpoint crashed the server (CWE-476
NULL Pointer Dereference; remote denial of service). The board export REST
endpoints look a user up by the login token in ?authToken=. A token that
matches nothing makes that lookup answer undefined, and the next line
dereferenced it — user._id.toString() — throwing a TypeError out of an async
route handler with no try/catch. That escapes as an unhandled promise rejection,
which this app turns into a full process crash, so one crafted GET against a
private board id took the server down for every user. Reachable by anyone who
can obtain a private board id, which on an open-registration instance means
anyone at all. It was an incomplete fix: models/exportPDF.js and
models/exportExcelCard.js already had the if (!user) guard; three handlers
in models/export.js and one in models/exportExcel.js were missed. Fixed on
both levels: every token lookup in the export models is now followed by that
guard — 401 "Invalid token" and return — including the two handlers that did not
crash because they hand the user to canExport() instead of dereferencing it;
and every export route body is wrapped in safeRoute()
(server/apiMiddleware.js), which awaits the handler, answers 500 once and logs
the request that failed, so a throw from any other cause is one broken request
instead of an outage. tests/exportTokenGuard.test.cjs finds every token lookup
in the export models and requires a guard on each, rather than checking four
places by hand
and adds the following new features:
<details> <summary><a href="https://github.com/wekan/wekan/commit/36b899b9d">Multitenancy: one WeKan server for many domains, with Organizations as the tenants</a>. Thanks to xet7.</summary>Multitenancy: one WeKan server for many domains, with Organizations as the
tenants. Hosting n customers meant running n WeKan servers — n Node.js
processes, n ROOT_URLs, n upgrades, n backups. One server can now serve
them all: an Organization claims its own hostnames (Admin Panel / People /
Organizations → Edit), carries its own branding — product name, logos, help
link, legal notice and theme colour — and gets its own Organization admins,
appointed from the row's ⋯ menu. An Organization admin administers their own
Organization's people and backups and nothing else: they can never grant the
site-wide Admin flag, never manage a site admin, and never see another
Organization. Per-tenant backup and restore is one new control in Admin
Panel / Attachments / Backup — Scope — and an Organization's archive holds its
boards and their attachments but no accounts and no instance settings, because
those are shared; restoring one only ever writes to boards that Organization
owns. The whole thing is OFF unless the server is started with
MULTITENANCY=true (plus MULTITENANCY_TRUST_PROXY_HOST=true when a trusted
proxy sets X-Forwarded-Host), so an instance that has never heard of tenants
answers exactly as before. The design — including what this deliberately does
NOT isolate, which is why one server per customer stays the recommendation for
customers who must not share a process — is
docs/Design/Multitenancy/Multitenancy.md, option D, and four new test suites
(tenants, tenantAdmin, tenantBackup, tenantWiring) pin the separation:
forged Host headers, cross-tenant queries, privilege escalation and cross-tenant
restores
Admin Panel / Settings / Visibility has a Change color section, above Logo:
the site theme, chosen with the same picker as Board Settings / Change Color and
Member Settings / Change color — one shared template for all three, the way one
shared table page serves every table. The order of themes is now WeKan's default
theme, then this site theme, then a user's own override; a board's own colour
still owns the board page. An Organization's admin sets their Organization's
colour from the same section, and the site admin's is the one every Organization
without one of its own inherits, which the line under the title says. The design
is the new docs/Features/Page/Theme.md
and reorganises the Admin Panel:
<details> <summary><a href="https://github.com/wekan/wekan/commit/fa0da9178">Admin Panel / Settings is reorganised so every setting sits with the thing it is about. Layout is…</a> Thanks to xet7.</summary>Admin Panel / Settings is reorganised so every setting sits with the thing it is about. Layout is now PWA and holds only the PWA settings — its custom head tags, web manifest and assetlinks.json; the branding it used to carry (product name, the login and top-left-corner logos, the text below the logo) moved to Visibility, along with the Wait Spinner, Support, the custom help link and the legal notice URL. The Accounts pane is gone: allow e-mail change went to E-mail, username change and self delete of an account to Login. The sign-in pane is called Login instead of Registration. "Don't show the board activities" became one global setting rather than a write over every board document, and the two All Boards hide settings joined it in Visibility. Translation became a Settings pane below Accessibility instead of a tab of its own
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e26706799">Every Admin Panel page renders ONE shared left menu, built from a plain item list instead of markup…</a> Thanks to xet7.</summary>Every Admin Panel page renders ONE shared left menu, built from a plain item
list instead of markup that had been retyped 44 times across seven templates —
and, in two of those pages, one click handler per entry instead of one per menu.
The selected entry is filled with the theme colour with a white label, the same
treatment the selected tab gets in the bar above it, so the menu says at a
glance which pane is showing. No Admin Panel page has a title bar repeating the
section name any more: the tab bar names the section and the menu highlights the
pane, so a third line only cost vertical space. The design, and the list of
pages that use it, is in docs/Features/Page/Left-Menu.md
Admin Panel / People's four table panes — Domains, Organizations, Teams and People — render through the shared table page, so all four page, search and lay out the same way; their rows stay interactive through a row slot, and a column header may carry controls (the "New" link, the select-all pairs) through a header slot. That pane's search box, filter dropdown, action buttons and total became features of the shared controls row, so any table page can have them, and Teams gained a working "previous page" it never had. Locked users, Roles and Shared templates are recorded in the design as what they are — forms and checkbox lists, not tables
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b72a4996d">The Admin Panel Features tab is removed</a>. Thanks to xet7.</summary>The Admin Panel Features tab is removed. Its last three panes — Performance, Security and Notifications — moved to Admin Panel / Problems, leaving a tab that opened an empty page. The route, the tab, its active-tab helper, the imports and both files are gone; the three pane templates and their handlers moved to the page that renders them, since deleting them would have left Problems rendering templates that no longer exist — which is not a build error, but a throw when the pane is opened. A new test requires every template include to name a template that exists, the mirror of the existing check that every template handler targets one
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4df87fd6b">Admin Panel / Problems: the Security report is renamed Security Report and moved above Broken…</a> Thanks to xet7.</summary>Admin Panel / Problems: the Security report is renamed Security Report and moved above Broken Cards, with Impersonation Report directly between the two; and the Performance, Security and Notifications panes moved here from Admin Panel / Features, below Summary. The rename is what makes that safe — the pane arriving from Features is also called Security, and the two now sit in one menu. Only the English source string changed, so every other language keeps its existing translation until that string is retranslated. The panes brought their helpers and handlers with them, which is the half that fails silently: all twenty-four were registered on the Features page template, and Blaze resolves a helper, and delivers an event, against the template the element is in — left there, each pane would have rendered on Problems with every checkbox reading as unchecked and no click doing anything. Admin Panel / Features is left with no panes; its page and route stay so nothing linking there breaks
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/47b03ef89">Admin Panel: the Login and E-mail panes moved from Settings to People, above Organizations — both…</a> Thanks to xet7.</summary>Admin Panel: the Login and E-mail panes moved from Settings to People, above Organizations — both are about the people who can sign in and how they are reached, which is what that page is for. The move was only half markup: every handler the two panes need was registered on the Settings template, and Blaze delivers an event to the handlers of the template the element is in, so left there each pane would have rendered on People and then quietly done nothing — no toggle sticking, no Save saving, nothing in the console. All ten moved onto the pane templates themselves, where they work wherever the pane is rendered. Settings now opens on Visibility, the first entry it has left; the pane ids and translation keys are unchanged, so nothing lost its translations
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d23219407">Every Admin Panel pane opens with the same heading, and the heading is the open left-menu entry's…</a> Thanks to xet7.</summary>Every Admin Panel pane opens with the same heading, and the heading is the open
left-menu entry's own label. Before this only the paginated table pages had a
title at all: Domains said "Domains" while Login, Announcement, Accessibility,
PWA and Version opened with no heading. Deriving it from the menu is what keeps
them identical — a pane cannot end up with a title of a different size, in
different words, or with none at all, and renaming a menu entry renames its pane
title with it. One class sizes it, and the shared table page's own title carries
that class too, so a table pane and a form pane look the same. Described in
docs/Features/Page/Left-Menu.md
Admin Panel / Settings / Translation renders through the shared table page
(docs/Features/Page/Table.md) instead of a hand-written table: the layout, the
search box, the themed pager and the total are the shared ones, and the pane
keeps only its four columns, its interactive row and its "New" link. It also
pages ONE page of 25 rows server-side with limit/skip and a count method, where
it used to grow a window by infinite scroll, and the publication publishes the
field it is sorted by so the client's order matches the server's page
Version is the FIRST pane of Admin Panel / Settings and the one that opens with
the page, so what an admin sees when opening the Admin Panel is the version,
database and system information they usually came for. Its own page, its
one-entry left menu and its tab in the Admin Panel bar are gone — a page whose
menu had a single entry was a page in name only — and the /information URL
redirects to Settings, the same move Translation made
Admin Panel / Settings / Visibility is four named groups instead of one long list: All Boards (boards visibility, board activities, card counter list, board member list, wait spinner), then URL (Support, custom help link, custom legal notice, custom URL schemes), then Product name, then Logo (hide logo and the login / top-left-corner logo fields) — each group after the first separated by a horizontal rule. The settings and their ids are untouched, so the one Save button at the bottom still writes exactly what it did. A group title is smaller than the pane title above it, so the page reads as one heading with groups under it rather than as several pages stacked
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0ea1601b">"Add board members only from the same Organization or Team" moves out of Admin Panel / People /…</a> Thanks to xet7.</summary>"Add board members only from the same Organization or Team" moves out of Admin Panel / People / Login — which is where neither of the two things it restricts lives — and becomes two checkboxes, each in the pane it is about: "Add board members only from the same Organization" in Admin Panel / People / Organizations and "Add board members only from the same Team" in / Teams. A user may be added to a board when they share an enabled kind with whoever adds them; with both ticked that is "an Organization or a Team", exactly the rule the single setting had. An install with the old setting is migrated to both on first start, so no instance's restriction changes by upgrading — and ticking only one is the narrower choice that was not expressible before
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/40c1c1e71">Admin Panel / People / Login is one "Login: Allow" group of checkboxes, each ticked when the thing…</a> Thanks to xet7.</summary>Admin Panel / People / Login is one "Login: Allow" group of checkboxes, each ticked when the thing is allowed: Forgot password, Self-Registration, Username Change, Self delete user account, Display Authentication Method. The pane used to mix "Disable X" checkboxes, where ticked meant OFF, with "Allow X: Yes/No" radios — half the rows meant the opposite of the other half, and every one repeated the word Allow or Disable. The two settings stored as "disable" flags are only inverted for display; the stored fields are untouched. The three former radios save on click like the checkboxes they now are, so the Save at the bottom keeps only the default authentication method and the OIDC button text. A dead handler that sat later in the same event map — where a duplicate key silently overrides the real one — would have swallowed every click on the Display Authentication Method box
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f15b37e4e">That pane's "Email domain name" is renamed to say what it does: "Email domain allowed to invite…</a> Thanks to xet7.</summary>That pane's "Email domain name" is renamed to say what it does: "Email domain allowed to invite people, when self-registration is disabled". It limits nothing about signing in — a non-admin whose address ends with this domain may send board invitations, and only while self-registration is disabled
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ef34924d">Admin Panel / Settings / Visibility, All Boards group: every row repeated the group title and the…</a> Thanks to xet7.</summary>Admin Panel / Settings / Visibility, All Boards group: every row repeated the
group title and the word "Hide" — "Hide card counter list on All Boards" under a
title reading "All Boards". The title is "All Boards: Hide" and each row says
only what is hidden: Public boards, Board activities, Card counter list, Board
member list. The three i18n keys used only by this pane were renamed with their
values, so no language shows the old sentence under the new title; they fall
back to English until retranslated. all-boards (the menu label everywhere
else) and tableVisibilityMode-allowPrivateOnly (the message shown on a board
you may not open) keep their own strings, so the group title and the row label
are new keys
Each section of Admin Panel / Settings / Visibility has its own Save, directly above the rule that closes it, and it writes only that section's fields. One Save for the whole pane meant pressing it in one group also wrote whatever was half-typed in another. Three buttons are folded into their section's Save and gone with their handlers: the pane-wide one, the single-setting Save under "don't show the board activities", and the one inside the Support block. The Support page's Enabled / Public checkboxes still save on click — they are toggles, not fields
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bbb1b3ff0">"Product name" appears once in that pane, not twice: the group holds one field, so its group title…</a> Thanks to xet7.</summary>"Product name" appears once in that pane, not twice: the group holds one field, so its group title is that field's label — at the group title's size, with the translation the field already has in every language
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc0fe3177">Admin Panel / Problems' left menu is two named groups: Summary, then a rule and a "Settings" title…</a> Thanks to xet7.</summary>Admin Panel / Problems' left menu is two named groups: Summary, then a rule and a "Settings" title over the two panes that came from the removed Features tab, then a rule and a "Reports" title over the reports. A title is a new kind of menu item with no id, no icon and no handler class — there is nothing to click, and it can never become the active row — and both use an i18n key the app already has, so no language has to translate anything new. Performance moves down to sit with the Speed, Tests and CPU usage streams it is about, below Impersonation Report
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d23219407">Admin Panel / People: Domains moves up beside E-mail, the settings it is about, instead of sitting…</a> Thanks to xet7.</summary>Admin Panel / People: Domains moves up beside E-mail, the settings it is about, instead of sitting at the end after the Roles and Shared templates checkbox lists
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc0fe3177">Admin Panel / Problems / Broken cards has the same controls as the Files Report beside it: a search…</a> Thanks to xet7.</summary>Admin Panel / Problems / Broken cards has the same controls as the Files Report beside it: a search box, the total, "page X / N" and the shared themed pager. It was the one entry in that menu with a different set, because it ran on the global-search machinery instead of the shared table page. It is a column spec like the other reports now, behind an admin-only publication that takes a search term plus limit/skip and a count method for the total; what "broken" means — no board, swimlane or list, or a type that is not a card type — is one selector shared with the standalone /broken-cards page, which is unchanged
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9a0db4343">No Admin Panel pane repeats the title above it</a>. Thanks to xet7.</summary>No Admin Panel pane repeats the title above it. Attachments / Backup showed "Backup" twice — the pane heading comes from the open left-menu entry now, and every Attachments pane still printed its own name under it. Those ten headings are gone, and with them the same repeat on Roles, Shared templates and Broken cards. A heading that says something the menu label does not — Limits' "Attachment And API File Size Limits", Locked users' "Brute Force Protection Settings" — is not a repeat and stays
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a9680fa3d">Admin Panel / Attachments opens on Backup, which is also the first entry of its menu — the first…</a> Thanks to xet7.</summary>Admin Panel / Attachments opens on Backup, which is also the first entry of its menu — the first row of a menu and the pane that opens are the same one now, on Attachments as on Settings. Landing there asks once whether a backup is already running, so an in-progress backup shows its status instead of an idle-looking pane
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eba25459a">Admin Panel / Attachments no longer has a Sandstorm pane</a>. Thanks to xet7.</summary>Admin Panel / Attachments no longer has a Sandstorm pane. It only had content
inside a Sandstorm grain — the one-time MongoDB 3 → FerretDB migration status
and a button that DELETED the raw MongoDB 3 database files to reclaim grain
disk. Compacting the MongoDB database frees that space as well, so deleting raw
database files is not something WeKan needs to offer — and anyone who does want
those files gone can download the Sandstorm grain, delete the raw MongoDB
database files from it, and upload the grain back to Sandstorm. The menu entry,
the pane, its helpers, its state and its click handler are commented out rather
than deleted, so nothing of it runs and the grain migration report can be
brought back if it is ever wanted. Everything else on the page is untouched: the
storages and their statistics, moving attachments between storages, the MongoDB
↔ FerretDB database migration and Backup. The grain's own migration, which runs
in sandstorm-src/start.js before the app boots, is unchanged
and updates the following dependencies:
@playwright/test 1.61.1 → 1.62.0 in tests/playwright — the browser test
runner, a development dependency that is not part of the shipped bundle (#6528,
merge commit )
docker/login-action 4.4.0 → 4.5.1 — the GitHub Actions step that signs in to the container registries when a release image is published (#6526, merge commit )
</details>and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/2973240a7">Clicking a card closes the one that was open, and keeping many open is a per-user setting</a>. Thanks to mimZD and xet7.</summary>Clicking a card closes the one that was open, and keeping many open is a per-user setting. Opening a card APPENDED it to the open-cards list, so the card that was already open stayed open behind the new one — visible the moment the new one was dragged. Clicking another card means "show me that card", not "show me both". Both ways of opening a card — the minicard and its title — go through one helper now, so neither path can drift from the other. Keeping several cards open at the same time is still possible, as the per-user setting it should always have been: Member Settings / "Open many cards at once", off by default, stored on the user profile and toggled by a server method like the settings beside it. A reader who is not logged in gets the default rather than an error
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/89920762a">All Boards on a phone sizes its scrollers by the viewport that is actually there</a>. Thanks to mimZD and xet7.</summary>All Boards on a phone sizes its scrollers by the viewport that is actually
there. The page sized them as 100vh minus a bit — the wrapper, the left menu,
the board list, and the mobile body and content area. On a phone 100vh is
deliberately the LARGEST the viewport can be: the height with the browser's
toolbars hidden. With the toolbar showing, every one of those boxes reaches
under it — and the wrapper is overflow: hidden, so what is underneath cannot
be scrolled to at all, which looks exactly like "the board list does not
scroll". Each rule now states the vh value first, as the fallback, and the
same value in dvh — the viewport as it is at that moment — immediately after.
The two scrollers also declare overscroll-behavior: contain and touch-action: pan-y, so a vertical swipe is given to them rather than to the page behind. Not
verifiable here: there is no phone in this environment and no browser toolbar in
Playwright, which is why the earlier layout fix could not have covered it
Choosing a theme now recolours the whole UI, not just the header. Every named theme publishes its own accent colour, so everything that reads it follows along: the Admin Panel's pane buttons, every Save button, the table-page controls and the popup header — all of which stayed on the stock blue whatever theme was chosen, because the accent was only ever set when a user picked a CUSTOM colour for a flat/clear theme. The Admin Panel's selected left-menu row goes further: it is painted by the theme's own header rule, so it matches the second header bar exactly — including a colour slide, which clearblue has and a single accent value cannot express. The top bars themselves used to spell the order of themes out in the template, which is why the new site theme coloured the buttons and left the bars alone; one helper answers for all of them now, in one order
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19a5ea0b7">Admin Panel / People / People / Edit User — and the Organization and Team edit popups — are fully…</a> Thanks to xet7.</summary>Admin Panel / People / People / Edit User — and the Organization and Team edit popups — are fully visible again, and fill the width they are given. They were positioned from the table row that opened them, so opened from a row in a wide table their right-hand side, and the Save button under it, hung off the edge of the window; they are centred on the window now, and no popup can be wider than the window at all. Their fields lay out in as many columns as there is room for — three on a wide desktop, one on a phone — so a dozen-field form no longer has to be scrolled to reach Save
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19a5ea0b7">Admin Panel / Settings / Visibility / Wait Spinner shows the spinner it names, spinning, beside the…</a> Thanks to xet7.</summary>Admin Panel / Settings / Visibility / Wait Spinner shows the spinner it names, spinning, beside the dropdown — and below it when the pane is too narrow for both. Choosing one no longer means saving it to find out what it looks like
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19a5ea0b7">Admin Panel / Settings / Translation puts its New column on the left, where every other Admin Panel…</a> Thanks to xet7.</summary>Admin Panel / Settings / Translation puts its New column on the left, where every other Admin Panel table page has it — at the far right it read as belonging to the last column rather than to the table
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d23219407">Admin Panel / People / People showed nothing at all — no table, no search box, no pager…</a> Thanks to xet7.</summary>Admin Panel / People / People showed nothing at all — no table, no search box,
no pager. buildFilters and buildActions were used to declare that pane's
filter dropdown and its two action buttons, but were never imported. That is not
a build error: it is a ReferenceError thrown inside a helper while rendering,
which Blaze answers by rendering nothing. Organizations, Teams and Domains use
neither function, which is what made it look like a People-only problem
The right-hand side of an Admin Panel table is no longer off screen. Reported on
Domains, and just as true of Admin Panel / Version, where the value column was
cut off at the window edge: two admin stylesheets forced min-width: 1200px !important; width: max-content !important with nowrap cells on a bare table
selector — one of them with no page in the selector at all, so it reached every
table in WeKan, and !important beat the shared table page's own width:100% +
table-layout:fixed. The same rules made a SHORT table shrink to its content
instead of filling the panel. No admin table is forced wide any more: it fits
its panel and its cells wrap, at every width, and the shared table page's width
and wrapping are !important so a stylesheet in another folder cannot silently
override them
An empty Admin Panel table now still shows its header, so Organizations, Teams, People and Translation can create their FIRST row: the "New" link is a column header, and the table used to be hidden entirely when there were no rows — leaving a pane with no way to add anything. The "no items" message sits below the table now
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d23219407">Every control in an Admin Panel table's controls row sits at the same height, and the action…</a> Thanks to xet7.</summary>Every control in an Admin Panel table's controls row sits at the same height,
and the action buttons follow the theme instead of coming out black. "Unlock all
users" was lower and shorter than "Teams" beside it: it carried a 20px top
margin and a 28px height from the old hand-written page header, and the buttons
that kept the global margin-bottom: 14px of forms.css were centred higher than
the ones that did not. The action buttons are themed with the pager, in the one
stylesheet that owns those colours, with every state spelled out — a bare
button otherwise falls through to the forms.css fallback, which is literally
black
A long Admin Panel left menu scrolls inside its panel instead of spilling out of it. Admin Panel / Problems is the longest — fifteen entries since Performance, Security and Notifications joined it — and its last four sat on the page’s grey with no panel behind them. The panel is stretched to the height of the page, so its background, border and rounded corners end there, and the entries simply kept rendering past that edge.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f8793bec4">The build stopped with Can't use the built-in 'if' here on the People page: a comment had been left…</a> Thanks to xet7.</summary>The build stopped with Can't use the built-in 'if' here on the People page: a
comment had been left between an if and its else if, which splits the chain
and orphans the else. The comment moved above the chain. The more useful half of
the fix is the test: the guard that compiles every template was calling only the
PARSE step, and this error comes from the code-generation step that runs after
it, so the suite was green while the build was red. It now runs both steps,
exactly as the build's own loader does, and a negative test feeds the broken
chain through both to show that parsing alone does not see it
Admin Panel / People / E-mail saved neither of the two settings it shows. Its Save handler starts by reading the SMTP fields, which are commented out of that pane — the read throws on an input that is not there, and the throw was caught and swallowed, so the handler returned before ever writing anything, including the e-mail domain. "Allow Email Change" was worse: the radios showed the stored value and no handler in the app ever wrote them back. The Save button moves out of the Yes/No row to its own line below both settings and writes both, each field only when its input is actually rendered
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/73cd930a4">The Admin Panel left menu, the page-title bars, the People tables and the Translation pane</a>. Thanks to xet7.</summary>The Admin Panel left menu, the page-title bars, the People tables and the Translation pane. Every Admin Panel page except Problems drew a bar under the top bar repeating the name of the pane you were already looking at; they are gone, and the controls that lived in People’s bar moved into the shared table page’s controls row. Organizations, Teams, People and Translation drew their search box and pager but no table at all: each referenced a helper that belongs to its parent template, and Blaze never searches an enclosing template for a name, so the data came out undefined and a table with no rows draws its chrome and stops. Domains and the other table pages needed scrolling right to see their last columns, because a rule written years ago forces every admin table to a 1200px minimum with non-wrapping cells — the shared table page is excluded from it now. The Translation search button was black, from the black fallback in the shared button style, and follows the theme like every other admin button. The selected left-menu entry is filled with the theme colour and its label and icon go white, matching the selected tab in the bar above it
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d34d28159">The Admin Panel left menu is back on every page</a>. Thanks to xet7.</summary>The Admin Panel left menu is back on every page. Settings, People, Features,
Attachments, Version and Problems all rendered an empty panel where their menu
belongs. The seam between the two halves of the shared menu was wrong: the
template iterates items, and every page handed it the bare array the builder
returns, so the lookup found nothing and rendered nothing. Nothing failed
anywhere — the template was correct, the builder was correct, every test passed,
and six pages simply lost their navigation to one mismatched shape. That shape
is now stated once and used by all six pages, and the missing guard checks the
seam rather than either half: it reads which variable the template iterates and
asserts the data context really has an array under that name
The password field is back on the Sign In and Register pages. Removing the
Accounts pane from Admin Panel / Settings — its three settings having moved to
E-mail and Login — left that pane's save handler registered on the template that
went with it, Template.accountSettings.events({…}). Registering a handler on a
template that no longer exists throws at MODULE LOAD, and because it threw, no
module after it in the client bundle ran: client/features/users.js never
executed, so the passwordInput template was never registered, useraccounts
logged "Warning no template passwordInput found!", and both password fields —
Password and Password (again) — rendered as nothing. One dead reference in the
Admin Panel took out the login form for everyone. The handler now belongs to the
template that renders the Login pane. A new guard,
tests/templateHandlersExist.test.cjs, asserts every
Template.X.events/helpers/onCreated/onRendered/onDestroyed has a matching
template(name="X"), because this class of bug parses cleanly, passes every
unit test, and shows up only in a browser console
A finished MongoDB → FerretDB migration on Snap no longer leaves WeKan on 503.
Upgrading from 6.09 to 10.37 migrated successfully and then served 503 until the
admin ran snap restart wekan by hand, with WeKan looping "MongoDB not ready
yet, retrying in 5 seconds..." against a MongoDB the migration had just shut
down for good. mongodb-control ends with exec bash $SNAP/bin/migration-control, so the migration script IS the wekan.mongodb
service process — and the switch stopped that service before restarting WeKan,
so systemd killed the script at the stop and the restart on the next line never
ran. WeKan is now handed over to FerretDB BEFORE MongoDB is stopped, the stop is
the last command in the switch, and the traps are disarmed first so being
stopped there no longer logs "Interrupted (snap refresh, stop or reboot)" after
a migration that in fact succeeded. Because that restart is the last act of a
script being killed, it is no longer the only way out: WeKan's MongoDB wait now
re-reads the live database setting each round and re-execs onto FerretDB when
it has been switched, so a migration finishing mid-wait recovers on its own
within five seconds
In mobile mode the fixed-size light grey bands around a swimlane are gone: the one between the blue resize bar and the next swimlane’s dark header, which travelled with the bar as the swimlane was resized, and its twin between a swimlane’s header bar and its first list. They were plain bottom margins on the swimlane header and on each list; on the last list that margin is exactly the band between the content and the resize bar, and being a fixed size it moved with the bar rather than growing. Lists are separated by the border under each of them, so nothing is lost. The board stylesheet restates these selectors in several later blocks, and later rules of equal specificity win, so eight duplicates had been putting the bands straight back; the guard test now checks every block rather than the first one. The swimlane header wrapper is also only as tall as its bar, and the minimum height that held a short swimlane open below the height chosen for it is gone — while the height being dragged is left alone, so the resize bar still resizes
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a5850296a">In mobile mode the board fits the width again</a>. Thanks to xet7.</summary>In mobile mode the board fits the width again. The lists area was slightly too
wide, giving a horizontal scrollbar with a small amount to scroll right and
nothing there to see. Two causes: 100vw sizing, which had been fixed in one
stylesheet but was still in two more — including the mobile list rows themselves
— and which is the viewport width INCLUDING the vertical scrollbar, so each of
those was a scrollbar wider than the box it sat in; and box-sizing, which is
globally unset, so every rule setting both a full width and padding came out
32px wider than its parent, at four nesting levels. Viewport width is kept where
it is correct — a position:fixed overlay such as the mobile card details, a
popover, the sidebar or the notifications drawer is sized to the viewport by
definition
In mobile mode the swimlane title, the list title, the board title and everything in the top bar sit on the vertical centre of their row instead of high up. The swimlane title was a block at the top of a band sized by its padding and the icons beside it; the list title was pinned to the bottom of one grid row with the card count pinned to the top of the next, so the pair hugged the middle and, with no card count, the title sat in the upper half; and the two header bars centred their contents inside boxes that were shorter than the touch-sized buttons in them, or padded above but not below
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/df27e349d">In mobile mode, clicking the Add List + no longer opens the list as well</a>. Thanks to xet7.</summary>In mobile mode, clicking the Add List + no longer opens the list as well. The whole list row is a link that opens the list and the + sits inside it, so one click did both. The handler prevented the link default, which is a different thing from stopping the click travelling up to the row
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/61979c0f6">In mobile mode the "Add List" + moved into each list header, between the caret and the drag handle…</a> Thanks to xet7.</summary>In mobile mode the "Add List" + moved into each list header, between the caret and the drag handle, and the row it used to occupy is gone — a whole row of a phone screen spent on one button. Desktop had already done this, so the two had drifted apart; the mobile button now uses the same class, and therefore the same handler and behaviour, and the same Font Awesome icon, rather than being a lookalike that can drift again. The list header row mirrors in RTL by itself, being a CSS grid, which lays out along the inline axis — handle, +, caret, then the title — and no control is pinned to a physical side. An empty swimlane and an empty board keep a + of their own, since there is no list header to host one and otherwise the first list could never be created. The template that rendered the old row is removed along with its handlers, mobile mode having been its last caller
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7dcd9da67">Checked for the same window-resizing bug everywhere else it could hide, and closed the gap that…</a> Thanks to xet7.</summary>Checked for the same window-resizing bug everywhere else it could hide, and
closed the gap that made the question unanswerable: the guard test only ever
looked at the client stylesheets. A viewport unit renders exactly the same from
an inline style= in a template, from CSS built as a JavaScript string (the
HTML export builds a card modal that way, the Sandstorm migration bridge builds
a whole page) and from the stylesheets in our own packages and public/css. All
three are now scanned, and all three are clean — what they contain is the
allowed shapes. One further test proves the new scans are not silently reading
nothing, since a scan that finds no files passes forever
Elements no longer change size when the browser window is resized — this time
the ones hidden inside clamp(). Reported on a board in mobile mode: widening
the window made the swimlanes, lists and cards taller, and narrowing it shrank
them again. Same complaint as the Sign In and Register pages, and the same
cause. The earlier sweep converted 355 bare vh/vw values but allowed
clamp() as a cap — which it is only at its two ends; between them it tracks
the window exactly like a bare vw. So a swimlane header, a list header and a
minicard sized with font-size: clamp(18px, 2.5vw, 32px) grew with the window,
and the rows grew with the text. All 75 of these, across 17 stylesheets, now use
the size they already rendered at on a phone. The board sidebar, a collapsed
list, the settings body, the search popover and the minicard custom-field chip
had the same behaviour spelled other ways and are fixed too. The guard test no
longer accepts clamp() or a viewport minimum; a full-viewport box, a
max-height/max-width cap, and a shrink-only min(400px, 52vw) — flat above the
crossover width — are still allowed
The second header bar in mobile mode is the same on a phone as in mobile mode on a desktop window. On an iPhone it held a handful of icons and no board title, while a desktop Firefox window in mobile mode showed the title and the full set of buttons. Mobile layout is driven from three switches that did not agree — the mobile-mode toggle, the mini-screen check that picks which template branch renders, and the 800px-wide media query, which is true on a phone and false on a wide desktop window even with mobile mode on. The rule hiding the left button group deleted edit title, visibility, watch, star and sort from the bar with no replacement whenever those switches disagreed, and is removed; the bar's fixed height on a phone, added for bigger touch targets, clipped the second row it wraps to when the title and the buttons do not fit, and is now a minimum height, so the bar still sits on one row whenever the content fits
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/72315d8f9">The list title is visible in mobile mode again, both on a phone and on a desktop</a>. Thanks to xet7.</summary>The list title is visible in mobile mode again, both on a phone and on a desktop. The list header has two markup shapes — on a mini screen the title is a direct child of the header, otherwise it is wrapped in the div that rotates when a list is collapsed. Mobile mode lays that header out as a grid and places the title by grid row and column, which only applies to a direct grid item, so inside the wrapper the placement was ignored and the title landed in the 30px first column, squeezed to nothing. The wrapper is now dropped from the layout so the title is placed in both shapes, while a collapsed list keeps the box it needs to rotate
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/72315d8f9">The board in mobile mode reflows sideways again instead of overflowing by a sliver</a>. Thanks to xet7.</summary>The board in mobile mode reflows sideways again instead of overflowing by a
sliver. The canvas, swimlanes, lists and list headers were sized with 100vw —
the viewport width including the vertical scrollbar. The canvas scrolls
vertically, so each of those was about a scrollbar's width wider than the box it
sat in, at every nesting level, and a matching min-width meant it could not
shrink. They now use 100%, which resolves against the parent's content box and
already excludes the scrollbar
The minicard drag handle is below the minicard menu button, on the same edge. On a touch pointer it was a full-height strip down the leading (left in LTR) edge — added because on a touch device the handle is the only way to drag a card, the card body deliberately panning the board instead, so the small corner icon was too small a target. That reached the size goal but put the handle on the opposite side of the card from every other card control, and from where a mouse user's handle already sits. Both pointer kinds now use the same column: details menu on top, drag handle directly below. The touch target is not given up — the handle is 44px wide and runs from under the menu button to the bottom of the card, with a floor for a card holding nothing but a title, and the card reserves that column so its text never runs underneath. It mirrors to the other edge on an RTL board
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a5246fbf5">Drag-to-scroll now works on both top header bars, not only below them</a>. Thanks to xet7.</summary>Drag-to-scroll now works on both top header bars, not only below them.
Everywhere else a drag scrolls — the board canvas on a board, the page on All
Boards / My Cards / … — but the two header bars were dead, and on a phone they
are a large share of what is on screen. They cannot simply be tagged
dragscroll: that class scrolls the element carrying it, and neither bar is a
scroll container — #header sits outside #content and outside the board
canvas. A new module, client/lib/headerDragscroll.js, forwards the drag to
whatever actually scrolls: a scroller inside the header first (the
starred-boards list scrolls sideways), then the board canvas, then the page.
Mouse and touch are both handled, since a phone sends no mouse events and the
dragscroll library is mouse-only. A tap is still a tap: scrolling begins only
past a 4px threshold, and only then is the click that ends the gesture swallowed
The board title is shown again in mobile mode, and the second header bar now
reads title, buttons, hamburger. The title was hidden by a display: none rule,
which left that bar with nothing but icons — you could not tell which board you
were on. A board name is how you know where you are, so it wraps rather than
truncating. The buttons start at the right of the title and drop to a second row
as a block when the space between the title and the menu button runs out; they
are one flex item, so they never split across the two rows
The sidebar hamburger, with its divider to its left, is pinned in the top right corner of the second header bar in mobile mode. It was lifted out of the right-hand button group into its own flex item, because inside that group it could only ever wrap down together with the other buttons — a flex line breaks in order. On a phone it is taken out of the flow and the bar reserves its width, so a long title or a full row of buttons never runs underneath it; on a wide screen it is ordered back to the end of the bar, visually identical to before
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a5246fbf5">Lists and cards are full width in mobile mode on a desktop browser too, not only on a phone</a>. Thanks to xet7.</summary>Lists and cards are full width in mobile mode on a desktop browser too, not only
on a phone. Mobile mode was decided in two places that disagreed.
Utils.isMiniScreen(), which drives the mobile-view class on each list,
swimlane and minicard, looked only at screen width and user agent — the explicit
mobile-mode toggle was honoured on iPhone only, and even there through a test
that could never be false. An explicit choice now wins on every device, with the
per-device branches left as the defaults for when the user has never chosen. And
the rule that persists a resized list width outranked the mobile full-width rule
(both !important, the other more specific), which showed only in mobile mode
on a desktop window because no per-list width is emitted on a phone; a collapsed
list still stays narrow
The Sign In and Register pages have real scrollable space below the form again, so the last field and the button are reachable on a short window instead of being cut off by a container that never scrolled
</details>and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b0bffe8c1">The Playwright tests address an Admin Panel menu entry by its data-id, not by a per-page class</a>. Thanks to xet7.</summary>The Playwright tests address an Admin Panel menu entry by its data-id, not by
a per-page class. Fourteen tests were failing in every browser on
locator('.js-people-menu'), which no longer exists: Admin Panel / People
renders the shared left menu, whose entries are
a.js-left-menu-item(data-id="…"), and data-id is what the design says a
page's click handler reads — so a conversion cannot take it away the way it took
the per-page class. The same sweep updated the Problems report entries, the
per-report search inputs and pagination (one shared table page for all of them)
and the People pager in the page object
Every paginated admin table — Security, Speed, Tests, CPU usage, Files Report,
Rules Report, Boards Report, Cards Report, Impersonation Report and Recovery —
now renders through ONE shared table page instead of ten copies of the same
markup, helpers and handlers. They differ only in a column list. That made three
long- standing layout complaints fixable in one place: the table is full width
with table-layout: fixed, so every column gets the same percentage of the
width and a long id or file name can no longer widen the table past the panel
and push its right-hand columns outside the browser window; cell text wraps
instead of stretching its column; and on windows ≤ 800px the left menu goes full
width on top with the table BELOW it, rather than the two being squeezed side by
side until the table is a few dozen pixels wide. Rows run title, status,
controls, table. Paging still fetches only the current page, and now takes that
window from the same helper that renders "page X / N". The design, and the list
of every page that uses it, is in docs/Features/Page/Table.md; the History, CPU
usage and Recovery design docs link to it and keep only what is specific to them
Every .jade template is now compiled by a test, using the same compiler the
build uses. Nothing else caught a broken template: the other test suites read
.jade as text and grep it, so a file the compiler rejects still passed them
all and the failure appeared only when the app was rebuilt. Written after a
<body> written inside an indented comment block broke the build — the comment
text is still lexed, so the angle brackets opened a tag that never closed. All
107 templates parse in about 0.2s
The shared left menu is a design of its own — docs/Features/Page/Left-Menu.md —
beside the table page: pure helpers (buildMenuItems(), paneTitle(),
activeCount()), one template, one stylesheet, and one suite,
tests/leftMenu.test.cjs, that asserts the helpers, the template, the side the
menu is on and its mirroring under a right-to-left language, and that no page
re-implements it. tests/tablePage.test.cjs does the same for the table page,
asserting against exactly the files that design's own Related files table lists
— so a path that moves without the doc being updated fails the suite
and updates the documentation:
<details> <summary><a href="https://github.com/wekan/wekan/commit/fd12199e9">docs/Design/Multitenancy/Multitenancy.md designs the alternatives to running one WeKan Node.js…</a> Thanks to xet7.</summary>docs/Design/Multitenancy/Multitenancy.md designs the alternatives to running
one WeKan Node.js server per customer. The shipped topology gives each tenant
its own ROOT_URL, database, uws.port and upgrade; this page works out what
one server serving many domains would cost, from WeKan's own code — the six
things such a process must solve and the Meteor 3 API for each
(Meteor.onConnection's httpHeaders for the tenant,
WebApp.addRuntimeConfigHook for the bundle's baked in ROOT_URL,
Meteor.absoluteUrl's rootUrl override), and four alternatives compared in a
table: process per tenant, a tenant field with mizzao:partitioner, a database
per tenant in one process, and Organizations as tenants — which WeKan already
half has. The recommendation is to keep one process per tenant and reach for
Organizations when the tenants are groups of one organisation, because the other
two trade a merely tedious operational cost for a silent cross-tenant disclosure
risk across 46 collections and 66 publications
The Admin Panel documentation mirrors the Admin Panel menu: a menu path is a
docs path, so Admin Panel / Settings / Visibility is
docs/Features/Admin-Panel/Settings/Visibility.md and Admin Panel / People /
Teams is People/Teams.md, with an index per section listing its panes in menu
order. The prose is swept with it — Version as the pane that opens, Visibility's
four groups (absorbing the separate Allow-private-boards-only and Custom-Logo
pages), Layout being PWA, Registration being People / Login, the removed
Accounts and Features tabs, the invite-domain field, the two board-member
restrictions, Backup first in Attachments, and the two named groups of the
Problems menu. Every link into the moved pages was updated, and the wekan.fi
pages that described the old panes were edited too
and fills untranslated strings in every language:
<details> <summary><a href="https://github.com/wekan/wekan/commit/78e2adb69">The twelve strings this release's multitenancy and site-theme work added — an Organization's…</a> Thanks to xet7.</summary>The twelve strings this release's multitenancy and site-theme work added — an Organization's domains and what they do, the domain-already-taken error, the Organization admins popup and its description, the backup scope and its description, and the line telling a site admin their theme applies to all tenants — are translated into all 142 languages, each in that language's own existing WeKan terminology for Organization, backup, admin and theme. They are written through the same tooling as before, so a human translation can never be overwritten and none of them is pushed to Transifex, where a real translation can replace them at any time
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/526905483">The strings that are untranslated everywhere — on Transifex and in git — are written directly…</a> Thanks to xet7.</summary>The strings that are untranslated everywhere — on Transifex and in git — are written directly, using each language's own existing translations and its kanban terminology as the reference, with no external translation service, API or password involved. 1,191 strings in fi, sv, da, nb, de, nl, fr, es, pt, pt-BR, it, ca, gl, eu, ro, cs, sk, sl, hr, sr, pl, hu, lt, lv, et, ru, uk, bg, el, tr, ar, he, fa, hi, id, vi, th, ja, ko, zh-CN and zh-TW — mostly this release's Admin Panel work, plus the recovery report, the problems status, the broken-card repair, the CPU figures, the import timeout and the map-to-existing-user dialog. A further 1,935 strings reach the regional variants (de-AT, es-MX, fr-CA, pt-PT, ru-UA, zh-Hant, ca@valencia and 57 more) from their base language, which is the same language's own words rather than a translation of anything. Nothing can overwrite a human translation: a string is filled only while it is a placeholder — missing, or equal to the English source — and a key that already holds a translation is skipped and reported
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cc71cb28c">Every remaining language followed, worked through one at a time from the most spoken to the least…</a> Thanks to xet7.</summary>Every remaining language followed, worked through one at a time from the most spoken to the least: Chinese, Hindi, Spanish, Arabic, French, Portuguese, Indonesian, Malay, German, Italian, Thai, Tamil, Telugu, Punjabi, Swahili, Dutch, Yoruba, Igbo, Gujarati, Odia, Uzbek, Azerbaijani, Khmer, Uyghur, Zulu, Belarusian, Xhosa, Armenian, Afrikaans, Turkmen, Mongolian, Wolof, Georgian, Tamazight, Macedonian, Welsh, Occitan, Breton, Yiddish, Venda, Asturian, Walloon, Frisian, Acehnese, Esperanto, Flemish, Volapük and Klingon — each in its own kanban vocabulary, and each regional variant taking its base language's words afterwards. What is left untranslated is, in the main, the strings a language spells exactly as English (Action, Format, Status, Server, Menu, Type, the colour names, the poker numbers, LDAP / OAuth2 / GridFS / S3): the tooling ignores a fill equal to the English source, so they can never be recorded as translated — and they are already correct on screen. The long prose of the Volapük and Klingon files is deliberately left in English rather than invented
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/526905483">releases/translations/fill_translations.py joins the .mjs of the same name: the same rules — the…</a> Thanks to xet7.</summary>releases/translations/fill_translations.py joins the .mjs of the same name:
the same rules — the same definition of a placeholder, the same en.i18n.json key
order, the same 2-space indent — in Python, for an environment without a node
runtime. Either can list what a language still needs and apply the translations
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary>js/incomplete-sanitization (High), alert #428.</summary>js/incomplete-sanitization (High), alert #428, in
tests/boardHeaderOneLine.test.cjs: the CSS rule bodies were split with
rule.split('{') and the trailing brace removed with body.replace('}', ''),
which strips only the FIRST }. The block is now matched with one regex that
captures selector and body as separate groups, so no brace ends up in the text
and there is nothing to strip.
js/incomplete-sanitization (High), alert #427, in
tests/testsAreRegistered.test.cjs: a file name was spliced into a RegExp
after escaping only dots, leaving every other metacharacter — the backslash
above all — able to change the meaning of the pattern. It now goes through an
escapeRegExp() that escapes the full set, backslash included.
and resolves the following GitHub Dependabot alerts in npm dependencies:
<details> <summary><a href="https://github.com/wekan/wekan/commit/56cbcdb7e">brace-expansion 5.0.7 → 5.0.8</a>.</summary>brace-expansion 5.0.7 → 5.0.8 (RUNTIME dependency, transitive via
minimatch): fixes
CVE-2026-14257
(GHSA-mh99-v99m-4gvg, High, CVSS 7.5, alert #119) — expand() bounds how MANY
results it produces (max, 100000) but not how LONG they get, so chained brace
groups keep the count under the cap while every result grows one character per
group. A ~7.5 KB input ('{a,b}'.repeat(1500)) exhausts memory and kills the
Node process with a fatal, uncatchable out-of-memory error that try/catch
cannot contain. 5.0.8 bounds the total characters one expand() call may
accumulate (maxLength, default 4000000) inside the output-building loops, so
intermediate arrays are bounded too and oversized input is truncated rather than
fatal. Patch release in the same major line; the only other change is
engines.node, which drops Node 18 — WeKan builds and ships on Node 24
(Dockerfile NODE_VERSION=v24.18.0, CI NODE_VERSION: '24'), so nothing is
affected (#6523, merge commit )
postcss 8.5.15 → 8.5.22 (dev dependency; enters only through css-loader,
which the rspack build uses for .css, and is not itself part of the shipped
bundle): fixes
GHSA-r28c-9q8g-f849 (High,
CVSS 7.5) — postcss follows a /*# sourceMappingURL=... */ comment in the CSS
it parses and builds the path with path.join(dirname(from), annotation), which
normalises but does not sandbox .., so a crafted comment discloses arbitrary
.map files. Affected <= 8.5.17, patched in 8.5.18. nanoid 3.3.12 → 3.3.16
rides along as postcss's own dependency (#6522, merge commit )
Both are lock-only changes whose tarball integrity hashes were verified against
the npm registry, and the brace-expansion lock edit was reproduced independently
with npm update brace-expansion --package-lock-only, which produced a
byte-identical diff. elliptic (alert #55, CVE-2025-14505) is still left pinned
for the reason recorded in v10.14: no fixed version is published upstream — the
latest release is still 6.6.1 and the advisory lists no patched version. It is a
dev-only transitive polyfill (@meteorjs/rspack → node-stdlib-browser →
crypto-browserify → browserify-sign/create-ecdh), no WeKan client module
imports node crypto, and the built client bundle contains no
elliptic/secp256k1/crypto-browserify code at all, so it never reaches
users.
and has the following developer-tooling fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/e876b6029">docs/Security/gh/pull-security-reports.sh reported OK for every endpoint and then wrote empty split…</a> Thanks to xet7.</summary>docs/Security/gh/pull-security-reports.sh reported OK for every endpoint and
then wrote empty split files and blank counts. gh api --paginate --slurp
returns ONE ARRAY PER PAGE, so a list endpoint arrives as
[[alert,...],[alert,...]]; nothing flattened that, so .[] yielded PAGES
rather than alerts and every select(.state == "open") ran against an array. jq
failed, its error went to /dev/null, and the > redirect left a 0-byte file —
not valid JSON, so every later step on it failed too and printed nothing. A
security report that quietly claims there is nothing to see is worse than one
that fails. The pages are now concatenated (only when every element is itself an
array, so single-object endpoints like the SBOM are untouched), a failed filter
leaves a valid empty array plus a readable error instead of truncating the file,
the secret-scanning redaction validates the redacted copy before deleting the
dump that still holds live secrets (and removes it on the failure path too), and
counts always print a number. The same shape had also broken the newest-analysis
lookup and the codeql-databases dump
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/8e58456c4">The Change profile image window (Member settings > Change Avatar, and the same window in Admin…</a> Thanks to xet7.</summary>The Change profile image window (Member settings > Change Avatar, and the same
window in Admin Panel > People) rendered each avatar row's "Delete" link and
file name INSIDE the row's select link. An <a> inside an <a> is invalid
HTML: the browser closes the outer anchor, so the file name and the Delete link
fell out of the row's flex layout and rendered as bold text floating at the
right edge, and a click on Delete also selected that avatar. Delete and the file
name were shown only for the avatar currently in use, so no other upload could
ever be deleted, and an empty p.sub-name added a stray block in the middle of
the row. Each row is now one flex line - thumbnail, file name (truncated with
the full name as a tooltip), a check mark when it is the avatar in use, and a
delete button that is a SIBLING of the select link, so deleting never also
selects and every uploaded avatar can be deleted. The row in use is highlighted
like in the other pop-over lists, the initials row shows its "Default avatar"
label under the name, and the upload hints (max filesize, allowed filetypes,
invalid filename) are grouped into one muted paragraph above the upload button.
Deleting the avatar in use left profile.avatarUrl pointing at a file that no
longer existed - a broken image everywhere - so both windows now fall back to
the initials when the deleted file is the one in use. The Admin Panel window
also passed userData= to userAvatarInitials, which reads this.userId, so
its initials rendered empty
On iPad Safari a full-screen "Uncaught runtime errors: ERROR Script error."
panel appeared after login, and again when the Safari Share sheet was opened on
All Boards to add WeKan to the home screen. That panel is webpack-dev-server's
error overlay, shipped by the rspack DEV server only - a production build has no
dev server and no overlay. It opens for every window error event, including
ones that carry no information: a browser sanitises an error thrown by a
CROSS-ORIGIN script to the bare message "Script error." with no error object and
no stack, and iOS Safari raises those from its own machinery and from content
blockers and extensions, which is what the Share sheet triggered. It cannot come
from WeKan's own code, because the bundle is served same-origin (no CDN_URL),
so a real WeKan error always reaches the overlay with its actual message and
stack. The overlay now ignores exactly that one message and still shows every
other runtime error and all compile errors
The board bar (the second header bar) fits on one line on iPad. In landscape it wrapped onto two rows - board title, edit, visibility, watch, star and sort on the first, Filter / Search / view / dependencies / multi-selection / hamburger on the second. Measured at a 1180 CSS px viewport the two rows needed about 1304px on one line, ~124px more than the viewport, and nearly all of that is per-button empty space rather than text: each button carries 12px side margins, 10px on each side of its icon and a 10px gap after its label, about 54px of chrome around a ~60px icon+label. So nothing has to be hidden at that width: halving the chrome between 801px and 1400px frees ~295px, which fits the bar on one line with room to spare for languages with longer labels, and icon and label sizes are untouched so the buttons stay comfortable to tap. In tablet portrait (820px), where even the halved chrome cannot fit the labels, they drop to icons only between 801px and 1000px - the same treatment phones already get below 801px, with titles, tooltips and popups unchanged. Wide desktops, which never wrapped, look exactly as before, and phones keep their 44px touch targets
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9df568c0c">A long board title now moves ALL of the board bar's buttons to its second row, under the title</a>. Thanks to xet7.</summary>A long board title now moves ALL of the board bar's buttons to its second row, under the title. Before, the controls were split across the two rows: the title kept the left group (edit, visibility, watch, star, sort) beside it and only the right group (Filter, Search, view, dependencies, multi-selection, hamburger) wrapped underneath. That is what separate flex items do - the title and the three button groups were four children of the header row, so the row broke wherever it ran out of width. The three groups now live inside one wrapper, so the row has just two items, the title and the buttons, and either the buttons fit beside the title or the whole set moves down together. A short title is unaffected, because the wrapper measures as its own content width and only moves down when it genuinely does not fit. A title longer than the whole bar now wraps inside the title instead of overflowing the viewport
</details>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/f92b49d1b">Copying a board works again (POST /api/boards/:boardId/copy). swimlane.copy(), which runs on the…</a> Thanks to xet7.</summary>Copying a board works again (POST /api/boards/:boardId/copy).
swimlane.copy(), which runs on the server, called the SYNCHRONOUS
getDefaultSwimline() on the source board to detect its default swimlane; for a
board with no pickable swimlane that getter self-heals via a synchronous
Swimlanes.upsert(), which Meteor 3 rejects on the server (update is not available on the server. Please use updateAsync() instead.), so the whole copy
threw. It now calls the existing getDefaultSwimlineAsync() (same pick +
idempotent upsertAsync self-heal). Two related hardenings landed with it: the
copy REST route caught the error with sendJsonResult(res, { data: error }),
and that helper defaults the HTTP status to 200 when no code is given — so a
copy that THREW was returned as 200 with the error object as the body,
indistinguishable from success (that is what the REST test caught); the route
now returns 500 with the message and logs the stack, so a failed copy is a
clean, diagnosable error. And board.copy()'s custom-field remap did
card.customFields.map(...), which throws for a card whose customFields is
absent (the schema defaults it to [], but a card copied via .direct or
seeded raw can lack it) — it now skips cards that have none, matching the
!Array.isArray(this.customFields) guard used elsewhere in the model
All Boards header on a phone, follow-up: the section icon and the
Multi-Selection / Sort buttons really do share ONE row now, with the Search box
on the row below, and a workspace name no longer hides under its "bars" menu
button. The buttons and search box live inside a .path-right wrapper, so the
v10.34 attempt (giving that wrapper flex) only made the WRAPPER wrap as a
two-line block beneath the section icon, leaving the icon alone on row 1. On a
phone .path-right is now display: contents, which dissolves the wrapper so
its buttons and search field become direct items of the header's own flex row;
the header then wraps them itself — section icon + buttons on row 1, full-width
search on row 2. And the narrow-left-menu workspace NAME had a min-width floor
that stopped it shrinking, so in the cramped row its box overflowed onto the
"bars" workspace-menu button to its right and the hamburger looked like it sat
on top of the name; the floor is removed and the name's box is clipped, so the
name truncates with an ellipsis and can never cover the menu, while the menu
button and count never shrink
Upgrading no longer grows a spurious "Restored Items" column full of cards that
look empty but are not. Four obsolete per-swimlane-lists-era board migrations
fought today's data model and are removed. restoreLostCards created a "Lost
Cards" swimlane and a "Restored Items" list and moved cards into them — it
counted a card "orphaned" when its list was merely ARCHIVED (archived lists are
excluded from the set it checks) and "lost" when its swimlaneId was ''
(which is NORMAL for a board-wide shared card), so it dragged real, healthy
cards into "Restored Items", where they rendered oddly.
comprehensiveBoardMigration and fixMissingListsMigration converted today's
board-wide SHARED lists (swimlaneId '') back into per-swimlane DUPLICATE
columns — the exact damage the startup schema step merge-per-swimlane-lists
now UNDOES — and restoreAllArchived un-archived every swimlane / list / card
at once. All four had NO callers left (their admin migration dashboard was
already removed), so they were dead code exposed only as admin-callable Meteor
methods that could re-corrupt a board if invoked. The correct paths remain: the
board-open self-heal (repairBoardData) relinks a genuinely missing swimlaneId
or an orphaned card to the board's real first list/swimlane, and
merge-per-swimlane-lists merges any per-swimlane duplicate columns back into
one shared list while cards keep their swimlaneId; it still keys off the era's
board markers in existing data, so removing the code that once wrote them
changes nothing for already-migrated boards. A test keeps the four from coming
back
and has the following developer-tooling changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c2ad9dddb">build.sh and build.bat gain a Setup → "Update git" action that makes the working copy current in…</a> Thanks to xet7.</summary>build.sh and build.bat gain a Setup → "Update git" action that makes the
working copy current in one step: git fetch --all --prune, git pull --rebase --autostash onto origin/<branch>, then it repoints any CHANGELOG commit links
the rebase made stale, then shows git status. It never commits — if the hash
fix changed CHANGELOG.md it says so and leaves it for review — and on a rebase
conflict it stops with the exact commands to continue or abort. The commit-link
repair that lived inline in releases/release-all.sh is extracted to
releases/fix-changelog-hashes.sh so there is ONE implementation, now called by
both the release script and the new menu action; behaviour is unchanged (only
the unreleased section is touched, stale links are repointed to the same-subject
rewritten commit, unresolved links warn and never abort). The Windows .bat
runs the shared bash script via Git Bash and degrades gracefully with
instructions when bash is not on PATH
The Playwright WebKit tests no longer fail spuriously from renderer crashes on a
headless ARM host. A test run left eight core dumps in tests/playwright/, all
of WebKit's WPEWebProcess renderer, aborting (SIGTRAP) from the Mesa llvmpipe
software-GL stack on this Apple Silicon / Asahi host; a renderer that dies
mid-test makes the next click/navigation time out, so it looked like WebKit-only
failures (admin users, checklist delete, rules, All Boards sort) rather than
real WeKan bugs. playwright.config.js now defaults
WEBKIT_DISABLE_DMABUF_RENDERER=1 (unless already set) — the standard fix for
WPE WebKit aborting in headless / containerized / VM / software-GL environments;
Playwright passes process.env to the browser so it covers every run path, and
it is harmless for Chromium and Firefox. .gitignore now ignores core dumps
(core.<pid>) so a crashing test browser can no longer leave untracked multi-MB
files to be committed by accident
The developer build/test helper is renamed rebuild-wekan.sh → build.sh and
rebuild-wekan.bat → build.bat (a shorter name; the menu and every option are
unchanged). All references were updated across the repo — the docs
(Build-from-source, Windows, Sandstorm, Raspberry Pi, etc.), README.md,
CONTRIBUTING.md, CLAUDE.md, releases/release-all.sh and
releases/fix-changelog-hashes.sh, tools/forge-mirror.js, and the tests (the
guard suite is renamed to tests/buildDevServer.test.cjs). If you invoke it by
the old name, use ./build.sh / build.bat now
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/d168a9d30">Admin Panel → Problems → Summary now has a Repair button for "Broken cards N"</a>. Thanks to xet7.</summary>Admin Panel → Problems → Summary now has a Repair button for "Broken cards N".
The page reported the count and told the admin to "run the repair migration",
but there was no button anywhere to run one — and no repair in the app could
actually clear that count. countBrokenCards() counts every card missing a
boardId OR a swimlaneId OR a listId, while repairAllBoards() walks only
NON-ARCHIVED boards and only ever sets swimlaneId, so three kinds of counted
card could never be repaired by anything: cards on an archived board, cards with
a missing listId, and cards with no boardId at all. The new admin-only
repairBrokenCards method runs the standard per-board pass and then closes
those gaps, assigning the affected cards to their board's first usable
list/swimlane (creating a default one when the board has none) and resolving
each board's defaults once rather than once per card. Cards with no boardId
cannot be placed on any board — guessing would drop a user's card onto an
unrelated board — so they are reported as unfixable and left alone, never
deleted, instead of leaving a count that never reaches zero unexplained. What
counts as broken now lives in ONE pure module used by both the count and the
repair, so they can no longer drift apart, with unit and negative tests for the
rule
Admin Panel → Problems → CPU usage now shows the CURRENT CPU usage between the
page title and the Search box. The page listed only PAST high-CPU periods, so it
could not answer "what is the CPU doing right now?" — the one question an admin
opens it to ask. The new line shows the system CPU percentage, core count,
1/5/15 minute load average and the coarse "what WeKan is doing" label, refreshed
every 5 seconds and outlined in red while the monitor considers the CPU
sustainedly high. It reads the same sample the monitor writes its event rows
from, so the header and the rows below it can never disagree, and with the
background monitor disabled (WEKAN_CPU_MONITOR=false) it measures between
calls using a separate baseline instead of always reading 0%
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1fb382f0e">Admin Panel → Problems no longer shows a finished board-repair as still running, nor counts…</a> Thanks to Alishara and xet7.</summary>Admin Panel → Problems no longer shows a finished board-repair as still running,
nor counts informational CPU rows as "new problems". On an idle server with far
fewer than 146 boards the Status page showed "Board data-repair — 146/146
boards" running forever (with a matching "Migration or repair" login warning),
and the CPU area showed dozens of "new problems". The repair actually finishes
and writes running:false, but the startup pass ALSO fired a non-awaited
running:true progress write at the last board (boardsDone === boardsTotal);
being fire-and-forget it could land after the awaited completion write and pin
the doc at "running, 146/146" — and a process killed mid-repair leaves
running:true too. The startup pass now persists only intermediate progress
(the completion write owns the final state), and the Status page asks a pure
isStatusActive() instead of reading the raw flag: a migration/repair counts as
in progress only while it is running AND not in a terminal phase AND not already
finished by count (done >= total) AND updated recently (a running flag with no
recent progress write is a crashed run), so a doc already stuck on an instance
heals itself and the "Migration or repair" warning clears with it. The CPU "new
problems" count excluded nothing: the monitor writes one detected row when a
high-CPU period starts and several severity:'info' rows (remediated when it
ends, mitigation and FerretDB-governor rows), so each brief, already-over spike
looked like several problems; the count now excludes severity:'info' rows —
notices that a problem was handled or cleared are not problems — while rows with
no severity still count, so nothing unclassified is dropped
"Map to existing user" for an imported (virtual) member now searches every user
instead of listing only board members. After importing a Trello board and
choosing to map users later, the picker offered nothing but the admin: it listed
only the ACTIVE REAL MEMBERS of the board, and right after an import the real
people have no membership at all — the placeholders do — so on a board whose
only real member is the importing admin there was literally nobody to pick, and
no search box either. The picker now has a search box that goes through the same
permission-checked searchUsers method the normal add-member typeahead uses, so
the same board-membership check and the same #6116 same-org/team restriction
apply; with the box empty it still lists the board's real members (which changes
no roles at all), and typing searches every real user and labels the ones who
are not on this board yet. Mapping onto a user who is not a board member adds
them with the PLACEHOLDER's own role — the role the import recorded for that
person, never a higher one — and only after passing exactly the checks inviting
them would: the Admin Panel "roles allowed to invite" setting, the same-org/team
restriction, and the refusal to add a deactivated account. This also fixes
sanitizeUserForSearch dropping profile from every search result — it read
the Mongo-style paths in its allowlist (profile.fullname) as FLAT keys, so
both this picker and the add-member typeahead showed a blank name with only the
username in brackets
Admin Panel report styles were never loaded, so every report's prev/next
pagination button rendered with a black background.
client/components/settings/adminReports.css was not imported anywhere, and
package.json sets meteor.mainModule, which disables Meteor's eager loading —
a CSS file that nothing imports is simply not in the bundle. The buttons fell
through to the global button { background: var(--theme-accent, #000) } in
forms.css, which is BLACK when no custom theme colour is set. All prev/next
controls in WeKan (admin reports, People/Org/Team/Domain, All Boards, board
Table view, cron tables) now share one themed stylesheet that also covers
:focus and :active — forms.css styles those at a specificity equal to or
higher than a plain .some-pagination button rule, so even a loaded stylesheet
lost as soon as the button was clicked and pressing "next" left it dark grey
until it lost focus. Disabled is now the same themed outline faded rather than a
grey block, and the "3 / 42" page info inherits the surrounding text colour
instead of a hard-coded #333 that vanishes on dark backgrounds
Admin Panel → Problems → Cards report loads and pages faster. The report already
paginated server-side (25 rows per page via limit/skip), so it never loaded all
cards into the browser, but the cardsReport publication sent WHOLE card
documents — description, customFields, vote/poker sub-documents, every date
field — when the table renders only six columns, so a 25-row page could be
hundreds of kilobytes on boards with long descriptions; it now projects only the
six columns shown. And every prev/next click re-ran the report's count method as
well as re-subscribing, paying for a full collection count plus a second server
round trip even though the total cannot change just because you moved to the
next page; opening a report and searching in it now recount, plain paging does
not. Both changes also apply to the files, rules, boards, impersonation and
recovery reports, which share the same loader
Six Meteor methods threw Did not check() all arguments during call to '<method>', because this app runs with the audit-argument-checks package: a
method that RECEIVES an argument it never check()s fails the call outright.
problemDetailReport(area) had the defect but nothing had ever passed it an
argument, so it surfaced only when the new Admin Panel → Problems → CPU usage
header started polling problemDetailReport('cpu') every 5 seconds and filled
the server log with the exception. Auditing every server method for the same
defect found five more that ARE called with arguments from the client, so those
features were failing outright: unlockUser (Admin Panel → People, unlocking a
locked-out user), runBackup, restoreBackup and saveBackupSchedule (Admin
Panel → Attachments) and migrateTextDatabase (the MongoDB ↔ FerretDB text
migration), plus getServiceConfiguration, which has no current client caller.
Each now checks its arguments against the types its call sites actually pass
Every bundled binary now runs through cpu-exec on Sandstorm and in the snap,
so a CPU missing an instruction-set feature falls back to qemu-user instead of
killing the app with SIGILL. cpu-exec (#6458) exists for exactly that — AVX
masked by a hypervisor (QEMU/KVM/Proxmox), an old CPU, ARMv8.0 on a Raspberry Pi
— and with no features declared it is a plain exec, which is why the design is
to route everything through it. Sandstorm SHIPPED cpu-exec and qemu-x86_64
but its grain launcher spawned all eight bundled binaries directly (ferretdb,
mongod 3.0 twice, niscud, the legacy mongo CLI twice, and node for the bridge
and the importer), so the safety net sat in the package unreachable — on the
platform with the least control over its hardware, since a grain runs on
whatever CPU the host has. The snap routed mongod but not FerretDB
(ferretdb-control, both the external-DB and SQLite launches) and not node
(wekan-control, the main application start and both maintenance-page starts).
Both are routed now, each with a direct-exec fallback so an older deps image or
snap revision still works; on Sandstorm the guard also requires /bin/bash,
since cpu-exec is a bash script, and the snap's main start keeps its ulimit -s 65500. Docker and the bundle launcher already routed their binaries and were
rechecked for other unrouted launches. The wiring guard previously asserted only
that the Sandstorm build SHIPS cpu-exec — exactly how the gap survived — and
now asserts that each launcher actually uses it
build.sh now raises the inotify watch limit when it is too low, and
.meteorignore stops Meteor watching .tools/ and FerretDB/. Meteor's file
watcher takes ONE inotify watch per DIRECTORY, and fs.inotify.max_user_watches
is per USER, shared with every other watcher — an editor is usually the other
big consumer. When it runs out, Meteor fails with a message that blames the disk
and sends people looking in the wrong place: inotify_add_watch on '<path>' failed: No space left on device. ENOSPC from inotify_add_watch means the
WATCH LIMIT is exhausted, not that the disk is full. Two non-app trees were 72%
of this repo's 41,040 watched directories — .tools/ (20,535 dirs, 4.1 GB: a
full Node install with headers for every openssl arch, a Go toolchain with its
caches, build logs) and FerretDB/ (9,148 dirs, 3.4 GB: the Go fork checkout).
Both were already in .gitignore, but the watcher only honours .meteorignore,
so Meteor kept walking them; excluding them takes the count to 11,213. And
build.sh now checks the limit on every run and raises it to 524288 (plus
max_user_instances to 1024) with sysctl, persisting it to
/etc/sysctl.d/60-wekan-inotify.conf. It never aborts a build: it is a silent
no-op when the limit is already high, uses sudo only when it can, prints the
exact commands to run by hand when it cannot, honours WEKAN_INOTIFY_WATCHES=0
to skip, and returns immediately on macOS/BSD, which have no inotify
Drag handles: the "Show desktop drag handles" toggle now works on touch screens,
the card body pans the board when handles are on, the handle is finger-sized on
touch, and tab order no longer jumps to invisible controls. The rule was
isTouchScreen() || preference, so on a touch screen the OR was already true
and the toggle could never hide the handles — and every handle in the app plus
every sortable handle selector goes through that one helper. The setting is now
three states (on, off, never-chosen): an explicit choice always wins, including
OFF on touch, and only never-chosen lets the device decide, where a touch screen
still gets handles by default. Handles also decide what a DRAG MEANS, and that
half was broken too: .minicard and .list-header carried nodragscroll
unconditionally, so with handles ON a finger dragging across a card neither
moved the card (only the handle does) nor panned the board — it did nothing;
they now opt out only when handles are OFF, i.e. only when the element IS the
drag source. The handle itself was a 20–28px icon in the bottom-right corner, a
mouse-sized target that is close to unusable on a wall-mounted infoscreen; on a
coarse pointer it becomes a 48px full-height strip down the leading edge with
the card content padded to match, while a mouse keeps the compact corner handle.
And the keyboard card/list move buttons are focusable but opacity: 0 at the
START of the card content, so tabbing went card → invisible → invisible → next
card; they are now revealed while focused
The quick-access top bar now fits on ONE row on phones. On an iPhone 12 mini
(375px) it wrapped to two rows — home + logo + mobile/desktop toggle on the
first, drag-handle toggle + zoom + notifications + user menu on the second. The
mobile CSS chose that deliberately (a nowrap row overflowed and was clipped),
but that treated the symptom: the row did not fit because the home link kept its
full "All Boards" label — the widest item in the bar, and longer still in most
languages — the logo was allowed 120px of a 375px bar, TWO items had
margin-inline-start: auto (an auto margin absorbs all remaining space on its
line, so the first one filled row 1 and pushed the rest onto row 2), and items
carried 8px gaps plus 0.5rem margins on both sides on the iPhone path. The row
now fits: icon-only home link, an 84×20 logo cap, exactly one auto margin, 6px
gaps and 3px margins — 342px of 375px by the declared sizes. The home label is a
bare text node so it cannot be selected directly; font-size: 0 on the link
collapses it and the icon's own size brings the icon back, with the text still
in the DOM for screen readers. Both the @media path and the .iphone-device
path (added by boardBody.js, so present on a board page and absent on All
Boards) were changed together, or the bar would have changed height when
navigating between those two screens The "All Boards" label is also hidden
whenever mobile MODE is on, which is a different thing from a narrow viewport:
body.mobile-mode comes from the phone/desktop toggle in that same bar, is a
deliberate user choice and applies at any width, so on a wide screen in mobile
mode the label was still
showing](https://github.com/wekan/wekan/commit/b9f178ef5). Hiding the label
needed !important and the last word in the file: ELEVEN rules in header.css
set a font-size on that same link, most of them !important, including three
scoped to the All Boards page itself (body:not(.board-view) / .wrapper ~)
and one setting 16px !important — so the first attempt lost to all of them and
the label kept showing on exactly the page it was reported on. The notification
bell also sat in 36px of margin around a 28px icon (10px each side of the button
plus 0.5rem each side of its wrapper); both layers now give 2px a side, taking
the measured one-row budget from 342px to 320px of
375px](https://github.com/wekan/wekan/commit/76516a1a2). Finally, the avatar
still dropped to a second line with space left beside the bell, because a
width-based rule forced flex-wrap: wrap !important on that bar — added
deliberately, "so ~7 items fit in two wrapped rows". An !important wrap beats
any nowrap, so none of the one-row work took effect at small width, and a
wrapping flex row breaks BEFORE the last item rather than shrinking anything.
All eight flex-wrap rules on the bar are now nowrap, and the username/full
name beside the avatar is hidden at small width too (the template hides it
unless isMiniScreen, but that also weighs mobile mode and the device, so it
can be true while the bar is narrow, and a full name is easily 100px+ of a 375px
bar). Measured budget: 328px of
375px](https://github.com/wekan/wekan/commit/fff495127)
Board tiles and the "+ Add Board" / "+ Add Template Container" tile are now the
same, shorter height on mobile. A board tile was 8rem (128px) on a phone while
the add tile had no mobile height at all — it fell back to the desktop
min-height: 72px plus 42px of padding and then grew further around its wrapped
label — so the two were never the same height and both wasted vertical space.
Both are now a flat 4rem (64px) with box-sizing: border-box, so the number is
the rendered height rather than a floor that padding is added to, and the board
tile's 24px/18px vertical padding (what made it tall) is cut to 6px. One rule
covers every All Boards tab — Starred, Templates, Remaining and Workspaces all
render the same .board-list tiles Only the add tile actually shrank at first,
leaving a board icon towering over "+ Add Board" on the Remaining tab: two
selectors set that height and they are not equally specific —
.board-list.mobile-view .board-list-item (0,3,0) beat .board-list .board-list-item (0,2,0), while the add tile escaped because its own selector
is also (0,3,0) and came later. Both .mobile-view shapes are now spelled out,
and because that 8rem rule sits outside any media query — .mobile-view comes
from Utils.isMiniScreen(), which weighs mobile mode and the device as well as
width — the override is repeated outside the media query
too](https://github.com/wekan/wekan/commit/000a29129). The tiles STILL differed
for a reason unrelated to either height: the mobile board list is a grid with a
fixed height and no align-content, so it defaulted to stretch and its single
row filled the whole column — "+ Add Board" showed a 4rem grey label at the top
of a stretched cell, while a board's li carries the board colour itself and
painted the entire cell. align-content: start plus align-items: start keeps
rows at their content height, and the list still scrolls because that comes from
the fixed height plus overflow-y: auto. The two mobile columns are also
rebalanced from minmax(84px, 30%) to minmax(140px, 42%): at 30% of a 375px
phone the left menu was ~112px so "Workspaces" and the counts wrapped, while the
board column had more width than two 4rem tiles need — it is now ~158px of menu
and ~209px of boards, still fitting two ~96px tiles side by
side](https://github.com/wekan/wekan/commit/534b80f92)
All Boards on a phone now matches what a wide screen already does: board titles
read left to right instead of breaking one letter per line, "+ Add Board" spans
both columns, the folder name and the search box share one row, and the search
placeholder is gone. The titles broke because space for the single ~26px
absolutely-positioned drag handle was reserved THREE times on the way down —
32px on the li, 30px in the tile padding and 30px again on .details — which
on a ~96px tile left about nothing for text; it is now reserved once, on the
innermost container, leaving ~52px. The header wrapped because the search box
would not shrink (a flex item's default min-width: auto refuses to go below
its content width), so min-width: 0 plus flex: 1 1 auto lets it take what is
left beside the title. The "Search boards" placeholder filled the whole box on a
phone and is replaced by an aria-label, which is what a placeholder should
never have been standing in for "+ Add Board" and a board tile are then made
identical in size: the add tile is one grid cell again rather than spanning both
columns, and — since .js-add-board carries margin: 8px !important while
.js-board carries margin: 8px — both are zeroed at small width rather than
only one, which would have made the add tile the odd one out instead. The grid's
own 8px gap already separates the cells, so every tile is one 96×64px cell at
375px, add and board alike](https://github.com/wekan/wekan/commit/8fd2982ec).
The menu title also goes ABOVE the search box on a phone rather than beside it:
squeezed onto one row, "Remaining" was ellipsised to "R.." and the search still
had barely any width. They stack again, but without the tall empty band that
made stacking look wrong before — each part takes exactly one full-width row
with a fixed 6px row-gap, so the search sits directly under the
title](https://github.com/wekan/wekan/commit/9d0908e4b)
"Show desktop drag handles" now works on the All Boards page too. It governed
swimlanes, lists and cards on a board, but board TILES always showed their
handle and were always draggable from anywhere, so turning handles off did
nothing there. The handle markup is now behind the same helper in both places it
appears, and — the half that matters — so is the drag source: with handles ON
the handle is the only place a board drag may start, so a finger dragging across
the rest of the tile scrolls the list instead of picking the board up, the same
contract cards and lists follow. Boards use HTML5 drag-and-drop rather than
jQuery sortable, so this could not be done by swapping a handle option: the
li keeps draggable="true" (a drag can only start from a draggable element,
and moving the attribute onto the handle would drag the handle rather than the
board) and dragstart cancels the drag unless it began inside .board-handle.
With no handle rendered, the title reclaims the 28px reserved for it
Adding a workspace now shows a real input field. The "+" beside Workspaces
called window.prompt(), which a browser is free to refuse — iOS Safari offers
"don't allow further prompts" for a page and then suppresses every later one, so
the button did nothing at all and no input field ever appeared. It now opens a
normal WeKan popup with a text input and a submit button, the same pattern every
other create flow uses; submitting it empty keeps the popup open and refocuses
the field rather than closing silently, which is what a cancelled prompt looked
like. This was the only window.prompt() left in the client
Admin Panel tables fit the screen and wrap their text on a phone. Admin Panel /
Version needed a horizontal scroll to read a value that easily fits on screen —
"WeKan Version" at the left edge and its version number about a thousand pixels
away — because every admin table is forced to min-width: 1200px !important
with white-space: nowrap cells, to guarantee a scrollbar for the wide data
tables. Reasonable for those, but a two-column table is pushed to 1200px for
nothing and nowrap cells can never use a second line. At small width the table
is now at most as wide as the screen and cells may use as many lines as they
need, with long unbroken values (version strings, paths, URLs) breaking rather
than pushing it wide again; the label column keeps a 45% share so it does not
collapse beside a long value. Horizontal scrolling still works where a table
genuinely cannot fit
Search All Boards on a phone now fits the width, scrolls down, and drags with a
finger. Label Colors ran off the right edge because .global-search-page is
width: 40%; min-width: 400px — sized for a desktop column — so on a 375px
phone the block was already wider than the screen and the palette's flex-wrap: wrap had nothing to wrap into. The page could not be scrolled down AT ALL
because .wrapper, the GLOBAL layout element, is given height: 100vh; overflow: hidden by the All Boards stylesheet at this width: fine for the
boards page, which has its own scroll container, but it applies to every page
and this one had none, so everything below the fold was unreachable. And
dragscroll did nothing because dragscrollTouch.js scrolls the nearest
.dragscroll ancestor that can actually scroll, and no element here carried the
class. The help panel and the results list are now width-constrained, bounded
scroll containers carrying .dragscroll; the query form shares the class but is
a short fixed header, so it is explicitly excluded from becoming a scroll box of
its own Giving each panel its own scroll box turned out not to work — the panels
start BELOW the search form, so their bottoms and their scrollbars ended up past
the bottom of the clipped wrapper, leaving text cut mid-character after "Notes"
on iPhone and the list ending at "Differing operators … red label." on Fairphone
4. The page now has exactly ONE scroll container: its wrapper is tagged
global-search-wrapper and is height: auto; min-height: 100vh; overflow-y: auto at phone widths, with the panels back to plain content. .dragscroll
moved to that wrapper and was REMOVED from the panels, which is required rather
than tidying — dragscrollTouch.js takes the nearest .dragscroll ancestor and
gives up if it cannot scroll, so leaving the class on a now-static panel would
have disabled finger dragging on the whole
page](https://github.com/wekan/wekan/commit/be07356cc). That
single-inner-scroller still fought the ancestor: the wrapper box (min-height: 100vh) is taller than its parent #content, which is itself a scroller in
mobile mode, so both scrolled — on iOS Safari the page rubber-banded back to
"Differing operators" at the bottom, while Fairphone Firefox merely reached the
end. The clip is now released WITHOUT adding any scroller (.wrapper for this
page is height: auto; overflow: visible), leaving #content — the page's
existing scroller, and what a finger scrolls natively on touch — as the single
scroller, which cannot fight itself. The .dragscroll class is dropped from the
wrapper since native scrolling now handles it. A last touch clears iOS Safari's
bottom toolbar, which was still covering the final line: the scroller #content
is calc(100vh - 48px) and on iOS 100vh is the large viewport (toolbar
hidden), so its bottom edge sits behind the toolbar — the wrapper now has
calc(120px + env(safe-area-inset-bottom, 0px)) of trailing space so the last
line scrolls clear of it. The bottom stayed unreachable through several attempts
because the cause was #content, not the wrapper: #content is a flex child,
and a flex child defaults to min-height: auto — it grows to fit its content
and never scrolls, so anything past the fold overflowed out of reach. On this
page (min-height: 0 on #content, scoped with
body:has(.wrapper.global-search-wrapper) so no other page is affected) it
becomes a real scroller, inside a 100dvh body so the bottom tracks Safari's
toolbar instead of hiding behind the large-viewport 100vh, with
overscroll-behavior: contain to stop the landscape pull-to-refresh. A finger
drag over the grey content area still did nothing while the header bars scrolled
— because the header sits outside #content, that proved the body is the real
scroller and #content's own overflow-y: auto was trapping the touch;
#content is now a pass-through (overflow: visible; flex: 0 0 auto) so the
body is the single scroller, draggable by a finger
anywhere](https://github.com/wekan/wekan/commit/762547b92)
All Boards in iPhone landscape: the left menu no longer balloons and the folder
header no longer collides. Landscape is ~844px CSS-wide — above the 800px
breakpoint but still an iPhone, so the device-width media query applies while
the max-width: 800px rules do not. That split made the left menu 42% of
844px (~354px), flinging each count chip ~250px from its label, and left the
folder header ("Remaining" + multi-selection buttons + search) on a single row
where the buttons overlapped the title. The menu is now minmax(140px, min(42%, 210px)) (portrait unchanged at 158px, landscape capped at 210px) with the count
packed next to its label, and the header's stacking media query carries the same
device-width branch as the grid so it stacks in landscape too
Board tiles no longer merge on a laptop. The phone equal-tile work had six rule
groups whose selector lists carried a bare .board-list ... variant next to the
intended .board-list.mobile-view ... one, so they also hit the desktop float
layout where .mobile-view is absent — margin: 0 !important on .board-list li.js-board killed the 8px gap that .board-list li { width: 20%; margin: 8px } relies on, so adjacent coloured tiles touched and read as one block. All six
are now .mobile-view-only, restoring the desktop layout while leaving the
phone layout unchanged
The top-bar zoom %, notification bell and user/avatar menu now stay at the END
of the quick-access bar — the right edge in LTR, the left edge in RTL. They are
the last items in the bar and the middle starred-boards list normally pushes
them there with flex: 1, but that list is display: none on a phone, so with
nothing filling the middle the group bunched at the start with empty bar beside
it. A single margin-inline-start: auto (logical, so it flips for RTL
automatically) on the first item of the group pushes the whole group to the end,
with the row's other auto margin cleared so they do not split apart
Board tiles on All Boards are now a responsive grid, so widening the window adds
more boards per row instead of stretching the tiles. Both layouts were
fixed-column — desktop float: width 20% (5, wrapping to 4) and the mobile grid
repeat(2, 1fr) (always 2) — so more width just made the tiles wider. They are
now repeat(auto-fill, minmax(…, 1fr)): desktop minmax(200px, 1fr) (~3
columns at 1000px up to ~11 at 2500px) and mobile-view minmax(min(46%, 200px), 1fr) (keeps ~2 columns on a narrow phone but scales up on a wider one), with
the 8px grid gap replacing the per-tile margins. Phones still pin two columns
via the small-width media queries
All Boards drag-and-drop and touch targets. A mouse drag can reorder boards
again: the board list <ul> is a .dragscroll element so the mouse dragscroll
library was panning it instead of starting the tile's HTML5 drag, and
a.js-open-board being a link meant grabbing the title dragged the URL — the
tile is now .nodragscroll and the link draggable="false". With drag handles
ON, dragging the handle did nothing because the gate checked dragstart.target,
which in Chrome/Firefox is the whole draggable li, not the handle; the press
location is now recorded on mousedown and read in dragstart. And "+ Add
workspace" is now a 44px touch target instead of a ~20px "+" a finger kept
missing. Reordering by FINGER did not work yet at this point — board reorder
uses HTML5 drag-and-drop, which browsers do not fire from touch; that is fixed
by the touch drag-and-drop bridge in a later entry below (a separate, tested
change, without discarding any of the mouse fixes here)
All Boards drag-reorder now persists and can move a board to the end, and the
header is decluttered. Dropping one board onto another threw INVALID [400]
because profile.boardSortIndex (written on reorder) had no schema entry while
the user profile enumerates its subfields — a schema field is added. Dropping
on a board can only place it BEFORE that board, so a dashed "drop here to move
to the end" placeholder follows the drag: while a board is dragged, an empty gap
opens up before or after the tile under the cursor — anywhere in the current
view (Starred / Templates / Remaining / a workspace), the end included — showing
where it will land, backed by a pure computeReorderedIds(..., after) that
inserts on either side and no-ops a drop into the board's own slot; the drop
reads the gap's real DOM position (so releasing OVER the gap works — the
placeholder is not a board tile, which had made the board snap back to its
original position). Hovering anywhere on a board icon opens the gap at THAT
icon's slot (drop to take its place), rather than needing to aim the icon's left
half; only the last icon's trailing half inserts after, to keep the end
reachable. The dragged icon is taken out of the grid flow while dragging (the
other icons reflow as if it were gone), so the gap lands exactly where the icon
will go rather than one slot to the left, and the drag ghost is centred on the
cursor (setDragImage) so the gap opens for the icon the ghost is over rather
than needing to aim the target's left edge. And the current folder's NAME beside
its icon in the header ("Starred"/"Remaining"/a workspace name) is hidden —
redundant with the icon, the highlighted menu row and the "My Boards" title, and
it crowded the Multi-Selection / Sort / search controls — kept for screen
readers only. The section icon left of the Multi-Selection button is now the
same Font Awesome glyph and colour as the matching left-menu item (fa-star /
fa-clipboard / fa-folder / fa-folder-open, #4d4d4d) instead of a Unicode emoji
Board icons can now be reordered by FINGER, not just with a mouse — and the same
fix makes every other HTML5 drag-and-drop in WeKan work from touch. HTML5
drag-and-drop (draggable="true" + dragstart/dragover/drop) is never fired from
touch by any browser, so on a touchscreen the board-icon reorder above, the
workspace drag-to-share and the card/rule drags could only be driven with a
mouse. Rather than rewrite reorder onto jQuery-UI-sortable (which would have
discarded every mouse fix in the two entries above), a small bridge,
client/lib/dragDropTouch.js, makes a finger drive the SAME handlers: a long
press (250 ms) on a draggable element starts a synthetic drag that dispatches
real, bubbling dragstart/dragenter/dragover/dragleave/drop/dragend events with a
dataTransfer shim, so the delegated handlers run unchanged. It is deliberately
safe beside the existing touch scrolling — a quick swipe on a draggable tile
still scrolls, and it only calls preventDefault once a drag is actually under
way; it fires a mousedown on the pressed element first so the reorder's handle
gate (which records the press location) works from touch too; it honours
setDragImage so the ghost is centred on the finger as it is on the cursor; and
it leaves form fields and jQuery-UI sortables (lists / swimlanes / cards,
already covered by touch-punch) alone. Loaded globally, with source-guard tests
pinning the scroll-coexistence and handle-gate properties
All Boards header on a phone: the section icon and the Multi-Selection / Sort
buttons now share ONE row with the Search box on its own row below, instead of
the section icon sitting alone on a wasted first line; the blue Sort button
always shows a sort icon (the plain fa-sort glyph, as a direct i.fa child
like the Multi-Selection button beside it, rather than the wrapped
fa-sort-alpha-asc that showed as a blank blue square); and on the narrow left
menu each workspace NAME keeps a small floor of width so at least the first few
characters stay visible (with the trailing menu button and count shrinking
first), so a workspace can be told apart while it is dragged — an unnamed
workspace still shows just its icon
and removes the following dead code:
<details> <summary><a href="https://github.com/wekan/wekan/commit/a440d44ea">Removed the cron migration subsystem: it never ran and had no UI, yet every logged-in client paid…</a> Thanks to xet7.</summary>Removed the cron migration subsystem: it never ran and had no UI, yet every
logged-in client paid for it. initializeCronJobs() was NEVER called, so its
SyncedCron.add() registrations and the 5-second job-queue processor never
started; the board-migration detector sat behind a commented-out startup hook
noting "Automatic migration detector is disabled — migrations only run when
opening boards"; and the Admin Panel had no Cron page at all — no route, no
menu button, no template ever included, and every helper and event for it
commented out. Despite that, imports/cronMigrationClient.js WAS imported by
client/imports.js, so every client subscribed to three publications and polled
cron.getMigrationProgress every 10 seconds forever for a feature with no user
interface. Migrations already run and are already reported through the paths
that work: Admin Panel → Attachments and the on-board-open repair, both driven
by the shared migrationProgress overlay mounted app-wide, plus the in-progress
list on Admin Panel → Problems → Summary. Removed the templates and styles, the
client module, the cronJobStatus model, the cron migration manager (28
admin-gated cron.* methods no UI called), the job storage, the board-migration
detector and the three publications that fed only the deleted client module.
server/cron/syncedCron.js is KEPT — it is SyncedCron itself, used by the
backup schedule and scheduled rules. The Problems in-progress list no longer
reads cronJobStatus, because a leftover status:'running' doc from an older
version would now have nothing to clear it and would report a migration running
forever
and has the following developer-tooling changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/a3dfc2b19">build.sh Dev server menu can now run on a custom port with a custom ROOT_URL host</a>. Thanks to xet7.</summary>build.sh Dev server menu can now run on a custom port with a custom ROOT_URL
host. It could only offer localhost:3000, the current IP, or a custom IP:PORT,
so running on a different local port with a different ROOT_URL host meant
editing the script. ROOT_URL is not cosmetic — Meteor builds absolute URLs from
it (e-mail links, OAuth redirects, attachment URLs) — so a subdomain setup has
to be told about it. The host answer takes either form: a bare label becomes
<label>.localhost (browsers and systemd-resolved resolve *.localhost to
127.0.0.1 with no /etc/hosts entry), and anything containing a dot is used
as-is with a note that it must resolve locally. A a bad port falls back to 3000,
the chosen port is freed first like the other dev options, and WEKAN_DEV_PORT
/ WEKAN_DEV_ROOT_URL skip the prompts so it can be scripted. The listen port
and ROOT_URL are treated as the different things they are: the port is appended
ONLY when the dev server is browsed directly, so https://wekan.example.com
stays exactly that instead of becoming http://wekan.example.com:4000 — behind
a Caddy proxy the public URL has no port, is https, and nothing listens on that
host:port at all
releases/release-all.sh now repoints stale CHANGELOG commit links before
releasing. Every bullet links the commit it describes, and those links are
written BEFORE the release, so anything that rewrites history in between — a
rebase onto an upstream change, an amend, a squash — changes the hashes and
every link in the not-yet-released section 404s once pushed (which is exactly
what happened to all eight links of this section). As its first local step,
before the Upcoming heading is renamed and before the release commit is made,
the script now leaves alone any hash that is still an ancestor of HEAD, and
resolves a stale one by reading the OLD commit's subject (the pre-rewrite object
is still in the clone) and finding the commit on this branch with exactly that
subject — the rewritten copy of the same commit. The substitution is bounded to
the section being released, so the same hash quoted in an older, already-pushed
entry is never touched; it works whether or not the heading has already been
renamed; and anything it cannot resolve is reported as a WARNING and left as-is,
because a broken doc link is never a reason to abort a release
Added docs/Security/gh/pull-security-reports.sh, which downloads every
security and code-quality report GitHub exposes for a repo — open AND closed —
into a timestamped run directory with one subdirectory per category of the
GitHub Security tab (CodeQuality/StandardFindings, CodeQuality/AIFindings,
CodeScanning, Dependabot/Vulnerabilities, Dependabot/Malware, SecretScanning,
Advisories, DependencyGraph), each split into open/ and closed/, as raw JSON
plus readable per-rule / per-file / per-severity text summaries. Each category
is fetched ONCE with no state filter and split locally, so open + closed are a
partition of one snapshot rather than two requests taken at different moments,
and a failing endpoint is recorded and skipped instead of aborting the run. The
live credential the secret-scanning API returns is deleted before anything is
written, and the reports directory is gitignored because these files describe
unfixed vulnerabilities. Needed because the VSCode Flatpak sandbox has neither
gh nor network access to api.github.com
Five mocha suites that sat in the tree looking like they ran, but were not, are
now actually run, and a guard keeps it that way. meteor.testModule points at
the two lib/tests/index.js files, which list their suites with explicit
imports, and five suites were never added to those lists (server/lib/tests had
49 suite files but 48 imports). Two are *bleed security regression suites —
checklistbleed (cross-board checklist / checklist-item move denial) and
proxybleed (header-login trusted-source and request-IP handling) — exactly the
kind of test whose silent absence lets a fixed vulnerability come back
unnoticed; the other three are filenameTruncation, alwaysShowCodeAsText and
renderLinksPlainText. All five are now imported (every symbol they use was
checked to still exist first), and tests/testsAreRegistered.test.cjs now fails
if any *.tests.js in either directory is not imported by its index — naming
the two *bleed suites explicitly so a future tidy-up cannot drop them again
quietly. This also deletes three files nothing loads:
client/components/main/responsiveFixes.css (imported by nothing, so never
bundled — meteor.mainModule disables eager loading) and the empty
models/backgrounds.js / models/backgrounds.server.js /
server/permissions/backgrounds.js tombstones, left over from board backgrounds
becoming board-level Attachments; the live server/boardBackgrounds.js and
server/publications/backgrounds.js are different files and are untouched
Pushing the English source to Transifex works again. tx push -s failed with
parse_error: Duplicate string key because en.i18n.json had two cpu-cores
keys — the existing "CPU Cores" (already on Transifex with human translations)
and a second "cores" added by the CPU-usage feature. A JavaScript JSON parser
silently keeps the last, so the app never noticed, but Transifex rejects
duplicates — and the collision also made cpu-cores resolve to "cores"
everywhere. The CPU-usage string needs a different value, so it moves to a new
unique key cpu-cores-suffix; the pre-existing cpu-cores is left untouched,
matching every other language file, so no human translation is disturbed
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following updates:
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/99e04ec98">List header card count is now scoped to the swimlane it is shown in, so a shared list no longer…</a> Thanks to xet7.</summary>List header card count is now scoped to the swimlane it is shown in, so a shared
list no longer shows the whole-list count over an empty second swimlane (the "5
Cards" header above a swimlane with no cards). A shared list — one whose
swimlaneId is empty, the pre-per-swimlane-lists layout — renders under every
swimlane, but cardsCount() counted by the list's OWN swimlaneId (''),
giving the whole-list total in every swimlane while the cards rendered below
were correctly swimlane-scoped. It now takes the container swimlane id (the same
../../_id the card body already uses) and counts that swimlane's cards,
matching what is rendered; list.cards() and the lazy (#6480) server count both
already scope by that id, including first-swimlane shared/orphaned surfacing
Re-attach mouse drag-scrolling (dragscroll) when a board's swimlanes/lists
render or change, so lower swimlanes stay reachable by dragging.
@wekanteam/dragscroll attaches its mouse handlers per element in reset(),
but the board's reset autorun only depended on the touch/permission reads — not
the swimlanes/lists — so when they rendered AFTER onRendered (adaptive lazy
card loading, the on-open data-repair adding a default swimlane, or a board
switch that reuses the template instance without re-firing onRendered) the new
.dragscroll containers never got mouse handlers: dragging no longer scrolled
and, with the vertical scrollbar hidden, only the top swimlane was reachable
(touch still worked via the document-delegated dragscrollTouch). The autorun
now reads the board view + swimlanes + lists, so reset() re-runs and
re-attaches when the rendered containers change
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/79410e73d067723a23d6f5b58f0ad34bdd509db4">Fix #6512 for OAuth / OIDC logins: after "Login with Google" (or any redirect-style social login)…</a> Thanks to xet7.</summary>Fix #6512 for OAuth / OIDC logins: after "Login with Google" (or any
redirect-style social login) the user is now sent to All Boards instead of being
left on the sign-in route showing only the language selector until a manual
reload. The earlier #6512 fix covered only the password/register flow
(onSubmitHook); OAuth logins do not go through it, so a client
Accounts.onLogin handler (fires for every login method after Meteor.userId()
is set) now navigates home when still on an auth route — guarded so an
auto-login on a deep-linked board URL is not redirected away
[Fix #6511 and #6514 (and the v10.30–10.31 Docker / Sandstorm / production
"board maintenance spinner", missing top user/settings bar, and
login-form-not-rendering reports): the whole client broke with Uncaught Error: ES Modules may not assign module.exports or exports.* followed by Error: No such template: swimlane / notifications / boardButtons.
imports/collectionHelpers.js — a side-effect shim imported FIRST in
client/main.js — ended with module.exports = {} while referencing the Meteor
pseudo-global Package bare; the client rspack build's ProvidePlugin rewrites
Package into an injected ESM import, marking the file an ES module, and an
ES module that assigns module.exports throws at evaluation time. That threw
before any template registered, so Blaze reported "No such template" for the
board and header templates, the board stayed on the spinner, the top bar was
missing and the board went blank grey after login. It reproduced on the official
root-domain boards.wekan.team and on plain Docker with no reverse proxy
(ROOT_URL=http://neptun:4001), so it was a global build bug, not a
reverse-proxy / sub-path issue. collectionHelpers (the first such file) was
fixed to use export {},
and then a transitive walk from client/main.js found and converted EVERY
remaining client-reachable CommonJS helper (~55 files under models/lib,
client/lib, imports/lib, imports/) from module.exports = { … } to
export { … }, so nothing in the client bundle assigns module.exports any
more — done in
two
parts Server-only helpers (the server/lib files Meteor never ships to the
client, and the fs-using file-storage helpers) stay CommonJS. Their plain-Node
.cjs unit tests now use await import(…js); the full tests/*.test.cjs suite
has no new failures. This is NOT an rspack version regression — the lockfile has
pinned @rspack/core ~1.7.x since v8.40, unchanged between the working v10.17
and the broken v10.27
Removed the unused jam:offline / jam:method / jam:pub-sub packages. Once
the client booted (after the ES-module fix above), jam:offline's startup sync
threw can't access property "remove", localCollections[name] is undefined — it
tried to reconcile a collection the client never registered, because WeKan uses
no jam:offline .keep() and no jam:method / jam:pub-sub APIs anywhere.
These were already removed once for the same reason and had been re-added; the
offline-warning UI uses Meteor.status(), so it is unaffected
Fixed six pre-existing plain-Node unit-test failures that surfaced while
verifying the ES-module conversion: five were stale source-guards (a slice
window too small, a regex matching a pattern quoted in a comment, a moved
template button, a widened valid-width range, a report-controls count that grew)
and one was a real cleanup race — streamHeaderToTemp unlinked its partial temp
file fire-and-forget while the write stream was still opening, so a caller could
see the file after the rejection; it now removes the file only after the stream
closes
and updates the following dependencies:
<details> <summary><a href="https://github.com/wekan/wekan/commit/eca452d6745e48c8d18917cf0d49fa2fa761c1dd">Pinned @rspack/core, @rspack/cli and @meteorjs/rspack to exact versions (removing the ^ caret…</a> Thanks to xet7.</summary>Pinned @rspack/core, @rspack/cli and @meteorjs/rspack to exact versions
(removing the ^ caret ranges) so a fresh npm install cannot silently drift
the bundler to a newer release that changes CommonJS/ES-module handling and
breaks the client build
and has the following developer-tooling fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d61eba49f1fc3fb992a0cc0e9860e983fbdfd833">build.sh can now stop a running dev server on a minimal Linux that has neither fuser (psmisc) nor…</a> Thanks to xet7.</summary>build.sh can now stop a running dev server on a minimal Linux that has neither
fuser (psmisc) nor lsof installed: free_tcp_port gained an ss fallback
that parses the owning pid from ss -ltnpH "sport = :PORT" and kills it.
Previously it did nothing on such a host — while port_in_use (which uses ss)
kept reporting the port busy — so the menu failed with "Port 3000, 8080 or 3001
is still in use … Stop it manually and retry" and the dev server could not
restart
and improves the translation workflow:
<details> <summary><a href="https://github.com/wekan/wekan/commit/4146c5374">Fix #6494 (Transifex human translations were overwritten and the fetch was omitted): removed the…</a> Thanks to s2ldan and xet7.</summary>Fix #6494 (Transifex human translations were overwritten and the fetch was
omitted): removed the external machine-translation service
(machine-translate.mjs, which called LibreTranslate/DeepL and needed a
WEKAN_MT_URL / WEKAN_MT_API_KEY). Remaining strings that are untranslated
everywhere are now translated DIRECTLY (no external service, key or password),
using each language's existing translations and general kanban terminology as
the reference, and applied with a new fill-translations.mjs that writes ONLY
into English-placeholder keys — so a fill can never overwrite a human
translation. In the releases/translations/ scripts a HUMAN translation is
ALWAYS PREFERRED and merged: the pull always fetches the newest Transifex
translations first, merge-translations.mjs keeps a real Transifex translation
and restores a committed human translation the pull reverted to English (leaving
English only where a string is untranslated everywhere), and filled strings stay
LOCAL — they are never pushed to Transifex as if human. This restores the
two-way process; verify-human-preference.mjs proves both directions with 10
cases
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/3c6d2b54e388f1c7d455f1f16d15bdd6369d7355">Fix #6512: after a successful login or register, go straight to the All Boards page instead of…</a> Thanks to akshat-goel and xet7.</summary>Fix #6512: after a successful login or register, go straight to the All Boards
page instead of landing back on the login layout that shows only the language
selector ("Vaihda kieltä") until a manual reload. onSubmitHook navigated home
immediately, but the home route's sign-in guard checks Meteor.userId()
non-reactively and bounced back to sign-in while the userId was still
propagating (a tick after login); the same guard also stranded a returning user
during auto-login from a stored token. onSubmitHook now waits reactively for
Meteor.userId() before navigating (with a timeout fallback), and the All
Boards guard no longer bounces while a login is in progress
(Meteor.loggingIn())
Fix #6515: the startup / board-open board repair no longer unbinds per-swimlane
lists. repairBoardsOnStartup ran the shared repair over every board and
cleared swimlaneId to null on EVERY list that had one — treating any
swimlane-bound list as #6484 corruption. But a list with a swimlaneId is a
legitimate PER-SWIMLANE list (rendered only in its own swimlane),
indistinguishable from a #6484-corrupted board-wide list at the data level, so
on upgrade to v10.26+ the routine silently unbound every per-swimlane list
(listsUnbound: 24 in the reporter's log) and all lists then rendered in every
swimlane; board open re-ran the repair, so even a manual restore was nulled
again. The automatic repair no longer clears any list's swimlaneId (the safe
card repairs stay); the #6484 code bug is already fixed, and an admin can still
deliberately un-bind a specific board. Affected boards keep their bindings now,
and a restore from a pre-upgrade backup will stick
Thanks to above GitHub users for their contributions and translators for their translations.
This release updates the following dependencies:
<details> <summary>The bundled FerretDB v1 fork now creates the OpLog ts index on the PostgreSQL, MySQL and SAP HANA… Thanks to xet7.</summary>The bundled FerretDB v1 fork now creates the OpLog ts index on the PostgreSQL,
MySQL and SAP HANA backends too (previously only SQLite), so an idle Meteor
OpLog tail resumes with an index range scan instead of re-scanning the whole
capped collection on every poll. Best-effort per backend, with a descriptive log
if a live engine rejects the index syntax (see the FerretDB fork changelog).
Ships with the bundled FerretDB update.
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/50a7c8fe59c900036460ca778e222baeef7dcca1">Fixed a stale #5623 "select all cards scoped to swimlane" unit test that failed after the…</a> Thanks to xet7.</summary>Fixed a stale #5623 "select all cards scoped to swimlane" unit test that
failed after the doubled-cards render fix: shared/orphaned cards (no swimlane,
or a deleted swimlane) now surface ONCE in the FIRST swimlane, so the helper
returns them only when the first swimlane's other-swimlane ids are passed; the
test still asserted the old behaviour. The test now covers both the
first-swimlane (own + orphaned) and non-first-swimlane (own only) contracts; no
runtime code change
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/2538e2434406010887bc3d827a15841b35ff1666">Snap: run the bundled FerretDB v1 against an EXTERNAL PostgreSQL / MySQL / SAP HANA server instead…</a> Thanks to xet7.</summary>Snap: run the bundled FerretDB v1 against an EXTERNAL PostgreSQL / MySQL / SAP
HANA server instead of the embedded SQLite backend. Two new snap settings —
snap set wekan wekan-ferretdb-handler=postgresql|mysql|hana and
wekan-ferretdb-url=… — point FerretDB at a database you run; the default stays
the zero-dependency sqlite backend, so existing installs are unchanged
Translations: machine translation now only fills the English strings that are
untranslated EVERYWHERE, so it can never overwrite a human translation. A new
releases/translations/machine-translate.mjs step (run after the per-key
Transifex merge, opt-in via WEKAN_MT=1 / WEKAN_MT_URL) translates exactly
the leftover English placeholders — which also fully fills any language that has
no translation at all — while a string a human already translated is never
selected. Machine output stays local and is not pushed to Transifex
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d69c83f949291837eb24409562ecd4b42c5d2033">snap run wekan.problems now loads the snap settings before its checks, so ROOT_URL / LDAP and the…</a> Thanks to akshat-goel and xet7.</summary>snap run wekan.problems now loads the snap settings before its checks, so
ROOT_URL / LDAP and the other login-page checks read the SAME configuration
WeKan runs with. Without this the command read an empty environment and reported
ROOT_URL is not set even after snap set wekan root-url=…, which was
confusing when diagnosing a blank login page or missing header controls (#6512, #6514)
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/5433dfbc5">Fix #6508 (follow-up): the board member popup now shows "Remap User" for an imported (placeholder)…</a> Thanks to AmigaAbattoir and xet7.</summary>Fix #6508 (follow-up): the board member popup now shows "Remap User" for an
imported (placeholder) member. The action is gated on the member's
authenticationMethod === 'imported', but the board publication shipped board
members' user docs without authenticationMethod, so the field was undefined on
the client and the action never appeared. It is now published with the board
members' user fields (already part of Users.safeFields), so the in-board Remap
→ map-to-existing-user flow works
Fix #6511: a board showed NO cards, with Error: Bad index in range.removeMember: 0 in the console. The card list is a Blaze {{#each}} over
a LIMITED, ordered reactive cursor sorted by { sort: 1 }, which has TIES —
when several cards share the same sort value (or due date, etc.) their order
is non-deterministic across observe/poll cycles, so Meteor's ordered diff
computes an out-of-range index and the #each throws, rendering no cards (and
cascading into the undefined.remove() teardown error). A unique _id
tiebreaker is now appended to the card-list sort (client cursor and server
window), making the order deterministic
Opening a card no longer takes ~40 seconds on FerretDB v1 (SQLite). A card's
attachments are looked up with the dotted key {'meta.cardId': ...}, which the
bundled FerretDB dropped from the WHERE (it skipped any dotted-path key), so it
full-scanned the whole attachments collection with a per-row decode on every
poll while a card was open — slow card content and high idle CPU. The FerretDB
v1 fork now pushes down a dotted-path equality/$in as the nested ->
expression that matches the existing meta.cardId index (see the FerretDB fork
changelog). Ships with the bundled FerretDB update.
Activity feed pushes down on FerretDB: the comment-only feed selected activities
with a top-level $and, which FerretDB v1 (SQLite) does not push down, so the
activities collection was full-scanned on every poll (slow board/card history on
a big board). The scope and activityType are now at the top level — exactly
equivalent, and both push down to the index
snap run wekan.problems: the login-page checks used affirmative titles, so a
FAILING check printed e.g. "[error] ROOT_URL is set: ROOT_URL is not set" —
confusing. Titles are now neutral subjects ("ROOT_URL", "Migration or repair")
so each line reads correctly whether the check passes or fails
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/2f07b2933">Fix #6508: two crashes in the import "map users later" workflow. (A) In the import "map members"…</a> Thanks to AmigaAbattoir and xet7.</summary>Fix #6508: two crashes in the import "map users later" workflow. (A) In the
import "map members" step, clicking a searched user threw Cannot read properties of undefined (reading '_id') — the handler read
Template.currentData()._id, which is undefined when the click lands on a child
of the row (the avatar or the name span); it now reads the user id from the
row's data-id. (B) In Admin Panel / People, saving an imported (placeholder)
user as an active person with an email threw Cannot read properties of undefined (reading '0') because the submit handler read user.emails[0] and an
imported placeholder (and some SSO) user has no emails array; the primary
email is now read defensively
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/72768e05f">Fix #6507: on a fresh install the Admin panel would not open, the title bar sometimes failed to…</a> Thanks to AmigaAbattoir and xet7.</summary>Fix #6507: on a fresh install the Admin panel would not open, the title bar
sometimes failed to load or showed "You are not authorized to view this page",
and the browser console looped Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'remove').
ReactiveCacheClient.getCurrentSetting() / getCurrentUser() guarded their
DataCache with !this.__x || !this.__x.get() — so while the Settings document
did not exist yet (a fresh install) or Meteor.user() was briefly null (right
after the first admin logs in), a brand-new DataCache and Tracker computation
were rebuilt on EVERY reactive read, churning computations app-wide and racing
Blaze's view teardown into an infinite reactive loop + removed-DomRange crash.
Every other getter creates its DataCache once; these two now do the same,
returning a stable value until the setting / user exists
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/a51afa5a1">Fix boards that loaded their lists/columns but never their CARDS on FerretDB v1 (SQLite) (10.22…</a> Thanks to xet7.</summary>Fix boards that loaded their lists/columns but never their CARDS on FerretDB v1
(SQLite) (10.22: pages took minutes, columns appeared, cards never did). Every
board card query used { boardId: { $in: [board._id, board.subtasksDefaultBoardId] } }, and subtasksDefaultBoardId defaults to
null; on FerretDB a $in containing null does not push down at all, so the
WHERE was dropped and the whole cards collection was full-scanned and decoded
on every poll-and-diff cycle (across ~7 card cursors per board open), while
lists/swimlanes used a plain boardId equality that stayed index-backed. A new
models/lib/boardCardScope.js builds the scope WITHOUT null (a plain equality —
an index seek — when only the board is in scope, else an all-string $in),
applied to every card-scope site in server/publications/boards.js,
cardsWindow.js and cards.js; the lazy card-window selector is also flattened
out of a top-level $and (which FerretDB does not push down) so
boardId/archived push down too. The FerretDB v1 fork is hardened separately
so a mixed $in (including null) uses the index instead of full-scanning
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/0e0391f6c">Fix #6506: board import shows the "map members" step again, and unmapped members become virtual…</a> Thanks to AmigaAbattoir and xet7.</summary>Fix #6506: board import shows the "map members" step again, and unmapped members
become virtual users instead of the importer. A regression in v10.12 dropped the
importMapMembers step from the import wizard and sent an empty member mapping,
so the map-users screen never appeared and every imported card / member /
comment was assigned to the importing user even when that person already existed
in WeKan. The (auto-suggesting) map-members step is restored and stays OPTIONAL
(a Skip button and the textarea "import without mapping" button bypass it);
finishImport builds the mapping from the members the user mapped. Members left
unmapped — or skipped, including the Trello API all-boards background import
which has no interactive step — are now brought in as inert virtual
(placeholder) users (mapped to an existing user by username where one matches),
so board membership and authorship keep the original person instead of
collapsing onto the importer. Covers the single-board Trello JSON import, the
Trello API import, and the WeKan JSON import
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUES:
<details> <summary><a href="https://wekan.fi/hall-of-fame/exportbleed/">ExportBleed</a>.</summary>** ExportBleed : stored XSS in HTML board exports through a card-title second
parse** (CWE-79 Cross-site Scripting; GitHub Security Advisory
GHSA-8r5p-4q9j-f5jx, severity High; client/lib/exportHTML.js). A board member
could store entity-encoded markup in a card title (e.g. <img src=x onerror=...>). It stays inert on the live board — Blaze escapes it and the
+viewer sanitizer neutralizes handlers, keeping the entity payload as text —
but the exported index.html embedded a card-click handler that read the card
title/body via .textContent (which DECODES HTML entities) and then
concatenated those values into content.innerHTML. That SECOND parse revived
the tag and ran it when a recipient clicked the card in the export, disclosing
all data in that document (including cards added AFTER the attacker's board
membership was removed)
textContent, never innerHTML, so they are inserted as
inert text (commit).** SpliceBleed follow-up: incomplete multi-character sanitization re-flagged in
the filename markup strip** (GitHub CodeQL code scanning alert #426, rule
js/incomplete-multi-character-sanitization, CWE-116 Improper Encoding or
Escaping of Output; imports/lib/fileNameDisplay.js). The SpliceBleed fix
(#425) looped a CHAIN of six replaces to a fixpoint — runtime-safe, but CodeQL
(a local check) could not attribute the fixpoint to the individual <[^>]*>?
tag replace, so it kept flagging it ("this string may still contain <script")
client/lib/importDependencies.js stripHtml() (which cleared the sibling
alert #421): remove template/PI/CDATA tokens (looped), then strip HTML/XML
tags with a SINGLE replace(/<[^>]*>/g, '') looped to a fixed point, then
drop any stray angle bracket so even an unclosed tag (a trailing <script)
cannot survive. Behaviour is unchanged, and Blaze {{ }} still HTML-escapes
every rendered filename, so this stays defence-in-depth
(commit).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/5a1c9a61e">Admin Panel / Problems "Status" overview + snap run wekan.problems</a>. Thanks to xet7.</summary>Admin Panel / Problems "Status" overview + snap run wekan.problems. Problems /
Summary now shows everything in progress (database text migration, board
data-repair, recovery, cron migration, attachment migration), detected problems
(broken cards), and a login-page checklist of the causes that make the login
page show "Must be logged in" or sit on the logo + "Loading, please wait."
spinner (a migration/repair running, wrong/unset ROOT_URL, LDAP, Sandstorm). The
same overview is available WITHOUT the Admin Panel from the server command line
— snap run wekan.problems (with migrations / login / broken-cards /
cpu sub-commands) — so an admin with only server access can read it.
Migration/repair progress is persisted to the database so a separate process can
see it. Docs: docs/Features/Admin-Panel/Problems/
Shared board data-repairs run on board open, on server startup, AND during the MongoDB <-> FerretDB v1 (SQLite) text migration — shown in one blue, Product-name-branded progress dashboard. Beyond the #6484 list-unbind, the shared set now also fixes cards with no swimlane and orphaned cards whose swimlane was deleted (invisible cards), reassigning them to the board's first swimlane. The repairs run SERVER-SIDE and do not depend on a user watching a progress page: a version-gated background pass runs once at startup, board open repairs per board, and the database migration repairs the live data before copying so the migrated copy is clean too. The progress dashboard is mounted app-wide (board + Admin Panel), styled in WeKan blue (#2980b9), and branded with the configured Product name
</details>and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/fae1ccd17">FerretDB high CPU: run FerretDB standalone (no OpLog) by default</a>. Thanks to xet7.</summary>FerretDB high CPU: run FerretDB standalone (no OpLog) by default. Several sites
reported the ferretdb process pegging ~2 CPU cores for a long time while the
WeKan node process sat idle — pages taking 5+ minutes, All Boards counts stuck
at 0, raw i18n keys, and the login page showing "Must be logged in" or a stuck
"Loading, please wait." spinner. That idle-node / pegged-ferretdb split is the
signature of an OpLog tail: Meteor holds a tailable cursor on local.oplog.rs
and FerretDB re-scans it server-side. wekan-control already defaulted to
polling-only, but ferretdb-control passed --repl-set-name unconditionally,
so FerretDB always maintained the capped OpLog even when WeKan would not tail
it. It now runs STANDALONE in the default polling mode and enables the replica
set / OpLog only when wekan-ferretdb-oplog=true. Opting into OpLog is also
cheaper now on the FerretDB v1 fork side (the {ts: {$gt}} tail filter is
pushed down to SQL and the OpLog Timestamp is indexed). See
docs/Features/Admin-Panel/Problems/Migrations.md
Fix #6504: a rule that moves a card to another board failed with
"newBoard.getNextCardNumber is not a function". On the server
ReactiveCache.getBoard() is async; the cross-board branch of Cards.move()
used it without await, so the destination board was a Promise (its methods
missing) and label carry-over from the source board was silently dropped. Both
getBoard() calls are now awaited (same fix as PR #6505)
Swimlanes view: a shared card no longer appears in every swimlane ("doubled
cards"). A card with no swimlane (swimlaneId null / '' — a shared /
pre-migration card) was surfaced in EVERY swimlane, so on a board with several
swimlanes the same card rendered once per swimlane (and the extra DOM
contributed to the search hang / can't-scroll on big boards). A non-first
swimlane now shows only its own cards; shared and orphaned cards surface ONCE,
in the first swimlane, so a card is never shown in more than one swimlane.
Board-wide label filtering (#6441) is preserved
Repair for boards where a list "disappeared" from swimlanes (the #6484
corruption). The #6484 bug — nudging a board-wide list in swimlanes view bound
it to one swimlane and moved its cards there, so it vanished from the other
swimlanes — was fixed in code, but boards already hit keep the wrongly-set
list.swimlaneId. A new board-admin method repairBoardWideLists(boardId)
clears those lists back to board-wide (rendered under every swimlane); it is
idempotent and scoped to the one board. (Cards the bug moved into one swimlane
are not restored — recover those from a backup.)
Opening a board now auto-detects the #6484 corruption and repairs it. On board
open, a cheap boardListRepairNeeded check reports whether any list is wrongly
bound to a swimlane; if so and the viewer is a board admin,
repairBoardWideLists runs automatically while a progress modal is shown, then
completes. The check runs once per board, fire-and-forget, and never blocks
rendering. The migration-progress modal is now branded with the configured
Product name (Admin Panel / Settings / Layout / Product name), falling back to
WeKan when none is set, so every migration reusing the modal shows the admin's
Product name instead of the hard-coded brand
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/191f82739">Card links keep working after the card is moved to another board</a>. Thanks to Robdebert and xet7.</summary>Card links keep working after the card is moved to another board. A card URL is
/b/:boardId/:slug/:cardId; after moving the card the link opened the old board
and found nothing. The card route now resolves the card's CURRENT board via the
permission-checked card publication and, if it differs, redirects there — so a
link stays valid across board moves as long as the viewer has access to the
destination board (like Trello). If the user cannot see the card's new board,
the URL's board is kept; a redirect loop / stale navigation is guarded against
(#4758)
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/27201f642">Copy-code-block button in the card viewer is now reliable</a>. Thanks to C0rn3j and xet7.</summary>Copy-code-block button in the card viewer is now reliable. It previously ran
once with a global query, so it decorated only whichever code blocks were on the
page at that instant ("works sporadically"), was lost after editing, and copied
the HTML-escaped inner markup (wrong content). It is now added per viewer in
Template.viewer.onRendered (so every description / checklist item / comment
gets it and it is restored on re-render), de-duplicated, copies the RAW code
text via the async Clipboard API (with a fallback), and the button title is
translated (#5149)
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/8f5300053">Public read-only boards: list dragging is disabled for anonymous / not-logged-in users</a>. Thanks to GenericUK and xet7.</summary>Public read-only boards: list dragging is disabled for anonymous / not-logged-in
users. On a public board opened while logged out, lists could still be dragged
(the reorder was denied server-side and snapped back, but the drag itself made
mobile left/right scrolling almost impossible). Already fixed — every list
sortable is disabled: !Utils.canModifyBoard(), and canModifyBoard() requires
an active, non-read-only board member, so an anonymous user can never drag
lists; this release adds a unit guard so it cannot regress (#1158)
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/14dd48bb6">Moving a card between boards via the REST API now works (and is unit-tested)</a>. Thanks to rptl and xet7.</summary>Moving a card between boards via the REST API now works (and is unit-tested).
Editing only the raw boardId/listId left the card half-moved so opening it
bounced back to the old board; the supported way is the edit-card API's
newBoardId + newSwimlaneId + newListId, which the handler recognises as a
full board move — it validates the destination list and swimlane belong to the
destination board and re-homes the card through cardMove() (updating board,
swimlane and list together). A partial set is intentionally NOT treated as a
board move, so a card is never half-moved (#3037)
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/5234d09d1">Admin Panel: the per-org/team "Propagate Members To Boards" flag now actually propagates…</a> Thanks to xet7.</summary>Admin Panel: the per-org/team "Propagate Members To Boards" flag now actually
propagates. orgPropagateMembersToBoards / teamPropagateMembersToBoards were
stored and shown in the Admin Panel, but the method that acts on them had no
caller anywhere — dead code — so turning the flag on did nothing. Turning the
flag on now adds that org/team's members to the boards that list it (strictly
add-only; existing board members are never removed or modified; template boards
are skipped), via a pure, unit-tested models/lib/propagateMembers.js (#4737, #5850)
A user added to a team AFTER it was assigned to a board now gains membership on
that board. Assigning a team to a board adds the team's current users to the
board's members, but a user added to the team afterwards only got read
visibility (via the publications) while every authority gate —
board.hasMember(), the board permission checks, protected-attachment access,
and export visibility — looks at board.members, so the late-joining team
member had strictly less authority than the team's original members. Already
fixed in current WeKan (addUserToTeamBoards + the pure
models/lib/teamBoardMemberSync.js, called from both the admin People-panel
editUser path and the create-user-with-teams path); this release adds source
guards so the wiring cannot silently regress (#4593)
Add-card: pressing Tab no longer loses the typed card text. The add-card form's Tab handler is a deliberate power feature — Tab jumps to the next column's add-card form so cards can be added across columns quickly — but it opened that next form while abandoning whatever was typed in the current one, so pressing Tab mid-entry silently lost the card text (the card appeared to "move to the next column" with no content). It now submits the current card first (only when the textarea has non-whitespace content, so Tab in an empty box does not create a blank card), then jumps to the next column's form — preserving both the content and the column-jump feature (#1195)
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c0bbacaf9">Due-date reminder mail: honour a midnight reminder hour and notify card assignees</a>. Thanks to SirConfigMgr and xet7.</summary>Due-date reminder mail: honour a midnight reminder hour and notify card
assignees. Two bugs kept due-date reminders from reaching people: the reminder
hour was parsed as parseInt(NOTIFY_DUE_AT_HOUR_OF_DAY) || 8, so a configured
hour of 0 (midnight) silently became 8 (0 is falsy), and a due-card activity
only notified the card's creator and members — a user assigned the card but not
a member was never told about it. Midnight is now honoured (and out-of-range
hours rejected), and assignees are included in the notification participants
(still gated by each user's board tracking level). The built-in reminder still
requires NOTIFY_DUE_DAYS_BEFORE_AND_AFTER to be set (#3192)
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/b15d89dd0">FerretDB now defaults to polling-only on all platforms, fixing high FerretDB CPU</a>. Thanks to xet7 and the FerretDB high-CPU reporters.</summary>FerretDB now defaults to polling-only on all platforms, fixing high FerretDB
CPU. FerretDB v1 can tail an OpLog for real-time updates, but on its SQLite
backend Meteor's tailable+awaitData OpLog tail keeps FerretDB CPU pinned
(reporters saw ~190–390% even when idle; a 2-core VPS maxed out and WeKan got
stuck on the loading spinner), and a struggling tail also shows up in the log as
oplog catching up took too long, stalling board and login loading. Even with
the FerretDB-side mitigations the peg persisted on real deployments, while
polling-only reliably drops CPU to ~10%. So polling is now the default on every
launcher (snap, bundle, Docker/compose, Windows, Sandstorm);
WEKAN_FERRETDB_OPLOG defaults to false. The OpLog remains available as
opt-in (WEKAN_FERRETDB_OPLOG=true, or on snap snap set wekan wekan-ferretdb-oplog=true). The calmer polling throttle and SQLite pragma
tuning keep poll-and-diff cheap (#6503)
i18n never gets stuck showing raw translation keys if a dynamic language import
fails. TAPi18n.init() awaited a dynamic import('./data/en.i18n.json') and
only marked i18n ready afterwards, so if that lazy chunk hung or failed — e.g. a
stale snap client bundle after a refresh — the whole UI showed raw keys (like
changeLanguagePopup-title) forever with no recovery. The default English is
now statically bundled and registered first (the UI is always readable), and the
dynamic load is bounded by a timeout so readiness is reached in every path.
Note: this hardens the raw-key symptom; the dead OIDC/Register buttons in that
report point to a stale dynamic-import bundle cache on the snap (a hard reload /
clean snap rebuild is the confirming test), not a change in WeKan's login code
(which is unchanged between 10.11 and 10.13) — and the FerretDB polling-only
default above also removes the oplog catching up took too long stalls that
make the login page slow to become interactive (#6503)
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/e1ebe978b">Rules: the "remove all members" action now actually removes the card's assignees</a>. Thanks to sfahrenholz, mweiss237 and xet7.</summary>Rules: the "remove all members" action now actually removes the card's
assignees. The rule member actions act on a card's assignees
(addMember→assignMember, removeMember→unassignMember), but the "remove
every member" wildcard branch iterated card.members — a different collection —
so it removed nobody a rule had assigned. It now iterates the card's assignees
(#2674)
Cards added in List view no longer land in an archived swimlane. When a board's
first/default swimlane was archived or deleted, adding a card in List view — or
dragging a card to another list — assigned it to that archived/deleted swimlane,
so the card was invisible in Swimlane view (visible only in List view until the
swimlane was restored). Board.getDefaultSwimline() now prefers a NON-archived
swimlane via a pure, unit-tested pickDefaultSwimlane() helper (falling back to
the first only when every swimlane is archived); cards already orphaned by a
swimlane deletion keep surfacing in the first swimlane via the #6443 fallback
(#1971, #1959)
Case-insensitive @ member autocomplete in the card title. The add-card-title
@ member mention matched with user.username.indexOf(term) === 0 —
case-sensitive, prefix-only, username-only, and it threw when the user lookup
returned null. So @ann did not suggest Anna, @Anna did not match
username anna, and a full-name search never matched. Both the add-card and the
card description/comment editor @ mentions now share a pure, unit-tested
models/lib/memberAutocomplete.js that matches the term case-insensitively as a
substring of the username or full name, and is null-safe (#5116 follow-up)
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://wekan.fi/hall-of-fame/splicebleed/">SpliceBleed</a>.</summary>** SpliceBleed : incomplete multi-character sanitization when stripping exploit
markup from a filename** (GitHub CodeQL code scanning alert #425, rule
js/incomplete-multi-character-sanitization, CWE-116 Improper Encoding or
Escaping of Output; imports/lib/fileNameDisplay.js). stripExploitPatterns()
removed HTML/script/XML/template markup from a shown filename in a single pass —
so an input crafted with nested or interleaved fragments (for example
<scr<x>ipt> or <scr{{y}}ipt>) could have its inner part removed and the two
surviving outer fragments SPLICED together into a fresh <script> token that
the single pass no longer re-examined (CodeQL: "this string may still contain
<script")
{{ }} already
HTML-escapes every rendered filename, so this is defence-in-depth on the
displayed text rather than a live XSS, but the incomplete single-pass strip
was genuinely wrong.** IdentityBleed : identity string replacement (a no-op replace)** (GitHub
CodeQL code scanning alert #424, rule js/identity-replacement, CWE-116
Improper Encoding or Escaping of Output; tests/securityLog.test.cjs). A test
built a menu-id regex with id.replace('report-', 'report-') — replacing a
substring with itself, a no-op that CodeQL flags because it is almost always a
mistake for a real transformation
replace and matching 'js-' + id directly.
Test-only code with no runtime exposure, but the no-op was removed for
correctness.and resolves the following GitHub Dependabot alerts in the rspack
build-toolchain dev dependencies, without touching the rspack major version so
builds are unaffected (both packages enter only via @rspack/dev-server — the
rspack serve dev server — and are not used by meteor build):
webpack-dev-server 5.2.5 → 5.2.6 (npm overrides): fixes
CVE-2026-14631 (DoS
via a malformed Host/Origin header, alert #113) and CVE-2026-14620 (CSRF via the
internal /webpack-dev-server/open-editor and /invalidate endpoints, alert #114). Patch release in the same major line
http-proxy-middleware 2.0.9 → 2.0.10 (npm overrides): fixes CVE-2026-55602
(a host+path router key matched by unanchored substring, so a crafted Host
header could route to an unintended backend, alert #107). Patch release in the
same major line
The lock resolves with only those two entries changed; @rspack/cli and
@rspack/core stay at 1.7.11. elliptic (alert #55, CVE-2025-14505) is left
pinned because no fixed version is published upstream (latest is still 6.6.1,
dev-only, Low). The @rspack/cli 1→2 (PR #6497) and @babel/parser 7→8 (PR #6484, a runtime dependency) major bumps are deferred pending build/ runtime
testing.
and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/728cd929c">Case-insensitive # label autocomplete in the card title</a>. Thanks to rjl9 and xet7.</summary>Case-insensitive # label autocomplete in the card title. Typing #test in the
add-card / edit-card-title field did not suggest a label named Testing — the
label search matched with a case-sensitive label.name.indexOf(term), so only
#Test matched. The match now lowercases both the typed term and the label
name/color (matching the @ member autocomplete), extracted into a pure,
unit-tested models/lib/labelAutocomplete.js (#5116)
Thanks to above GitHub users for their contributions and translators for their translations.
This release updates dependencies:
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/930da57fd">Fix: Docker fresh docker compose up showed "Connection reset by peer" and never loaded (#6500)</a>. Thanks to youhajjioui and xet7.</summary>Fix: Docker fresh docker compose up showed "Connection reset by peer" and
never loaded (#6500). The compose ferretdb service downloads its binary on first
run, so it is not listening for a while after its container starts, but WeKan's
depends_on used condition: service_started (waits only for the container to
start, not for the database to accept connections) — so WeKan started against a
not-yet-ready database and failed. A healthcheck (a dependency-free bash
/dev/tcp probe of 127.0.0.1:27017, with a start_period that covers a slow
first download) is added to the ferretdb service and WeKan now waits for
condition: service_healthy, matching the intent already documented in the
compose file
Fix: the admin Files report showed "No results" on FerretDB (every attachment
was hidden). Reproduced against a real FerretDB with the Mongo driver: older
FerretDB v1 builds reject {members:{$elemMatch:{userId,isActive:true}}} with
"(BadValue) unknown operator: userId", and accessibleCardIds used exactly that
query while the publication's catch swallowed the error, so the report silently
returned nothing. It now matches board membership by the dotted path
{'members.userId': userId} (which works on every FerretDB build) and confirms
the user's own member entry is active in JS, preserving the exact $elemMatch
semantics; the publication also no longer swallows query errors silently
Import always creates virtual users; map to real users later from the board
sidebar; imports can no longer hang. Board import no longer asks for member
mapping up front — every imported member is brought in as a virtual
(placeholder) user carrying its avatar, username and full name, added to the
board inactive with no permissions, and import runs immediately (single step,
nothing to get stuck on). Mapping a virtual member to an existing user is a
deliberate, later action by a board admin from the sidebar member-avatar popup
("Map to existing user"), and cannot be used to gain privileges: it only maps a
virtual member onto an existing ACTIVE, REAL member of the SAME board and never
changes that member's role (no new membership, nothing escalated); it reassigns
that virtual member's cards/comments/activity on the board to the target and
removes the placeholder. Automatic hang mitigation bounds every import so it can
never spin forever — a client watchdog surfaces a timeout (and clears the
spinner) and the server bounds the import with the same deadline
(WEKAN_IMPORT_TIMEOUT_MS, default 2 min) via a new generic withDeadline
wrapper. Covered by unit tests (no-escalation + auth matrix, the deadline
wrapper) and wiring tests
Fix: board/JSON import "Assign members" — typed suggestions appeared but could
not be selected, so a migrated board's members could not be mapped to existing
WeKan users (neither clicking nor Enter did anything). The "Select member" popup
mapped Template.currentData().__originalId, a field never set on the search
results (they are plain user documents with _id), so it mapped undefined and
silently did nothing. It now maps the clicked result by its _id, and Enter
selects the first (highlighted) result so a name can be assigned by keyboard.
Member mapping stays optional — the step-1 "import without mapping" and the
in-step "skip mapping" buttons import immediately (unmapped members default to
the current user). Guarded by a test
Fix: cards not opening — clicking a card did nothing (#6493). The global
cleanFilename / downloadFilename template helpers (which show every filename
safely) are registered by client/components/main/safeFilename.js, but that
module was never imported on the client, so the helpers were never registered.
Every template using {{cleanFilename name}} — card attachment thumbnails and
the admin Files report — then threw "No such function: cleanFilename" during
render, and on a card with attachments that aborted the card's Tracker recompute
so the card would not open. client/features/main.js now imports
safeFilename.js so the helpers are registered at startup; guarded by a test
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/a4c57cf77570ec4a0d7ae1a8d099e1cc6fef1751">Fix: FerretDB high CPU that continued even with no clients connected (#6498)</a>. Thanks to Alishara, bluetopaz1204, mueschel and xet7.</summary>Fix: FerretDB high CPU that continued even with no clients connected (#6498).
Meteor tails the OpLog (local.oplog.rs) with a tailable+awaitData cursor that
starts at boot and runs with no clients; on FerretDB v1 that tail was re-running
its query every 10 ms (~100 scans/second, forever), pinning CPU. This is a
distinct cause from the OpLog bloat capped in the earlier fix — it is the tail's
poll rate, not the OpLog size. The bundled FerretDB fork now polls awaitData at
a calmer 500 ms (still within the 1 s await budget, so reactivity latency stays
low; tunable with FERRETDB_TAILABLE_AWAIT_POLL_MS), cutting idle tail load
~50x — see the FerretDB CHANGELOG. FerretDB OpLog stays ON by default (a
reporter confirmed the earlier CPU fix worked), so this only makes the default
mode cheap at idle. Additionally, for anyone who explicitly turns the OpLog OFF
(WEKAN_FERRETDB_OPLOG=false), every launcher (snap, bundled release, Docker,
Sandstorm) now also clears MONGO_OPLOG_URL in that polling-only branch,
because merely having it set makes Meteor tail the OpLog regardless of the
reactivity order — so polling-only is now truly tail-free. Covered by tests
Fix: on Sandstorm, the browser error that a page "can not be displayed embedded
in another page" after a grain's first-launch data migration. The grain launcher
migrates the old MongoDB data to FerretDB v1 (SQLite) before starting WeKan, and
while nothing was listening on the grain's app port (during migration and the
handoff to WeKan) sandstorm-http-bridge returned connection-refused, so the
browser showed the framed-grain error and the user had to close and reopen the
grain. The launcher now runs a tiny child-process bridge that answers the app
port with an auto-refreshing "please wait" page across the whole migration and
handoff — yielding the port to the migration importer's own progress dashboard
and releasing it just before WeKan binds it — so the grain stays framed until
WeKan is up. Best-effort (guarded, killed on grain exit) so it never breaks
grain startup; covered by wiring/ordering tests
The recovery maintenance page now works the same on ALL FerretDB v1 platforms
(#6492). Besides the in-app spinner (which shows once Meteor serves the client),
every launch path now also serves a tiny standalone "recovering your data" page
(HTTP 503) on the web port for the brief window while a just-restored FerretDB
comes back up and before the app is up — so users never hit a bare connection
error: the snap reuses wekan-maintenance-page.mjs with a recovery wording, and
the bundled release/Docker paths serve the portable recovery-bridge.mjs. The
bridge is time-bounded (WEKAN_RECOVERY_BRIDGE_SECONDS, default 20s) so it can
never block WeKan from starting, hands straight over to the in-app spinner, and
is skipped if its page or the marker is absent. Only the server clears the
RECOVERY_IN_PROGRESS marker (after a real health probe), so the spinner
behaves identically everywhere; tests enforce the bridge stays bounded and no
launch script deletes the marker
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/54b1e9cd17b0ff15f193da4a3db9178b7eb0483f">Fix: high FerretDB CPU (300%+, even idle) from a bloated or corrupt simulated OpLog (#6492)</a>. Thanks to bluetopaz1204, mueschel and xet7.</summary>Fix: high FerretDB CPU (300%+, even idle) from a bloated or corrupt simulated
OpLog (#6492). FerretDB v1's SQLite OpLog (local.oplog.rs, in the local
database = local.sqlite) is not reliably capped, so it grows/corrupts over
time and Meteor busy-polls it — a reporter confirmed that deleting
local.sqlite* drops CPU straight back to ~10%. The local database is
transient system data (the OpLog + replica-set metadata), NOT user data
(boards/cards/attachments live in wekan.sqlite), so every FerretDB launch path
(snap, bundled release, Docker) now resets ONLY the local database on start —
FerretDB recreates a fresh, correctly capped OpLog — so a corrupt/bloated OpLog
can never persist across a restart. Guarded by WEKAN_FERRETDB_RESET_OPLOG
(default on; set false to keep the OpLog), and a test enforces that the reset
never touches wekan.sqlite
Safety measures against SQLite corruption/bloat (#6492). (1) Before FerretDB
opens its files, every launch path keeps a rotating backup of the text-data
database (wekan.sqlite*) in a backup/ subfolder of the same data dir, so a
known copy is ready to restore if the live database is ever detected corrupt —
it only ever COPIES the live database (never moves/deletes it), keeps the
previous generation under backup/prev, and does not copy attachments/avatars
(they live on the filesystem); disable with WEKAN_SQLITE_BACKUP=false. (2) The
bundled FerretDB fork now automatically DETECTS corruption (a fast quick_check
on every database open, logged prominently) and automatically REPAIRS bloat
(VACUUM when a file is large and its free pages dominate), and caps the OpLog
small (16 MiB) — see the FerretDB CHANGELOG. Both WeKan safety scripts are
covered by tests that enforce they never destroy the live text data
Automatic recovery/remediation for the SQLite text data, with an Admin Panel /
Problems / Recovery report (#6492). A pure, unit-tested decision helper
(decideRecovery) picks the least-invasive recovery when the database is KNOWN
corrupt — latest good backup → previous backup → re-migrate text data from
MongoDB → (else) manual — and never acts on a healthy/unknown database. On
request (WEKAN_FORCE_RESTORE env or a RESTORE_REQUESTED marker) the startup
scripts restore a known-good backup INTO the live database before FerretDB opens
it (backups are never deleted, the main wekan.sqlite is only overwritten,
attachments/avatars on the filesystem are untouched). Every safety action is
recorded and shown newest-first in the new admin-only Recovery report (readies
up front so it can't hang), and admins can record a manual event. Covered by
unit tests (decision logic, JSONL parser, search selector) and wiring/negative
tests (report is admin-gated; restore never destroys the live data or a backup);
documented in docs/Features/Admin-Panel/Problems/Recovery.md
While a data recovery is in progress, WeKan now shows a full-page maintenance spinner instead of errors or half-loaded data (#6492). A public status document (everyone, including logged-out users on the sign-in page, sees it) drives a full-screen overlay in both the app and sign-in layouts. The startup scripts mark recovery in progress when they restore/re-migrate; the server keeps the spinner up until it verifies the database serves reads, then clears it — or, if it still cannot read, keeps the spinner and records that manual recovery is required. Admins can also toggle maintenance for a server-initiated re-migration. The status publication is public while the toggle method is admin-only, enforced by tests
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/576f44ff592cdae7d3bae65f0486a33d248f1980">Fix: on the phone All Boards layout, board titles were cut off the right edge and workspace names…</a> Thanks to xet7.</summary>Fix: on the phone All Boards layout, board titles were cut off the right edge and workspace names were hard-cut in the narrow menu. The board column now shrinks to its track (min-width:0) so the tiles and titles fit on screen, the mobile tile's drag handle is smaller so more of the title shows, and workspace names truncate with an ellipsis instead of mid-word
</details>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/c68dc3fb29618685214297484ca7a51084451b23">Fix: on phone-sized touch screens the All Boards page put the board icons BELOW the left menu and…</a> Thanks to xet7.</summary>Fix: on phone-sized touch screens the All Boards page put the board icons BELOW the left menu and search bar (a single stacked column), and because dragscroll does not work over the menu / search area you could not drag-scroll down to the boards below the fold. The board icons now stay in their own column on the RIGHT (narrow left menu, boards right) in both mobile and desktop mode, and BOTH columns are their own drag-scrollable areas — a tall left menu and the boards can each be scrolled down to. In RTL languages the layout mirrors automatically (menu on the right, boards on the left) via the direction-aware grid and logical borders
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/00ad2c5a688d500e3a22f43b0625498234ef0f6c">Fix: the Files admin report (Admin Panel / Reports / Files) was stuck on the loading spinner and…</a> Thanks to xet7.</summary>Fix: the Files admin report (Admin Panel / Reports / Files) was stuck on the
loading spinner and never listed its files. An await in the attachmentsList
publication never resolved (a ReactiveCache read / the ostrio FilesCollection
cursor's old-CFS backward-compatibility fallback), so this.ready() never ran
and the subscription never became ready — and the report template only renders
once ready. The publication now signals readiness UP FRONT (then streams the
page rows, which appear reactively), and computes its data by querying the
Boards / Cards / Attachments.collection collections directly with fetchAsync
instead of through ReactiveCache, so it always resolves. The client helpers are
also defensive so a data error can never blank the report
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/88642c3bd439c816e783f62281b651d6839a04a4">Verified that list widths on public boards (#5659) are fixed: uncustomized lists render the one…</a> Thanks to NadavTasher and xet7.</summary>Verified that list widths on public boards (#5659) are fixed: uncustomized lists
render the one shared default width for logged-out public-board visitors and
members alike, board-wide widths apply to everyone, and anonymous viewers
customize via localStorage. The listWidthDefaults.test.cjs regression test
existed but was never wired into npm run test:unit:node, so it did not run; it
is now wired in so the fix stays verified
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/616304e2d0f043afbd03cd5d2af52ca7eeb79361">Fix: the Statistics board view never rendered. statsView.jade / .js / .css were not imported…</a> Thanks to xet7.</summary>Fix: the Statistics board view never rendered. statsView.jade / .js / .css
were not imported anywhere in the client build, so the statsView Blaze
template was never registered — switching a board to the Statistics view left
the board canvas empty (the header still showed "Statistics", which only
reflects the saved board view). The three files are now imported alongside the
other board views, so the view renders its board counts and time summary
Fix: on phones the All Boards list grew to the full height of all its boards
instead of staying a bounded, scrollable region, so boards below the fold were
clipped by the surrounding overflow:hidden and could not be reached. Two
leftover .board-list.mobile-view rules (min-height: 100vh and a phantom
::after) re-introduced the exact bug an earlier fix removed elsewhere;
removing them lets the bounded height take effect and the two-column list
scrolls
Files admin report (Admin Panel / Reports / Files): read the underlying reactive
Attachments.collection instead of the ostrio FilesCollection wrapper, whose
FilesCursor does not reliably re-run in a Blaze helper
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/0376665882267d0e77116a1182010a4f35882a3f">Fix: force changeStreams out of the reactivity order for FerretDB on all platforms, not only the…</a> Thanks to uusijani, mueschel and xet7.</summary>Fix: force changeStreams out of the reactivity order for FerretDB on all
platforms, not only the snap. FerretDB v1 does not implement MongoDB change
streams, so a $changeStream aggregate returns "not implemented" and Meteor
busy-loops retrying it (high FerretDB CPU, cards not opening). The snap fix
already stripped changeStreams from METEOR_REACTIVITY_ORDER; the same strip
is now applied to the remaining FerretDB run-paths — the standalone FerretDB
release start-wekan.sh, the FerretDB Docker image wekan-entrypoint.sh, and
the Windows start-wekan.bat — so changeStreams can never enter the order
however it was passed in (OpLog + polling only). The Docker compose files
already hardcode safe values, and the real-MongoDB start-wekan.sh /
start-wekan.bat are unchanged (#6492, #6493, #6480)
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/383b3dd432194d157c84b5ed30972098883cd2fd">Fix: FerretDB high CPU and cards not opening — FerretDB v1 does not implement MongoDB change…</a> Thanks to uusijani, mueschel and xet7.</summary>Fix: FerretDB high CPU and cards not opening — FerretDB v1 does not implement
MongoDB change streams, but the snap default reactivity order put
changeStreams first, so Meteor issued $changeStream aggregates that FerretDB
rejected thousands of times per second (a busy-loop pinning FerretDB CPU at
100-390% — the "aggregate=…" that dominated the operations summary — and
starving board/card loading so cards would not open). The snap now forces
changeStreams out of the reactivity order for FerretDB (oplog + polling only);
real MongoDB is unchanged (#6492, #6493, #6480)
Fix: switching the board view now persists — the setBoardView method did not await the profile write, so the client reloaded before it was saved (and a rejected write was invisible), making view switching (e.g. to the new Statistics view) unreliable; and All Boards on phones now lays the board icons out as a real 2-column CSS grid so at least 2 show per row, where the earlier float rule did not (#6488)
</details>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/81dbecfb8719ff99dac367e9921e44282c8b3ded">Safe filename handling everywhere: attachment and avatar names are always shown clean…</a> Thanks to xet7.</summary>Safe filename handling everywhere: attachment and avatar names are always shown
clean — URL-decoded, normalized to generally-used characters (Unicode NFKC plus
confusable-homoglyph folding, so a typosquatting pаypal.exe with a Cyrillic
а is shown as paypal.exe), invisible/control/bidi characters removed and
HTML/JS/XML markup stripped. Uploads are hardened (exploit and EICAR virus-test
filenames rejected, extension corrected to the real detected type, length capped
to a portable 30 chars), SVG JavaScript and XML-loop content is sanitized at a
staging path before storage, existing files can be corrected/sanitized on the
fly, storage moves check free disk space, and migrations fix + disambiguate
names. One general cleanFileName function via {{cleanFilename}} /
{{downloadFilename}}. Design at docs/Features/Filename/Filename.md
Log every filename/content sanitization to Admin Panel / Problems: whenever a name or file required sanitization (upload, migration, existing-file corrector, viewing), the Security report records WHEN, WHO uploaded it, FOR WHAT REASON (URL-encoding, invisible characters, typosquatting, the exploit kind such as JavaScript / XML code / XML loop, wrong file type, too long), the filename from → to, and WHERE (board › swimlane › list › card, plus organization and team)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1c636f09b35eb48f777bef847f4df5f40c0df969">CPU-usage monitor + governor + Admin Panel / Problems / CPU usage report: watch system-wide CPU…</a> Thanks to xet7.</summary>CPU-usage monitor + governor + Admin Panel / Problems / CPU usage report: watch
system-wide CPU, record only the START and END of each sustained high-CPU period
with what WeKan/FerretDB were doing, the automatic mitigation taken and whether
it helped; a governor pauses long batch operations to yield the CPU. On high CPU
WeKan asks the bundled FerretDB (via its throttle command) to slow down in an
adaptive feedback loop, FerretDB also self-regulates on its own and reports its
own process CPU% so a core-peg is visible even when system-wide CPU looks
moderate. Design at docs/Features/Admin-Panel/Problems/CPU-usage.md
Automatic adaptive card loading (no admin toggle): WeKan decides per board by
size — a board over the threshold (default 500 cards,
CARDS_LOADING_LAZY_THRESHOLD) loads only the visible cards (infinite-scroll
windows) plus a live count, so very large boards stay fast; smaller boards keep
loading every card. The board publication also publishes comments/attachments
with one board-level cursor instead of one per card (an N+1 that pinned FerretDB
CPU), with supporting indexes
Statistics board view (Finnish: Tilastot): a full-width board view alongside Swimlanes / Lists / Calendar / Gantt / Table showing the board's card-loading mode, counts (swimlanes, lists, cards, archived cards, labels, members, custom fields) and a time-spent summary; counts come from the server so they are accurate even in lazy mode, and its text can be selected and copied with mouse or finger. Design at docs/Features/Board/Sidebar/Status.md
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f306061273e12958d715452f27d19c8001925a2c">Admin Panel / Problems documentation reorganized under Admin-Panel/Problems/ with new RAM-usage and…</a> Thanks to xet7.</summary>Admin Panel / Problems documentation reorganized under Admin-Panel/Problems/ with new RAM-usage and Disk-usage design specs (log how much RAM+swap / disk is used and record only the start and end of each sustained high-usage period, mirroring the CPU-usage monitor)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dd21cfdc0519bb433737976ea3155f9db18143e0">Admin Panel report improvements: paginated, searchable report tables (Files, Rules, Boards, Cards…</a> Thanks to xet7.</summary>Admin Panel report improvements: paginated, searchable report tables (Files, Rules, Boards, Cards, Impersonation) that load one page at a time via index-backed sorts instead of the whole collection, theme-following buttons, and the Files report shows every filename decoded and cleaned
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/54cc0ec614fcbb2abf059dd07b75894cf7ea4fe6">Organize docs/Features into categories (Admin-Panel, Board, Cards, Editor, Reports, Automation…</a> Thanks to xet7.</summary>Organize docs/Features into categories (Admin-Panel, Board, Cards, Editor, Reports, Automation, Lists, Troubleshooting) and update all links
</details>and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/4a3800973a3f93321a7c2f92ade95d91ffb29153">Fix: removing a member from a board now visibly works — the sidebar member list kept showing…</a> Thanks to mueschel and xet7.</summary>Fix: removing a member from a board now visibly works — the sidebar member list kept showing removed members (removeMember keeps the entry with isActive:false), so "Remove from board" looked like it did nothing; the list now shows only active members (#6479)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5d56af56cd419ef09b69ed9cdcbdb30ad1b56ba1">Fix: opening a board no longer pins FerretDB CPU / takes minutes — the board publication opened one…</a> Thanks to mueschel and xet7.</summary>Fix: opening a board no longer pins FerretDB CPU / takes minutes — the board publication opened one live comments cursor and one attachments cursor per card (an N+1 that pinned FerretDB/SQLite CPU), now one board-level cursor each plus indexes; FerretDB also reports its own process CPU% so Problems shows a core-peg (#6480)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b4fd7d851f887930c0640a687847bf49223ee2ab">Fix: All Boards on mobile is scrollable again and shows at least 2 board icons per row — the list…</a> Thanks to mimZD and xet7.</summary>Fix: All Boards on mobile is scrollable again and shows at least 2 board icons per row — the list was forced min-height:100vh so it grew to fit and was clipped by the overflow:hidden wrapper; it now has a bounded height + overflow-y:auto, and the left menu is narrowed so the boards get room (#6488)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e424fb1bba82d168afa6e0cafae57a4fb4ad6b58">Fix: Rules → Workflow view can create rules again — rulesWorkflow.js used TAPi18n without importing…</a> Thanks to xet7.</summary>Fix: Rules → Workflow view can create rules again — rulesWorkflow.js used TAPi18n without importing it, so building a rule threw ReferenceError and the Add Rule handler aborted before saving (#6489)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/25f9b468256279d8f483657372e70da379c9e604">Fix: Rules list — Delete and "View rule" act on the clicked rule, not the first — the buttons live…</a> Thanks to xet7.</summary>Fix: Rules list — Delete and "View rule" act on the clicked rule, not the first — the buttons live in the rulesList child template but their handlers on the parent rulesMain used Template.currentData(), so the rule id was null (delete failed with "Match failed", View always opened the first rule); the buttons now carry an explicit data-rule-id (#6490)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e6b001320c27a11fdbd15e17f2ad230bdeab2290">Fix: a rule's action never executed even though its trigger matched — the trigger match required…</a> Thanks to xet7.</summary>Fix: a rule's action never executed even though its trigger matched — the trigger match required fields a moveCard trigger omits (oldListName), and a Mongo $in does not match a missing field unless null is in the list, so the rule silently never fired; null is now included for every field, like the cardTitle handling (#6491)
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dded8ed47695092a698ab3e34a8abc5c6f567607">Fix: admin reports load on FerretDB — the paginated report publications returned a sorted+limited…</a> Thanks to xet7.</summary>Fix: admin reports load on FerretDB — the paginated report publications returned a sorted+limited live cursor, whose limited live observe hangs on FerretDB's OpLog, so the report was stuck on the loading spinner; they now publish the page manually so ready always fires
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5949e85cfd9996651e1844fa6238d5a0f2da793e">Fix: the paginated admin reports never get stuck on the loading spinner if a report subscription…</a> Thanks to xet7.</summary>Fix: the paginated admin reports never get stuck on the loading spinner if a report subscription fails — they now handle onStop, clearing the spinner and surfacing the error
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL VULNERABILITIES, all reported by meifukun: RedirectBleed, SourceBleed, LiveBleed, CasBleed, MetricsBleed, ImpersonateBleed and InviteBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix RedirectBleed: avatar localization validated only the original host and then followed redirects…</a> Thanks to meifukun and xet7.</summary>Fix RedirectBleed: avatar localization validated only the original host and then followed redirects with native fetch, so a public avatar URL could 302 to a loopback/private/metadata address (SSRF). Avatars are now fetched via the DNS-pinned, redirect-rejecting fetchSafe, and profile.avatarUrl is scheme-validated (http/https/data:image/local) at write time
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix SourceBleed: a Trello board imported with a javascript: url was stored as an activity…</a> Thanks to meifukun and xet7.</summary>Fix SourceBleed: a Trello board imported with a javascript: url was stored as an activity source.url and rendered as a clickable activity-sidebar link, so a board admin clicking it ran attacker JavaScript (stored XSS, Meteor.loginToken theft). Only http(s) source URLs are now stored and linked
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix LiveBleed: the live Trello import fetched attacker-controlled attachment/background/avatar URLs…</a> Thanks to meifukun and xet7.</summary>Fix LiveBleed: the live Trello import fetched attacker-controlled attachment/background/avatar URLs with bare fetch (no SSRF guard) and stored the response for read-back through the attachment API (non-blind SSRF). Every live-import download is now gated by validateAttachmentUrl, matching the offline import path
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix CasBleed: CAS login stored validated user data in a module-global (_userData) shared across…</a> Thanks to meifukun and xet7.</summary>Fix CasBleed: CAS login stored validated user data in a module-global (_userData) shared across concurrent logins, so two logins could race and issue an attacker a session for a victim's account. The user data is now bound per credential token
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix MetricsBleed: the /metrics endpoint trusted a client-supplied X-Forwarded-For header…</a> Thanks to meifukun and xet7.</summary>Fix MetricsBleed: the /metrics endpoint trusted a client-supplied X-Forwarded-For header unconditionally, so anyone could forge a whitelisted IP and read operational metrics. XFF is now trusted only when METRICS_TRUST_PROXY is set (parsed spoof-resistantly from the right); otherwise the real socket address is used
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix ImpersonateBleed: several board export endpoints treated any historical ImpersonatedUsers…</a> Thanks to meifukun and xet7.</summary>Fix ImpersonateBleed: several board export endpoints treated any historical ImpersonatedUsers record as an authorization bypass (canExport || impersonateDone), so a demoted former admin could export private boards forever. The impersonation bypass is removed; export requires real board visibility (canExport) only
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1669a1af196ba48395e01376b3a0fc784ad42cc3">Fix InviteBleed: invitation registration used a 6-digit Math.random() code (~900k keyspace) with no…</a> Thanks to meifukun and xet7.</summary>Fix InviteBleed: invitation registration used a 6-digit Math.random() code (~900k keyspace) with no throttling, brute-forceable to hijack an invited account and its private boards. Codes are now a 128-bit crypto.randomBytes value and account creation is rate-limited (DDPRateLimiter)
</details>The OIDC login shared-serviceData race from the same report was already fixed
earlier (#4897 moved the per-login profile/serviceData/userinfo objects
inside the OAuth callback); the new source-guard test verifies it stays that
way. Thanks to meifukun and xet7.
and adds the following new features:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1d82ef8eae344ca0565d0858fc697f470e471dd7">Added an automatic security/speed/tests remediation-logging subsystem (design docs…</a> Thanks to xet7.</summary>Added an automatic security/speed/tests remediation-logging subsystem (design
docs docs/Security/Remediation/WeKan.md and FerretDB.md). WeKan's runtime guards
(SSRF, upload/avatar rejection and sanitization, forged X-Forwarded-For, export
authz, invite rate-limit, …) now record each block/sanitize/remediate event into
the existing WeKan database (a single eventlog collection, stream
security/speed/tests) via normal Meteor JavaScript queries — no new files or
databases under WRITABLE_PATH, so it works the same on FerretDB and MongoDB.
FerretDB reports its problems to WeKan, which records them. Events use general
category names plus the hall-of-fame *Bleed names
Added Admin Panel → Problems (a new button on the 2nd header bar to the right of the Info/version button, with a warning icon that turns red when there are new problems). The old Reports button is removed and its page becomes the Problems page: a left menu with Summary, Security, Speed and Tests above the moved Broken Cards/Files/Rules/Boards/Cards/Impersonation reports. Summary is a checkbox list of problem areas with one Acknowledge button (the only place to acknowledge, which resets the per-area new-problem count); Security/Speed/Tests are read-only, paginated, searchable event tables
</details>and adds the following updates:
<details> <summary><a href="https://github.com/wekan/wekan/commit/dd21cfdc0">Reworked the Admin Panel → Problems pagination tables (Files/Rules/Boards/ Cards/Impersonation and…</a> Thanks to xet7.</summary>Reworked the Admin Panel → Problems pagination tables (Files/Rules/Boards/ Cards/Impersonation and the Security/Speed/Tests event tables): the search field and pagination controls now sit in one row (pagination on the right, RTL-aware), the redundant Search button is gone (typing + Enter searches), and the controls follow the current theme — they use var(--theme-accent), so Member Settings → Change color recolors them
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/52acd4b75">Made buttons follow the theme: the global button base and the primary buttons (forms.css) plus the…</a> Thanks to xet7.</summary>Made buttons follow the theme: the global button base and the primary buttons (forms.css) plus the admin and People-panel buttons now use var(--theme-accent, <original>), so a Change-color theme override recolors buttons across the app while the default look is unchanged
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fccc4a739">Admin Panel → Files report: URL-encoded filenames (e.g. "%D0%93%D1%80") are decoded for display…</a> Thanks to xet7.</summary>Admin Panel → Files report: URL-encoded filenames (e.g. "%D0%93%D1%80") are decoded for display, filenames are always shown as plain text (never markdown/ HTML), a name hiding invisible/zero-width/bidi characters gets a red warning triangle on the left and each invisible character is replaced inline by its red Unicode name (e.g. "evil[U+200B ZERO WIDTH SPACE].png"), and a filter lists only such names
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6d3c550d8">Removed clickable column-header sorting from the board Table view and the Admin Panel → People →…</a> Thanks to xet7.</summary>Removed clickable column-header sorting from the board Table view and the Admin Panel → People → Domains table; both now show a stable fixed order (and keep their search + pagination). Sorting a server-paginated table means sorting the whole set, which fights the per-page perf fix
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1039b2e3286135a1749aeab9526b52d40fb82e43">Enabled FerretDB OpLog tailing by default so Meteor stops poll-and-diff, the main fix for FerretDB…</a> Thanks to uusijani, Nissulya and xet7.</summary>Enabled FerretDB OpLog tailing by default so Meteor stops poll-and-diff, the main fix for FerretDB sitting at 100–390% CPU on busy boards. With FerretDB there is no MongoDB oplog, so Meteor re-ran every live query on a timer; FerretDB v1 now ships an OpLog (auto-created capped local.oplog.rs + replica-set hello handshake), so every FerretDB launch path starts ferretdb with --repl-set-name=rs0 and points WeKan at it via MONGO_OPLOG_URL, and Meteor TAILS the OpLog instead of polling. OpLog is used ONLY when tailing actually works: every platform that defaults to FerretDB — Snap, Sandstorm, the Docker image/compose and the prebuilt bundle — starts ferretdb with --repl-set-name and sets METEOR_REACTIVITY_ORDER=oplog,polling, so Meteor falls back to polling if the OpLog cannot be established (a broken/absent OpLog never blocks startup). Kill-switch WEKAN_FERRETDB_OPLOG=false reverts to polling only. Admin Panel → Version shows which driver is actually live ("Reactivity mode") next to the configured METEOR_REACTIVITY_ORDER and DDP_TRANSPORT, so you can confirm OpLog came up rather than fell back. Also trimmed the client activity feed page from 500 to 50 rows (infinite scroll still loads more on demand)
</details>and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f7c4a916f">Fix the Admin Panel → Problems → Cards report spinning while it loaded on big sites: it paginates…</a> Thanks to xet7.</summary>Fix the Admin Panel → Problems → Cards report spinning while it loaded on big sites: it paginates, but sorted by an unindexed { boardId, sort }, so every page full-sorted all cards (11761+) in memory. It now sorts by the existing { boardId, createdAt } index, so one page is a bounded index scan; also added a { stream, at } index for the Security/Speed/Tests tables
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19870ad52">Fix the Admin Panel → Translation page loading the ENTIRE Translations collection at once: it…</a> Thanks to xet7.</summary>Fix the Admin Panel → Translation page loading the ENTIRE Translations collection at once: it subscribed with a hardcoded limit of 0 (= no limit). It now uses the infinite-scroll window, loading a page at a time
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e205f6b7a">Fix the Board Archive → Boards list loading every archived board at once: it now pages server-side…</a> Thanks to xet7.</summary>Fix the Board Archive → Boards list loading every archived board at once: it now pages server-side (30 per page) with a search box and prev/next controls, so only the current page is loaded
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7da21c46b">Fix "Did not check() all arguments" server-log spam from the Admin Panel → Problems detail pages…</a> Thanks to xet7.</summary>Fix "Did not check() all arguments" server-log spam from the Admin Panel → Problems detail pages: the eventLogCount/eventLogPage methods awaited the admin check before check()ing their arguments, so a non-admin call (or one before admin status resolved) made Meteor's audit-argument-checks mask the real error. check() now runs first in every argument-taking event-log method
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2f7b49d8def4bb03d233b83671ac6c78308124ea">Fix MongoDB 3.x → FerretDB v1 migration failing on non-finite numbers: cards whose sort was…</a> Thanks to Nissulya and xet7.</summary>Fix MongoDB 3.x → FerretDB v1 migration failing on non-finite numbers: cards
whose sort was ±Infinity or NaN (written by old WeKan before #6472) were
rejected by FerretDB/SQLite ("infinity values are not allowed") and dropped, so
boards stayed stuck on the loading spinner. The migration now clamps every
non-finite double to a finite 0 before insert, and a new nonfinite-sort-repair
startup schema-upgrade step heals the same corruption in an already-running
database (MongoDB or FerretDB)
Fix LDAP mail → email field mapping returning undefined (regression since
the ldapjs → ldapts move): the sync read the mapped attribute case-sensitively
while the rest of wekan-ldap reads it case-insensitively, so a directory that
returns the attribute with a different case synced no email address. It is now
read via getLDAPValue() like everywhere else
Fix a memory/listener leak in board and CSV export: each write paused on socket backpressure leaked one 'error' listener on the response ("MaxListenersExceededWarning: 11 error listeners added to [ServerResponse]"), which on a large export grew unbounded. Both listeners are now removed once the write settles
</details>Thanks to above GitHub users for their contributions and translators for their translations.
This release adds a new developer documentation set and release automation.
It adds the following documentation, a new docs/Design/Autoupdate/ set
describing how software is installed and updated — manually and automatically
(Snap-like) — on every operating system, and how to release WeKan to more
platforms:
Added why the ppc64el/s390x snap builds fail on core24 (the QEMU multiarch action caps at core22), moved to Autoupdate/Forks/Snap-Core.md
</details>Added per-OS install/update comparison docs for all operating systems (Linux, BSD, Haiku, Amiga, RISC OS, Solaris/illumos, AIX, HP-UX, OpenVMS, z/OS, ArcaOS, ReactOS, FreeDOS, Plan 9, Redox, SerenityOS, Android, iOS, ChromeOS, HarmonyOS, Ubuntu Touch, Sailfish, postmarketOS, KaiOS, Tizen, webOS and TV/streaming OSes), plus the Snap-Ondra-Gantt.md variant-snap spec
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/58c0e6d1f8bfabc624c870c9e91c6a54f0877cda">Added UCS.md: releasing the Docker WeKan UCS App Center app with automatic MongoDB 3.x to FerretDB…</a> Thanks to xet7.</summary>Added UCS.md: releasing the Docker WeKan UCS App Center app with automatic MongoDB 3.x to FerretDB v1 SQLite migration
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8ae0ee54460444f6517fbe5a63f6ba143d5b6277">Added Nextcloud.md: building WeKan as a Nextcloud ExApp, App Store publishing, and AI (Task…</a> Thanks to xet7.</summary>Added Nextcloud.md: building WeKan as a Nextcloud ExApp, App Store publishing, and AI (Task Processing API) / Deck / OIDC SSO integrations
</details>and the following documentation reorganization:
and the following release automation:
<details> <summary><a href="https://github.com/wekan/wekan/commit/2054ea2b54a193b9e53267b3dc3fbe15056c9e1e">Added guarded snap-variants / ucs / nextcloud publish jobs to release-all.yml (dormant until their…</a> Thanks to xet7.</summary>Added guarded snap-variants / ucs / nextcloud publish jobs to release-all.yml (dormant until their secrets are set, so ordinary releases are unaffected)
</details>and adds the following updates:
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/b8bb00465037389eb5829157441f2b75819353d9">Member Settings — optional "Submit editors with Enter"</a>. Thanks to xet7.</summary>Member Settings — optional "Submit editors with Enter". A new per-user
setting in the Member Settings (Change Settings) popup, saved to
profile.submitOnEnter and off by default: when on, plain Enter saves
the card title, description and other inline editors and Shift+Enter inserts
a new line; when off, behaviour is unchanged (Ctrl/Cmd+Enter saves). This
restores the fast Enter-to-save workflow for users who prefer it without
regressing #4236.
Setting,
translated to all languages
Right board sidebar — drag the edge to resize its width (desktop). Drag the
sidebar's edge like a spreadsheet column; the width is saved to
profile.sidebarWidth for logged-in users and to browser localStorage for
anonymous users on a public board, then re-applied on load. The handle uses
logical positioning, so in LTR it is the left edge of the right-docked sidebar
and in RTL the right edge of the left-docked sidebar (drag direction inverts);
on phones the sidebar stays full width.
Feature,
tooltip translated to all languages
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/e0991d6602c915e13d77776af36ecac410965909">Upgrade migration no longer drops cards/activities whose exported data holds NaN or Infinity</a>. Thanks to Nissulya and xet7.</summary>Upgrade migration no longer drops cards/activities whose exported data holds
NaN or Infinity (#6481).
mongo 3.x mongoexport emits non-finite doubles as the bare tokens
NaN/+Infinity (e.g. a card's "sort":+Infinity, an activity's
"value":NaN), which are not valid JSON, so EJSON.parse threw and the whole
document was dropped — leaving boards/cards missing after an otherwise
"successful" 6.09 → 10.x migration. Those bare tokens are now rewritten (outside
string literals) to the canonical EJSON {"$numberDouble":"…"} form so the
document migrates instead of being lost. Fix
Add List — the inline composer's previous options are back (#6465). The per-list-header inline Add List composer had been reduced to just a title input; the "add after which list" position selector (pre-selecting the list whose header opened it) and the "or template" link are restored. Fix
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b8a21e4ce3acca427a25f11bcb8c0fdf6bc26ee3">Right sidebar width no longer doubles on wide screens, and its input fields fill the width</a>. Thanks to xet7.</summary>Right sidebar width no longer doubles on wide screens, and its input fields
fill the width. The sidebar used width: 30vw, so it grew unbounded on very
wide screens (looking right only at mid widths); it is now clamped. The filter
text inputs (Filter List by Title, Filter by card title, …) had no width and
rendered narrow with a big gap on the right; they now fill the padded content
width with equal spacing on both sides. Fix
The Multi-Selection sidebar "Copy selection" button showed its raw translation
key. The copy-selection key was missing from every language file — including
the English source — so i18next had no value to fall back to. It was added
("Copy selection") and translated to all languages. Fix
Follow-up fixes surfaced by the full test run. Now that generic Chinese
(zh) is its own registered language, a bare zh browser tag resolves to that
entry (Simplified Chinese) instead of the old zh → zh-Hans alias, which is
removed; and the board-actions end-to-end test opens the board menu from the
right sidebar (the header Board Settings cog was removed). Fix
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following updates:
and adds the following features:
imports/i18n/data/ is now filled in (previously most were largely
English). Each language matches its own existing terminology as a glossary
plus standard Kanban terms; product/brand names, acronyms, placeholders and
markup are preserved; regional variants were seeded from their base language;
and a few wrong ones were corrected (Wu Chinese wuu-Hans was registered; the
wa-RR file is Waray/war, not a Walloon variant, so it was re-translated;
Wolof/Malay display names were fixed) so that all languages are
browser-detectable and visible in the Change Language popup.
releases/translations/push-all-translations.sh force-pushes every language
to Transifex. Commits:
Member menu / Change Color — global theme override, visible swatches, applies
everywhere, no Save button ( #5778 , docs/Theme/Theme.md). The member menu
gets a Change Color entry that works like Board Settings / Change Color but
is a per-user global override saved to your profile, so the chosen theme
applies to the whole UI — All Boards, Search, Admin Panel, My Cards, Due
Cards and board pages — via a board-color-<name> class on <body>/header
(while you are on a board, that board's own color still wins). This makes a
dark theme everywhere possible. Done differently from the first cut: the
picker was initially built as two-level dropdowns (category then theme);
that was replaced by visible color swatches grouped by category (Flat /
Clear / Dark / Special, category name above each group), and the Save button
was removed — clicking a swatch applies immediately. Flat themes allow 1
custom color and clear themes 2 (a gradient) via the native color
wheel; dark/special are fixed. Custom colors are validated server-side
as #rrggbb and applied as CSS variables (--theme-accent /
--theme-accent-2). Category titles are left-aligned and the popup has a title
Member Settings / Font — pick an installed UI font, size and text colors
(part of #4759 ). A new Font member-menu entry lets you choose a UI font
that already exists in your browser (canvas-detected; names are plain text,
never HTML/markdown, validated server-side against a curated whitelist — only a
known font name and a preset percentage ever reach the DOM), a font size,
and custom text and text-background colors (each unsettable). Done
differently from the first cut: the font and size were first built as
dropdowns; that was replaced by buttons — one font-name button per
detected font, each rendered in its own font, and a row of size buttons
(smaller … 100% in the middle … larger) — that apply immediately on click
(no Save). A preview pangram shows below the font buttons, the Unset
links are styled as buttons, and the popup has a title. Stored as
profile.uiFont / profile.uiFontSize / profile.uiTextColor /
profile.uiTextBgColor. (Only the font part of #4759, so the issue stays open.)
Thanks to xet7. Commits:
Soft delete + Undo/Redo + change-History — deleting is reversible, and moves
can be undone (#1023, #6478 ,
docs/Features/Undo/Undo.md). Deleting a list no longer destroys it: the
list and its cards are marked deleted
(deletedAt/deletedBy/deleteBatchId), hidden from the board, and restorable
via lists.restore or Ctrl+Z — the first slice of a general "no
permanent delete in ordinary use" principle (physical deletion limited to
GDPR/account erasure and an explicit Global-Admin purge behind an off-by-default
Admin Panel / Features / Delete flag). Card/list/swimlane moves are now
undoable/redoable with Ctrl+Z / Ctrl+Y (the userPositionHistory collection
was silently not recording — its guard checked an un-imported global — and
had no redo or key bindings; all fixed, selection logic unit-tested). Design
unifies this into one universal change-History with scoped views. Activities
were confirmed to already load progressively
Home board — set a board that opens automatically after login ( #2220 ). A
per-user default "home" board (profile.defaultBoardId) opens automatically
after login on all deployments (a once-per-session router redirect), and on
Sandstorm a grain with exactly one board again opens straight into it
(without saving anything; a saved Home board wins, a many-board grain with
no choice shows All Boards). You set the Home board from the All Boards page
via Multi-Selection: turn Multi-Selection on, select a board, and click the
home action in the new "Selected:" row (which also has a star
action); the Home board then shows a home badge and sits at the top of
Starred. Done differently: the earlier per-board-tile home toggle was
hidden, but setting a Home board was brought back through the Multi-Selection
"Selected:" action — so the feature is available, not disabled, and not
Sandstorm-only — and the initial Sandstorm "save the single-board choice" was
changed to no-save
Add List moved from a standing column to a per-list header button ( #6465 ).
The Swimlanes/Lists views no longer show a permanent "Add List" composer column;
each list header has a far-right add-list button that opens the composer as a
column immediately after that list (right in LTR, left in RTL), and an
empty swimlane/board shows a + button. Also fixes the composer's Save
doing nothing (createListAfter's check() used a non-Optional matcher for
nextListId, so the inline composer 400'd with "Match failed"). (First cut —
desktop/main views.) Thanks to csonkaoszimt (report) and xet7. Commits:
All Boards, board header, top bar and popups — a large batch of layout/theming polish. Highlights: the right-sidebar hamburger is pinned to the right edge at every width with an ~8px gap, and the duplicate Board Settings cog was removed; the All Boards My Boards toolbar wraps (order Multi-Selection then Sort then Search), the Multi-Selection button matches the Swimlanes view, the empty grey band was removed, "+ Add …" tiles match board-tile height, the board-tile drag handle sits at the right middle (transparent, icon only), and the search box is half width with the multi-selection hint beneath it; the mobile/desktop toggle and zoom swapped places; the Change Language popup shows multiple columns; the Change Avatar upload button and the Search All Boards / Archived boards buttons follow the theme colors; the Archived boards modal is full width and starts below the header; the Notifications drawer is full width with a black hamburger + X and black menu icons; and the member menu's duplicate Notifications entry was removed. Done differently: an "icons only — always hide button text labels" board-header change was tried and then reverted, so button text labels stay.
</details>Denser default layout: thinner lists, board tiles and card dock;
taller-looking minicards ( #6465 ). List width default 272 to 220px
(minimum 270 to 200px), All Boards board tiles min-height 100 to 72px,
and the card-detail right dock max-width 800 to 520px — all overridable,
user customizations untouched. Minicards no longer reserve a tall empty band
under a one-line title (the title viewer gets min-height: 0), and the swimlane
header no longer clips at non-100% zoom
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f6f021f8cc8de452593f499e1306dca021679325">Confirmation dialogs work again</a>. Thanks to mueschel (report) and xet7.</summary>Confirmation dialogs work again — clicking any .js-confirm button (e.g.
"Remove Member") did nothing because Popup.afterConfirm() stashed the pending
action on the Blaze data context (an immutable Minimongo doc that Blaze
re-creates on re-render); it is now stored on the Popup instance
(#6479)
Moving a card between swimlanes is no longer broken with long lists — jQuery UI
sortable cached geometry at drag-start and never re-cached on WeKan's manual
auto-scroll, so the drop re-homed the card in the source swimlane; fixed with
sortable('refreshPositions') after a scroll
(#6477)
Filter by date -> Overdue no longer lists cards with no due date — $lte/range
selectors matched null under both FerretDB and minimongo;
DateFilter._getMongoSelector now adds $ne: null (reported by email)
A board-wide list no longer disappears from other swimlanes when nudged — the
drop handler treated any small drag of a board-wide list (swimlaneId === null)
as a swimlane change; it now also requires an original swimlane (reported by
email)
FerretDB (bundled) — #6476 crash-loop root cause fixed, and label/title/date
filters pushed down to SQLite ( #6476 ). A startup SyncedCron upsert hit an
orphaned-table error (table "…connections_<hash>" already exists) that became
an unhandledRejection and crash-looped WeKan so its port never opened; the
bundled FerretDB now creates tables/indexes with IF NOT EXISTS. It also pushes
label ($in), title ($regex) and dueAt range filters down to SQLite instead
of scanning every card in Go. (Details in the fork's own CHANGELOG.) Thanks to
uusijani, a1bert01 and xet7. Changelog commits:
Snap release, CI and dev tooling. The exotic ppc64el/s390x snaps now
build on GitHub Actions under QEMU instead of Launchpad (which returned exit
0 with no artifact); build.sh CURRENT-IP detection is subnet-agnostic (no more
http://:3000); and the Playwright E2E workflow was disabled. Commits:
Snap release: build ppc64el and s390x under QEMU on GitHub Actions, not Launchpad
build.sh: robust CURRENT-IP detection (fix http://:3000 Invalid URL)
Changelog, docs and test housekeeping. Commits:
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following BUG:
<details> <summary><a href="https://github.com/wekan/wekan/issues/6469">LDAP connection leak: WeKan exhausted the directory server with "too many open connections"</a>.</summary>LDAP connection leak: WeKan exhausted the directory server with "too many open
connections" (#6467, #6469 ).
Operators reported that after an update WeKan "dies really quickly", that
restarting the server did not help, and that it "kills our openldap server with
too many open connections"; others saw logins hang for minutes and then fail
with "Must be logged in". Root cause: every LDAP login attempt
(packages/wekan-ldap/server/loginHandler.js) and every background sync run
(packages/wekan-ldap/server/sync.js) created a fresh new LDAP() and called
connect(), but the code never called disconnect() on any path — success,
failure, or fallback. Each login attempt (including every failed one) and each
background-sync tick (every minute by default) therefore leaked one socket to
the LDAP/AD server. Over time this grew without bound until the directory server
hit its per-client connection limit and started refusing connections, which took
it — and, with it, every WeKan login — down
packages/wekan-ldap/server/connectionGuard.js (runWithLdapDisconnect(ldap, fn))
runs the work and disconnect()s in a finally, on every exit path. The disconnect is best-effort and
never masks the original result or error.runWithLdapDisconnect, so connect() is always
paired with a disconnect() — on a successful login, a thrown Meteor.Error, or a fallback to the
default account system.sync() releases its connection in a finally, and importNewUsers() disconnects only
the connection it opened itself (never a connection borrowed from sync()).ldap_test_connection method (packages/wekan-ldap/server/testConnection.js), which had the
same leak, now disconnects on both the success and failure paths, so repeated "Test Connection" clicks
no longer leak either.LDAP.disconnect() is now a safe no-op when connect() was never reached, so cleanup can never throw.tests/ldapConnectionRelease.test.cjs, added to
test:unit:node): behavioural coverage of the guard (disconnects after
success and returns the result; disconnects exactly once; disconnects and
re-throws when the work throws — the failed-login path that exhausted
OpenLDAP), negative cases (a failing disconnect never turns a success into a
failure nor hides the real error; a missing/null ldap is tolerated), plus
source-level guards that the leak-prone call sites actually route through
the guard and that connect() lives inside the guarded region.and updates the bundled FerretDB (see the fork's own CHANGELOG Upcoming): the
SQLite backend connection pool no longer caps MaxOpenConns at 16, which had
starved WeKan's cursor-heavy load and made
boards take minutes to load and logins fail with "Must be logged in" (#6467,
#6469).
Snap: FerretDB never becoming ready made WeKan hang with no web port and no
explanation ( #6476 ). With database=ferretdb, snap-src/bin/wekan-control
waits for FerretDB to accept connections before starting WeKan — WeKan does
not open its HTTP port until the database answers. That wait loop had no
timeout and no diagnostics: if FerretDB never started listening (a crashed or
CPU/architecture-incompatible per-arch binary, a locked or corrupt SQLite
database, or the FerretDB service left disabled by a failed migration hand-off),
WeKan blocked there forever. The service showed as active, but the port never
opened and the only symptom was a silent FerretDB not ready yet, retrying in 5 seconds repeating — exactly what #6476 reported (Apache proxy could not connect
to port 3333). The reporter's wekan.log stopped right after the startup env
dump, confirming control never reached node main.js
WEKAN_DB_WAIT_TIMEOUT
seconds (default 120, overridable) the FerretDB wait loop now prints an
actionable hint once and keeps retrying — pointing to snap logs <snap>.ferretdb for the real error, naming the SQLite directory to check
for locks/corruption, the arch-mismatch (exec format error) case, the
snap start --enable <snap>.ferretdb recovery, and the snap set <snap> database=mongodb fallback to keep working meanwhile. This turns "port never
opens, no clue why" into a clear message. Regression test:
tests/ferretdbWaitTimeout.test.cjs, including a negative guard that the
FerretDB wait is not a silent unbounded loop again.Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://wekan.fi/hall-of-fame/escapebleed/">EscapeBleed</a>.</summary>** EscapeBleed : incomplete string escaping when building a regular expression**
(GitHub CodeQL code scanning alert #423, rule js/incomplete-sanitization,
CWE-116 Improper Encoding or Escaping of Output;
tests/maximizedCardPosition.test.cjs). Code that turned a CSS declaration into
a RegExp escaped only parentheses (str.replace(/[()]/g, '\\$&')) instead of
the full regex metacharacter set — an input containing other metacharacters
(including a backslash) would not be escaped correctly, so the generated pattern
could match the wrong thing
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), matching the correct pattern
already used elsewhere in the tests. This is test-only code with a fixed,
trusted input list, so there was no injection exposure, but the incomplete
escape was genuinely wrong.** RandomBleed : biased random ids from a cryptographically secure source**
(GitHub CodeQL code scanning alert #422, rule js/biased-cryptographic-random,
CWE-1204 / weak randomness; server/lib/schemaUpgradeSteps.js). The startup
schema upgrade generates Meteor-style document ids with
crypto.randomBytes(len) mapped through byte % ID_CHARS.length. Because 256
is not a multiple of the 55-character alphabet, that modulo skews generated ids
toward the first 36 characters of the alphabet (each ~1.4% more likely than the
rest) — reducing entropy of the ids used for the swimlanes/lists/checklist-items
the upgrade creates
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/issues/6473">Snap: SELF-HEALING attachments — the fixes below apply themselves automatically on upgrade, no…</a> Thanks to mueschel and xet7.</summary>Snap: SELF-HEALING attachments — the fixes below apply themselves
automatically on upgrade, no commands needed ( #6473 ,
snap-src/bin/attachment-repair (new), snap-src/bin/wekan-control,
snap-src/bin/migration-control, snap-src/bin/wekan-force-migrate,
releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs, snapcraft.yaml,
snapcraft-core26.yaml). The snap auto-refreshes on ~15k servers, so a fix that
needs snap run wekan.migrate typed by hand does not reach most of them — and a
full re-migration would be WRONG anyway, because it rebuilds FerretDB from the
frozen MongoDB source and would lose boards/cards users created on FerretDB
since migrating. Instead, on every start on FerretDB, wekan-control now
launches attachment-repair in the background: it starts a temporary source
mongod (7.x or the bundled 3.2 reader — the MongoDB data was never modified, so
everything is recoverable) and runs the migration importer in a new
incremental FILES_ONLY mode that checks what is already migrated and
migrates only what is missing: text collections are never touched, every
attachment/ avatar whose target record is on fs with the file actually on disk
is verified and skipped, records deleted by users since the migration are never
resurrected, and only the missing binaries/records are extracted — so on healthy
servers the repair is a fast no-op and on #6473-affected servers the attachments
simply appear, live, while WeKan runs. It runs once (marker
$SNAP_COMMON/.attachments-files-v2-done; a fresh successful migration
pre-writes it, snap run wekan.migrate clears it, failures retry on the next
start without ever blocking WeKan from starting), and a manual snap run wekan.repair-attachments command is registered too. The importer itself now
stamps the migration marker with filesVersion (currently 2) and, when it finds
a marker with an older filesVersion — or FILES_ONLY=true in the environment
— automatically switches to this incremental repair, so Docker/source installs
get the same self-healing by simply re-running the same importer command they
migrated with. Verified end-to-end against two live FerretDB instances with the
real importer: a broken-migration target (missing CollectionFS record,
gridFsFileId-only record, marker without filesVersion) was repaired — record
re-created with its card linkage, binary extracted, record repointed — while a
board renamed on FerretDB after the migration stayed untouched, and the second
run exited immediately as already-migrated. Behavioral tests (positive +
negative) drive the real bash script with stubbed snap tooling:
tests/attachmentRepair.test.cjs
All platforms: startup schema upgrade — WeKan now CHECKS on start that data
from EVERY old WeKan version has been migrated to the newest database structure,
and migrates only what is missing
(#6473 follow-up,
#1959, #1971 ,
server/lib/schemaUpgradeSteps.js (new), server/startupSchemaUpgrade.js
(new), server/migrations/ensureValidSwimlaneIds.js, server/imports.js).
WeKan v0.9–v8.00 ran startup migrations; v8.01 disabled them (large databases
meant long downtime) in favour of read-time compatibility — which covers the
swimlane era but not everything, so text data from old versions could sit
invisible in the database. The new startup upgrade reinstates the safety net
without the downtime: version-gated (the _wekan_migration marker stores
the WeKan version and datetime of the previous successful re-check, so while the
version is unchanged a boot costs ONE findOne — a full re-check is mandatory
only after a new WeKan release, or WEKAN_FORCE_SCHEMA_UPGRADE=true; opt out
with WEKAN_SKIP_SCHEMA_UPGRADE=true), non-blocking (runs in the
background, WeKan serves immediately), with a live migration dashboard at
/schema-upgrade-status on every platform that shows the Admin Panel product
name when one is set (not "WeKan") plus per-step progress, and fast on big
databases (bounded existence probes, distinct() set-joins instead of card
scans, and server-side updateMany batches instead of per-document round trips
— thousands of cards never mean thousands of queries). Steps, each verified
against git show v8.00:server/migrations.js and each idempotent:
archived-flag-backfill (docs missing archived never match the archived: false view queries — whole boards/lists/cards were invisible in the Swimlanes
and Lists views), swimlane-structure (every board gets a visible swimlane,
every list/card a swimlaneId, cards with a dangling listId are rescued to a
visible list — and, fixing #1959 and #1971, unarchived cards whose swimlane was
DELETED, ARCHIVED or belongs to another board are reassigned to the board's
first visible swimlane, so everything is visible in both the Swimlanes view and
the Lists view; the card-insert hook now also validates client-supplied
swimlaneIds so new cards can never land under a deleted/archived swimlane
again), checklist-items-embedded (pre-v0.79 embedded checklist.items[]
extracted to the ChecklistItems collection — the text was in the database but
never shown), customfields-boardIds (pre-v2.49 scalar boardId → boardIds
array — old custom field definitions and their card values were orphaned),
board-allows-defaults (the ~33 defaultValue-true allows* flags backfilled —
a missing flag rendered as false and HID existing
descriptions/checklists/comments/attachments), board-members-isactive (members
without isActive were denied board access), board-permission-lowercase
('PUBLIC' boards had silently become member-only), and fs-path-heal
(filesystem attachments/avatars whose recorded path predates the current
WRITABLE_PATH layout — v6.10-18 uploads/<coll>, v6.19-v8.4x
WRITABLE_PATH/<coll>, CFS→ostrio temp files — are located and repointed/copied
into the current layout). A step that fails or leaves unresolved work never
blocks WeKan from starting and keeps the version un-stamped so the next boot
re-checks. 36+ unit tests with negative cases
(tests/schemaUpgradeSteps.test.cjs), plus verified end-to-end against a live
FerretDB (SQLite) with old-shape seed data: all 8 steps migrate correctly and
the second boot is gated to a no-op
Migration speed: batched writes and preloaded lookups instead of per-document
round trips (5-hour migrations reported;
releases/migrate-mongodb-to-ferretdb.mjs). The text phase now copies each
batch with ONE unordered insertMany round trip (falling back to per-document
replaceOne upserts only for batches that hit duplicates on resumed/re-run
migrations), and the attachment/avatar phases preload the target's metadata
records once per bucket instead of one findOne per file (bounded: collections
over 100k records fall back to per-file lookups). The startup schema upgrade
uses the same philosophy (distinct()/updateMany).
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/issues/5695">OAuth2/OIDC: OAUTH2_LOGIN_STYLE=redirect was ignored — a popup always opened</a>. Thanks to ArturRuta and xet7.</summary>OAuth2/OIDC: OAUTH2_LOGIN_STYLE=redirect was ignored — a popup always
opened ( #5695 , packages/wekan-oidc/oidc_client.js,
server/authentication.js, server/models/settings.js,
client/components/main/layouts.js). The setting was stored correctly in the
OIDC service configuration, but the login button always passed loginStyle: 'popup', and Meteor's OAuth._loginStyle gives the caller's option precedence
— so the admin's redirect setting silently lost on every login (and the
Meteor-internal loginStyle option even leaked into the provider's
authorization URL). The client now honors a configured loginStyle: 'redirect'
over the button's generic popup default (explicit caller choices still win;
Safari-private-mode popup fallback kept) and no longer leaks the option to the
provider. Also repaired the existing OIDC_REDIRECTION_ENABLED=true "go
straight to the provider" feature, which was doubly broken since the Meteor 3
port: isOidcRedirectionEnabled inspected a Promise (always false), and the
client handler assigned an undeclared variable (strict-mode ReferenceError).
Behavioral tests drive the real client code in a VM against Meteor's loginStyle
precedence: tests/oauth2LoginStyle.test.cjs (12 tests, positive + negative)
Deleting a user left "ghost users" on boards and cards ( #1289 ,
models/users.js, new models/lib/userDeletionCleanup.js): every deletion path
(admin method, self-service delete, DELETE /api/users/:userId) removed only
the user document, leaving dangling references — empty-avatar board members that
could not be removed, stale card members/assignees/watchers, orphaned avatar
files — reproducible for 8 years. A server-side Users.after.remove hook now
prunes boards.members/watchers, cards.members/assignees/watchers,
lists.watchers and the user's avatar files on every deletion path; activities
and comments are deliberately kept for history (their rendering is already
null-guarded). Tests: tests/userDeletionCleanup.test.cjs (6)
Swimlanes jumped up and down when starting/ending a card drag ( #2877 ,
client/components/boards/boardBody.css): drag start hid every list's "+ Add
Card" composer link with display: none, collapsing its row — lists shrank,
auto-height swimlanes shrank, and every swimlane below jumped up ~23px (and back
down on drop), shifting the drop target under the cursor mid-drag (root cause
proven by frame-diffing the issue's own GIF). The composer now hides with
visibility: hidden, keeping its layout box, so nothing moves; collapsed
multi-selection cards stay collapsed intentionally (they preview the post-drop
list). Tests: tests/swimlaneDragJump.test.cjs (8)
Dragging a card toward an off-screen list never auto-scrolled the board ( #443 , client/components/lists/list.js, new imports/lib/boardAutoScroll.js):
the horizontal auto-scroll targeted .board-canvas, which only overflows
vertically since the swimlane layout — its scrollLeftMax was always 0, so the
guard never fired and users had to drop on an intermediate list and scroll by
hand. Edge-proximity auto-scroll now drives the .js-lists lane actually under
the pointer (clamped, overshoot-safe), with vertical scrolling kept on the
canvas. Tests: tests/boardAutoScroll.test.cjs (15, incl. a proof the old
no-overflow target could never scroll)
Board Rules: the Card Title Filter did nothing on several triggers — and rules
created via the REST API never fired at all
(#2345, #2674 ,
client/components/rules/triggers/boardTriggers.js, .jade,
server/rulesHelper.js, server/models/rules.js, new
models/lib/ruleCardTitleFilter.js, docs/API/Rules.md,
docs/API/REST-API.md, api.py): the generic moved/archive trigger builders
never saved the filter (and a trigger doc MISSING the field can never satisfy
the matcher's $in, so those rules fired for nothing); a set filter never
showed in the rule details; archive activities carry no card title so their
filters compared against undefined; and REST-created rules skipped the wildcard
defaulting entirely — the exact "remove user when moved away" rule from #2674
silently never ran. Filters are now stored (empty → *), shown in the rule
description, matched with the title resolved from the card when the activity
lacks it, legacy field-less triggers keep matching, the API normalizes missing
matching fields to wildcards and validates types, and the rule actions no longer
crash on unresolvable usernames or member-less cards. The Rules REST API is now
documented (docs/API/Rules.md) and listed in api.py's help with the #2674
two-rule example. Tests: tests/rulesCardTitleFilter.test.cjs (12) and
tests/rulesApiTriggerNormalize.test.cjs (19)
Sandstorm: username uniqueness probe was case-sensitive and raced concurrent
logins ( #574 , sandstorm.js, new models/lib/sandstormUsername.js):
deriving max, max1, … from the preferred handle matched exact case only (an
existing Max did not stop a new max) and the check-then-set window let a
concurrent insert claim the name first, aborting the hook with E11000. The probe
is now an anchored, escaped, case-insensitive regex and the claim retries the
next number on a duplicate-key loss. Tests: tests/sandstormUsername.test.cjs
(11)
Inviting a second user whose email shares a local part failed with a bare
"403" ( #619 , server/models/users.js, new models/lib/inviteeUsername.js):
inviting [email protected] creates user "cats"; inviting [email protected] then
crashed into Meteor's raw 403 Username already exists. The invitee's username
now probes cats, cats1, cats2, … to the first free variant, and exhaustion
raises the translated error-username-taken instead of a number. Tests:
tests/inviteeUsername.test.cjs (11, incl. the literal reported scenario)
Date pickers ignored the configured default time and stored 12:00 ( #1502 ,
client/lib/datepicker.js, new imports/lib/datePickerTime.js): the due-date
picker configures a 17:00 default (and now() for received/start/end), but an
inverted guard applied it only when the card ALREADY had a date — exactly when
it is unnecessary — so empty time fields fell back to a hard-coded 12:00 on
save. The default now pre-fills empty pickers and backs the submit fallback;
existing dates keep their own time. The issue's original AM/PM parse mismatch
was already resolved by the native date/time inputs (79b94824e). Tests:
tests/datePickerDefaultTime.test.cjs (10)
Dragging a card while someone added a card to the target list dropped it into
the WRONG swimlane ( #2769 , new client/lib/cardDragGeometry.js,
client/components/lists/listBody.js): jQuery UI sortable snapshots container
geometry at drag start; a mid-drag DOM insertion (another user's new card, or
the drag's own composer auto-close) shifted every swimlane below while the
cached rectangles stayed put — the drop landed in the neighbouring swimlane with
no visible placeholder. A MutationObserver now refreshes the active drag's
geometry on real mid-drag layout changes (sortable's own churn filtered out).
Tests: tests/cardDragGeometry.test.cjs (13)
Board import lost card dates ( #1992 , models/wekanCreator.js, new
models/lib/importedCardDates.js): the importer derived createdAt only from a
createCard activity (absent in Sandstorm/pruned exports — dates silently reset
to import time) and never imported receivedAt/endAt at all. All five date
fields now restore with sane fallbacks (activity → the card's own exported date
→ import time), and the card creator falls back to the exported userId. The
missing-cards half of the report was already fixed by 68e0032c6. Tests:
tests/importedCardDates.test.cjs (13)
Archiving a swimlane made its cards disappear — and restore brought back an
empty swimlane ( #2292 , models/swimlanes.js, new
models/lib/swimlaneArchive.js): only the swimlane document was flagged; its
unarchived cards became invisible everywhere (board views render unarchived
swimlanes, Archive lists archived docs). Archiving a swimlane now archives its
cards (mirroring lists), and restore brings back exactly the cards archived WITH
it — individually archived cards stay archived. Tests:
tests/archiveSwimlaneCards.test.cjs (11)
"Move/Copy selection to board" wrote sort: NaN to every card ( #2494 ,
imports/reactiveCache.js): the client-side noCache card lookup returned a
PROMISE since the Meteor 3 port, so the max-sort read was undefined and every
moved card got NaN — cards appeared and disappeared and could not be reordered.
The uncached client path is synchronous minimongo again. Tests:
tests/reactiveCacheNoCacheCard.test.cjs (8, incl. a proof the pre-fix routing
yields NaN)
Subtask "View it" did nothing when the subtask lives on another board ( #1853 , client/components/cards/subtaskViewHelpers.js, subtasks.js): the
original crash (board._id of undefined) had become a silent no-op guard — when
the deposit board is not in minimongo the button just did nothing. Navigation
now falls back to the subtask's own boardId (the route loads the board), and
truly broken subtasks warn instead of dying. Tests:
tests/subtaskViewNavigation.test.cjs (11)
Labels/members could not be dragged onto cards added after the board
rendered ( #1554 , client/components/lists/list.js): the
droppable-initializing autorun lost its reactive dependency in 7673c77c5 (2023),
so it ran once per list render and later-added minicards silently rejected
sidebar drags until the board was re-entered ("works after search-and-back").
Dependency restored via the ReactiveCache. Tests:
tests/labelDragDroppable.test.cjs (3)
"Add filtered cards to selection" swept cards from OTHER boards into bulk
actions ( #2306 , client/lib/filter.js, client/lib/multiSelection.js, new
models/lib/boardScopedSelection.js): the filter selector carried no boardId,
and minimongo legitimately holds foreign-board cards (linked boards, dialogs,
notifications) — a bulk archive could silently mutate other boards. The
selection and its bulk-action selector are now board-scoped, and foreign ids are
rejected at insertion. Tests: tests/boardScopedSelection.test.cjs (17, incl.
the exact reported repro)
REST API: login tokens could never be revoked ( #1437 ,
server/apiAuthRoutes.js, new models/lib/apiLogout.js): every POST /users/login minted another ~90-day resume token with no way to invalidate any
of them. New POST /users/logout revokes the presented token (or all of the
user's tokens with {"all": true}), always scoped to the authenticated user.
Tests: tests/apiLogout.test.cjs (12)
Editing one checklist item and clicking another left BOTH edit forms open —
and submitting overwrote the new item's title with the previous item's text ( #2418 , client/lib/inlinedform.js, new client/lib/inlinedFormManager.js):
since a 2021 change, the "close the previously opened inline form" call was a
silent no-op (the escape action is disabled for click execution), and the submit
handlers grab the template's FIRST textarea — with two forms open the wrong
form's text was saved. Subtasks reproduced the full bug; checklists' workaround
corrupted the open-form tracker so Escape closed the whole card pane. A small
state manager restores the single-open-form invariant (opening a form closes the
previous one, without closing popups — preserving the 2021 intent). Tests:
tests/inlinedFormSingleOpen.test.cjs (9, incl. the exact reported repro chain)
Advanced Filter never matched date custom fields ( #2989 ,
client/lib/filter.js, new imports/lib/advancedFilter.js): the tokenizer
treated every / as a regex delimiter even inside quotes, so 'Date de fin' == '06/04/2020' broke tokenizing and the filter silently did nothing; and date
custom fields store Date OBJECTS while the selectors compared strings/parseInt —
==/</> could never match and != matched everything. Dates typed in the
user's date format (day-first respected) now build half-open Date-range
selectors (== means "that day"), with the legacy behavior untouched for
non-date fields. Tests: tests/advancedFilterDate.test.cjs (14, incl. a proof
the old selector shape never matched a stored Date)
Cards could not be reordered by drag in lists full of subtask cards — with
silent data loss on multi-selection drops ( #3826 , server/models/cards.js,
client/components/lists/list.js, new models/lib/cardSortRepair.js):
addSubtaskCard inserted EVERY subtask card with the constant sort: -1, so
such lists contained only tied sorts; dropping between two equal sorts computes
a zero increment, the move modifier came out empty and the card snapped back —
and a multi-selection drop wrote the SAME sort to every selected card,
permanently destroying their order. Subtask cards now append with a unique sort,
and the drop handler detects degenerate (tied/inverted) gaps and repairs the
siblings' sorts to a strict order before recomputing the drop index. Tests:
tests/subtaskCardReorder.test.cjs (14)
Cross-board subtask full path disappeared after refresh ( #3453 ,
server/publications/boards.js, new server/lib/subtaskAncestors.js): the
board publication shipped only the DIRECT parent cards, while the full-path
label walks the whole ancestor chain client-side — after F5 the grandparents
were missing from minimongo and the path truncated/vanished. The publication now
walks and publishes the full ancestor chain (batched per level, cycle-safe,
tolerant of deleted ancestors). Tests: tests/subtaskAncestors.test.cjs (11)
Linked cards: phantom empty custom-field rows and a template TypeError on
every render (from #3748 , models/cards.js, new
models/lib/customFieldsWD.js): a linked card keeps the ORIGINAL board's
custom-field snapshot; unresolvable definitions rendered as empty {}
placeholders — a phantom row per entry and Cannot read properties of undefined (reading 'type') from the card details template (also reachable on normal cards
with deleted definitions). Unmatched entries are now skipped. The rest of #3748
is by design: linked cards are pointers that mirror the original;
label/custom-field ids are board-scoped, and name-based inheritance is the COPY
feature. Tests: tests/customFieldsWD.test.cjs (9)
Archive sidebar: Restore/Delete links floated ambiguously between two archived
cards ( #3199 , client/components/sidebar/sidebarArchives.jade,
sidebar.css): each card and its links were loose siblings with near-equal
spacing above and below, so the links seemed to belong to the card underneath.
Each archived card is now grouped with its own links in one container with a
clear separator gap below (RTL-safe logical properties, theme-neutral). Tests:
tests/archiveLinkGrouping.test.cjs (9)
Attachments uploaded inside card comments: listing in the card's Attachments
section is now guaranteed and regression-locked ( #3843 , new
models/lib/attachmentMeta.js, client/lib/utils.js): the rich comment editor
already uploads into the same Attachments collection with the same card meta as
the Attachments popup, so they DO list — but nothing pinned that invariant and
the meta was built in two places. One shared, null-safe builder now feeds both
paths, with tests pinning that gallery queries key on meta.cardId with no
source filter (and that board backgrounds stay excluded). Tests:
tests/commentAttachmentsList.test.cjs (12)
Maximized card rendered at the wrong place after scrolling in Swimlanes view
( #4822 , client/components/cards/cardDetails.css): the legacy maximized-pane
CSS had no position of its own, so the pane stayed an in-flow item inside the
scrolled board canvas (off-screen after scrolling down); the desktop-mode
floating-window rules also out-specified every maximize geometry rule, and
inline drag offsets survived maximizing. The maximized pane is now
viewport-fixed with explicit insets that beat both the floating-window rules
and stale drag offsets (drag position is restored on minimize), RTL-safe via
logical properties. A cascade-resolver regression test pins the behavior against
the real stylesheet and fails on the pre-fix CSS:
tests/maximizedCardPosition.test.cjs (11 tests)
LDAP group filter locked out admins and rejected multiple groups ( #4036 ,
packages/wekan-ldap/server/ldap.js): with LDAP_GROUP_FILTER_ENABLE=true,
only members of the single LDAP_GROUP_FILTER_GROUP_NAME group could log in —
an admin who was only in LDAP_SYNC_ADMIN_GROUPS could not log in at all, and a
comma-separated group list produced the literal filter (cn=A,B) that matches
nothing. The filter now ORs across every comma-separated group name and, when
admin sync is enabled, also admits the admin-sync groups
Board invitation emails could carry a dead invitation code — and signup then
failed with "The invitation code doesn't exist" ( #4043 ,
server/models/settings.js, server/models/users.js, new
models/lib/invitationCodeEmail.js): re-inviting an unregistered user re-sent
the SAME stale code (invalidated by an earlier OAuth2 signup or deleted account)
instead of regenerating it; a failed SMTP send deleted a previously delivered,
still-valid code; codes were mailed without checking they exist and are valid;
the invitee address was only lowercased client-side; and the signup hook deleted
the code BEFORE the account insert was committed, so a failed insert burned the
code for every retry. Re-invites now regenerate stale codes (still-valid ones
are kept so earlier emails keep working), sends fail loudly on unusable codes,
rollback only removes codes the failed send itself created, and consumption
happens only after a successful signup. Tests:
tests/invitationCodeEmail.test.cjs (14)
Japanese/Chinese UI: the add-card button and footer links wrapped mid-word ( #4023 , client/components/forms/forms.css): CJK text has no spaces, so the
narrow add-card composer footer broke 追加 / リンク / 検索 / テンプレート between any two
characters. The composer/edit footers now use word-break: keep-all with
flex-wrap: wrap (wrapping between links, never inside a word) and
white-space: nowrap on the button and each link group; Latin wrapping is
unchanged and the negative tests pin that no global word-break was introduced.
Tests: tests/cjkLabelWrap.test.cjs (9)
Users added to a team AFTER the team was assigned to a board never became
board members — and the Admin Panel bulk team add/remove silently did nothing
( #4593 , server/models/users.js, client/components/settings/peopleBody.js,
new models/lib/teamBoardMemberSync.js): assigning a team to a board
snapshotted its then-current members, so later joiners could see the board via
publications but every authority gate (hasMember, card/list mutations,
attachment downloads, export) denied them; and the Admin Panel "Add/Remove team
to selected users" used a direct client-side Users.update that server
permissions silently deny. editUser/createUser now add new team members to
all boards their teams are assigned to (never touching existing member entries,
skipping template boards), and the bulk actions go through the admin editUser
method. Tests: tests/teamBoardMemberSync.test.cjs (13)
LDAP background sync only worked once per user ( #4654 ,
packages/wekan-ldap/server/ldap.js, sync.js, new userIdFilter.js):
getUserById crashed or built the invalid filter (|(=user)) when
LDAP_UNIQUE_IDENTIFIER_FIELD was unset/empty (it never consulted
LDAP_USER_SEARCH_FIELD, where the stored id actually comes from), and the
username sync passed $set as query OPTIONS to findOneAsync — logging
"Syncing user username" while writing nothing. New shared buildUserIdFilter()
ORs across both configured fields, idAttribute is persisted on new LDAP users,
and the username sync actually updates. Tests: tests/ldapUserIdFilter.test.cjs
(11)
All Boards page: per-list card counts and member avatars never showed
(#5174, #4825 , new
models/lib/boardTileData.js, server/publications/boards.js,
client/components/boards/boardsList.js, .jade): the helpers were stubbed to
[] to stop the #4214 reactive "icons dance", and the gating flags were never
published. New non-reactive getAllBoardsTileData method (one boards query, one
lists query, one grouped card count) fetched once per page visit; per-board
"Show card count per list"/"Show Board members avatars" settings enforced
strictly both ways. Tests: tests/boardTileData.test.cjs (17)
Lists rendered with different widths by default ( #5659 , new
models/lib/listWidth.js, client/components/lists/list.js, listHeader.js,
models/users.js): the default width was duplicated in four resolution paths
that disagreed (270 vs 272), so lists on the same (public) board could differ
with no customization. Single source of truth (272), all paths normalize
out-of-range values the same way; customized widths still win. Tests:
tests/listWidthDefaults.test.cjs (12)
Declining a board invitation kept the board active in the member's overview
( #4730 , server/models/boards.js, new models/lib/boardInvites.js): the
decline flow called quitBoard (deactivate) then acceptInvite, which
unconditionally REACTIVATED membership — it also let any removed member re-add
themselves. quitBoard now clears the pending invitation (and works for
stale-invite-only users); acceptInvite only activates when an invitation
actually exists. Tests: tests/boardInvites.test.cjs (11)
After migrating a user from password to LDAP, the old local password still
logged in ( #4419 , new server/lib/ldapPasswordLoginGuard.js,
server/authentication.js): a validateLoginAttempt hook now rejects
password-service logins for authenticationMethod: 'ldap' users while LDAP is
enabled — respecting the LDAP_LOGIN_FALLBACK=true feature, never touching
other services or session resumes, and opt-out-able with
LDAP_MIGRATION_ALLOW_PASSWORD_LOGIN=true so no deployment is hard-locked.
Tests: tests/ldapPasswordLoginGuard.test.cjs (12)
LDAP_ENCRYPTION=true (the documented value) silently connected WITHOUT
encryption ( #4158 , new packages/wekan-ldap/server/encryptionSetting.js,
ldap.js, docs/Login/LDAP.md): only the undocumented ssl/tls values did
anything, and any other value (including true, which JSON-parses to a boolean)
meant silent plaintext. Now true→LDAPS, starttls→STARTTLS, legacy
ssl/tls keep their historical meanings with a deprecation notice, and
unknown values log a clear warning listing the accepted ones. Tests:
tests/ldapEncryptionSetting.test.cjs (22)
OIDC login onto an existing account wiped the profile — avatars and templates
disappeared ( #4560 , server/models/users.js): the
OAUTH2_MERGE_EXISTING_USERS merge path replaced the whole profile with the
OIDC-derived one, losing avatarUrl, templatesBoardId (+ template swimlanes),
language and preferences. The merge now preserves the stored profile and only
fills gaps/updates the asserted fullname; the fail-closed linking rules
(GHSA-mp7g-hj5q-gxhq) are untouched. Tests: tests/oidcProfileMerge.test.cjs
(7, fails on pre-fix code)
OAuth2/OIDC: concurrent logins could contaminate each other's user data —
users saw stale or missing emails/username/teams, and the database could
disagree with the UI ( #4897 , packages/wekan-oidc/oidc_server.js,
packages/wekan-oidc/loginHandler.js). The OIDC server flow kept profile,
serviceData and userinfo as MODULE-SCOPE variables shared by every login of
every user: fields the current login did not overwrite leaked from the previous
user's login (refreshToken, whitelisted id-token claims, branch-dependent
email), and because the handler awaits the token/userinfo requests, two
interleaved logins wrote into the SAME objects — a login could complete carrying
another user's id/email/username, updating the wrong user document. The
PROPAGATE_OIDC_DATA group/attribute path additionally ran on implicit GLOBALS
(teamArray, isAdmin, user_email, …) with awaits between assignment and
use, so concurrent logins could write one user's email/teams/admin flag onto
another user's document — real database corruption, matching the "web interface
shows different data vs mongodb" report. All login state is now per-login
locals, the login handler compares actual values (the old username/fullname
comparisons compared a string to an object, always true), and a regression test
proves isolation under concurrent logins and fails against the pre-fix code:
tests/oidcLoginStateIsolation.test.cjs (11 tests, positive + negative)
Snap/Docker: after the MongoDB → FerretDB migration ALL CollectionFS-era
attachments were missing ( #6473 , releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs,
snap-src/bin/migrate-gridfs-to-fs.mjs). Real CollectionFS (old WeKan's
FS.Store.GridFS('attachments')) stores each file's GridFS id at
copies.<bucket>.key (copies.attachments.key / copies.avatars.key) —
but the importers only looked at original.gridFsFileId, gridFsFileId and
copies.gridfs.key, none of which exist in that layout. So the modern importer
"skipped" every file silently and the MongoDB 3.x importer extracted the
binaries but never created an attachment record (and the records it did create
lost their meta.cardId/boardId, which CollectionFS keeps at the record's TOP
level — an attachment without meta.cardId shows on no card). Either way the
migration reported success with zero attachments visible. A new shared
resolveCfsGridFsId() resolves the id from all four layouts
(original.gridFsFileId, gridFsFileId, copies.<bucket>.key,
copies.gridfs.key, then any copies.*.key), the modern importer now drives
extraction from cfs_gridfs.<bucket>.files itself (so a missing/empty
cfs.<bucket>.filerecord collection no longer skips everything — binaries
without a filerecord are still extracted to disk), the mongo3 importer copies
the top-level boardId/cardId/listId/swimlaneId/ userId into meta,
and filerecords whose binary cannot be located are reported as errors on the
migration dashboard instead of being silently dropped. If you already migrated
and attachments are missing, run the migration again: snap run wekan.migrate
(the source MongoDB data was never modified). Behavioral positive/negative
tests: tests/migrationAttachmentExtraction.test.cjs
Snap/Docker: Meteor-Files attachments stored in GridFS without a storage: 'gridfs' flag were left pointing at a GridFS that no longer exists after
migration ( #6473 , releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-gridfs-to-fs.mjs). WeKan's own getFileStrategy serves a
version from GridFS when its storage flag says 'gridfs' or it carries a
versions.*.meta.gridFsFileId reference — those reference-only records worked
fine on MongoDB, but the migration's file phase only matched
versions.original.storage: 'gridfs', so their binaries were never extracted
and the record kept pointing into the void (404 after the switch). The record
scan now matches both forms (and every version, not just original), and a
second, bucket-driven sweep walks <bucket>.files by its metadata.fileId
back-reference (the way WeKan writes GridFS uploads), recovering binaries even
when the record's flags say nothing about GridFS. Any GridFS-flagged version
whose binary genuinely cannot be located is reported on the dashboard
Admin Panel > Attachments showed "/data" as the Filesystem Storage path on
every platform ( #6473 , client/components/settings/attachments.js,
client/components/settings/settingBody.js,
server/models/attachmentStorageSettings.js,
models/lib/attachmentStoragePath.js). The Blaze helpers computed the path from
process.env.WRITABLE_PATH in the browser, where process.env never has
it, so the page always fell back to "/data" — a path that does not exist on a
Snap install (the real path is /var/snap/wekan/common/files/attachments),
sending admins hunting for a directory that was never there. The client now asks
the server via a new admin-only getAttachmentStoragePaths method, whose
Snap-aware computation (shared, dependency-free
models/lib/attachmentStoragePath.js — WRITABLE_PATH already ends in /files
on Snap, /files is appended elsewhere) is also used for the settings
document's default filesystem path, which pointed at /data/attachments instead
of /data/files/attachments on Docker. Unit tests with negative cases:
tests/attachmentStoragePath.test.cjs
Snap: snap run wekan.database ferretdb looked like it re-ran the migration —
it only switches databases ( #6473 , snap-src/bin/wekan-database). Running
it while already on FerretDB printed "WeKan now uses FerretDB (SQLite)." and
users reasonably read that as "migration done" while their attachments stayed
missing. It now says when nothing was switched, states that the command does NOT
migrate data, and names the command that does: snap run wekan.migrate
FerretDB (SQLite) rejected documents with literal dotted field names, silently
dropping them during migration
(#6473, wekan/FerretDB
internal/types/document_validation.go). MongoDB has accepted documents with
literal . in field names since 3.6, and data migrated from a real MongoDB can
legitimately contain them — but FerretDB v1's document validation rejected every
such document ("invalid key: … (key must not contain '.' sign)"), and since
per-item migration errors are deliberately non-fatal (#6466), those documents
simply went missing. Fixed in the bundled wekan/FerretDB fork: dotted keys are
stored and round-tripped literally with MongoDB's own semantics (query/update
paths still treat . as a path separator), while the other key rules ($
prefix, duplicates, UTF-8) still reject. Verified end-to-end against a live
FerretDB (SQLite): insert, nested $set, round-trip, and the negative cases
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following updates:
<details> <summary>Releases now include the Sandstorm .spk package. Thanks to xet7.</summary>Releases now include the Sandstorm .spk package
(.github/workflows/release-all.yml). The release-all workflow got a new
build-sandstorm job 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 attaches wekan-<version>-sandstorm.spk to 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.
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-path mongod invocation. The new cpu-exec helper is one general
mechanism for all scripts, sandboxes, platforms and CPUs: cpu-exec --features x86_64=avx,aarch64=atomics <binary> [args] checks /proc/cpuinfo and, 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_FEATURES declares 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=atomics covers MongoDB's ARMv8.2-A requirement (Raspberry Pi 4 and
older lack it). release-all.yml now ships cpu-exec plus 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
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), and tests/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 into test:unit:node in package.json; plus Go table
tests in the wekan/FerretDB fork's internal/backends/sqlite/query_test.go and
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 so
TestOtelComment passes — 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 extracted
normalize/normalizeTrigger functions; 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 _id and top-level equality filters, and that dotted paths,
operators, non-strings and unsafe strings stay with the in-Go filter.
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/issues/6456">Subtasks: creating a subtask crashed with "Exception while invoking method 'addSubtaskCard'"</a>. Thanks to sjohnen and xet7.</summary>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 SYNC
Boards.insert / Swimlanes.insert / Lists.insert / Boards.update APIs,
which Meteor 3 removed from the server — so the addSubtaskCard method'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 read getDefaultSwimline()._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 in
getDefaultSubtasksBoardAsync/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 to models/boards.js
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) return
false (upstream returns true), so a tag whose inline text compiles to an
ARRAY — the label {{_ 'source-board'}}: and label {{_ '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 a rootUrl helper that does not exist
(now absoluteUrl). Board and swimlane menus intentionally offer archive
(delete lives in Sidebar → Archive), so only the card menu was actually broken
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
(or null when 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
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
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
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 on list.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 made Math.min()/Math.max() of
nothing write a corrupt sort: ±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.createRule server method as the
wizard, and defaults missing trigger matching fields (e.g. userId) to the *
wildcard so hand-written JSON matches; (5) rules.createRule let an empty
boardId: '' from a not-yet-loaded board selector override the real board. Also
fixed the server-side orphaned-cards fallback in
List.cards()/cardsUnfiltered(), which silently never applied because an async
lookup was read synchronously
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 (the clamp(...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
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
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, set migrate=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 endless db-eval.mjs ping loop 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
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 to METEOR_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
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 proper escapedToHex hex escaping. It turned already-correct
RFC 4515 escapes like \5c and \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 (escapedToHex
still hex-escapes the username)
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 nested
Meteor.callAsync inherits the current method invocation's connection, so
when the login handler called the setUserOrgsTeamsFromLdap method, its admin
guard saw the client's login connection 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 from addLdapUser at the
right point (it was previously used as a user object first), fixes
external-avatar localization to use Avatars.writeAsync (the callback-style
write no longer exists in ostrio:files 3.x, so localizing avatars silently did
nothing), and adds */? wildcard support with unit tests to the
LDAP_SYNC_ORGANIZATIONS_GROUPS / LDAP_SYNC_TEAMS_GROUPS allowlists
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/6457">FerretDB: WeKan did not work after migrating to FerretDB — every logged-in browser was logged out…</a> Thanks to markusst1982 and xet7.</summary>FerretDB: WeKan did not work after migrating to FerretDB — every logged-in
browser was logged out again and the database CPU was pinned at 100% ( #6457 ,
server/accounts-resume-login.js, server/imports.js). Meteor's
accounts-base "resume" login handler projects one array element with the
positional operator ({fields: {'services.resume.loginTokens.$': 1}}), and
FerretDB rejects that find with "Executor error during find command :: caused
by :: positional operator '.$' couldn't find a matching element in the array"
(code 51246). Resume is how an already-logged-in browser re-authenticates on
every page load and every DDP reconnect, so the throw logged the user out,
the client reconnected, resume threw again, and the retry loop pinned the
FerretDB CPU — the board looked broken right after a migration that had just
taken hours. WeKan now replaces that login handler with one that projects the
whole (small) loginTokens array and picks the matching token in JavaScript —
which is what upstream already does in its own $or fallback query, so nothing
else about login behaviour changes
Snap: a snap refresh during the MongoDB → FerretDB migration threw away hours
of migration progress (snap-src/bin/migration-control,
releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs). A big migration can run for 5
hours, so being interrupted by a snap refresh, snap stop or a reboot is
normal — but it was treated as a failure: snapd's SIGTERM killed the importer,
migration-control read its signal exit code (143) as "the migration failed",
and fail_and_run_mongodb deleted the partial FerretDB SQLite and set
migrate=off. Every refresh meant starting the whole migration from zero. Now
an interruption is distinguished from a failure (a SIGTERM/SIGINT trap, plus an
importer exit code >= 128) and keeps the partial database and the
checkpoint, leaves auto-migration on, and hands back to MongoDB so WeKan keeps
working until the next start resumes.
Snap: an interrupted migration re-extracted every attachment, needing double
the disk space (releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs). Only fully-copied collections
were checkpointed; the file phase started over, and because the destination path
was picked with a "add a _1, _2, … counter while the file exists" loop,
every already-extracted attachment was written a second time under a new
name, orphaning the first copy — so a resumed migration silently needed twice
the disk the up-front space check had budgeted for. Extracted files are now
checkpointed individually (recorded only once the bytes are on disk and the
record points at them, and re-verified by size on resume, so a half-written file
is never mistaken for a finished one) and skipped when resuming, and the
destination path is deterministic — an existing file at that path can only be
this record's own partial extraction, so it is overwritten. The MongoDB 3
importer had no checkpoint at all and now resumes collections and files the same
way.
Snap: discarding a partial FerretDB migration left the resume checkpoint
behind, so the retry could switch to a database missing most of its data
(snap-src/bin/migration-control). The importer's checkpoint lists the
collections it has already copied and lives in $SNAP_COMMON, not in the
SQLite directory that discard_partial_ferretdb wipes — so it survived. The
next migration then trusted it, skipped every "already migrated" collection,
copied only the rest into the now-empty database, and reported success — leaving
the snap serving a FerretDB missing most of its data. The checkpoint is only
meaningful together with the SQLite it describes, so the two are now always
discarded together.
Snap: releases are now published to the Snap Store stable channel
automatically (.github/workflows/release-all.yml). Both snap jobs (native
and Launchpad) pushed only to candidate, beta and edge, and stable had
to be released by hand afterwards. Both now publish to
stable,candidate,beta,edge.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
<details> <summary>Snap: the database setting is now authoritative, with a snap run wekan.database mongodb|ferretdb… Thanks to xet7.</summary>Snap: the database setting is now authoritative, with a snap run wekan.database mongodb|ferretdb command and no-downtime handling of a failed
migration (snap-src/bin/wekan-database, snap-src/bin/wekan-control,
snap-src/bin/mongodb-control, snap-src/bin/ferretdb-control,
snap-src/bin/migration-control, snap-src/bin/migration-pending). Previously
the control scripts force-switched to FerretDB whenever a *.sqlite file
existed, so snap set wekan database=mongodb would not stick and there was no
way to keep WeKan on MongoDB while fixing a migration — a failed migration meant
downtime. Now the database setting decides which database WeKan runs on, and
snap run wekan.database mongodb switches WeKan to MongoDB (and pauses
auto-migration, migrate=off) while snap run wekan.database ferretdb
switches to the migrated FerretDB. Auto-migration can be paused with snap set wekan migrate=off and, crucially, a migration that FAILS now pauses itself
and hands back to MongoDB automatically (migration-control
fail_and_run_mongodb) so WeKan keeps working on MongoDB instead of
retrying-and-failing every start; re-run it with snap run wekan.migrate. A
successful migration also restarts wekan.wekan so it reconnects to
FerretDB. The only remaining auto-override is the safety guard that refuses to
start an empty FerretDB while MongoDB still holds data.
and fixes the following bugs:
<details> <summary>Snap: the MongoDB → FerretDB migration failed on GridFS collections and left a half-migrated… Thanks to xet7.</summary>Snap: the MongoDB → FerretDB migration failed on GridFS collections and left a
half-migrated database behind (releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs,
snap-src/bin/migration-control). FerretDB v1 rejects collection names
containing a dot ("invalid key: 'attachments.chunks' (key must not contain '.'
sign)"), and the importers tried to copy the GridFS internals collections
(attachments.chunks, attachments.files, avatars.*, cfs_gridfs.*) and the
CollectionFS cfs.<bucket>.filerecord collections as text, so the migration
aborted. Now the text phase skips every dotted collection — none of WeKan's
real data collections contain a dot, and the dotted ones are all GridFS
internals (extracted in the file phase), CollectionFS filerecords (turned into
bare attachments/avatars records in the file phase, now created with an
upsert) or system.* collections. And crucially, when a migration fails
partway it now deletes the partial FerretDB SQLite it wrote
(migration-control's discard_partial_ferretdb): otherwise that
non-empty-but-incomplete files/db/wekan.sqlite looked "migrated" to the data
check, so the snap disabled MongoDB and tried to serve an incomplete FerretDB.
Now a failed migration cleanly keeps MongoDB and retries.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
<details> <summary>Snap: new snap run wekan.migrate command to force a fresh MongoDB → FerretDB migration. Thanks to xet7.</summary>Snap: new snap run wekan.migrate command to force a fresh MongoDB → FerretDB
migration (snap-src/bin/wekan-force-migrate, registered in
snapcraft.yaml). Ignores the database setting and the migration marker,
removes any partial FerretDB SQLite + resume checkpoint, then re-runs the
migration so it re-reads the existing MongoDB (3 or 7) data and migrates text +
attachments + avatars to FerretDB again. The source MongoDB data is never
modified or deleted. Watch it with snap logs -f wekan.mongodb or the migration
dashboard.
Snap: new maintenance mode (snap run wekan.maintenance on|off) so you can
run MongoDB OR FerretDB by hand for data access
(snap-src/bin/wekan-maintenance, snap-src/bin/wekan-maintenance-page.mjs,
and the DB control scripts). While the $SNAP_COMMON/.wekan-maintenance marker
is present, the control scripts do not auto-migrate or auto-disable, so
snap start wekan.mongodb (old MongoDB data) or snap start wekan.ferretdb
(migrated FerretDB SQLite data) stays up instead of shutting itself down —
letting you reach the data over the MongoDB wire protocol on port 27019 (one at
a time; they share the port). WeKan itself serves an "under maintenance" page
(HTTP 503) on the web port for all URLs while maintenance is on, so end users
see a clear message. The page shows the Admin Panel product name if one is
set (not "WeKan"): it is cached to $SNAP_COMMON/.productname.txt while a
database is running (by wekan-control on startup and by the migration
importers), so it is still available in maintenance mode when both databases are
stopped.
and fixes the following bugs:
<details> <summary>Snap: db-eval could not load the MongoDB driver, so EVERY database readiness check silently failed. Thanks to xet7.</summary>Snap: db-eval could not load the MongoDB driver, so EVERY database readiness
check silently failed (snap-src/bin/db-eval.mjs; also
migrate-schema-v843.mjs, migrate-gridfs-to-fs.mjs). db-eval is the small
Node helper the snap uses (instead of mongosh) to check whether
MongoDB/FerretDB is up, elect the replica-set primary, and run the migration's
mongod-7 readiness probe. It did import { MongoClient } from 'mongodb' and the
wrapper set NODE_PATH=$SNAP/programs/server/node_modules to point at the
bundled driver — but Node's ESM loader ignores NODE_PATH (that env var is
honored only by the CommonJS loader). So the import resolved nothing, db-eval
exited before ever opening a connection, and every ping/primary/rs-*
check "failed." This was the single root cause behind a whole family of
symptoms: WeKan looping "MongoDB not ready yet, retrying…" or "FerretDB not
ready yet…" forever even though the database was running and listening;
replica-set initialisation failing; and — most damaging — the MongoDB → FerretDB
migration falling back to MongoDB 3.2 because the mongod-7 readiness probe
never connected (mongod 7 opened the data fine, but db-eval couldn't reach it,
so migration-control wrongly concluded "mongod 7 can't open this" and tried the
3.2 reader, which cannot read WiredTiger-7 files — producing no FerretDB
SQLite). Proven by the migration source mongod log: mongod 7 reached Waiting for connections with zero Connection accepted before being killed by the
readiness timeout. Fixed by resolving the driver with createRequire (CommonJS,
which does honor NODE_PATH and the bundle layout), anchored inside the
modern bundle.
Snap: the presence of a FerretDB SQLite database — not the migration marker —
now decides whether to use FerretDB, so a FAILED migration no longer leaves the
snap with no database (snap-src/bin/ferretdb-control,
snap-src/bin/mongodb-control, snap-src/bin/wekan-control). An earlier
attempt keyed the "use FerretDB / disable MongoDB" decision off the
$SNAP_COMMON/.migration-to-ferretdb-done marker. But migration-control also
writes that marker when the migration falls back (data unreadable, tools
missing) or when there is nothing to migrate — with no FerretDB data
produced. So on a server whose MongoDB → FerretDB migration failed,
mongodb-control saw the marker, logged "migration already finished; disabling
mongodb service" and disabled MongoDB, while FerretDB's SQLite was empty —
leaving WeKan with no database at all, looping "MongoDB not ready yet,
retrying…" forever and refusing to keep wekan.mongodb running. Now all three
scripts decide from actual data on disk: MongoDB is disabled / FerretDB is
forced only when a *.sqlite database exists in files/db; an empty
files/db means the migration did not succeed, so MongoDB starts normally and
WeKan keeps working while the migration can be retried. The check is a single
shared helper (snap-src/bin/ferretdb-has-data) that requires a *.sqlite file
bigger than 0 bytes, not merely present, so a 0-byte stub never counts as
"migrated". And migration-control's finish_success now verifies that
non-empty SQLite (with its WAL) exists before switching to FerretDB — if the
importer returns success but wrote no data, the snap keeps MongoDB and retries
next start instead of switching to an empty database.
Snap: the migration's mongod-7 readiness probe fast-fails when the temporary
mongod has exited (snap-src/bin/migration-control). ready_via_node waits
up to 45 s for the temporary source mongod to accept connections (a large
MongoDB 6/7 database can need that long to replay its journal, #6454), but it
now also checks the process is still alive: if the temp mongod has exited — e.g.
mongod 7 cannot open old MongoDB 3.x data — it stops waiting immediately and
drops to the MongoDB 3.2 reader instead of pinging a dead process for the full
timeout. Matters across many unattended 6.09 → 9.x upgrades.
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/6454">Snap: WeKan never started on a running MongoDB, looping "MongoDB not ready yet, retrying in 5…</a> Thanks to xet7.</summary>Snap: WeKan never started on a running MongoDB, looping "MongoDB not ready
yet, retrying in 5 seconds..." forever, on any server with less than ~34 GB
RAM ( #6454 , snap-src/bin/mongodb-control,
snap-src/bin/migration-control). The WiredTiger cache size was hardcoded to
32 GB. On a smaller box (e.g. 8 GB) mongod was OOM-killed seconds after it
started — including the temporary mongod that mongodb-control starts to
initialise the replica set, which died before it could be reached, so the
replica set was never initiated. The final mongod then ran with --replSet rs0 but no config, no PRIMARY was ever elected, and since Meteor requires
the replica-set primary (change streams), wekan-control waited on it forever.
Fixes: (1) the cache size is now RAM-aware — ~50 % of (RAM − 1 GB), the same
formula mongod uses for its own default, capped at 32 GB, min 1 GB, overridable
with MONGODB_WIREDTIGER_CACHE_GB; (2) the temporary-mongod readiness wait is
raised from 30 s to 90 s so a large database that needs longer to replay its
journal still gets its replica set initialised; and (3) the temporary source
mongod that the MongoDB → FerretDB migration starts to read the data now uses
a conservative RAM-aware cache (¼ of (RAM − 1 GB), cap 8 GB) so it leaves room
for the target FerretDB and the importer running alongside it
Snap: the MongoDB 6/7 → FerretDB importer now resolves the correct driver and
matches the MongoDB 3.2 importer's safety and dashboard
(releases/migrate-mongodb-to-ferretdb.mjs). It now (a) resolves the mongodb
driver by probing the WeKan bundle and using whichever actually works —
preferring a v6+ driver that exposes GridFSBucket and speaks OP_MSG, because
an older v2 driver's OP_QUERY is rejected by FerretDB ("Unsupported OP_QUERY
command: update"); and (b) gained the same disk-space guard and progress UI as
migrate-mongo3-to-ferretdb.mjs: it measures the total attachment/avatar size
up front and stops before extracting if it will not fit, and if space runs
out mid-run it stops, deletes the partially-migrated files, and reports how
much more disk is needed (a full disk can corrupt the still-running source
MongoDB), shows a live per-file progress bar, and translates the dashboard
into the browser's language.
Snap: verified the migration's disk-space guard actually works inside snap
confinement (snap-src/bin/migrate-mongo3-to-ferretdb.mjs,
releases/migrate-mongodb-to-ferretdb.mjs — comment/documentation only, no
behaviour change). statfs (and statfs64 / fstatfs / fstatfs64 /
statvfs / fstatvfs) is in snapd's default seccomp allow-list
(snapcore/snapd → interfaces/seccomp/template.go), and a snap has no
per-snap disk quota by default, so the migration's free-space check
(statfsSync on $SNAP_COMMON/files) returns the real free space and its "stop
before the disk fills" safety is fully active on Snap. quotactl is not
allowed, but the migration never calls it; the one case it would matter — a
snapd storage-quota group (project quotas invisible to statfs) — is still
caught by the ENOSPC/EDQUOT write-failure fallback. Only Sandstorm grains
genuinely lack statfs (FUSE does not implement it, quotactl blocked), where
the guard already relies on that write-failure fallback.
Snap: after a MongoDB → FerretDB migration, FerretDB never started and WeKan
was stuck on "MongoDB not ready yet, retrying..."
(snap-src/bin/migration-control). The wekan.ferretdb service disables itself
while database is mongodb; the migration then switches the snap with an
internal snapctl set database=ferretdb, but an internal snapctl set does
not re-run the configure hook that flips the two database services — so
FerretDB was left disabled and MongoDB enabled but stopped, leaving WeKan
with no database (both bind the same port, so only one runs at a time).
finish_success now performs the flip itself, exactly as the configure hook's
ferretdb branch does: enable + start (and restart) wekan.ferretdb, then stop +
disable wekan.mongodb — using the snap instance name so parallel snaps
target their own services. Immediate recovery on an already-migrated install —
flip the setting so the configure hook re-runs and enables + starts FerretDB
and stops MongoDB (do not try snap start wekan.ferretdb directly; while
database is still mongodb, ferretdb-control reads the setting and disables
itself again — see the next entry): sudo snap set wekan database=ferretdb.
Snap: the migration-success marker is now authoritative, so FerretDB starts
even if the database setting was never flipped
(snap-src/bin/ferretdb-control, snap-src/bin/mongodb-control,
snap-src/bin/wekan-control). Every DB service keyed its behaviour off the
database setting alone: ferretdb-control logged "database is 'mongodb', not
'ferretdb'. Disabling ferretdb service." and self-disabled whenever the setting
still said mongodb — so on an already-migrated install snap start wekan.ferretdb started and then immediately stopped itself, unfixable by hand
without first setting database=ferretdb. Now the marker
$SNAP_COMMON/.migration-to-ferretdb-done overrides the setting: when present,
ferretdb-control repairs database=ferretdb and keeps running instead of
self-disabling; mongodb-control disables itself (before the migration-pending
check, so a finished migration is never re-attempted); and wekan-control
forces ferretdb and brings the service up. WeKan thus recovers on its own after
a migration whose setting flip was lost.
Snap: WeKan now starts its database itself on startup instead of waiting
forever for a stopped one (snap-src/bin/wekan-control). Previously the
wekan.wekan service only waited for whichever database database pointed at
(FerretDB not ready yet… / MongoDB not ready yet…) and never started a DB
service, so if both wekan.ferretdb and wekan.mongodb were stopped/disabled
WeKan hung indefinitely. Now, before waiting, it enables + starts whichever DB
service is configured (snapctl start --enable = snap enable + snap start,
targeting the snap instance name so parallel snaps hit their own services).
Snap: WeKan never starts an EMPTY FerretDB while data still lives in MongoDB
(snap-src/bin/wekan-control). On startup WeKan now checks what data actually
exists on disk — the FerretDB SQLite dir ($SNAP_COMMON/files/db) and the
MongoDB WiredTiger data ($SNAP_COMMON) — instead of trusting the database
setting alone. If database=ferretdb was selected (or forced) but the migration
never ran, so the SQLite database is empty while MongoDB still holds the data,
WeKan reverts to database=mongodb and prints how to run the migration,
rather than starting an empty FerretDB that would look like all data was lost.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
<details> <summary>Snap / Sandstorm migration dashboard: live per-file progress and a disk-space safety guard. Thanks to xet7.</summary>Snap / Sandstorm migration dashboard: live per-file progress and a disk-space
safety guard (snap-src/bin/migrate-mongo3-to-ferretdb.mjs). While a MongoDB
3 database's attachments/avatars are migrated to the filesystem, the progress
page now shows a live per-file progress bar for the file currently being
extracted — its name, size, "file N of TOTAL", percent, and whether it is an
attachment or an avatar (using WeKan's existing translations in the viewer's
browser language, with an English fallback; there is no logged-in user during
migration, so the browser's Accept-Language is used). Big files no longer land
in RAM: each GridFS file is streamed chunk-by-chunk straight to disk (the
old approach buffered the whole file through mongoexport's 512 MB stdout limit
and failed on large attachments). Because a full disk can corrupt the
still-running source MongoDB, the migration guards disk space where it can
measure it (Snap with a working statfs): it shows remaining disk space,
checks the total size of all files up front and stops before extracting if
the volume cannot hold them, and stops mid-run if free space drops below a
safety margin (default 1 GB, MIGRATION_MIN_FREE_BYTES). A Sandstorm grain
cannot see its own free space or quota (its FUSE layer does not implement
statfs and quotactl is blocked — statfs on /var would report the host
disk, not the grain quota), and a locked-down Snap container may not report it
either; in those "unknown free space" cases the migration hides the space
figures and instead treats an actual ENOSPC/EDQUOT write failure as "out
of space". On any such stop it deletes the partially-migrated files to
free the space back, reports how many files were migrated before stopping, and
shows how much more disk space is required to migrate them all. Platform is
detected from the environment ($SNAP; SANDSTORM / SANDSTORM_RAW_MONGO_PATH
/ METEOR_SETTINGS).
and adds the following updates:
<details> <summary>i18n: 10 more selectable languages, corrected native names, and a Transifex-pull safety report. Thanks to xet7.</summary>i18n: 10 more selectable languages, corrected native names, and a
Transifex-pull safety report. Added languages.js picker entries for
translation files that were pulled from Transifex but had no entry (so they were
never selectable): Català (Valencià), English (Indonesia / Singapore / Turkey),
Español (Colombia), Français (France), Português (Portugal), Русский (Украина),
Türkmençe (Türkmenistan) and 吴语(简体). Fixed language names that showed the
English name instead of the native one: Welsh → Cymraeg (and cy-GB →
Cymraeg (Y Deyrnas Unedig)), Acehnese → Bahsa Acèh, and Afrikaans (South
Africa) → Afrikaans (Suid-Afrika). Also,
releases/translations/pull-translations.sh now runs a new
report-english-regressions.mjs after tx pull that lists any language file
where a previously-translated string reverted to the English source
(untranslated on Transifex), so the regression is visible instead of committed
silently.
and fixes the following tests:
<details> <summary>Test / dev infrastructure: starting a dev server now also frees the MongoDB port, not just the app… Thanks to xet7.</summary>Test / dev infrastructure: starting a dev server now also frees the MongoDB
port, not just the app and rspack ports (build.sh, kill_meteor_on_port).
Picking a "Run Meteor for dev" option stops any server already on the app port,
but it only freed the app port (3000) and the rspack dev-server port (8080) —
not Meteor's bundled MongoDB on app-port+1 (3001). When the previous meteor
parent is SIGKILLed its mongo child is often orphaned and keeps holding 3001, so
the new meteor run died with Unexpected mongo exit code 48 ... port was closed, or was already taken. The stop step now also frees app-port+1 (which
additionally clears a leftover standalone test mongod on :3001) and waits for
all three ports before starting.
and fixes the following bugs:
<details> <summary>Snap: MongoDB 3 → FerretDB migration failed with "EJSON.parse unavailable", and the progress…</summary>Snap: MongoDB 3 → FerretDB migration failed with "EJSON.parse unavailable",
and the progress dashboard should stay on the page you were on
(snap-src/bin/migrate-mongo3-to-ferretdb.mjs). The migrator reads the 3.2
source with the legacy CLI and inserts into FerretDB with the modern Node
driver, which needs WeKan's current bson (with EJSON) and mongodb v6
(OP_MSG). It anchored those requires at paths relative to the script — but in
the snap the script runs from $SNAP/bin/ while the bundle is at
$SNAP/programs/server/… (one level up), so every anchor resolved
$SNAP/bin/programs/server/… (nonexistent) and fell through to the ancient
meteor-spk base bson 1.x that has no EJSON → FATAL: EJSON.parse unavailable
and the migration aborted. Now it builds candidate bundle roots from $SNAP
(env) and the script's parent directories and searches the known modern-bundle
sub-paths under each (npm-mongo's nested v6 driver first, so the ancient v2 that
FerretDB rejects with "Unsupported OP_QUERY" is never picked). The migration
progress dashboard already answers on every URL (so reloading any board page
during migration shows progress); on completion it now reloads the same page
you were on instead of forcing All Boards. (The dashboard itself also gained
live per-file progress and a disk-space safety guard — see the new features
above.) Thanks to xet7.
Admin Panel / Features / Security: "Always show all code as plain text" did
not take effect (links stayed clickable, code stayed rendered). The setting is
applied by the inner markdown helper, which reads a ReactiveVar
(Markdown.alwaysShowCodeAsText) that only a separate startup autorun kept in
sync. But the mentions viewer wrapper re-renders whenever the settings doc
changes (it reads it for "render links as plain text"), and that re-render
usually ran BEFORE the startup autorun updated the ReactiveVar — so the markdown
helper read the stale value and rendered normally, and because mentions does
not depend on that ReactiveVar it never re-rendered again (the race persisted
even after reload). Fixed by setting the flag inside the mentions helper, from
the same reactive getCurrentSetting() it already reads, right before it
renders the inner markdown — so the toggle now takes effect immediately in every
rich-text field (card titles, descriptions, comments, checklists).
i18n: several languages fell back to English (or clobbered another language) because a browser language string did not map to the right translation file. Fixes:
</details>ja_JP overwrote ja. The Transifex .tx/config lang_map
had ja_JP: ja, writing Transifex's ja_JP into
imports/i18n/data/ja.i18n.json — the same file the real Japanese (ja)
uses — so tx pull -a -f reverted Japanese to the English source. Mapped to
its own file (ja_JP: ja-JP); ja, ja-JP and ja-Hira are now three
separate files, matching languages.js.isLanguageSupported and a new TAPi18n.resolveTag() map any
input to the canonical tag (zh-hant → zh-Hant, JA-JP → ja-JP,
browser af-ZA → legacy key af_ZA);
loadLanguage/setLanguage/ensureLanguageLoaded resolve through it so
the loaded file, the i18next code and the stored current tag all agree.zh-Hans-CN / zh-Hant-TW (and bare zh) fell back to
English. Browser detection now strips trailing subtags progressively
(zh-Hans-CN → zh-Hans, zh-Hant-TW → zh-Hant) instead of jumping to
the first segment, and bare zh is aliased to zh-Hans (Simplified). Each
Chinese variant (zh-CN, zh-TW, zh-HK, zh-Hans, zh-Hant, zh-SG)
maps to its own file.cmn) was unselectable — its tag was the typo cnm (and
code cn), which mismatched supportedLngs; fixed to cmn.Regression tests added in imports/i18n/i18n.test.js. Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUE of MimeBleed:
<details> <summary><a href="https://github.com/wekan/wekan/security/advisories/GHSA-jhph-whx8-wq6p">MimeBleed: file-upload MIME-type validation bypass → stored XSS on deployments without the file…</a></summary>MimeBleed:
file-upload MIME-type validation bypass → stored XSS on deployments without the
file binary ( GHSA-jhph-whx8-wq6p , CWE-434 Unrestricted Upload of File with
Dangerous Type). WeKan's upload validation (models/fileValidation.js) detects
a file's real MIME type by running the Unix file command. On minimal
Docker/Alpine images where file is not installed, detectMimeFromFile()
silently returned undefined and the code fell back to the client-supplied
fileObj.type. An authenticated board member (with WITH_API=true) could
therefore upload an HTML file containing JavaScript while setting fileType: "image/png": the spoofed type is not on the dangerous-MIME deny-list, so the
dangerous-content scan was skipped and the file was stored, yielding stored XSS
served under the WeKan origin (session theft / actions as the victim, including
admin)
file is unavailable, WeKan now falls back to a
dependency-free JS content sniff (looksLikeDangerousMarkup()) that
inspects the real bytes for HTML/SVG/XML/<script> signatures and forces
the dangerous-content scan regardless of the claimed MIME — so a spoofed
image/png that is actually HTML+JS is caught and rejected. The sniff only
matches definitive markup signatures, so genuine binary uploads (real
PNG/JPEG/PDF, including large ones) are unaffected. WeKan also now logs a
one-time warning when the file command is missing (previously the failure
was silent). A regression test
(server/lib/tests/fileValidationBypass.security.tests.js) covers both the
spoofed dangerous uploads and safe binaries. CVSS:3.1 8.3 High
(AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N).file binary and
WITH_API=true; fixed at the upcoming WeKan release. Reported by
HNUfwj. Also install the file package for full content-based MIME
detection (WeKan's official images already do).
Thanks to HNUfwj and xet7.and adds the following new features:
Admin Panel / Features / Security: import/export privacy controls. Six new optional toggles govern how boards and user data cross the WeKan boundary:
localizeAvatarFromBuffer choke
point; export covers WeKan JSON and CSV export.@username mentions plus the
requested-by / assigned-by fields inside card and comment content, so the
imported board / exported file carries no real user identity. The
placeholder word "user" follows the language of the person
importing/exporting (e.g. "käyttäjä1" in Finnish). Both export paths are
covered — the in-memory build() and the streaming buildStream() (which
does a lightweight id-only pre-scan so mentions streamed before the users
array still resolve to matching labels).All six default to off (current behaviour). Enforcement lives server-side in the Exporter, the WekanCreator import path and the avatar localizer, so it cannot be bypassed from the client. Thanks to xet7.
Admin Panel / Features / Security: new optional "Always show all code as plain
text" toggle. When enabled, rich text is never rendered as markdown or HTML —
the entire source is shown as escaped plain text in every rich text field (board
and card titles, descriptions, comments, checklists, etc.), so hidden content is
always revealed: HTML comments (<!-- -->), the target URL inside a markdown
link, JavaScript and any other code. All code is always visible, not clickable,
and not running. This extends the existing invisiblebleed protection (which
already showed raw source for description-less markdown links) to all content.
The wekan-markdown package cannot import app code, so the setting is bridged
to the markdown renderer through a reactive flag on the exported Markdown
object, kept in sync by the rich text viewer. Stored as the global
alwaysShowCodeAsText setting; default off, so markdown renders normally.
Admin Panel / Features: new optional "Render links as plain text" security
toggle. When enabled, all links — both markdown links like [label](url) and
raw HTML <a href> tags — are always shown as plain, non-clickable text in
every rich text field (board and card titles, descriptions, comments,
checklists, etc.), so a link can never be clicked and cannot present misleading
anchor text. This is a hardening option on top of the existing XSS sanitization
(which already strips javascript:/data: schemes, event-handler attributes
and dangerous tags): it addresses #6453 , where a board title could render as a
clickable link. The toggle lives under Admin Panel / Features / Security, is
stored as the global renderLinksAsPlainText setting, and defaults to off
(links stay clickable). Implemented by forbidding the <a> tag (while keeping
its visible text) in the shared DOMPurify sanitizer used by the rich text
viewer, gated on the setting so toggling it re-renders reactively
and adds the following updates:
<details> <summary><a href="https://github.com/wekan/wekan/issues/5143">Outgoing webhooks / notifications: include the card description</a>. Thanks to xet7.</summary>Outgoing webhooks / notifications: include the card description ( #5143 ). A
card's description was saved to the activity log (a a-changedDescription
activity, #5482) but was never put into the outgoing-webhook / notification
payload, so integrations received an event with no description text. The payload
now carries description (the card's current description, added only when
non-empty) for every card event, and — for a description change — the
before/after text as oldValue / value (previously only
timeValue/timeOldValue were forwarded). New fields are additive, so existing
webhook consumers are unaffected
and fixes the following tests:
<details> <summary>Test infrastructure: fix meteor test (Mocha, server-side) crashing at boot with 0 tests run.</summary>Test infrastructure: fix meteor test (Mocha, server-side) crashing at boot
with 0 tests run (scripts/patch-yargs-dirname.cjs). The whole yargs
package is dragged into the Meteor test server bundle as a transitive
devDependency, and Meteor 3 wraps every module in a CommonJS function
function(require, exports, module, __filename, __dirname){…}. yargs (and its
ESM dependencies) ship native-ESM constructs that are illegal inside that
wrapper and crash the bundle before a single test runs:
import.meta.url / import.meta.resolve(…) → "Cannot use 'import.meta'
outside a module"const __dirname = … → "Identifier '__dirname' has already been declared"const require = createRequire(import.meta.url) (and the guarded bundler
variant const require = createRequire ? createRequire(import.meta.url) : undefined) → "Identifier 'require' has already been declared"A postinstall patch (patch-yargs-dirname.cjs) rewrites those constructs to the
wrapper's own __filename / __dirname / require (yargs is never executed here —
it only needs to parse as CommonJS). Getting the patch right took several iterations,
each of which failed in a way that looked like an unrelated source-level syntax error:
import.meta.url before stripping
const require = createRequire(…). The expansion injected a require('url')…
call whose ) then terminated the paren-naive createRequire\([^)]*\) match early,
leaving a dangling .pathToFileURL(__filename).href)) — reify then died with
SyntaxError: Unexpected token (19:33) on the generated yargs esm.mjs, not on
any repo file. Diagnosing it required instrumenting reify's Babel parser in the
Meteor dev_bundle copy to dump the exact source string it was choking on, because
every scan of the repo's own *.js came back clean (the broken file was in
node_modules, was .mjs, and was produced by the patch itself). Fixed by
removing the createRequire line before expanding import.meta.url, plus a
repair rule that collapses any already-corrupted …href)) leftover back to a marker.node_modules/yargs-parser/build/lib/index.js (not under
node_modules/yargs, so import.meta.url there was still unexpanded, a tell that
the old yargs-only scan had never touched it). Its line 30 used the guarded ternary
const require = createRequire ? createRequire(import.meta.url) : undefined;, whose
createRequire ? (space, not () slipped past the direct createRequire\( matcher
→ the leftover const require collided with the wrapper parameter and the server
bundle crashed at boot with "Identifier 'require' has already been declared".
Two changes fixed it: (a) generalize the removal (and its trigger) from
const require = createRequire\(…\) to a line-wise const require = …createRequire…;
so it matches both the direct call and the guarded-ternary form, running it before
the import.meta.url expansion so the injected require('url')… parens can never
confuse it; and (b) broaden the scan from just node_modules/yargs to yargs and
its ESM dependency packages (yargs-parser is the actual offender; cliui, escalade,
string-width, y18n, get-caller-file, require-directory are scanned too and are
harmless to include).The patch is idempotent, best-effort (never fails an install), and re-runnable by hand
(node scripts/patch-yargs-dirname.cjs) to repair a node_modules tree left broken by
an older version. This is a pre-existing test-only build breakage (the yargs bundling
predates these fixes); production runtime bundles were never affected. Separately, three
server test files were present on disk but missing from the curated loader
server/lib/tests/index.js (which the meteor test entry imports explicitly rather than
by *.tests.js convention), so they had silently never run — the MimeBleed file-validation
bypass regression, the import/export privacy settings, and the impersonation report query
are now registered. With all of the above, the server suite boots and runs clean:
450 passing, 0 failing (411 before the three files were wired in). Thanks to xet7.
Test infrastructure: build.sh and build.bat now show live progress in
every test path. Several places used to sit silent for minutes, which looked
like a hang. Audited both scripts and closed every gap:
:3000 server
came up, the readiness wait printed only dots. .sh now shows live progress
(see the .build/bundle item below, where it streams the boot log
scrolling); .bat prints a check counter now and then and points at the
live server log to type in another window (on cmd, echoing arbitrary log
lines is unsafe as they can contain > < | &).[0s]). The wait polled curl http://127.0.0.1:3000/sign-in with no timeout; Meteor binds the :3000 proxy
early and accepts the TCP connection while the app is still building but
sends no HTTP response until it finishes, so curl blocked on that first
connection for the whole build and the loop never advanced. Added
--connect-timeout 2 --max-time 4 so each poll returns quickly, and the
wait is now bounded by wall-clock time (.sh 1200s / .bat ~240 polls)
rather than a fixed count (also applied to the Playwright-ALL precheck)..sh now streams each suite's reporter output
straight to the console via tee (also saving the log), so you see
every test tick by one-by-one as Mocha / Playwright / the E2E harness prints
it, then a final PASS/FAIL + count line; .bat runs each job in its own
minimized window (reusing the proven parallel start-commands + .done-<key>
flags) and polls a live pass counter, one at a time..sh option 10 /
run_playwright_parallel). It redirected each browser to a log and only
dumped the output after all finished. It now streams each browser's
Playwright list reporter live via tee (progress visible per test) while
still saving the log; PIPESTATUS[0] keeps the per-browser pass/fail
accurate through the pipe.Test infrastructure: "Run ALL tests" reuses the precompiled .build/bundle
for the :3000 server instead of recompiling with meteor run (both build.sh
and build.bat). Node E2E and Playwright drive a live WeKan over HTTP; that
server is now started from the production bundle you already built with meteor build .build --directory — node main.js boots in seconds with no recompile,
using Meteor's bundled node and mongod (mongod on :3001, or an
already-running MongoDB there is reused). Its one-time programs/server npm
install and its boot log now stream live (scrolling) so you can see exactly what
is happening. Two caveats that are inherent to Meteor and are called out in the
script: (1) the bundle is run as-is, so after changing source you must
rebuild it or the tests run old code; (2) the server-side Mocha suite still
uses meteor test (its own .meteor/local-test build) — the in-process
unit/security tests cannot run from a production bundle, so copying the bundle
would not help them. In sequential mode the suites run strictly one at a
time, each streaming its own output, and the parallel-only combined table is
skipped (only the live WeKan server + MongoDB run alongside — they are the
system under test, not parallel test jobs). Before starting, the run checks for
the .build/bundle directory specifically (not just .build) and builds it
once with meteor build .build --directory if it is missing or incomplete, so a
first run with no bundle still works. The bundle server talks to the
meteor database on :3001 — the DB name Meteor's built-in mongo used under
meteor run and the one the Playwright / Node E2E tests seed into
(tests/playwright/helpers/db.js, tests/e2e/list-regressions.js); an earlier
/wekan name made the app read an empty database while the tests seeded a
different one, so every seeded test failed until the names were aligned. Both
the Bash and the Windows batch runners now use this bundle-based :3000 server
(the .bat resolves Meteor's bundled node / mongod from the dev_bundle,
starts mongod in a minimized window, and stops it on exit only when it started
it).
and fixes the following bugs:
<details> <summary>Sandstorm: the WeKan Admin Panel is now always available in a migrated grain, not just a freshly… Thanks to xet7.</summary>Sandstorm: the WeKan Admin Panel is now always available in a migrated grain,
not just a freshly created one. On Sandstorm the grain owner (Sandstorm
configure permission) is mapped to a WeKan admin, which gates the Admin Panel
(and Admin Panel / Attachments / Sandstorm, used to delete leftover files after
a migration). A brand-new grain worked because Users.after.insert derives the
role for the new user, but a grain migrated from an older WeKan already contains
the user, so that insert hook never runs. Sandstorm auto-login uses
connection.setUserId() (bypassing accounts-base, so Accounts.onLogin never
fires), and the only remaining hook — an observeChanges on
services.sandstorm — fires solely when that field actually changes on login.
When the migrated user's stored permissions already equal the grain's current
permissions, the login $set is a no-op with no oplog entry, so the role was
never derived and the owner had no Admin Panel. WeKan now reconciles this at
grain startup (a migrated grain reboots once migration completes): every
Sandstorm user whose stored permissions include configure is granted WeKan
admin. It only promotes, never revokes, so a stale/empty stored permission set
can never lock the owner out.
Admin Panel / Version: show the MongoDB storage engine even with per-database
credentials (no more "unknown"). The storage engine was read only from the
serverStatus command, which requires the cluster-level clusterMonitor role.
A per-database WeKan user (readWrite on the wekan database only, as in the
docs/Platforms/FOSS/Docker/Meteor3/ setup) is not authorized to run it, so the
command was rejected and the field stayed unknown. WeKan now falls back to a
$collStats aggregation (which needs only the collStats action that
read/readWrite already grant) and reads the real engine — wiredTiger (or
inMemory) — from storageStats. The MongoDB compatible version and Database
commit were already correct: they come from buildInfo, which needs no special
privileges.
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUE of SortBleed:
<details> <summary><a href="https://wekan.fi/hall-of-fame/boardbleed/">SortBleed: a low-privilege (comment-only / read-only) board member could escalate to board admin…</a></summary>SortBleed:
a low-privilege (comment-only / read-only) board member could escalate to board
admin and take over a private board via the board sort collection-allow rule
(GHSA-xm8x-c8wg-jhmf,
CWE-863 Incorrect Authorization, CWE-269 Improper Privilege Management). Same
broken-access-control class as BoardBleed (CVE-2026-55234) — a Meteor collection
allow-rule field conflation — but on the Board document itself. To support
drag-to-reorder on the All Boards / Public Boards pages, a second
Boards.allow({ update }) rule returned true for any board member whenever
the update touched the sort field. Meteor evaluates allow rules with OR
semantics and does not scope an approving rule to the field that satisfied
it: once any allow callback returns true and no deny callback returns true,
the entire modifier is applied. Because canUpdateBoardSort only checked
that sort was among the modified fields (not that it was the only
one), a comment-only / read-only member could smuggle arbitrary board mutations
into the same $set as sort in a single unprivileged DDP Boards.update
call: {$set: {sort: 99, members: [...only themselves as admin...], permission: 'public', title: '...'}}. The member could therefore make themselves board
admin, flip a private board to public (world-readable in Wekan), rename it, and
evict the legitimate owner. The last-admin deny rule did not help because it
only inspected $pull, so a wholesale $set of the members array bypassed it
entirely
canUpdateBoardSort (server/lib/utils.js) so the
sort-reorder rule approves an update only when sort is the sole
modified field (fieldNames is exactly ['sort']) — it can no longer
approve a modifier that also mutates members, permission, title or
anything else. As defense in depth, the last-admin deny rule
(server/permissions/boards.js) now also rejects a $set rewrite of the
members array that would drop the last active admin, not just a $pull. A
regression test covers the multi-field smuggling case
(server/lib/tests/boards.security.tests.js). The legitimate All Boards
drag-reorder is unaffected: it persists the order per-user in
profile.boardSortIndex (Users.setBoardSortIndex), not in the board
document. CVSS:3.1 8.8 High (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H).and adds the following updates:
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following new features:
and fixes the following bugs:
<details> <summary>Sandstorm build: fixed "not writing through dangling symlink" that failed the sandstorm.yml… Thanks to xet7.</summary>Sandstorm build: fixed "not writing through dangling symlink" that failed the
sandstorm.yml workflow at sandstorm-src/build-deps.sh step [3/7]. The
meteor-spk 0.6.0 base ships some runtime libs in meteor-spk.deps/lib as
DANGLING symlinks (e.g. libstdc++.so.6 → a library that no longer exists), so
refreshing them with the host's newer libs failed: cp -fL refuses to write
through a dangling destination symlink. Added --remove-destination so cp
deletes the existing destination (dangling symlink included) before copying the
host's real library.
Sandstorm build: write the signing keyring to the path meteor-spk pack
actually reads. The sandstorm.yml "Restore the Sandstorm signing keyring"
step wrote the decoded SANDSTORM_KEYRING secret to
~/.sandstorm/sandstorm-keyring, but meteor-spk / spk read the app private
key from ~/.sandstorm-keyring (a file directly in $HOME), so packing aborted
with open(~/.sandstorm-keyring): No such file or directory even when the
secret was set. Now it writes ~/.sandstorm-keyring, and — since the .spk
cannot be signed without it — fails early with an actionable message when the
secret is missing, instead of the cryptic later crash.
Sandstorm .spk: trim it back toward Cloudflare's 100 MB upload limit. The Sandstorm build no longer bundles the ~300 MB of modern MongoDB Database Tools (wekan/mongo-tools) — they are unused in Sandstorm (grains back up/restore themselves; the MongoDB 3 → FerretDB migration uses the legacy migratemongo CLIs
.mjs importer). Also set PUPPETEER_SKIP_DOWNLOAD=1 for the Meteor
build (puppeteer is a test-only devDependency, so its ~150 MB Chromium never
belongs in the .spk), and the pack step now prints the .spk size and warns
if it exceeds 100 MB. (The Database Tools remain in the WeKan .zip bundle /
Docker / Snap, which do use them.) Thanks to xet7.Sandstorm .spk: bundle the matching glibc dynamic loader so grains start : the
.spk bundled the host's new glibc libc.so.6 (Ubuntu 24.04, glibc 2.39) but
kept the old glibc 2.31 ld-linux from the meteor-spk 0.6.0 base, so node
failed at startup with libc.so.6: undefined symbol: _dl_audit_symbind_alt, version GLIBC_PRIVATE (HTTP-BRIDGE exit 127) and the grain crash-looped.
sandstorm-src/build-deps.sh now also copies the host's ld-linux-x86-64.so.2
so the loader and libc are the same glibc, and adds a [verify] gate that fails
the build if they differ or the bundled node cannot run under them — turning a
silent grain crash-loop into a loud build failure
Sandstorm build workflow: enable unprivileged user namespaces and build the
dispatched branch : Ubuntu 24.04 defaults
kernel.apparmor_restrict_unprivileged_userns=1, which blocks the unprivileged
user namespaces the Sandstorm install and the spk supervisor rely on, so
sandstorm.yml now relaxes it on the runner. A new ref workflow_dispatch
input also lets the workflow build a fix branch before it is merged, instead of
a hardcoded main
Meteor unit tests: fix server-boot crash from __dirname in an ESM test file :
server/lib/tests/dependencies.openapi.tests.js is an ES module that referenced
the bare __dirname global; under Node 24 / Meteor 3.5 the compiler injects
const __dirname = fileURLToPath(import.meta.url), colliding with the
__dirname the CommonJS module wrapper already provides, so the server bundle
failed to boot with Identifier '__dirname' has already been declared and the
"Meteor unit tests" CI job died before any test ran. Drop the __dirname seed
(process.env.PWD already reaches the repo root under meteor test)
Sandstorm .spk: point FerretDB state dir to writable /var so the grain starts :
FerretDB persists a state.json (version/UUID) via its state provider even with
telemetry disabled, and its --state-dir defaults to . — which in a Sandstorm
grain is /, read-only. So FerretDB failed with Failed to create state provider: failed to persist state: open /state.json: read-only file system,
exited (code 1), and the grain crash-looped. sandstorm-src/start.js now
creates /var/ferretdb and passes --state-dir=/var/ferretdb (plus
FERRETDB_STATE_DIR) in startFerret(), covering both the migration and
steady-state FerretDB launches
Sandstorm .spk: don't crash the grain when capnp.node can't load on Node 24 :
WeKan on Sandstorm bundles Node 24, but capnp.node (node-capnp) is built for
an older Node ABI (NODE_MODULE_VERSION 83 = Node 14), so Npm.require('capnp')
in sandstorm.js failed with ERR_DLOPEN_FAILED and crash-looped the whole
grain on boot. Cap'n Proto is now loaded lazily in a try/catch and degrades
gracefully: the grain boots and core WeKan works, because login and user
identity come from the sandstorm-http-bridge X-Sandstorm-* HTTP headers
(wekan-accounts-sandstorm) and need no Cap'n Proto. Only the two capnp-only
features are skipped when the addon cannot load — the Powerbox identity-claim
method and Sandstorm activity notifications — with a clear warning. They can be
restored by rebuilding node-capnp for Node 24, or reimplemented over the
bridge's HTTP/JSON API
Snap release: build amd64+arm64 natively on GitHub, exotic arches non-blocking
on Launchpad : the single snap job ran snapcraft remote-build for all 5
platforms on Launchpad and blocked until the slowest resolved, so the release
hung ~3.5h on riscv64's Launchpad queue even though amd64+arm64 finished in
minutes. It is now split in two: snap-native builds amd64 (ubuntu-24.04) and
arm64 (ubuntu-24.04-arm) natively on GitHub runners via
snapcore/action-build; snap-launchpad builds s390x/ppc64el/riscv64 on
Launchpad in a non-blocking (continue-on-error), per-arch matrix
(remote-build --build-for <arch>) so a slow or failed exotic arch never delays
the release or the other arches. Each arch publishes to the Snap Store
(candidate,beta,edge) and attaches to the GitHub Release the moment it finishes
— all 5 arches still ship
Sandstorm .spk: fix server-boot crashes from stale globals (Users, HTTP) in
sandstorm.js : once the capnp load was made non-fatal, the grain reached the
rest of sandstorm.js and hit two latent Meteor-2.x-isms the Meteor 3.x
migration missed (this file only runs on Sandstorm, so it was not exercised): it
referenced the Users/Boards/Swimlanes/Activities collections as implicit
globals, but those are now ES module default exports, so Users.after.insert
threw Users is not defined at boot; and it monkey-patched HTTP.methods from
the removed meteor/http package, so HTTP was undefined. The collections (and
Accounts) are now imported explicitly, and the obsolete HTTP.methods patch
is removed
Sandstorm .spk: use boolean index options so FerretDB accepts the users index :
wekan-accounts-sandstorm created the unique index on services.sandstorm.id
with {unique: 1, sparse: 1}. Real MongoDB accepts the truthy 1, but FerretDB
(used by the Sandstorm .spk) is strict and rejects it with The field 'unique' has value unique: 1, which is not convertible to bool, crashing the grain at
boot during index creation. Now uses real booleans (unique: true, sparse: true)
Sandstorm .spk: strip Accept-Encoding so the grain doesn't serve corrupted
(gzip) content : the grain boots, but the page failed to load with a browser
"Corrupted Content Error" (NS_ERROR_NET_CORRUPTED_CONTENT).
sandstorm-http-bridge advertises Accept-Encoding: gzip to the app regardless
of what the browser actually sent, so Meteor served gzip/brotli-encoded
responses the browser could not decode. A WebApp.rawHandlers middleware now
strips Accept-Encoding (it runs before Meteor's static/boilerplate serving) so
responses go out uncompressed; bandwidth is a non-issue behind the local bridge
Sandstorm .spk: bundle a modern sandstorm-http-bridge to fix "Corrupted Content"
: the grain boots, but the page failed with a browser "Corrupted Content Error"
(NS_ERROR_NET_CORRUPTED_CONTENT) on WeKan's / redirect. The meteor-spk 0.6.0
base bundles an ancient (~2016) sandstorm-http-bridge that mangles responses
(it always advertises Accept-Encoding: gzip to the app and mishandles
redirect/encoding). build-deps.sh now overwrites the bundled
/sandstorm-http-bridge with the modern one from a Sandstorm install
(/opt/sandstorm/latest/bin/sandstorm-http-bridge; override with
SANDSTORM_HTTP_BRIDGE), and sandstorm.yml installs Sandstorm before
assembling the deps so the bridge is available on the CI runner
Sandstorm .spk: fix the "/" redirect (malformed Location + Content-Length
mismatch) : the grain's / served a broken 301: the Location was
.../:6080board because FlowRouter.path() does not resolve on the server in
Meteor 3.x (it returned the bare route name board), and the response
advertised Content-Length: 90 while sending an empty body — which the browser
rejected as a "Corrupted Content Error" (NS_ERROR_NET_CORRUPTED_CONTENT). The
handler now builds the board path directly (/b/:id/:slug) and sends a real
HTML body (meta refresh + link) with a matching Content-Length, so the
redirect to the hard-coded Sandstorm board works
Sandstorm .spk: open the grain on the All Boards page, not a hard-coded board :
WeKan on Sandstorm originally opened a single hard-coded board
(/b/sandstorm/libreboard) and redirected / to it. It now supports many
boards and that board is no longer the right destination, so the
WebApp.handlers.get("/") redirect is removed entirely — / now falls through
to WeKan's normal serving, whose client home route renders the All Boards
list, the right landing page for a multi-board grain. This also removes the last
server-side redirect the browser was rejecting as a Corrupted Content Error
Sandstorm .spk: don't auto-create a board; map grain permissions to global role
: a new grain/user no longer gets a hard-coded sandstorm/libreboard board
auto-created — WeKan on Sandstorm is multi-board now, so the user creates their
own boards from the All Boards page. updateUserPermissions previously added
the user as a member of that single board (which would now crash since the board
no longer exists); it now maps the grain's Sandstorm permissions to the user's
global WeKan role — configure (grain owner) becomes a WeKan admin,
everyone else is a regular user who can create and manage their own boards
Sandstorm .spk: set SANDSTORM=1 so the header-based auto-login runs : WeKan
loaded in the grain but every page showed "Must be logged in". The
wekan-accounts-sandstorm client only starts the automatic X-Sandstorm-*
header login when __meteor_runtime_config__.SANDSTORM is set, and the package
only sets that when process.env.SANDSTORM is present — which nothing did. The
launcher now sets process.env.SANDSTORM = '1' before loading the WeKan bundle,
so the client auto-logs-in the Sandstorm user from the headers
Sandstorm .spk: rewrite ROOT_URL per request to the grain URL (fixes login +
CORS) : WeKan loaded but stayed on "Must be logged in", and the console showed
Cross-Origin Request Blocked … http://127.0.0.1:4000/__meteor__/dynamic-import/fetch. The launcher sets a
fixed ROOT_URL (http://127.0.0.1:4000, the internal bridge target), but
Sandstorm serves each grain at a per-session host (ui-<hash>.<host>), so the
client sent its DDP connection and dynamic-import fetches to 127.0.0.1:4000 —
cross-origin and unreachable — and the header-based login handshake (a DDP
method call) never completed. A WebApp.addRuntimeConfigHook now rewrites
ROOT_URL to the grain's real base URL (X-Sandstorm-Base-Path) per request,
so DDP, dynamic imports and the Sandstorm auto-login work
Sandstorm .spk: bounce from sign-in to the boards list once auto-login lands :
the Sandstorm login was actually succeeding (Meteor.userId() gets set), but
the grain stayed on the sign-in page. WeKan's home route checks
Meteor.userId() once and, because the Sandstorm header login is asynchronous
and uses connection.setUserId() (bypassing accounts-base, so
Accounts.onLogin never fires), finds it still null on first render and
redirects to atSignIn — where the user is stranded even after login completes.
A Sandstorm-only reactive autorun now sends the user from atSignIn back to
home as soon as Meteor.userId() is set, so the grain lands on the All Boards
page
Sandstorm .spk: keep the grain URL in sync with the in-app route : navigating
between boards worked but the Sandstorm shell's grain URL never updated (it
stayed on the grain root), unlike standalone WeKan. The path was synced via a
global FlowRouter.triggers.enter callback, which does not fire reliably on
client navigation in this flow-router-extra / Meteor 3 setup; it is now synced
from a reactive Tracker.autorun on FlowRouter.watchPathChange() (the same
mechanism the title sync uses), so every route change updates the grain URL to
/grain/<id><path>
Sandstorm .spk: upload attachments over DDP (bridge strips Meteor-Files' HTTP
headers) : adding a file to a card in the grain failed with HTTP 400 Can't continue upload, session expired [408] and the file silently disappeared.
Meteor-Files' HTTP upload signals the first chunk with an x-start header and
tracks the session with x-mtok/x-chunkid/x-fileid/x-eof, but Sandstorm's
request-header whitelist does not include them, so the sandstorm-http-bridge
strips them — the server never sees x-start, treats every request as a chunk
continuation, cannot find the session and returns 408. Attachment uploads now
use transport: 'ddp' on Sandstorm (DDP method calls, no custom HTTP headers);
HTTP transport is kept everywhere else
Sandstorm .spk: authorize attachment/avatar downloads via X-Sandstorm-User-Id :
once uploads worked, the uploaded image still showed as a broken thumbnail,
minicard cover and slideshow in the grain — the file was on disk but the
download returned HTTP 403. The download route
(server/routes/universalFileServer.js) authorizes files on private boards with
a Meteor login token (Authorization / X-Auth-Token / authToken query /
meteor_login_token cookie), but Sandstorm has none of these: authentication is
via connection.setUserId() and the sandstorm-http-bridge X-Sandstorm-*
request headers, so extractLoginToken() returned null and
isAuthorizedForBoard() denied every request. Sandstorm already gates grain
access at the platform level, so any request that reaches WeKan is an
authenticated grain user — both isAuthorizedForBoard() and
isAuthorizedForAvatar() now allow when Meteor.settings.public.sandstorm is
set and the request carries the bridge-injected X-Sandstorm-User-Id header.
Gating on the setting means a spoofed header cannot bypass auth on non-Sandstorm
deployments
Sandstorm .spk: open All Boards (not sign-in) and keep the grain URL in sync :
two grain-navigation regressions against the last working Sandstorm build
(v6.15). (1) Opening a grain showed "Must be logged in" instead of the All
Boards page: on Sandstorm the platform authenticates the user asynchronously
over DDP via connection.setUserId(), which (unlike a password login) does not
set Meteor.loggingIn(), so Meteor.userId() is null for the first moments
after the grain opens — and useraccounts' ensureSignedIn trigger plus
renderBoardList() both bounced that brief null window to the atSignIn route,
a sign-in page that does not exist inside a grain. config/router.js now uses a
Sandstorm-aware ensureSignedInUnlessSandstorm wrapper (a no-op on Sandstorm)
and renderBoardList() no longer redirects on Sandstorm; the list renders and
fills in reactively once the login lands. (2) The Sandstorm shell's outer grain
URL did not update when switching boards — the shell rewrites
/grain/<id><path> when the app posts a { setPath } message, but the sync was
a bare top-level Tracker.autorun that could run before flow-router-extra's
reactive path tracking was ready and then never re-run. Restored v6.15's
event-driven FlowRouter.triggers.enter (fresh entering path,
order-independent) and kept a watchPathChange() autorun wrapped in
Meteor.startup as a backup
Sandstorm .spk: fix the MongoDB 3 → FerretDB migration of an existing
grain:
importing an old WeKan grain crash-looped in the one-time migration — mongod 3.0
forked and its child aborted with exit code 14. Two bugs. (1) WiredTiger
cache_size=0G: mongod 3.0 sizes its WiredTiger cache from detected RAM (RAM/2 − 1GB), but inside a Sandstorm grain sandbox RAM detection returns 0, so it
computed cache_size=0G and WiredTiger refused to open (minimum is 1MB),
logging "Value too small for key 'cache_size'" / "Fatal Assertion 28561" to
/var/migration-mongod.log before aborting. Both mongod invocations (the niscu
→ 3.0 stage and the 3.0 → FerretDB stage) now pass an explicit
--wiredTigerCacheSizeGB 1 so the cache size never depends on RAM detection
(mongod 3.0.7 parses this option as an integer number of GB — a decimal like
0.25 fails with Bad digit ".", fixed to 1 — and it is a cache cap, not a
preallocation). The data itself is intact (the "unclean shutdown" notice is
harmless — WiredTiger recovers from the last checkpoint). (2) Wrong source
database: Sandstorm WeKan grains store their data in the Meteor-default
database meteor, but the importer was told SRC_DB=wekan, so even once mongod
started it would have exported zero collections; only the FerretDB target
database is wekan
Sandstorm/Snap migration: import mongodb and bson as default (CommonJS) exports
under Node
24:
with the WiredTiger cache fixed mongod 3.0 started and the migration importer
ran, but crashed immediately on its named imports — import { EJSON } from 'bson' and import { MongoClient } from 'mongodb' each threw "Named export
'…' not found. The requested module is a CommonJS module". Both packages, as
bundled in WeKan's server node_modules, are CommonJS, so Node 24's ESM loader
exposes no named exports on them. Import the default and destructure, as Node's
own error message advises (bson in the commit linked above; mongodb in the same
way ). Shared by the Snap MongoDB 3 → FerretDB migration too (same Node 24)
Sandstorm/Snap migration: connect mongoexport over IPv4 so it can read the old
data:
with the importer finally running, every mongoexport of a source collection
failed — at first silently (its own --quiet flag suppressed the
reason), and once that was
dropped the real error showed: "error connecting to db server: no reachable
servers". mongoexport is a Go tool whose --host defaults to localhost,
which resolves to ::1 (IPv6) first, but the migration mongod listens only on
--bind_ip 127.0.0.1 (IPv4); the mongo shell defaults its host to 127.0.0.1
and so connected fine (it listed all 42 collections), which is why only the
shell worked. Pass --host 127.0.0.1 explicitly. The one-time progress
dashboard also now shows a live Activity panel (mongoexport-ready line,
per-collection export/insert counts, GridFS extraction counts) with a spinner
and an auto-updating timestamp , so it is clear what the migration is doing
rather than sitting on "(waiting…)"
Sandstorm/Snap migration: resolve bson/mongodb from the modern server bundle so
EJSON exists: once mongoexport
connected, every collection failed with "Cannot read properties of undefined
(reading 'parse')" — EJSON was undefined. The importer script sits at the
deps root right next to the OLD meteor-spk 0.6.0 base node_modules (kept only
for the niscu → 3.0 stage): its bson is 1.x with no EJSON at all, and its
mongodb is ancient — it has MongoClient (so the connection worked) but no
EJSON re-export. A bare import/require from the script resolved those
adjacent old copies, so every way of reaching EJSON (bare bson,
mongodb.EJSON) came back undefined. WeKan's current bson 7.3 and mongodb
driver (with EJSON) live under programs/server/npm/node_modules; anchor
createRequire inside that modern bundle first (falling back to the deps root)
and load both mongodb and bson through it — which also moves the importer to
the same modern mongodb driver the WeKan app uses against FerretDB. An upfront
guard fails loudly with a diagnostic list if EJSON.parse is still unreachable
Sandstorm/Snap migration: insert into FerretDB with the modern mongodb driver
(OP_MSG) : text collections exported and "inserted N/N" was logged, but every
document actually failed with "Unsupported OP_QUERY command: update" — nothing
reached FerretDB. requireAny('mongodb') had resolved the ancient meteor-spk
base driver (v2.x, at the deps root) because the modern driver is not directly
under programs/server/npm/node_modules — Meteor nests it at
…/meteor/npm-mongo/node_modules/mongodb (v6.16). The 2.x driver speaks legacy
OP_QUERY, which FerretDB rejects; the 6.x driver speaks OP_MSG (the same driver
the WeKan app uses against FerretDB). Add the npm-mongo path as the first
resolver anchor, and log the resolved driver version
Sandstorm/Snap migration: extract Meteor-Files GridFS attachments + parse legacy
v1 binary : with text migrating, attachments still produced 0 files and "parse
attachments.chunks: Unexpected Binary Extended JSON format". Two causes. (1)
mongo 3.x mongoexport writes binary as legacy Extended JSON v1
{"$binary":"<b64>","$type":"00"}, but modern bson EJSON.parse only accepts
v2 {"$binary":{"base64":…,"subType":…}} — rewrite v1→v2 per line before
parsing. (2) The grain stores attachments in Meteor-Files' own GridFS buckets
(attachments.files + attachments.chunks, with the FilesCollection record in
the attachments collection), not CollectionFS's cfs_gridfs.*. Reassemble
those buckets to disk, link each GridFS file to its record via
metadata.fileId/versionName, then repoint the record's versions.<v> at the
file and drop versions.<v>.meta.gridFsFileId — otherwise WeKan's
getFileStrategy keeps choosing the now-empty GridFS backend and the image
404s. Verified end to end on a real grain: boards/cards/lists/swimlanes migrate
and all 3 attachments extract and display
Sandstorm/Snap migration: auto-open All Boards after migrating, WeKan-themed
dashboard : the one-time progress dashboard used to sit on the grain URL for 60s
after completion, forcing a manual reload to reach WeKan. On success the
importer now hands off quickly and the done page polls / until WeKan's app
shell answers (riding out the brief importer→WeKan port hand-off) and then opens
All Boards, with the spinner still spinning so it is clear the grain is still
working. The dashboard is also recoloured to WeKan's blue/white/grey
Avatars and original members now travel with a board (export/import), from every identity source. A board could be moved between servers (or imported into Sandstorm) but member avatars vanished — they lived in Sandstorm / LDAP / OAuth2, not in WeKan — and original members were collapsed onto the importing user by a mapping that could attach the wrong person and leak board permissions. Now:
</details>jpegPhoto/thumbnailPhoto,
OAuth2/OIDC picture claim, gravatar or a pasted URL — every network fetch
guarded against SSRF (http/https only; no private, loopback, link-local or
cloud-metadata address; timeout + size + image-type caps). (board-open
trigger. Inside a
Sandstorm grain outbound fetch is sandboxed, so a still-external Sandstorm
picture is a best-effort no-op there — but any avatar that is already a
local file exports/imports fully, grain included.)authenticationMethod:'imported', loginDisabled, isActive:false, no
secrets), reusing each original _id so card/comment/activity references
resolve to the right person with NO mapping at import time, and restores
their avatar. The importer stays the sole admin; imported members hold no
permissions until reconciled.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/b227a931933a3a8abff67c1084f9561b8847f444">Normal users cannot move cards between swimlanes</a>. Thanks to xet7.</summary>Normal users cannot move cards between swimlanes : in the swimlanes view the
.js-swimlanes sortable — which also carries moving a card from one swimlane to
another — was disabled for every non-admin (!isBoardAdmin()), even though its
own comment said it should be disabled only for non-members. So ordinary board
members could move a card within a swimlane but not between swimlanes, and could
not reorder swimlanes. Now it is disabled only for users without write access
(!canModifyCard(): comment-only, worker, read-only), so board members can move
cards between swimlanes again
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
FerretDB v1 is now downloaded as an individual per-arch binary, not
ferretdb.zip: the wekan/FerretDB release now attaches one ferretdb-<arch>
(.exe on Windows) asset per platform instead of a single multi-platform
ferretdb.zip. Every WeKan build path now downloads only the one binary for the
platform it targets from
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-<arch>:
release-all.yml (the amd64/arm64/win64/mac/ppc64le/s390x/riscv64 bundle jobs —
the separate build-ferretdb job and its ferretdb-zip artifact are removed),
the default docker-compose.yml, and the Sandstorm
sandstorm-src/build-deps.sh. FerretDB itself also moved its Go toolchain to
1.25.11 to clear the Quay.io stdlib security advisories.
Drop the mongosh binary; use bundled Node.js 24 + the mongodb driver
instead: WeKan no longer bundles or downloads the MongoDB Shell anywhere
(snap, Windows bundle). Every scripted database operation it was used for —
readiness ping, replica-set initiate/status, and the v8.43 schema
migration — now runs through the new snap-src/bin/db-eval (a tiny wrapper
around the bundled Node.js 24 + the mongodb driver), so the snap control
scripts (wekan-control, mongodb-control, migration-control), the
start-wekan.sh/start-wekan.bat launchers, and the ported
migrate-schema-v843.mjs are all mongosh-free. This removes a large, CVE-prone
binary and works identically on every architecture (including
s390x/ppc64le/riscv64, which have no prebuilt mongosh). The legacy MongoDB 3.2
mongo shell (migratemongo, amd64) stays for migration-time reads only.
MongoDB Database Tools now come from wekan/mongo-tools, not the MongoDB
website: every WeKan build downloads the per-arch <tool>-<arch> binaries
(bsondump, mongodump, mongoexport, mongofiles, mongoimport, mongorestore,
mongostat, mongotop) built by the wekan/mongo-tools fork (pure Go,
cross-compiled for every architecture) from its newest release, replacing the
fastdl.mongodb.org / downloads.mongodb.com downloads. They are embedded in
the Linux .zip bundles (amd64/arm64/s390x/ppc64le/riscv64) — and therefore in
the Docker image, which is built from those bundles — the Windows and macOS
bundles, the Snap (mongotools part), and the Sandstorm .spk. The MongoDB 7
server (mongod) is still fetched from MongoDB (amd64/arm64 only), since MongoDB
ships no server for the other architectures; the legacy MongoDB 3.2 CLIs
(migratemongo, amd64) remain only for the one-time MongoDB 3 migration
Snap: the default snapcraft.yaml is now the base: core24, grade: stable
build: the previous snapcraft.yaml (core26, grade: devel) is renamed to
snapcraft-core26.yaml, and the former snapcraft-core24.yaml becomes
snapcraft.yaml. Because the default is base: core24 (a released base), it
can be published to the Snap candidate channel, which core26 cannot. The
automated release-all.yml workflow publishes the snap to the candidate +
beta + edge channels; the stable channel is published manually later, once
it is proven stable enough.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following features and fixes:
Docker: FerretDB v1 + SQLite is now the default docker-compose.yml:
The default database for docker compose up -d is now FerretDB v1 with embedded
SQLite (from https://github.com/wekan/FerretDB) — light and self-contained, no
separate database server. The compose files were renamed accordingly:
docker-compose-ferretdb-v1-sqlite.yml -> docker-compose.yml (the new default),
and the previous MongoDB default docker-compose.yml -> docker-compose-mongodb-v7.yml.
The other files are unchanged: docker-compose-ferretdb-v2-postgresql.yml (FerretDB 2
docker-compose-multitenancy.yml (MongoDB multitenancy). To use
MongoDB 7, run docker compose -f docker-compose-mongodb-v7.yml up -d or rename that
file to docker-compose.yml. build.sh / build.bat Docker menus now
list FerretDB v1 SQLite first (default) and point at the new filenames, the compose
files' own header comments were updated, and the Docker docs now describe which
compose file maps to which database. The Docker menus also gained a Build from
source & start (up -d --build) action per compose file, which builds the wekan-app
image from the local Dockerfile (tagged as the image the compose file references)
and starts that freshly built container instead of a possibly-stale prebuilt image —
useful when a change (e.g. the FerretDB Version-page detection) isn't in the pulled
image yet. Finally, the obsolete version: attribute (which Docker Compose v2 warns
about and ignores) was removed from all compose files (docker-compose.yml,
docker-compose-mongodb-v7.yml, docker-compose-ferretdb-v2-postgresql.yml,
docker-compose-multitenancy.yml, .devcontainer/docker-compose.yml, and the
ToroDB docs example).build.sh / build.bat: reorganized into category submenus + Docker start/logs/stop:
The long flat menu is now grouped into a short top-level menu — Setup,
Dev server, Tests, Docker, Tools, Quit — each opening a small
submenu with 0) Back, so you read only a handful of items at a time and labels
are shorter (the category provides the context). Docker is a two-stage submenu:
pick a backend (MongoDB docker-compose.yml, FerretDB v1 SQLite, FerretDB v2
PostgreSQL, MongoDB Multitenancy), then an action — Start (up -d),
Follow logs (logs -f) or Stop (down) — which removes the previous
repetition of 12 near-identical entries. The .sh auto-detects docker compose
vs legacy docker-compose. All existing actions are unchanged, just regrouped.
build.sh / build.bat: Dev server options now stop the previous server (including the rspack :8080 dev server) before starting, plus a new "Kill all dev servers" option, and docs updated for the new menu:
</details>Every Dev server option (localhost:3000, + trace warnings, + bundle visualizer, CURRENT-IP:3000, CURRENT-IP:3000 + MONGO_URL 27019, and
CUSTOM-IP:PORT) now stops any Meteor dev server already running before starting a
fresh one, so re-running a dev option no longer fails because a port is taken — no
need to hunt down and kill the old processes yourself. Crucially it frees both
the app port and the rspack dev-server port 8080: meteor run starts an rspack
dev server on 8080 that can outlive the meteor parent, and a leftover one made the
restart crash with Error: listen EADDRINUSE ... :8080. Port detection now checks the
listening socket directly (ss/lsof, with a bash /dev/tcp fallback that needs no
external tools, and netstat on .bat) instead of an HTTP probe, so it also catches
a server that is still building. A new Dev server -> Kill all dev servers option
frees every dev/test port the scripts use at once — the dev app (3000) and its Mongo
(3001), the Mocha test server (3100) and its Mongo (3101), a Sandstorm standalone dev
server (4000) and its Mongo (4001), and the rspack dev server (8080) — killing meteor,
the rspack watcher and Meteor's bundled --replSet meteor Mongos (never a
production/system Mongo). Both scripts escalate to SIGKILL if a port does not free up.
Also updated the build-from-source docs (README.md, Build-from-source.md,
Build-and-Create-Pull-Request.md, Emoji.md, and the two Sandstorm developer docs) to
the new two-level menu (Setup -> Install dependencies, Setup -> Build WeKan,
Dev server -> localhost:3000) and fixed a stale dev-server port
(localhost:4000 -> 3000).
FerretDB: quieter logs, and removed a dead MongoDB-driver-selection subsystem:
On FerretDB (SQLite), the driver debug logs revealed a second, TLS-enabled Mongo
monitor connection retrying every ~0.5s and being rejected by the plaintext FerretDB
port, which FerretDB logged at WARN (Connection stopped … invalid message length /
before secure TLS connection was established) — harmless (WeKan runs fine on the real
plaintext connection) but very noisy. FerretDB is now started with --log-level=error
in all bundled launch points (Docker entrypoint, snap ferretdb-control, the release
start-wekan.sh, and the docker-compose-ferretdb-v1-sqlite.yml example), which drops
the per-connection WARN spam. Separately removed a dead, unused "MongoDB Driver System"
(server/mongodb-driver-startup.js + models/lib/{meteorMongoIntegration,mongodbConnectionManager,mongodbDriverManager}.js)
— an abandoned attempt to auto-detect MongoDB 3.0–8.0 and pick versioned driver packages
that were never even installed; WeKan uses the mongodb-7 driver via Meteor.
Fix snap build failing on the caddy part (Cloudsmith unreachable on
Launchpad):
The snap installed Caddy from the Cloudsmith apt repo
(curl … dl.cloudsmith.io … | gpg --dearmor), which fails on the Launchpad remote
builders — their network is restricted to a fetch proxy that can't reach Cloudsmith, so
gpg got no key (no valid OpenPGP data found) and the caddy override-build failed
with code 2 on both arches, all attempts. Both snapcraft.yaml and snapcraft-core24.yaml
now download the official prebuilt Caddy static binary from GitHub releases (per-arch,
latest stable with a pinned fallback) instead. WeKan uses only built-in Caddy directives,
so vanilla Caddy is sufficient — no apt repo, no gpg, no xcaddy/custom-module build.
Fix Admin Panel / Version showing "MongoDB" when running on FerretDB:
The database detection only recognised a buildInfo.ferretdb sub-document, but the
wekan/FerretDB v1 fork reports its identity as a top-level ferretdbVersion string
(e.g. v1.24.2-60-gb5523566) plus ferretdbFeatures, with its git commit in gitVersion.
So the Version page showed Database type: MongoDB and hid the FerretDB rows. Detection now
handles both shapes (server/statistics.js),
so it shows Database type: FerretDB, the FerretDB version and FerretDB commit rows, and
the SQLite storage engine. (FerretDB v1's version: 7.0.42 is the MongoDB version it emulates.)
Design doc: WeKan on Sandstorm (Meteor 3.5 / Node 24) with MongoDB 3 → FerretDB migration:
Added docs/Platforms/FOSS/Sandstorm/Meteor3/Migration.md
describing how to build a modern Sandstorm .spk (Node 24, replacing meteor-spk
0.6.0's Node 14) that runs on FerretDB v1 (embedded SQLite) instead of MongoDB 3.0,
migrating an existing grain's MongoDB 3.0 data on first launch — reusing the snap's
proven migrate-mongo3-to-ferretdb logic (mongoexport read → FerretDB insert;
CollectionFS/Meteor-Files GridFS attachments+avatars → filesystem). Includes the
grain sandbox (seccomp) compatibility analysis, the rewritten start.js, and a new
isSandstorm-only Admin Panel / Attachments / Sandstorm section (migration status,
raw-MongoDB disk usage, and a guarded delete-raw-MongoDB-files action). Implementation
of the in-app pieces follows.
Sandstorm: Admin Panel / Attachments / Sandstorm (migration status + free raw-MongoDB disk space):
Implemented the in-app pieces from the design above. When WeKan runs inside a
Sandstorm grain (isSandstorm), a new Sandstorm section appears in Admin
Panel / Attachments showing whether the one-time MongoDB 3 → FerretDB v1
migration succeeded, and the disk space the raw MongoDB 3 database files, the
FerretDB SQLite, and the attachments/avatars currently use inside the grain.
An admin can delete the now-redundant raw MongoDB files to free disk space —
guarded so it only runs after a confirmed-successful migration, behind a
confirmation. New admin-gated server methods sandstormMigrationStatus /
sandstormDeleteRawMongo (server/methods/sandstormMigration.js);
the migration importer now writes a migration-status.json the panel reads.
Sandstorm: grain launcher + spk build tooling (Node 24 / FerretDB, no releases.wekan.team):
Added the grain launcher sandstorm-src/start.js:
on first launch it runs the migration chain for whatever an existing grain holds —
niscu (MongoDB 2.x) → MongoDB 3.0 (the preserved legacy path for very old grains) then
MongoDB 3.0 → FerretDB v1 — then runs WeKan (Node 24) on FerretDB. Migration support from
old versions is permanent (niscud + mongod 3.0 are kept). Added the deps-assembly script
sandstorm-src/build-deps.sh
which builds a modern meteor-spk.deps on top of upstream meteor-spk 0.6.0
(dl.sandstorm.io) — swapping in Node 24, adding FerretDB + the Mongo 3.x CLIs + the
launcher/importer, keeping niscud — with extra binaries fetched from GitHub releases
(the retired releases.wekan.team / old projects.7z are no longer used). sandstorm.yml
now calls it, and WRITABLE_PATH in sandstorm-pkgdef.capnp is /var/files. Build/CI only —
not yet packed/tested end-to-end in a grain.
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following features and fixes:
Fix scheduled backup cron init crashing at server startup on Meteor 3:
The scheduled-backup cron used Meteor's synchronous Mongo API
(BackupSettings.findOne/upsert), which Meteor 3 no longer allows on the
server — startup logged findOne is not available on the server. Please use findOneAsync() instead. and the cron silently never registered, so scheduled
backups did not run. registerCron() is now async and uses findOneAsync,
the getBackupSchedule/saveBackupSchedule methods use findOneAsync/
upsertAsync, and every registerCron() caller awaits it. Added
tests/backupCron.test.cjs (positive + negative cron-registration tests and a
source guard that fails if the synchronous server-forbidden API is
reintroduced), wired into the test:unit:node suite.
Snap: show the correct writable path for parallel installs in backup instructions:
The backup/migration help text printed by wekan.help hardcoded
/var/snap/wekan/common/files, which is wrong for a parallel snap install
(e.g. wekan_customer, whose data lives under /var/snap/wekan_customer/common).
It now prints $SNAP_COMMON/files, which snapd sets per instance. Display-only;
all functional snap paths already derive from $SNAP_COMMON/$SNAP_DATA, so
parallel installs were already fully supported.
Fix flaky server-side Mocha i18n test (TAPi18n .loadLanguage):
The .loadLanguage suite stubbed addResourceBundle on the shared
TAPi18n.i18n singleton and asserted the call count. Because loadLanguage()
reads this.i18n late and each test re-init()s the singleton, cross-test
state could leave the stub watching a different i18next instance than the one
the bundle was registered on, so the count read 0 and the run intermittently
reported expected addResourceBundle to be called once. The tests now assert
on observable i18next state (hasResourceBundle/getResourceBundle under the
normalised toI18nCode), which is instance-agnostic and deterministic and
mirrors the reliable .setLanguage suite. Also stopped a leaked
Tracker.autorun in the .getLanguage reactive test (the cross-test hazard).
The extra-arch bundle build ran node:24-slim under QEMU, but the official
node:24 image publishes no linux/s390x manifest (only amd64/arm64/ppc64le),
so the s390x leg failed with "no matching manifest for linux/s390x". The
emulated native-module rebuild now runs on an ubuntu:26.04 base (which
publishes every arch) and installs Node.js from nodejs.org; riscv64 uses
unofficial-builds.nodejs.org (nodejs.org ships no riscv64).
MongoDB Community only ships amd64/arm64 server binaries, so on ppc64le, s390x
and riscv64 — the other architectures with a Node.js 24 build — WeKan now
bundles FerretDB v1 (the wekan/FerretDB
fork with its embedded pure-Go SQLite backend, which speaks the MongoDB wire
protocol) instead of requiring MongoDB. The FerretDB binary is cross-compiled
once for all five architectures (CGO off, static, no QEMU) and embedded in
every .zip next to main.js, so amd64/arm64 users can opt in too with
WEKAN_DB=ferretdb. The Docker image now covers all five architectures and
auto-starts FerretDB on the MongoDB-less ones. FerretDB telemetry is disabled
and locked (--telemetry=disable, plus DO_NOT_TRACK). armv7l/32-bit ARM is
still not built: there is no Node.js 24 build for it anywhere, so nothing could
run WeKan there regardless of RAM.
** Snap: choose MongoDB or FerretDB v1 with "snap set wekan database=ferretdb"; FerretDB default for new installs; disable mongosh telemetry **:
</details>The WeKan snap gains a database setting (mongodb or ferretdb). A new
ferretdb service runs FerretDB v1 (SQLite) on the same port MongoDB would
use, so MONGO_URL is unchanged; only one database runs at a time, and the
configure hook stops one and starts the other when the setting changes. Switch
with snap set wekan database=ferretdb (or back with database=mongodb).
New snap installs default to FerretDB on all platforms (via the install hook,
which runs only on fresh installs — upgrades keep MongoDB, and a pre-existing
MongoDB data directory is detected and kept so no data is lost). There is no
snap install --db=ferretdb flag (snapd has no custom install options); use
the two-step snap install wekan --channel=latest/beta then
snap set wekan database=ferretdb.
mongosh collects anonymized usage analytics by default and the snap invokes it several times; it is now disabled so nothing phones home. mongod itself has no phone-home telemetry (Cloud Free Monitoring is opt-in and stays off).
Admin Panel / Version: show database type, version, commit, storage engine and reactivity mode:
Admin Panel / Version now shows whether WeKan is using MongoDB or FerretDB v1 (SQLite), the MongoDB-compatible version and the server git commit, FerretDB's own version and commit when FerretDB is in use, the storage engine, and which reactivity mechanism is currently in use — changeStreams / oplog / polling (FerretDB has no oplog, so it uses polling).
** Standalone ferretdb.zip for all platforms, and rebuild-all.yml uses the prebuilt one from wekan/FerretDB releases **:
</details>FerretDB v1 (the wekan/FerretDB fork, pure-Go SQLite backend, CGO off, no QEMU)
is now cross-compiled for every platform Go and modernc.org/sqlite support — far
beyond the arches Node.js ships — and packed into a single ferretdb.zip:
ferretdb/<arch>/ferretdb-<arch> (Linux/macOS/BSD, executable)
ferretdb/<arch>/ferretdb-<arch>.exe (Windows only)
ferretdb/README.md (links to https://github.com/wekan/FerretDB)
That zip is produced and released in the wekan/FerretDB repo (by its build.sh,
which gained sequential and parallel "Build ferretdb.zip" menu options). The
WeKan release workflow no longer builds FerretDB with Go: it downloads the newest
ferretdb.zip from https://github.com/wekan/FerretDB/releases and every WeKan
build (the bundle .zip for each arch, and via the bundle the Docker image and
snap, including the Windows and macOS bundles) embeds its per-arch binary from
that one source. ferretdb.zip is not re-attached to the WeKan releases.
Fix #6445: dynamic-import chunks 404 under a sub-path (duplicated build-chunks/build-chunks/):
Under a sub-path deployment (ROOT_URL like https://host/wekan, usually behind
a reverse proxy that strips the prefix), language selection and other
lazy-loaded features failed with ENOENT ... build-chunks/build-chunks/<id>.js.
rspack's client runtime builds each chunk URL as public-path + chunk-name, and
the chunk name already carries the build-chunks/ prefix, but
client/00-startup.js set the sub-path public path to <sub-path>/build-chunks/,
so rspack appended a second build-chunks/. It now sets the public path to just
<sub-path>/ and lets rspack add build-chunks/ itself.
Add snapcraft-core24.yaml so the newest WeKan can be published to the Snap Stable channel:
The main snapcraft.yaml uses base: core26, which (until core26 is released)
needs build-base: devel + grade: devel, so it can only go to Snap Beta/Edge.
snapcraft-core24.yaml builds the SAME newest WeKan (Meteor 3.5, Node.js 24,
FerretDB v1, MongoDB 7, Caddy 2) on base: core24 (a released base, grade: stable), so the Snap Stable channel can finally be updated from the old 6.09
snap. Only base/build-base/grade differ.
Self-contained release bundles: bundle Node.js + FerretDB + start-wekan.{sh,bat}:
Each wekan-<version>-<arch>.zip is now fully offline. Its bundle/ directory
contains the WeKan server, a Node.js binary for that platform, a FerretDB v1
(SQLite) binary, and a start-wekan.sh (start-wekan.bat on Windows) that by
default runs WeKan on the bundled Node against the bundled FerretDB SQLite,
storing data and attachments/avatars on the filesystem under WRITABLE_PATH
(as in the Windows Offline guide) — no separate Node or database install
needed. The Docker image and snap, which have their own Node and entrypoint,
strip the redundant bundled Node + launchers to stay small.
Sandstorm packaging (mirroring releases/release-sandstorm.sh +
install-sandstorm.sh: installs Meteor, meteor-spk 0.6.0 and a dev Sandstorm,
runs meteor-spk pack) is not tested well enough yet, so it no longer runs
as part of a full release — the build-sandstorm job was removed from
release-all.yml. Instead a separate, manually-triggered sandstorm.yml builds
ONLY the .spk and uploads it as wekan-sandstorm-YYYY_MM_DD-HH_MM_SS.spk to the
newest WeKan GitHub Release (plus a workflow artifact), so it can be downloaded
and tested for errors without affecting releases. Experimental in CI — Sandstorm
needs unprivileged user namespaces, and signing the .spk needs the app private
key via the SANDSTORM_KEYRING secret; spk publish / scp upload stay manual.
Migration: resumable progress in WRITABLE_PATH + compact the old MongoDB after success:
The MongoDB → FerretDB / GridFS → filesystem migrator (used by the Snap and
Sandstorm migrations) now checkpoints progress to
$WRITABLE_PATH/migration-progress.json after every collection and file phase,
so an interrupted migration (snap refresh, Sandstorm grain restart, power loss)
resumes on restart instead of starting over — skipping collections already
copied. Once migration has completed, a later boot reclaims the now-duplicated
disk space in the old MongoDB by running compact on each source collection
(best-effort, once). The existing ROOT_URL progress dashboard and disk-space
checks are kept, and the dashboard is restored from the checkpoint on resume.
Snap: migrate the Caddyfile from Caddy v1 to Caddy v2 format on upgrade:
When upgrading from an old WeKan snap, $SNAP_COMMON/Caddyfile may still be in
Caddy v1 syntax, which Caddy 2 cannot parse (caddy would fail to start).
caddy-control now runs a converter before caddy run when caddy is enabled:
a no-op if the file already parses as Caddy 2, otherwise it backs up the
original, converts the common v1 directives (proxy / TARGET →
reverse_proxy TARGET, drop the v1 websocket/transparent presets Caddy 2
does by default, gzip → encode gzip, strip the http:// scheme), validates
with caddy adapt, and only keeps a valid result — falling back to the shipped
Caddy 2 template otherwise, so caddy always starts with valid config.
Rename docker-compose-ferretdb.yml to -v2-postgresql.yml and add -v1-sqlite.yml:
docker-compose-ferretdb.yml (FerretDB 2 + PostgreSQL) is renamed to
docker-compose-ferretdb-v2-postgresql.yml, and a new
docker-compose-ferretdb-v1-sqlite.yml runs WeKan against FerretDB v1 with the
embedded SQLite backend — no PostgreSQL or MongoDB — fetching the v1 binary for
the container's architecture from the newest wekan/FerretDB release. Both
compose files now carry the FULL wekan service from docker-compose.yml
(every documented environment variable and feature); the only differences are
database-related (the ferretdb service replaces mongodb, MONGO_URL points
at FerretDB, MONGO_OPLOG_URL is dropped and reactivity is polling, since
FerretDB has no MongoDB change streams / replica-set oplog).
Snap: one-time MongoDB → FerretDB v1 migration on upgrade, with live progress at ROOT_URL:
On first boot after upgrading an old MongoDB-based WeKan snap, mongodb-control
hands off to a new migration-control before starting mongod. It opens the
existing MongoDB data with the right bundled mongod — mongod 7 for MongoDB 7
data (all arches, including the arm64 snaps already on newest WeKan), or a
bundled old mongod 3.2 (amd64 only) for WeKan 6.09 / MongoDB 3.2 data —
starts a temporary FerretDB v1 (SQLite), and runs the migrator, which moves text
data to FerretDB and both CollectionFS GridFS and Meteor-Files GridFS
attachments+avatars to the filesystem, shows a live progress counter at
ROOT_URL, checkpoints to $SNAP_COMMON (resumable), and compacts the old
MongoDB when done. It then switches the snap to database=ferretdb. Idempotent,
resumable, never deletes the source data, only switches on success. (The amd64
6.09/MongoDB-3.2 path needs testing on real 6.09 data; the arm64/MongoDB-7 path
uses the already-bundled mongod 7.)
Snap: bundle migratemongo (MongoDB 3.2 binaries + old libraries + AVX wrappers) to read 6.09 data:
Per docs/Backup/Backup.md and https://github.com/wekan/migratemongo, running the
old MongoDB tools/server in the snap needs LC_ALL=C and their libraries on
LD_LIBRARY_PATH ($SNAP/lib/<arch>-linux-gnu), and the 2016 MongoDB 3.2
binaries additionally need old libraries (libssl/libcrypto.so.1.0.0,
libpng12, libexpat) that modern bases lack. A new migratemongo snapcraft
part (amd64) stages https://github.com/wekan/migratemongo at $SNAP/migratemongo
(its MongoDB 3.2 bin/, the old lib/x86_64-linux-gnu/, and the avx/ QEMU
wrappers) — also filling in the $SNAP/migratemongo/avx path that
mongodb-control/-backup/-restore already referenced but was never bundled.
migration-control now reads the amd64 6.09 MongoDB 3.2 data with that mongod
(old LD_LIBRARY_PATH) and the legacy mongo shell (mongosh cannot talk to 3.2).
The bundled Node MongoDB driver can't connect to a 3.2 server, and no single
driver version spans 3.2 and MongoDB 7 / FerretDB — so rather than aliasing an
EOL Node driver into package.json, the amd64 6.09/3.2 case uses the
migratemongo CLI: migration-control dumps the wekan database with the old
migratemongo mongodump (MongoDB 3.2, old libraries), then loads it into a fresh
temporary mongod 7 with the snap's modern mongorestore. The existing
driver-based migrator then reads that mongod 7 exactly like the arm64 MongoDB-7
case (both GridFS types → filesystem, text → FerretDB v1 SQLite); the MongoDB-7
path connects the driver directly, unchanged. Also fixes a real bug: the
standalone migrator in $SNAP/bin could not resolve its mongodb/bson
imports — NODE_PATH now points at the WeKan bundle's node_modules.
Refines the snap migration to the intended design: a MongoDB 7 database works
with newest WeKan as-is and is not migrated; only the old 6.09 / MongoDB 3.2
data is. migration-control now checks whether mongod 7 can open the data — if
so it keeps MongoDB; otherwise it migrates the 3.x data. It reads it with the
legacy mongoexport CLI (the Node driver can't talk to 3.2) and inserts into
FerretDB with the Node driver — text streamed directly, GridFS attachments+avatars
reassembled per-file straight to files/attachments/files/avatars — with no
mongodump/mongorestore and no intermediate MongoDB 7 (which the earlier commit
used). FerretDB's SQLite now lives at <files>/db, next to attachments/avatars
(the files/<name> layout), across the snap, offline launchers, Docker entrypoint
and the v1-sqlite compose.
A new "Database migration" section in Admin Panel / Attachments with two buttons:
migrate text-based data (everything except attachments/avatars, which stay on the
filesystem) to FerretDB v1 (SQLite) or back to MongoDB. WeKan is connected
to one database at a time, so the server opens a second driver connection to the
OTHER database (both speak the MongoDB wire protocol) and copies the text
collections into it, upserting by _id (idempotent). The target is
WEKAN_FERRETDB_URL (default mongodb://127.0.0.1:27018/wekan) or
WEKAN_MONGODB_URL (default :27019); both must be running. Progress is shown
live; afterwards point MONGO_URL at the other database and restart (Snap:
snap set wekan database=ferretdb / =mongodb). Admin-only.
Admin Panel / Attachments / Backup: scheduled backups streamed to storage, restore + list:
A new Backup section in Admin Panel / Attachments. Select any of Attachments,
Avatars, Data (all text-based collections that are not attachments/avatars)
and a storage (filesystem, S3/MinIO, Azure, GCS). "Backup now" streams the
.zip directly to the selected storage — no temp file, no extra disk — as
backup/YYYY/MM/DD/HH_MM_SS/backup.zip containing
YYYY_MM_DD-HH_MM_SS/{attachments,avatars,data/<collection>.json} (filesystem pipes
to the file; S3 uses @aws-sdk/lib-storage streaming, Azure uploadStream, GCS
createWriteStream, with the cloud credentials from the storage tabs). A
scheduler (off/daily/weekly/monthly + time/day) runs backups via synced-cron.
List backups shows a table (storage, datetime, path); pick one and Restore
with "Add missing data only" or "Replace all data". Admin-only. (Cloud upload and
restore are not exercised end-to-end yet; jszip assembles the whole zip, so very
large attachment sets use notable memory.)
Backup: switch from jszip to archiver+unzipper for low-memory streaming:
Follow-up to the Backup section above: it no longer holds whole files or the
whole zip in memory. The backup .zip is written with archiver, streaming
each attachment/avatar straight from disk and each text collection a document at
a time from a Mongo cursor as NDJSON, piped directly to the destination
(filesystem or S3/Azure/GCS streaming upload). Restore uses unzipper: each
file entry is piped to disk and each NDJSON data entry is applied line-by-line
in 200-doc batches. A board with thousands of cards or a 5 GB attachment now
backs up and restores with flat memory.
Stream board exports (JSON, CSV/TSV, Excel) with bounded memory:
The board export routes used to buffer the whole board in memory: the JSON
export built one object with every card, comment, activity, checklist and
base64 attachment; CSV called that same builder; Excel additionally loaded data
it never renders and did O(n²) find() lookups. On large boards this peaked at
gigabytes and could exceed V8's max string length. Now the JSON export writes
the document straight to the response a card at a time from raw cursors
(attachments base64-encoded in aligned chunks), CSV streams one row per card
keeping only the small lookup tables in memory, and Excel uses the exceljs
streaming WorkbookWriter, committing each row and resolving card titles via an
id→title map. Peak memory stays flat regardless of board size, and the JSON
output is unchanged so import round-trips.
Export board to HTML .zip: stream to disk and include every card:
The HTML export cloned the live DOM and built the whole .zip as an in-memory
blob — but infinite scroll keeps only ~10 cards per list in the DOM, so most
cards were missing, and the archive was buffered whole in browser RAM. Every
list's card limit is now lifted so the entire board renders before the
snapshot, and the zip is written with JSZip's generateInternalStream piped
straight to the chosen file via the File System Access API, chunk by chunk with
backpressure (browsers without the API fall back to the previous blob download).
Lazy card loading for very large boards: CARDS_LOADING=all|lazy + Admin Panel / Features:
A board's board publication normally ships every non-archived card (with
full fields, comments, attachments and checklists) into each viewer's minimongo.
The list rendering is already infinite-scrolled (~10 cards per list in the DOM),
but the whole dataset still crosses the wire and sits in browser memory, so a
board with thousands of cards is heavy for every viewer.
A new CARDS_LOADING mode (all default, or lazy) makes each list load
only the cards it is about to render. In lazy mode the board publication ships
no cards; instead each list/swimlane subscribes to a windowed publication
(boardCardsWindow) for just its visible window — growing as you scroll — plus
a reactive total count (boardListCardCount) so it knows when more remain. The
window selector is ANDed with a server-forced board scope and refuses $where.
This is set by the CARDS_LOADING env var (exposed in docker-compose*.yml,
the bundle start-wekan.sh/.bat, and snap set wekan cards-loading=lazy) and
also at runtime in a new Admin Panel / Features section — the intended home
for optional / performance / future tier-gated capabilities. (client + Features,
platform env)
Lazy mode is opt-in and experimental: card counters and WIP limits are
accurate (they read a server count for the list's exact selector), but the
Calendar/Table/Gantt views and multi-select currently reflect only the cards
loaded so far, and open boards must be reloaded after switching modes. Default
all is unchanged. (accurate counters + WIP)
Fix Transifex push: remove duplicate database-migration i18n keys:
en.i18n.json had database-migration and database-migration-description
defined twice, so the Transifex source push failed with "Duplicate string key".
The later, unused copies ("Database Migration" / "Updating database structure…")
were removed, keeping the values the Admin Panel / Attachments migration UI
actually renders. JSON now parses with unique keys.
Finnish translations for the newest features:
Translated the remaining English strings of the Upcoming features into Finnish
(fi.i18n.json), matching existing terminology: Admin Panel / Version
(database type, FerretDB/MongoDB version + commit, reactivity mode), Attachments
/ Database migration, Features (card loading all/lazy), and Backup (schedule,
storage, restore, list). 44 keys; product names/acronyms kept as-is.
Unit + negative tests for the newest features:
Extract the pure, security-/correctness-critical logic of the recent features
into Meteor-free models/lib/* modules (shared by production and tests) and add
plain-Node tests with positive and negative cases, wired into test:unit:node:
the windowed-card publication's $where selector safety, CARDS_LOADING mode
resolution + window-count id, the JSON export's streaming base64 chunker, and
the backup files-root + schedule-text helpers. 41 assertions, all passing.
docs: Design/Multiverse/Alternative-Architectures.md:
Document which CPU architectures WeKan can run on and why (the limit is Node.js, not FerretDB): the Node 24 / MongoDB / FerretDB v1 matrix, why armhf/armv7l/i386 are unsupported (Node dropped 32-bit) and loong64 is not buildable in CI (no QEMU emulation / base image), the JavaScript-engine alternatives (Deno/Bun cover fewer arches; QuickJS/JSC/SpiderMonkey/JVM run on 32-bit but cannot run Meteor), and server-rewrite options (Go recommended, QuickJS niche, Tcl/Tk a poor fit).
Fix backup build: archiver@8 (ESM) + @aws-sdk/lib-storage dependency:
The Meteor/rspack build failed on server/methods/backup.js: archiver@8 is now
pure ESM with no default export or archiver('zip', …) factory (it exports
classes), and the S3 streaming upload used @aws-sdk/lib-storage, which was not
a dependency. Use import { ZipArchive } from 'archiver' /
new ZipArchive({ zlib: { level: 6 } }), and add @aws-sdk/lib-storage
(pinned to ~3.1073.0 to match the installed @aws-sdk/client-s3).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following bugs:
Fix #3624 swimlane REST regression: await the async board.swimlanes():
The swimlanes CRUD e2e test (23-rest-api-more.e2e.js:209) regressed on all browsers. Root cause: on the server ReactiveCache.getSwimlanes is async, so board.swimlanes() returns a Promise in the REST handler. The #3624 change read board.swimlanes().map(s => s.sort); .map on a Promise throws, the insert never ran, the error was swallowed by the handler's catch (returned as 200 with no _id), and the test read .title off a null swimlane.
The earlier "move the require to a top-level import" commit was not the real
cause (the same module.exports import pattern works server-side, e.g.
ruleDeletePermission in rulesButton.js). The actual fix is to await
board.swimlanes() before mapping. This also means API-created swimlanes now get
a real max(existing sort)+1 sort — previously the un-awaited .length yielded
undefined, so they were stored with no sort at all, which is the #3624 symptom.
Verified: the swimlane test passed in every kept local run through 2026-07-09_02-38-07 and failed starting 09-52-25 (right after the #3624 change), confirming the regression window; the tests/swimlaneSort.test.cjs unit test still passes.
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/9f7eac158e99f1ae38dedf586bc43225e267722c">Fix #5536: automated rule can now move/link a card to a different board</a>. Thanks to DarthKillian and xet7.</summary>** Fix #5536: automated rule can now move/link a card to a different board **:
The rules "move card to top/bottom of a list on another board" and "link card to
another board" actions failed for boards the rule creator did not own. The
action showed BLANK after creating the rule (the wizard's optimistic client
inserts landed in minimongo limbo / were rejected by allow-deny for non-owner
members) — these now create the rule through the server rules.createRule
method, keeping the action's destination boardId. And execution crashed with an
"Internal Server Error" because the destination swimlane fallback dereferenced
._id on a possibly-undefined swimlane titled exactly Default
(renamed/translated/deleted on the destination board); resolution now uses the
board's real default swimlane via a Meteor-free resolver
models/lib/ruleActionResolve.js, unit tested in
tests/ruleActionResolve.test.cjs
** Fix #4978: board background updates when switching boards via the favorites
bar **: Switching directly between two boards via the favorites bar reused the
same boardBody template instance, so the one-shot setBackgroundImage() in
onRendered never re-ran and the previous board's background stuck. It is now
applied inside a reactive autorun and clears any stale inline background when
the new board has no image. The decision is a Meteor-free helper
models/lib/boardBackground.js, unit tested in tests/boardBackground.test.cjs
** Fix #4881: Due this week / next week filter respects the start day of week
**: The "Due this week" filter selected next week's cards and ignored the
configured start weekday, because it derived its window from startOf(now(), 'week') — which the native dateUtils never implemented, so it returned the date
unchanged. A new Meteor-free helper models/lib/weekStart.js computes the
correct week window for any start day of week; the this/next-week buttons now
toggle per week too. Unit tested in tests/weekStart.test.cjs
** Fix #4946: calendar week numbers respect the defined start day of week **: In
the Calendar view the week-number column was numbered from Sunday regardless of
the start-day-of-week setting. The calendar now computes the number with
weekNumberByFirstDay() (in models/lib/weekStart.js) from the same firstDay
used to lay out the grid, unit tested in tests/weekStart.test.cjs
** Fix #4653: LDAP username with a hyphen no longer becomes a dot **: With
LDAP_UTF8_NAMES_SLUGIFY enabled, limax(text, { separator: '.' }) turned
every non-alphanumeric run — hyphens included — into ., so an LDAP username
like p.parta-partb became p.parta.partb and the user could not log in. The
username is now slugified per hyphen-separated segment and rejoined with -,
preserving hyphens while still transliterating UTF-8. Pure helper
packages/wekan-ldap/server/usernameSlug.js, unit tested in
tests/ldapUsernameSlug.test.cjs
** Fix #4236: Enter adds a new line in the card title, consistent with the
description **: The card title textarea submitted on plain Enter (only
Shift+Enter made a new line), unlike the description field which inserts a new
line on Enter and saves on Ctrl/Cmd+Enter. The title now uses the same shared,
Meteor-free rule isSubmitKey() (models/lib/editorSubmitKey.js): submit only
on Ctrl/Cmd+Enter, newline otherwise. Unit tested in
tests/editorSubmitKey.test.cjs
** Fix #4055: ISO week-number regression test (already correct in current code)
**: #4055 reported the week number was one/two weeks too high for 2021-10-25..31
(ISO week 43). That was the old moment-based math; the current native, DST-safe
getISOWeek() computes it correctly. Added tests/isoWeek.test.cjs pinning the
reported dates so it cannot regress
** Fix #4394: Register / Forgot Password links stay hidden after a failed login
**: Security. With registration / forgot-password disabled in the Admin Panel,
the links were hidden by a one-shot .hide() that a useraccounts form re-render
(e.g. an LDAP failed login) dropped, so the links reappeared. The disable state
is now a class on the stable <body> ancestor with matching CSS, so the links
are re-hidden on every re-render
** Fix #4494: creating a board from a template no longer breaks subtasks **: A
board created from a template inherited the template's subtasksDefaultBoardId /
dateSettingsDefaultBoardId; when those pointed at the template board itself,
subtasks created on the new board were dropped onto the TEMPLATE board and
linked back across boards. Board.copy() now repoints such self-referential
defaults to the copy and clears the paired list id so it self-heals on the new
board. Pure helper models/lib/boardCopyDefaults.js, unit tested in
tests/boardCopyDefaults.test.cjs
** Fix #4249: filter by card title now works for renamed linked cards **:
Renaming a linked card wrote the new title only to the linked target, leaving
the linking card's own title field stale; filter-by-title queries the own field,
so linked cards dropped out of title filters after a rename. Card.setTitle() now
also writes the linking card's own title. Pure helper
models/lib/linkedCardTitle.js, unit tested in tests/linkedCardTitle.test.cjs
** Fix #3606: activity feed no longer shows "edited/deleted comment undefined"
**: The feed passed the comment id (absent on old activities, and gone for a
deleted comment) into the activity string. Edit/delete activities now store the
comment text and render it via Activities.commentDisplayText(), which falls
back to the live comment text and then to an empty string — never "undefined".
Pure helper models/lib/commentActivity.js, unit tested in
tests/commentActivity.test.cjs
** Fix #3624: new_swimlane REST API appends the swimlane last and accepts a sort
**: POST /api/boards/:boardId/swimlanes set the sort to the swimlane count, so
a new swimlane appeared FIRST when existing sort values were non-contiguous. It
now appends at max(existing sort)+1 and honors an optional explicit sort in
the body. Pure helper models/lib/swimlaneSort.js, unit tested in
tests/swimlaneSort.test.cjs
** Fix #3185: copying a card (or template) now copies its subtasks' checklists
**: Copying a card inserted each subtask as a bare card document, so the
subtasks' checklists (and items) were dropped and the copied subtasks came out
empty. Card.copy() now copies each subtask's checklists onto the new subtask.
Pure helper models/lib/subtaskCopy.js, unit tested in
tests/subtaskCopy.test.cjs
** Fix #5439: list scrollbar is always visible on desktop and mobile browsers
**: List bodies used overflow-y: auto, so overlay scrollbars
(macOS/iOS/Android/ Firefox) auto-hid. The list body now keeps a scrollbar
visible across all engines — overflow-y: scroll, ::-webkit-scrollbar
(Chrome/Safari/Edge/ mobile WebKit), scrollbar-width/scrollbar-color
(Firefox/Gecko) and scrollbar-gutter: stable. Cross-browser rules in
models/lib/scrollbarCss.js, applied in client/components/lists/list.css,
unit tested (builder contract + applied CSS, positive + negative) in
tests/scrollbarCss.test.cjs
** Fix #6444: RTL — typing a card title no longer garbles other lists' minicard
titles **: In an RTL language (e.g. Arabic) the board root is dir="rtl", and
both the minicard title (a dir="auto" .viewer) and the add-card composer
textarea were dir="auto" with no bidi isolation, so they shared the
surrounding bidirectional context. Typing a strong RTL character into one list's
composer re-resolved that shared context and visibly reflowed the displayed
minicard titles of the OTHER lists (no data change, reverted on refresh). Each
title and the composer now use unicode-bidi: isolate. Locked in (positive +
negative) by tests/minicardBidiIsolation.test.cjs; browser RTL behaviour is
covered by tests/playwright/specs/18-rtl-layout.e2e.js
Fix the Playwright CI test failures introduced by the #3624 and #4236 fixes
above:
Two browser tests failed on Chromium, Firefox and WebKit after this release's
changes. (1) 23-rest-api-more.e2e.js:209 (swimlanes CRUD): on the server
ReactiveCache.getSwimlanes is async, so board.swimlanes() returns a Promise
in the REST handler. The #3624 change read board.swimlanes().map(s => s.sort);
.map on a Promise throws, so POST /api/boards/:boardId/swimlanes never ran
the insert, the error was swallowed by the handler's try/catch (returned as
200 with no _id), and the test read .title off a null swimlane. The old
board.swimlanes().length had tolerated the un-awaited Promise by silently
yielding undefined (so API-created swimlanes were stored with no sort at all —
the very #3624 symptom). Fixed by awaiting board.swimlanes() before mapping,
which also gives API-created swimlanes a real max(sort)+1 value. (2)
02-cards-open-view.e2e.js:93 (editing the card title): #4236 deliberately made
plain Enter insert a newline in the card title (Ctrl/Cmd+Enter saves), but the
CardPage page object still saved with plain Enter, so the title never
persisted and the read timed out — updated to save with Control+Enter (
984ec16 ). The server-side Mocha, import-regression and Node E2E jobs were
already green; this makes the Playwright jobs green too
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/b07c4d0a7bed0f03ba2d7d1ad1f030e7edf3dfff">Feature #5394: the Link-card popup's Cards dropdown is now sorted alphabetically</a>. Thanks to xet7.</summary>** Feature #5394: the Link-card popup's Cards dropdown is now sorted alphabetically **: In the "Link to this card" popup, the Cards pull-down list is now sorted alphabetically by card title (case-insensitive, locale/numeric aware) instead of board sort order, so a card can be found on boards with many cards
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7ce71dbb2e1eae5159503eefac7e6f349dfb59c8">Feature #5396: edit Lists (title, color) via the REST API + api.py commands</a>. Thanks to C0rn3j and xet7.</summary>** Feature #5396: edit Lists (title, color) via the REST API + api.py commands
**: Lists can now be edited through the REST API like cards can. The endpoint
PUT /api/boards/:boardId/lists/:listId already accepted title, color,
starred and wipLimit, but the color was stored unvalidated; it now validates
the color with normalizeListColor (a named palette color or a custom #rrggbb
hex) and rejects an unknown color with a clear 400 instead of silently storing
None. The pure field/validation logic lives in a Meteor-free helper
models/lib/listApiUpdate.js and is unit tested in
tests/listApiUpdate.test.cjs. The api.py reference CLI gains two new
commands mirroring editcard/editcardcolor: editlist BOARDID LISTID NEWLISTTITLE and editlistcolor BOARDID LISTID COLOR
** Feature #5514: custom color-wheel (RGB/hex) picker with automatic readable
text contrast **: The color pickers that previously offered only a fixed set of
named colors now also include a native color wheel (<input type="color">), so
any #rrggbb color can be chosen. The wheel was added alongside the existing
swatches for card labels ("categories"), swimlanes ("tabs"), lists and cards.
The schema color fields accept a custom hex in addition to the named palette,
and existing named-color data keeps working; a stored hex is rendered with an
inline background-color instead of the named CSS class. A new pure,
Meteor-free helper models/lib/contrastColor.js computes a readable text color
from sRGB relative luminance (white text on dark backgrounds, black on light),
maps the named palette to hex, and validates/normalizes hex; it is applied as an
inline text color wherever a chosen color is a background behind text (label
chips, swimlane / list headers, minicard, card details header), so text stays
readable on any color. Covered by tests/contrastColor.test.cjs (20 assertions
pass)
** Feature #5621: Rules can set a date field to a custom time (value +
minute/hour/day/week/month later) **: The Rules "Set date relative to now"
action previously only offset a date field (Start / Due / End / Received) by a
whole number of DAYS. It now has a unit selector, so a rule can set a date field
to now + <value> <unit> later, where the unit is minute(s) / hour(s) /
day(s) / week(s) / month(s); negative values move the date earlier. Months use a
real calendar-month add (e.g. keeping the same day-of-month) rather than a fixed
30-day approximation. Existing rules created before this change have no stored
unit and keep working exactly as before (no unit ⇒ days), so the change is fully
backward compatible. The offset math lives in a new pure, Meteor-free helper
models/lib/relativeDateOffset.js used by server/rulesHelper.js and covered
by tests/relativeDateOffset.test.cjs (15 assertions pass). Note that the
related "overdue" Rules trigger and the "set date field to now" action from the
same feature request already existed in an earlier release; this change adds
only the missing custom-time unit selector
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d41e17d6a088edf6abbb1f9517d2ecb58d618f03">Fix #5351: users are auto-added to organizations matching their email domain on sign-up</a>. Thanks to xet7.</summary>** Fix #5351: users are auto-added to organizations matching their email domain
on sign-up **: the Organization setting "Automatically add users with the domain
name" (org.orgAutoAddUsersWithDomainName) could be configured, but nothing at
sign-up ever read it, so a new user whose email domain matched an organization
was never added to it; the Accounts.onCreateUser hook now, on every non-admin
sign-up path (password registration, LDAP, invitation code, new OIDC user), adds
the user to each organization whose configured domain exactly matches the domain
part of their email (case-insensitive, exact — a subdomain does not match and an
empty org domain matches nobody), using the same { orgId, orgDisplayName }
membership shape used everywhere else and never duplicating an existing
membership, with the matching decision extracted into a Meteor-free, unit-tested
orgsToAutoAddForEmail helper
** Fix #5369: the Activities show/hide control is now a clear eye / eye-slash icon toggle **: the Activities panel's show/hide control was a generic, unlabeled material toggle switch whose ON/OFF meaning was counterintuitive; it is replaced on the card details Activities panel with an eye / eye-slash icon toggle (open eye = activities shown, crossed eye = activities hidden) that mirrors the login/register password-visibility toggle, so the icon reflects the current visibility, clicking flips it, and the tooltip states the action (Show activities / Hide activities). For consistency the board sidebar Activities toggle, which drives the same showActivities state, was switched from the check / empty-square icon to the same eye / eye-slash toggle. The underlying show/hide setting and its persistence are unchanged
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aad00cdba2bb896068f4a896cf67df2a7bd79a73">Fix #5442: outgoing webhooks now include the label name on add/remove label</a>. Thanks to xet7.</summary>** Fix #5442: outgoing webhooks now include the label name on add/remove label
**: the addedLabel/removedLabel outgoing webhook (and notification) text showed
a bare, generic "label" with no name, because labels are embedded in the board
document but the Activities label() helper looked the label id up in the Cards
collection and always returned undefined, so the __label__ token was never
filled; the hook now resolves the label from the already-loaded board via
getLabelById and a pure, unit-tested labelDisplayName helper (name, then
color for a nameless label as shown in the UI, then the id), so the webhook
always carries the label's display name
** Fix #5482: adding/editing a card description now triggers outgoing webhooks
**: outgoing webhooks fire only when an operation logs an activity (the
Activities.after.insert hook posts to the board's webhooks), but changing a
card's description created no activity — unlike title/date changes — so no
webhook was sent; the Cards before.update hook now logs an
a-changedDescription activity on first-time set and later edits (but not on
no-op / empty-to-empty saves), so the existing webhook hook fires, with a
Meteor-free unit-tested descriptionChanged helper
** Fix: the Rules Workflow view is now fully translatable via i18n *: the Rules Workflow view rendered its trigger/action palette chips ("Card is created", "Move card to top", "Set received date to now", etc.) as hardcoded English regardless of the UI language, while the rest of the page was translated; each palette entry now carries an i18n key translated at render time via TAPi18n.__ (reusing existing rule keys where they fit, plus new r-w- keys for workflow-only labels), and the slot clear tooltip now uses the existing r-remove key, so the whole view follows the selected language
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/68dd14140f620574a95db42dc1078a7b4bd97916">Fix: deleting a board rule no longer fails with "Access denied [403]"</a>. Thanks to xet7.</summary>** Fix: deleting a board rule no longer fails with "Access denied [403]" **: deleting a rule ran three separate client-side Collection.remove() calls (Rules
allowIsBoardAdmin returned false, and Meteor rejected
the mutation with 403 "Access denied" — so the delete failed even for a
legitimate board admin. Rule deletion now goes through a new server method
rules.deleteRule that authorizes once (active board admin or site admin) and
removes the rule, its trigger and its action server-side, bypassing the brittle
client allow/deny; the permission decision is a Meteor-free, unit-tested helper
and no permission is loosened** Fix #5510: adding a board label via the REST API no longer errors/hangs **:
PUT /api/boards/:boardId/labels only sent a response when the body had a label
key, so a body without one hung until the client timed out, and a bare-string
label pushed a schema-invalid label and returned 200; the handler now always
returns JSON (2xx on success, 4xx on bad input, real error status otherwise) via
a pure, unit-tested input helper
** Fix #5604: CSV export no longer crashes on boards with dangling references
**: exporting a large/old board to CSV failed with "Couldn't download - Network
issue" because Exporter.buildCsv read .title/.username/.name directly on
the result of looking up a card's deleted
list/swimlane/owner/member/assignee/label/customField by id, throwing "Cannot
read property 'title' of undefined"; the per-card row builder is now a null-safe
helper that emits a blank cell for missing references while keeping identical
output for well-formed cards
** Fix #5656: the Calendar view now honors the active board filters **: the Calendar view queried cards by board and date only and ignored the active Filter sidebar (member / assignee / due-date / label / custom-field), so it showed every card in the interval unlike the Board / Lists / Swimlanes views; it now ANDs the Filter selector into its query and refetches when the filter changes
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/457713968cf3c365899271606398b7796a7b07f4">Fix #6442: All Boards "Custom (drag order)" — drop now persists the reorder</a>. Thanks to jullbo and xet7.</summary>** Fix #6442: All Boards "Custom (drag order)" — drop now persists the reorder
**: Follow-up to #6439, which restored the drag preview but left the drop a
no-op: dragging a board on the All Boards page in Custom (drag order) mode
showed the dashed-border preview, but releasing it snapped the board back
without reordering. The drop handler built the current on-screen order from
el.classList[0] of each .js-board, but the item is
li.js-board(class="{{_id}} …") and Jade emits the literal js-board class
FIRST, so classList[0] was the string "js-board" for every board — never the
board _id. The ordered ids were therefore ['js-board','js-board',…], the
(correct, unit-tested) computeReorderedSortIndex helper could not find the
dragged/target ids among them, returned null, and nothing was written to
profile.boardSortIndex; the preview still worked because it is driven by the
dragover CSS class, independent of the id. The fix reads each board's _id
from its Blaze data context (the same source dragstart uses via this._id)
instead of the literal class, so the real display order reaches the reorder
helper and the drop persists. Guarded by a new case in
tests/boardSortReorder.test.cjs (the wrong-class extraction yields no mapping;
real ids reorder)
** Fix #6443: cards on a deleted swimlane are invisible in swimlane view **: On
some old boards a swimlane was deleted while its cards kept the now-dangling
swimlaneId (an "orphaned" card), so those cards showed no content in swimlane
mode even though they worked in list mode. Cards with no swimlane at all (null
/ '' / missing) already appear in every swimlane, but an orphaned card matched
no existing swimlane and so was visible in NO swimlane (while list view, which
applies no swimlane scope, still showed it) — exactly the reported symptom. The
fix mirrors the existing orphaned-list fallback
(Swimlanes.orphanedSwimlaneLists, which surfaces orphaned lists in the first
swimlane) for cards: when a list's cards are fetched for the board's FIRST
swimlane, the swimlane-membership clause becomes a single { swimlaneId: { $nin: <otherSwimlaneIds> } } (everything not owned by another existing swimlane: own
id, null/'', missing, or orphaned). It stays a single field clause with no
second $or, so the #6441 board-wide label filter still holds, and orphaned
cards appear once — in the first swimlane — without a database migration.
Threaded through the pure models/lib/swimlaneFilter.js helpers, the in-memory
filterCardsByListAndSwimlane, a new List.orphanedCardsSwimlaneIds helper and
the cards()/cardsUnfiltered()/allCards() model methods plus the listBody
cardsWithLimit render helper. Covered by new cases in
tests/swimlaneFilter.test.cjs (18 assertions pass; the #6441 regression guards
stay intact)
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/issues/5850">Admin Panel Domains table: pagination, column sort and search (like the Board Table view)</a>. Thanks to xet7.</summary>** Admin Panel Domains table: pagination, column sort and search (like the Board
Table view) **: The Admin Panel > People > Domains table loaded every domain
(aggregated from all users) into the browser at once, with a fixed order and no
search. It now behaves like the Board Table view: the server aggregates the
domains and returns only one small page, so the whole list is never sent to the
browser. You can order by the Domain or Users column (click the header to toggle
ascending / descending, with a ▲/▼ indicator) and filter with a search box;
prev/next controls page through the results. The search + sort + slice runs in
the new pure, unit-tested models/lib/domainTablePage.js behind a new
getDomainsWithUserCountsPage admin method (server/models/users.js), and the
domainGeneral template (client/components/settings/peopleBody.{jade,js,css})
is now self-contained and fetches only the current page. Covered by
tests/domainTablePage.test.cjs
and adds the following tests:
<details> <summary><a href="https://github.com/wekan/wekan/commit/dfa6c78d4f032a698f6feaad189c86da903d27fb">Verified and added a regression test for the board-invitation email language</a>. Thanks to xet7.</summary>** Verified and added a regression test for the board-invitation email language
**: Confirmed that a board-invitation email is localised in the existing
recipient's own profile language, or — when the invitee is a new account created
by the invite — in the inviter's profile language, defaulting to en
(en.i18n.json) when none is set. The behaviour was already correct; the
language choice is now extracted into the pure, unit-tested
models/lib/inviteEmailLanguage.js used by inviteUserToBoard, and locked in
by tests/inviteEmailLanguage.test.cjs
and fixes the following bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/9ef7f4a07a6b40a2582af24b27fe133119adcd18">Linked-card minicard now shows the cover image of the real card</a>. Thanks to 32Dexter and xet7.</summary>** Linked-card minicard now shows the cover image of the real card **: A linked
card (created by "Link card to this card") on one board did not show the cover
image of the real card it points at on another board, even though the card's
other fields did. A linked card is only a placeholder — its real content lives
on the card at linkedId — and every other minicard getter resolves through the
real card (getTitle/getReceived/getDue/…), but the cover helpers read
this.coverId directly, and a linked card has no coverId of its own. The real
card's cover attachment is already published to the linking board (see the
"linked cards" / "attachments for linked cards" children of the board
publication), so this was purely a client-side resolution gap. Card.cover()
and the minicard cover() helper now resolve the cover id through the real card
via the pure, unit-tested models/lib/linkedCardCover.js; normal cards are
unaffected. Covered by tests/linkedCardCover.test.cjs
** Fix date-picker calendar stays fully visible when opened low on a scrolled
page **: Opening a date field (due/start/end date, or a date custom field) low
on the screen showed the calendar popup extending past the visible area, and —
because the pop-over is position: absolute (document coordinates) — scrolling
to reach it moved the calendar along with the page, so the full calendar could
never be seen (the workaround was to close it, drag the field to the center and
reopen). Popup._getOffset computed the space above/below the opener and the
clamped top from the opener's DOCUMENT offset mixed with the VIEWPORT height,
ignoring the page scroll, so on a scrolled page the anchored popup landed
outside the visible viewport. The geometry now runs in viewport coordinates
(subtracting the page scroll) and clamps the popup fully within the visible
viewport, then converts back to document coordinates for the absolute style;
when the page is not scrolled the output is unchanged. Extracted the math into
the pure, unit-tested client/lib/popupOffset.js, used by
client/lib/popup.js. Covered by tests/popupOffset.test.cjs
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/f8e31745d8104c58dc7bbcb4c1e4c615141a8e82">Fix #6439: Custom (drag order) sort on All Boards page now reorders via drag-and-drop</a>. Thanks to jullbo and xet7.</summary>** Fix #6439: Custom (drag order) sort on All Boards page now reorders via
drag-and-drop **: On the All Boards page the jQuery-ui sortable that reordered
boards in the "Custom (drag order)" mode was removed when the page switched to
HTML5 drag-and-drop for workspaces, and nothing replaced it, so dragging a board
showed a not-allowed cursor and never updated profile.boardSortIndex. Added
HTML5 dragover/drop reorder handlers on the board tiles in
client/components/boards/boardsList.js (active only in the custom sort mode),
a setBoardSortIndexes helper in models/users.js to persist the new order in
one write, a drop-hint style in client/components/boards/boardsList.css, and
extracted the reorder decision/index math into the pure, unit-tested
models/lib/boardSortReorder.js. Covered by tests/boardSortReorder.test.cjs
** Fix #6440: '+' add-item button on minicard checklist does nothing **: On the
minicard the checklist add-item <form> is rendered inside the
a.minicard-wrapper anchor, so the native form submit event never reached the
Blaze event map — the #5565 minicard-checklist work wired only a submit .js-add-checklist-item handler (which never fires there) and gave the Save
button no click handler, so clicking "+" Save did nothing. Added an explicit
click .js-submit-add-checklist-item-form handler in
client/components/cards/minicard.js (mirroring the working edit-item button)
that inserts the item, with the blank-input guard and title parsing extracted
into the pure, Meteor-free models/lib/checklistItemTitles.js. Card-detail
checklists are unaffected. Covered by tests/checklistItemTitles.test.cjs
** Fix #6441: label filter now applies board-wide across all swimlanes **: A
label filter hid non-matching cards in one swimlane (e.g. "Focus") but left
another swimlane (e.g. "Background") unfiltered. Each list scopes its cards to
the current swimlane while also showing shared/orphaned cards that have no
swimlane; that fallback was written as a bare top-level $or, which competes
with the board Filter's own top-level $or (label/member criteria) when the two
selectors are combined — dropping the label criterion in every swimlane except
the default one. The swimlane-membership fallback is now a single swimlaneId: { $in: [id, null, ''] } clause (the same form already used in
sidebarFilters.js, cardDetails.js and dialogWithBoardSwimlaneList.js),
extracted to the pure, unit-tested models/lib/swimlaneFilter.js and used by
client/components/lists/listBody.js and models/lists.js, so no second $or
exists and the filter is always $and-combined and applied board-wide. Covered
by tests/swimlaneFilter.test.cjs
** Fix flaky server-side Mocha test (i18n zh-CN "is not a spy") **: The
server-side Mocha suite intermittently reported "1 failing" on the #5756
imports/i18n/i18n.test.js region-tag test with TypeError: [Function] is not a spy. The assertion re-read TAPi18n.i18n.addResourceBundle, and the sinon-chai
matcher (routed through chai-as-promised) could evaluate after this suite's
afterEach had already run sinon.restore() — at which point the property is
the original function, not the stub. The .loadLanguage tests now assert on the
captured stub reference (restore unwraps the property but leaves the spy
intact), which is deterministic. Reproduced and verified with a standalone
sinon/chai script
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/b1b414e850c5494dd3849f8dab6db44fecef0196">Fix "Internal Server Error" when signing up despite the account being created</a>. Thanks to Firas-Git and xet7.</summary>** Fix "Internal Server Error" when signing up despite the account being created
**: Registering a new account showed a red "Internal server error" on the
sign-up form even though the account was created and could sign in — which
typically happens when SMTP is not configured. Root cause: useraccounts'
ATCreateUserServer creates the account and then calls
Accounts.sendVerificationEmail() (because sendVerificationEmail: true); when
SMTP is missing/misconfigured that send throws after the user row is
inserted, and with no try/catch the exception leaves the createUser method as an
opaque HTTP 500. The verification email is best-effort at sign-up, so
Accounts.sendVerificationEmail is now wrapped to log and swallow a transport
failure — registration completes and redirects to sign-in — while an "already
verified" error is re-thrown so the resend-verification flow still reports it.
This mirrors the #5706
reset-password hardening. Covered by tests/verificationEmail.test.cjs
** Fix can not add members to a Linked Card **: A linked card (created by
"Link card to this card") is only a placeholder on the board that links it — its
members are stored on the real card it points at (linkedId), which lives
on another board, and Card.getMembers() / assignMember() /
unassignMember() already read and write that real card. But the member
picker listed the members of the board you were viewing the linked card
on, not the board the real card lives on. So on a board that links a card from
another board, the picker offered the wrong set of members and toggling them did
not behave as a consistent add/remove — "can not add members to the linked
card". The picker now resolves a linked card to its real card's board and offers
that board's active members (matching where the membership is actually
stored), falling back to the current board for normal cards or when the real
card isn't loaded yet. Extracted the target-card/target-board resolution into
models/lib/linkedCardMembers.js, covered by tests/linkedCardMembers.test.cjs
** Fix can't search numbers in custom fields **: Searching a board for the value
of a number or currency custom field (e.g. a transaction number
2025001, or a currency amount 123) found nothing. Those field types store
their value as a JS Number (the inputs save parseInt(...) /
Number(...)), but Board.searchCards() only matched custom fields with { value: <regex> } — and a MongoDB / Minimongo regex only matches string
values, so it silently skipped every numeric custom field. The search now also
adds an exact numeric-equality clause when the term is a plain number (a comma
is accepted as a decimal separator, matching the currency input), so numeric
custom fields match too; text/title/description matching is unchanged. Card
search runs against Minimongo on the client, which — like MongoDB — cannot regex
a numeric field, so equality is the correct cross-environment match. Covered by
tests/cardSearch.test.cjs
** Fix "Removed nonexistent document" crash during notification_cleanup **: The
scheduled notification-tray cleanup logged Exception in removed observeChanges callback: Error: Removed nonexistent document …. The crash itself came from the
old cottz:publish-relations package (since replaced by
reywood:publish-composite), but the cleanup that provoked it still fired one
un-awaited removeNotification() per expired notification — a separate
Users.update $pull each time — so a user with K stale notifications produced
K writes to the Users collection, and every publication that republishes user
documents re-ran its observers K times in quick bursts (the churn that surfaced
the removed-document error), while any rejected write went unhandled. The
cleanup now scans only users that have notifications and prunes each user's
stale notifications in a single awaited $pull … $in. It also removes an
activity's notifications only when every entry for that activity is read and
past its removal age (so a freshly re-created unread notification sharing an
activity id is not dropped), and guards missing/invalid read timestamps.
Covered by tests/notificationCleanup.test.cjs
** Fix impossible to select another board in rules **: In the IFTTT-Rules "Move
card to the board" and "Link card to the board" actions, the board dropdown was
empty for some users — the current board included — while colleagues in the same
company could pick boards normally. Root cause: the dropdown filtered the
(already access-scoped) client cache with 'members.userId': me, i.e. it only
kept boards where the user has a direct member entry. A user who reaches a
board through an Organization, Team or email-domain share — but is not listed
individually in board.members — matched nothing, so the whole selector came up
empty. The dropdown now filters by the same visibility rule as
Boards.userBoards() (public OR active member OR active org OR active team OR
active domain), so org/team/domain-shared boards appear too, while archived
boards, template containers, the user's templates board and internal helper
boards stay excluded
** Fix board disappeared after adding another user **: A board admin adding
another user could make the whole board silently vanish from a user's board
list, with no archive and "nothing out of order" in the logs. Root cause: the
setBoardTeams server method (used by the board Teams/Members management
popups) blindly overwrote the board's entire members array with a snapshot
sent by the client. When that client's board document was stale — e.g. a member
had just been added via inviteUserToBoard on the server and the change had not
yet propagated to the client — the overwrite dropped members, and in the worst
case the board's own admin, so the board no longer matched the board-list
publication (which requires an active membership) and disappeared. Because a
wholesale $set: { members } never passes through foreachRemovedMember(), no
removeBoardMember activity was logged and no card/watcher/star cleanup ran,
which is why the logs looked normal. setBoardTeams now reconciles against the
authoritative server-side members instead of trusting the client snapshot: it
never drops an active admin, keeps every existing member the client still lists,
adds the members the client introduces, and only removes non-admin members the
client explicitly omitted (an intentional team-leave) — logging and cleaning up
each such removal so it is auditable rather than silent
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/issues/6454">Snap: migrate any existing MongoDB (3, 7, or other) to FerretDB (SQLite) on upgrade, and fix the…</a> Thanks to xet7.</summary>Snap: migrate any existing MongoDB (3, 7, or other) to FerretDB (SQLite) on
upgrade, and fix the upgrade that failed with "could not start migratemongo
mongod on the MongoDB 3.x data" ( #6454 ). The WeKan snap now moves EVERY
existing MongoDB database onto FerretDB (SQLite) on first boot after an upgrade
— text data into SQLite and the CollectionFS + Meteor-Files GridFS
attachments/avatars onto the filesystem (files/attachments, files/avatars) —
then shuts MongoDB down so only WeKan (Node.js) + FerretDB (SQLite) run. The
MongoDB version only decides HOW the source is read: a modern MongoDB (6/7) is
read with the mongodb driver, old MongoDB 3.x with the bundled migratemongo
3.2 CLI (mongoexport). This also fixes the reported failure: the old check was a
false dichotomy — it probed whether mongod 7 could open the data and, if that
short probe failed for ANY reason (journal recovery, a stale mongod.lock, a
slow disk, a large oplog), assumed the data was MongoDB 3.x; a healthy MongoDB 7
database then went down the 3.x path, mongod 3.2 also could not open it, and —
because mongodb-control execs the migration script — mongod never started,
so the MongoDB service failed to activate and looped on every restart. Now a
mongod-7-openable database is migrated with the modern importer instead of being
misclassified; the mongod-7 readiness probe was lengthened (20s → 45s) so large
databases needing recovery are not misread; the temporary source mongod is
tracked by pidfile and force-stopped so it never leaves the dbpath locked; and
if neither mongod can open the data (unusual/corrupt) or the tools are missing,
the snap falls back to a normal MongoDB start (never leaving the service dead)
and retries next boot. The MongoDB data is never modified or deleted; the snap
only switches to FerretDB once the migration succeeds
Migration dashboard: use the Admin Panel product name instead of "WeKan". If
the migrated database has a product name set in Admin Panel
(settings.productName), both the Snap and Sandstorm migration progress
dashboards now show that name and do not mention WeKan; otherwise they default
to WeKan.
** Retry snapcraft install in the release-all.yml snap job **: The v9.76
release's snap job failed at sudo snap install snapcraft --classic with
error: cannot install "snapcraft": too many requests — a transient Snap Store
rate limit (429-style throttle) that instantly failed the whole job before any
build ran. The install now retries up to 5 times with 30s backoff, so one store
throttle no longer fails the release (matching the remote-build retry already
in the same job)
Thanks to above GitHub users for their contributions and translators for their translations.
This release adds the following fixes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/8b21158736f9e1360332903707c100bce3d6b164">Fix notification emails linked to /b/undefined/board/<cardId> instead of the real board</a>. Thanks to titver968 and xet7.</summary>** Fix notification emails linked to /b/undefined/board/<cardId> instead of
the real board **: On the server ReactiveCache.getBoard() is async and returns
a Promise, but Cards.board() did not await it, so the synchronous
Card.originRelativeUrl()/absoluteUrl() interpolated a Promise — board._id
and board.slug were undefined, producing /b/undefined/board/<cardId> in
card activity notification emails (the client UI was unaffected because
this.board() is synchronous there). Fixed by making
Card.originRelativeUrl(board)/absoluteUrl(board) accept an already-resolved
board and fall back to this.boardId (always available synchronously) when the
board is a Promise, and by passing the awaited board from
server/models/activities.js so the correct board id and slug are used
** Release workflow: publish the Helm chart only after the Docker image is
pushed **: In .github/workflows/release-all.yml the charts job now depends
on the docker job (needs: docker) instead of running in parallel right after
bump. GitHub Actions runs a job only when all of its needs jobs succeed, so
the wekan/charts Helm chart is published only after the multi-arch image is live
on Docker Hub / Quay.io / GHCR (and is skipped if docker fails). This prevents
ArtifactHub from scanning a freshly published chart whose image tag does not
exist yet and emailing the maintainer about the missing Docker image
** Fix thousands of unsolicited empty "Default" swimlanes created on some boards
**: Board.getDefaultSwimline()/getDefaultSwimlineAsync() self-heal a missing
default swimlane by reading the board's swimlanes and inserting one if none
exist. That check-then-insert is a race: concurrent or repeated server calls for
a swimlane-less board each saw zero swimlanes and each inserted a new one, so
some boards accumulated 30 000+ empty "Default" swimlanes and became unloadable
(the key 'default (en)' returned an object instead of string log is a harmless
i18n side effect — the title correctly falls back to the string Default).
Follow-up to the client-side #6382
fix, which only stopped the browser from auto-creating them. Fixed by making the
server self-heal idempotent: the default swimlane is now upserted with a
deterministic _id (<boardId>-default), so the _id unique index guarantees
at most one default swimlane per board no matter how many times, or how
concurrently, the getters run. archived/type are set explicitly in the
$setOnInsert because their schema autoValue/defaultValue only fire on insert,
not upsert. Note: this prevents new duplicates; boards that already accumulated
thousands still need a one-off cleanup
** Reduce card flicker on drag by only writing changed fields on a card move **:
Card.move() wrote boardId/swimlaneId/listId into the update
unconditionally, even for a same-board drag to another list (the reported case).
Keeping boardId in the $set on every drag re-ran the boardId-gated
Cards.after.update hook that re-syncs the card's checklists and checklist
items via multi updates, ran the cross-board consistency guard and the
denyCrossBoardMove deny-rule DB lookup, and invalidated more reactive
dependents than necessary — server work and reactivity churn that contributed to
a ~1s card flicker on large boards. Card.move() now writes only the fields
that actually change (via the pure computeCardMoveModifier helper) and skips
the write entirely when a card is dropped back in the same place, so a
same-board move no longer touches boardId or the cross-board hooks. The
moveCard/moveCardBoard activity generators already re-check doc.X !== oldX, so trimming the modifier does not drop any activity. Note: this reduces
the drag-time work behind the flicker; the residual reactive re-render cost on
very large boards is a separate performance topic
** Fix DEFAULT_AUTHENTICATION_METHOD env var ignored, and Admin Panel Layout
save hanging **: Two related problems with the default login authentication
method
password
and DEFAULT_AUTHENTICATION_METHOD was never applied, so operators setting
it (e.g. Kubernetes/Helm DEFAULT_AUTHENTICATION_METHOD: ldap) saw no
effect. Startup now applies the env var authoritatively: it seeds the value
on a fresh install and, on existing installs, keeps the stored
defaultAuthenticationMethod in sync with the env var on every boot (the
operator's env is the source of truth), so the method can be configured
entirely by env without the Admin Panel. The value is normalized (trimmed +
lower-cased), so DEFAULT_AUTHENTICATION_METHOD=LDAP works.<select> is populated by an async Meteor.call, so clicking **Admin Panel
Layout > Save** before it loaded sent an empty value for the required
defaultAuthenticationMethodfield, which silently failed validation — the save looked stuck and nothing changed. The save now falls back to the currently stored method when the select is empty, so a real value is never overwritten by''. Both paths share one pure helper (resolveDefaultAuthenticationMethod) that never resolves to an empty string. Thanks to joe-speedboat and xet7.
** Fix #5808: linking a card to another linked card made both cards inaccessible
**: The "Link to this card" target picker only excluded template cards, so an
existing linked card (or a card that already links back to the current
board) could be chosen as a link target. That builds a chain/cycle of linkedId
pointers, but the card helpers (getTitle/getBoardTitle/getRealId) resolve
linkedId only one hop, so such a card renders as an empty/broken pointer
and becomes effectively inaccessible (the reported freeze). As the reporter
suggested, the fix prevents the configuration rather than allowing it: only
a real card — not a linked card/linked board, not one of the linking board's own
cards, and not a card that links back to one of them — may now be a link target.
This is enforced both in the picker's query and re-checked at creation time (the
options can be stale), via the pure isLinkableCardTarget guard, mirroring the
existing #3328 parent/subtask cycle guard. Note: this stops new inaccessible
links; any already-created ones still need manual cleanup
** Fix the "Board not found" flicker (stale-while-revalidate for the client
board cache) **: While viewing a board, the board view could briefly flash the
"Board not found" shell — and on WebKit throw a Blaze Can't select in removed DomRange error tearing down the card view. Root cause: the client board
cache (imports/lib/dataCache.js) re-fetches its value inside a reactive
computation, and when the board doc is momentarily absent from minimongo (a
subscription stops and restarts, so Meteor transiently removes the doc) the
re-fetch returns undefined and that empty value is surfaced immediately. It
self-recovers when the subscription re-delivers the doc, so it presents as a
flicker — reliably reproduced only on Firefox/WebKit, where the reactive-render
timing hits the window (Chromium did not, which is why it surfaced as
browser-specific Playwright failures in 14-voting-watchers and
24-feature-issues). Fixed with an opt-in stale-while-revalidate mode on
DataCache, enabled only for getBoard: a transient miss over an
already-cached board keeps the last value and re-checks after a short delay,
surfacing an empty result only if the board is still gone then (a genuine
deletion / access loss). First-ever loads and caches that did not opt in are
unchanged. Core decision extracted to the pure shouldDeferCacheMiss helper
with unit tests
Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUE of ScannerBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1a222c4477e68c76fd6a866954b535fba0a78d05">ScannerBleed</a>.</summary>** ScannerBleed : shell injection (RCE) via a malicious upload filename in the
external antivirus scanner command path**
(GHSA-x3xm-pxrv-jg7p,
CWE-78 OS Command Injection). Same RCE class as
AvatarBleed (CVE-2026-52891,
GHSA-35j7-h385-2q9g) and its follow-up regression (CVE-2026-53447 /
GHSA-qfqv-42qw-vvwh area), but in a code path that was never covered by
those fixes. In models/fileValidation.js, when an admin has configured an
external scanner (antivirus) command line with a {file} placeholder, the
uploaded file path was interpolated into the command and run through asyncExec
(promisify(exec)), which spawns /bin/sh -c and interprets all shell
metacharacters: ```js await asyncExec(externalCommandLine.replace("{file}", '"'
$(...), backticks and
\, so a filename such as a`id`.png or $(touch /tmp/pwn).png escaped
the argument and executed as the Wekan server process. Any authenticated
user who can upload an attachment could trigger it, on servers that have an
external scanner configured. Unlike the sibling MIME-detection path
(detectMimeFromFile, which already uses execFile with no shell) and unlike
the AvatarBleed fix (which strips non-alphanumeric characters from the
filename), this scanner path had zero sanitizationshellQuote() helper (wrap in single quotes, escape embedded ' as
'\''). Inside single quotes the shell interprets no metacharacters, so a
malicious filename can no longer break out of the argument or inject
commands, while the admin's arbitrary command line and the exact on-disk
path are both preserved. CVSS:3.1 9.9 Critical
(AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H).Thanks to above GitHub users for their contributions and translators for their translations.
This release fixes the following CRITICAL SECURITY ISSUES of DnsBleed and ExcelBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/ef845fe4a0adb82af436313310939cd48c0b1347">DnsBleed</a>.</summary>** DnsBleed — SSRF filter bypass via DNS-resolving hostname in outgoing
webhooks**
(GHSA-66m2-4wfr-c45p,
CWE-918). Incomplete-fix follow-up to
WebhookBleed
(GHSA-hc3x-hq3m-663q)
and IntegrationBleed /
RebindBleed. The synchronous
URL validator on outgoing-webhook (board Integrations) URLs in
models/integrations.js blocks private/loopback/link-local IPs by
regex-matching the URL hostname string and never resolves DNS. A public
hostname that resolves to a blocked address — e.g. 169-254-169-254.nip.io →
169.254.169.254 (cloud metadata), 127-0-0-1.nip.io → 127.0.0.1, or any
attacker-controlled domain with an A/AAAA record pointing at an internal IP —
passes that string blocklist. The report notes the underlying weakness: string
matching is not an SSRF boundary because it can't see the resolved IP
fetchSafe
(server/lib/ssrfGuard.js), which resolves the hostname, validates the
resolved IP, pins the connection to it and blocks redirects — so
169-254-169-254.nip.io is rejected before any request is made. The REST
write paths (POST/PUT /api/boards/:boardId/integrations) were likewise
already hardened in WebhookBleed to run the DNS-aware
validateAttachmentUrl() at input time.dns.resolve4), leaving it blind to AAAA (an IPv6-only internal target was
merely fail-closed, and legitimate IPv6 webhooks were unreachable) and
keeping a second, less-complete private-range block-list that could drift
out of sync with the input-time one. fetchSafe now resolves both address
families via dns.lookup({ all: true }) — the same resolver call the
input validator uses — and validates every resolved IP through the single
shared isIpBlocked block-list in models/lib/attachmentUrlValidation.js.
The three previously-separate block-lists (schema regex, delivery guard,
input validator) are now one source of truth, and the schema-level url
validator is documented in code as a non-authoritative first-line UI check
only.** ExcelBleed — broken access control in the Excel-export REST route lets any
authenticated user export any private board**
(GHSA-mwq8-ccpm-r533,
CWE-862 / CWE-639). Same un-awaited async-auth bug class as
BFLABleed (48 REST endpoints,
v9.22), CloneBleed (un-awaited
allowIsBoardMemberByCard, v9.35) and
TokenBleed. models/exportExcel.js
called its access-control guard exporterExcel.canExport(user) without
await. Because canExport is async, it returns a Promise (always truthy),
so if (exporterExcel.canExport(user) || impersonateDone) was always true and
exporterExcel.build(res) ran regardless of the guard's real result — any
authenticated user could download the full contents of any board (card titles +
descriptions, lists, swimlanes, members, metadata) via GET /api/boards/:boardId/exportExcel, including private boards they are not a
member of. The JSON export route (/export) was correctly awaited and returned
403 for the same non-member
if ((await exporterExcel.canExport(user)) || impersonateDone) — matching every other export route
(models/export.js, exportPDF.js, exportExcelCard.js, import.js). The
Excel-export route was the lone remaining un-awaited canExport call site.and adds the following updates:
and fixed the following bugs:
** Playwright E2E: fixed three cross-process-contention flakes in the parallel run ** The Chromium / Firefox / WebKit browser jobs run as separate processes against ONE shared server + DB, so specs that used fixed identifiers or global cleanups raced each other:
</details>26-shared-templates.e2e.js seeded a fixed email domain
([email protected]) in all three browsers, hitting E11000 duplicate key on the unique emails.address index. Now uses a unique-per-run token
for the org / team / domain and template titles.38-impersonation.e2e.js cleaned up with a global deleteMany({ reason: 'clickedImpersonate' }) that deleted another browser's in-flight audit
record mid-poll. Scoped the cleanup to the test's own adminId.32-org-team-feature-toggles.e2e.js exercises setAllOrgsFeature /
setAllTeamsFeature, which do a global updateMany({}) across every org /
team, so two browsers clobbered each other's rows. These browser-agnostic
server-method tests now run in a single project (Chromium); Firefox / WebKit
skip them.
Thanks to xet7.imports/i18n/i18n.test.js crashed the whole run at load: chai 6.x
plugins (sinon-chai, chai-as-promised) are ESM-only, so
use(require('sinon-chai')) handed chai.use() a module namespace instead
of the plugin function ("fn is not a function"). Now imports the default
export.PositionHistory.helpers is not a function: the meteor test entry (server/lib/tests/index.js) never ran
the .helpers / .attachSchema shim that server/main.js bootstraps, so
the first model to call Collection.helpers({...}) threw. The shim is now
imported first in the test entry.meteor test
only loads what the specs import (not the app's /server/imports). The
specs now import the files that register the methods / globals under test
(cards.vote / cards.pokerVote, api.attachment.*, cloneBoard,
getBackgroundImageURL, applyListWidth, updateListSort,
moveChecklist, userPositionHistory.*, archiveBoard,
sendSMTPTestEmail, and the Attachments global). Also fixed genuine test
bugs: the cards.vote / cards.pokerVote specs were written synchronously
against async methods; the header-login trust specs restored env vars with
process.env.X = undefined (which stores the string "undefined" and
shadowed the trusted-IP allowlist, making every trusted source read as
untrusted); the DnsBleed decimal-loopback matcher assumed the integer host
survived URL normalisation; and the dependencies-OpenAPI spec could not
locate its source file from the built bundle. The two cards.archive /
cards.move specs tested Meteor methods that do not exist (archive / move
are Minimongo document helpers secured by Cards.allow / Cards.deny,
already covered by the cards security tests) and were removed. Server-side
Mocha is now 409 passing, 0 failing.
Thanks to xet7.Thanks to above GitHub users for their contributions and translators for their translations.