CHANGELOG-big.md
Rama main · réplica de lectura del repositorio original.
# Status
<details>
<summary>More status info</summary>
https://wekan.fi/status/
</details>
<details>
<summary>Newest WeKan at these platforms</summary>
- [Install](https://wekan.fi/install/)
- [Upgrade WeKan](https://wekan.fi/upgrade/)
- [Docs](https://wekan.fi/docs/)
- [Mac ChangeLog](https://github.com/wekan/wekan/wiki/Mac)
- Older releases: [2026-10](old-CHANGELOG/2026/10.md),
[2026-10 part 2](old-CHANGELOG/2026/10-part2.md),
[2026-10 part 3](old-CHANGELOG/2026/10-part3.md),
[2026-10 part 4](old-CHANGELOG/2026/10-part4.md),
[2026-10 part 5](old-CHANGELOG/2026/10-part5.md),
[2026-10 part 6](old-CHANGELOG/2026/10-part6.md),
[2026-10 part 7](old-CHANGELOG/2026/10-part7.md),
[2026-09](old-CHANGELOG/2026/09.md),
[2026-09 part 2](old-CHANGELOG/2026/09-part2.md),
[2026-09 part 3](old-CHANGELOG/2026/09-part3.md),
[2026-09 part 4](old-CHANGELOG/2026/09-part4.md),
[2026-09 part 5](old-CHANGELOG/2026/09-part5.md),
[2026-09 part 6](old-CHANGELOG/2026/09-part6.md),
[2026-08](old-CHANGELOG/2026/08.md),
[2026-08 part 2](old-CHANGELOG/2026/08-part2.md),
[2026-08 part 3](old-CHANGELOG/2026/08-part3.md),
[2026-08 part 4](old-CHANGELOG/2026/08-part4.md),
[2026-07](old-CHANGELOG/2026/07.md),
[2026-07 part 2](old-CHANGELOG/2026/07-part2.md),
[2026-06](old-CHANGELOG/2026/06.md), [2026-05](old-CHANGELOG/2026/05.md),
[2026-04](old-CHANGELOG/2026/04.md), [2026-03](old-CHANGELOG/2026/03.md),
[2026-02](old-CHANGELOG/2026/02.md), [2026-01](old-CHANGELOG/2026/01.md),
[2025](old-CHANGELOG/2025.md), [2024](old-CHANGELOG/2024.md),
[2023](old-CHANGELOG/2023.md), [2022](old-CHANGELOG/2022.md),
[2021](old-CHANGELOG/2021.md), [2020](old-CHANGELOG/2020.md),
[2019](old-CHANGELOG/2019.md), [2018](old-CHANGELOG/2018.md),
[2017](old-CHANGELOG/2017.md), [2016](old-CHANGELOG/2016.md),
[2015](old-CHANGELOG/2015.md)
</details>
<details>
<summary>Databases</summary>
Which databases WeKan supports
([docs/Databases/Supported.md](docs/Databases/Supported.md)):
- FerretDB v1 with SQLite: the default on every platform - bundle, Docker, Snap
and Kubernetes.
- FerretDB v1 with PostgreSQL: works. With MySQL or MariaDB: experimental. With
SAP HANA: experimental, amd64 only.
- FerretDB 2 with PostgreSQL/DocumentDB: not tested.
- MongoDB 6, 7, 8 and 9: the query conformance catalogue answers the same on
6.0.28, 7.0.43, 8.0.32 and 9.0.2. WeKan's Mocha tests and dev server run on
Meteor's bundled MongoDB 8.0.29, and docker-compose-mongodb-v7.yml runs WeKan
on MongoDB 7.
- MongoDB refuses to start on Linux kernels 6.19 to 7.0.13 (Ubuntu 26.04
included) until MongoDB 8.0.35 or 9.0.4 or kernel 7.0.14; FerretDB is not
affected.
- MongoDB 3.x to 5.x: not tested with current WeKan; only to dump an old
database and restore it into a supported one.
</details>
<details>
<summary>Version</summary>
- Version numbers v11.33, v11.54, v11.57, v11.59 and v11.61 do not exist: a bug
in `releases/release-all.sh` measured the version step from the two newest
CHANGELOG headings instead of always advancing by one, so once a prepared-
but-never-published release had its heading removed rather than renamed back
to `# Upcoming WeKan ® release`, the resulting gap was read as the new normal
cadence and re-applied on every later release, repeatedly skipping a number.
Fixed to a fixed +1 step; nothing was lost, these numbers were simply never
used.
- WeKan 8.75 and newer uses Meteor 3.5
- WeKan 8.43 upgraded to Meteor 3.x, huge thanks to harryadel:
- https://harryadel.com/dev-diary-24/
- https://harryadel.com/dev-diary-25/
- https://harryadel.com/dev-diary-26/
- WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after
use mostly Font Awesome 4.7 icons.
- WeKan 8.00-8.06 had wrong raw database directory setting
/var/snap/wekan/common/wekan and some cards were not visible, it was fixed at
WeKan 8.07 where database directory is back to /var/snap/wekan/common and all
cards are visible.
</details>
<details>
<summary>TODO Later</summary>
<details>
<summary>Carried to a future release.</summary>
What is not built yet, checked against the code on 2026-10-17: nothing. The
helpers that need no AI and the whiteboard are built and in the Upcoming
release. New work that cannot be done here is added below by category, with
its reason.
</details>
<details>
<summary>Need the running app to reproduce/verify</summary>
- The WebKit stage cannot run in the Flatpak sandbox: MiniBrowser needs its
own sandbox, and turning that off is not allowed there. Run it on a host.
- [#6834](https://github.com/wekan/wekan/issues/6834) (the minicard due date is fixed; the board header bar wrapping to a second row and the list header icons under the card count did not reproduce on the current code at the same window width, in Persian, with the card count on - which WeKan release and browser zoom showed them is asked; wrapping the header rather than hiding buttons is by design)
</details>
<details>
<summary>Needs a maintainer decision on the intended contract</summary>
- [#6837](https://github.com/wekan/wekan/issues/6837) (a Simple and an Advanced mode, or profiles an administrator defines and assigns, to hide features from new users: which features each mode or profile shows, who chooses it, and how it relates to the existing per-board Board Settings / Card and Show on Minicard switches and the Admin Panel feature toggles)
</details>
</details>
# Upcoming WeKan ® release
**In short:** **CodeQL** alerts #602 and #603 in the tests are fixed - no
test hands a path to a shell any more - and **fast-xml-parser** is updated to
5.12.0 for Dependabot alert #188, its quadratic scan of an unterminated stop
node.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2b65b713a0f7b537efbe7c25bb2a8b454e88d1ad">Alerts #602 and #603: no test hands a path to a shell at all</a>. Thanks to GitHub CodeQL and xet7.</summary>
The fix for #601 passed the paths to `bash -c` as arguments rather than
pasting them into the script, but that still hands a temporary path to a
shell, and CodeQL flagged the two umask tests in
`tests/releaseDownloads.test.cjs` again. They no longer use a shell: `fetch.sh`
runs as bash's program with its arguments, the umask is set on the test's
process and put back, and `chmod +x` is `fs.chmodSync`. The macOS sandbox test
runs its probes as programs the same way. The guard now refuses any value
given to a shell's `-c` script as an argument, and found one more test doing
it. The findings were in tests, not in WeKan, so there is no attempt to show in
Admin Panel → Problems.
</details>
and updates the following dependencies:
- [**fast-xml-parser 5.11.2 → 5.12.0**](https://github.com/wekan/wekan/commit/a76a19058ac896651ccd30311c36d48ee7f47b86) — Dependabot
alert #188: with `stopNodes`, an unterminated stop node made it rescan the
rest of the input from every `<`, O(n²) CPU; 5.12.0 stops at the first
failed scan. Brought in by the Azure and Google Cloud storage clients. The
test runs the alert's input and pins that no lockfile resolves an older
version. Thanks to dependabot.
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.06 2026-11-11 WeKan ® release
**In short:** **CodeQL** alert #601 in a test is fixed, with two more tests
that had the same fault. A card's **overtime** set through the REST API is
saved where WeKan reads it, and cards that lost it before are repaired at
startup. Card **members and assignees** whose account was deleted can be removed
and added without an error, and clearing a card **date** no longer passes as a
date set to the rules, the due date count and the activity feed.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d482856e03129d13c6e171a1503db60d81ab5166">Alert #601: no path is quoted into a bash -c script in the tests</a>. Thanks to GitHub CodeQL and xet7.</summary>
CodeQL flagged a shell command built from environment values (CWE-78, CWE-88)
in `tests/npmRetry.test.cjs`, whose `bash -c` script pasted a temporary path
and a checkout path into its text with `JSON.stringify` - JSON's quotes are not
the shell's, so a path with `$(`, a backtick or `$` would change the command.
The paths now go in through the environment. The guard written for alerts #549
and #550 only knew `source "${path}"`; it now refuses any value quoted into a
`-c` script across the tests, and found the same fault in
`tests/releaseDownloads.test.cjs` and `tests/macosSandbox.test.cjs`, fixed as
arguments. The finding was in tests, not in WeKan, so there is no attempt to
show in Admin Panel → Problems.
</details>
and fixes the following bugs:
**Card overtime** - what the REST API stores, and what it stored before.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f74ef4c40d85f9f6b91b8f4ecfc7372f67d32100">A REST card update saves isOverTime as the isOvertime WeKan reads</a>. Thanks to AhmedLukman.</summary>
A PUT with `{"isOverTime": true}` answered 200 and changed nothing anyone saw:
the handler wrote `isOverTime`, while the schema and the UI use `isOvertime`.
The request keeps its spelling and the validated value is stored in
`isOvertime`. Browser tests cover JSON and form updates, the stored value, a
REST read, the open card, a reload, and refused requests, and the unit test
pins the stored field name.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/569756b66c0fc3e3c41aba93fa8311994c6fa754">An isOverTime stored before that fix becomes isOvertime at startup</a>. Thanks to AhmedLukman and xet7.</summary>
Every overtime set through the API since `b895e61237` was stored under the
field nothing reads. The `card-overtime-field` schema upgrade step folds it
into `isOvertime` where the card has none, keeps the value the UI already shows
where it has both, and removes the stray field - version-gated and idempotent
like the steps beside it. Tests pin the fold, the precedence and a second pass
finding nothing; as negatives, no work on cards without the field, and no
server code writing `isOverTime` into a card.
</details>
**Card members and assignees** - people whose account is gone.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5e9d1cb6305a4a36f9cb890c7f5babb66ab4f43c">A member or assignee whose account is gone can be removed</a>. Thanks to AhmedLukman.</summary>
Removing a card member or assignee whose user account no longer exists read
`user.username` of nobody and answered HTTP 500, so the stale id could never
be removed. Both removal hooks now look the name up safely and still record
the removed id. Unit tests run the real hooks, and browser tests cover stale,
missing, repeated and normal removals with their authentication and scope
checks.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/32590ee7ab89b41ca6f764a58ec0f43d56d48ab8">Adding a member or assignee whose account is gone no longer throws either</a>. Thanks to AhmedLukman and xet7.</summary>
The joining halves of the same two hooks had the same read. The REST route
checks the user before adding, but a client update, a copied or imported card,
or a rule can still add such an id. Both now record the join with the id and
no username. Tests run the real hooks, and a tree-wide test finds no
`const x = user.field;` straight after a user lookup without a guard.
</details>
**Card dates** - what clearing a date records.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/50334816caf227237124b007c53a467c8f6919f5">Clearing a card date no longer passes through the timing hook as a date set</a>. Thanks to xet7.</summary>
The hook that records `a-dueAt` and the other date activities read
`modifier.$set[field]` for every date update, but clearing a date sends only a
`$unset`: without a `$set` the hook threw, and with one beside it an `a-dueAt`
without a value was written - counted as a due date change (#6081), matched by
the "date set or changed" rule triggers (#2474) and shown as an empty date in
the activity feed. Now a date set records its value, a date cleared records
only the date it had - as a rule's removeDate does, and as the board timeline
rewinds - and clearing a date the card does not have records nothing. The rules
skip a cleared date, and the feed shows only the date it had. Tests run the real
hook with a set, a `$unset` alone and a `$unset` beside a `$set`, and a
tree-wide test finds no `modifier.$set[...]` read that assumes the `$set` is
there.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.05 2026-11-10 WeKan ® release
**In short:** WeKan is updated to **Meteor 3.6**. The **Android** bundles
build again, after v13.04's npm retry broke their install, and the **Super
Productivity** export no longer fails for a board with a card whose list is
archived.
This release updates the following dependencies:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0fbef8936fa2524d21048669e4c41be15e4e3f38">Updated to Meteor 3.6</a>. Thanks to xet7.</summary>
From 3.6-rc.1. `meteor update` refused it, because WeKan's forks of six Meteor
packages in `packages/` declared 3.6-rc.1's versions, and a local package
overrides the release's. Meteor 3.6 changed those six only in their version
line, so each fork now declares 3.6's version with its `_1` wrap number. The
Dockerfile builds with Meteor 3.6, and the Bun build's patch applies unchanged
to it as `meteor-3.6.patch`. WeKan builds with Meteor 3.6, and the node suites
pass.
</details>
and fixes the following bugs:
**Release builds** - what the release workflow builds.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d08f658deae3b8369b75f1c54371ad8c1cf9367d">The Android bundles build again: npm-retry.sh is given Android's node and npm</a>. Thanks to xet7.</summary>
v13.04's four Android bundle jobs all failed with "environment: node: unbound
variable". v13.04 sent the Android npm installs through
`releases/npm-retry.sh` by the name of `build-bundle.sh`'s own `npm` shell
function. `npm-retry.sh` is a new bash with `set -u`: the exported function
reached it, but the `$node` and `$npm_cli` it reads did not, so it failed before
npm ran. The two installs now give `npm-retry.sh` the program itself, still
retried on a network error. `tests/npmRetry.test.cjs` fails when any
`releases/` script gives `npm-retry.sh` one of its own shell functions, and
reproduces the failure.
</details>
**Exports** - a board in another tool's format.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8b4fe0e7376194c498219a0b496340a94020098a">The Super Productivity export no longer fails on a card whose list is archived</a>. Thanks to xet7.</summary>
A card whose list is archived is collected with an empty list title. The
Super Productivity formatter tagged that card with a tag that did not exist, so
the export threw "Cannot read properties of undefined (reading 'taskIds')" and
the whole board's export failed. That list is now "Untitled", for its tag and
its panel alike, so the panel shows its cards and every tag a task names
exists. `tests/superProductivityFormat.test.cjs` fails with that error on the
old formatter. It was found while checking Weddy's Super Productivity export
against WeKan's formatter.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.04 2026-11-09 WeKan ® release
**In short:** **ListLockBleed** is fixed: when a list refuses a role write
access, that refusal now also holds for the checklist and checklist-item REST
routes. The **Sandstorm** and **Android** bundles retry their npm install when
github.com is briefly unavailable.
This release fixes the following CRITICAL SECURITY ISSUE of [ListLockBleed](https://wekan.fi/hall-of-fame/listlockbleed/):
<details>
<summary><a href="https://github.com/wekan/wekan/commit/214b20e7ad">A list that refuses a role write access refuses its checklist REST writes too</a>. Thanks to tsurutanmen and xet7.</summary>
[ListLockBleed](https://wekan.fi/hall-of-fame/listlockbleed/),
[GHSA-4734-8vrg-8j3w](https://github.com/wekan/wekan/security/advisories/GHSA-4734-8vrg-8j3w).
A board admin can refuse a role write access in one list (#4527). The card
and comment REST routes and the DDP checklist writes applied that refusal,
but the six checklist and checklist-item REST routes asked only the board. A
member the list refused could therefore still create, rename, tick and delete
the checklists of its cards with an API token. Those routes now ask the
card's list and answer 403, and the refusal shows in Admin Panel / Problems.
`tests/listPermissions.test.cjs` fails for any REST write route of the tree
that names a card without asking its list. And
`tests/playwright/specs/list-lock-checklists.e2e.js` replays the six writes
against a running server.
</details>
and fixes the following bugs:
**Release builds** - what the release workflow does when a download fails.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c3f12b5659">The Sandstorm and Android bundles retry their npm install through a GitHub outage</a>. Thanks to xet7.</summary>
v13.03's Sandstorm .spk failed with "npm error 504 Gateway Time-out" for
github.com's `meteor/node-source-map-support` tarball: the same kind of
outage `releases/npm-retry.sh` has retried since v10.86. That install in
`releases/sandstorm-slim-spk.sh`, and the Android bundle's server and sharp
installs, ran npm without it, because `tests/npmRetry.test.cjs` read only
`release-all.yml`. All three go through the retry now. The test follows
every `releases/` script a workflow runs and fails on a bare npm install in
any of them.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.03 2026-11-09 WeKan ® release
**In short:** The amd64 **snaps** no longer fail the release when the Ubuntu
archive is briefly unreachable: the build waits for it and retries once.
This release fixes the following bugs:
**Release builds** - what the release workflow does when a build fails.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a7ace18af8">The amd64 snaps wait out an Ubuntu archive outage instead of failing</a>. Thanks to xet7.</summary>
v13.02's native amd64 snap and both amd64 snap variants failed within eight
minutes of each other with "Failed to refresh package list: failed to run apt
update" while Snapcraft installed build-packages; the arm64 snaps, served by
`ports.ubuntu.com`, built. `releases/snap-refresh-managed-apt.py` recovered
only a stale `.deb` 404, so the one guarded retry never ran. It now also
recognises a failed apt update in the managed build instance, retries
`apt-get update` there with a 30/60/120/240 s backoff, and lets the Snapcraft
retry run only once the archive answers; a longer outage stays fatal. It
prints apt's own error lines first, so a failing job says why apt failed.
`tests/snap-managed-apt.py` pins the recovery, its bound, and that unrelated
failures are still refused.
</details>
and has the following developer-tooling fix:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6cc84a07cd">The security regression inventory counts UnassignedArchiveBleed as guarded</a>. Thanks to tsurutanmen and xet7.</summary>
`tests/securityRegressionCoverage.test.cjs` failed once the Hall of Fame
published UnassignedArchiveBleed, because its inventory did not have the name.
It is guarded now by `tests/assignedCardMethods.test.cjs` and
`tests/playwright/specs/assigned-card-methods.e2e.js`, which name it.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.02 2026-11-09 WeKan ® release
**In short:** **UnassignedArchiveBleed** is fixed: a member with the
**Normal, assigned only** role archives and converts only the cards assigned
to them, through every card method.
This release fixes the following CRITICAL SECURITY ISSUE of [UnassignedArchiveBleed](https://wekan.fi/hall-of-fame/unassignedarchivebleed/):
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1513b2e98c">An assigned-only member archives and converts only the cards assigned to them</a>. Thanks to tsurutanmen and xet7.</summary>
[UnassignedArchiveBleed](https://wekan.fi/hall-of-fame/unassignedarchivebleed/),
[GHSA-6xqf-jh8x-4wxc](https://github.com/wekan/wekan/security/advisories/GHSA-6xqf-jh8x-4wxc).
The DDP methods `archiveSelectedCards` and `createBoardFromCard` asked only
for the board's write capability, which the Normal, assigned only role has, so
such a member who knew a card's id archived any card of the board - and its
subtasks - and turned any card into a linked-board card. Both now also ask
the card, as `copyCard` does; a selection is checked whole before the first
card is archived. A refusal is shown in Admin Panel / Problems.
`tests/assignedCardMethods.test.cjs` fails for any server method that takes a
card id and decides by the board's write access alone.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.01 2026-11-08 WeKan ® release
**In short:** Translations: the strings still in English are written in **33
more languages**, **Sardinian** is Sardinian instead of Italian and **Moroccan
Arabic** is Darija instead of Persian. Dependabot's updates of **mailparser**,
**isomorphic-git**, **pdfjs-dist** and the S3 libraries are checked and kept,
with the S3 client brought to the version the upload library needs.
This release updates the following dependencies:
- **mailparser 3.12.4 → 3.14.2** — inbound email and bounces; it brings
nodemailer 10.3.1, libmime 5.6.4 and @zone-eu/mailsplit 5.4.30, the same
authors' packages ([#6886](https://github.com/wekan/wekan/pull/6886)).
- **isomorphic-git 1.44.8 → 1.44.10** — the GitHub wiki sync; reads commit and
tag headers the way git does ([#6885](https://github.com/wekan/wekan/pull/6885)).
- **@aws-sdk/lib-storage 3.1155.0 → 3.1161.0** — S3 uploads
([#6884](https://github.com/wekan/wekan/pull/6884)); it needs the
client-s3 below.
- **filesize 11.0.28 → 11.0.30** — file sizes shown
([#6883](https://github.com/wekan/wekan/pull/6883)).
- **pdfjs-dist 6.4.299 → 6.5.438** — PDF previews
([#6882](https://github.com/wekan/wekan/pull/6882)).
- **mongodb 7.7.0 → 7.8.0** in tests/playwright — the browser tests' seeding
only ([#6880](https://github.com/wekan/wekan/pull/6880)).
- **helm/chart-releaser-action 1.8.0 → 1.9.0** and **vmactions/netbsd-vm
1.5.2 → 1.5.3** — CI, each still pinned to the commit its tag names
([#6881](https://github.com/wekan/wekan/pull/6881),
[#6879](https://github.com/wekan/wekan/pull/6879)).
Each was checked before it was kept: every package's integrity matches its
lockfile entry, none has an install script, the action pins are the commits of
their tags, and the new versions add no network calls or telemetry.
Thanks to dependabot.
- **@aws-sdk/client-s3 3.1159.0 → 3.1161.0** - [the version @aws-sdk/lib-storage 3.1161.0 requires as a peer](https://github.com/wekan/wekan/commit/e33b3f26d3db1f15fcea20542c6143d142e84f65); tests/dependencySecurityUpdates.test.cjs caught the mismatch, and only AWS SDK packages changed version. Thanks to xet7.
and updates the following translations:
**Languages updated:** 31 languages.
**Every string in the language** - what was still English, or another language.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1211221a0fe35a9be4b41ef2954935df1c71e62a">The strings still in English, written in 33 more languages</a>. Thanks to xet7.</summary>
Every value still equal to English was written in the locale's language from
its existing translations and kanban terms; only English placeholders were
filled, human translations were not touched and every `__x__` and `%s` is
English's. The commit names the languages written with low confidence.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d1d3beb12d62f245ca8f044e5d389e4edb950b92">Sardinian is Sardinian, not Italian</a>. Thanks to xet7.</summary>
1772 values of `sc.i18n.json` were Italian or Italian/Sardinian mixtures; the
locale tag is authoritative, so they are Sardinian now.
`tests/sardinianTranslationProgress.test.cjs` scans every value for words only
Italian has, and proves the scan catches the old strings.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/fe0e187fa0907cf77017e7029eeabdb2450ef1f1">Moroccan Arabic is Darija, not Persian</a>. Thanks to xet7.</summary>
374 values of `ary.i18n.json` were Persian, Persian words in Arabic text, or
English left in. `tests/moroccanArabicNotPersian.test.cjs` scans every value for
words only Persian has, pins repaired values and checks every placeholder.
</details>
and has the following developer-facing changes:
- [Companion repositories are found in .tools/w, .tools/xet7 and .tools/Secretchronicles](https://github.com/wekan/wekan/commit/11bed267889c77ffc6651e858a6eb6ec1e4c83a1). Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v13.00 2026-11-07 WeKan ® release
**In short:** **CreateBleed** is fixed: "Only admins can create boards" now
holds on every path that creates a board - the browser console, the REST API,
imports, copies, clones and cards - not only on the Add Board form. The **REST
API documentation** spells the name **WeKan**, so the API pages published on
wekan.fi pass its brand-spelling check again.
This release fixes the following CRITICAL SECURITY ISSUE of [CreateBleed](https://wekan.fi/hall-of-fame/createbleed/):
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5c1f6088f4">Only admins create boards on every path, not only through Add Board</a>. Thanks to tsurutanmen and xet7.</summary>
[CreateBleed](https://wekan.fi/hall-of-fame/createbleed/),
[GHSA-qcfm-2g4w-wqcc](https://github.com/wekan/wekan/security/advisories/GHSA-qcfm-2g4w-wqcc).
Admin Panel / Settings / Visibility / "Only admins can create boards"
([#4475](https://github.com/wekan/wekan/issues/4475)) was checked only inside
`createBoardWithInitialSwimlanes`, the Add Board form. A user who is not an
admin still created working boards with a console `Boards.insert` - the
board collection's allow rule approves any signed-in user - with
`POST /api/boards` and with both board import routes, as reported, and also
with `importBoard`, `cloneBoard`, `copyBoard`, `POST /api/boards/:boardId/copy`
and `createBoardFromCard`. The boards belonged to their creator; no other
user's data was read or changed.
One decision (`models/lib/boardCreationPolicy.js`) and one guard
(`server/lib/boardCreationGuard.js`) are now asked by every one of those paths
before they write, and by a new `Boards.deny` insert rule. A board template
stays every member's own, and boards WeKan makes by itself - a user's
Templates board, a board's subtasks board - are not user creations. A refused
REST call answers 403. A refusal is recorded in Admin Panel / Problems as
CreateBleed (`authz.board-creation`, low, CWE-863, never disables the
account), and Duplicate and "Create board from card" are hidden like Add
Board, so only a client bypassing the interface reaches it.
`tests/createBleed.test.cjs` drives the decision, the guard and the real deny
rule against the reporter's console insert, and fails if any path that creates
a board for a user does not ask the guard, or if anything else reads the
setting; `tests/playwright/specs/createbleed-board-creation-admin-only.e2e.js`
replays the report against a running server.
</details>
and fixes the following documentation bugs:
**REST API documentation** - the name is WeKan in the API reference, from the
generator to the page wekan.fi publishes.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4d5fe63fc4">The REST API documentation spells the name WeKan</a>. Thanks to xet7.</summary>
The header of `openapi/generate_openapi.py` and the JSDoc of six models said
"Wekan REST API", "extend Wekan", "a production Wekan server" and "in the
Wekan board", so every `public/api/wekan.yml` and the API pages built from it
said Wekan too, and the v12.95, v12.96 and v12.97 pages on wekan.fi failed its
`tests/brand-spelling.test.cjs`. The generator, the model comments and
`wekan.yml` now say WeKan. `tests/openapiBrandSpelling.test.cjs` checks the
generated file, the generator header and every model JSDoc block, and fails on
the files before this change.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0275363043">The built API page says WeKan, so the v12.99 page on wekan.fi does too</a>. Thanks to xet7.</summary>
The v12.99 version bump rebuilt `public/api/wekan.html` with the generator from
before the fix, and `releases/release-website.sh` copies that file to wekan.fi
as `api/v12.99/index.html`. Its visible text and the JSON Redoc rebuilds the
page from now say WeKan, and `tests/openapiBrandSpelling.test.cjs` checks the
built page as well.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.99 2026-11-06 WeKan ® release
**In short:** The **mac-arm64** release bundle builds again: its job asked for
GitHub's retired **macos-14** runner and was cancelled in the v12.98 release
run. The **weba** instructions now live in the weba repository itself.
This release fixes the following release build bug:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/7e4483b1a9">The mac-arm64 bundle is built on macos-15, because GitHub retired macos-14</a>. Thanks to xet7.</summary>
In the v12.98 release run, build-mac-arm64 was cancelled with "The
configuration 'macos-14-arm64-us-default' is not supported", so v12.98 has no
mac-arm64 bundle. The job now runs on `macos-15`, which is Apple Silicon too
and is what `mac.yml` already uses. `tests/workflowRunnerLabels.test.cjs` lists
`macos-14` as retired, so no workflow can ask for it again, and checks that
build-mac-arm64 asks for a current Apple Silicon runner.
</details>
and has the following developer documentation change:
- [The weba instructions moved from AGENTS.md and CLAUDE.md into the weba repository](https://github.com/wekan/wekan/commit/b4cb989391).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.98 2026-11-06 WeKan ® release
**In short:** **GroupLinkBleed** is fixed: a board admin can no longer skip the
board methods by writing the board directly, to attach an organization or team
they do not belong to or to add a member past the invitation checks.
**Public board CSV and TSV exports** honour the selected columns, and the REST
**card archive, restore and custom-field** endpoints answer an unmatched card
with a JSON 404 instead of 500, and a REST **card update** can set sort 0, turn
overtime off and clear a description. The **notifications drawer** gets its
attachments again, and the developer instructions cover the **weba** Gambas
companion repository.
This release fixes the following CRITICAL SECURITY ISSUE of [GroupLinkBleed](https://wekan.fi/hall-of-fame/grouplinkbleed/):
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e3458004ec">Board admins can no longer attach foreign teams or add members by direct board writes</a>. Thanks to bhaswanthc and xet7.</summary>
[GroupLinkBleed](https://wekan.fi/hall-of-fame/grouplinkbleed/),
[GHSA-v544-rmr5-38gx](https://github.com/wekan/wekan/security/advisories/GHSA-v544-rmr5-38gx).
The board allow rule lets a board admin update any field, and two checks lived
only in Meteor methods: adding only the organizations and teams the admin
belongs to ([#5141](https://github.com/wekan/wekan/issues/5141)), and the
invitation's checks, including the same organization/team policy
([#6116](https://github.com/wekan/wekan/issues/6116)). A client writing the
board over DDP skipped both, so a whole team could be given an attacker's
board and any user added to it. Client writes naming `orgs`, `teams` or
`domains`, and client updates that add or reactivate a member, are now
refused and shown in Admin Panel / Problems; role changes and removals still
pass. `tests/groupLinkBleed.test.cjs` runs the real deny rules and fails if a
method-guarded board field is client-writable.
</details>
and fixes the following bugs:
**Exports** - the CSV and TSV columns a public board exports.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4762bed8c1">Public board CSV and TSV exports honour the selected fields</a>. Thanks to AhmedLukman and xet7.</summary>
On a public board the CSV and TSV export ignored `fields=` and wrote all 20
columns, while a private board wrote only the selected ones (PR
[#6875](https://github.com/wekan/wekan/pull/6875)). The validated selection is
now parsed once, before the public/private branch, so both get the same
columns; a missing, empty or unrecognised selection still exports all of them,
and the export guards are unchanged. `tests/publicCsvExportFields.test.cjs`
drives the route for comma, semicolon and tab, and a Playwright spec checks the
export menu's checkboxes on a public board.
</details>
**REST API** - what the card endpoints accept, and answer when no card matches.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3b33c7d7ba">Card archive and restore answer a JSON 404 for an unmatched card instead of 500</a>. Thanks to AhmedLukman and xet7.</summary>
`POST …/cards/:cardId/archive` and `…/unarchive` threw `Meteor.Error(404)`
when no card matched the board, list, id and archive state. A WebApp handler is
a plain connect handler, so that reached the client as HTTP 500 (PR
[#6876](https://github.com/wekan/wekan/pull/6876)). They now answer through
`sendJsonResult` with `{ error: 'Card not found' }` and 404, writing nothing.
`tests/restCardArchive.test.cjs` and a Playwright spec cover a missing card,
the wrong list, the wrong board, the wrong state and refused callers.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b0a71ba57b">A card update can set sort to 0</a>. Thanks to AhmedLukman and xet7.</summary>
`PUT …/cards/:cardId` with `{ "sort": 0 }` answered 404 and left the card
where it was, because the handler read `sort` by truthiness (PR
[#6877](https://github.com/wekan/wekan/pull/6877)). It now writes any sort that
is present. `tests/restCardSort.test.cjs` and a Playwright spec check 0, other
numbers, form-encoded `'0'`, an omitted sort and refused callers.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b895e61237">A card update checks sort, isOverTime and description, so false and empty work</a>. Thanks to AhmedLukman and xet7.</summary>
Writing `sort` whenever it is present would also have written `null`, `''` or
`'abc'` as the card's position, and the same handler read `isOverTime` and
`description` by truthiness, so overtime could not be turned off and a
description could not be cleared. `parseCardScalarFields()` decides all three
by presence before the first write: sort a finite number, isOverTime a boolean
(`'true'`/`'false'` from a form too), description a string or `null` to clear.
Anything else is a 400 and the card is unchanged.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6d5f660c27">The card custom-field update answers a JSON 404 for an unmatched card instead of 500</a>. Thanks to AhmedLukman and xet7.</summary>
`POST …/cards/:cardId/customFields/:customFieldId` had the same
`throw new Meteor.Error(404)` as the archive endpoints, and now answers the
same JSON 404. `tests/restHandlerNotFound.test.cjs` drives it for a matching,
missing, wrong-list, wrong-board and archived card, and scans every
`WebApp.handlers` route in the tree so no REST handler answers not-found by
throwing again.
</details>
**Notifications** - what the notifications drawer receives.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/adb73fd8ee">The notifications drawer receives its attachments again, without a server exception</a>. Thanks to xet7.</summary>
Every `notificationAttachments` subscription failed with `Publish function can
only return a Cursor or an array of Cursors`: Attachments is an ostrio:files
FilesCollection, and the publication returned its FilesCursor instead of the
Mongo cursor it wraps. It now returns `.cursor`, as every other attachment
publication does. `tests/publishFilesCursor.test.cjs` runs the publication and
scans every publishing file so no attachment or avatar cursor query is
published without it.
</details>
and has the following developer-facing changes:
**Tests** - guards that had drifted from the code they pin.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/68b575637d">Three client guards follow the code they pin again</a>. Thanks to xet7.</summary>
`sidebarBelowHeader`, `maximizedCardPosition` and `titleClickToEdit` read
source that the full screen button, the phone-below-header rule and the
Pointer Events drag had since moved. The behaviour had not changed; the guards
now read the new places, `titleClickToEdit` calls `cardHeaderPress()` from
`models/lib/windowDrag.js`, and each still fails when its behaviour is removed.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/081690875d">The real single executable test says why it cannot run on a kernel with large pages</a>. Thanks to xet7.</summary>
`--build-sea` injects the SEA blob with LIEF, which adds 4 KiB-aligned
segments; a Linux kernel with 16 KiB or 64 KiB pages cannot map them, and the
executable dies with SIGSEGV - a one-line `console.log` SEA does too. The test
failed there as `null !== 0`; it is now skipped on such a kernel with that
reason. Release builders have 4 KiB pages and still run it.
</details>
**Developer instructions** - what an assistant working in this repository is
told.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1ef7a57ce4">AGENTS.md and CLAUDE.md describe the weba companion repository</a>. Thanks to xet7.</summary>
`.tools/weba` is the separate `wekan/weba` repository, a Gambas Web UI Kanban
with SQLite by default. The instructions say where its project, forms, browser
reports and database modules are, that WeKan's author, local-commit and no-push
rules apply there too, how to compile and run it with `gbc3`/`gbx3` from the
local Gambas under `.tools/bin/gambas3`, how its tests run on a copy of the
database, and to keep its SQL portable across Gambas database drivers.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.97 2026-11-06 WeKan ® release
**In short:** `build.sh` and the `releases/` scripts now work on **Arch Linux**
and **Omarchy**, and the release builds no longer lose the **OpenBSD arm64
bundle** to a smoke test that left WeKan running, or the **s390x snap** to
Launchpad outages that were counted as build failures.
This release fixes the following release build bugs:
**Release builds** - bundles and snaps that built correctly but failed the job.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b46f155fa3">The BSD smoke test stops WeKan and FerretDB and removes its files before returning</a>. Thanks to xet7.</summary>
v12.96 openbsd-arm64: `releases/bsd/smoke.sh` returned OK with WeKan and
FerretDB still running, because `start-wekan.sh` runs its TERM trap only after
the foreground node exits. FerretDB then removed `wekan.sqlite-wal` and `-shm`
while the VM's copyback rsync walked the workspace, and rsync exit 24 failed a
bundle that had built and started. The smoke test now stops both, waits until
they are gone and deletes its work directory. `tests/bsdBundles.test.cjs`
runs it against a launcher of `start-wekan.sh`'s shape and fails if a process
or a smoke-test file is left behind.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b46f155fa3">Launchpad outages seen only in snapcraft's execution log earn snap retries</a>. Thanks to xet7.</summary>
v12.96 s390x: all three attempts met Launchpad's `HTTP Error 503` /
`Error: Timeout`, but snapcraft printed only its last exception, "Could not
find repository at path ...", so the outage was counted as three WeKan
failures. `infra_failure()` now reads this attempt's snapcraft execution logs
too, and counts the "You do not have permission to create refs/" push refusal
from the half-created repository. `tests/snapLaunchpadInfraRetry.test.cjs`
pins both, and its negatives an earlier attempt's log and one with no outage.
</details>
and has the following developer-tooling fix:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c61a1b7f41">build.sh and releases scripts work on Arch Linux and Omarchy</a>. Thanks to xet7.</summary>
Omarchy is Arch with Hyprland: pacman, the Arch repositories and yay for the
AUR. `releases/ensure-tools.sh` names it and the other Arch derivatives
explicitly. Packages are installed with `pacman -S --needed` instead of the
unsupported `-Sy` partial upgrade; a full `-Syu` runs only on a never-synced
pacman or with `WEKAN_PACMAN_SYU=1`, and a failed install says to update first
(`omarchy-update` on Omarchy). 7zip replaces Arch's retired p7zip, snapcraft
gets snapd from the AUR with yay or paru, the SSH server menu entries use
`sshd.service` outside Debian/Ubuntu, and Playwright's browser libraries come
from pacman (or dnf) instead of the apt-only `--with-deps`, with WebKit in the
Playwright Docker image on Arch. `tests/archOmarchyScripts.test.cjs` drives
the helpers offline, and its negative test fails if any shell script does a
`pacman -Sy` partial upgrade or installs p7zip.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.96 2026-11-05 WeKan ® release
**In short:** The release **bundle starts again**: v12.95's build stopped on
boot because the **nodemailer** 10 raised inside Meteor's email package no
longer had the files that package was built against.
This release fixes the following bug:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5a21b074d4">The bundle starts again, with nodemailer 10 inside Meteor's email package</a>. Thanks to xet7.</summary>
v12.95 raised the nodemailer in `meteor/email` from 8.0.3 to 10.3.1, and
nodemailer 10 moved `lib/` to `dist/cjs/`. `packages/email.js`, compiled
against 8.0.3, links `lib/nodemailer.js` and `lib/well-known/index.js` and
`Npm.require`s `lib/mail-composer` by path, so build-amd64 failed its smoke
boot. `releases/bundle-npm-security-bumps.json` now shims nodemailer as it
already shims uuid 11: `lib/nodemailer.js` and every `lib/<name>` directory the
exports map lists are written back, forwarding to `dist/cjs`, with a
`lib/package.json` marking them CommonJS because nodemailer is `"type":
"module"`. A fresh local bundle taken through build-amd64's install, bump, trim
and prune steps passes `releases/bundle-smoke-boot.sh`.
`tests/bundleNpmSecurityBumps.test.cjs` requires the recorded paths through the
shim, checks every exported `lib/<name>`, and fails without the CommonJS marker.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.95 2026-11-05 WeKan ® release
**In short:** **Continuous backup** now runs on one server at a time, with a
lease, when WeKan has several servers. Missing-board **exports** and REST
updates of missing **integrations**, **users**, **organizations** and **teams**
answer 404 instead of 200, the **minicard** shows a picked Requested by and
Assigned by, a card stops offering the attachment "+" when uploads are blocked,
and the Docker image's **nodemailer**, **body-parser**, **qs**, **uuid** and
**postcss-selector-parser** are raised past the advisories a Grype scan found.
This release fixes the following bugs:
**Exports** - what the export routes answer when something is wrong.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0c0d666ffa">Missing-board exports return 404, and an over-long export token returns 400</a>. Thanks to AhmedLukman and xet7.</summary>
The board Excel export answered a missing board with HTTP 200 and only the text
`Board not found` (PR #6869). The two PDF export routes had the same fault, and
the Excel and PDF routes answered an over-long auth token with 200 as well,
while `export.js` already used 404 and 400. All of them now choose their status
the same way.
`tests/exportErrorStatuses.test.cjs` scans every `models/export*.js` route for a
text answer without a status, so the fault cannot return anywhere there; the
PR's route and browser tests stay.
</details>
**Continuous backup** - which server streams it.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/880ec2d707">Only one server streams it at a time, holding a lease</a>. Thanks to Raymond-Voogt and xet7.</summary>
With several WeKan servers on one database, every server applied the continuous
backup setting, so
each of them tailed the oplog and wrote its own archives into the same target
directory
(\#6861). The leader now holds a lease, one `continuousBackupLease` document
with its id and an
expiry. Taking it is one atomic operation, the leader renews it every 15 seconds
and it lapses after
45, so another server takes over within a minute when the leader stops, and a
server that loses the
lease stops its stream. Startup, saving the settings and a change saved on
another server converge
through that loop; a restore or a fetch from the cloud takes the lease and waits
one renewal so it
is the only writer. One server is always its own leader.
Checked against the real driver on FerretDB (SQLite): 24 clients at once give
exactly one leader, and
renewal, expiry, the old leader losing the lease and release behave.
`tests/continuousBackupLease.test.cjs`
pins the same and the wiring.
</details>
**Attachments** - what a card offers when the administrator blocked uploads.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d8b3579117">A card no longer offers the attachment "+" when uploads are blocked for the whole site</a>. Thanks to Raymond-Voogt and xet7.</summary>
Admin Panel / Attachments / Transfer limits already has "Attachments upload" and
"API upload" set to
Blocked, and the server refuses on every path: the upload pipeline,
`api.attachment.upload` and
`.copy`, and the `/api/attachment` upload, upload-background and copy routes.
The card still showed
the "+" button and accepted a drop or paste for an upload that would then be
refused. It now asks
the server one boolean, at most once a minute, and hides both. No second setting
was added
(\#6870 proposed a `disableAttachments`), so nothing can disagree with what the
server enforces.
`tests/attachmentUploadPolicy.test.cjs` pins the client, the method, the server
enforcement it relies
on, and that no `disableAttachments` setting exists.
The method was not imported at first, so the call failed and the "+" stayed;
[a follow-up](https://github.com/wekan/wekan/commit/3aaa46fdae) loads it, and
`tests/serverModulesLoaded.test.cjs` now passes.
</details>
**REST API** - what the integration, user, organization and team update routes
answer for something that is not there.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6012521ccc">An integration update for a missing or wrong-board integration returns 404</a>. Thanks to AhmedLukman and xet7.</summary>
`PUT /api/boards/:boardId/integrations/:intId` answered 200 with the requested
id when no
integration matched that id and board (PR #6871). The two activities routes
(`POST` and `DELETE`
`.../activities`) updated nothing and answered 200 with `data: null`. All three
now look the
integration up on that board after board-admin authorization and answer the JSON
404 the `GET`
route already uses, before validating or writing anything.
`tests/restIntegrationWriteRoutes404.test.cjs` pins, for the whole file, that
every handler that
updates an integration looks it up first and only after authorization; the PR's
route and browser
tests stay.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d3edc56170">An update of a missing user returns 404</a>. Thanks to AhmedLukman and xet7.</summary>
`PUT /api/users/:userId` answered 200 with no data when no user had that id,
for every action (PR #6873). After the admin check it now answers a JSON 404
`User not found` before taking ownership, disabling or enabling a login.
`tests/restUserUpdate.test.cjs` runs the handler for every action against a
missing user, and checks that authorization still comes first, that
self-disable stays a no-op and that a database failure stays a safe 500;
`tests/playwright/specs/rest-user-update.e2e.js` drives it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4d44349ab6">A feature update of a missing organization or team returns 404</a>. Thanks to AhmedLukman and xet7.</summary>
`PUT /api/admin/orgs/:orgId/features` and `PUT
/api/admin/teams/:teamId/features`
answered 200 with `data: undefined` when there was no such organization or team
(PR #6874). They now answer a JSON 404 `Organization not found` / `Team not
found`. `tests/restPeopleFeatures.test.cjs` runs both handlers for missing and
existing records, empty bodies, denied authorization and database failures;
`tests/playwright/specs/rest-people-features.e2e.js` checks the Admin Panel /
People feature checkboxes.
</details>
**The minicard** - who requested and who assigned a card.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/22e5df55d7">Requested by and Assigned by show when a member was picked, with one icon per field everywhere</a>. Thanks to mueschel and xet7.</summary>
The opened card's + button picks board members into `requesters` /
`assigners`, and the free-text name is optional, but the minicard drew only the
free text: a requester picked the usual way never showed although Show on
Minicard was on (#6872). The minicard now shows the picked members as avatars
beside any typed name. Card Settings also had the icons swapped - a head with a
plus for Requested by and a cart for Assigned by - and now uses the cart and
user-plus that the opened card and the minicard use.
`tests/minicardRequestedAssignedBy6872.test.cjs` pins the avatars, the
either-or condition and one icon per field, and fails on a minicard block gated
on the free text alone;
`tests/playwright/specs/minicard-requested-by-6872.e2e.js`
drives it.
</details>
and updates the following dependencies:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dad03959d9">nodemailer 10.3.1, body-parser 1.20.9 and 2.4.0, qs 6.16.1, uuid 11.1.1 and postcss-selector-parser 6.1.5 in the bundle</a>. Thanks to the Grype scan reporter and xet7.</summary>
A Grype scan of the image found High and Medium advisories in packages that
Meteor's own packages carry inside `programs/server/npm/node_modules/meteor/`,
where `package.json` and its `overrides` cannot reach.
`releases/bump-bundle-npm-deps.mjs` raises them after `meteor build`.
- **nodemailer 8.0.11 to 10.3.1** (GHSA-2x7j-588g-ccc2, GHSA-v53p-9fqp-m79j,
GHSA-p6gq-j5cr-w38f and the rest fixed in 10.1.4 or later). It was listed as
not fixable because the fix is a new major. `meteor/email` only calls
`createTransport`, the `stream` plugin hook, `nodemailer/lib/well-known` and
`nodemailer/lib/mail-composer`, and all four were run against 10.3.1 through
the bundle's own package specifiers.
- **body-parser 1.20.6 and 1.20.8 to 1.20.9, and 2.3.0 to 2.4.0**
(GHSA-27r3-jxmh-p828). A minimum may now be written `body-parser@2`, which
applies to copies of that major only, so each major is raised inside itself.
- **qs to 6.16.1** (GHSA-6pq8-jhm2-2vcw) and **postcss-selector-parser 6.1.4 to
6.1.5** (GHSA-rj75-hqrm-r3gf).
- **uuid 8.3.2 and 9.0.1 to 11.1.1** (GHSA-w5hq-g745-h8pq). uuid 11 moved its
entry point from `dist/index.js` to `dist/cjs/index.js`, which killed the
server on boot the one time it was tried. The bumper now writes the old
`dist/index.js` back as `export * from './esm/index.js'`: it is an ES module
because uuid is `"type": "module"`, and Node 26 `require()`s it.
glob 14 and minimatch 11 were already in `package.json`; the scanned image
predates them. The runtime image installs no compiler (`gcc-14-base` is Debian's
one-file package), so the multi-stage build the report asks for is already how
it is built. Run against a copy of the published image's bundle, the bumper
raised all of the above and every raised copy loaded.
**This release's bundle did not start.** "Every raised copy loaded" was checked
by loading each package, not by booting the bundle: nodemailer 10 moved `lib/`
to `dist/cjs/`, Meteor's `email` package still links the `lib/` paths of 8.0.3,
and build-amd64 failed its smoke boot with "Cannot find module
.../meteor/email/node_modules/nodemailer/lib/nodemailer.js". The fix is in the
next release.
`tests/bundleNpmSecurityBumps.test.cjs` pins the minimums, the per-major
rule and the shim, and runs the bumper with `--dry-run` on a bundle of
body-parser, uuid and nodemailer copies at, below and above their minimums.
</details>
and has the following documentation change:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/016b4bf3bd">Only maintainer mode commits directly and updates changelogs, and a pull request has one feature or fix</a>. Thanks to xet7.</summary>
CLAUDE.md and AGENTS.md now say that a pull request contributor, to wekan/wekan
or to the maintainer's repositories under `.tools/`, sends each feature or fix
as its own pull request with only related changes in it, and changes no
changelog. The maintainer, or an agent working as the maintainer, writes the
entry after merging, when there is a commit to link.
`tests/changelogFormat.test.cjs` checks both files.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.94 2026-11-04 WeKan ® release
**In short:** On an iPad the **opened card covers the header bar** instead of
sitting
behind it, and the speed test measures where the time goes when a WeKan board is
opened, on FerretDB and on MongoDB.
This release fixes the following bugs:
**Opened cards** - where a card opens.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/efaf94242e">On a phone in desktop mode the opened card starts below the header bar</a>. Thanks to xet7.</summary>
The header wraps to several rows on a phone, far taller than the 8px the
floating card docks at, so the card's top row was among the header's rows and
was only seen by dragging it down. Below 801px a card without an inline top,
popup cards included, now starts at `--wekan-header-height` + 8px. A dragged
place is an inline top, so it is kept when the finger is lifted.
`tests/cardBelowHeaderOnPhone.test.cjs` pins the rule and that it never
matches a dragged card.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/006d24502c">The opened card covers the header bar on an iPad instead of sitting behind it</a>. Thanks to xet7.</summary>
On iPad Chrome (and Safari) the opened card's top row - the collapse caret, the
card
number, the menu, maximize and X - was behind the header bar, although the
card's
`z-index` is 2001 and the header's 1000. iOS WebKit makes every scrolling
element
(`#content`, the board wrapper) a stacking context of its own, so the card's
`z-index`
is compared only inside it, and the header bar, itself a stacking context, is
painted
over all of it. While a card is open the header bar is now at the level of the
content,
which comes after it in the page and paints over it; with no card open it keeps
its
own level. A browser spec makes the same trap with `isolation: isolate` on
`#content`
and the board wrapper and shows the card over the header, open and collapsed,
with its
close button reachable, in Chromium and WebKit; without the rule it fails as the
screenshot shows. `tests/cardOverHeaderStacking.test.cjs` pins the rule and its
selector. An iPad is the final check.
</details>
and has the following documentation change:
**Database speed test** - how fast the databases answer WeKan's queries.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3621413d6f">The speed test measures where the time goes when a WeKan board is opened, on FerretDB and on MongoDB</a>. Thanks to Raymond-Voogt and xet7.</summary>
`docs/Databases/FerretDB/1/SpeedTest/app` opens a board of 200, 2,000 and 5,000
cards in
Chromium on WeKan with FerretDB (SQLite) and with MongoDB 8, and profiles the
Meteor
server (Node inspector) and FerretDB (pprof). Two runs each. The Meteor server
uses
0.1-0.5 s of CPU, the same on both, and its DDP server is 1% of that; the
browser gets
the same ~210 KB at every size. A 5,000-card board takes FerretDB 400-600 ms
longer to
open (first card +42-48%), a small board only 70-100 ms, and FerretDB spends
about 4 s
of CPU on it, 39% in the SQLite engine and 34% filtering in Go what it read. So
a
backend rewritten in another language would save part of a fraction of a second,
while
the part that scales with the board is the database layer. The README says what
was not
measured: a production build, the browser's own time, and which query sends the
`aggregate`. `tests/boardOpenProfile.test.cjs` pins the analysis of the profile
and what
the README claims.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.93 2026-11-04 WeKan ® release
**In short:** The database **speed test** has a new small-scale result, ordered
from fastest to slowest with how many times slower each database is, and two
more databases: **pgrust** (PostgreSQL in Rust) and **TiDB**. Noria, YugabyteDB
and CockroachDB were looked at and cannot be used, for reasons the README gives.
This release adds the following new features:
**Database speed test** - how fast the databases answer WeKan's queries.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/46c0ad43e8">Results.md is a quick small-scale run, fastest first, with how many times slower each is</a>. Thanks to Raymond-Voogt and xet7.</summary>
The speed test was only ever run at twice a production WeKan, which takes hours.
`WEKAN_SPEEDTEST_SCALE=0.05` is 1/20 of one and the whole run takes under half
an
hour. Results.md opens with an **At a glance** table: rank, the total of all
queries, and **how many times slower** than the fastest database, for the
average
user and for the slowest one (the biggest board or the busiest user). Every
table
is ordered by its own numbers, so the slowest-user table can rank differently,
the
loading table is ordered by the whole load time, and the header says the real
data
size and the real operating system (it said "twice a production WeKan" and
"Linux" whatever was measured). `tests/dbSpeedTest.test.cjs` pins the ranks, the
ratios, the per-table order and the header, with a negative test for each.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/46c0ad43e8">The speed test includes pgrust, PostgreSQL rewritten in Rust</a>. Thanks to Raymond-Voogt and xet7.</summary>
[pgrust](https://github.com/malisper/pgrust) v0.4-beta is wire compatible with
PostgreSQL 18, so FerretDB's PostgreSQL handler uses it unchanged, and it gives
the
same answers as MongoDB. It is **not faster** than PostgreSQL for WeKan's
queries
(174 ms against 145 ms in total), and creating WeKan's 47 indexes takes 38 s
against 1 s. In the conformance run it is opt-in (`WEKAN_CONFORMANCE_PGRUST=1`).
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f576a0cca9">The speed test includes TiDB, and the README says which other databases cannot run FerretDB and why</a>. Thanks to Raymond-Voogt and xet7.</summary>
TiDB 7.5 (Apache-2.0, MySQL protocol, one process on its own storage) runs
FerretDB's MySQL handler and gives the same answers as MongoDB, after one change
in FerretDB: its metadata table's generated columns are `VIRTUAL`, because TiDB
refuses `ALTER TABLE ... ADD COLUMN ... STORED`. Opt-in in the conformance run
(`WEKAN_CONFORMANCE_TIDB=1`). Noria has no JSON type, triggers or generated
columns, which the MySQL handler needs; YugabyteDB refuses a `CREATE INDEX` on a
`jsonb` expression, which every FerretDB v1 PostgreSQL index is; CockroachDB is
under a proprietary licence. The README's **Other databases tried** has the
reasons.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1c44876beb">The speed test includes Dolt, and VersionHistory tests what version history would give undo and redo</a>. Thanks to Raymond-Voogt and xet7.</summary>
[Dolt](https://github.com/dolthub/dolt) (git for data, MySQL protocol) runs under
FerretDB's existing MySQL handler and gives the same answers, but is by far the
slowest: 20,470 ms in total, 1,230x MongoDB, because every query over cards is a
2.8 s whole-table scan. `docs/Databases/FerretDB/1/VersionHistory` drives
WeKan-shaped changes through FerretDB onto Dolt and through the REST API of
TerminusDB. Dolt makes a commit per FerretDB write for +0.4 ms, undoes and
redoes
with `DOLT_REVERT` and reads any earlier state with `AS OF`, but names the SQL
user and not the WeKan user, and cannot restore one field of a document that
changed again (a conflict). TerminusDB records the author and message of every
commit and restores one field while later changes stay, but speaks neither the
MongoDB protocol nor SQL. WeKan's own History remains the undo; Dolt would be a
backup and audit layer under it. ClickHouse and StarRocks were tried with the
MySQL handler and need a handler of their own, which has not been written.
`tests/versionHistoryExperiment.test.cjs` and `tests/dbSpeedTest.test.cjs` pin
the
scripts and the findings.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/96d56ecc6c">Go databases that could be linked into FerretDB were compared with its SQLite: none is faster where it counts</a>. Thanks to Raymond-Voogt and xet7.</summary>
`docs/Databases/FerretDB/1/SpeedTest/embedded` is a Go module of its own (pure
Go,
no cgo) that loads the speed test's cards into SQLite set up as FerretDB sets it
and into Pebble, Badger, bbolt, BuntDB, Nutsdb and Clover, and asks each the
questions WeKan asks of cards, at 15,377 and 153,768 cards. Every engine must
give
the right counts first. A board's cards take the same time on every engine that
is
not slower, because decoding the JSON dominates; the key-value stores win single
reads and writes by microseconds that a FerretDB call hides, and would make the
handler do in Go what SQL does in SQLite. Pebble is 12x slower on fetching a
board, Clover 90-190x slower in total, and bbolt with its default fsync 100-160x
slower on writes. The README accounts for every entry of the awesome-go
databases section. `tests/embeddedEnginesBench.test.cjs` pins the export, the
engines, the SQLite setup and the conclusion.
</details>
**The header bar** - what sits beside the logo.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f25957dcf3">A full screen button to the right of the logo in the header bar</a>. Thanks to xet7.</summary>
A button puts the whole page in full screen and takes it out again with the
Fullscreen API, in iPad Safari, desktop browsers and Android browsers. Its label
is the existing Maximize, so no language file changes. It is drawn only where
the
browser offers the API and the page may use it: iPhone Safari does not have it
for pages (use Add to Home Screen, the manifest says `display: standalone`), and
neither does a page in an iframe without `allowfullscreen`. The icon follows the
browser's own events, so leaving with Esc is understood.
`tests/fullscreenButton.test.cjs`
drives the pure part with fake documents, including a refusal, and
`tests/playwright/specs/header-fullscreen.e2e.js` checks the position, the
label,
entering, leaving, Esc and the negative case in Chromium and WebKit.
</details>
and updates the following dependencies:
- **minimatch 10.2.6 → 11.0.4** in the `npm-packages/exceljs` and vendored
`jade` lockfiles ([GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249),
Dependabot alerts \#180 and \#182), through overrides on `readdir-glob`,
`glob` 13 and
`test-exclude`, as the root lockfile has; a test reads every
`package-lock.json`.
- **klauspost/compress 1.18.0 → 1.18.7**, **golang.org/x/net 0.23.0 → 0.62.0**,
**golang.org/x/text 0.14.0 → 0.43.0**, **golang/glog 1.0.0 → 1.2.4** and
**OpenTelemetry 1.41.0 → 1.42.0** in the Go module of the embedded-engine
benchmark (Dependabot alerts \#183-\#187; `govulncheck` reports none). A test
holds
every `go.mod` at or above these floors.
Thanks to dependabot.
and fixes the following bugs:
**Opened cards** - where a card opens.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/84b0480aa8">A card opened as a popup covers the header bars instead of opening below them</a>. Thanks to xet7.</summary>
On an iPad, a card opened from the Roadmap view (and from search results and the
table view) opened below the top header, and collapsed it was a bar floating
over
the swimlane. A card opened as a popup got the generic wide-layout rule
`top: 97px` and `z-index: 30`, because the desktop-mode rules that dock an
opened
card at the top skip a popup, while the popup wrapper rule in the same file says
it must cover the header bars. It now docks at `top: 8px` with `z-index: 2001`
in
the wide layout; mobile mode keeps its full-window rule. This was reproduced in
Chromium and in WebKit with an iPad profile; whether it is the whole of what an
iPad shows is for the iPad to say.
`tests/cardPopupOverHeader.test.cjs` pins the rule and
`tests/playwright/specs/card-popup-over-header.e2e.js` shows the card over the
header, open and collapsed, and fails without the fix (top 97 against a header
88
high).
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1b05673200">The opened card and every popup move under a finger, on an iPad</a>. Thanks to xet7.</summary>
On an iPad the opened card did not move when dragged from the collapse caret,
the
space beside it, the card number, or the drag handle, while the page behind it
scrolled. The card window was moved by mouse events only, which a finger does
not
send while it moves, and its drag areas did not say `touch-action: none`, so the
browser took the gesture for a scroll. It is moved by Pointer Events now: the
header row or the handle starts the drag; a link or button in the row (the
caret,
close, the menu) still does its job when tapped and moves the window when
dragged,
without also collapsing or closing; the title still edits when tapped; and with
drag handles on only the handle moves it. The card number is above the title's
edit
zone, so it drags too. The window is kept within reach, and `touch-action: none`
is
on the card header, its handle and every popup's title bar and handle, so popups
move by finger as well.
`tests/windowDragTouch.test.cjs` pins the decisions and the wiring, and
`tests/playwright/specs/card-window-touch-drag.e2e.js` drags with a real touch
from
each place with handles on and off, taps the caret, drags with a mouse and moves
a
popup; the finger tests fail without the fix. Tested in Chromium; an iPad is the
final check.
</details>
and has the following documentation change:
**Database speed test** - what was improved, and what was tried and was not.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d05a42a5af">The speed test README lists the improvements since the last run, and what did not help</a>. Thanks to Raymond-Voogt and xet7.</summary>
SQLite sorts in the database and stores JSONB, PostgreSQL answers membership
from a GIN index and pushes down `$or`, `$elemMatch` and `$and`, MySQL and
MariaDB read through their indexes with multikey side tables, and WeKan no
longer scans whole collections every hour or at every start of every server. The
small run found one more: **PostgreSQL planned from no statistics** until
autovacuum analyzed a freshly indexed table, so a Due Cards query took 407 ms
and
84 ms after; FerretDB now analyzes the table when its indexes are created (the
PostgreSQL row went from 454 to 145 ms, and from last place to eighth). A
profile of Due Cards on SQLite found another: a membership test made SQLite turn
the whole `members` array into JSON text and parse it again for every row, and
reading it from the column instead cut the visible-boards query from 26 to 16 ms
(All Boards 6.9 → 4.9 and 5.9 → 3.9 ms, Due Cards 54.5 → 43.9 ms, the SQLite
total 82.7 → 68.3 ms) with identical answers. Tried and
not kept, with the numbers: sorting in the database on PostgreSQL (52 ms against
38-49 ms decoded in Go) and `ANALYZE TABLE` on MySQL and MariaDB (no change).
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.92 2026-11-03 WeKan ® release
**In short:** A missing **REST checklist** answers a JSON 404 instead of an
empty 500.
This release fixes the following bugs:
**REST API** - answers for things that are not there.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d231df56fc">A missing checklist, or one under another card, answers a JSON 404 instead of an empty 500</a>. Thanks to AhmedLukman and xet7.</summary>
[\#6867](https://github.com/wekan/wekan/pull/6867). `GET
/api/boards/:boardId/cards/:cardId/checklists/:checklistId` answered `500` with
no body when the checklist did not exist or belonged to another card. It now
answers `404` with `Checklist not found or does not belong to the specified
card`, the text the update and delete routes already use; the board-access check
and the parent-card check are unchanged. `tests/restChecklistGet.test.cjs`
drives
the registered handler (found, missing, another card, another board, access
denied, and a failing lookup that must not become a false 404) and
`tests/playwright/specs/rest-checklist-not-found.e2e.js` reads the checklist
over REST and after a page reload.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.91 2026-11-03 WeKan ® release
**In short:** A **GitHub CodeQL** finding in a test is fixed, and the guard
against
replacing text with itself now also sees patterns with a `^` or `$` anchor.
This release has the following developer-tooling fix:
**Code scanning** - the tests that guard the tree.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/62da564a1e">A test no longer replaces getLabel with itself, and the identity-replacement guard knows anchored patterns</a>. Thanks to GitHub CodeQL and xet7.</summary>
Code scanning alert #600 (`js/identity-replacement`, CWE-116):
`tests/boardLabelLookup.test.cjs` cut the `getLabel` method out of
`models/boards.js` and then ran `.replace(/^getLabel/, 'getLabel')` on it, which
does nothing because the slice already starts at `getLabel`. The line is gone.
`tests/noIdentityReplacement.test.cjs` skipped every regular expression with an
anchor, so it could not see the shape; a leading `^` or a trailing `$` over
plain characters is now compared like any other, with samples of both for the
guard to detect and the existing samples that must not be reported.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.90 2026-11-03 WeKan ® release
**In short:** The login page can have its own **background color**, Member
Settings
has a per-member switch, **Show focus outline**, to hide the blue focus ring,
and
**Escape closes the add-list composer**.
This release adds the following new features:
**Member Settings** - what each member can set for themselves.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/144777ae6b">Change Settings has Show focus outline, to hide the blue focus ring for yourself</a>. Thanks to mimZD and xet7.</summary>
[\#6755](https://github.com/wekan/wekan/issues/6755). The blue ring WeKan draws
round the focused control is an accessibility feature (WCAG 2.4.7) and stays on
by
default, but it also shows after a click on a minicard, and a member asked
whether
it can be turned off. Change Settings now has **Show focus outline**, ticked
while
the ring shows. Unticking it stores `profile.hideFocusOutline` for the signed-in
member (a method that takes no user id from the client) and puts a class on the
page that hides the outline and its halo for that member only; field borders and
the colour a button takes on focus stay. The label is translated into every
language file; the translations of these were written without a native speaker
and
are the least certain: Akan, Aymara, Bambara, Tibetan, Breton, Cherokee,
Dzongkha,
Ewe, Fulah, Fijian, Guarani, Manx, Hausa, Hawaiian, Igbo, Inuktitut,
Kalaallisut,
Kashmiri, Cornish, Luganda, Ladin, Nahuatl, Neapolitan, Northern Ndebele,
Northern
Sotho, Chichewa, Oromo, Quechua, Kirundi, Aromanian, Kinyarwanda, Northern Sami,
Samoan, Shona, Swati, Southern Sotho, Tigrinya, Tigre, Klingon, Tswana, Tongan,
Tsonga, Venda, Volapük, Walamo, Wolof, Xhosa and Tamazight.
`tests/hideFocusOutline6755.test.cjs` and
`tests/playwright/specs/hide-focus-outline-6755.e2e.js` pin it, including that
one
member hiding the ring does not hide it for another.
</details>
**The login page** - how the sign-in page and the support page look.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6b27764452">Admin Panel / Lay-out has a Login page background color, and the login text and the support page have a readable layout</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6866](https://github.com/wekan/wekan/issues/6866). The login page stayed
grey whatever the Admin Panel said: the custom HTML fields only render after
sign-in and the text below the login logo is sanitised. The new setting
`loginBackgroundColor` takes a hex colour only (`#rgb` or `#rrggbb`); anything
else means "not set", so the value in the CSS custom property can never carry
CSS of its own. The text under the logo is white on a dark colour and black on a
light one. It is put on the page only while the sign-in layout is shown and
removed when it closes. The support page was text flush against the window edge
with no headings: it is now a centred white column of at most 860px, and the
text under the login logo gets the same column without a card of its own, both
with sized bold headings and space between paragraphs. The label is translated
into every language file. `tests/loginBackground6866.test.cjs` pins the colour
decision, that nothing but the validated value reaches the page, and the layout.
</details>
and fixes the following bugs:
**Lists** - the add-list composer.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f4b5025a7b">Escape closes the inline add-list composer</a>. Thanks to mimZD and xet7.</summary>
[\#6865](https://github.com/wekan/wekan/issues/6865). The composer that opens after
a list (or in an empty swimlane) stays open once a list is created, so several
can
be added, and only its X closed it: Escape did nothing. Escape now closes it,
with
the same reset as the X, at the level of an inlined form, and a click elsewhere
still does not. `tests/addListEscape6865.test.cjs` and
`tests/playwright/specs/add-list-escape-6865.e2e.js` pin it before and after a
list
is created.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.89 2026-11-03 WeKan ® release
**In short:** Keyboard **shortcuts run once on a non-Latin keyboard**: `d` no
longer opens the Due date popup and closes it again on a Farsi layout.
This release fixes the following bugs:
**Keyboard shortcuts** - what a key does on each layout.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0110e84ce5">A shortcut runs once on a Farsi, Cyrillic or other non-Latin keyboard</a>. Thanks to mimZD and xet7.</summary>
[\#6840](https://github.com/wekan/wekan/issues/6840). `d` opened the Due date
popup and closed it at a glance, "sometimes", for a reporter whose keyboard was
Farsi. `client/lib/keyboard.js` re-dispatched every key typed on another script
as its Latin key, a leftover from Mousetrap, which read the typed character.
hotkeys-js, which replaced it, resolves a key from `event.code` and `keyCode`
and so already ran the shortcut for the physical d key on any layout; the copy
ran
every shortcut a second time, the due date editor opened and the repeat clicked
the
same control again, closing it. The same doubling was behind Shift+D on a Latin
keyboard (#6750). The re-dispatch is gone. `tests/keyboardNonLatinOnce.test.cjs`
pins that no handler turns a typed character back into a key, and reads
hotkeys-js's own rule; `tests/playwright/specs/keyboard-nonlatin-once.e2e.js`
sends
real key events for Latin, Farsi and Cyrillic and counts one run each.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.88 2026-11-03 WeKan ® release
**In short:** **Every board theme** and **every color a member chooses** is
readable: the dark themes' **Card Settings**, popups and sidebars, the header of
eight themes, and a custom accent, text or label color made legible. A padded
**REST label** name no longer creates a second label.
This release fixes the following bugs:
**Dark themes** - cleandark, moderndark, dark and exodark.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a6b10e3592">Card Settings, popups, sidebars and the quiet text are readable in the dark themes</a>. Thanks to Samunosuke and xet7.</summary>
[\#6863](https://github.com/wekan/wekan/issues/6863). Card Settings drew white
text on light grey blocks (1.07:1 in cleandark); popup items, the board
sidebar's Keyboard shortcuts row, "Add custom field" and its sister buttons and
the sidebar's labels were black on dark (1.18 to 2.19:1); and a list header's
`!important` light grey beat each theme's own and left its light text on it
(1.09:1 in moderndark, 1.27:1 in exodark). Part of it was new: the accessibility
audit made the quiet text greys dark for every theme, so the dark themes showed
dark grey on dark; the rest was older, rules that never followed the dark
themes. The quiet text is now a variable, `#595959` on the light themes and a
light grey where the surface is dark (cleandark, moderndark, and dark and
exodark on the board, the card and the sidebar); Card Settings' sections, its
muted text, the popup items, the quiet buttons, the shortcuts row, the sidebar
labels, dark's swimlane header and moderndark's selected minicard and primary
buttons follow the theme; and the list header's `!important` is gone.
`tests/playwright/specs/46-axe-theme-contrast.e2e.js` runs axe-core's colour
contrast rule on the four themes (the board, the sidebar, Card Settings, an
opened card and the custom fields popup) and finds nothing;
`tests/darkThemeContrast6863.test.cjs` pins each rule. The header colours of
relax, nephritis and modern, and a few lines of clearblue and cleanlight, are
still below 4.5 and are listed in the accessibility audit.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8b2114cd0f">Every board theme, and every color a member chooses, is readable</a>. Thanks to Samunosuke and xet7.</summary>
[\#6863](https://github.com/wekan/wekan/issues/6863) asked to check the other
themes too, and the same scan was run on all 25. White text on the accent was
2.1 to 4.3:1 in nephritis, pumpkin, moderatepink, strongcyan, limegreen, relax,
corteza and modern, and their dark, hover and emphasis shades were as low: the
accents are darkened, with the same hue, until white text and the header's
`#f2f2f2` labels read at 4.5:1, and the hover and selected shades follow
(limegreen
is a forest green now, strongcyan a teal: white at 4.5:1 cannot be had on a
bright
accent). The colour-slide themes that end in the same colours read too. The
filter sidebar's headings (`#808080`, in every theme), the clear themes' quiet
text and cleanlight's add-dependency link are darker. No colour contrast failure
is left on the board, the sidebars, Card Settings, an opened card and its popups
in any theme. axe-core cannot measure text over a gradient, so the six
colour-slide headers are not covered by it.
A color a member chooses is one half of a pair, and the other half is worked out
(`models/lib/contrastColor.js`). A custom theme accent, or the two ends of a
custom
slide, is darkened under the header's white text until it reads, and the
picker's
preview shows the color that will be drawn. The text color from Member Settings
is
moved, with its hue, until it reads at 4.5:1 on the surfaces of the theme in
use,
says so when it changed, and no longer reaches the header bars, labels and
primary
buttons, which sit on a color of their own (`#626262` was 1.18:1 on the blue
header). A dependency line is drawn at 3:1 against the surface. Kanri's tag text
is the better of dark and light, which the brightness test it replaces was not.
A
custom label, card, list or swimlane color already derived its text, black or
white, and that is now pinned at 4.5:1 for every color.
`tests/playwright/specs/46-axe-theme-contrast.e2e.js` scans every theme and four
colors that do not read; `tests/customColorContrast.test.cjs` pins the rules.
</details>
**REST API** - board labels.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0426c7741a">A padded label name finds the stored label instead of creating another</a>. Thanks to AhmedLukman and xet7.</summary>
[\#6864](https://github.com/wekan/wekan/pull/6864). A label is looked up by name
and color before one is added, and the lookup compared the name as it was sent
while the schema stores it trimmed, so a padded " Bug " missed the stored "Bug"
and every repeat of the request created another label with the same name and
color. The pull request trimmed the name in the REST builder; the lookup is
shared
by the card's label form, the importers and the list sync, so it trims too, and
a
legacy label stored with padding is still found. A different color, letter case
or inner space stays a different label. `tests/boardLabelLookup.test.cjs` pins
it.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.87 2026-11-03 WeKan ® release
**In short:** A **REST API** "get one" that finds nothing answers **404**
instead of 200 with an empty body.
This release fixes the following bugs:
**REST API** - what a request for one item answers when there is none.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1ded34dfdd">A single-item GET that finds nothing answers 404, not an empty 200</a>. Thanks to AhmedLukman and xet7.</summary>
[\#6862](https://github.com/wekan/wekan/pull/6862). The scoped card GET
(`/api/boards/:boardId/lists/:listId/cards/:cardId`) sent HTTP 200 with no body
for a missing, wrong-list, wrong-board or archived card, which a client cannot
tell from a card with nothing in it. The pull request fixed that one route; the
same shape was in the list, swimlane, custom field, comment and integration
GETs, and the user GET threw a TypeError for an unknown name, which reached the
caller as a server error. Each now answers 404 ("Card not found", "List not
found" and so on). The board check stays first in each, so someone with no
access gets the same answer for an existing and a missing id. The global card
GET, which finds a card by id alone, already answered 404.
`tests/restSingleItem404.test.cjs` runs every one of these routes from its own
source with a cache that finds nothing, something, and a stranger, and fails if
any single-item GET route under server/ has no not-found branch.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.86 2026-11-02 WeKan ® release
**In short:** The daily **notification cleanup** and the **file integrity
audit**
run once for the whole installation, not once per server.
This release fixes the following bugs:
**Database load** - work that every server of a cluster repeated.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4e28465964">The notification cleanup and the file integrity audit run once, not on every server</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6861](https://github.com/wekan/wekan/issues/6861). The notification cleanup
ran at every start and daily on every server, reading and rewriting the same
18,000 users 24 times at a rollout, and a notification another server added
between a read and its write was lost. A run now claims a document per job and
day in the dailyJobRuns collection (a duplicate key means another server has
it), and the timer looks hourly for an unclaimed day, so the cleanup still
happens daily when no server restarts. The file integrity audit read its last
run, checked it, and then wrote it, so two servers whose hourly check fell at
the same moment both scanned every file; one atomic update, matching only while
the last run is older than the interval, now claims the run. Continuous backup
has no leader and every server writes its own stream into the same directory;
that needs a lease, which is not built, so the documentation now says it is for
one server, and item 3 of the issue stays open. `tests/dailyClaim.test.cjs`
pins the claims with 24 servers at once, that other database errors are not
swallowed, and that neither job runs unclaimed.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.85 2026-11-02 WeKan ® release
**In short:** The findings of the **accessibility audit** are fixed in the
default theme: header and label **colour contrast**, names that contain the
visible text, named form fields, card section headings that are real buttons,
and landmarks and headings on every page. axe-core now finds no violation on
22 pages. A cleared database connection pool is reported as a lost connection.
This release fixes the following bugs:
**Accessibility** - what an automated axe-core scan of the default theme found
(see Audit.md in docs/Features/General/Accessibility).
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c8bcc4817d">axe-core finds no violation on 22 pages of the default theme</a>. Thanks to xet7.</summary>
The audit of 2026-11-02 found, on seven pages, colour contrast of the header
bars and the labels (WCAG 1.4.3), a Label in Name mismatch on the Starred menu,
the user menu and the avatars (2.5.3), form fields without a name (4.1.2),
headings that were buttons holding other controls (4.1.2), and missing
landmarks and headings. Each is fixed, and the scan, now on 22 pages (the
signed-out ones, All Boards and its lists, the other pages, the Admin Panel, a
board and an opened card), finds nothing.
The default theme's blue is `#2573a7` instead of `#2980b9` everywhere the
stylesheets, the accent table and the colour picker name it, so white and the
`#f2f2f2` button labels reach 5.14 and 4.59. The label colours green, red, sky,
magenta and gray and the Global search list chips have black text. The quiet
greys `#8c8c8c` and `#727479`, the "no items" text and the grey board tiles
are darker. The Starred menu, the user menu link and an avatar drawn as initials
have those letters or numbers in their accessible name. The Pomodoro field and
the comment, description and edit-comment editors have a name (the editor takes
a `labelKey`). A section heading of the card details is an `h3` holding a real
`button` with `aria-expanded`; the section menu and the layout toggle are
beside the button, and Enter, Space and a click on the heading still fold the
section. The sign-in and register form is the `main` landmark, its title is the
level-one heading and the empty logo `h1` is a spacer; every other page's title
in the header bar is the level-one heading, Activity, Import and Bookmarks have
one, and the shortcuts list can be reached by keyboard.
`tests/accessibilityAuditFixes.test.cjs` pins each fault by its shape (contrast
ratios are computed from the stylesheets), and
`tests/playwright/specs/45-axe-accessibility.e2e.js` runs axe-core on the pages
and fails on any violation. Other board colours and themes, the phone width,
dark mode, popups, and the views not scanned still need the same check.
</details>
**Database problems** - how Admin Panel -> Problems and the startup read the
database.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/abc23a7eda">A cleared connection pool is a lost connection, and the board key backfill uses the key index</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6860](https://github.com/wekan/wekan/issues/6860). While a MongoDB pod
restarted, a method's "Connection pool for <host> was cleared" reached Admin
Panel -> Problems as "did not match its schema": collection2 wraps a failed
write as "ValidationError: Failed validation" plus the driver's text, so the
class name PoolClearedError was gone, and the host in the text is a replica set
member's name (wekan-mongodb-0.wekan-mongodb-internal in a StatefulSet) that
MONGO_URL does not list, so the "getaddrinfo ENOTFOUND" behind it did not count
as the database's. The text itself is now a lost connection, for the Problems
page and for both startup guards, and a message about a pool setting is not.
The board key backfill looked for boards without a key with an `$or` that
included an exists-false branch, a scan of every board at every start of every
server (32,600 boards in the report); `key: null` already matches a missing
field, so one `$in` on the `key` index finds the same boards, and the same
selector still guards the writes, so a key a board admin set meanwhile is never
replaced. `tests/databaseErrorsWrappedCause.test.cjs` and
`tests/boardKeyBackfill.test.cjs` pin both, and that no exists-false branch on
`key` is left.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.84 2026-11-02 WeKan ® release
**In short:** GitHub code scanning's **code-quality** findings are fixed: a
promise formatted as text, undeclared globals, an upload that hung on an
unreadable file, and the lint around them. `npm run lint:promises`
now finds promises that are not awaited or handled.
This release fixes the following bugs:
**Code quality** - findings of GitHub code scanning.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/582a22c291">The real faults among the code-quality findings, and the lint around them</a>. Thanks to GitHub CodeQL and xet7.</summary>
A card's original-position description formatted a promise, because it did
not await the history it reads. The attachment swipe distances and two
drag-scroll locals were undeclared globals. A file that cannot be read during
a GridFS upload now unlinks itself instead of leaving the upload open, since
`pipe()` does not pass a read error on. `api.py` called `exit()`, which is
missing when the site module is not loaded, and its usage branch never
exited; it uses `sys.exit()`. A rules test redefined a property and a
security-log test kept an unreachable branch. Besides these: 31 missing
semicolons, unused imports in the card details, the exporters and the
attachment API, unused Python imports, and a comment on each empty except
clause. The list-sync scanner test cut its source at the last
`Meteor.startup`, which the index startup of the schema-scan fix had moved;
it now cuts at the first one after the scanner. Two unhandled rejections are
caught as well: the instance-editing
hook's board lookup and the service worker registration.
`tests/codeqlFindingsRegression.test.cjs` pins the shape of each fault.
Findings in vendored bundles (the Office viewer, Jade, capnp-es, exceljs)
and style-only ones are left as they are.
</details>
and has the following developer-tooling fixes:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f1d2a497bc">npm run lint:promises finds promises that are not awaited, handled or marked void</a>. Thanks to xet7.</summary>
GitHub code scanning's "Missing await" finding was a real fault in
`models/cards.js` (a description formatted a promise), and a plain ESLint
cannot see that kind of mistake because it does not know which calls return a
promise. `npm run lint:promises` runs typescript-eslint's
`no-floating-promises` and `no-misused-promises` over `models/lib`,
`server/lib` and `imports/lib`, with the type information read from
`tsconfig.lint.json`; the code stays JavaScript and nothing is type-checked.
The eleven findings were promises that already catch their own errors, now
marked `void`, and three checks that used a promise as a boolean.
`tests/promiseLint.test.cjs` keeps the configuration in place and the tree
clean.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.83 2026-11-02 WeKan ® release
**In short:** An **LDAP** login with a reused login name no longer opens the
previous owner's account, and existing accounts record their directory
identifier.
This release fixes the following bugs:
**LDAP** - matching a directory entry to a WeKan account at login, import and
background sync.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e91c14cd9a">A reused login name no longer opens the previous owner's account</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6859](https://github.com/wekan/wekan/issues/6859). The login looked for the
account by the entry's unique identifier, then by username, then with
LDAP_EMAIL_MATCH_ENABLE by e-mail, and the last two did not check whether the
account already belonged to another directory entry. When a directory gave a
deleted employee's sAMAccountName to a new colleague, the new colleague logged
into the old account and saw its boards, cards and comments; two entries
sharing an e-mail address did the same, and so did the import with
LDAP_MERGE_EXISTING_USERS. An account that records an identifier of the same
attribute with a different value is now refused at login ("LDAP
authentication failed") and skipped by the import. An account without an
identifier, with an identifier of another attribute, or with an identifier
without its attribute (made before \#4654) is still accepted, so switching
LDAP_UNIQUE_IDENTIFIER_FIELD to objectGUID locks nobody out. When that field is
empty the identifier is the login name itself, so a reused name cannot be
detected: set it to objectGUID or entryUUID.
Three more faults in the same matching are fixed. An existing account never
recorded services.ldap.id and its attribute (only a new account did), so
accounts from before \#4654 never got an attribute and a changed
LDAP_UNIQUE_IDENTIFIER_FIELD never took effect; the login and the sync now
record both. LDAP_EMAIL_MATCH_REQUIRE looked the user up with _id equal to the
username, which never matches a random id, so every existing user's login
tried to create the account again; it uses the username. The background sync
bound an account without an identifier to the first of several entries with
its name; it now skips that account, neither updating nor disabling it.
`tests/ldapAccountBinding.test.cjs` pins each of these, and that no lookup by
_id equal to the username is left in the package and no name or e-mail match
reaches the login without the check. A refused login is not recorded in
Admin Panel -> Problems: the LDAP package cannot reach the application's
security log, and the new colleague who is refused is not an attacker.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.82 2026-11-02 WeKan ® release
**In short:** A card moved to another board, restored from the archive or given
a number no longer takes a **card number** another card on that board has.
This release fixes the following bugs:
**Card numbers** - the number each card has on its board.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e68938698c">A moved, restored or renumbered card cannot take a number another card has</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6858](https://github.com/wekan/wekan/issues/6858). Follow-up to
[\#6815](https://github.com/wekan/wekan/issues/6815), which checked the number
of every new card. A card also enters a board's numbering through an update: a
move to another board, where the browser computed the number from the
destination cards it had loaded, usually none, so the card got 1 or another
low number already taken; a restore from the archive, since an archived card
may share its number with an open one; and a number set by the REST API or an
import. Three card numbers used by more than one open card showed up again in
Admin Panel -> Problems -> Data health two days after the renumbering. A
before-update hook now does what the insert hook does: a number another card
on that board has is replaced by a fresh one from the board's counter, after
raising the counter to the board's highest number; a free number is kept and
the counter raised past it. The card itself never counts as taken, and
ordinary edits, moves within a board, archiving and cards without a number
are left alone. `tests/cardNumberUnique6815.test.cjs` pins which updates are
checked, the move, the restore and the free numbers.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.81 2026-11-02 WeKan ® release
**In short:** The **database health** probe reports a finding once, not once
per server.
This release fixes the following bugs:
**Database health** - the probe behind Admin Panel -> Problems -> Database.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/114e9ecd5a">One server probes the database per period, so a finding is reported once</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6857](https://github.com/wekan/wekan/issues/6857). Every server read the
same MongoDB log and counters every five minutes and recorded each finding
itself, so one collection scan became up to 24 identical rows with 24
replicas - more than 400 warnings over 68 pages in the report. A probe now
claims the period in one shared document (collection databaseHealthProbe);
only the server that gets it probes, a duplicate key means another server has
it. The state that compares one probe with the next - log lines seen, the
scanned-documents counter, uptime, restarts, latencies - lives in that
document, so the next probe on whichever server continues where the last one
stopped. A server without the claim still checks its own data directory, and
a claim that cannot be made at all does not stop the probe.
`tests/databaseHealthProbeClaim.test.cjs` pins the claim, the shared state
and that each finding is reported once.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.80 2026-11-02 WeKan ® release
**In short:** **minimatch** is updated to 11.0.4 for GHSA-g3hv-75wg-m249, a
glob pattern that used up CPU and memory.
This release updates the following dependencies:
- **minimatch 10.2.6 → 11.0.4** — Dependabot alerts #180 and #182,
[GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249)
(CVE-2026-118764, CWE-405, moderate): before 11.0.1 a crafted glob pattern
made it grow its glob parts without bound and compare them pairwise, using
up CPU and memory. It came in through archiver, typescript-eslint and
Babel's CLI; all of them now use 11.0.4, and
`tests/minimatchGlobparts.test.cjs` fails if a 10.x copy returns
([commit](https://github.com/wekan/wekan/commit/a3630d96ce)).
Thanks to dependabot.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.79 2026-11-02 WeKan ® release
**In short:** A cluster of servers runs the **schema re-check** and the
**due-date reminders** once, and a slow-query warning says which query it is.
The site **announcement** shows on Home and All
Boards too. The **date picker** popup shows the year and has year, month and
day drop-downs under the calendar. The **macOS sandbox** for VSCodium no longer
asks for a keychain, starts from the checkout, and can carry a **Claude Code**
token so its login sticks.
This release fixes the following bugs:
**Database load** - work that every server of a cluster repeated.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d42369e96a">The schema re-check and the due-date reminders run once, not on every server</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6855](https://github.com/wekan/wekan/issues/6855). With 24 replicas on one
database every start of every server re-checked the schema, reading 1.1
million cards several times, and each server sent its own due-date reminder
for every card. The re-check now claims a run in the migration collection, so
one server does it and the others skip; a claim older than 2 hours, from a
server killed mid-run, is taken over. A run that leaves work it cannot do
(an attachment whose file is gone) is retried once a day instead of at every
start, and a clean run clears that. The daily reminder scan is claimed per day
in the dueReminderRuns collection, so the first server scans and the rest get
a duplicate key and skip. A partial index on lists serves the 15-minute list
sync scan, and a sparse index on boards.dueReminderDays the due-date lookup.
`tests/schemaUpgradeSteps.test.cjs` and `tests/dueScanClaim.test.cjs` pin the
claims, the stale takeover, the daily retry and that the scheduler claims
before it scans.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bc79de8c57">A collection-scan row names the command, the operators per field and the queryHash</a>. Thanks to Raymond-Voogt and xet7.</summary>
[\#6856](https://github.com/wekan/wekan/issues/6856). Admin Panel -> Problems
-> Database named only the collection and the filtered fields, for example
"wekan.lists (filter on archived,syncSource.enabled,syncSource.type)", which
does not say which query it is, and a slow getMore read "filter on nothing".
The row now shows the command (find, distinct, aggregate, ...), for a getMore
the query it continues, each field with the operators used on it - for example
`archived:$exists` or `$nor[sort:$gte$lte]` - and MongoDB's queryHash, so
the query can be found in the code and with explain(). Values never reach the
row. `tests/databaseHealthScans.test.cjs` pins the shape, the queryHash, the
getMore origin and that no value leaks.
</details>
**Announcement** - the site announcement of Admin Panel -> Announcement.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/64b5762020">The announcement shows on Home and on every All Boards list</a>. Thanks to titver968 and xet7.</summary>
[\#6854](https://github.com/wekan/wekan/issues/6854). The banner waited
for the user to be a member of the current board, and Home, Remaining, Starred,
Templates and the other All Boards pages have no current board, so it showed
only on boards. It now shows to every signed-in user on those pages, and to the
members of the open board on a board page; a visitor who is not signed in still
does not see it (#1281).
`tests/announcementAudience.test.cjs` pins the rule and that no membership test
of a current board is left in front of the banner.
</details>
**Date picker** - the calendar in the Received, Start, Due and End date popups.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/45c291b406">The calendar heading shows the year as well as the month</a>. Thanks to brlin-tw and xet7.</summary>
[\#6853](https://github.com/wekan/wekan/issues/6853). The popup calendar
heading named only the month, so after the arrows had moved past December
there was no sign of which year was shown; the selected date beside it does
not change when the month does. The heading now reads for example "January
2027", in every calendar system. `tests/calendarPickerControls.test.cjs` pins
the year in the heading and that no picker mode leaves it out again, and the
Playwright date display spec expects it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dcaa316a01">Any year by number, and month and day drop-downs, under the calendar</a>. Thanks to brlin-tw and xet7.</summary>
[\\#6853](https://github.com/wekan/wekan/issues/6853). The heading above the
calendar now shows only the month and the year, no longer the YYYY-MM-DD of
the selected day. Under the calendar are three controls: a year field, where
any whole year can be typed or stepped to - thousands of years back or ahead,
0 being 1 BC and -2999 3000 BC, as far as a date reaches (about 275,000 years
either side of 1970) - a drop-down of the months of that year, and one of the
days of that month. A pick selects the date and shows its month; Enter in the
year field applies the year instead of saving the popup, and a value that is
not a year puts the field back. The months and days are read from the calendar
system in use, so they hold for the Islamic, Hebrew, Japanese and every other
system, including leap months and a short thirteenth month. A day that the new
month lacks falls to its last day. Calendars whose eras restart or switch the
count (Japanese, ROC, Coptic, Ethiopic) refuse a year no number can name.
`tests/calendarDropdowns.test.cjs` checks ten calendar systems and that no
date text is left in the heading.
</details>
and has the following developer-tooling fixes:
**The macOS sandbox** - running VSCodium and Claude Code in the sandbox on a
Mac.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c58b2896d1">VSCodium no longer asks for a keychain in the macOS sandbox</a>. Thanks to xet7.</summary>
The sandbox refuses `~/Library/Keychains`, so VSCodium's secret storage asked
for a keychain to keep "VSCodium Key" in, with a "Reset To Defaults" button
that would have replaced the login keychain. `sandbox.sh` now starts VSCodium
with `--use-mock-keychain`, so the real Keychain is never touched. The README
explains the price: extension secrets in `.tools/vscodium/user-data` are only
obfuscated. `tests/macosSandbox.test.cjs` covers the sandbox script.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/21c94ce098">Claude Code stays signed in inside the macOS sandbox, and the sandbox starts in the checkout</a>. Thanks to xet7.</summary>
Claude Code keeps its login in the Keychain, which the sandbox cannot reach.
`sandbox.sh --claude-token` now saves a token from `claude setup-token` to
`.tools/secrets/claude-oauth-token` (read from the clipboard, line breaks
joined, a cut-short token refused) and passes it as `CLAUDE_CODE_OAUTH_TOKEN`,
with onboarding marked done and the extension's login prompt off. The script
also changes to the checkout first, because Finder starts it in the home
directory, which the sandbox refuses ("EPERM: process.cwd failed"), and starts
VSCodium with `nohup` so closing the launcher's Terminal does not close it.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.78 2026-11-02 WeKan ® release
**In short:** **CrossBoardExportBleed** and **UnassignedAttachBleed** are
fixed: a checklist scope stays on its board, every export obeys Disable all
export and Anonymize export users, and an assigned-only member changes
attachments only on their own cards. **SAML** logins use **node-saml 5.2.0**,
and **continuous backup** no longer misses a change made right after it starts.
**Keyboard shortcuts** keep working after a popup closes, and the Rules page has
a way back to the board.
This release fixes the following CRITICAL SECURITY ISSUES:
**Exports** - what a board export may contain, and whether it runs at all.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/be6727607c">A checklist scope stays on its board, and every export obeys Disable all export and Anonymize export users</a>. Thanks to ccsimplyspolit and xet7.</summary>
[CrossBoardExportBleed](https://wekan.fi/hall-of-fame/crossboardexportbleed/),
[GHSA-8885-xrq4-j47h](https://github.com/wekan/wekan/security/advisories/GHSA-8885-xrq4-j47h).
A board export scoped with `?checklistId=` streamed a checklist of another,
even private, board with its items and creator; it is now bound to the
exported board's cards. The streaming CSV/TSV export ignored Disable all
export and Anonymize export users, and so did the Excel, PDF, chart, calendar
and external-format exports: every export route now refuses with 403 when
export is disabled, and every exporter that names people anonymizes. Refusals
are shown in Admin Panel / Problems.
</details>
**Attachments** - who may add or delete a card's files through the API.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/532a96da11">An assigned-only member adds and deletes attachments only on cards assigned to them</a>. Thanks to Javier García Herrera and xet7.</summary>
[UnassignedAttachBleed](https://wekan.fi/hall-of-fame/unassignedattachbleed/),
[GHSA-w26g-rc43-q6cm](https://github.com/wekan/wekan/security/advisories/GHSA-w26g-rc43-q6cm).
A member with the Normal, assigned only role whose card assignment was removed
kept the card and attachment ids, and `POST /api/attachment/upload` and
`DELETE /api/attachment/delete/:id` still added a file to that card and
soft-deleted its existing one: both asked only for board write access. The
DDP methods, and copy and move onto a target card, had the same gap. All
eight handlers now apply the card rule the ordinary card Delete used, and a
refusal is shown in Admin Panel / Problems.
`tests/unassignedAttachBleed.test.cjs`
fails if any attachment API write lacks it.
</details>
and updates the following dependencies:
- **isomorphic-git 1.44.3 → 1.44.8** — the GitHub wiki sync
([#6846](https://github.com/wekan/wekan/pull/6846)).
- **@node-saml/node-saml 5.1.0 → 5.2.0** — SAML; see the fix below, without
which it changed nothing that runs ([#6850](https://github.com/wekan/wekan/pull/6850)).
- **@typescript-eslint/eslint-plugin 8.72.0 → 8.72.1** — lint only
([#6849](https://github.com/wekan/wekan/pull/6849)).
- **@rspack/dev-server 2.2.1 → 2.2.2** — development server only
([#6848](https://github.com/wekan/wekan/pull/6848)).
- **mailparser 3.12.2 → 3.12.4** — inbound email and bounces
([#6845](https://github.com/wekan/wekan/pull/6845)).
- **i18next 26.5.4 → 26.5.11** — translations
([#6847](https://github.com/wekan/wekan/pull/6847)).
- **@aws-sdk/client-s3 3.1155.0 → 3.1159.0** — S3 backups; see the
@smithy/core fix below ([#6844](https://github.com/wekan/wekan/pull/6844)).
- **imapflow 2.8.0 → 2.9.1** — the IMAP mailbox
([#6842](https://github.com/wekan/wekan/pull/6842)).
- **glob 13.0.6 → 14.0.3** — no WeKan code loads it: it is only the target of
an `archiver-utils` override that archiver 8 no longer needs; @babel/cli and
rimraf keep their own copies ([#6851](https://github.com/wekan/wekan/pull/6851)).
- **helm/chart-releaser-action 1.7.0 → 1.8.0** and **docker/login-action 4.6.0
→ 4.7.0** — CI, each still pinned to the commit its tag names
([#6843](https://github.com/wekan/wekan/pull/6843),
[#6841](https://github.com/wekan/wekan/pull/6841)).
Thanks to dependabot.
and fixes the following bugs:
**Dependencies** - what the updates above did, and did not, change.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5c50f38e25">SAML logins are verified by node-saml 5.2.0, as package.json says</a>. Thanks to xet7.</summary>
The bump changed the app's `package.json`, but a login is verified by the
`wekan-accounts-saml` package's own copy, still pinned to 5.1.0 - so nothing
that runs changed. 5.2.0 rejects a message with more than one Issuer, checks
`idpIssuer`, rejects a `RelayState` that is not the signed one, and checks a
POST LogoutResponse's status and issuer. The package pins 5.2.0 now, and
`samlLogin.test.cjs` requires both to name the same version. A login
roundtrip with the package's copy logs in a signed Response and a signed
Assertion, and refuses a changed NameID, another key, an unsigned response and
an answer to a request WeKan never made.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/338afc15f8">@smithy/core 3.38.1, the AWS SDK's logger redaction fix</a>. Thanks to xet7.</summary>
The S3 SDK bump resolved `@smithy/core` 3.37.0, deprecated for a bug that
passed sensitive fields nested in lists or maps unredacted to a custom logger
([aws-sdk-js-v3#8393](https://github.com/aws/aws-sdk-js-v3/issues/8393));
3.36.2 before it had the bug too. WeKan's only AWS client, the S3 backup,
passes no logger, so nothing leaked. Updated in the lockfile only, one
package; the telemetry gate (`releases/check-telemetry.py`), which had failed
on the deprecation note's new github.com link, passes again. The ten updates
add no package, install script, network host or telemetry, and every tarball
matches its lockfile hash.
</details>
**Continuous backup** - what it sees right after it starts.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8cbe2605fe">Files are watched before the first scan, and a macOS start catches up once</a>. Thanks to xet7.</summary>
The file engine scanned its sources first and only then started watching, so
a change made between the two waited for the next scan, five minutes by
default. macOS FSEvents also delivers nothing for a moment after a watcher is
created. The watchers start first now, sharing one queue with the scan so a
change is recorded once, and on macOS one catch-up scan runs two seconds after
the start; Linux's inotify needs none. A new test with a silent watcher pins
the order, the macOS catch-up and that Linux runs no second scan.
</details>
**Single-file builds** - signing and the start/backup commands.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8d4f4caa2f">A macOS build is signed whatever its target is called, and start/backup return once WeKan runs</a>. Thanks to xet7.</summary>
Only a target named mac or darwin was ad-hoc signed, so a Mach-O built under
another label had the signature its injected archive invalidated, and macOS
killed it before it printed anything. Every Mach-O output is signed now, read
from the file's magic number. `start --background` and `backup` returned
before WeKan wrote its pid file, so a `stop` right after them said WeKan was
not running and left it running; both wait now until the pid file names the
process they started. `singleFile.test.cjs` passes 24 of 24, the real single
executable included.
</details>
**Release builds** - the bundles each platform downloads.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/24a7270b7f">Android bundles start again: npm 12 runs argon2's and bcrypt's install scripts</a>. Thanks to xet7.</summary>
npm 12 lets only declared dependencies run install scripts, and the rebuild
directories declared none, so argon2 and bcrypt were never compiled. Prebuilds
hid it everywhere except Android, whose bundles crash-looped on "No native build
was found for platform=android". Each rebuild directory now declares the
reviewed installers it holds, and the Android build loads both before starting
WeKan.
</details>
**Filters** - the filter sidebar's date ranges.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c5f5ad2338">A refused date range says why in the form, so Clear still works in Firefox</a>. Thanks to xet7.</summary>
A reversed card date or card movement range was reported with the browser's
validation bubble, which Firefox opens over the Apply and Clear buttons and
which took every click on them: the range could not be cleared. The end date
is still marked invalid, and the reason is a `role="alert"` line in the form;
editing the range or Clear takes it back.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4ad13db5ef">The date-range forms are checked by WeKan, not refused by WebKit after Clear</a>. Thanks to xet7.</summary>
After Clear, WebKit kept a half-edited date in the From field and silently
refused the next submit, so a new range never applied. Both forms are
`novalidate` now; WeKan's own check already marks a refused range and says
why.
</details>
**History recovery** - an undo or redo that is still waiting for the server.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/96845f6f4b">Retry is enabled only once the board lets this person write on it</a>. Thanks to xet7.</summary>
After a reload the notice showed before the board and its members arrived,
and a Retry clicked then did nothing and the notice stayed. Retry is disabled
until it can act; Forget stays usable.
</details>
**Keyboard shortcuts** - when a focused button or link lets them through.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ba0de93f79">Shortcuts keep working after a click on a minicard or a closed popup</a>. Thanks to mimZD and xet7.</summary>
The shortcut filter refused every key while a link or button had the focus.
The minicard is itself a link, so clicking its date badge or menu left it
focused, and closing a popup returns focus to its opener: hovering a card and
pressing d did nothing until a reload. A focused control now keeps only its
own keys - Enter, Space for a button, the arrows for a tab - and every other
key is a shortcut again ([#6755](https://github.com/wekan/wekan/issues/6755)).
</details>
**Rules** - the way back from the Rules page.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/34d9817110">The Rules page has a Back to the board</a>. Thanks to mimZD and xet7.</summary>
The page lost its Back with its old sidebar, and only the trigger, action and
rule details pages kept one, to the rules list. The list and its Workflow,
Bots, Blocks and History views now link back to the board; the three
sub-pages keep only their own Back ([#6852](https://github.com/wekan/wekan/issues/6852)).
</details>
**Sign-in** - the error shown on the sign-in page.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3a2fef4dc7">Login errors are rendered from template state, so a redraw of the form keeps them</a>. Thanks to xet7.</summary>
The error region sits inside the form's loading switch, and an error written
into it was lost whenever that part of the form was drawn again. The template
now renders the message as escaped text from a reactive value. The SAML error
test also waits for the finished form before it clicks.
</details>
**Whiteboard** - how its lines are drawn.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/40f60582b7">A line is drawn at its own width, not the whiteboard's 800</a>. Thanks to xet7.</summary>
The line's `stroke-width=width` found the template's `width` helper - the
drawing's width - before the line's own field, so every line was drawn 800
wide, one black shape over the whole whiteboard. The lines use `strokeWidth`
now, and a test fails if any of their fields shares a helper's name.
</details>
and has the following developer-tooling fixes:
**The whole-suite run** - EVERYTHING, on a Mac and in a sandboxed editor.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/16103f9ad2">The test database is kept in .tools/, not beside the checkout</a>. Thanks to xet7.</summary>
`build.sh` and `build.bat` put the :3001 test database in the checkout's
parent directory, the one thing a test run wrote outside the repository, and a
sandboxed editor could not create it. It is `.tools/mongodb-test-<port>` now;
`toolsDirOnly.test.cjs` pins it in both scripts.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/7f948bf438">On macOS the Playwright container uses Docker's bridge network, not the host network</a>. Thanks to xet7.</summary>
Firefox tests waited the full 60 seconds for a page whose document never
came. Docker Desktop's host network lost about one connection in 75 to
`host.docker.internal` (8 of 600 connects never completed), while the bridge
network lost none in 1000. Nothing there needs the host network on macOS, so
the container uses the bridge network; Linux keeps the host network.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d9e48a909d">The list-sync specs' mock servers are reachable from WeKan when the browsers run in Docker on macOS</a>. Thanks to xet7.</summary>
WeKan fetched from mock Jira and GitLab servers on the container's loopback,
which Docker Desktop keeps from the host. build.sh already forwarded the test
SMTP port into the container; it now forwards a small pool of ports the same
way, and those specs take one of them.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ffe207e191">The Docker test proxy dials the host again when a connect is never answered</a>. Thanks to xet7.</summary>
On the bridge network too, Docker Desktop's gateway now and then left one
connect to the host unanswered through nine retransmits while the next went
straight through. The proxy reads nothing from the browser until a connection
is up, drops a dial that has not connected in 1.5 seconds and dials again, up
to five times, so no byte is lost or sent twice.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8a1f7a14a9">The Playwright container sees the repo and the files root at their own host paths</a>. Thanks to xet7.</summary>
Specs and the server share files by path. With the repository mounted at
`/repo`, the continuous backup tests gave the server a key file it could not
find and looked for a stream it wrote elsewhere. Both are mounted at their
host paths now, so a path names the same file on both sides.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a6bec88266">The phone invitation tile is measured in one layout with its buttons</a>. Thanks to xet7.</summary>
Measured one at a time, the tile and its buttons were taken from two layouts:
Firefox drew the row above the board list in between and moved the tile down
29 pixels. They are measured together now, and the test still fails when the
tile clips its buttons, the original #6488 bug.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f5eb4ecf61">The Mac smoke test asks libproc, not ps, whether a process group still lives</a>. Thanks to xet7.</summary>
When macOS refuses to signal a group of zombies, the smoke test asked `ps`
whether a member was alive, and a Mac that does not let the build run `ps`
failed a correct app. On macOS it asks the kernel through `proc_listpids` and
`proc_pidinfo` now, starting no process; elsewhere it uses `ps` as before.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/897376ba54">The button theme test checks the Board Settings that still have action buttons</a>. Thanks to xet7.</summary>
Card Settings has had no column actions since card actions moved into each
section's own menu, so the test failed on Card in every theme. Swimlane and
List are checked in all four themes, and the test says why Card is not.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d5e3325e87">Card Settings section order waits for the published board before reading the card</a>. Thanks to xet7.</summary>
The test writes the board straight to the database and reloads. The board
publication's cursor has `limit: 1`, so on the standalone test MongoDB it
polls and a reload could get the old board for up to ten seconds. The test
waits for the published board, up to 15 seconds, before it reads the card.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/89b64966c6">The password minimum specs wait until the server has the policy they wrote</a>. Thanks to xet7.</summary>
The specs write the password policy straight into the settings document, and
on the standalone test MongoDB the server's settings observer polls, so for up
to ten seconds it still enforced the old policy. `untilServerHas()` asks
`getPasswordPolicy` until the server has it, then reloads the sign-up form; 6
of 6 with two workers.
</details>
**Browser tests** - specs that read the page before it had settled, in Firefox.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dbf07aadf9">The activity recovery report is searched again until no worker lease covers it</a>. Thanks to xet7.</summary>
The report is a snapshot, and the live recovery worker's short lease could
make it show a seeded orphan as being delivered. The test searches again
until the snapshot falls between leases.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9188843369">The checklist drag tests drag once the opened card's rows hold still</a>. Thanks to xet7.</summary>
Sections drawn above the checklist moved its rows 50 pixels between measuring
and releasing, so the release opened an item's editor or dropped it in the
wrong place.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0cea141702">The duplicate relation picker is opened once the card has opened</a>. Thanks to xet7.</summary>
A picker opened while the card was still opening was closed again when the
card settled.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0403e27c62">The notification delivery sections are opened until they stay open</a>. Thanks to xet7.</summary>
The language arriving after the click drew the popup again with its sections
closed.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2042fd5102">The Blocks drag test grabs the block's body, not its edge</a>. Thanks to xet7.</summary>
A Khmer label overflows its block in Firefox, and the first point found was
the block's outline, where a press drags nothing.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0a1f8584ee">The board view popup is opened until it stays open for the Product Backlog</a>. Thanks to xet7.</summary>
The language arriving after the click drew the header again and closed the
popup.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/efe0e9287c">A language switch is waited for, and WebKit does not freeze on asking for a push subscription</a>. Thanks to xet7.</summary>
A spec that switched language and clicked at once clicked into the redraw
that followed, which closed its popup; every page of the test context now
waits until it shows the new language. Playwright's WebKit froze the page on
asking for a push subscription, which Member Notification Settings does on
opening; there the page answers that it has none.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0c8c254dc5">The viewport matrix cleans up only what it seeded</a>. Thanks to xet7.</summary>
The matrix is Chromium-only. In Firefox and WebKit its setup never ran, but
its cleanup did, and failed every viewport.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f241681e80">Background notification recovery checks its own mail</a>. Thanks to xet7.</summary>
The shared SMTP sink had caught another worker's mail first.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c71c875649">The custom field settings popup is opened until its Create button shows</a>. Thanks to xet7.</summary>
The opened card was still drawing and closed the popup again.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4a9c3f21d2">The drawing surface's width is waited for</a>. Thanks to xet7.</summary>
The element's layout follows its viewBox a frame later.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c8ed3e0fd0">The bulk export popup is opened until it stays</a>. Thanks to xet7.</summary>
All Boards was still drawing after the route changed, in all five Haitian
Creole tests.
</details>
**Test coverage** - October changes that had no test of their own.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/148d18d456">Server changes are tested: OpenSSL 4 decrypt refusal, the Scrum rollover lease, the backup catch-up scan</a>. Thanks to xet7.</summary>
Each with a negative test, and the lease with a tree-wide one: every lease
field is written only by its claim.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/79c40407d1">Admin Panel pane titles, audit log alignment, storage help, Data health and card reports are tested</a>. Thanks to xet7.</summary>
Unit and negative tests, and a Chromium browser test of the titles, the
right-to-left audit log, the storage help shown once and Data health's own
"Never".
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/121f37be91">The drawing refit, Due Cards scope, dependency import counts and SAML's one Issuer are tested</a>. Thanks to xet7.</summary>
A signed SAML Response with two Issuers is refused in the browser, which
node-saml 5.1.0 accepted; an assigned-only member's Due Cards show only their
own cards.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3ccb465c3f">The keyboard shortcut browser test focuses the minicard link itself</a>. Thanks to mimZD and xet7.</summary>
Chromium does not leave the focus on the minicard after its menu is closed,
so the test makes the reported state directly; it passes in Chromium and
WebKit.
</details>
**The source-loaded server** - starting WeKan from source for development.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/15877dd1bc">A local package fork that a prebuilt package implies is loaded first</a>. Thanks to xet7.</summary>
The source server stopped at "isopack dynamic-import not yet loaded". It now
gets past that, and still stops later on "Cannot find module
'meteor/accounts-base'"; the browser tests ran against the bundle server.
</details>
**The changelog** - its guards between releases.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/05cc2460ea">The short changelog guard holds between releases too, when there is no Upcoming section yet</a>. Thanks to xet7.</summary>
`changelogShort.test.cjs` assumed an Upcoming section was always there, so it
failed right after every release, before anything new was written. With none
it checks that the newest release comes first and that nothing links an
Upcoming that does not exist.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.77 2026-10-31 WeKan ® release
**In short:** Two **node test guards** follow the movable popups and the date
picker of v12.76.
This release has the following developer-tooling fix:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ff2c0ed7f6">The date-popup guards follow movable popups and the date picker's empty cells</a>. Thanks to xet7.</summary>
`calendarPopupResize.test.cjs` asserted that a popup without a date editor does
not drag, and `calendarTodayMarker.test.cjs` picked a month's days with
`filter(Boolean)`. Both described v12.76 before it changed on purpose: every
popup drags by its title bar, or by its handle alone with drag handles on, and
an empty picker cell is an object with its own `_id`. Both are pinned instead.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.76 2026-10-31 WeKan ® release
**In short:** Security features: a **password policy** with an optional
breached-password check, **Admin Panel / Problems / Password length**,
**compulsory two-factor authentication**, sign-out of **idle sessions** and of
every session after a password change, an **audit log** of what administrators
did, **HSTS**, a cross-site request guard, and **S3 encryption at rest**.
**Sign-up** works again; the date picker has **Today**, the whole card title
edits and **every popup can be moved**; and an old **Sandstorm** grain migrated
from MongoDB 3 shows its boards again.
This release adds the following security features:
**Signing in** - which passwords are accepted and which are reported, who must
have a second factor, and when sessions end.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d7abe9910a">New passwords have a minimum length, and a breached one can be refused</a>. Thanks to xet7.</summary>
`PASSWORD_MIN_LENGTH` (default 8) and `PASSWORD_CHECK_PWNED` (off by default),
in Admin Panel / People / Login and on every platform. The browser sends a
password to Meteor hashed, so the server checks every password that reaches it
as text - Admin Panel Set password and Add user, `POST /users/register` - and
the sign-up, Change Password, reset and enrolment forms check in the browser.
Signing in is never checked, so an existing shorter password keeps working
until it is changed. A breached password is found with Have I Been Pwned's
k-anonymity range API: only 5 hex digits of the SHA-1 leave WeKan, and an
unreachable service never refuses a password. A refused password is ordinary
use, not an attack, so it is not recorded in Admin Panel / Problems.
`tests/passwordPolicy.test.cjs` checks every place a password reaches the
server as text, and that nothing else sets one without the check.
</details>
- [The breached-password check computes its lookup digest once, with Web Crypto, in browser and server; it is never stored (CodeQL \#599)](https://github.com/wekan/wekan/commit/f406d46fe7).
Thanks to GitHub CodeQL and xet7.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c99aa7f845">Admin Panel / Problems / Password length lists who signs in with a too short password</a>. Thanks to xet7.</summary>
The minimum above was enforced as soon as it existed. Now `PASSWORD_MIN_LENGTH`
(default 8) is the length below which a sign-in is REPORTED, and the new
`PASSWORD_MIN_LENGTH_ENFORCE` (off by default) makes it a rule: a shorter new
password is refused, and an account that signs in with an older shorter one
sees only "Change your password" until it has a long enough one. Signing in is
never refused, so nobody is locked out by a minimum set after their password.
LDAP, OAuth2, OIDC, SAML, CAS and Sandstorm passwords are their provider's, and
are neither reported nor asked. Meteor sends a password hashed, so the browser
that typed it says "it was short" after signing in - never the password or its
length. The page has both settings and one row per account: username, first and
last such login, the latest IPv4 and IPv6 address with flag and city when a CDN
says where it is, the count and when it was fixed, filtered Too short (default),
Fixed or All. Node suites `passwordPolicy`, `passwordChangeRequired` and
`shortPasswordLogins`; Playwright `password-length-report` changes each setting
on the page and checks what it does, in Chromium and WebKit.
[Password length](docs/Features/Admin-Panel/Problems/Password-length.md).
</details>
- [The Password length pane names itself in the title bar, like every other Admin Panel pane](https://github.com/wekan/wekan/commit/85af731c1b).
Thanks to xet7.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c99aa7f845">Changing one's own password signs every session of the account out, this one too</a>. Thanks to xet7.</summary>
Meteor's Change Password kept the session that changed it, and a reset link
signed the reset in. Now Member Settings, the forced change and a reset link
all end every session of the account, and only the new password signs in
again; the browser goes to the sign-in page as soon as it is signed out, since
the server closes the connection before the answer can arrive. An
administrator's Set password already signed everything out.
`password-change-other-sessions.e2e.js` changes it in one of two browsers:
both are signed out, the old password is refused and the new one signs in.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d7abe9910a">Two-factor authentication can be made compulsory, for administrators or for everybody</a>. Thanks to xet7.</summary>
`REQUIRE_2FA`: `off` (default), `admins` or `all`. It covers the accounts that
sign in with a WeKan password; LDAP, OAuth2, SAML, CAS and Sandstorm accounts
sign in through an identity provider and are never asked. Such an account can
still sign in - it has to, to enrol - and sees only the set-up until it has
2FA. Until then the server refuses every method and publication but signing in
and enrolling, HTTP routes do not accept its login and the REST API gives it no
token; once required, 2FA cannot be switched off. An account between signing
in and enrolling is ordinary use, so nothing is recorded in Problems.
`tests/requireTwoFactor.test.cjs` runs the real wrappers.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/41173f0f6b">A browser session is signed out after LOGOUT_IDLE_MINUTES without activity</a>. Thanks to xet7.</summary>
`LOGOUT_WITH_TIMER` ends a login at a deadline however busy its user is; nothing
ended a session somebody walked away from. The browser now reports a key press,
click, touch or scroll at most once a minute, the server keeps the time per
login token in `idleSessions`, and once a minute every token idle past the
limit is removed, which signs its connections out. A tab that reconnects by
itself - a laptop waking up - is not activity, and REST API tokens are never
removed. The time is kept beside the user rather than on the token, because
updating one element of the token array needs the positional `$` operator,
which not every database WeKan runs on supports. `tests/idleLogout.test.cjs`.
</details>
**HTTP** - what every response tells the browser, and which requests are
accepted only from WeKan's own pages.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4993242c5f">Strict-Transport-Security on every response of an HTTPS site</a>. Thanks to xet7.</summary>
Without HSTS, a link typed as plain HTTP or a downgrade on a hostile network
could strip TLS from the next visit unless the reverse proxy added the header.
It is now sent whenever `ROOT_URL` is https: 180 days, this host only.
`HSTS_MAX_AGE`, `HSTS_INCLUDE_SUBDOMAINS` and `HSTS_PRELOAD` change it; 0 stops
it, and preload is sent only with includeSubDomains and at least a year. Every
response also carries `X-Content-Type-Options: nosniff` and
`Referrer-Policy: strict-origin-when-cross-origin`. A header has no attempt to
record. `tests/securityHeaders.test.cjs`.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/48a2e97ea3">A state-changing request that logs in only with the cookie must come from WeKan itself</a>. Thanks to xet7.</summary>
`POST /import-trello` and other routes accept the login cookie as the login.
The cookie is SameSite=Strict, which keeps it off a cross-site request in
today's browsers; this is the second line, for older browsers, same-site
sibling hosts and routes added later. A POST, PUT, PATCH or DELETE whose only
login is the cookie must carry an Origin, or Referer, naming `ROOT_URL`'s host
or the Host / X-Forwarded-Host it was sent to - on Sandstorm, the grain's own
host. A refusal is recorded in Admin Panel / Problems as
`csrf.cross-site-cookie` and never blocks the account whose cookie it was.
`tests/crossSiteRequest.test.cjs` also fails when a route registers a handler
that runs before the guard.
</details>
**The Admin Panel** - a record of what administrators changed.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4d5b832499">People / Audit log shows what site administrators did</a>. Thanks to xet7.</summary>
Nothing recorded who set somebody's password, made somebody an admin, changed
a login setting or restored a backup. Every call of those administrative
methods by a site administrator is now kept for a year, one document each:
when, who, the action, its outcome - a refused call too - its arguments and
the address. A password or a secret never reaches the trail: credential-named
keys, password arguments by position and passwords inside URLs are replaced by
`[redacted]`. An administrator editing cards is not in it, and a trail that
cannot be written never changes what the method did.
`tests/adminAudit.test.cjs`.
</details>
- [The audit log has its own URL, /admin/people/audit-log, a title, and right-to-left alignment](https://github.com/wekan/wekan/commit/2017f2871f).
Thanks to xet7.
**Attachments** - where the files are kept, and how.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/482c2c29f4">Files and backups in S3 can be encrypted at rest</a>. Thanks to xet7.</summary>
Admin Panel / Attachments / S3 / Server-side encryption: SSE-S3 (AES256) or
SSE-KMS, with a KMS key or the account's default. S3 encrypts each object as it
is written and decrypts it for WeKan on read, so reads, range requests and
moves are unchanged; every upload, renamed object and backup written to S3
carries it. On the filesystem, encryption at rest stays with the disk or volume
(LUKS, an encrypted cloud volume): exports, PDF, thumbnails, virus scans,
backups and WebDAV all read the stored files directly.
`tests/s3Encryption.test.cjs`.
</details>
and adds the following new features:
**Dates** - choosing a date in a date popup.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bc21c901f4">The date picker has a Today button, back to today's month from wherever the arrows went</a>. Thanks to mimZD and xet7.</summary>
The board Calendar view and Calendar mode already had Today; the date picker
of every date popup had only « ‹ › ». Today shows today's month and focuses
today, and changes nothing until a day is clicked and saved. Testing it found a
picker bug that was already there: a month navigated back to was drawn wrong -
October as "29, empty, 30, empty", without its 31st - because Blaze matched the
weeks and days, which had no `_id`, by position. Every week and cell has one
now. `calendarPickerControls.test.cjs` and `calendar-today-marker.e2e.js`,
which checks every day of the month in order after Today.
</details>
**The opened card and popups** - editing a card's title, and moving windows.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/22a12dc7f8">The whole card title edits; the window and every popup move by their bar or drag handle</a>. Thanks to mimZD and xet7.</summary>
The title was split like a minicard's: the leading half edited and the
trailing half dragged the window, so a click on the right of the title did not
edit. All of it edits now. With drag handles off the window is moved from the
empty space of its header row above the title, between the collapse caret and
the menu; with them on, by the drag handle alone. Every popup moves the same
way - by its title bar, or its handle alone - where only four kinds moved
before, and a drag no longer makes a popup placed from the right edge, or one
with a margin, jump as it begins. `card-title-edit-and-move-6839.e2e.js` in
Chromium and WebKit.
</details>
**Member Settings** - Change Settings.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ffd5cbd2ad">Change Settings: icons in a column on the first line, compact wrapped lines, rows divided</a>. Thanks to mimZD and xet7.</summary>
Its settings are sentences, and most wrap; on the shared popup list each line
was 33px high with the icon centred between them and nothing between one
setting and the next. Each row is an icon column, the words and the check mark
now, the icon on the first line, a wrapped line an ordinary line high, and a
rule between rows. `change-settings-popup-6835.e2e.js` in Persian and English.
</details>
and fixes the following bugs:
**The d shortcut** - opening the Due date editor from the keyboard.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b1403ca7af">d opens the Due date editor of a card that is still being opened, and it stays open</a>. Thanks to mimZD and xet7.</summary>
On a slower server a card's details render a few hundred milliseconds after
the click that opens it, and `d` pressed in that moment found no Due date
control and did nothing; only the hovered-minicard path waited. Both wait now
for that card's control. The card details also closed their card's popup when
merely rendered again, so a popup could open and shut at once; they close it
now only when the card is closed or another card opened. The Playwright test
emulates a 300 ms connection, and fails on the code before this change.
</details>
- [Typing d in the card title types at the end of the title in WebKit too](https://github.com/wekan/wekan/commit/73c21dfe3e).
Thanks to xet7.
**Minicards** - what a minicard shows, and where.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/cf4b48df09">A due date uses the minicard's width instead of three lines beside an empty strip</a>. Thanks to mimZD and xet7.</summary>
The dates row reserved 84px at its trailing edge - a third of a narrow card -
for nothing, and a due date with its "1 day left" was broken over three lines.
It reserves the drag handle's 24px now. The rest of
[#6834](https://github.com/wekan/wekan/issues/6834) is in TODO Later.
`minicard-dates-width-6834.e2e.js` in English and Persian.
</details>
- [A card-button rule is a button on the minicard: Board Settings / Card, Card buttons, Show on Minicard, since v12.16, now with a test](https://github.com/wekan/wekan/commit/429fdc5319).
Thanks to mimZD and xet7.
**Signing up** - registering a new account with the sign-up form.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/508484ef1e">Registering no longer answers Internal server error, and signs the new user in</a>. Thanks to xet7.</summary>
The REQUIRE_2FA wrapper of every method and publication, and the Audit log's
wrapper of the administrators' methods, awaited before calling the handler -
even with REQUIRE_2FA off. audit-argument-checks asks synchronously, the moment
a handler returns, whether check() saw every argument, so the handler's own
check() came too late: sign-up answered "Internal server error" while the
account was created anyway (the next try said it already existed), and the
starredPublicBoards and translation publications failed for everybody. A
wrapper now calls the handler at once when it needs no lookup, and hands the
audit over when it must await (`server/lib/argumentChecks.js`).
`tests/wrappedHandlerArgumentChecks.test.cjs` runs both wrappers under a model
of the audit, and the sign-up Playwright test registers - with no mail server
configured - and checks that the new user is signed in.
</details>
**Upgrading old data** - documents from old WeKan versions are brought to the
current structure at startup, so nothing that is in the database stays hidden.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2ab0afdc93">Boards from before the templates feature are shown on All Boards again</a>. Thanks to xet7.</summary>
A Sandstorm grain migrated from MongoDB 3 to FerretDB copied every document -
1 board, 31 cards, 8 lists - and then showed an empty All Boards page. Boards,
swimlanes, lists and cards made before the templates feature have no `type`:
the schemas fill it on insert only. All Boards publishes `type: 'board'` and
the card queries ask for `cardType-card`, and a missing field matches neither.
The new `type-defaults-backfill` startup step sets the schema default where
`type` is missing, null or empty, and never changes an explicit type such as
a template or a linked card. `tests/schemaUpgradeSteps.test.cjs` upgrades the
grain's shape and checks that typed documents are left alone.
The same log showed `merge-per-swimlane-lists FAILED ... missing schema for key
"swimlaneId"`: FerretDB's SQLite `distinct` could not read documents without
the fields it asked for. That is fixed in the FerretDB fork (Upcoming FerretDB
release), which the next bundle includes.
</details>
**Snap** - backing up, restoring and repairing a snap from the host's shell.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0b5c855820">Backup and restore instructions find mongodump and mongorestore on AlmaLinux, Rocky Linux and Fedora</a>. Thanks to Group-Deed and xet7.</summary>
snapd keeps the snaps in `/var/lib/snapd/snap` on Fedora, RHEL, AlmaLinux,
Rocky Linux, CentOS Stream and Arch Linux, which have no `/snap`. The
instructions exported `/snap/wekan/current/bin` and its `lib/x86_64-linux-gnu`,
so restoring a backup on Rocky Linux failed until both were found by hand, and
only Arch had a note - for the library path alone. Every instruction now picks
the directory with one `S=` line and the CPU's library directory with
`$(uname -m)`, so arm64 works too. `database-role`, `database-compare` and
`database-merge`, which can be run from a host shell, find the snap in either
place. `tests/snapPathElLinux.test.cjs` fails on an export of a `/snap`-only
path in any document and on a script that falls back to `/snap` alone.
</details>
and has the following developer-tooling fixes:
**The build log** - what a build from build.sh and build.bat writes while it
runs, and how much of it.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6551062492">EVERYTHING builds without the per-require module trace, so it no longer fills the disk</a>. Thanks to xet7.</summary>
The build's `NODE_DEBUG=module` trace wrote over 12 GB in the first half hour
of an EVERYTHING run on macOS - twice, through tee - and slowed the build it
traced, so the run would fill the disk before it reached the tests. A build
from the `build.sh` menu still traces by default; `WEKAN_BUILD_TRACE_MODULES=0`
turns it off, and EVERYTHING sets that unless the caller asked for `1`. The
other build diagnostics (resolver commands, npm verbose, `METEOR_PROFILE`) are
unchanged. `tests/buildCommandOutput.test.cjs` checks that a menu build still
traces and that nothing exports the module trace unconditionally.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5a13e927bc">Every build traces module resolution only when WEKAN_BUILD_TRACE_MODULES=1 asks for it</a>. Thanks to xet7.</summary>
Turning the trace off for EVERYTHING alone was not enough: a dev build from the
menu wrote 1.1 GB, three million lines, in its first three minutes. So the
trace is now opt-in in `build.sh` and `build.bat` alike, and the diagnostics
line says whether it is on and how to turn it on. The entry above describes
the menu default as it was then. `tests/buildCommandOutput.test.cjs` pins the
opt-in in both scripts, and fails on a trace turned on by default or
unconditionally.
</details>
**Meteor in .tools** - starting the Meteor that build.sh and build.bat keep in
the repository's own tools directory.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9a118c83b2">Meteor starts with one springboard instead of springboarding forever</a>. Thanks to xet7.</summary>
A build from the menu printed `uname -p` and `sysctl hw.cpu64bit_capable` over
and over and never reached its first package. With `METEOR_WAREHOUSE_DIR` set
and no `SANDBOX`, Meteor takes the running tool to be wherever the warehouse's
`meteor` link points - 3.5.4, the release it was installed with - so with
`.meteor/release` at 3.6-rc.1 every start springboarded to 3.6.0-rc.1, found
itself the wrong tool again and springboarded again: 28 times in 20 seconds of
`meteor --version`. The 1.1 GB build log above was the same loop, its module
trace printed again on every exec. `releases/tools-env.sh` and `tools-env.bat`
now set `SANDBOX=true` beside the warehouse, as Meteor's own self-test does,
and so do the Nextcloud test scripts and the dev-source runner.
`tests/meteorSpringboardLoop.test.cjs` checks both scripts, fails on any file
that sets the warehouse without `SANDBOX`, and runs the installed Meteor to
count its springboards.
</details>
and updates the following documentation:
- [Security settings: every hardening option of a WeKan site on one page](https://github.com/wekan/wekan/commit/302b47db6f).
Thanks to xet7.
- [Admin Panel / People / Audit log: what it records and what it never does](https://github.com/wekan/wekan/commit/f165a24192).
Thanks to xet7.
and updates the following translations:
**Languages updated:** 234 languages.
**Password length** - the strings of Admin Panel / Problems / Password length
and of the Change your password dialog.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3f9379ac86">The 18 new password-length strings in every language</a>. Thanks to xet7.</summary>
Each string is in the language and script its locale tag names, with the terms
the file already uses for password, account, save and sign in, and `__min__`
kept exactly; only those 18 keys changed in each file. The commit lists the
languages written with low confidence, for a speaker to correct on Transifex.
</details>
**Security features** - the strings of the password policy, two-factor, the
audit log and S3 encryption.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/de23dece66">The 16 new security strings in every language, and Frisian two-factor text that was Dutch</a>. Thanks to xet7.</summary>
Each string is in the language and script its locale tag names, with the terms
the file already uses for password, account, attachment, backup and two-factor
authentication, and `__min__` kept exactly. fy and fy-NL had Dutch two-factor
strings, now Frisian (`tests/frisianTwoFactorLanguage.test.cjs`); Tigre spells
storage መክዘን, not Tigrinya's መኽዘን. The commit lists the languages written
with low confidence, for a speaker to correct on Transifex.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.75 2026-10-30 WeKan ® release
**In short:** Changing a password signs the account out in every other browser,
and now a test proves it in two real browsers.
This release adds the following security regression test:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dc9a7bb1ac">A changed password signs the account out in every other browser</a>. Thanks to xet7.</summary>
A report described a session that stayed signed in on a second browser after
the password was changed on the first. WeKan does not do that: Member Settings
/ Change Password is Meteor's changePassword, which removes every login token
but the changing session's, the admin's Set password and a password reset
remove them all, and a session whose token is gone is closed.
`password-change-other-sessions.e2e.js` runs the report in two browsers - the
other one is signed out within seconds, stays signed out after a reload, and
only the new password works - three times in Chromium and in WebKit.
`tests/passwordChangeSessions.test.cjs` pins those paths and fails on a password
change that keeps the other sessions.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.74 2026-10-30 WeKan ® release
**In short:** **Caddy** and **FerretDB**, the Go servers WeKan ships, no longer
open MPTCP listeners, which Linux kernel bug **CVE-2026-98432** is reachable
through. The real fix is a patched host kernel.
This release has the following security hardening:
**Go servers** - Caddy and FerretDB, and the kernel bug behind their listeners.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e9751b65d4">Caddy and FerretDB run with no MPTCP listener (CVE-2026-98432)</a>. Thanks to xet7.</summary>
CVE-2026-98432 is a Linux kernel use-after-free in IPv4 TCP connection
handling, shown to give remote code execution through an MPTCP listener, and
since Go 1.24 every Linux listen socket of a Go program is MPTCP by default.
Every place WeKan starts Caddy or FerretDB sets `GODEBUG=multipathtcp=0`: the
snap, the Docker entrypoint, the bundle launcher, every docker-compose FerretDB
and the Sandstorm grain. Caddy needs no rebuild, because Go reads `GODEBUG` at
start, and wekan/FerretDB now builds with `godebug multipathtcp=0` too. An
operator's own `multipathtcp` setting is kept. Checked on Linux: a Go server's
default listener uses MPTCP, and with the setting it does not.
`tests/goMptcpOff.test.cjs` pins every launcher. This is a kernel bug, not
WeKan's, so there is no attempt to show in Admin Panel → Problems; upgrade the
host kernel.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.73 2026-10-30 WeKan ® release
**In short:** **CHANGELOG.md** is now short: the In short of the upcoming
release and of the five newest releases, in plain Markdown. The status and every
change of each release are in **CHANGELOG-big.md**, which is the one that is
written. **CodeQL** alert #598 in the Sandstorm host test is fixed, and the
release script pulls before releasing, so a moved main no longer stops it.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/fcd995095d">Alert #598: the Sandstorm host test names no host but sandstorm.org</a>. Thanks to GitHub CodeQL and xet7.</summary>
CodeQL flagged incomplete URL substring sanitization (CWE-20) in
`tests/sandstormOrgHosts.test.cjs`, which picked the files to check by whether
they contained the old Sandstorm host name as a substring. The test no longer
names that host at all: it finds every Sandstorm host on a top-level domain
other than `org` by its shape, written as a URL, so property paths, file names
and the domains reserved for tests are not taken for hosts. Run against the
files from before the switch to sandstorm.org, it finds the 13 old hosts. The
finding was in a test, not in WeKan, so there is no attempt to show in Admin
Panel → Problems.
</details>
and has the following developer-tooling changes:
**Publishing a release** - how the release script reaches GitHub.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e372016826">Release All pulls before releasing, and pulls and retries when its push is refused</a>. Thanks to xet7.</summary>
v12.73 was prepared on a main that origin had moved past: the release bot's
v12.72 version bump had landed meanwhile. The push of "Prepare v12.73 release"
was refused and the script stopped before dispatching the workflow, so no
release ran and no tag was made. `release-all.sh` now runs build.sh's git pull
before it chooses the version or renames anything, and pushes with build.sh's
git push, which pulls and pushes once more when origin moved meanwhile. If the
push still does not finish, it says the workflow was not dispatched and how to
dispatch it. `tests/releaseUpcomingPreflight.test.cjs` pins the order.
</details>
**Release history** - which file is written, and which is read.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f1c0db247a">CHANGELOG.md is the In short of the five newest releases, written from CHANGELOG-big.md</a>. Thanks to xet7.</summary>
The changelog is `CHANGELOG-big.md` now, renamed in a commit of its own
(`61e838df46`) so its history follows it: `# Status`, `# Upcoming` and every
release in full. The release tooling, the release workflow and its notes'
link, the commit-link repair, the archive and the tests use it.
`CHANGELOG.md` is generated from it by `releases/changelog-short.mjs`: no
Status or Upcoming, only each of the five newest releases' In short and a link
to its full section, with no `<details>` or `<summary>` and only Markdown
links. `release-all.sh` regenerates it in the release commit, the Sandstorm App
Market shows it, and `tests/changelogShort.test.cjs` fails when it is not what
the generator writes. CLAUDE.md and AGENTS.md describe both files.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1af642069c">The short CHANGELOG.md starts with the upcoming release's In short</a>. Thanks to xet7.</summary>
What was about to be released was only in `CHANGELOG-big.md`. The short file
now starts with the unreleased `# Upcoming WeKan ® release` - its In short and
a link to its full section - with the five newest releases below it; right
after a release, when there is no Upcoming yet, none is invented. CLAUDE.md and
AGENTS.md say what "update wekan changelog" does to both files.
`tests/changelogShort.test.cjs` pins it.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.72 2026-10-30 WeKan ® release
**In short:** A card link can no longer launch an operating-system handler with
a code-execution history, such as **ms-msdt** (Follina) or **telnet**, even if
an administrator lists it, and an attempt to write one shows in **Admin Panel →
Problems**. The **Apache** guide follows the 2024 Confusion Attacks, and the
Sandstorm store build publishes to **both app markets**.
This release has the following security hardening:
**Card links** - which custom URL schemes may become clickable.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b4e5f90afb">A card link never launches an OS handler with a code-execution history, even when listed</a>. Thanks to xet7.</summary>
Custom URL schemes link only when Admin Panel → Features → URL lists them, and
never `javascript:` or another scheme that runs code in the page. A listed
operating-system handler was linked, though, and some have a public
code-execution history: `ms-msdt` (Follina, CVE-2022-30190), `search-ms`,
`search`, `ms-officecmd`, `ms-appinstaller`, `ms-cxh`, `ms-cxh-full` and
`telnet`, the last step of a 2026 browser full chain. They are never linked now,
by the app and by the markdown package, and a click on one does nothing. A card,
comment or checklist that adds a link to one shows in Admin Panel → Problems as
HandlerLink, with who wrote it. The Visibility docs say what a listed `file:`
does on Windows. `tests/urlSchemeAllowlist.test.cjs` and
`custom-url-schemes.e2e.js` (Chromium and WebKit) pin it.
</details>
**Apache** - the reverse proxy guide.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1a97c8bd86">The Apache guide asks for httpd 2.4.60 and builds no proxy target from the request</a>. Thanks to xet7.</summary>
Checked against the 2024 Confusion Attacks on Apache httpd (CVE-2024-38472 to
CVE-2024-38477 and CVE-2024-39573, fixed in 2.4.60). The examples passed the
WebSocket with `ProxyPassMatch` and `$1`, a request capture in a proxy target,
and the sub-URL one never matched its WebSocket at all. Both use
`ProxyPass ... upgrade=websocket` now, and a new section lists what not to add.
Verified with Apache 2.4.67 in front of a WebSocket server, at `/` and at
`/wekan`; `tests/apacheReverseProxy.test.cjs` pins it.
</details>
and has the following developer-tooling fixes:
**Sandstorm** - publishing the Sandstorm package, and how its app market listing
looks.
- [The store build publishes WeKan to both Sandstorm app markets](https://github.com/wekan/wekan/commit/10851cd3f4),
apps.sandstorm.org and apps.sandstorm.io. Thanks to ocdtrekkie.
- [Fix the publish command's argument order](https://github.com/wekan/wekan/commit/2c1fc39f61).
Thanks to ocdtrekkie and xet7.
- [The sandstorm.org host test allows that publish step](https://github.com/wekan/wekan/commit/55b71ce19e),
and checks both markets are published to. Thanks to xet7.
- [New board view screenshots for the Sandstorm app market listing](https://github.com/wekan/wekan/commit/a086323a1f).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.71 2026-10-30 WeKan ® release
**In short:** The **Sandstorm** build installs Sandstorm from
**install.sandstorm.org**, as the Sandstorm workflows already did, and the
docs link sandstorm.org. The Hall of Fame guards read every vulnerability name
now that each one links to its own page.
This release has the following developer-tooling fixes:
**Releases** - where the Sandstorm build gets Sandstorm.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ca2dc5fae5">The Sandstorm build installs Sandstorm from install.sandstorm.org</a>. Thanks to xet7.</summary>
v12.69's Sandstorm build stopped because the certificate of
install.sandstorm.io had expired. The Release All job fetches the installer
from install.sandstorm.org, as the standalone Sandstorm workflows already did,
and so does `releases/install-sandstorm.sh`. That installer downloads Sandstorm
from dl.sandstorm.org and sets up a development server at local.sandstorm.org,
so the docs and code comments link sandstorm.org too, each address checked;
only the demo stays on sandstorm.io, which has no sandstorm.org name.
`tests/sandstormOrgHosts.test.cjs` keeps it that way.
</details>
**Tests** - the guards that compare WeKan's security tests with the Hall of
Fame.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/43c4d41877">The Hall of Fame guards read a linked vulnerability name too</a>. Thanks to xet7.</summary>
The two suites that compare WeKan's security tests with the Hall of Fame read
only names without a link, so the four whose names already linked to their
pages - ImportPlacementBleed, SamlSubjectBleed, StampBleed and UploadBleed -
were never checked. On wekan.fi every name is now a link; the suites read it,
and list those four as guarded by the tests that name them.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.70 2026-10-29 WeKan ® release
**In short:** The **AppImages** and the **Launchpad** snaps build again: their
FerretDB and mongo-tools are readable by every user, and the snap source
carries what its downloads need. **Continuous backup** names a changed
encrypted file as changed with **OpenSSL 4**, and the release and test tooling
runs on macOS and reports a bad certificate as one.
This release fixes the following bugs:
**Continuous backup** - restoring an encrypted backup file.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ffe49dd2fd">A changed encrypted file is reported as changed, also with OpenSSL 4</a>. Thanks to xet7.</summary>
A Node.js linked to OpenSSL 4 reports an AES-GCM tag mismatch as
`ERR_OSSL_BAD_DECRYPT` ("bad decrypt"), where OpenSSL 3 says "Unsupported state
or unable to authenticate data". `decryptFile()` matched only the second, so
with OpenSSL 4 a changed backup file or a wrong key surfaced as a raw OpenSSL
error instead of "An encrypted backup file has changed or the key is wrong".
Both are recognised, and the partial output is still removed.
`tests/continuousBackupEncryption.test.cjs` passes with OpenSSL 4.1.0 and
3.5.9.
</details>
and has the following developer-tooling fixes:
**Releases** - the AppImages, the Launchpad snaps, the Bun assets and what a
failed download says.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dfdadaa8ca">The AppImages are built again: FerretDB and mongo-tools are readable by every user</a>. Thanks to xet7.</summary>
`releases/fetch.sh` downloaded into a `mktemp` file and moved it into place,
so every download kept mktemp's 0600, and the bundled `ferretdb` and
`mongo-tools` were 0711 after `chmod +x`. Since that day's appimagetool
refuses such a file - a user other than the owner gets "Permission denied" -
all six v12.69 AppImages failed. A download gets the mode its umask gives a
new file, and the AppDir is made `a+rX` before it is packed;
`tests/releaseDownloads.test.cjs` fails on the old `fetch.sh` with 711.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a1f2c006b4">The Launchpad snap source carries the tools-env.sh that fetch.sh needs</a>. Thanks to xet7.</summary>
Only the files the snap recipe reads are uploaded to Launchpad, and the list
had `releases/fetch.sh` without the `releases/tools-env.sh` it sources. In the
armhf build the caddy part's `fetch.sh` had no `TMPDIR`, and curl got an empty
`-o` seven times, with pauses of up to 480 s, before it was called an outage.
The file is uploaded, `tools-env.sh` makes `.tools/tmp` in such a tree, and
`fetch.sh` stops at once without a temporary file or when curl cannot start.
`tests/releaseSnapLaunchpadFlatten.test.cjs` requires every `releases/` file a
listed script uses to be listed. s390x had failed on Launchpad itself: even a
new build answered "Could not find repository at path".
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6bbb4d39bc">A download whose TLS certificate cannot be verified is named as that, not as an outage</a>. Thanks to xet7.</summary>
v12.69's Sandstorm build stopped because the certificate of
install.sandstorm.io expired that day, and `fetch.sh` reported "The server is
having an outage" for curl's exit 60. It now says the certificate could not be
verified and that only that server's owner can fix it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2dcccf0d4a">The Bun release assets build with Python 3.9, which macOS ships as python3</a>. Thanks to xet7.</summary>
`hashlib.file_digest` is Python 3.11+ and `Path.write_text(newline=)` is
3.10+, so `releases/bun/assets.py` stopped with AttributeError and TypeError on
macOS's python3 3.9. It hashes in chunks and writes the LF-only `.sha256sum`
through `open(newline='\n')`; `tests/bun-release.py` still pins LF on every
platform and that neither newer call comes back.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/48a8937ff0">A snap attempt finds its own Launchpad build log by name, not only by mtime</a>. Thanks to xet7.</summary>
Whether a build log belonged to the current attempt was decided by `-nt`
against the attempt's marker alone. bash 3.2 compares whole seconds, so a log
written in the marker's second was not "newer": it was neither printed nor
checked for a Launchpad infrastructure failure. `this_attempts_log()` counts a
log whose name did not exist when the attempt started, or one rewritten after
the marker, and an earlier attempt's untouched log still does not count.
`tests/snapLaunchpadInfraRetry.test.cjs` covers all three cases.
</details>
**Tests** - the suites that need process listings and sandbox-exec on macOS,
and three that tested the wrong row, too early or the old layout.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bf266fb4da">DB conformance recognises a live run where ps is not allowed</a>. Thanks to xet7.</summary>
Inside a sandboxed macOS app `ps` fails with "operation not permitted", so
every live conformance run looked dead and a second run would remove its
containers. `command_line_of()` falls back to `pgrep -lf`, which still works
there, and `tests/dbConformanceStaleContainers.test.cjs` pins the fallback.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5534c8830c">The macOS suites run inside the sandboxed editor too</a>. Thanks to xet7.</summary>
`tests/mac-smoke-app.py` checked with `ps` that a killed descendant was gone;
it waits for `os.kill(pid, 0)` to find it gone and uses `ps` only to tell a
zombie from a live process. `tests/macosSandbox.test.cjs` skips its live
`sandbox-exec` check only when even an `(allow default)` profile cannot be
applied, which is the case inside the sandbox it tests.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d06b468629">ReplyBleed's mocha test counts the attempt in every ReplyBleed row</a>. Thanks to xet7.</summary>
ReplyBleed is one Admin Panel → Problems row per source, and the webhook's
source is the client address, so in a test database that persists between
runs the test's `findOne` read the bounce test's row, whose count the spoofed
reply never moves. It sums the count over all ReplyBleed rows, as
`emailBounces.tests.js` already does.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e4b83d2274">The keyboard card move test waits for the reset sorts to reach the client</a>. Thanks to xet7.</summary>
The test reset three cards to sort 0 in the database and waited only for id
order on the page, which the old sorts already gave one time in six, so the
move was sometimes computed from stale values and failed one run in three. It
waits for the client's own copy of the three cards.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/856a32968b">The board drag settings test opens Card's Draggable table in its section hamburger</a>. Thanks to xet7.</summary>
Since Board Settings / Card became the card's Card Settings, its Draggable
table is in the Card section's hamburger, and the test failed in every
language waiting for it in the popup. It opens Card as the cog menu does, as
`board-settings-columns.e2e.js` already does.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.69 2026-10-29 WeKan ® release
**In short:** **ImportPlacementBleed** is fixed: an import is placed only on its
own board. **Invite via Email** adds the invited new user again, instead of
the first user of the database. The **OpenBSD** bundle starts again, and
**History** works on **FerretDB**: date custom field changes are recorded there,
and the database migration says when documents did not arrive.
This release fixes the following CRITICAL SECURITY ISSUE of [ImportPlacementBleed](https://wekan.fi/hall-of-fame/importplacementbleed/):
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0d6d481fcb">An import into a board is placed only beside that board's own list, swimlane or card</a>. Thanks to Zachariah Mithani (AploSec) and xet7.</summary>
The scoped importer, reached by `POST /api/import/zip` and the `importScoped`
method, authorized the import on `boardId` alone. The `listId` and
`swimlaneId` beside it were written onto the new cards as given, and a `cardId`
was looked up with no board, so a writer of board A could create cards on A
whose list and swimlane were board B's, and probe whether a card id existed on
a board they cannot see. Before anything is written, the list, swimlane and
card are now looked up together with the board's id; an id of another board is
refused (400 over REST, `error-import-placement` from the method), with the
same answer for a foreign card and a missing one. A refusal is shown in Admin
Panel → Problems as ImportPlacementBleed, without blocking the account, since
an import menu left open while its list moved can reach it.
`tests/importPlacementBleed.test.cjs` pins the decision, the check before every
write and a negative scan for a target id used without the board;
`security-import-placement.e2e.js` runs the reporter's attack by both routes
and fails on the previous code.
</details>
and fixes the following bugs:
**Board members** - who an invitation adds to a board.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8556459f97">Invite via Email adds the invited new user, not the first user of the database</a>. Thanks to momosari and xet7.</summary>
Meteor 3's server `Accounts.createUser` is `createUserAsync`, and
`inviteUserToBoard` called it without `await`. The new user's id was a Promise,
which `findOneAsync` read as the empty selector `{}`, so it returned the FIRST
user - usually the site administrator - who was added to the board and got the
invitee's language and invited boards
([#6831](https://github.com/wekan/wekan/issues/6831)). It happened on any
database, not only FerretDB. `createUser` and `sendEnrollmentEmail` are awaited,
the user found must be the one just created, and every server `ReactiveCache`
getter now refuses a Promise as its id or selector, so a missing `await` fails
loudly instead of picking the first document.
`tests/inviteNewUserByEmail.test.cjs`
pins the guard, the reporter's case and that no server file calls an async
Accounts API without `await`; `invite-member-email-case.e2e.js` invites a new
address and checks that exactly that account joined - it fails on the previous
code.
</details>
**Bundles** - starting WeKan on the BSDs.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ec6cf66bff">The OpenBSD bundle starts again, start-wekan.sh takes only a number from sysctl</a>. Thanks to xet7.</summary>
OpenBSD's `sysctl` prints nothing and exits 0 for a name it does not have, so
`sysctl -n hw.memsize || sysctl -n hw.physmem` stopped at the empty
`hw.memsize`, and `$(( / 1048576 ))` ended `start-wekan.sh` before WeKan
answered (the v12.68 BSD bundles job). Both `start-wekan.sh` copies now try
`hw.memsize`, `hw.physmem64` (NetBSD, whose `hw.physmem` is 32-bit) and
`hw.physmem` in turn and take the first numeric answer.
`tests/macosShellPortability.test.cjs` runs those lines against fake OpenBSD,
NetBSD and macOS `sysctl`s, and shows the old chain failing on the OpenBSD one.
</details>
**History on FerretDB** - what the History of a card keeps, and what the
database migration copies.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/01968fc637">Date custom field changes are recorded on FerretDB, and the migration reports refused documents</a>. Thanks to vijlautens and xet7.</summary>
A History row of a date-valued custom field held `datePaths` as an array of
path arrays, `[["3","value"]]`. FerretDB v1 does not store an array inside an
array, so on FerretDB the change was not recorded, and the text database
migration to FerretDB dropped those rows while it swallowed the per-document
error and still reported success
([#6832](https://github.com/wekan/wekan/issues/6832)). Each path is now a JSON
string, `'["3","value"]'`; rows already written in the array form on MongoDB
still restore, and the migration to FerretDB rewrites them to strings. The
migration counts every document the target refuses and ends as failed, with the
count per collection and the first error. `tests/changeHistoryGroups.test.cjs`
pins the string form, the legacy restore, the rewrite with no array left inside
an array, and that a refused document is counted before success;
`history-nested-dates.e2e.js` checks the stored row.
</details>
and has the following release-tooling fix:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/99d9958e39">Release all missing builds the missing assets even when the rerun of failed jobs fails again</a>. Thanks to xet7.</summary>
The planner needed `recover`, so one job failing again in the rerun of the full
release skipped the planner and every build after it. For v12.68 the rerun
failed on four Launchpad snaps (503 / timeout) and on the OpenBSD bundle, whose
`start-wekan.sh` fault is in the tagged code and so in every rerun of it. The
planner now runs unless the run is cancelled, each job after it checks the
planner's own result, and `recover` stays red and names the jobs that failed
again. `tests/retryFailedRelease.test.py` pins both.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.68 2026-10-29 WeKan ® release
**In short:** **Card Settings**, in Board Settings / Card, becomes two panes of
checklists - the minicard's and the card's - with a checkbox per section and
the board's custom fields as items, and the opened card and the minicard
follow its sections and order; every opened card shows every field its board
allows, empty or not. WeKan moves to **Meteor 3.6-rc.1**, keeping its own
security additions to Meteor's packages. **Data health** in Admin Panel /
Problems can now clean up custom fields whose boards were all deleted, and
empty fields no longer show **Scrum** sections on boards that have Scrum
turned off.
This release adds the following new features:
**Card Settings** - one set of checklists for what cards and minicards show,
on the opened card and in Board Settings / Card alike.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c0ec28e82f">Card Settings on the opened card, its sections as checklists</a>. Thanks to xet7.</summary>
The opened card's "+ Add" is now **Card Settings**, and it no longer opens the
Board Settings / Card popup: the button and everything below it are replaced
by Card Settings itself until the X beside its title puts the card back. Under
the title, **Show** chooses Show on Minicard or Show on Card, and the sections
(Card, Date, Time, ... Comments) are drawn like checklists - the section is
the checklist's title, its rows the items, each checkbox the board-wide Show
on Card or Show on Minicard of that field. A row's hamburger holds On this
card and Show empty; a section's holds Select all / Select none, its card
actions and, for Card, Show empty fields, Ask for a reason and the Draggable
table. Sections and rows drag like checklists and their items; a section's
order is the board's new `cardSettingsSectionOrder`. Sections collapse, kept
per user. Board Settings / Card is still the popup. Design:
[Card Settings](docs/Features/Cards/Card-Settings.md).
`tests/cardSettingsOnCard.test.cjs` and `card-add-fields.e2e.js` pin it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/52de57c844">Board Settings / Card shows the same checklists as the opened card, by the same templates</a>. Thanks to xet7.</summary>
Board Settings / Card had its own layout - a left menu of sections and a pane
of two lists with column headings. It now draws the opened card's Card
Settings itself, with no card: Show on Minicard / Show on Card, the sections
as checklists and the same hamburgers; only On this card is left out, and the
popup's header is the title and X. It is 600px wide instead of 1200px. With
drag handles on, a row or section is dragged only by its handle and the rest
scrolls the page; with them off, by its icon and name. A row dropped on
another section goes back and nothing is saved. Testing both open at once
found three faults, now fixed: their Show radios were one group, Select all
could act on the other one, and switching Show could put a click on the
neighbouring row. Docs:
[Card Settings](docs/Features/Cards/Card-Settings.md).
`tests/cardSettingsOnCard.test.cjs` compares the templates and
`card-add-fields.e2e.js` the two views, row by row, and the drag rules.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5adfc1b9db">A space between a checkbox and its label, on the rows and in the hamburgers</a>. Thanks to xet7.</summary>
A checkbox touched its label on the rows of Card Settings and in their
hamburgers' popups. Both now have a space's width between them. A tick is
drawn rotated and reached past its box, so left to right it is drawn a little
further left: a ticked box keeps the same space and its label does not move.
`card-add-fields.e2e.js` measures it, ticked and unticked.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3e5bc58105">A checkbox per section, every field always shown, Card Settings only in Board Settings</a>. Thanks to xet7.</summary>
The opened card has no Card Settings button any more; Card Settings is Board
Settings / Card. A card shows every field its board shows on cards, empty or
not, so On this card, Show empty and Show empty fields are gone. Each section
has a checkbox before its title that hides the whole section on the chosen
side - the card or the minicard - while its rows keep their ticks. A section
with one row is one line, and a hamburger is drawn only when it holds
something. Custom Fields lists the board's custom fields as items, reordered
by drag and drop, with a + under them that creates one. Card actions stay in
the card's own section hamburgers. See
[Card Settings](docs/Features/Cards/Card-Settings.md).
`tests/cardSettingsSectionShown.test.cjs`,
`tests/cardSettingsCustomFields.test.cjs` and `card-add-fields.e2e.js` pin it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9c9742e09d">Hamburger menus open on top of Card Settings, next to their button</a>. Thanks to xet7.</summary>
A hamburger of Card Settings opened its menu in Card Settings' place, and
Card Settings disappeared until the back arrow. Its menus and forms - a custom
field's edit form, the + under Custom Fields - now open as a layer on top of
Card Settings, next to their button, Card Settings still visible under it.
Their X, their button again, a press elsewhere or Escape closes them. A layer
opens toward the middle of Card Settings, so the + form no longer ran out of
its left side. `tests/popupLayer.test.cjs` and `card-add-fields.e2e.js` pin it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2bd5d1180a">Two panes; the card and the minicard follow Card Settings' order; menus open inline</a>. Thanks to xet7.</summary>
The opened card and the minicard now draw Card Settings' sections in Card
Settings' order, each section's fields in its rows' order, so dragging a
section or a row there moves it on the card or the minicard; the order is per
side, and a default board's card starts with Labels. Every section of the card
has the same heading, Voting and Planning Poker included. Card Settings shows
the minicard's pane and the card's side by side, each folding and scrolling on
its own, with a splitter between them to change their widths, stacked when
narrow; the Show radio is gone and the corner grip resizes the popup. A
hamburger's content opens under its line instead of in a popup, the hamburger
showing it is open, which replaces the layers of the entry above.
`card-settings-sections-order.e2e.js` and the new node tests pin it.
</details>
and updates the following dependencies:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/120f484a30">Meteor 3.6-rc.1, with WeKan's six Meteor package forks rebased onto it</a>. Thanks to xet7.</summary>
[Meteor 3.6-rc.1](https://forums.meteor.com/t/meteor-3-6-rc-1-rspack-2-x-native-typescript-node-js-26-pnpm-monorepos-instrumentation-pwas-and-a-friendlier-cli/64773/18)
carries the security fixes of Meteor 3.5.3 and 3.5.4. `meteor update
--release 3.6-rc.1` first failed with "No version of shell-server satisfies
all constraints": WeKan's forks of allow-deny, ddp-server, dynamic-import,
facebook-oauth, google-oauth and shell-server in `packages/`, made because
3.6-rc.0 lacked the 3.5.3 fixes, were pinned to rc.0's versions. They were not
deleted, because rc.1 still lacks what is WeKan's own in them: the Admin Panel
/ Problems reports of refused attempts, and the crypto-RNG keys of
dynamic-import and meteor shell (rc.1 still uses `Math.random()`). Each fork
is now rc.1's package with WeKan's changes merged onto it three-way, versioned
rc.1's `_1`. The experimental Bun build's Meteor patch applies unchanged to
rc.1 and is renamed for it; the Dockerfile's `METEOR_RELEASE` follows. See
[Meteor 3.5.3 security fixes](docs/Security/Meteor-3.5.3-Security-Fixes-2026-10-22.md).
`tests/meteor353SecurityFixes.test.cjs` and `meteorForkRandomKeys.test.cjs`
pin the forks, and `meteor-353-security.e2e.js` passes on rc.1.
</details>
and fixes the following bugs:
**The opened card** - its sections show only what the board allows.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0a224b3cea">Show empty fields shows no Scrum section the board has turned off</a>. Thanks to jullbo and xet7.</summary>
With Board Settings / Card's "Show empty fields" on (since v12.66), every
opened card showed the "Scrum settings" heading and its sections although
Board Settings / Scrum had none of its fields turned on
([#6830](https://github.com/wekan/wekan/issues/6830)). The Scrum fields are
the card's only sections with no board flag in the template, and the card's
section list asked only whether the card shows the field - which Show empty
fields, Show empty and On this card answer yes. A Scrum section now needs the
board's Scrum visibility first, as Members, Vote and Poker already did; a
turned-on Scrum field is still shown empty.
`tests/scrumSectionsShowEmpty6830.test.cjs` pins the rule and that no other
client code decides a Scrum section by the card alone, and
`card-add-fields.e2e.js` checks the card.
</details>
**Data health** - Admin Panel / Problems' checks of custom fields.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9dc3fc3f31">Custom fields whose boards were all deleted get their own check</a>. Thanks to Raymond-Voogt and xet7.</summary>
"Custom fields listing a board that does not exist" also counted fields whose
boards were ALL deleted, which its Repair skips on purpose, since removing the
dead boards would leave the field on no board. The count could therefore never
go down: one instance showed 510 fields and "Repaired: 0" after every click
([#6829](https://github.com/wekan/wekan/issues/6829)). Those fields now have a
check of their own, "Custom fields whose boards were all deleted", whose
Repair deletes the definitions after the confirming second click. No existing
board shows them; a value a card on a live board still holds is left alone and
stays listed under "Cards with values for a custom field that is not on their
board". The old check counts only fields with a live board left, so its Repair
brings it to zero. `tests/dataHealth6818.test.cjs` pins both checks, the
confirmation, and that live fields and card values are not touched.
</details>
and fixes the following translation:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/81e6988491">Southern Sotho: dikarolo, not masimo, for data fields</a>. Thanks to xet7.</summary>
**Languages updated:** Southern Sotho.
Three Southern Sotho strings called a data field "masimo", which means farm
fields: Show empty fields, the fields of a cron schedule, and the webhook's
optional fields. They now use "dikarolo", the word the file already uses for
card and custom fields. `sesotho-popup-titles.e2e.js` caught the first; the
other two are low confidence.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.67 2026-10-28 WeKan ® release
**In short:** a list's **Add card** form closes once the card is added, unless
**Member Settings** keeps it open for the next one. And the v12.66 release build
is whole again: the **emulated-CPU Linux bundles** and **win32**, **FreeBSD**,
**OpenBSD**, the **Mac app**, the **Sandstorm** .spk, the **Ondra** and **Gantt
GPL** snaps and the **Bun** bundles had failed to build or to start - most
because build scripts had moved their files into `.tools/` and some callers had
not.
This release adds the following new features:
**Add card** - a list's form for new cards.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/70de255333">The form closes after the card is added, unless Member Settings keeps it open</a>. Thanks to brlin-tw and xet7.</summary>
The form stayed open after every card for the next one, which
[#6827](https://github.com/wekan/wekan/issues/6827) found unexpected. It now
closes once the card - or every card of a multi-line paste - is added. Member
Settings / Change Settings has a new switch, **Keep the Add card form open
after adding a card**, off by default, for typing several cards in a row. The
form is closed after the last title rather than at submit, because the paste
loop still reads the form between titles, and a failed add keeps the form and
its text. The two new strings are translated into every language, many with
low confidence (listed in the commit). `tests/keepAddCardFormOpen6827.test.cjs`
and `keep-add-card-form-open-6827.e2e.js` pin both settings, a paste and a
failure.
</details>
and fixes the following bugs:
**Linux bundles for emulated CPUs** - armhf, armv6, armv7, i386, ppc64le,
riscv64 and s390x, and win32 built from the i386 one.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/427d212627">The container that installs their Node.js gets .tools/tmp as its /tmp again</a>. Thanks to xet7.</summary>
`install-node-for-arch.sh` refuses to run without `TMPDIR`, because a
container's own `/tmp` is a small part of its writable layer.
`releases/repack-bundle-for-arch.sh` mounts this checkout's `.tools/tmp` there
and sets it; the release workflow's own `docker run` did not, so every
emulated arch stopped at "TMPDIR must be the mounted .tools/tmp" in v12.66,
and win32, which builds from the i386 bundle's Node.js, had nothing to build
with. The step now mounts `.tools/tmp/repack-<arch>-tmp` as `/tmp`.
</details>
**FreeBSD, OpenBSD and the Mac app** - bundles started by `start-wekan.sh`
where there is no `/proc`.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/290e525b46">start-wekan.sh starts WeKan again where there is no /proc</a>. Thanks to xet7.</summary>
Since the macOS shell-script work, the memory budget read `/proc/meminfo` with
no fallback. `awk` fails where there is no `/proc`, and under `set -eu` an
assignment from a failed substitution ends the script without a word - so in
v12.66 the FreeBSD and OpenBSD bundles and the Mac app never started WeKan,
with an empty log. The read has its `|| true` again and `sysctl` gives the
memory. `tests/macosShellPortability.test.cjs` runs those lines under `set
-eu` without `/proc` and requires a number, with the unguarded line as the
negative case, and fails on any `set -e` script whose `/proc` read in `$(...)`
has no fallback.
</details>
**Sandstorm** - the .spk and the meteor-spk toolchain it is packed with.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f68b130f15">meteor-spk is assembled where the .spk is packed from, in .tools</a>. Thanks to xet7.</summary>
`sandstorm-src/build-deps.sh` still assembled `meteor-spk.deps` in
`~/projects/meteor-spk`, and the workflows put that on PATH, while
`releases/sandstorm-slim-spk.sh` looked in `.tools/projects/meteor-spk`: "no
meteor-spk.deps", and the v12.66 .spk did not pack. `build-deps.sh` now uses
the same `.tools` directory, as do the Sandstorm, Sandstorm store and
meteor-spk workflows. `tests/toolsDirOnly.test.cjs` scans `sandstorm-src/`
too, and fails on a workflow that names `~/projects`.
</details>
**Snaps** - the Ondra and Gantt GPL variants, built from a copy of the tree.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d24598ce25">A copy of the source tree without .git gets its own .tools/tmp</a>. Thanks to xet7.</summary>
`releases/tools-env.sh` made `.tools/tmp` only in a git checkout, so that
scripts mounted alone into a container do not make a `/.tools` there. The
variant snaps build from a copy without `.git`, so no `TMPDIR` was found and
every `fetch.sh` in `snapcraft.yaml` stopped at "TMPDIR: unbound variable" -
which v12.66 reported as the Caddy download failing. A tree with
`package.json` beside `releases/tools-env.sh` counts as a source tree.
`tests/toolsDirOnly.test.cjs` checks such a copy, and `releases/` alone as the
negative case, where no `.tools/` is made and no `TMPDIR` is invented.
</details>
**Bun bundles** - their Linux smoke tests.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1167cbd3b0">The smoke-test MongoDB starts on a 6.19+ kernel, and Chromium is found</a>. Thanks to xet7.</summary>
linux-x64: `mongo:8` refused to start on the runner's newer kernel ("Linux
kernel versions 6.19 and newer has a known incompatibility"); the smoke test
pins `mongo:9.0.4`, which recognises Ubuntu kernels that have the fix
([Supported databases](docs/Databases/Supported.md)). linux-arm64: Chromium
was installed into `~/.cache/ms-playwright` while the build looked in
`.tools/ms-playwright`; the install step now uses the same path.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bce686ce46">Its guard pins mongo:9.0.4, not the mongo:8 that stopped starting</a>. Thanks to xet7.</summary>
`tests/bunSmokeMongo.test.cjs` required the workflow to run `mongo:8`, the
floating tag that refused to start on the runner's kernel. It now requires
`mongo:9.0.4`, and fails if `mongo:8` comes back.
</details>
and updates the following documentation:
- [WeKan vs Trello vs Jira says when WeKan added each feature it listed as missing](https://github.com/wekan/wekan/commit/cbdff47edb).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.66 2026-10-28 WeKan ® release
**In short:** two CRITICAL security fixes: **UploadBleed** - attachment uploads
were not authorized at all - and **StampBleed**, and a **FrameBleed** follow-up.
**Card Settings** opens on Card, with **Show empty fields**, a **Show empty**
column, **Select all** / **Select none** and **drag and drop** lists. A card
opened from its link no longer vanishes behind a **reverse proxy**; minicards
show only filled custom fields, lost cards reappear, card numbers stay unique, a
[label] title prefix is opt-in, Problems gets a nightly **Data health** scan,
**database load** on many-pod installations drops, and the **API's JSON
uploads** and the site logo are stored again.
This release fixes the following CRITICAL SECURITY ISSUES:
**Attachments, checklists and text notes** - what a client may put on a card,
and into which board.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/51111e1958">An attachment upload is authorized: signed in, and allowed to edit the card or write the board</a>. Thanks to xet7.</summary>
[UploadBleed](https://wekan.fi/hall-of-fame/uploadbleed/): Meteor-Files' upload
pipeline stores an attachment with a server-side insert, past the allow rules,
and its only hook cleaned the file name. Anyone, signed in or not, could put a
file on any card of any board - published to its members with an activity and
notifications - and the admin's switch blocking uploads was skipped too. Every
chunk is authorized on the server now, and a card attachment's board, list and
swimlane are its card's. Attempts are recorded in Admin Panel / Problems;
`tests/uploadBleed.test.cjs` drives the decision and fails on any
FilesCollection whose upload is not checked on the server.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/51111e1958">Nothing inserted on one's own card can carry another board's id or checklist</a>. Thanks to xet7.</summary>
[StampBleed](https://wekan.fi/hall-of-fame/stampbleed/): checklists, checklist
items, text notes and attachments are published by the board id they carry,
which the server kept as the client sent it, while the insert was checked on
the card only - so a document on one's own card could be stamped with a private
board's id, and a checklist item drawn in that board's checklist. The board is
taken from the card on insert
([with the #6813 fix](https://github.com/wekan/wekan/commit/4bd6a7f088)),
and a forged insert is refused and recorded in Admin Panel / Problems. Found
reviewing the contributed patch for
[#6813](https://github.com/wekan/wekan/issues/6813), which would have relied on
the stamp. `tests/stampBleed.test.cjs` fails on any card child without an
insert board check.
</details>
**Earlier fixes, regression audit** - every fixed vulnerability checked again,
and the same mistakes found elsewhere closed.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ebcd02be6f">Siblings of six fixed vulnerabilities closed; every earlier fix is still in place</a>. Thanks to xet7.</summary>
Every Hall of Fame vulnerability without a test naming it was checked against
the current source; none had been reverted. The same mistakes were found again
and closed: [FloppyBleed](https://wekan.fi/hall-of-fame/floppybleed/) - an
upgrade step could copy any server file into an attachment by its stored name;
[FilePathBleed](https://wekan.fi/hall-of-fame/filebleed/) - a top-level
`path` let a client point an attachment at any file to delete or read;
[MoveStorageBleed](https://wekan.fi/hall-of-fame/snowbleed/) - anybody
who could see a board could delete its attachments through validation;
[IntegrationBleed](https://wekan.fi/hall-of-fame/integrationbleed/) - the REST
reads gave webhook URLs to every member;
[AttachmentMigrationBleed](https://wekan.fi/hall-of-fame/snowbleed/) - migration
methods answered about any board;
[LegacyAttachBleed](https://wekan.fi/hall-of-fame/legacyattachbleed/) and
[MimeStorageBleed](https://wekan.fi/hall-of-fame/mimebleed/) - more
script types and handler forms are served safely. Attempts are recorded in
Admin Panel / Problems where they can be attributed. 128 of 137 published
vulnerabilities now have a named regression test
(`tests/securityRegressionCoverage.test.cjs`).
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e8a1106782">A 304 Not Modified no longer replaces a stored file's own Content-Security-Policy</a>. Thanks to xet7.</summary>
[FrameBleed](https://wekan.fi/hall-of-fame/framebleed/) follow-up, found by the
browser test of SVG attachments: files are served `private, no-cache`, so every
later view revalidates and gets a 304, and a browser updates the stored
response's headers with the 304's. The app-wide `frame-ancestors 'self'` of the
FrameBleed restore therefore replaced a file's own policy - an SVG's
`default-src 'none'` and `sandbox` - from the second view on. The SVG stayed
sanitized, so a layer of defence was lost rather than script allowed. The
framing headers are left off a 304 now; `tests/frameBleed.test.cjs` pins it and
fails on any other handler that sets a CSP for every response. Not recorded in
Problems: it fires on ordinary revalidation, not on an attempt.
</details>
and adds the following new features:
**Board Settings / Card** - the fields every card of a board shows from the
start.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/80b936223f">Show when empty: the fields every card of the board shows even while they are empty</a>. Thanks to jullbo and xet7.</summary>
[#6806](https://github.com/wekan/wekan/issues/6806): since an opened card shows
only the fields it has data in, a board where nearly every card gets labels, a
due date and a checklist needed + Add, the section and On this card on each new
card before a field could be filled in. A new column of Board Settings / Card -
and of + Add, the same popup - made a ticked field count like data, on every
card. It became Always show in the next entry.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b54525abbe">Always show, for every field: the description, attachments and comments are its default ticks</a>. Thanks to jullbo and xet7.</summary>
The column is **Always show** and covers every per-card field. The description,
attachments and comments were always shown by a fixed list; now they are the
default of the board's `cardFieldsAlwaysShown`, so a board that never set it
shows them as before, and unticked they are shown when the card has a
description, an attachment or a comment, like any other field. On this card
still hides an always-shown field from one card, and Show on Card off still
hides it from all. It is a board admin's setting.
`tests/cardFieldsAlwaysShown6806.test.cjs` pins the default, unticking the
four, the storage and the column, and fails on any caller that does not pass
the board; `card-add-fields.e2e.js` ticks Due date and unticks Comments in the
browser.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1aa9df6075">Card Settings opens on Card, Show empty fields for the board, and the column is Show empty</a>. Thanks to jullbo and xet7.</summary>
Card Settings opens on its topmost entry, Card - also from + Add, which opened
on the first section of fields. Under the Card pane's title, **Show empty
fields** (the board's `showEmptyCardFields`, off by default): ticked, every card
of the board shows all its fields, the empty ones too, and the Show empty
columns are hidden - by CSS only, so their ticks stay saved and come back as
they were. The Always show column is called **Show empty**, stored as before.
`tests/cardFieldsAlwaysShown6806.test.cjs` and `card-add-fields.e2e.js` cover
it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c8a15b514f">Card Settings: Select all and Select none per column, headings aligned with their cells, labels whole</a>. Thanks to xet7.</summary>
Under each column heading of every pane - On this card, Show on Minicard, Show
on Card, Show empty - **Select all** and **Select none** set that column for the
open section's rows at once, for who may change that column; a personal row
(Labels text, List title) is left as it is. The headings drifted left of their
cells and the labels were cut: the heading line had a smaller font, and the
columns are in em. The font is on the headings now, every heading and cell
starts at its column's start, and the popup is up to 1200px wide.
`tests/cardSettingsSelectAllAligned.test.cjs` and `card-add-fields.e2e.js` pin
it, the latter measuring the headings, cells and labels.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/37b3c8f599">Card Settings: Ask for a reason matches Show empty fields, and the select buttons match Draggable's</a>. Thanks to xet7.</summary>
Ask for a reason when moving cards was a Font Awesome square a few pixels off
Show empty fields above it; it is the same animated checkbox now, in the same
place, read reactively from the board. Select all and Select none under the
columns' headings were links; they are the Draggable table's buttons, white
text on the theme's colour. `tests/cardSettingsSelectAllAligned.test.cjs` pins
both, and `card-add-fields.e2e.js` measures the checkboxes and compares the
buttons' colours.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8224852de1">Card Settings: a Show on Card and a Show on Minicard list per section, reordered by drag and drop again</a>. Thanks to xet7.</summary>
The order arrows of 2026-10-25 could follow only one side's order, and their
number was the field's place among all of that side's fields: an arrow often
changed the number while nothing visibly moved. Each section has the two lists
again, each in its own order and reordered by drag and drop, as since
2026-10-02 - Show on Card with On this card and Show empty beside each row, and
Show on Minicard. Up and Down on a focused row move it one row in its list.
`tests/cardSettingsCoverage.test.cjs` runs the drop's placement against the
real layout; `settings-drag-reorder.e2e.js` drags both lists.
</details>
**Add card** - what a typed or pasted title becomes.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f89c8f8030">[Text] at the start of a new card's title becomes a label only when turned on in Member Settings</a>. Thanks to jullbo and xet7.</summary>
[#6810](https://github.com/wekan/wekan/issues/6810): since #3986 a title starting
with [Text] lost the prefix and got the label Text, created when the board had
none - for everybody, without a hint - so titles pasted from ticket systems and
mails ([EXTERNAL] ...) changed and filled boards with labels. It is a Member
Settings switch now, off by default; who wants #3986's behaviour turns it on.
`tests/titleBracketLabel6810.test.cjs` and `title-bracket-label-6810.e2e.js`
cover both states.
</details>
**Admin Panel / Problems** - what in the database points at nothing, and what
it reads far too much of.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d9cf523f9e">Data health: a nightly scan of broken references, with repairs</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6818](https://github.com/wekan/wekan/issues/6818): Problems now lists cards on
a missing or another board's list or swimlane, ghost and duplicate board
members, boards with no active admin, custom field values and boards that no
longer exist, orphan checklist items and dangling subtasks or linked cards. The
scan runs nightly when the CPU is low, claimed so one pod scans at a time. A
repair that moves data runs the same upgrade steps as an upgrade; one that
deletes asks twice and the server refuses it unconfirmed. Repairs use `$pull`
or guarded updates, so a concurrent edit is not lost, and a member dedupe keeps
the admin entry. `tests/dataHealth6818.test.cjs` pins it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/b87858baaf">Database: queries without an index and a high read rate are reported</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6826](https://github.com/wekan/wekan/issues/6826): a 10 GB collection scanned
three times per search and an hourly job reading every card were visible only
in MongoDB's own log. The 5-minute database probe now adds
`db.collection-scan`, the "Slow query" lines where a query read a whole
collection of at least 10,000 documents, grouped by collection and the field
names filtered on, never values; and `db.scan-rate`, over 50,000 documents
read per second between two probes. Without the `getLog` privilege, and on
FerretDB, the log check is skipped. Both rows are in
[Storage Requirements](docs/Databases/MongoDB/Storage-Requirements.md);
`tests/databaseHealthScans.test.cjs` pins them.
</details>
and fixes the following bugs:
**Board Settings / Card** - a field ticked Show on Card but empty, and the
Draggable
table's checkboxes.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/66ee2d12f3">A ticked Draggable checkbox is a checkbox, not a dot</a>. Thanks to mimZD and xet7.</summary>
[#6820](https://github.com/wekan/wekan/issues/6820): the Draggable table
brought its native checkboxes back but kept the 9px 14px padding forms.css
gives every input, so a ticked 16px box left the browser about 2px to draw
in. The WIP limit groups popup and task-list checkboxes in a description had
the same fault. `tests/nativeCheckboxPadding6820.test.cjs` fails on any
stylesheet that brings back a native checkbox with that padding.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ccc67ddf68">Show on Card and an empty field: Show empty fields, Show empty or On this card show it</a>. Thanks to mimZD and xet7.</summary>
[#6817](https://github.com/wekan/wekan/issues/6817): on v12.65 a Due date ticked
in Show on Card was not shown on a card without one. Show on Card says what a
card can show; an opened card shows a field when it has data. An empty one is
shown by Card Settings / Card's Show empty fields, the Show empty column, or +
Add / On this card, all in this release.
`tests/showOnCardEmptyField6817.test.cjs`
pins the reporter's board through each.
</details>
**Admin Panel / Attachments** - the Retention page.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e6f7d226ca">The Google Cloud Storage key help is shown once</a>. Thanks to xet7.</summary>
A leftover copy of the key's description, from before the English and
translated pairs, showed the same sentence twice under the field.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3e998651ea">A storage setting's English description is not shown twice</a>. Thanks to xet7.</summary>
Each storage setting shows its description and menu path in English and in
the user's language, and the English was typed into the template a second
time: in English, and wherever a translation is missing, the same sentence
stood twice. The English comes from en.i18n.json now, and the translation is
shown only when it differs.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2799eb900f">Retention shows readable labels, choice names and the default of an empty variable</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6819](https://github.com/wekan/wekan/issues/6819): the page showed only
variable names, the raw choices, and Default () when a variable was set but
empty, as a Helm chart or docker-compose file does. Fields have translated
labels with the variable name beside them, help lines and choice names, and an
empty variable shows its default. From the reporter's patch;
`tests/attachmentRetentionLabels6819.test.cjs`.
</details>
**Opened card** - a card opened from its link.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/7ed3ca7c5c">A card opened from its link stays open when the connection comes back while the page loads</a>. Thanks to brlin-tw and xet7.</summary>
[#6808](https://github.com/wekan/wekan/issues/6808): behind a reverse proxy the
DDP connection can drop and come back while a card's link loads; a
subscription re-run before the resumed login answered with nothing, the card
left Minimongo for a moment, and the close-on-removal of #3114 closed it and
went to the board's address. The window now waits while the subscription,
connection or login is settling, then asks the server - the 'card'
publication's own rule - and closes only on its no. A card really deleted,
archived or moved still closes. `tests/openCardClose6808.test.cjs` pins it and
fails on any close on Minimongo's word alone; the browser test drops and
restores the connection.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/036ac4f56b">A card deleted elsewhere while open also leaves the card's address</a>. Thanks to brlin-tw and xet7.</summary>
The browser test of [#6808](https://github.com/wekan/wekan/issues/6808) found
the other half: when the card window was already gone - the card deleted in
another tab - the check that asks the server ran inside the window's own
autorun, which was stopped with it, so the address stayed on the deleted card.
The check now runs outside the window and also from its teardown, and
`tests/openCardClose6808.test.cjs` pins both.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8f9103c336">It stays visible after Maximize and Minimize, or Collapse</a>. Thanks to xet7.</summary>
The class that keeps the window unpainted until it is placed beside its
minicard was a static class removed by hand. Blaze writes a tag's static
classes again whenever its class attribute changes, so maximizing and
minimizing, or collapsing, left the card open but invisible. It is a reactive
class now, off for good once placed; `tests/cardDetailsPlacement.test.cjs`
fails on a static one.
</details>
**Minicard** - its custom fields.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a24da2a039">Only the custom fields that have a value, as before</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6811](https://github.com/wekan/wekan/issues/6811): the rewrite of the
minicard's sections dropped the check of 2019, so every field a card carries -
also the ones added empty by Automatically on card / Always on card - was drawn
as an empty row. An empty, blank or unset value is not drawn; 0 and an unticked
checkbox are values. The opened card is unchanged. From the reporter's patch;
`tests/minicardFilledCustomFields6811.test.cjs` and its browser test.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f6fd560817">After the upgrade, Show on card custom fields stay on the minicards</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6812](https://github.com/wekan/wekan/issues/6812): custom fields on minicards
became an opt-in Board Settings / Card switch with false written into every
board, so fields marked Show on card vanished from minicards. A once-ever
upgrade step turns the switch on for each board that has such a field; a board
turned off later stays off. From the reporter's patch, with its three tests in
`tests/schemaUpgradeSteps.test.cjs`.
</details>
**Date popups** - Save without picking a day.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6f5aab875b">Save without picking a day saves today instead of Invalid date</a>. Thanks to brlin-tw and xet7.</summary>
[#6809](https://github.com/wekan/wekan/issues/6809): a date popup of a card
without that date opens the calendar on today, marks it and names it in the
heading, but its date field was empty, so Save answered Invalid date. The form
starts from today now, and Save saves today at the popup's default time. A
card's own date is kept, and an emptied field is still refused.
`tests/datePickerToday6809.test.cjs` and a browser test.
</details>
**Checklists** - a drag on one's own board, and deleting a checklist.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4bd6a7f088">Dragging a checklist item on one's own board is no longer reported as a cross-board attack</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6813](https://github.com/wekan/wekan/issues/6813): a drag sets cardId and
checklistId to where the item already is, and for a comment-only member or a
Worker their own board is one they cannot write to, so ChecklistBleed's canary
named them as attackers in Admin Panel / Problems. The board the item is on is
no destination now - the board of its source card, which the allow rule decides
on, not the item's own boardId as the reporter's patch read it: insert kept a
boardId the client sent, and a forged one would have let an item onto a victim
card. Checklists, checklist items and text notes take boardId from their card
on insert always. A move to another unwritable board is still denied and
reported. `tests/checklistSameBoardCanary6813.test.cjs` runs the real deny
functions, the forged boardId included.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/742996dcce">Deleting a checklist deletes its items</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6816](https://github.com/wekan/wekan/issues/6816): every way to delete a
checklist - the card, the REST API, a rule - removed only the checklist, and its
items stayed behind pointing nowhere (2335 of them in one production database).
The checklist's items are removed with it now, as deleting a card already
removes its children. Items orphaned before are left for the administrator.
From the reporter's patch; `tests/checklistDeleteItems6816.test.cjs` and a
browser test.
</details>
**Cards** - their numbers, and cards moved to another board long ago.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f2908f34e7">Their numbers stay unique: the server checks the number a browser sends</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6815](https://github.com/wekan/wekan/issues/6815): a card added in the browser
brought the number the browser computed from the cards in its view, so with
lazy loading numbers were reissued (842 duplicates in one production database).
The server replaces a number another card on the board has, and raises the
counter to a free one it did not issue - seeding a new counter from the board's
highest number, which the reporter's patch skipped. Existing duplicates are not
renumbered. `tests/cardNumberUnique6815.test.cjs` runs the real hook.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/32031c64c4">Moved to another board before #5874, they are shown again</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6814](https://github.com/wekan/wekan/issues/6814): a card moved before the
\#5874 fix could still point at a list of the board it left, and was shown
nowhere (706 such cards in one production database). An upgrade step gives each
its board's first unarchived list, or a Rescued Data list. It runs before
swimlane-structure, so a card left on the old board's swimlane gets one of its
board in the same pass. From the reporter's patch, with three tests in
`tests/schemaUpgradeSteps.test.cjs`.
</details>
**Add card** - the due date hint.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f852cb7032">The due date hint follows the board's switch while the form is open</a>. Thanks to xet7.</summary>
The hint was recomputed only on typing, so turning Due date from title off in
Board Settings / Card left a hint already shown. `tests/titleDueDate.test.cjs`
pins it.
</details>
**Editors** - the first keys typed into a form.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/69a8bae0c6">Text typed right after a form opens is no longer selected and replaced</a>. Thanks to xet7.</summary>
50 ms after an inlined form opens it focuses the field and selects what it
holds, so the first key replaces a title being edited. Text typed in those
50 ms was selected too, and Enter for a new line left only a line break. The
selection is skipped once the field has changed;
`tests/inlinedFormSingleOpen.test.cjs` pins it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9c0f408a00">Autofocus no longer takes the keyboard from a field being typed in</a>. Thanks to xet7.</summary>
Whenever a re-render added a field marked autofocus, the last one on the
page was focused and its text selected 50ms later - also while somebody was
typing elsewhere. On the import page that moved the attachment token into the
pasted Jira export, replacing it. Only a field just added is focused now, and
never away from a field being typed in.
</details>
**Board visibility** - the Private and Public choices.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/437da1d9c7">The Private and Public descriptions are shown again</a>. Thanks to xet7.</summary>
The helper handed the translation function over unbound, so it threw and
both descriptions were an empty line in every language since custom
descriptions came to Admin Panel / Settings / Visibility.
`tests/visibilityDesc.test.cjs` fails on any unbound one in the tree.
</details>
**Drawing** - the size of a new drawing.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2929b61c85">A new drawing fills the window it is shown in, also once maximized</a>. Thanks to xet7.</summary>
A new drawing was fitted to the popup before it was maximized and kept that
size. It is refitted whenever its room changes, until it is zoomed or resized
by hand.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bb614743fd">Maximized, a drawing takes the window, and opened at the right it is not cut off</a>. Thanks to xet7.</summary>
The refit above was right, but a maximized drawing was still drawn 784px
wide: popup.css kept the older design's 800 x 500 shape and overrode the
drawing's own size. That rule is gone. Opened from the card's menu at the
right of the window, the Drawing popup also grew past the window's edge,
cutting off Save; a movable popup that grows is now kept inside the window.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/58751aa339">The Drawing popup is as wide as its drawing, so nothing in it is cut off</a>. Thanks to xet7.</summary>
The fix above was not the whole story: measured in the rebuilt app, the
popup never grew. Every popup is at most 380px wide, the 820px drawing
overflowed inside it and the popup clipped the larger pen sizes and Save. The
Drawing popup is now up to 860px wide, so the rule that keeps a growing popup
inside the window has a size change to act on.
</details>
**Apple Glass Pastel** - muted text on light glass.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9679aaf636">Muted text is darker, readable on every light surface</a>. Thanks to xet7.</summary>
The theme's quiet text, `#6b7280`, was 4.51:1 on `#f7f7f7` and 4.44:1 on the
tinted card header, at or under the 4.5:1 it needs; `#4b5563` is about 7:1.
The opened card and sidebar contrast tests now wait for the theme before they
measure, as the header test does.
</details>
**Linked cards** - the fields a linked card shows.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1f5a7b8220">A linked card shows the fields and member avatars its source card has</a>. Thanks to xet7.</summary>
Which fields a card shows is decided from its data, and a linked card has no
data of its own: members, labels and dates live on the source card. The choice
counted the linked card's own empty fields, so its members' avatars were hidden
after a reload. The data and counts now come from the source card, while the
Show on Card choices stay those of the board the card is opened on.
`tests/linkedCardShownFields.test.cjs` pins it.
</details>
**Search** - board search and Global Search.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6c2edd6752">Board search highlights one result at a time</a>. Thanks to xet7.</summary>
Enter steps to the next card found, and each step added its highlight without
removing the previous one, so two cards were marked at once. The previous
highlight is now removed first.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/125e1659f8">Cards with attachments are found again, by has:attachment and by file name</a>. Thanks to xet7.</summary>
Global Search looked attachments up by a top-level `cardId`, but an attachment
records its card in `meta.cardId`, so has:attachment and a file name search
found nothing; has:-attachment and has:-checklist matched the same cards as
their positive forms. Both now read `meta.cardId`, skip deleted attachments and
honour the negation. `tests/searchHasAttachmentPredicate.test.cjs` pins it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/dad16609c9">Board search finds the text as typed, not as a regular expression</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6822](https://github.com/wekan/wekan/issues/6822): the sidebar search, the
link-card popup, the list, swimlane and board pickers and the import member
search compiled the typed term as a regular expression, so "C++", "(2026)" or
"[x" threw and showed nothing, and "." matched any character. They match the
text literally now, still ignoring case, like every server-side search; the
Advanced Filter's `/regex/` syntax is unchanged.
`tests/searchLiteralTerm.test.cjs` fails on any client or model file that
compiles a raw term again.
</details>
**Activities** - what a new card records.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5f3b211b37">A new issue and a linked card made over REST record "created" once, not twice</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6823](https://github.com/wekan/wekan/issues/6823): Version Control's New issue
and the REST route that creates a linked card inserted through the collection,
whose hook already records the activity, and then recorded it again, so
watchers got two notifications and two emails. The extra calls are gone, and
the hook names the card's author when it runs outside a method, so the REST
card keeps its author. `tests/noDoubleCreateActivity.test.cjs` fails on any
explicit record after a hooked insert.
</details>
**Database load** - background jobs and searches on a large installation with
many pods.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d481bffdfe">The History audit and the position-history cleanup run once per installation</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6821](https://github.com/wekan/wekan/issues/6821): both were per-process
timers, so 24 pods read the whole History 24 times an hour and asked one query
per board membership - 148,660 - 24 times a day to trim nothing. Both are
SyncedCron jobs now, one pod per scheduled time, waiting for a quiet CPU. The
cleanup visits only the user/board pairs over 1000 entries and keeps the same
newest 1000. `tests/positionHistoryCleanup.test.cjs` checks it against the old
result.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2be842351b">Search sessions are indexed, and everybody's stale sessions are removed</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6824](https://github.com/wekan/wekan/issues/6824): every global search and
card report scanned the whole `sessiondata` collection three times, and only
the searching user's own day-old sessions were ever removed - one instance held
4,751 sessions, 10 GB. It has indexes on `{ userId, sessionId }` and
`modifiedAt` now, and sessions older than a day are removed for everybody.
`tests/searchSessionIndexes.test.cjs` pins it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e9440d503e">The hourly recurring-card job no longer reads every card</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6825](https://github.com/wekan/wekan/issues/6825): with no index on
`recurrenceInterval`, the job was a full scan - 1.1 million cards, 1.1 s every
hour on one instance. A partial index holds only the cards that recur.
`tests/recurrenceIndex.test.cjs` pins it.
</details>
**Attachments** - files stored by the API, the site logo, and a validated move.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e7505a4fef">The API's JSON uploads and copies, and the site logo, are stored again</a>. Thanks to xet7.</summary>
The JSON branches of REST upload, upload-background and copy, their
Meteor-method twins and Admin Panel's site logo upload called
`Attachments.insertAsync`, which Meteor-Files has only on the client, so each
failed with "is not a function". They use `writeAsync` now, whose after-upload
hook also validates the file and moves it to the default or copy storage.
`tests/attachmentApiServerWrite.test.cjs` fails on any server-side
`insertAsync` of a Meteor-Files collection.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d3e80af1ac">Validate and move to storage moves the file again</a>. Thanks to xet7.</summary>
The method moved the file by calling `moveAttachmentToStorage` from
`Meteor.defer`, where there is no user, so that method's login check refused
every move. All its checks are made before the defer, so the deferred step moves
the file itself, as the upload path does. `tests/moveStorageBleed.test.cjs`
fails on any deferred or timed callback that calls a Meteor method.
</details>
**REST API** - where a new card goes.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c355926767">A new card's list and swimlane must belong to the board in the URL</a>. Thanks to xet7.</summary>
POST cards and POST cards/bulk wrote another board's list or swimlane onto a
new card, which then showed on neither board - the #6572 shape the PUT route
already refuses. Both answer 400 naming the field now, the bulk route per entry.
`tests/restCardPlacement.test.cjs` fails on any REST handler that inserts a
card without the check.
</details>
**Lists** - who sets a WIP limit.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a40c4495d4">A WIP limit is set by a board admin, on every path</a>. Thanks to xet7.</summary>
The WIP limit methods required a board admin, but a member with write access
could still set `wipLimit` with a plain list update or REST PUT. A deny rule and
a REST admin check apply the methods' rule everywhere; title, color and starred
stay with write access. `tests/wipLimitAdminOnly.test.cjs` runs the real deny
rule.
</details>
**Admin Panel / Problems** - AuthBleed attempts, and Data health's wording.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/314c153c9b">AuthBleed attempts are recorded</a>. Thanks to xet7.</summary>
[AuthBleed](https://wekan.fi/hall-of-fame/authbleed/): `copyBoard`, `copyCard`,
`copySwimlane` and `createCardWithDueDate` refused without a trace. A
signed-out caller of any of them, and any refusal of `createCardWithDueDate`,
which no WeKan client calls, is recorded as `authz.copy` now. Membership
refusals of the copy dialogs are not: a template or a stale dialog reaches them
in ordinary use, and a high-severity record would disable that login.
`tests/authBleedCopyMethods.test.cjs` pins both.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/77956c98ed">Data health's "Never" has a key of its own</a>. Thanks to xet7.</summary>
The status line reused List Sync's "never synced" key. `data-health-never-run`
is filled in every locale from that locale's existing translation of the same
English word, and a Transifex translation still replaces it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/35ba67ee69">File status lists every kind of finding, not 500 rows of one</a>. Thanks to xet7.</summary>
The report lists 500 findings and counts the rest, first come first served:
14358 integrity baseline move candidates took every row, and the wrong file
extension a type check is run to find was only a number in the totals. A
kind with fewer rows listed now takes the place of one from the kind with the
most. `tests/fileStatusAudit.test.cjs` pins it.
</details>
and has the following developer-facing changes:
**Tests** - guards and specs that read the wrong thing.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0c40503a2b">Three Firefox specs wait for what they test, and drag inside the window</a>. Thanks to xet7.</summary>
The search phrases spec searched before the browser had the setting it had
just written, the Card Settings column spec counted a pane that had not
rendered yet, and the drawing spec dragged a corner handle past the window's
edge, which Firefox does not deliver.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/21b2da9ad2">The title heading colour guard reads the heading block itself</a>. Thanks to xet7.</summary>
`tests/boardViewTitleViewer.test.cjs` read from the last comment of
`minicard.css` and `cardDetails.css` to the end of the file, so it passed only
while the rules after the heading sizes set no colour, and failed once the Card
kind check mark of v12.65 was appended with its own. It now reads the heading
block and still fails on a colour there.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2dfdf77c2b">The Card Settings drag and drop spec turns drag handles on for its user</a>. Thanks to xet7.</summary>
The fixture's user has an `id`, not an `_id`, so `setHandles(user._id, ...)`
matched no user and the handle half of `settings-drag-reorder.e2e.js` never had
drag handles on.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8788458a87">The browser specs' boards show every field, as the specs were written for</a>. Thanks to xet7.</summary>
With Show empty fields off by default, a new board hides a card's empty
fields, and the specs that open a card to add a checklist or a date found no
button to click. The seeded board turns Show empty fields on; the Card Settings
spec, which tests the switch itself, turns it off again.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/00cf8f0958">The shared-template language specs read the description where it is</a>. Thanks to xet7.</summary>
The Corsican and Southern Sotho specs looked for the description and the hint
inside the scope checkbox list; both are its siblings in `.templates-settings`.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6241be9053">The specs add a first checklist with the title line's +, where #6796 put it</a>. Thanks to xet7.</summary>
The bottom add checklist link appears only once a card has a checklist, so
every spec adding a first checklist failed, and 12-checklists skipped its
checks silently.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a4197f228f">The Card Settings specs open the section a row is in, and pick the list by its side</a>. Thanks to xet7.</summary>
The popup draws only the open section's rows, each once per list.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/978af29b33">The language specs read the option values, tabs, addresses and texts the app has</a>. Thanks to xet7.</summary>
Corsican, Haitian, Scottish Gaelic, Latin, Malagasy and Irish specs; a plain
member is not shown the Rules editor at all, and the server still refuses
their rule writes.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a8f65adff2">Specs follow deliberate changes, and their fake SMTP server waits for its port</a>. Thanks to xet7.</summary>
LinkedWriteBleed over REST is refused and recorded without disabling the
account (#6798); notification intents are seeded recent, past the expiry of
\#6768; an invite mail goes to a running SMTP sink, and a sink waits for a busy
port.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/13d3a74b3f">The S3 settings spec expects both of its translated Save buttons</a>. Thanks to xet7.</summary>
The S3 pane has had two Save buttons since \#6465, one under the Enabled and
Read switches and one after the connection fields; the spec matched one.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4a656baea3">The Scrum, Sesotho and drag reorder specs read one list per side, and open the SMTP fields first</a>. Thanks to xet7.</summary>
Card Settings draws each row once per list and leaves the minicard list head
empty; the Date section starts with the fixed Mark as complete row; the SMTP
host field is shown only while email settings are on and SMTP is chosen.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/02bb0814c3">linkedWritePolicy and webhookBleed read the code as it is now</a>. Thanks to xet7.</summary>
One sliced up to a line the double-activity fix removed and so read the rest of
the file; the other pinned copyBoard's signed-out refusal as one line, which now
records an AuthBleed attempt first. Both keep their assertions.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d8d7b01334">The Card Settings both-sides and Scrum title specs follow the current Card Settings</a>. Thanks to xet7.</summary>
Six rows have no minicard side by design, which one spec read as missing; the
other read the pane title before Card Settings, which opens on Card now, had
switched to Scrum. Both failed in the Chromium stage of a full run.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/cbf9d2f843">The UploadBleed and StampBleed specs do the legitimate case first, and check the block</a>. Thanks to xet7.</summary>
The attempt came first, and a signed-in account that attempts a
vulnerability is blocked and signed out - so the legitimate upload and
checklist that followed ran signed out and were refused. Each spec now does
the legitimate case first and checks that the attempt blocked the account.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/3f72fcd7f9">The Move and Copy Card helpers wait for the destination picker before reading it</a>. Thanks to xet7.</summary>
Asked while the popup was still rendering, the helper saw no boards select
and moved the card within its own board; #6793's Move Card spec then waited
for a board change that never came.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9549388dd0">The smaller font preset is measured on the Card Settings heading text</a>. Thanks to xet7.</summary>
The headings' smaller font was moved from their line to their text, so the
em-sized columns stay aligned with their cells; the spec still measured the
line.
</details>
**.tools/ and the editor sandboxes** - every file the scripts write stays in the
checkout, and VSCodium can be confined to it on Linux, macOS and Windows.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/aeff4900df">Every file the scripts write stays in .tools/: temporary files, downloads, Meteor, Node.js, npm</a>. Thanks to xet7.</summary>
They wrote to `/tmp`, `/var/folders`, `~/Downloads` or `~/Lataukset`,
`~/.meteor`, `~/.nvm`, `~/.npm`, `/usr/local` and into `~/.zshrc`. Now
`releases/tools-env.sh` keeps temporary files, downloads, release zips,
Meteor, Node.js, the npm cache and Go in `.tools/`, and puts those tools on
`PATH` without touching a shell rc file. Every `mktemp` names `.tools/tmp`,
because macOS's ignores `TMPDIR` otherwise. Containers get `.tools/tmp` as
their `/tmp`, verified with MariaDB's temporary tables.
`tests/toolsDirOnly.test.cjs` keeps it so.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e79ec61bcc">build.bat and the releases\ batch files keep every file in .tools\ too</a>. Thanks to xet7.</summary>
`releases\tools-env.bat` does on Windows what `tools-env.sh` does, and
`build.bat` unpacks the Dockerfile's Node.js from nodejs.org's zip into
`.tools`, verified against `SHASUMS256.txt`, instead of a system-wide
Chocolatey install. Meteor goes into `.tools\.meteor`.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/42ed50413f">macOS: VSCodium under sandbox-exec, reaching only ~/Documents/repos/wekan</a>. Thanks to xet7.</summary>
The macOS counterpart of the Linux Flatpak sandbox:
`docs/Security/Sandboxes/macOS/sandbox.sh` refuses the home directory,
`~/.ssh`, other users and other disks, and opens only the checkout.
VSCodium's settings, `HOME` and temporary files are kept in `.tools`.
Verified on macOS 27 with VSCodium running inside. The README explains the
design, including why Chromium's own sandbox is off.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6ac70c4cdd">Windows: VSCodium in Windows Sandbox, seeing only Downloads\repos\wekan</a>. Thanks to xet7.</summary>
`docs/Security/Sandboxes/Windows/sandbox.bat` starts a disposable Hyper-V
Windows Sandbox in which the checkout is the only mapped folder. VSCodium
and PortableGit are downloaded once into `.tools`, each verified before it
is unpacked. Not run on Windows yet; the README says what to check.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/8af1e8aec8">mirror.sh uses the WeKan checkout as the source, not a second clone</a>. Thanks to xet7.</summary>
Syncing wekan/wekan cloned all of GitHub's 500 MB into
`.tools/wekan-github`, although that is this checkout. The source's
branches and tags are fetched into it under a private namespace instead,
which took 1.7 seconds, and deleted after the push. Only the destination
mirrors are cloned.
</details>
**Helm chart** - the chart's settings reference and its memory.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9994e82bae">values.yaml's settings reference is generated from docker-compose.yml</a>. Thanks to xet7.</summary>
Copied by hand once, the chart's commented reference of every WeKan setting
had fallen 74 settings behind. `releases/charts-values-reference.mjs`
regenerates it, `release-charts.sh` runs it on every chart release, and a
comment after a value no longer becomes part of it. In wekan/charts the
WeKan pod now requests 512 MiB and is limited to 2 GiB, as #6606 had asked:
the old 1 GiB gave a heap WeKan crash-looped in.
</details>
**macOS** - the scripts and tests, run on a Mac with bash 3.2 and BSD tools.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/96ad680cd4">The release scripts find the checkout on every system</a>. Thanks to xet7.</summary>
They hard-coded Linux's `~/repos/wekan`, so on a Mac, where the checkout is
`~/Documents/repos/wekan`, they changed into nothing or a stale copy. Each finds
the checkout from its own location now, and the download folder is `Lataukset`
only where it exists. `tests/repoLocationPerOs.test.cjs` pins the three places.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9927adcb44">Companion repositories are read from .tools/ only, never from ../w/</a>. Thanks to xet7.</summary>
The Hall of Fame tests, the Helm chart scripts and the Docker cleanup still fell
back to an old `../w/` beside the repository; a stale `../w/wekan.fi` failed a
test with markup the site no longer has. `tests/noLegacySiblingRepos.test.cjs`
keeps `../w/` from coming back.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/283e51eac0">The root and releases/ shell scripts run on macOS too</a>. Thanks to xet7.</summary>
Every script was checked with macOS's bash 3.2 and for GNU-only options: an
associative array, an empty array under `set -u`, a broken `find-replace.sh`,
`sha256sum`, `nproc`, GNU `sed -i`, `ln -sfT`, `stat -c` and `ip` each have a
portable form or a fallback. `tests/macosShellPortability.test.cjs` parses every
bash script with bash 3.2 and fails when one of these shapes comes back.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/fa1bb293f9">The UCS migration hook locks without flock, and the Nextcloud package is packed without GNU tar</a>. Thanks to xet7.</summary>
Without `flock` the UCS hook takes the same lock with perl's `flock()` on its
file descriptor. `releases/nextcloud-pack.sh` packs the ExApp under its app id
with options GNU tar and bsdtar share, and keeps macOS's `._*` files out;
the release workflow and its test use it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1f9ada5599">No /proc: conformance runs keep each other's containers, and the start scripts see the real memory</a>. Thanks to xet7.</summary>
On a Mac `db-conformance.sh` saw every live run as dead and removed its
containers; it reads the command line with `ps` there. `start-wekan.sh` and the
bundle's start script assumed 2 GiB and now ask `sysctl`, and the speed monitor
shows the load average.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/43e09631c4">A Node.js without single executable applications is named, not run</a>. Thanks to xet7.</summary>
Homebrew's Node.js is built without them, and the single-file build stopped with
a bare exit 9. It says what to do now, and its tests skip that build and expect
the host's own arch rather than x64.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0ed8fb20f2">strip-node.sh is exercised on a real Mach-O</a>. Thanks to xet7.</summary>
On a Mac the ELF fixture is a Mach-O, so those cases skip, and a new case checks
that the local symbols go, the exported one stays and the binary is re-signed
and runs.
</details>
and updates the following documentation:
- [Show empty fields of Card Settings, the \[label\] title switch of Member Settings, and Data health repairs](https://github.com/wekan/wekan/commit/30e960e129).
Thanks to xet7.
and updates the following translations:
**Languages updated:** 232 languages.
**Board Settings / Card** - the Show empty and Show empty fields headings.
- [Show empty and Show empty fields in every language](https://github.com/wekan/wekan/commit/1aa9df6075).
Thanks to xet7.
**Member Settings** - the [label] switch of Add card.
- [Turn \[label\] at the start of a new card title into a label, in every language](https://github.com/wekan/wekan/commit/f89c8f8030).
Thanks to xet7.
- [Tigre and Tsonga wording of the switch](https://github.com/wekan/wekan/commit/595e26cfa8).
Thanks to xet7.
**Admin Panel / Attachments** - the Retention help line.
- [Tigre card noun in the Retention help line](https://github.com/wekan/wekan/commit/c7145402af).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.65 2026-10-25 WeKan ® release
**In short:** an **opened card** shows only the fields it has, with **+ Add**
for the rest. **Drawing** works with a pen on the whole window - from Add Card
and Add attachment, saved as SVG, with an eraser, palm rejection and a lasso
that moves, resizes, groups and layers lines - and **SVG** is an image again,
sanitized whole at any size, streaming, on upload and on every serve.
**FerretDB** answers WeKan's queries in the database on every backend, and
**SQLite** stores documents as JSONB. **Right-to-left** checkboxes and list
resizing are fixed.
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a90576bafa">The speed test report escapes a backslash before the pipe in a table cell</a>. Thanks to GitHub CodeQL and xet7.</summary>
CodeQL alerts #593, #594 and #595 (`js/incomplete-sanitization`):
`docs/Databases/FerretDB/1/SpeedTest/report.cjs` escaped `|` as `\|` in
Markdown table cells but not a backslash, so a value ending in one turned the
added backslash into a literal one and its pipe split the cell. One `mdCell()`
escapes the backslash first, then the pipe, and a line break becomes a space;
the database's name and the query's title now go through it too.
`tests/speedTestReportCells.test.cjs` checks that no value can add a column to
its row, and that nothing in the tree escapes a pipe without the backslash. The
report is built from local results, so there is no attempt to record in
Admin Panel / Problems.
</details>
and adds the following new features:
**Opened card** - only the fields a card has, and "+ Add" for the rest.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6fe0a7ef94">An opened card shows only the fields it has data in, and + Add under the cover adds the rest</a>. Thanks to xet7.</summary>
An opened card shows the fields it has data in, plus its description,
attachments and comments - empty or not - and its cover when it has one.
"+ Add", under the cover, opens the Admin Panel's own left menu and pane: the
sections (Date, Members, Scrum settings, ...) on the left, their fields from
Board Settings / Card on the right, each with a checkbox that shows or hides
the field on this card. Board Settings / Card still decides what a card can
show. The design is
[Card fields](https://github.com/wekan/wekan/blob/main/docs/Features/Cards/Card-Fields.md);
`tests/cardShownFields.test.cjs` and
`tests/playwright/specs/card-add-fields.e2e.js` cover it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c4f528159b">Cover on by default, a new drawing is the cover, no empty Similar cards or Custom Fields, and a better + Add popup</a>. Thanks to xet7.</summary>
The opened card's cover image is on by default; a once-ever upgrade step turns
it on for boards created while it was opt-in, and a board turned off later
stays off. A new drawing becomes the card's cover. There is one separator line
under "+ Add": the Sort group drew its rule for fields the card did not show.
Similar cards and Custom Fields are shown only when there is something in
them. The "+ Add" popup moves by its title bar (the drag handle of every
movable popup is shown only while drag handles are on), maximizes, is wide
and high enough for its labels and menu, and only a clicked checkbox animates.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/5d80b07bed">A maximized card has its comments in the right column</a>. Thanks to xet7.</summary>
The comments section is its own template, in the left column of the opened
card and, when the card is maximized, at the top of the right one beside the
fields.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/20e2273c07">Board Settings / Card and + Add are one popup, with sections in a left menu</a>. Thanks to xet7.</summary>
Board Settings / Card is the Admin Panel's left menu and pane, and the opened
card's "+ Add" opens the same popup for that card. Every row is in one section;
each line has, in reading order, On this card (from a card), Show on Minicard,
Show on Card and the field, each side with its checkbox, its position and up
and down arrows. A row's name leaves out its section's ("Sprint", not "Scrum
settings: Sprint"). "On this card" is new in every language.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4f20f24201">Five card actions are in the hamburger of the section they act on</a>. Thanks to xet7.</summary>
They left the card's actions menu: on the opened card each is in the hamburger
of the section it acts on - the Scrum fields the card shows are one Scrum
settings section with Card kind in its hamburger - and in Card Settings they
are at the top of that section's pane. `tests/cardSectionActions.test.cjs`
fails for a hamburger whose items are not in Card Settings.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/65621f44cc">The card actions menu in a new order, as one list</a>. Thanks to xet7.</summary>
Move Card, Copy Card, Save as Template, Copy Template to Many Cards, Card
recurrence, Move to Top, Move to Bottom, Watch, Star; Set color, Drawing; Copy
link to clipboard, Link to board, QR code, History, Export, Import, Move Card to
Archive, More. Each item keeps the permission it had.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c69f3fe25a">Card Settings: Card first, Subtasks and Planning Poker sections, settings in their pane</a>. Thanks to xet7.</summary>
The first entry is Card, with the move-reason question, Draggable and the
card's own rows. Subtask settings, Edit custom fields, Change spent time, Card
kind, Edit voting and Edit Planning Poker are forms at the top of their pane
instead of popups; saved there, Card Settings stays open.
</details>
- [Card Settings: the pane's columns line up, and an inline form's buttons no longer push the rows aside](https://github.com/wekan/wekan/commit/ede0d68b27).
Thanks to xet7.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f33987931f">Card Settings / Custom Fields: Create Field and Link fields are forms in the pane</a>. Thanks to xet7.</summary>
Under Edit custom fields, the pane has the two forms its buttons opened: Create
Field (also from Board Settings, for a board admin) and Link fields to another
card. A field saved there empties the form for the next one and Card Settings
stays open; opened as their own popup they step back when saved, as before.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/ca53019d96">Card kind shows which kind is selected</a>. Thanks to xet7.</summary>
In Card Settings / Scrum settings and in the Card kind popup the selected kind
has a check mark at the end of its line and is bold, and the list is a
radiogroup with `aria-checked`. Before, it only had an `is-active` class that
nothing styled. `tests/cardKinds.test.cjs` pins the mark on each kind.
</details>
**Drawing** - handwriting and sketching on a card, with a pen too
([#6802](https://github.com/wekan/wekan/issues/6802)).
<details>
<summary><a href="https://github.com/wekan/wekan/commit/54726264d3">A new drawing from Add Card and Add attachment, on the whole window, saved as SVG</a>. Thanks to mimZD and xet7.</summary>
Add attachment / Drawing, and Add Card's new Drawing button - shown while the
composer adds one card: the card is added, opened and drawn on - open a new
drawing on the whole window, saved as an SVG attachment of the card, its lines
kept for editing. The eraser takes away the lines it touches, and palm rejection
keeps a hand resting on a tablet from drawing once a pen has. A lasso tool is
not part of it. `tests/cardDrawingPen.test.cjs` and
`tests/playwright/specs/card-drawing-pen.e2e.js`; described in
[Drawing.md](docs/Features/Cards/Drawing.md).
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c11976a2e1">The Drawing popup moves by its title bar or a drag handle, and maximizes</a>. Thanks to mimZD and xet7.</summary>
Like the opened card: dragged by its title bar or the handle beside it, with the
card's maximize / minimize button. Minimizing puts it back where it was, a
maximized popup stays put, the drawing keeps its shape so the pen lands under
the
pointer, and a popup left maximized opens maximized again. Drag to move,
Maximize
and Minimize are in every language.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/4bea319239">Board Settings / Card calls the Whiteboard row Drawing</a>. Thanks to mimZD and xet7.</summary>
The row that shows or hides the Drawing popup's Open as whiteboard button is
named after the card menu's Drawing, with its existing translations.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/1c26e1bcd0">A lasso selects lines to move, resize, group and ungroup, and to change their layer</a>. Thanks to mimZD and xet7.</summary>
Issue #6802 suggested a lasso for selecting and moving things. A loop drawn with
it
selects every line wholly inside, and a grouped line brings its whole group.
The selection moves from inside its box, stopping at the edge; resizes from
its corner handle, line widths to the nearest allowed one; groups and
ungroups, the group saved with the drawing; and goes to the front, the back,
or one layer up or down. Undo now steps back through every change.
`tests/cardDrawingLasso.test.cjs` and
`tests/playwright/specs/card-drawing-lasso.e2e.js` cover each step, with
every locale translated.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d5d027a6f2">The whole window to draw on, growing from its corner, zoom, any color, and Save above the drawing</a>. Thanks to xet7.</summary>
The drawing area takes all the room the popup has instead of a fixed 800 x 500:
a handle at its corner makes it larger, or smaller without cutting a line, and
the magnifier zooms it. Clicking a color opens a color wheel, a brightness
slider and the HTML color code; custom colors are added to the palette, and
the eyedropper picks a color from the screen (or the drawing). Save is above
the drawing, and Add Card's Drawing is in Search / Template / Drawing / More
options. `tests/cardDrawingCanvasColors.test.cjs` and
`tests/playwright/specs/card-drawing-canvas.e2e.js` cover it.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/91c12ebcce">Custom colors are kept with the account when signed in, in the browser otherwise</a>. Thanks to xet7.</summary>
The custom colors added to the palette are stored in the user's profile
(`profile.drawingColors`, the `setDrawingColors` method), so they follow the
user to every browser. Only `#rrggbb` codes are stored, each once, at most 16,
and nobody who is not signed in can store any. Drawing on a public board
without signing in keeps them in that browser.
</details>
**SVG images** - an image like any other again, made safe where it is served.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/0afb1336db">SVG is uploaded and shown as an image: an attachment, a cover, a board background, an avatar</a>. Thanks to xet7.</summary>
SVG uploads were refused, and every SVG served as a download, after
JVN#74210258. Every uploaded SVG was already sanitized before it is stored - no
script, event handler, `javascript:` URI, `foreignObject` or XML loop - so they
are allowed again. Every route that serves a file now sanitizes an SVG whole
again before sending it, under a Content-Security-Policy with no script and
`sandbox`, so it runs nothing even opened directly. Removed active markup is
logged to Admin Panel / Problems.
`tests/svgServing.test.cjs` also fails when a serving route skips this, and
`tests/playwright/specs/svg-attachment.e2e.js` uploads one and uses it as a
cover and a board background.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e748559838">An SVG of any size is sanitized whole, streaming, on upload, on validation and when served</a>. Thanks to xet7.</summary>
The sanitizer was a set of regular expressions over the whole file: it read
the file into memory, missed an entity-encoded `javascript:`, saw nothing
in UTF-16, and the serve path sent anything over 8 MB as an unsanitized
download while validation rejected every SVG over its 1 MB scan. A streaming
tokenizer, `models/lib/svgSanitizer.js`, replaces it everywhere: script,
foreignObject, iframe/object/embed, SMIL set/animate, event handlers,
script URIs after decoding references, dangerous CSS, DOCTYPE/ENTITY and
processing instructions are removed wherever they are, however the file is
split into chunks. `tests/svgSanitizerStreaming.test.cjs` splits a hostile SVG
at every byte offset, and sanitizes a 132 MB file with payloads at the start,
middle and end in about 3 s with 38 MB of memory.
</details>
and has the following database speed improvements:
**FerretDB** - WeKan's queries answered in the database instead of in Go, on
every database FerretDB supports; measured with the speed test in
[docs/Databases/FerretDB/1/SpeedTest](docs/Databases/FerretDB/1/SpeedTest).
<details>
<summary><a href="https://github.com/wekan/FerretDB/commit/ea7951d7">SQLite stores documents as SQLite JSONB, and an existing database is converted once</a>. Thanks to xet7.</summary>
Every filter, sort key, index and multikey trigger parsed a document's JSON text
again. Documents are now kept in SQLite's binary JSONB and turned back into the
same text in Go ([the reader](https://github.com/wekan/FerretDB/commit/6e725c9b)). On 61,507 cards due cards went
from 1,003 to 519 ms, All Boards from 95 to 42 ms, search from 12.6 to 5.2 ms
and creating the 44 indexes from 107 to 70 s; nothing got slower.
**At the first start with this FerretDB, an existing SQLite database is
converted**, table by table, each in one transaction: 204,212 documents took
81 s, and the file grows until SQLite reuses the freed pages. A table whose
conversion fails stays text and keeps working. **After it, an older WeKan or
FerretDB cannot read the database**, so take a backup before upgrading if a
downgrade may be needed.
</details>
<details>
<summary><a href="https://github.com/wekan/FerretDB/commit/5be94bb7">PostgreSQL answers $and, $or and $elemMatch, and membership tests through a GIN index</a>. Thanks to xet7.</summary>
[An array's GIN index](https://github.com/wekan/FerretDB/commit/9c06c788) answers `members.userId`, [a top-level
`$and`](https://github.com/wekan/FerretDB/commit/555273c1) no longer reads the whole table, and an `$or` and an
`$elemMatch` are conditions in the database. All Boards count went from 8-9 s to
11 ms for a typical user on twice the production data, and due cards from 2,031
to 815 ms on 61,507 cards.
</details>
<details>
<summary><a href="https://github.com/wekan/FerretDB/commit/dbc7d65d">MySQL and MariaDB use their indexes, and adding them takes minutes instead of tens of minutes</a>. Thanks to xet7.</summary>
A card by `_id` went from 3.4 s to 1.8 ms on MySQL, and adding the 44 indexes
from
20 minutes to 3. [Multikey side tables](https://github.com/wekan/FerretDB/commit/a2caced7) answer membership tests,
[an `$or` of `_id` sets is one `UNION ALL`](https://github.com/wekan/FerretDB/commit/c7b0a1c8) - All Boards with a
starred board 900 ms to 6 ms on MySQL and 5.7 s to 7 ms on MariaDB - and
[`$and`, `$or`, `$elemMatch` and a long `$in`](https://github.com/wekan/FerretDB/commit/df43d76d) are answered in the
database: due cards 1,957 to 750 ms on MySQL. MariaDB, whose JSON is text,
keeps what an index answers.
</details>
<details>
<summary><a href="https://github.com/wekan/FerretDB/commit/19311038">SQLite sorts in the database</a>. Thanks to xet7.</summary>
A sorted query no longer reads and keeps every match in Go to return a few: the
ten newest of 30,000 users allocate 2.6 MB instead of 264 MB, and the first
batch of an unlimited sort comes 30% sooner ([each key read once](https://github.com/wekan/FerretDB/commit/34b02dbe)).
</details>
and updates the following dependencies:
- **fast-uri 3.1.8 → 3.1.9** and **undici 7.30.0 → 7.30.1** — [the npm
group](https://github.com/wekan/wekan/pull/6804).
- **actions/setup-python 7.0.0 → 7.1.0** — [in
bun.yml](https://github.com/wekan/wekan/pull/6803).
Thanks to dependabot.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/31332bfed4">@azure/msal-node 6.0.1 → 7.1.1 by an npm override, for GHSA-xcx6-jx8p-x3h5</a>. Thanks to xet7.</summary>
`npm audit` reported two high severity issues: msal-node below 7.1.0 may
disclose authorization code flow parameters to an attacker-controlled endpoint
([GHSA-xcx6-jx8p-x3h5](https://github.com/advisories/GHSA-xcx6-jx8p-x3h5)). It
comes through the Azure Blob storage adapter's `@azure/identity`, whose newest
release still asks for msal-node 6, so `npm audit fix --force` changed nothing.
`package.json` overrides it; 7's only breaking change is one `@azure/identity`
does not use, and a token request still reaches Microsoft Entra. WeKan's Azure
Blob storage signs in with `DefaultAzureCredential`, not the authorization code
flow. `npm audit` finds 0 vulnerabilities, and `tests/msalNodeAdvisory.test.cjs`
fails if a vulnerable copy is locked again.
</details>
and fixes the following bugs:
**Right-to-left languages** - checkboxes and list resizing work the way they do
left-to-right.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6bb926af19">A checked checkbox sits on its own square, not beside it</a>. Thanks to mimZD and xet7.</summary>
[#6786](https://github.com/wekan/wekan/issues/6786): after its first fix the tick
was a check mark right-to-left, but it was still placed with
`inset-inline-start`/`margin-inline-end`, which a right-to-left page flips: the
narrowed, rotated box started at the square's right edge and was pushed further
right, outside its box. It is placed with `left`/`margin-right` now, like its
borders, so it lands on the square in the same place as left-to-right and the
label does not move; the import page keeps its label gap on the reading side.
`tests/rtlCheckMark.test.cjs` fails on any tick placed logically, and the
Playwright spec measures the tick on its square in both directions.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/80208a7141">Resizing a list follows the pointer instead of moving the other way</a>. Thanks to mimZD and xet7.</summary>
[#6801](https://github.com/wekan/wekan/issues/6801): a list's resize handle is on
its inline end, which is its left edge right-to-left, but the new width was the
start width plus the pointer's movement - so dragging the handle out of the list
narrowed it and dragging it in widened it. The movement is now counted with the
list's own direction, as the board sidebar and the left menus already did.
`tests/rtlListResize.test.cjs` pins it and fails when any width resizer does not
flip right-to-left; a Playwright spec drags the handle out and back in, in both
directions.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/37024aa9f9">The comment form's avatar is inside the card</a>. Thanks to mimZD and xet7.</summary>
[#6805](https://github.com/wekan/wekan/issues/6805): the comment form kept 38px
free for the writer's avatar with a physical left margin, but drew the avatar at
the reading start, so right-to-left it was outside the card. The room is
`margin-inline` now, and the other margins of the comments are logical too.
`tests/commentFormRtl6805.test.cjs` fails on any left- or right-only margin or
padding in `comments.css`.
</details>
**Notifications** - what the server log says when one is not delivered.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/bf7520f972">A notification that is not delivered says which check failed</a>. Thanks to xet7.</summary>
"Activity notification delivery incomplete; pending intent retained" filled
the server log, one line for nearly every activity, and never said why: the
error was dropped. That line, and the recovery and cleanup ones, now carry the
failure's code, such as `activity-notification-intent-invalid` or `sync-busy`.
Any other error gives only its name and code, never its message, which can
contain an address or a card title. `tests/notificationFailureReason.test.cjs`
fails when a notification failure line drops its reason again.
</details>
and has the following developer-facing changes:
**Database tests** - the conformance catalogue and the speed test.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e9fa734853">Conformance checks indexed equality, array membership and mixed-type sorting</a>. Thanks to xet7.</summary>
Three cases for what FerretDB now answers from indexes or in the database: an
equality or `$in` on an indexed field (inside an array, a value differing only
by case, special characters, numbers, a long string), membership in an indexed
array of documents in every shape the array can take and after writes, and a
sort over every simple type with ties, null and missing fields, with and
without a limit, then with an array and a document among the values.
[A later commit](https://github.com/wekan/wekan/commit/494cbee598) adds both
inside an `$and`, as the due cards query sends them. SQLite, PostgreSQL, MySQL
and MariaDB answer all 114 cases as MongoDB 8 does.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/18878f6f29">Conformance checks the $or, $elemMatch and long $in shapes FerretDB now answers in the database</a>. Thanks to xet7.</summary>
The visible boards and the due cards of the `dueCards` publication - an `$or`
of the open permissions and a membership `$elemMatch`, and an `$or` of array
fields inside an `$and` - and an `$in` longer than the 32 values FerretDB's
MySQL backend compares with JSON functions, with negatives that must stay in Go.
SQLite, PostgreSQL, MySQL and MariaDB answer all 115 cases as MongoDB 8.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/9d2b64273b">db-conformance.sh no longer removes the containers of a run that is still going</a>. Thanks to xet7.</summary>
At start it removed every `wekan-conformance-db-*` container and network as
left behind by an interrupted run, including those of a run still in
progress: a speed test beside a conformance run lost its MySQL in the middle of
Due Cards ("connection refused") and its MariaDB while loading. Containers and
networks now carry the process id of their run as a label, and one whose run is
alive is kept; one without the label, of a run that ended, or whose process id
now belongs to something else is removed as before.
`tests/dbConformanceStaleContainers.test.cjs` runs the script's own functions
against a fake docker.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e81d17da80">New speed test results for SQLite, PostgreSQL, MySQL and MariaDB</a>. Thanks to xet7.</summary>
The four FerretDB v1 backends again, at twice the production data, after the
MySQL/MariaDB index reads, their multikey side tables, PostgreSQL's GIN
membership index and SQLite sorting in the database; every database returned
the same data as MongoDB. All Boards count went from 8-9 s to 159 ms on
PostgreSQL and from 9.4 s to 1.1 s on MySQL, and adding the 44 indexes takes
172 s on MySQL and 901 s on MariaDB instead of 20 and 40 minutes. The fixes for
the People page on SQLite, All Boards with a starred board on MySQL/MariaDB and
Due Cards' `$and` came after this run.
</details>
**Tests** - guards that had fallen behind a deliberate change.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/132fecbfd8">The import page guard pins the checked tick's physical margins from the first right-to-left fix</a>. Thanks to mimZD and xet7.</summary>
`tests/importPage.test.cjs` still required the checked box's label gap to be
`margin-inline-end`, which the [#6786](https://github.com/wekan/wekan/issues/6786)
fix changed on purpose. It now pins `margin-right` with the right-to-left rule
that moves the gap to the left, and the unchecked box's logical gap.
</details>
and updates the following translations:
**Languages updated:** Romanian, Standard Moroccan Tamazight, Tigre, Urdu.
**Wrong languages** - each locale in the language its tag names.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/d3dead539b">Romanian, Tamazight and Urdu in their own languages</a>. Thanks to xet7.</summary>
About 160 Romanian values were Italian, with some Spanish and French; about
330 Tamazight values were Arabic, or Kabyle and French in Latin letters, now
in Tifinagh; and most of the Urdu file was English spelled letter by letter in
Urdu script ("منتقل کو توپ" - توپ is a cannon - for Move to Top), now natural
Urdu. Search keywords in these locales are one word, as the query parser reads
them. `tests/wrongLanguageCorrections20261025.test.cjs` pins the languages,
the placeholders and the keywords.
</details>
**Other** - one more translation fix.
- [Tigre: the import size message uses the attested plural for files](https://github.com/wekan/wekan/commit/52bcd74efa).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.64 2026-10-24 WeKan ® release
**In short:** a **database speed test** times the common WeKan queries on every
database WeKan runs on, with twice a production WeKan of data, and checks they
all answer the same; its findings made **FerretDB** several times faster and
stopped MariaDB losing edits. Every platform runs the prebuilt
**ferretdb-sqlite**
FerretDB, the **Sandstorm .spk** carries only what a grain runs, and every build
loads **collection2** before the models.
This release adds the following new features:
**Sandstorm** - a smaller .spk, built from what a grain actually uses.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e413cd4720">The .spk leaves out every login method but Sandstorm's, and FerretDB's other database handlers</a>. Thanks to xet7.</summary>
A grain has one login (Sandstorm's), one database (FerretDB on SQLite) and one
mailbox (the grain's own address), but the .spk carried every login package
WeKan has and a FerretDB with the PostgreSQL, MySQL and SAP HANA handlers.
`releases/sandstorm-slim-spk.sh`, now used by `sandstorm.yml` and
`release-all.yml`, builds with a package profile
(`releases/sandstorm-slim.mjs`) that drops accounts-password, 2FA,
passwordless, lockout, OAuth2/OIDC, CAS, LDAP, SAML and the seven social logins
for that one build. It always trims the bundle for a linux/x64 sockjs grain,
verifies none of those packages got in and packs with `spk pack`.
`sandstorm-src/build-deps.sh` builds FerretDB from source with
`ferretdb_no_postgresql,ferretdb_no_mysql`. Under Sandstorm the Admin Panel
hides the login panes, Database migration, the IMAP/webhook mail settings and
the password fields, and the REST password login answers `sandstorm-sso-only`.
Against wekan-12.63-sandstorm.spk the file is 3.8 MiB (2.1 %) smaller and
unpacks 26.7 MiB smaller. Docker, Snap and the bundles are unchanged. (The
FerretDB source build was replaced by the prebuilt SQLite-only binary, see
FerretDB below.)
`tests/sandstormSlim.test.cjs` and
`tests/playwright/specs/sandstorm-slim.e2e.js` pin it. Design:
[Smaller-wekan-spk.md](docs/Platforms/FOSS/Container/Sandstorm/Smaller-wekan-spk.md).
</details>
**FerretDB** - every platform takes the smaller SQLite-only build, prebuilt.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/89da0e5be7">Every platform runs the prebuilt SQLite-only ferretdb-sqlite binary from wekan/FerretDB</a>. Thanks to xet7.</summary>
wekan/FerretDB now releases `ferretdb-sqlite-<arch>` beside `ferretdb-<arch>`:
the same FerretDB with only the SQLite handler, about 5 MB smaller, and also
as the `ferretdb-sqlite` image. `releases/ferretdb-asset.sh` chooses for every
download: the newest release that has `ferretdb-sqlite-<arch>`, and until one
does, the full `ferretdb-<arch>` of the newest release, which has the SQLite
handler too. The log says which one was taken. It verifies the download
against its `.sha256sum` and gives the tag, URL and checksum to the provenance
table. All bundle jobs of `release-all.yml`, `release-all-missing.yml`,
`bsd.yml`, the preflight and the local release bundle use it, so the Docker
images, snap, AppImage, Flatpak, macOS and Windows packages get it too. No
workflow builds FerretDB: the Sandstorm deps download the same binary. The
default `docker-compose.yml` downloads `ferretdb-sqlite-<arch>`
(`FERRETDB_BINARY`); the PostgreSQL, MySQL, MariaDB and SAP HANA compose files
keep the full binary. `tests/ferretdbSqliteAsset.test.cjs` pins it. See
[FerretDB v1](docs/Databases/FerretDB/1/README.md).
</details>
**Databases** - how fast each database answers WeKan, and whether the same.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/70485c38df">A database speed test: the common WeKan queries on every database, twice a production WeKan of data</a>. Thanks to xet7.</summary>
`docs/Databases/FerretDB/1/SpeedTest/speedtest.sh` (build.sh / build.bat ->
Tests -> Database speed test) loads 30,116 users, 92,306 boards, 221,038 lists,
222,546 swimlanes and 615,070 cards - twice a production WeKan, skewed like a
real one - with WeKan's own indexes into SQLite, PostgreSQL, MySQL and MariaDB
through FerretDB v1, MongoDB 6, 7, 8 and 9 and FerretDB 2, one at a time, and
times 18 queries as WeKan builds them. `Results.md` ranks the databases fastest
first, and the run fails when any answer or stored write differs from MongoDB's.
MongoDB 9 was fastest; FerretDB + SQLite, after the FerretDB fixes it led to
(below), came next. The README says what was found, what was improved and what
could still be: sorting in the database, multikey indexes beyond SQLite,
MySQL/MariaDB reads through their index columns, SQLite's JSONB.
</details>
and fixes the following bugs:
**Due Cards** - what the page reads to find them.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/c64b3a3b1d">The page reads only the ids and members of the boards it may show</a>. Thanks to xet7.</summary>
The publication fetched every board the user may see - every public board of
the instance among them, whole - to read two fields, and wrapped its board list
in an `$or` that neither MongoDB nor FerretDB could use an index for. It asks
for
`_id` and `members` only, and with no assigned-only board uses a plain `boardId`
list; an assigned-only board still shows only the user's assigned cards. With
FerretDB's fixes, Due Cards on FerretDB + SQLite went from 34 s to 12.8 s on the
speed test. `tests/dbSpeedTest.test.cjs` checks both board scopes.
</details>
**FerretDB 2** - the compose file that runs WeKan on it.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f9e1e90131">Its compose file starts with the current DocumentDB image</a>. Thanks to xet7.</summary>
`ghcr.io/ferretdb/postgres-documentdb:17` installs `pg_cron`, which may only be
created in the database named `postgres`; with `POSTGRES_DB: ferretdb` the
DocumentDB extension failed to install and the database never started.
`docker-compose-ferretdb-v2-postgresql.yml` uses `postgres` now - found when the
speed test started FerretDB 2 from that file's images.
</details>
**Collections** - which attachSchema the models get.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2699f4deea">Collection2 is loaded before the models, so every write is cleaned and given its defaults</a>. Thanks to xet7.</summary>
Collection2's attachSchema is in its lazy main module, which only
aldeed:schema-index loads, with an async `Collection2.load()`. The models
attached their schemas with whichever attachSchema existed first: collection2's,
or the bare shim in `imports/collectionHelpers.js`, which never cleans a write.
The full build happened to load collection2 first. The smaller Sandstorm build
reached the models sooner, so a new board's swimlane was stored without
`archived`, `sort` and `type`, and the board opened empty. `server/main.js` now
loads `meteor/aldeed:collection2/main` before the models in every build.
Without accounts-password the server also no longer stops on
`Accounts.emailTemplates` (`server/lib/accountsEmailTemplates.js`) or on
useraccounts' "at least one account service". `tests/sandstormSlim.test.cjs`
and the board, list and card test in `sandstorm-slim.e2e.js` pin it.
</details>
and has the following developer-tooling fix:
- [The Sandstorm package-import guard escapes the whole package name in its pattern](https://github.com/wekan/wekan/commit/f1e447dd36). Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.63 2026-10-22 WeKan ® release
**In short:** copying a card, linking it to another board or making one from a
**card template** no longer fails when the card carries values of another
board's **custom fields**, and no longer records AdminFieldBleed against the
user who tried.
This release fixes the following bugs:
**Custom fields** - which values a card brings to the board it is copied or
linked to.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/63ebdb9072">Copying or linking a card carries only the custom field values of its new board</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6800](https://github.com/wekan/wekan/issues/6800): a card made from a card
template, linked from another board or copied - and each subtask copied with it
- took its whole customFields array along: values of another board's fields,
of deleted fields, and entries with no field id. The admin-only field guard
could not match those to a field of the new board, refused the whole card and
recorded AdminFieldBleed against the user. Copy, subtask copy and link now keep
only values of fields defined on the destination board; an admin-only value of
that board is kept, so the guard still decides it.
`tests/copyForeignCustomFields.test.cjs` fails on the code before the fix.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.62 2026-10-22 WeKan ® release
**In short:** an opened card is **decluttered** - add buttons on their title's
line, a compact attachment list with its upload rules in the upload popup, and
two new **Show on Card** rows for attachment previews and details - and
**Board Settings / Card** spreads into columns to fit the screen, with rows
dragged by handle, icon or label. List moves no longer raise false
**HistoryIntegrity** reports, refused edits no longer **disable accounts**, and
a
restarting database is no longer a **crash**.
This release adds the following new features:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2c520ef382">A decluttered opened card, a compact attachment list, and Card Settings that fits the screen</a>. Thanks to mueschel and xet7.</summary>
[#6796](https://github.com/wekan/wekan/issues/6796): an opened card gave most
of its height to controls with nothing in them. A field's add button now sits
on its title's line; Requested and Assigned By put their two buttons side by
side; the checklist controls share a line. Attachments: the upload rules are in
the upload popup instead of above every list, "+" is a small button, a file
with nothing to preview gets a small file button, and the name has a line of
its own. Two new Board Settings / Card / Show on Card rows, **Attachments:
Preview** and **Attachments: Details**, are on by default.
Board Settings / Card is a centred panel as wide (up to 1800px) and tall as
the window, and each list spreads into as many columns as fit, read top to
bottom: on 1920x1080 nothing scrolls. Its rows are dragged by the handle, the
icon or the label with drag handles on, and by the icon or the label with them
off; before, only the handle dragged, and only vertically. A Node test and a
Playwright spec cover the layout, the new rows and every drag case.
</details>
and fixes the following bugs:
**History** - the checksum every change history row carries.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/76f9d41bf4">A list move with no swimlane no longer fails its own history checksum</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6797](https://github.com/wekan/wekan/issues/6797): Admin Panel / Problems /
Security showed HistoryIntegrity (critical) 81,833 times. A list bound to no
one swimlane has `swimlaneId: ''`; the legacy history append hashed the row
with '' and stored it through collection2's default clean, which turned it into
null, so every such move failed its own checksum. It now inserts as the
stored-chain path does, and rows already stored that way verify - null for ''
only in the id and label fields, so any real change still fails.
`tests/historyEmptyStringChecksum.test.cjs` fails on the code before the fix.
</details>
**Admin Panel / Problems** - which events disable an account or are reported as
a crash.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/425b4ddea7">An ordinary refused edit on one's own board no longer disables the account</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6798](https://github.com/wekan/wekan/issues/6798): four accounts on one
instance were disabled in three days. A Worker's allowed card move or checklist
tick was first refused by the general card check, which recorded
LinkedWriteBleed (high, blocked) and disabled the Worker; a read-only or
comment-only member whose screen still offered an edit was disabled the same
way. The Worker rules now ask without recording first and record only a change
still refused, and `authz.linked-write` is still recorded but no longer
disables an account. `tests/workerNoSelfBlock.test.cjs` runs both rules from
their source.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/f37204b1c3">A MongoDB pod restarting is a lost connection, not a critical crash</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6799](https://github.com/wekan/wekan/issues/6799): while a MongoDB pod
restarted in Kubernetes, its name did not resolve and nine critical CrashBleed
events were recorded although WeKan kept running. `getaddrinfo ENOTFOUND` and
`EAI_AGAIN` for the database's own host (from MONGO_URL and MONGO_OPLOG_URL) are
now a lost connection, transient at startup and at run time - another host
that does not resolve is still not a database problem - and the crash recorder
skips what is transient. `tests/databaseDnsRestart.test.cjs` runs the guards
and the recorder's own listeners.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.61 2026-10-22 WeKan ® release
**In short:** WeKan's npm dependencies move to their newest versions, among
them **Blockly 13.4**, whose new toolbox search is translated into every
language, and **imapflow**, **mailparser**, **pdfjs-dist** and
**isomorphic-git**, which now have tests that run them the way WeKan does. The
**core26 snap** gets the Caddy part the main snap already had, a language saved
as **ca@valencia** shows Valencian, and **Admin Panel / Problems** stops
re-reporting list indexes and calling a lost connection a schema bug.
This release updates the following dependencies:
- [The small npm updates](https://github.com/wekan/wekan/commit/8a00898252):
`@aws-sdk/*` 3.1155.0, `@fastify/busboy` 3.2.4, `@tweedegolf/sab-adapter-*`,
`blockly` 13.4.0, `dompurify` 3.4.17, `file-type` 22.2.0, `filesize`
11.0.28, `i18next` 26.5.4, `meteor-node-stubs` 1.2.32, `wasmagic` 1.0.12,
and for development `@rspack/core` 2.2.9, `@typescript-eslint/*` 8.72.0 and
`puppeteer` 25.15.0. Thanks to xet7.
- [imapflow 2.8.0, mailparser 3.12.2, pdfjs-dist 6.4.299, isomorphic-git
1.44.3 and pretty-ms 10](https://github.com/wekan/wekan/commit/189adaee52),
each with a test that runs it as WeKan does: the IMAP poller against
WeKan's fake IMAP server, attachment text search on a PDF, and the GitHub
wiki import's history reading. Thanks to xet7.
- [vmactions/openbsd-vm 1.4.9 and actions/setup-node v7.1.0](https://github.com/wekan/wekan/commit/92001bcfd9);
the BSD bundles are still built on OpenBSD 7.8. Thanks to xet7.
and fixes the following bugs:
**Admin Panel / Problems** - what a database message is reported as, and how
often.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/058a2dcbce">List sort indexes are made from key objects, so they are no longer re-created at every start</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6794](https://github.com/wekan/wekan/issues/6794): "Created the missing index
"-modifiedAt" on lists" came back at every start, from every pod. The list
sort options were passed to `ensureIndex` as strings, so lists got indexes on
fields literally named "-modifiedAt", "-title" and "-sort", and the "is it
there already?" check compared a string with an index key object, which never
matched. Startup now ensures one index per sort field and drops the three
stray ones; `ensureIndex` turns a string key into the key it means.
`tests/listSortIndexes.test.cjs` uses the reporter's real index list, runs
`ensureIndex` itself against a fake collection, and fails on any call in the
tree whose key is not an index key object.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/a9e680d4b2">A lost database connection inside a validation error is a connection problem, not a schema bug</a>. Thanks to Raymond-Voogt and xet7.</summary>
[#6795](https://github.com/wekan/wekan/issues/6795): while MongoDB restarted,
"ValidationError: Failed validation Socket 'connect' timed out" was reported as
an empty required field and a WeKan bug, and treated as fatal at startup.
collection2 wraps whatever failed during validation, so the schema rule now
yields to a lost connection, timeout, lock or too many connections in the same
message, and the MongoDB driver's own network, server-selection, pool and
not-primary errors are connection problems. The test runs both startup guards'
own decision on the reported error and on every wrapped transient cause.
</details>
**Snap** - the Caddy reverse proxy part of the core26 snap.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/2ad5999bc7">The core26 snap gets the fixed Caddy part, and both snaps fall back to Caddy 2.11.7</a>. Thanks to xet7.</summary>
`snapcraft-core26.yaml` still had the Caddy part `snapcraft.yaml` had before
v10.71: the version from `api.github.com`, whose 403 to a busy CI runner ended
the part under pipefail before its 2.8.4 fallback ran; a plain curl with no
retry; and a default Caddyfile that proxied to port 3000, where WeKan does not
listen when Caddy is on. Both snaps build from `snap-src`, so core26 now has
the main snap's part, and both pin 2.11.7 as the fallback.
`tests/releaseSnapArches.test.cjs` read `snapcraft.yaml` only, which is how
the copy was missed; it now checks every `snapcraft*.yaml` and the
Caddyfile's port.
</details>
**Languages** - which language a saved setting selects.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6a37160bc8">A language saved as ca@valencia shows Valencian, not English</a>. Thanks to xet7.</summary>
The Valencian file is `ca@valencia.i18n.json` but its registry tag is
`ca-valencia`, and the resolver treated only `_` as `-`, so a profile saved
as the file's name stayed in English. Both resolvers now compare through one
`normalizeLanguageTag`, and `tests/browserLanguageFallback.test.cjs` requires
every locale file name to resolve to a registry language.
</details>
and has the following translation updates:
**Blockly toolbox search** - the five texts Blockly 13.4 adds, in every
language.
**Languages updated:** 230 languages.
- [Toolbox search, in every locale](https://github.com/wekan/wekan/commit/8a00898252),
written from each file's own words for "block" and "search", with `%1`
kept. Corrected while there: Polish "Szukaj blokad" (search for
blockades) from Blockly's catalog, a stray "(1)" in Arabic, English mixed
into Odia's "no results", and Telugu's literal `null` that the importer
had replaced. The commit lists the languages translated with low
confidence, for a speaker to correct. Thanks to translators and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.60 2026-10-22 WeKan ® release
**In short:** **sanitize-html 2.18.1** fixes a stack overflow that nested
`<iframe>` tags caused in WeKan's import and export sanitizer, and a failed
import or export no longer **disables the account** of whoever ran it unless it
was an attempt. The forked Meteor **dynamic-import** and **meteor shell** keys
come from the crypto RNG.
This release fixes the following CRITICAL SECURITY ISSUES:
**Imports and exports** - what nested `<iframe>` did to the sanitizer, and who
a failure is blamed on.
<details>
<summary><a href="https://github.com/wekan/wekan/commit/96ed9ad202">sanitize-html 2.18.1: nested iframes no longer crash imports and exports, or lock out who ran them</a>. Thanks to xet7.</summary>
sanitize-html 2.18.0 re-sanitized the fallback of a discarded `<iframe>` with
no recursion limit
([GHSA-c9rx-x947-fp3h](https://github.com/advisories/GHSA-c9rx-x947-fp3h),
CWE-674). WeKan's server sanitizer (`server/lib/inputSanitizer.js`) discards
every tag, so it took that path: a few kilobytes of nested `<iframe>` threw
`RangeError: Maximum call stack size exceeded` in `secureTransfer`, which
sanitizes every import and export. `secureTransfer` records a throw as a high
"blocked" event naming the user, and that disables the account, so a card
planted with the payload locked out the next member who exported the board.
2.18.1 bounds the recursion and keeps `nestingLimit` across it; it also drops
the `deepmerge` dependency.
[The tests](https://github.com/wekan/wekan/commit/d89bcab8e0):
`tests/sanitizeHtmlNestedIframe.test.cjs` runs WeKan's own `sanitizeInput`
options on three nested-iframe payloads and checks the installed and locked
versions; with 2.18.0 it fails with the same `RangeError`. Its negative test
fails if anything else imports sanitize-html. The Playwright spec exports and
imports a board holding the payload and checks that nobody is locked out.
Nothing is refused, so there is no Admin Panel → Problems key.
</details>
<details>
<summary><a href="https://github.com/wekan/wekan/commit/567f99227b">A failed import or export disables an account only when it was an attempt</a>. Thanks to xet7.</summary>
`secureTransfer` recorded every throw as a high "blocked" event naming the
user, and that disables the account - so sanitize-html 2.18.0's crash, or any
other library fault, locked out whoever happened to run the import or export.
The transfer boundary now marks its own refusals (a cycle, a non-plain object,
a size limit), and only those are recorded as blocked: high, and medium for a
size limit, as before. Anything else shows in Admin Panel → Problems as
ImportExportFailure: detected, still naming whose import or export it was, and
never blocking an account.
`tests/transferFailure.test.cjs` drives real boundary refusals and a sanitizer
that throws through the account-blocking decision itself, and fails with the
old `secureTransfer`. It also fails if the boundary throws a plain `Error` or
anything other than `secureTransfer` calls it.
</details>
and fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/6efd39a70e">The dynamic-import and meteor shell keys come from the crypto RNG</a>. Thanks to GitHub CodeQL and xet7.</summary>
Alert 591 (js/insecure-randomness, CWE-338), in WeKan's fork of Meteor's
`dynamic-import`: the key that lets a request read the SERVER's dynamic modules
(`?key=` on `/__meteor__/dynamic-import/fetch`) came from `Math.random()`,
whose state can be recovered from earlier outputs, and was compared with
`===`. `meteor shell`'s connection key, which runs code in the server process,
was made and compared the same way. Meteor 3.5.3 and 3.6-rc.0 both have this.
Both now come from `crypto.randomBytes` (160 and 128 bits) and are compared
with `crypto.timingSafeEqual`. `tests/meteorForkRandomKeys.test.cjs` runs both,
and fails on any `Math.random()` in the forks and on a key, secret, token,
nonce, salt, password or code made from `Math.random()` anywhere in the tree. A
wrong key is not recorded in Admin Panel → Problems: another WeKan instance
behind the same load balancer legitimately sends its own.
</details>
Thanks to above GitHub users for their contributions and translators for their translations.
# v12.59 2026-10-22 WeKan ® release
**In short:** WeKan carries **Meteor 3.5.3's six security fixes** - DDP,
allow/deny, dynamic import, Google and Facebook login, and meteor shell - on
Meteor 3.6-rc.0, which does not have them yet. **Release recovery** no longer
stops on a passing GitHub API error, and **Cancel workflow** is kept able to
stop every release workflow.
This release fixes the following CRITICAL SECURITY ISSUES:
<details>
<summary><a href="https://github.com/wekan/wekan/commit/e0189bc53b">Meteor 3.5.3's six security fixes on Meteor 3.6-rc.0</a>. Thanks to Meteor developers, Vext Labs and xet7.</summary>
Meteor published the same six fixes as 3.3.3, 3.4.2 and 3.5.3
([announcement](https://forums.meteor.com/t/meteor-security-releases-3-3-3-3-4-2-and-3-5-3/64836)).
3.6-rc.0, which WeKan runs, does not have them: its tag is behind 3.5.3 by
exactly the 15 security commits. Going back to 3.5.3 would also mean Node.js
24 and Rspack 1, so WeKan carries 3.6-rc.0's `allow-deny`, `ddp-server`,
`dynamic-import`, `facebook-oauth`, `google-oauth` and `shell-server` in
`packages/` with Meteor's 3.5.3 patch applied unchanged:
- DDP: a message named `__proto__`, `toString` or not a string, and an async
handler that rejected, crashed the server.
- allow/deny: a top-level `__proto__` in an update was missing from the field
list the validators saw.
- dynamic `import()`: requests are bounded in size (413) and complexity (400).
- Google and Facebook direct-token login: a token issued to another
application was accepted. Identity now comes from the provider, and a
Facebook verification error no longer carries the app secret.
- `meteor shell`: its history and compile cache are private.
- Deploy discovery is in `meteor-tool`, which an app cannot replace. WeKan is
never deployed with `meteor deploy`, and a test keeps it so.
Refused attempts show in Admin Panel → Problems as Meteor DDP, Meteor
allow-deny, Meteor dynamic-import and Meteor OAuth, and none disables an
account. `tests/meteor353SecurityFixes.test.cjs` runs each fix's own code and
searches the tree for the shape of each fault; every check fails on the unfixed
packages, and the suite fails when `.meteor/release` moves on, because a fork
would silently keep 3.6-rc.0's code. The Playwright spec sends each attack to
a running WeKan built with the forks. See
[Meteor 3.5.3 security fixes](docs/Security/Meteor-3.5.3-Security-Fixes-2026-10-22.md).
</details>
and has the follow