old-CHANGELOG/2026/08.md
Moved out of CHANGELOG.md to keep that file small enough to open (wekan/wekan#6580). Nothing here has been changed: a release section is a record, and it reads the same as it did there.
Releases per day:
| 2026-08 | Releases |
|---|---|
| 02 | 3 |
| 03 | 1 |
| 04 | 11 |
| 05 | 2 |
| 06 | 1 |
| 08 | 2 |
| 09 | 5 |
| 10 | 2 |
| 11 | 3 |
| 12 | 6 |
| 13 | 2 |
| 15 | 2 |
| 16 | 5 |
| 17 | 2 |
| 18 | 2 |
| 19 | 3 |
| 21 | 1 |
| 22 | 1 |
| 23 | 3 |
| 25 | 2 |
| 26 | 2 |
| 27 | 2 |
| 28 | 4 |
| 29 | 8 |
| 30 | 7 |
| 31 | 4 |
In short: Notifications respect Muted as the final board-level opt-out, including assignment and mention paths. Board views keep cards in their real swimlanes and keep the selected layout visible while its profile change reaches the client.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Notifications - board watch levels remain authoritative for every source.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4be77e718">Muted boards no longer send assignment or other activity notifications</a>. Thanks to Nissulya and xet7.</summary>Direct assignment notifications bypassed the board watch-level selection, so a card assignment could send email from a Muted board. Mentions, card/list watchers and broad-event configuration could enter through the same path. The final recipient boundary now requires an active board member with Watching or Tracking selected. Regression coverage keeps those positive cases and rejects explicit/default Muted, inactive and non-member candidates.
</details>Board views - cards and the selected layout remain where the user put them.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7b060f57e">Cards stay in real swimlanes and view changes no longer snap back</a>. Thanks to hmeunier95 and xet7.</summary>The orphan-card fallback considered only active swimlanes, so cards correctly belonging to an archived swimlane looked orphaned and appeared in the first active one. It now excludes every real swimlane while retaining genuine orphan rescue. Separately, a successful persistence callback could clear a pending view before the reactive profile caught up and reveal the old view again. The pending choice now remains until the profile confirms it. Selector and persistence regressions cover both positive and negative paths.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Member Settings reliably preserve language, profile identity and uploaded-avatar choices instead of reverting them after the interface changes. Board views switch to Calendar and Gantt reactively without a full page reload that restores the previous view, while authenticated server methods and awaited writes keep all three preferences durable.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Member Settings - profile identity, language and avatars remain selected.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b9690c068">Language, full name and initials persist immediately</a>. Thanks to mzch and xet7.</summary>Language and profile identity changes used direct optimistic client collection updates. When the server rejected or cleaned that modifier, Minimongo rolled the visible change back, so language returned to the default and full name or initials appeared lost until another login. Authenticated server methods now validate and await these writes; unsupported languages, oversized fields and logged-out callers are rejected. Regression coverage excludes the old direct write path and checks both persistence methods.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/18ee57900">An uploaded avatar remains selected</a>. Thanks to hmeunier95 and xet7.</summary>Avatar upload completion previously left the profile-pointer update as a floating promise. The interface could refresh while the old or default avatar was still selected. Upload completion now awaits that durable profile write, and a negative regression prevents the unawaited call from returning.
</details>Board views - Calendar and Gantt remain open after they are selected.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5969643f5">Calendar and Gantt switch without reloading the application</a>. Thanks to hmeunier95 and xet7.</summary>An authenticated board-view change persisted its choice and then forced a full page reload. Calendar or Gantt therefore appeared briefly before the navigation restored the preceding profile view. A pending reactive value now renders the choice immediately while the authenticated method persists it; the full reload is removed. Coverage checks the positive persistence path and rejects any reload inside the authenticated branch.
</details>and improves the documentation:
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Packaged network configuration is explicit again. Bundles, containers, Docker Compose, AppImage, Windows and Snap no longer guess a network interface or replace configured ports and URLs at startup; stable localhost defaults remain where a self-contained bundle needs them, and administrator environment variables and Snap settings remain authoritative.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/cc94828e8">Packaged startup no longer guesses network endpoints</a>. Thanks to xet7.</summary>Automatic ROOT_URL, PORT and database-port discovery is reverted from the
release bundles, Docker image and Compose files, AppImage, Windows executable and
Snap because it caused packaged services to remain down. These targets again use
their explicit environment or Snap configuration, while self-contained launchers
retain their established localhost defaults. The automatic detector and its
generated state and lock files are no longer shipped. Regression coverage checks
that every packaged launcher stays free of endpoint discovery and preserves
explicit configuration.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Snap startup remains inside strict confinement while it compares MongoDB and FerretDB data, so the web service starts instead of failing before it can bind its port. The comparison keeps its configurable time limit, cleanup and recovery diagnostics without depending on a host executable that a confined Snap cannot run.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Snap startup - confined startup reaches the selected database and web port.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4f0d78daf">Snap database comparison no longer executes a host timeout command</a>. Thanks to xet7.</summary>The automatic comparison of MongoDB and FerretDB data used
/usr/bin/timeout. Strict Snap confinement denied that executable, so
wekan-control stopped before Node could listen on the configured web port even
though the service appeared active. A shell-native process monitor now enforces
the same configurable bound, allows the comparison's cleanup trap to stop its
temporary database readers, force-stops an unresponsive child, and retains the
existing timeout diagnostics. Regression coverage rejects any return to the host
command and checks malformed timeout settings.
Setting autopick=false previously disabled only the comparison script itself,
after wekan-control had already tried to launch it through the forbidden host
timeout command. The launcher is now skipped entirely. FerretDB startup also
defaults its polling throttle and interval through unset-safe shell expansion,
instead of terminating under set -u before Node binds the web port. Negative
tests reproduce both conditions from Snap revision 3872.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: The Snap Problems report connects to the database endpoint selected by the running services, including an automatically chosen FerretDB port, while preserving explicitly configured external database URLs. Snap services also recover an abandoned endpoint lock instead of appearing active without starting FerretDB.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Snap startup - database services and diagnostics share one live endpoint.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e1a8ac001">Snap problem reports use the database URL of the running services</a>. Thanks to xet7.</summary>Snap services can select a free FerretDB port when the preferred port 27019 is
occupied, but wekan.problems continued connecting to the old fixed default and
reported ECONNREFUSED even while FerretDB and WeKan were active. The command
now sources the services' shared startup-network state and passes its resolved
MONGO_URL to the report. An administrator's explicit external database URL
remains authoritative. Positive tests execute the real wrapper with a dynamic
port and an external URL; negative coverage prevents the obsolete fixed port
from returning to the wrapper.
WeKan 11.34 could leave an empty .startup-network.lock directory without its
endpoint state file when a service stopped between acquiring the lock and the
atomic state-file rename. Every later service then waited forever: snap showed
the shell wrappers as active, but FerretDB never launched and port 27019 refused
connections. New locks record their owner. A dead owner's lock is reclaimed
immediately, an older empty lock after a short grace period, and a live owner is
never displaced. Waiting for a live owner is bounded and fails explicitly
instead of presenting a permanently active service. Executable regressions
cover both the exact empty-lock failure and a dead-owner lock.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Release metadata advances WeKan to v11.34 consistently across the Node package, Docker, Snap and Sandstorm packages, generated API files and Windows offline documentation. This keeps every published platform and its documentation on the same version instead of leaving consumers with conflicting package identities.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release updates the following release metadata:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b14c3a5d2">All package and documentation versions advance to v11.34</a>. Thanks to xet7.</summary>The release workflow updates package.json and its lockfile, the Docker and
Stacker image definitions, Snap and Sandstorm package metadata, generated API
documentation, and Windows offline-install documentation together. Recording
the generated version commit makes the actual contents of v11.34 visible instead
of leaving the released section with an Upcoming placeholder.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: PDFKit is statically included in every fresh production bundle while using its platform-portable CommonJS entry, so Docker and offline packages start without a missing module or a Linux build-runner path.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following build and release bugs:
Release builds - packaged dependencies and published version metadata stay portable and synchronized.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aedb7a2a8">Every release target receives portable PDFKit code</a>. Thanks to xet7.</summary>The v11.32 runtime resolver avoided PDFKit's Linux build-time URL on Windows,
but also bypassed Meteor's application-dependency discovery. Fresh Docker
images consequently omitted PDFKit and stopped during boot with Cannot find module '/build/programs/server/npm/node_modules/pdfkit'. WeKan again uses a
static import so Rspack includes the dependency, while an install-time,
fail-closed patch selects PDFKit's published CommonJS entry and its deployed
__filename resource paths on every operating system. Positive and negative
tests cover the package layout and dependency-discovery boundary. A fresh
production build, the Docker-equivalent server install and every release prune
pass the real bundle smoke boot.
The version-manifest regression no longer repeats WeKan, Meteor, Node and NPM versions as test constants that become stale at the next release. It reads the same validated Dockerfile pins as the release helper under test, while keeping the injected FerretDB version independent. This restored the complete Node test run after the WeKan 11.31 version bump and preserves exact manifest coverage.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Windows release bundles load PDFKit from their deployed path instead of retaining a Linux build-runner URL, and Snap builds retry transient Store failures while installing LXD. Cross-platform PDF generation and native release assembly now have regression coverage for both failure boundaries, while a CodeQL incomplete-escaping alert is removed.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUE found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1eaf0a117">Version tests compare generated text without incomplete escaping</a>. Thanks to xet7.</summary>The website-version test no longer interpolates generated manifest text into a
regular expression after escaping only dots, which left backslashes and every
other regular-expression metacharacter active. It now compares the expected
pre block as literal text, removing CodeQL alert 441 and the unnecessary
encoding boundary. The positive manifest and negative injected-version cases
both pass through the real release helper.
and fixes the following build and release bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c39c6ad3">Windows PDF and Snap release builds stay portable</a>. Thanks to xet7.</summary>The server PDF renderer now asks Meteor's runtime CommonJS resolver for PDFKit,
selecting the package entry that derives resource paths from the deployed
__filename. The Linux-built Windows bundle therefore no longer passes its
build runner's file:// URL to Node's createRequire. The native Snap jobs
also preinstall LXD with five bounded attempts before the official build action,
so a transient Snap Store nonce timeout no longer ends a release before
Snapcraft starts. Positive and negative regression tests preserve both paths;
the PDF generation suites and production Meteor bundle build pass.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: User sessions now survive page refreshes and board-view navigation with the HttpOnly-cookie security model. Admin version checks report the newest published WeKan and FerretDB releases with safe localized failure handling. Packaged WeKan also starts on the current Ethernet or WLAN IPv4 address, preferring port 80 and selecting free web and loopback-only FerretDB ports when not configured; Docker Compose follows the same path.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release adds the following new feature:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b39d57bf7">The Version pane reads one published component manifest</a>. Thanks to xet7.</summary>A button above the installed-version table asks the server for the fixed
https://wekan.fi/version.txt resource, then shows its WeKan, FerretDB, Meteor,
Node and NPM lines as escaped text in a pre block. Only Global Admins can call
the method; a ten-second timeout, five exact labels, strict version grammar and
a one-kilobyte limit turn offline, malformed and injected responses into one
safe localized failure. The release-all website job and the local release path
now generate both that file and the identical install-page block from the
release and Dockerfile pins plus the validated FerretDB tag. The
website companion commit
adds the initial files. Unit and negative tests cover parsing, generation and
workflow wiring, and the production Meteor build passes.
and improves packaged startup:
Packaged startup - automatic endpoints stay consistent everywhere.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0bf470c57">Packages advertise a working URL on the current network</a>. Thanks to xet7.</summary>When ROOT_URL is not configured, the bundle ZIP, Docker image, AppImage,
Windows single executable and Snap now discover the active IPv4 address and a
port the runtime can bind. Port 80 is preferred and produces a clean
http://IP-address URL; another free port is selected and included in the URL
when port 80 is unavailable. Bundled FerretDB independently selects a free
localhost-only port, and MONGO_URL follows that exact endpoint. Startup logs
show the resulting ROOT_URL, PORT, FerretDB address and MONGO_URL, while
explicit deployment settings remain authoritative. Positive and negative
tests cover automatic and configured endpoints, loopback enforcement and all
five packaging paths; all 694 Node suites pass.
The seven single-instance Docker Compose files no longer activate hard-coded
ROOT_URL, PORT or MONGO_URL values. Their commented configuration explains
that startup selects the current IPv4 and a usable port, derives the internal
MongoDB or FerretDB service URL from Compose metadata, and prints every resolved
endpoint in the Node console. Published container port 80 now agrees with the
automatic preference. The POSIX and Windows offline launchers document and use
the same resolver, and Docker no longer preselects port 27017 before its free
localhost-port check. Positive and negative tests cover MongoDB, FerretDB v1,
authenticated FerretDB v2, explicit overrides and every configuration file.
The private-board refresh browser regression passes with both repo-local
FerretDB and Meteor's included MongoDB; all 694 Node suites pass.
and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/924d8269e">Private-board sessions survive page refreshes</a>. Thanks to lucasarrudadev and xet7.</summary>WeKan configured Meteor's HttpOnly-cookie session mode after the Accounts client had already performed its one startup resume check. A successful login wrote the protected cookie, but a fresh page held no token in its intentionally memory-only client storage and never asked the cookie endpoint to restore it. WeKan now starts Meteor's public cookie-login path immediately after enabling the mode, so refreshes and board-view navigations restore the same user instead of showing Sign In. Static positive and negative tests preserve the protected flow, and a Chromium regression reloads a private board against the repo-local Meteor server and included MongoDB and verifies both the user and board remain.
</details>and improves the following translations:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c6b8c7301">Version checking reports failures in the user's language</a>. Thanks to xet7.</summary>The check button and its offline, timeout or invalid-response message now use dedicated translation keys. Every locale keeps the English key order, every previously complete non-English locale has localized text, and only the same 37 already-incomplete locales retain English placeholders. Regression coverage checks all locale files, representative natural translations and the existing placeholder inventory; all 695 plain-Node suites and the production Meteor build pass.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Custom fields can now be removed from cards backed by FerretDB, completing the selection, checkbox and export repairs for issue #6611. The database now follows MongoDB's document-condition behavior for array removals, so a saved field value cannot make the field reappear after it is deselected. Compatibility and browser coverage protect the complete flow.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/2c61454c1">Custom fields stay deselected on FerretDB</a>. Thanks to Heart1010 and xet7.</summary>FerretDB compared the $pull operand {_id: customFieldId} with the complete
array element, which also contains value, so it could never match. Its
MongoDB-compatible document-condition fix
now matches fields within each element. Positive and negative backend tests
cover matching and non-matching wider documents; WeKan's server contract and
browser test cover removing a valued field from a card. The earlier checkbox
and deleted-export fixes remain covered in the same interaction suite.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Release builds now wait for every AppImage input, hand a slow riscv64 Launchpad build off cleanly, and start the Enigma-packaged Windows EXE despite its false legacy-Windows version result. Browser tests follow the new HttpOnly session model, the Helm chart has a viable default memory budget, and AWS storage, PDF generation, browser automation and Playwright's MongoDB driver are updated.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release updates the following dependencies:
Thanks to dependabot.
and fixes the following developer-tooling bugs:
Build and release tooling - every scheduled artifact path completes or hands its remote work off cleanly.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9129125f4">Release builds keep every scheduled artifact path healthy</a>. Thanks to xet7.</summary>The AppImage workflow now waits for the i386 and armhf bundles before deciding whether those images can be built. The riscv64 Launchpad waiter uses an isolated timeout process group, so its five-hour limit returns the status handled as a pending remote build instead of cancelling the Actions step. The Windows single-EXE launcher also sets Node's platform-check escape hatch when Enigma's virtualization reports a false legacy Windows version, while preserving an explicit administrator value. Static positive and negative regression tests pin all three build paths; the platform-specific builds remain verified by their workflow smoke tests.
</details>The test harness - authentication and static checks match the production session model and the workflows they validate.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7f9ed6915">Browser tests retain their HttpOnly sessions across navigation</a>. Thanks to xet7.</summary>The Node E2E and Playwright login helpers now seed their generated resume token
through the browser's native HttpOnly, SameSite=Lax cookie before logging in.
This preserves authentication when a navigation creates a new DDP connection
without exposing the credential to page JavaScript or restoring Local Storage
tokens. The release-script parity test also classifies the private FerretDB
preflight correctly, and Windows release commands put --repo on the command
line their static guard inspects. Positive and negative session tests cover the
cookie attributes and navigation boundary; all 693 Node suites pass.
The Helm chart - the default pod budget fits WeKan's container-aware Node.js heap and native processes.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: MailTitleBleed prevents stored board, list, card and other activity text from becoming active HTML in notification emails. The completed localized message is escaped at the final HTML boundary, and notification subjects cannot inject additional mail headers. CookieTokenBleed moves persistent resume authentication from JavaScript-readable cookies and Web Storage into Meteor's native HttpOnly cookie flow.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUES:
<details> <summary><a href="https://github.com/wekan/wekan/commit/96b42c3d3">Notification activity text remains text in HTML email</a>. Thanks to binary-lover and xet7.</summary>GHSA-hp9m-vff5-7pvw, Moderate, CWE-79. Stored board, list and card titles and other activity values were interpolated into localized notification prose before that prose was used as an HTML email body. A writable member could therefore place active markup in a title and have it delivered to another member's mail client.
The complete localized notification is now HTML-escaped once at the final output boundary, before its plain-text newlines become HTML line breaks. This protects every current and future activity parameter without changing ordinary text or the plain-text notification mode. The subject formatter also removes newlines so stored values cannot create additional mail headers. Positive and negative unit coverage exercises active markup, ordinary titles, header newlines, the final output wiring and all 246 locale bundles.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/78a3a21c3">Persistent resume tokens are unavailable to browser scripts</a>. Thanks to binary-lover and xet7.</summary>CookieTokenBleed -
GHSA-8phm-9rqm-v9hc,
Moderate, CWE-1004. WeKan's custom file-route authentication mirror wrote the
Meteor resume token into a cookie from browser JavaScript, which cannot apply
the HttpOnly attribute. Meteor also kept its original token in Local Storage,
so script running in the WeKan origin could read either persistent copy.
WeKan now enables Meteor 3.5's maintained HttpOnly resume-cookie flow on both
client and server with persistent Web Storage disabled. The custom readable
cookie synchronization and bootstrap code is removed, leaving only an
in-memory credential in the active tab. Header login applies HttpOnly as well
as SameSite=Lax and HTTPS Secure to the cookies it issues. Positive and
negative regression coverage pins both configurations, the absence of browser
cookie/token copying and retention of the Secure header-login path; existing
request-auth and session-isolation suites remain green.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: SearchBleed closes a global-search authorization bypass that let an authenticated user submit an executable database selector and search cards outside their boards on MongoDB deployments. Initial searches and stored pagination selectors are now independently rejected or constrained to the caller's current board access.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of SearchBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/773776214">Global search cannot execute untrusted selectors or cross board boundaries</a>. Thanks to crypto-nidh and xet7.</summary>GHSA-33h9-rc5h-667p, High, CWE-943. The global-search publication accepted a selector supplied by an authenticated client and used it instead of the normal board-scoped selector. On MongoDB, execution operators could cause denial of service and act as an oracle over cards belonging to inaccessible boards. FerretDB rejected the reported execution operator, but the missing authorization boundary existed in the application and is now enforced independently of the database backend.
Executable selectors now go through WeKan's shared NoSQL guard before reaching the database. Other client selectors are conjoined with the caller's authorized board IDs rather than replacing that scope. The shared query path repeats both checks for stored next-page and previous-page selectors, so sessions created by an older vulnerable release cannot preserve broader access after upgrade. Positive, negative and legacy-session unit coverage pins both layers; the existing rendered-browser search regression verifies that inaccessible-board cards remain absent while ordinary global search continues to work.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: The Windows single-EXE CI smoke test no longer fails with port-binding errors for FerretDB (ports 27017 and 8088) on GitHub Actions runners that ship MongoDB 7 as a pre-installed system service. The release also corrects the GitHub issue-closing metadata for the persistent-login fix shipped in WeKan 11.25.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CI reliability issue:
Windows single EXE - smoke test was failing because the GitHub Actions Windows runner ships MongoDB Server 7.0 as a running system service that holds port 27017 (the same port the bundled FerretDB needs) and optionally port 8088.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f0ecb1434">Free FerretDB ports before the Windows EXE smoke test</a>. Thanks to xet7.</summary>The GitHub Actions windows-latest runner ships MongoDB Server 7.0 as a
running Windows service. The bundled FerretDB inside the packaged WeKan EXE
tries to bind to 127.0.0.1:27017 (main listener) and 127.0.0.1:8088
(debug handler). When these ports are already occupied, FerretDB exits
immediately, causing WeKan to restart in a loop and the smoke test to time
out with "The packaged EXE did not answer on port 8080."
The fix adds a "Free ports used by the packaged EXE before smoke test" step
that stops the MongoDB (and mongod) Windows service and then kills any
remaining process holding ports 27017 or 8088 before the smoke EXE is
launched. This is CI-only and has no effect on the packaged EXE behavior for
end-users.
The failing run shows the port collision and timeout this change addresses.
</details>and corrects the following release metadata:
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Persistent login survives page refreshes again on FerretDB deployments. Docker replaces an incompatible cached database binary instead of retaining it indefinitely across every FerretDB v1 backend, and Helm refreshes its moving database image. Portable Windows WeKan packages Node.js, FerretDB SQLite and the server into one executable with its data beside it. Release preflight also prevents bundles from shipping an incompatible FerretDB version.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release adds the following new feature:
<details> <summary><a href="https://github.com/wekan/wekan/commit/a02d4a499">Windows gets a portable single-file WeKan</a>. Thanks to xet7.</summary>The release workflow wraps the already verified win64 bundle with the freeware
Enigma Virtual Box archived in the WeKan v11.25 release, verifies its pinned
SHA256, and produces WeKan-<version>-win64.exe with its own SHA256. A small
native entry point launches the bundle's existing start-wekan.bat, which starts
Node.js, FerretDB's embedded SQLite backend and WeKan together. Persistent SQLite
data, attachments and avatars remain outside the virtual executable in
wekan-files beside it, so moving the EXE and directory together keeps the
installation portable. The final packed executable must answer its HTTP
sign-in route before it can be attached to a release. Full and missing-asset
release workflows both publish it, with regression coverage for the complete
startup and delivery chain.
and fixes the following login bug:
Persistent sessions - deployment paths deliver the FerretDB behavior that Meteor's standard resume-token query requires.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7ac958888414c118a25c68a6a5f45fafecf97dcd">Persistent login survives page refreshes with FerretDB</a>. Thanks to majo1989 and xet7.</summary>WeKan 11.24 restored Meteor's standard resume-login query, but its release delivery could retain a FerretDB binary from before nested positional projections were fixed. Refreshing either a local-password or SSO session then returned 403 and exposed an empty board shell before the login page appeared. Bundles now reject FerretDB releases older than v1.63.0. Docker records the release represented by its cached binary and replaces a changed requirement atomically instead of retaining an incompatible executable indefinitely. Unit coverage accepts compatible and newer versions, rejects older and unknown versions, and verifies cache replacement; existing authenticated Playwright board tests cover reloads through the UI.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2258d8228959aaa2ac6f731d3a640ca94217f5b7">Release workflows always select the newest published binaries</a>. Thanks to xet7.</summary>Docker resolves the newest FerretDB release on every start, verifies its
published checksum and atomically replaces a differently tagged cached binary.
Regression coverage also audits the release workflow: FerretDB and MongoDB
Database Tools come from their latest releases, while the Node resolver walks
maintained Node versions newest-first and uses the matching newest available
node-patches binary when the official sources do not publish that platform.
The PostgreSQL, MySQL, MariaDB and SAP HANA Compose variants previously downloaded FerretDB only when their persistent volume had no executable. They now resolve the concrete newest release on every start, compare it with a release marker, verify the replacement's published SHA256 and move it into place only after verification. Regression coverage applies the same cache, checksum and atomic-replacement requirements to all five FerretDB v1 Compose files.
</details> <details> <summary><a href="https://github.com/wekan/charts/commit/7970ae5">Helm refreshes its moving FerretDB image on pod start</a>. Thanks to xet7.</summary>The chart deliberately uses ghcr.io/wekan/ferretdb:latest, but
IfNotPresent allowed Kubernetes to reuse an older image already cached on a
node. Its FerretDB container now uses Always, so every newly started pod asks
the registry for the current image. Chart regression coverage pins that policy.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Notification title structure now lives in server code instead of translation files, so translators cannot accidentally rename or remove its board and card values. All locale bundles retain their translated prose while the application owns these two non-language layouts, with focused regression coverage for formatting, translation fallback and every supported locale.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following translation regression:
Notification titles - structural board and card layouts no longer depend on translator-maintained executable tokens.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c9f42889af5804042ab16582d8b2aa8ca8259fa9">Protected activity placeholders remain unchanged across locales</a>. Thanks to xet7.</summary>A translation refresh translated or renamed underscore-delimited placeholders in sixteen locale files. Those placeholders are executable tokens rather than prose, so this restores their exact English-source spelling. Seven focused translation regression suites verify both their inventories and values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/162740e88585724175de6c097e6d56db660d3928">Structural title values are kept out of translations</a>. Thanks to xet7.</summary>The board-only and board/card notification subjects contained no translatable prose, yet their executable markers appeared in every locale. The server now formats both layouts itself, while ordinary notification subjects still use the recipient's language. The two obsolete keys are removed from all 246 locale files, and tests cover both hardcoded layouts, the translated fallback and the complete locale inventory.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Snap assembly finds its migration helper from the working directory it actually uses, while extra-architecture builds tolerate a transient registry response without accepting incomplete manifest data.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following release build failures:
<details> <summary><a href="https://github.com/wekan/wekan/commit/e85d9615d">Snap and extra-architecture builds find and verify their inputs reliably</a>. Thanks to xet7.</summary>Snapcraft enters .build before assembling the downloaded bundle, so all six
native and variant Snap jobs looked for the MongoDB-to-FerretDB migrator under
the nonexistent .build/releases directory. The Core 24 and staged Core 26
recipes now step back to the repository before copying it. The extra-architecture
preflight also retries a Docker registry manifest request five times, writing to
a temporary file so a partial response is never parsed. A persistent registry
failure still stops the build. Focused Snap, ARM-container and Node-source tests
cover both fixes.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: RuleBleed closes the server-side automation path that could write cards into an inaccessible private board. Its browser regression now uses isolated rule data, while Snap and multi-architecture builds resolve their inputs reliably across working directories and transient registry responses.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of RuleBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/dffe85cce">Rules cannot write cards into inaccessible private boards</a>. Thanks to crypto-nidh and xet7.</summary>GHSA-9w4x-hf2r-hc9v, High, CWE-862 and CWE-863. Move, link and bulk-move automation actions execute server-side, so they bypass the collection deny hook that protects direct DDP writes. Rule creation now validates destination write access before inserting anything, and execution validates again before resolving destination structure. This protects legacy, imported and scheduled rules while removing the private list/swimlane oracle. Denied attempts are bounded and summarized as RuleBleed in Admin Panel → Problems. Positive, negative, legacy-path, REST-preservation and rendered-browser regressions cover both authorization layers.
</details>and fixes the following release build failures:
<details> <summary><a href="https://github.com/wekan/wekan/commit/21ec491ee">Snap and extra-architecture builds find and verify their inputs reliably</a>. Thanks to xet7.</summary>Snapcraft enters .build before assembling the downloaded bundle, so all six
native and variant Snap jobs looked for the MongoDB-to-FerretDB migrator under
the nonexistent .build/releases directory. The Core 24 and staged Core 26
recipes now step back to the repository before copying it. The extra-architecture
preflight also retries a Docker registry manifest request five times, writing to
a temporary file so a partial response is never parsed. A persistent registry
failure still stops the build. Focused Snap, ARM-container and Node-source tests
cover both fixes.
and fixes the following test isolation bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6d50564c8">Rule authorization checks use isolated browser data</a>. Thanks to xet7.</summary>The RuleBleed browser regression correctly received not-authorized, but its
global count by a non-unique title found a stale rule left by an earlier
interrupted run. The assertion now scopes its query to the current fixture
board. Board cleanup also removes its rules, triggers and actions, so one
browser project cannot contaminate the next. A source regression pins all three
cleanup paths.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: File responses, board writes and automation rules close four security gaps. Database launchers use FerretDB's write-notified OpLog with an explicit standalone fallback, while DEBUGSPEED diagnostics make comparative MongoDB/FerretDB traffic runs measurable, and FerretDB board creation and loading now complete reliably on a restored large dataset despite a small remaining delay. Card date badges share one self-cleaning minute ticker, translations render bundled English without waiting for database overrides, minicard composers and board creation save from the form the user submitted, database selectors no longer accommodate FerretDB query-planner gaps, and Snap assembly uses the correct extracted release bundle on every architecture. Regression coverage now preserves translation placeholders and isolates browser fixtures while accepting the bounded delivery time observed with a restored database.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUES:
File responses - stored MIME metadata cannot turn downloads into active content.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8d427583e">Avatar MIME metadata cannot serve browser-executable content</a>. Thanks to crypto-nidh and xet7.</summary>AvatarMimeBleed -
GHSA-4mjm-vj9f-p629,
Moderate, CWE-79 and CWE-434. Both current and CollectionFS-compatible avatar
routes trusted their stored MIME type. HTML, XML, SVG or JavaScript content
could therefore render under the WeKan origin. A shared policy now serves every
browser-executable type as a sandboxed opaque download with nosniff and frame
denial, while safe images remain inline. No Problems event is emitted because
legitimate avatar views use this path and cannot attribute old metadata to an
attacker. Behavioral tests cover every dangerous family, and a whole-route
negative scan forbids stored MIME fields from reaching Content-Type directly.
LegacyAttachBleed -
GHSA-xcxp-hx9w-q5q9,
Moderate, CWE-79 and CWE-693. The legacy attachment route forced downloads but
applied CSP, nosniff and frame denial only to SVG filenames, leaving stored
HTML, XML and JavaScript MIME types active. It now uses the same centralized
opaque-download policy as avatars. There is no attributable Problems event:
ordinary legacy downloads use the same path, and logging would blame a viewer
for metadata supplied earlier. Positive MIME decisions and the repository-wide
negative response-header scan cover the fix.
Board structure - creating a swimlane requires the shared write capability.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8d427583e">Read-only members cannot create default swimlanes</a>. Thanks to crypto-nidh and xet7.</summary>SwimlaneBleed -
GHSA-2x89-6mq2-834r,
Moderate, CWE-862. ensureDefaultSwimlane authorized an insertion with read
membership, bypassing the collection's write rule and allowing read-only roles
to alter board structure. The method now requires the same shared write
capability as every swimlane mutation. Rejected direct calls are bounded and
summarized as SwimlaneBleed in Admin Panel → Problems, and logging failure
cannot weaken the denial. Tests pin the guard before insertion and forbid read
membership or public visibility from authorizing the method.
Automation rules - cross-board actions enforce destination permissions.
<details> <summary><a href="https://github.com/wekan/wekan/commit/72fd50a03">Rules cannot write cards into inaccessible private boards</a>. Thanks to crypto-nidh and xet7.</summary>RuleBleed - GHSA-9w4x-hf2r-hc9v, High, CWE-862 and CWE-863. Move, link and bulk-move automation actions execute server-side, so they bypass the collection deny hook that protects direct DDP writes. An authenticated user could store a destination board ID in a rule on their own board and inject cards into a private board they could not access. Rule creation now validates destination write access before inserting any document, and execution validates again before resolving destination structure, protecting legacy and imported rules while removing the list/swimlane oracle. Denied attempts are bounded and summarized as RuleBleed in Admin Panel → Problems. Positive, negative, legacy-path, REST-preservation and rendered-browser regressions cover both authorization layers.
</details>and improves the following database deployment configuration:
Database launchers - bundled processes use the correct reactive mode.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ecfbd0bf66c4e72d0b9c7d95047d339c43260ada">Keep FerretDB launchers standalone and polling-only</a>. Thanks to xet7.</summary>The bundle, Windows, container entrypoint and every FerretDB v1 Compose backend
now clear inherited OpLog settings and start without replica-set arguments.
MongoDB 7 keeps its explicit rs0 connection, while the Meteor 3 multitenancy
guide gains an idempotent replica-set initializer and scoped OpLog credentials.
Positive and negative regression tests pin the separation.
The Snap and DEBUGSPEED launcher now start FerretDB as a single-node replica
set and give Meteor an OpLog URL, using oplog,polling without attempting
unsupported change streams. FerretDB wakes idle awaitData tails when a write
actually appends an OpLog record and emits directly applicable replacement
updates, eliminating the SQLite query loop and invalid whole-document $set
that previously made polling the safer default. Setting
wekan-ferretdb-oplog=false retains the standalone polling fallback. The
FerretDB write notification fix
has broadcast, gate and update-shape coverage. Its follow-up
idle-tail fix makes cursors
wait before querying and repairs the logical timestamp index on older OpLogs.
The filled-batch fix
returns a notified OpLog batch before waiting for another write, so reactive
board lists and newly opened boards cannot remain one mutation behind. A
restored large-dataset run confirms that creating a board, receiving its tile
and opening it now work reliably; a small query delay remains but does not
prevent completion. The
projection fix avoids
recursively decoding unrequested large fields during selective collection
scans, and its ID follow-up
retains MongoDB's implicit _id through the internal projection pipeline.
The distinct optimization
also decodes only its result path and filter fields instead of every complete
candidate document. Its SQLite pushdown
now constructs minimal result documents and collapses duplicate top-level keys
before they cross into Go. Its indexed follow-up
selects an existing distinct-key index and labels slow query shapes by their
originating command. The linear deduplication follow-up
sorts once and compacts adjacent equal BSON values instead of repeatedly
scanning the growing result array; 45,640 unique values now compact in about
5–18 milliseconds instead of consuming 15–17 seconds. The raw-value SQLite
follow-up deduplicates
schema/value pairs before constructing SJSON, reducing the restored
299,539-card distinct(listId) SQL workload from 3.60 to 2.94 seconds while
preserving all 41,875 BSON-distinct values. The full-document decoder
optimization
reuses bounded parsed schemas and avoids a streaming decoder allocation for
each common scalar. Its allocation follow-up
uses direct full-document JSON parsing, strict scalar conversion and
preallocated ordered fields, cutting the representative decoder benchmark from
617 to 561 allocations and about 53 KB to 44 KB per document. The hot-schema
and indexed-write optimization
replaces the cache's periodic full flush with a bounded 4,096-entry LRU; across
the restored 299,539-card dataset it reduced isolated complete decoding from
35.1 seconds to 17.2–19.0 seconds. It also makes SQLite updates and deletes use
the existing unique _id expression index instead of scanning the collection.
The non-finite numeric pushdown
keeps the unchanged numeric $type plus negated-range repair query inside
SQLite: across five restored ordered collections containing about 747,000
documents, it returns zero candidates in 1.9 seconds instead of spending about
30 seconds decoding every document. Ambiguous values remain candidates for the
authoritative MongoDB-compatible filter. The nested-composite decoder
optimization also replaces
per-document and per-array streaming decoders with complete-value parsing. The
representative nested-card workload drops from 561 to 431 allocations and from
about 44 KB to 31 KB per document, with median decode time improving from about
179 to 141 microseconds while preserving malformed-input behavior. The
linear document-construction follow-up
uses the document's existing key-count map to distinguish new fields from
replacements instead of searching all previously appended fields. The restored
299,539-card collection averages 31.3 fields per document; a representative
40-field decode improves by about 8% without adding allocations.
The SQLite iterator follow-up
also resolves result-column metadata once per query instead of requesting and
comparing it for every row. Full card scans avoid nearly 300,000 redundant
metadata calls, and each distinct scan avoids one call per returned key.
The schema-covering index optimization
appends internal BSON schema expressions to eligible SQLite indexes while
retaining their original value prefix and Mongo-visible definition. Existing
indexes are upgraded transactionally once. The restored 299,539-card
distinct(listId) SQL falls from 2.96 seconds to 45 milliseconds using a
covering scan, while unique and dotted indexes remain unchanged. Its
compound-index follow-up
also covers every top-level logical key and selects the narrowest index
containing the distinct and filter fields. The restored filtered
distinct(swimlaneId, archived) SQL becomes a 176-millisecond covering scan;
its live non-covering stage previously took about 12.2 seconds.
FerretDB now also creates a private scalar access path when a numeric corruption
check targets a field already present in a declared compound index. SQLite
cannot seek a non-leading compound key; the restored 299,539-card sort check
instead falls from 1.31 seconds to 17 milliseconds after a 1.26-second one-time
index build, without changing the MongoDB query or visible index definition.
Top-level $exists probes now run exactly inside SQLite as well, distinguishing
a missing field from explicit BSON null. The schema upgrader's no-match
archived probe previously decoded all 299,539 cards for 20.6 seconds; its
equivalent SQLite scan takes 20 milliseconds and needs no additional index.
The DEBUGSPEED launcher now waits for FerretDB to finish one-time database
preparation and accept connections before starting Meteor, reporting progress
every ten seconds and failing clearly if FerretDB exits or exceeds its bounded
readiness timeout. This prevents OpLog startup from crashing WeKan while an
index-format migration is still running. During that wait, the normal WeKan
port serves the existing blue migration page with the Admin Panel Product name,
a rebuilding-indexes explanation and automatic refresh. FerretDB publishes its
current database, collection and index atomically, and the page turns that into
a progress bar, percentage, current step and total, elapsed time, and estimated
time remaining. A configured Product name replaces both application names in
all user-facing migration text. The standalone page is stopped and its port
released before Meteor starts; source runs keep the Product name current in
their writable cache just as Snap already does.
Launcher tests pin both modes, while FerretDB unit tests and benchmarks cover
projected, distinct and complete decoding, malformed input, cache eviction,
SQL filtering and missing keys, numeric and logical fallback safety, implicit
and excluded IDs, and every query field retained for filtering and sorting.
and improves the following client performance behavior:
Card date badges - one shared clock replaces per-card timers.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ebf1ca6143b5b5043f966461aea35ac553aae497">Share one minute ticker between card date badges</a>. Thanks to xet7.</summary>Card dates and date custom fields subscribe to one reactive clock. Each view unsubscribes when destroyed; duplicate cleanup is harmless, the interval remains for other subscribers, and the final unsubscribe stops it. Regression tests exercise teardown and later restart as well as rejecting per-badge intervals.
</details>and fixes the following minicard bug:
Minicard composers - saving targets the form the user submitted.
<details> <summary><a href="https://github.com/wekan/wekan/commit/dbbc2fd9e">Save the title from the submitted top or bottom composer</a>. Thanks to xet7.</summary>Each list renders independent add-card composers above and below its cards, but the bubbled submit handler read the first textarea under the whole list. Saving the visible bottom composer therefore read the hidden top composer's empty title, closed the form and sent no database insert. The handler now resolves the actual submitted form and obtains its title, position, labels, members and custom fields from that same form. Regression coverage pins both positive form targeting and the absence of list-wide first-textarea lookup; the existing Playwright add-card flow covers the rendered save behavior.
</details>and fixes the following board creation bug:
Shared board forms - every rendered variant submits through its owner.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e1fa6ce71">Create Board submits from the shared rendered form</a>. Thanks to xet7.</summary>The All Boards and popup variants render one included form, but their submit events remained attached to the parent templates. Blaze scopes an event map to the template that rendered the matching DOM, so clicking Create never called the server method and no board reached MongoDB or FerretDB. The shared form now owns its events and receives its state owner explicitly, preserving visibility, template-board and header-star behavior. Positive and negative regression checks pin the event owner, submitted form and all four callers.
</details>and adds the following performance diagnostic tooling:
DEBUGSPEED - opt-in measurements and failure logs explain local database behavior.
<details> <summary><a href="https://github.com/wekan/wekan/commit/186035df5">Compare MongoDB and FerretDB with opt-in speed diagnostics</a>. Thanks to xet7.</summary>DEBUGSPEED=true records bounded process, event-loop and slow DDP handler
measurements in Admin Panel → Problems → Speed without recording arguments,
credentials or query values. debug-speed-server.sh starts WeKan on a chosen
localhost port with either Meteor's MongoDB or a freshly compiled FerretDB, and
keeps their raw logs together; debug-speed-test.sh drives repeatable anonymous
or authenticated browser traffic. The
matching FerretDB diagnostics
add SQLite query-shape, candidate-row and separate SQL/decode timings while keeping
small indexed lookups silent. Static positive/negative guards, FerretDB unit
tests, both database startup modes and an anonymous browser traffic run cover
the workflow.
debug-speed-server.sh now explicitly starts FerretDB at the safe info log
level and prints the exact ferretdb.log path. The
matching FerretDB change
makes DEBUGSPEED=true select that level itself, so connection warnings,
errors and existing bounded query-shape diagnostics reach the log even outside
this launcher. Debug-level wire messages remain disabled because they may
contain user data. Positive launcher and FerretDB unit tests pin the level and
path; a negative guard prevents enabling payload-bearing debug logging.
debug-speed-server.sh now owns separate process groups for WeKan/Meteor,
FerretDB, terminal log following and a resource watcher. One Ctrl-C stops and
reaps all of them, including Meteor's child processes. The watcher records
timestamped CPU, RSS, process state, load, available memory and disk space in
resources.tsv without command arguments or environments. WeKan console output
goes to wekan.log, while bounded structured timings go only to the private
wekan-debugspeed.jsonl file beside it, never to Admin Panel → Problems.
FerretDB continues to use ferretdb.log. Static positive and data-exposure
guards cover supervision, file routing and the absence of the Problems logger;
shell syntax and a live watcher smoke test cover the executable paths.
debug-speed-ferretdb.sh builds and starts the same DEBUGSPEED-enabled local
FerretDB used by the comparative launcher, but deliberately leaves WeKan and
Meteor stopped so mongorestore does not compete with application writes for
SQLite's single write lock. It preserves the existing port, state-directory,
log-level and log-directory overrides, records ferretdb.log, build output and
resource samples together, and stops all supervised processes on one Ctrl-C.
Positive tests pin the shared options and cleanup; negative tests ensure the
restore-safe launcher cannot start Meteor or enable payload-bearing debug logs.
debug-speed-test.sh now verifies Playwright's actual Chromium executable, not
only its installed npm module, and downloads the matching browser revision when
it is absent. The browser cache lives under .tools/ms-playwright, consistently
with the main Playwright configuration, instead of depending on a populated
home-directory cache. Static tests cover detection, installation and the local
cache path; a live one-client traffic run completed 17 iterations with no
failures.
and fixes the following registration bug:
Account creation - first-user detection stays bounded on large databases.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0ea301d7e">Registration checks one user instead of scanning every account</a>. Thanks to xet7.</summary>The account-creation hook needs only to know whether any account already exists so it can make the first one administrator, but it counted and decoded the entire users collection. On a restored 14,631-user FerretDB database that made registration take 55 seconds: the account and verification email were created, but the browser timed out and incorrectly displayed “Something went wrong.” A projected one-document lookup now preserves the first-administrator rule without work proportional to the instance's user count. Positive and negative regression checks pin the bounded lookup and forbid a full count in the hook.
</details>and removes the following database-specific application workarounds:
Database compatibility - ordinary selectors work without client exceptions.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ca2838ab2">Send ordinary MongoDB selectors to every compatible database</a>. Thanks to xet7.</summary>Board card scopes again use their normal null-containing $in, client and
authorization selectors remain explicit $and conjuncts, activity predicates
are not flattened, and one-branch visibility still uses $or. The matching
FerretDB backend fixes now push null and empty $in, booleans, $or, $and,
and indexed single-field or compound equality constraints into SQLite. WeKan's
tests pin the database-independent selector shapes; FerretDB's unit suite pins
the SQL constraints and index choices.
WeKan no longer replaces Meteor's resume-login handler to avoid a FerretDB
positional-projection error. FerretDB now correctly selects an array element
for a projection such as services.resume.loginTokens.$ when the query matches
services.resume.loginTokens.hashedToken, so Meteor's standard authentication
path works unchanged. FerretDB unit tests cover a matching token and a negative
no-match case.
and fixes the following client startup bug:
Language startup - bundled text renders before optional overrides arrive.
<details> <summary><a href="https://github.com/wekan/wekan/commit/92d25f750">Render bundled English before loading database translation overrides</a>. Thanks to xet7.</summary>English is already part of the client bundle, but i18n readiness still waited up to ten seconds for the optional custom-translation DDP subscription. A busy restored database therefore printed a default-language timeout even though the English data was present and usable. Bundled English now becomes ready immediately; database overrides load asynchronously and invalidate translation helpers when they arrive. Positive and negative loader tests pin the ordering, fallback and late reactive update.
</details>and fixes the following release tooling bug:
Snap releases - assembly consumes the bundle produced for the release.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4122588b3">Build snaps from the extracted release bundle</a>. Thanks to xet7.</summary>Snapcraft entered .build and extracted the archive as bundle/, but then
looked for .build/bundle/, effectively asking for .build/.build/bundle/.
That made native, variant and Launchpad snap jobs fail while copying Node.js.
The Core 24 and staged Core 26 definitions now use the correct relative path,
with a regression test that pins the working-directory transition.
and fixes the following regression-suite failures:
Test reliability - assertions follow current behavior without weakening product checks.
<details> <summary><a href="https://github.com/wekan/wekan/commit/357bdc1e9">The complete suite distinguishes defects from valid runtime behavior</a>. Thanks to xet7.</summary>Protected activity placeholders are restored in sixteen locale files, and a
repository-wide token inventory confirms that every locale now matches English.
The Traditional Chinese check accepts correct human wording while still
rejecting the wrong script. Source guards recognize the current FerretDB
$or/$and pushdown and board publication shapes, while runtime-only files and
DEBUGSPEED diagnostics are excluded from documentation and release-menu parity
checks. The lazy-board browser test allows 30 seconds for a remote update and
move: its focused Chromium run passed in 22.2 seconds, matching Firefox's valid
slow path instead of failing at the former ten-second bound.
The RuleBleed browser regression correctly received not-authorized, but its
global count by a non-unique human-readable title found a stale rule left by an
earlier interrupted run. The assertion now scopes its query to the current
fixture board. Board cleanup also removes its rules, triggers and actions, so
one browser project or interrupted run cannot contaminate the next. A source
regression pins all three cleanup paths.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Translation maintenance commands no longer pass repository filenames through a shell, resolving two CodeQL command-injection findings in standalone tooling. Launchpad snap assembly reuses completed per-architecture bundles and packs them with faster compression, reducing work on scarce builders. Large boards now show card edits and moves immediately without a browser reload. The table below is carried over from the release under this one, and is refilled from each build's provenance.tsv when this release is made.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/27395417e">Translation Git commands treat repository filenames only as arguments</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>CodeQL alerts #439 and #440 found that the translation merge and English- regression report interpolated Git revisions and locale paths into shell command strings. The supported pull path supplied a snapshot directory and did not reach these commands, but standalone use could interpret shell metacharacters from a contributor-controlled locale filename.
Both scripts now invoke Git directly with separate arguments and consume its changed-file list with NUL delimiters. Positive coverage retains normal translation restoration and regression reporting; negative coverage uses a locale filename containing shell syntax and proves neither script executes it.
</details>and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/29fd50331">Large boards refresh after card edits and moves</a>. Thanks to hmeunier95 and xet7.</summary>Lazy card loading kept each sorted, limited window as a one-time snapshot to avoid a FerretDB cursor hang. The snapshot rendered initially, but later card edits and cross-list moves did not publish changes to an already-open board, so the browser showed stale cards until a forced reload.
Each window now observes its unrestricted indexed selector and fetches a new bounded snapshot only when a matching card changes. It diffs that snapshot and publishes additions, field changes and removals without overlapping refreshes; the problematic limited live cursor remains unused. Unit coverage checks every diff outcome, including removed fields and unchanged cards, and a forced-lazy browser scenario checks a remote title edit and cross-list move without reload.
</details>and improves the following developer tooling:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c12a9c96">Launchpad assembles snaps without rebuilding completed bundles</a>. Thanks to xet7.</summary>The s390x, riscv64 and other Launchpad jobs previously downloaded bundles whose dependencies had already been rebuilt and pruned for their target architecture, deleted that completed dependency tree, installed an npm build toolchain and repeated the entire installation through the build-farm proxy.
Snap assembly now uses the no-op plugin, retains the prepared dependency tree and omits the unnecessary compiler and npm packages. Both maintained base definitions also select LZO instead of the slower default XZ compression for the large final SquashFS, trading a larger download for faster packing and cold startup. Regression coverage pins the prebuilt-bundle boundary, the absent npm commands and build packages, and the compression choice.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: HostnameBleed regression coverage now checks documented hostnames as exact parsed tokens, resolving two follow-up CodeQL findings in test-only code. Test-matrix reliability covers resumed profile languages, speech scrolling, local Node discovery and bounded reusable AppImage jobs. Translations now merge valid Transifex human work over local fills without uploading machine translations as human, and recover human values displaced after mass filling. The table below is carried over from the release under this one, and is refilled from each build's provenance.tsv when this release is made.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/5fb0a77bf">It compares hostname examples as exact parsed tokens</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>CodeQL alerts #437 and #438 reported incomplete URL substring sanitization in the HostnameBleed regression test. The findings were limited to fixed test data: no request, user input or runtime application path reached these assertions. Parsing the example list and comparing its complete tokens avoids ambiguous URL substring checks while retaining positive coverage and a negative check for the wildcard lookalikes that the original HostnameBleed fix rejects.
</details>and improves the following developer tooling:
Test matrix - local tool discovery and bounded workflows keep every stage running against the intended versions.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f011debe3">The WeKan matrix covers resumed languages, speech scrolling and AppImage timeouts reliably</a>. Thanks to xet7.</summary>Persisted profile languages now apply reactively when a stored login finishes resuming, so Chinese and Japanese accessibility checks observe the user's real locale instead of the browser default. The speech-command regression gives its focusable list a deterministic overflow boundary before testing Page Down.
GitHub does not allow timeout-minutes on a job that calls a reusable workflow,
so the release guard now recognizes that syntax and separately proves every job
inside the called AppImage workflow has its own timeout. The previously
unbounded release lookup and publishing jobs are limited to 15 and 30 minutes.
The test runner previously added only the exact Node version named by the
Dockerfile to PATH. When that version had just advanced beyond the toolchain
already downloaded under .tools, the plain import stage could not find Node
and Playwright version discovery silently selected its obsolete 1.60 fallback
for dependencies that required 1.62.1.
It now selects the newest installed architecture-matching local Node until setup downloads the exact release. A regression pins the fallback, version-aware ordering and exported path used by every subsequent test stage.
</details>and improves the translation workflow:
Translation precedence - human work stays preferred without trusting wrong-language or provenance-unknown values.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0e12a467a">Transifex human translations safely take precedence over local fills</a>. Thanks to translators and xet7.</summary>The pull previously treated every committed non-English value as human and
force-pushed restored languages, so direct machine/LLM fills could be uploaded
to Transifex under false provenance. It also compared against HEAD, allowing
an uncommitted local fill to be lost before the merge saw it.
The workflow now snapshots the complete pre-pull tree, keeps each valid
target-language Transifex value, and restores the snapshot wherever Transifex
returns English or malformed code tokens. Nothing is pushed automatically.
Known Russian-seeded Mongolian values are rejected even though both languages
use Cyrillic, and protected @PH…@ markers are restored without discarding the
surrounding human prose.
The interrupted pull merged 47 non-English human values across 29 locale files and four new source keys into eleven English variants. All available local fallbacks remain; the result has zero detected wrong-script values, placeholder markers or local translations lost to English.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/57c1fcabf">Pre-fill human translations displaced by bad Transifex values return</a>. Thanks to translators and xet7.</summary>A history audit compared every non-English value at the final pre-mass-fill boundary with the current locale files. Of 31 differences, seventeen are newer valid Spanish, Kinyarwanda or Traditional Chinese human translations and remain preferred. Fourteen older human values had regressed: Breton, Occitan, Volapük, Walloon, Wolof and Tamazight received French; Welsh retained an English word; Xhosa gained malformed prose; and three French variants received a broken placeholder example.
Those fourteen exact historical values are restored. A reproducible audit pins the reviewed boundary and keys, while the pull merge rejects the known bad fingerprints on recurrence without blocking a future corrected Transifex human translation.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/38ed4a0d4">Reviewed human values are restored after Transifex markers are decoded</a>. Thanks to translators and xet7.</summary>The first corrected live pull exposed an ordering edge case: eight known bad
Welsh, French and Xhosa values arrived with protected @PH…@ markers, so their
bad-value fingerprints did not exist until marker repair ran after the merge.
The workflow now performs the reviewed historical-human restoration after that
decoding step. The eight regressions from the run were restored, and a test pins
the required stage order. A subsequent clean live run pulled all 147 Transifex
locales, restored 4,976 local fallback values across 124 files, decoded 43
protected markers in 31 values and restored the eight reviewed human values.
It pushed nothing, left no working-tree difference and finished with zero
displaced historical translations, machine markers or detected wrong-script
values.
The bulk, single-locale and English-copy helpers all force-pushed complete local locale files to Transifex. Because those files intentionally combine human work with direct machine/LLM fallbacks, the helpers could publish provenance-unknown values as human translations and overwrite newer community work.
Removing them makes the supported workflow pull-only for target translations; the separate English-source upload remains available. Regression coverage rejects any translation shell script that invokes a target-translation push and also pins the intentional source-only command.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Archive keeps archived subtask helper boards reachable for restoration or permanent deletion, and Card locations can again be removed with their X button. The table below is carried over from the release under this one, and is refilled from each build's provenance.tsv when this release is made.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Archive - every counted archived board remains visible and manageable.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7dd3673c5">Archived subtask helper boards appear in Archive</a>. Thanks to majo1989 and xet7.</summary>Ordinary board lists deliberately hide internal helper boards whose titles are wrapped in carets. The shared client query also applied that filter to Archive, even though the archived-board publication and count included those boards, so the counter increased while the tile stayed invisible.
Archive now keeps published helper boards in its tile query, allowing their owners to restore or permanently delete them, while active views continue to hide them. Positive, negative and live browser regressions cover both sides of that boundary.
</details>Card locations - location controls act on the surrounding card.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7dd3673c5">The X button removes its card location</a>. Thanks to majo1989 and xet7.</summary>Each location row has its own Blaze data context, so the X handler previously mistook that row for the card and never reached the removal model method. It now resolves the open card, awaits removal by the location's stable id and retains the separate legacy-location cleanup path.
Source regressions prove the nested context cannot return, and a live browser test adds a map location, removes it and confirms both the rendered row and database value disappear.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/34a1cc9e1">The location-removal browser regression checks the authoritative card</a>. Thanks to majo1989 and xet7.</summary>The live test names issue 6644 and locates the seeded card by its board and title before checking its stored locations array. This keeps the final database assertion independent of fixture-only convenience fields while still proving the X removed the same location that disappeared from the UI.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: HostnameBleed, found by GitHub CodeQL, makes tenant-hostname translation coverage compare example domains as exact text. Card titles keep markdown links clickable and mouse text selection native, while comment markdown retains its per-render fallback. REST user deletion now reports the database-confirmed outcome and documents both success and missing-user responses. Translations complete Swati, begin Sesotho and raise the measured essentially-complete total to 197 while protecting target-language vocabulary and code tokens. The table below is carried over from the release under this one, and is refilled from each build's provenance.tsv when this release is made.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b5a36bd3a">HostnameBleed: tenant hostname examples are compared as exact text</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>HostnameBleed covers code
scanning alerts #435 and #436, rule js/incomplete-hostname-regexp (CWE-20),
in tests/marathiTranslationProgress.test.cjs. The test passed
a.example.com and kanban.example.org directly to RegExp, so each dot acted
as a wildcard. A wrong hostname such as aXexampleXcom could therefore satisfy
coverage that was meant to require the exact documentation example.
The affected code runs only in a translation regression test over hardcoded strings. It is not shipped in the server or browser bundle, accepts no request or user input and denies no operation, so there is no attributable runtime attempt to record in Admin Panel → Problems.
The test now uses exact includes() comparisons. Positive and negative cases
prove literal dots are required, while a repository-wide source guard detects
the reported loop-to-RegExp shape and confirms it exists nowhere else in
tracked first-party JavaScript.
and fixes the following bugs:
Card titles - links, editing and mouse selection remain distinct gestures.
<details> <summary><a href="https://github.com/wekan/wekan/commit/052aa291a">Markdown links in minicard titles remain clickable</a>. Thanks to jullbo and xet7.</summary>The fixed-width transparent edit overlay previously covered the leading half of every title, which swallowed every pixel of a short rendered markdown link. The title container itself is now the edit target, while nested viewer links receive and stop their own click before editing can open.
Keyboard access remains on the title container, and its focus semantics are preserved. Positive and negative source tests cover the event boundary; a live Chromium regression edits a title to a markdown link, clicks it, and proves that the editor stays closed.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b8528e30c">Mouse dragging in card title editors selects text</a>. Thanks to rmb82 and xet7.</summary>Card sorting and opened-card window dragging previously claimed mouse gestures that began inside a title textarea. Those editors and their controls are now excluded from sorting, and the window-drag handler returns before suppressing the browser's native selection behavior.
Source regressions pin both drag boundaries. A live Chromium test drags across an opened-card title, verifies a non-empty selection range and confirms the card window did not move.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ce5ef7fde">Card interaction regressions use stable browser targets</a>. Thanks to jullbo, rmb82 and xet7.</summary>Specs that open a card now activate the shared wrapper-link helper instead of clicking title text, whose intended action is inline editing. The Firefox mouse selection regression keeps both drag coordinates over rendered title text, and the failed-copy negative check uses the stable board canvas rather than a strict union that fails when both healthy regions are visible.
The affected title-selection, failed-copy, LTR/RTL card-panel and board-comment paths pass together in a fresh Chromium run. Firefox and WebKit remain covered by the full Docker browser matrix.
</details>Comments and Activities - one markdown render cannot blank surrounding UI.
<details> <summary><a href="https://github.com/wekan/wekan/commit/73eb57cb3">Comment markdown retains its independent rendering fallback</a>. Thanks to rmb82 and xet7.</summary>The per-render exception boundary already added in 75a23b76a catches markdown
or sanitization failures and returns safe escaped text, so one bad comment does
not abort the card comment list or Activities rendering. The reported French
emphasis, list and literal greater-than forms now run through the real shipped
markdown configuration as explicit regression coverage.
REST API - user deletion responses reflect the authoritative database result.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6acfb396e">User deletion confirms removal and reports missing users</a>. Thanks to AhmedLukman and xet7.</summary>The route previously discarded removeAsync's result and returned the requested
id even when no account matched. It now returns 200 only when exactly one user
was removed, returns a deterministic 404 for zero matches and treats unexpected
counts or database failures as internal errors.
Repeatable response annotations let the OpenAPI generator document the exact 200 and 404 response bodies. Unit and negative tests cover authorization and every removal outcome, regenerated YAML matches the route source byte for byte, and a live Chromium API test confirms deletion in MongoDB before repeating the request and receiving 404.
</details>and improves the following translations:
Translations - Swati reaches completion, Sesotho begins and measured completion assertions stay current.
<details> <summary><a href="https://github.com/wekan/wekan/commit/87d527c89">Swati is complete</a>. Thanks to xet7.</summary>The remaining 417 Swati strings now cover jobs, storage, migrations, backups,
privacy, monitoring, repositories and imports. A same-script vocabulary audit
also replaces the Zulu word ibhodi with the Swati libhodi.
Regression coverage proves the locale has no English placeholders, preserves the exact source placeholder and HTML inventories and uses Swati vocabulary. These direct translations have lower confidence than reviewed human work and remain open to correction by Swati speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ac8006417">Sesotho translation begins with core board activity</a>. Thanks to xet7.</summary>The first 50 Sesotho strings translate activity titles and descriptions, members, comments, dates, attachments, labels, checklists, custom fields, archives and imports. Source placeholders remain exact, and a language-specific regression checks Sesotho vocabulary while recording the 2,117 strings still untranslated at this commit.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fd694a164">Translation completion assertions match the measured files</a>. Thanks to xet7.</summary>The translation documentation now reports 197 essentially complete locales, and the Northern Sotho regression expects its measured zero remaining English placeholders. Tests calculate these values from the locale files so stale completion claims fail visibly.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Search pagination now binds every stored query to its authenticated owner, closing cross-board card disclosure through reused session IDs. The full release now publishes AppImages automatically after its core bundles, while 32-bit AppImages distinguish an unavailable runner from a broken package and keep i686 Node within its virtual-address-space limit. Card details regain their inner gutters, including on widened desktop panels. The table below is carried over from the release under this one, and is refilled from each build's provenance.tsv when this release is made.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of SessionBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/18bcbdd0d">Search pagination sessions cannot be reused across users</a>. Thanks to crypto-nidh and xet7.</summary>The nextPage and previousPage DDP publications accepted a client-supplied
global-search session ID, loaded its stored board selector without checking its
owner, and could return private card titles, descriptions and custom-field
values to a logged-out or different user. The session ID had to be obtained
first, but neither publication enforced the authentication boundary.
Both paths now refuse logged-out clients and perform one owner-scoped lookup using the authenticated user and session ID together. Missing or foreign sessions complete without publishing data. Attributable logged-out probes are folded into a bounded SessionBleed summary in Admin Panel → Problems; ordinary authenticated pagination is never logged. Positive decision tests, a negative whole-publication scan and a live Chromium DDP test cover the boundary.
</details>and fixes the following bugs:
AppImage packaging - 32-bit images start safely and report runner limits.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e8b6003e7">The 32-bit AppImages pass the right startup checks</a>. Thanks to xet7.</summary>The i686 launcher no longer gives a 32-bit Node process a 4 GiB V8 heap that
exhausts its virtual address space while deserializing the startup snapshot.
Automatic selection is capped at 1 GiB for an ELF 32-bit Node, while an explicit
NODE_OPTIONS remains authoritative.
The armhf workflow now probes the Node binary inside the image instead of only
the outer AppImage runtime. A runner whose kernel can execute the wrapper but
lacks /lib/ld-linux-armhf.so.3 therefore uploads the package with an honest
unchecked warning; a runnable Node must still pass the real HTTP smoke test.
Positive and negative plain-Node coverage pins the heap ceiling, override,
inner-runtime probe and smoke-test ordering.
The v11.15 rerun proved that fixing the bundle launcher alone was insufficient:
an AppImage wraps the ZIP already attached to that release, so its bundled
start-wekan.sh still predated the fix and i686 again died with GC during deserialization.
The generated AppRun now supplies the same 1 GiB V8 ceiling for i686 and armhf
before invoking the bundle launcher. This makes a missing AppImage safely
rebuildable around an older published ZIP, while ${NODE_OPTIONS} supplied by
an administrator still wins. Regression coverage pins that old-bundle boundary
and the architecture substitution.
release-all.yml built and published the core ZIP bundles but never called the
reusable AppImage workflow, leaving AppImages to a separate manual or
missing-assets run. It now calls AppImage.yml immediately after the core
GitHub Release has attached its amd64 and arm64 ZIPs. The exact new tag is
passed explicitly, release-asset write permission is scoped to the call, and
the work runs in parallel with the other post-release formats.
Regression coverage pins the release dependency, reusable-workflow path, tag, publish flag and permission, while the workflow-interface guard proves the called workflow exists and accepts every supplied input.
</details>Card details - visible content stays inset at standard and custom widths.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b26c6d3c5">Opened cards apply their side gutters to the visible content</a>. Thanks to rmb82 and xet7.</summary>An accessibility markup change accidentally made .card-details-canvas an
empty sibling of the card header and body. Its computed 20-pixel padding still
looked correct, but no visible content was inside it, so descriptions and custom
fields touched the panel edges.
The canvas once again owns the whole card body. Positive and negative source coverage pins that ownership and rejects the empty-sibling structure, while the Playwright regression reproduces an 1100-pixel desktop panel and verifies the actual content-to-edge gaps rather than trusting the wrapper's computed style.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Three authentication boundaries now prevent OIDC claim-based account takeover, logged-out membership discovery and account-recovery floods. Blocked metadata probes and recovery throttles appear in Problems. Nine translations are now complete, replacing 18,653 English placeholders while preserving human translations, format tokens and markup exactly. Cross-board card moves retain newly created destinations and assign stable positions.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUES:
OIDC identities - access-token metadata cannot replace trusted account identity.
<details> <summary><a href="https://github.com/wekan/wekan/commit/12f2fddc2">Whitelisted token claims cannot overwrite service-owned fields</a>. Thanks to crypto-nidh and xet7.</summary>ClaimBleed blocked access-token
claims from replacing the trusted userinfo id, username, email and related
service-owned values, which could otherwise link an attacker's OIDC session to
a victim. Prototype keys and WeKan's token bookkeeping are protected too,
while ordinary whitelisted profile metadata continues to merge. There is no
Problems event because every legitimate OIDC login uses this path and a bad
claim cannot be distinguished from an administrator's mistaken whitelist.
Positive behavioral coverage pins safe metadata and a negative whole-package
scan prevents any bulk assignment into serviceData.
User metadata - sensitive membership and authentication fields require login.
<details> <summary><a href="https://github.com/wekan/wekan/commit/12f2fddc2">Logged-out clients cannot subscribe to authentication metadata</a>. Thanks to crypto-nidh and xet7.</summary>MembershipBleed made the
user-authenticationMethod publication return ready without querying whenever
the DDP connection has no authenticated user. This closes enumeration of
authentication methods, teams and organizations. Denied probes are safely
folded into one MembershipBleed summary in Admin Panel → Problems with their
source address. Positive ordering coverage pins the guard before the query, and
a negative scan requires every sensitive user publication to retain a login
boundary.
Account recovery - unauthenticated email and token methods are bounded by address.
<details> <summary><a href="https://github.com/wekan/wekan/commit/12f2fddc2">Recovery and verification DDP calls are rate-limited</a>. Thanks to crypto-nidh and xet7.</summary>ResetBleed limits each source
address to five forgotPassword or resetPassword calls and ten verifyEmail
calls per minute, stopping unbounded email floods, enumeration loops and token
guessing. Only refused requests are folded into a ResetBleed Problems summary;
ordinary recovery use is never logged, and a logging failure cannot weaken the
denial. Behavioral tests cover allowed and refused decisions, while negative
source coverage prevents any recovery method from escaping the shared callback.
and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/04bd60dd3">Move Card keeps newly created destination boards selectable</a>. Thanks to rlach and xet7.</summary>The Move Card and Copy Card dialogs now own an authorization-filtered live destination subscription and query its Minimongo results reactively. A board created through WeKan therefore remains in the selector instead of disappearing behind a cached empty result. The #2494 regression creates the destination board, list and cards through the visible UI, uses the real Move Card dialog, verifies finite unique sort values in the database, and confirms every card remains visible in Chromium, Firefox and WebKit. A negative publication test also pins active membership and excludes unrelated public boards.
</details>and completes the following translations:
Hausa - every interface area now uses Hausa instead of English placeholders.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4f0c64191">The remaining Hausa interface is translated</a>. Thanks to xet7.</summary>Across 27 direct-translation batches, 1,317 remaining values gained Hausa activity, board, card, automation, account, search, report, storage, migration and problem-management vocabulary. Whole-locale regression coverage verifies that no English placeholders remain and that named and formatted tokens and HTML tags match English exactly; the human-preference suite proves a future Transifex translation still replaces a direct fill while existing human work cannot be overwritten.
</details>Haitian Creole - the complete interface is available in Kreyòl ayisyen.
<details> <summary><a href="https://github.com/wekan/wekan/commit/eec23e770">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Haitian Creole wording for activity, workspaces, cards, imports, automation, search, administration, storage and migrations. Whole-locale regression coverage pins zero remaining placeholders and exact token and markup inventories, while the shared negative tests protect human translations from direct fills.
</details>Icelandic - every interface area now has Icelandic wording.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0caf1e74b">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Icelandic activity, board, import, automation, search, reporting, storage and migration terms. The locale regression proves completion and exact code-token and markup preservation; shared negative coverage ensures fills cannot replace human translations.
</details>Inuktitut - the complete interface is available in Inuktitut syllabics.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e84873cea">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Inuktitut wording spanning the full board, card, automation, search, administration, storage and migration interface. Regression coverage checks zero remaining English values, preserves the syllabic-script requirement and compares every code token and HTML tag with English, including negative human-preference tests.
</details>Javanese - every interface area now uses Javanese.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d2e027588">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Javanese activity, workspace, board, import, automation, search, report, storage and migration vocabulary. Whole-locale regression coverage pins completion and exact placeholders and markup; shared negative tests protect human translations.
</details>Kazakh - every interface area now uses Kazakh and its expected script.
<details> <summary><a href="https://github.com/wekan/wekan/commit/93f9ad02c">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Kazakh wording throughout activity, boards, imports, automation, search, reporting, storage and migrations. The locale regression verifies completion, Cyrillic vocabulary and exact code-token and markup inventories; shared negative tests continue protecting human translations.
</details>Greenlandic - the whole interface is now available in Kalaallisut.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8edba0949">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 44 direct-translation batches, all 2,167 placeholders gained Greenlandic activity, board, import, automation, search, reporting, storage and migration terms. Whole-locale coverage verifies zero English placeholders and exact token and markup inventories, with shared negative tests preventing replacement of human translations.
</details>Kannada - every interface area now uses Kannada and its native script.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9f2bcb319">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 62 direct-translation batches, all 2,167 placeholders gained Kannada wording for boards, cards, imports, automation, search, administration, storage and migration operations. Regression coverage verifies completion, Kannada script and exact token and markup preservation; shared negative tests protect human translations from direct fills.
</details>Konkani - the complete interface now uses Devanagari Konkani.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4d65e2773">Every remaining interface value is translated</a>. Thanks to xet7.</summary>Across 56 direct-translation batches, all 2,167 placeholders gained Konkani activity, board, import, automation, search, reporting, storage, migration and problem-repair vocabulary. The locale regression verifies completion, Devanagari vocabulary and every code token and HTML tag against English; shared negative coverage ensures direct fills cannot overwrite human translations.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Card details retain their side gutters so content no longer crowds either edge. Every shared date editor opens centered in the visible viewport, and its Date and Time controls shrink within the available width instead of creating a horizontal scrollbar. Ordinary menus remain anchored to the controls that open them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
Card details - opened cards and their date editors remain comfortably inside their visible bounds.
<details> <summary><a href="https://github.com/wekan/wekan/commit/548add4fe">Side gutters remain visible and date editors open centered without horizontal scrolling</a>. Thanks to Alishara and xet7.</summary>The card canvas now uses border-box sizing so its 20-pixel inline padding is part of its available width instead of allowing content to consume the gutters. All seven shared date editors open at the horizontal center of the visible viewport, using their rendered 400-pixel shell width in the offset calculation. Their Date and Time flex columns may shrink, and their controls fit the columns, removing the bottom scrollbar introduced when those controls were placed on one row.
Pure geometry coverage checks every date editor while scrolled, positive CSS coverage pins the shrink and gutter contracts, and the existing ordinary-popup tests ensure menus remain anchored to their controls. A Playwright regression opens a card and its due-date editor, then checks both gutters, exact centering and the absence of horizontal overflow.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: FerretDB closes its CodeQL findings while preserving required legacy compatibility. The Board Table view is responsive, sortable and can group cards by swimlane. Rules save date and member actions again, card details close when their card disappears, legacy minicards retain their creator choice, email notifications use recipient languages, and mobile Search returns directly to the board. Speech scrolling targets lists and cards. Private linked cards remain usable without exposing their source boards. LDAP/OIDC account linking, login boundaries, provider endpoints and group restrictions now preserve account data, resolve provider URLs consistently, use directory authentication consistently and fail closed when required. Sandstorm member cleanup distinguishes WeKan visibility from grain access, and Snap database settings preserve external URLs and restore safely.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
FerretDB - query values remain fixed-width until their bounds are proven.
<details> <summary><a href="https://github.com/wekan/FerretDB/commit/298216d7">Remaining CodeQL alerts are closed</a>. Thanks to xet7.</summary>Readiness probes redact MongoDB credentials before any URI reaches a log.
Aggregation array, string, date and conversion operators keep client-provided
BSON numbers fixed-width until their bounds are validated, including on 32-bit
builds. Developer-tool URL expressions match the complete github.com hostname
literally. The MongoDB-mandated MD5 preparation inside legacy SCRAM-SHA-1 is
narrowly documented and suppressed rather than changed incompatibly; new
deployments retain SCRAM-SHA-256 as the stronger option. Native unit tests,
extreme-value regressions, 32-bit cross-compilation, the FerretDB build, vet and
the affected tools-module tests pass.
Numeric aggregation type codes are compared without narrowing, every $range
result is checked at its int32 conversion boundary, and array and code-point
indexes retain int64 results when they exceed BSON's int32 range. The legacy
SCRAM-SHA-1 compatibility path now performs its MongoDB-mandated MD5 preparation
as one direct digest, placing its narrow CodeQL exception on the reported
operation. Native boundary tests, vet and 32-bit cross-compilation pass for
alerts 6, 11 and 38 through 42.
Aggregation indexes are parsed directly at the requested BSON width instead of narrowing an architecture-dependent native integer. MongoDB's mandatory legacy SCRAM-SHA-1 password preparation carries both current and legacy query-specific CodeQL annotations at the exact MD5 digest operation, documenting why replacing it would reject protocol-compatible credentials rather than improve security. Native boundary tests, vet and 32-bit cross-compilation pass for alerts 43 and 44; the MongoDB-generated authentication vectors remain unchanged.
</details>and adds the following new features:
Board Table view - a responsive overview adapts to each reader's workflow.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ad6a7b7c8">It uses the browser width and offers compact or wrapped card titles</a>. Thanks to rmb82 and xet7.</summary>Card, List and Swimlane cells no longer stop growing at a hardcoded 250 pixels. The table consumes its available container width, and card titles default to a single ellipsized line whose native tooltip retains the complete text. A toolbar control opts into wrapping, with the choice stored separately for each user. Unit coverage checks defaults, persistence and user isolation; a browser test checks width, tooltip, both modes and persistence across reload.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/828cb5e53">Its column headers sort rows in either direction</a>. Thanks to rmb82 and xet7.</summary>Card, List, Swimlane, people, label and date headers now toggle ascending and descending client-side sorting. Empty date values remain last in both directions, and card title provides deterministic ordering when selected values match. Comparator tests cover text, numeric titles, dates, nulls and state transitions; a browser regression checks header toggling and indicators.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0847af8ec">It optionally groups cards under swimlane sections</a>. Thanks to rmb82 and xet7.</summary>Flat rows remain the default. A per-user toolbar toggle groups cards in board swimlane order while retaining the selected column sort inside each lane. Section headers are inserted after pagination, so they never consume any of the 25 card slots; a page beginning partway through a lane still names that lane. Unit coverage checks defaults, isolation and boundaries, and a browser scenario checks lane order, card count and persistence across reload.
</details>and fixes the following bugs:
Rules - actions save reliably and moved-from-list triggers match the correct direction.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ea9002328">Date and add-member Rules save without schema errors</a>. Thanks to rmb82 and xet7.</summary>The three affected card-action handlers copied their generated description into
both the Action and its schema-bound Rule, although desc belongs only to the
Action. SimpleSchema rejected the unknown Rule key and returned the user to the
add-rule screen. Current-date, remove-date-value and add-member actions now
match every working handler by storing desc solely on the Action. Positive and
negative source coverage protects all three branches, and a Playwright flow
checks that the UI saves a date Rule without desc while its Action retains the
description.
The reported checklist, email, member-removal and date-action failures were already corrected by earlier changes. The remaining moved-from-list case is also fixed: its trigger resolves the activity's old list and matches a move away, while rejecting a move into that list. The existing rule-engine suite now names issue 1972 on both the positive and negative cases so that the historical report cannot silently regress.
</details>Lists and swimlanes - color choices persist behind prefixed reverse proxies.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4c5332419">Their color forms stay on the board while saving under a URL path</a>. Thanks to RowhamD, cautiouscoyote and xet7.</summary>Firefox treated the popup's Save buttons as implicit form submissions and
navigated before the list or swimlane mutation completed, which was most
visible when ROOT_URL included a path prefix. The existing handlers now
prevent that navigation on both the click and submit paths. Four focused
positive and negative checks pass. A production-mode Meteor server under
/path compiled and started, and live Chromium changed both stored colors
without leaving the prefixed board URL. The Firefox browser regression is
registered but could not run locally because its Playwright binary is absent.
FerretDB and the service-worker 404 are unrelated to these client mutations.
Minicards - upgrades preserve whether creator avatars are displayed.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6af6db72d">Legacy boards keep minicard creators hidden until explicitly enabled</a>. Thanks to tamasberesoebb and xet7.</summary>Boards created before the dedicated minicard creator setting existed no longer inherit the separate opened-card creator setting in the Card Settings popup. Their checkbox and rendered cards now both remain off by default, while an explicit click persists the opt-in and shows the creator. Five focused positive, negative and wiring checks pass. A live Meteor development stack compiled and started, and Chromium reproduced the legacy missing field, checked the initial UI state, enabled it and observed the avatar appear. FerretDB only stores the board setting and is not involved in its interpretation.
</details>Filters and bulk selection - cached cards remain confined to the board being edited.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ec9410a3a">The cross-board filter-selection report is closed with its existing regressions</a>. Thanks to Gobliins and xet7.</summary>The earlier board-scoping fix constrains both the filter-to-selection query and every subsequent bulk action to the current board. Its positive, negative and end-to-end selector tests reproduce two boards whose cards share the filtered member, then prove that the cached card from the other board is neither selected nor mutated. This closing commit records that verified fix against issue 2306.
</details>All Boards - tiles stay stable and every board-creation path sees templates.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4f702d535">The legacy-board card-counter report is closed with its existing regressions</a>. Thanks to Meeques, helioguardabaxo and xet7.</summary>The earlier startup-schema and tile-data fix restored card counters and member avatars without reactive cursor loops. The current suite verifies enabled and disabled counters independently, treats a missing legacy flag consistently as disabled, keeps boards and their lists isolated, and confirms that the template uses the resolved per-board settings. This closing commit records that verified fix against issue 4825.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/02a3630e2">Per-list board details are confirmed restored without reactive tile movement</a>. Thanks to SmartPhoneLover, helioguardabaxo and xet7.</summary>The non-reactive tile-data implementation already restored the optional list names and card counts that were removed to stop board icons moving beneath the pointer. The positive regression now names issue 4959 and confirms that an enabled board receives every list in board order. Disabled, legacy and cross-board negative cases remain covered by the same suite.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3cdc64d14">The top-bar plus creates boards from only the current templates</a>. Thanks to Jimmi61 and xet7.</summary>The top-bar Create Board control is restored and now classifies its nested
picker as a board-template search, just like Add Board on the overview. The
picker remains open while its template-container subscription becomes ready,
instead of returning early with stale or empty data. Focused coverage protects
both entry points, archived filtering and custom-field remapping. Live Chromium
shows only the active Std template, excludes an archived old template and an
ordinary card, creates the board and verifies its custom-field definition.
The Admin Panel - People paging stays consistent with stored account data.
<details> <summary><a href="https://github.com/wekan/wekan/commit/68e370226">The inconsistent People-list report gains exact paging and field regressions</a>. Thanks to jas01, dvschuetz and xet7.</summary>The current server-side People paging explicitly names every page and the table renders only those users in the server's stable creation-time order. Each row also receives its email, creation time and authentication method. Positive and negative coverage now names issue 4897 so unrelated cached user documents cannot make rows disappear, duplicate or display different account data while an administrator scrolls or pages.
</details>LDAP accounts - directory data, access filters and transport settings stay explicit and valid.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7f12b0a59">Multi-valued email attributes are decoded before account creation</a>. Thanks to mlang38 and xet7.</summary>LDAP servers can return one email as a Buffer or several aliases as an array of Buffers. Expanding a Buffer produced numeric byte values, while preserving an array passed binary objects into Meteor's string-only email schema and rejected the first login. WeKan now decodes every shape to UTF-8 text, keeps all aliases, uses the first as the account email, ignores empty values and removes duplicate aliases case-insensitively. Nine focused positive, negative and wiring tests pass, all LDAP tests pass, the package compiles in the live Meteor stack, and the related live Chromium suites pass 19 tests with one opt-in scenario skipped.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/433348c66">Incomplete group filters name their missing settings and fail closed</a>. Thanks to fgoe and xet7.</summary>Enabling LDAP group filtering without its member attribute could omit the only clause tying a group search to the current user. The directory then returned every group, and the logs did not explain which setting was absent. Every group consumer now requires the identifier, member attribute and member-value format; the login restriction additionally requires an allowed group. Missing values are named in the error and produce no groups or a refused login before an LDAP search begins. Six focused configuration and wiring tests pass, the full LDAP suite remains green, and the package compiles and starts in the live Meteor stack. FerretDB is not involved in the pre-login directory searches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/139bf4341">Admin-only and multiple LDAP groups retain exact login filtering</a>. Thanks to Hellnino18 and xet7.</summary>The existing fix already ORed ordinary login groups with admin-sync groups, so an administrator no longer had to be duplicated into the ordinary-user group. The closing audit now parses that combined list once, removes blank and case-insensitive duplicate names, escapes every LDAP assertion value and refuses a blank-only list before it can become a broad directory query. Nine positive, negative and wiring cases cover admin-only users, multiple groups, disabled admin sync and malformed lists; the full LDAP suite passes and the package compiles and starts in Meteor. A live bind still needs an external LDAP directory; FerretDB is not involved.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88d37ae98">REST email login rejects stale local passwords on LDAP accounts</a>. Thanks to akitzing and xet7.</summary>The existing Meteor login-attempt guard already rejected a migrated LDAP account's retained local password in browser logins. The REST email form did not use Meteor's login hooks and could still compare that stale bcrypt hash directly. It now applies the same guard first, performs dummy bcrypt work and returns the route's uniform failure. Fourteen guard, eight REST LDAP, seven timing and ten throttle tests pass. In the live Meteor stack, the same valid password receives HTTP 401 for an LDAP account and HTTP 200 for a local account. FerretDB is not involved in authentication.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7145d7047">LDAP to OIDC linking retains the complete existing account profile</a>. Thanks to funakura and xet7.</summary>The earlier merge fix updates an LDAP account with verified OIDC identity data instead of replacing its profile, so avatars, template board and swimlane references, language and preferences remain present. The behavioral regression executes the real account-creation hook and now also proves that existing login sessions, LDAP service data and organization membership survive. Seven profile merge, eleven concurrent-login, twelve login-style and three CAS boundary tests pass, and the full app compiles and starts in Meteor. A real provider handshake still requires external LDAP and OIDC services; FerretDB is not involved.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/433455e0e">Background sync updates LDAP profiles and disables only confirmed removals</a>. Thanks to RowhamD, q16marvin and xet7.</summary>Existing LDAP accounts are re-found by their stored identifier attribute, with the configured search field as a fallback, so later username, full-name and email changes are written instead of stopping after the first import. The opt-in authoritative mode disables a user only when a successful directory search returns zero entries and re-enables that user on reappearance. Missing identifier configuration and ambiguous matches now abort safely rather than masquerading as deletion. Eight new background-sync and eleven identifier filter tests pass, every LDAP plain-Node suite remains green, and Meteor compiles and starts. A live directory sync still requires an external LDAP server; FerretDB only stores the resulting user document.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/148525195">LDAP groups returned by broad searches are not imported as users</a>. Thanks to MiteNy and xet7.</summary>A base DN containing both people and groups could return Active Directory,
OpenLDAP or POSIX group objects when LDAP_USER_SEARCH_FILTER was empty. Their
name plus the configured default email domain was enough to create spurious
WeKan accounts. Bulk import now skips known group object classes and account
creation independently rejects them, case-insensitively and with string,
Buffer or array attributes. Standard and custom user schemas remain accepted.
Six focused positive, negative and wiring tests and every LDAP plain-Node suite
pass, and Meteor compiles and starts. The guide also shows appropriate user
filters. FerretDB only stores accounts after this validation.
The maintained LDAP guide now recommends true for immediate TLS (LDAPS),
starttls for an upgraded connection and false only for an unencrypted
connection. It removes the obsolete claim that STARTTLS is inherently more
secure than LDAPS and updates the FreeIPA example. Legacy ssl and tls
values remain compatible with explicit deprecation warnings. Twenty-five
positive, negative, wiring and documentation tests pass, and the package
compiles and starts with Meteor's development MongoDB. A real TLS handshake
still needs an external LDAP directory; FerretDB is not involved.
The REST route called Meteor's private bcrypt checker directly, so every LDAP account without a duplicated local password failed before WeKan's LDAP handler could run. Existing and first-time LDAP username logins now use Meteor's server-side login-handler API, then receive the same REST token as local users. Local-password accounts, uniform failure messages, throttling and two-factor checks retain their existing paths. Seven focused positive, negative and wiring tests and 28 related authentication tests pass; the full app compiles and all 25 live REST API browser tests pass. A real LDAP bind still requires an external directory fixture and was not available locally. FerretDB is not involved in credential validation.
</details>OIDC login - provider endpoint settings work as either relative paths or complete URLs.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aee94c263">Spring and complete HTTP provider endpoints resolve correctly</a>. Thanks to Esther125 and xet7.</summary>Authorization, token, Oracle token and UserInfo requests now share one endpoint
resolver. Complete HTTP and HTTPS URLs remain intact, while relative paths are
joined to the provider URL without duplicate slashes. The maintained guide
documents Spring Authorization Server's /oauth2/authorize, /oauth2/token
and /userinfo defaults and the exact WeKan callback. Six endpoint and 30
related OIDC/CAS tests pass, and the live Meteor app compiles and starts. A
complete Spring handshake still requires an external authorization server;
FerretDB is not involved in provider communication.
Language and fonts - CJK text follows the reader's selected locale.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6aa455fb6">Chinese text retains a neutral fallback instead of Japanese glyphs</a>. Thanks to ZhouMoHan-work and xet7.</summary>The Japanese-specific Yu Gothic and Meiryo defaults were removed in 2017,
and the current client also synchronizes the document language with the selected
locale while retaining a validated per-user font override. Positive and
negative source coverage protects all three boundaries. A live Chromium
regression selects Chinese, requires html[lang="zh-CN"] and rejects both
Japanese fonts from the computed default stack.
The earlier scoped CJK wrapping fix keeps Japanese words such as 追加 (Add)
and リンク (Link) intact while allowing the footer to wrap between controls.
Nine positive, negative and scope checks prevent either mid-word CJK breaks or
a global rule that would damage Latin and long-URL wrapping. A live Meteor
development stack compiled and started, and Chromium selected Japanese, opened
the real add-card composer and measured every translated control as one line.
FerretDB is not involved in client layout.
Accessibility - independently scrolling board regions can be targeted without a mouse.
<details> <summary><a href="https://github.com/wekan/wekan/commit/fb10b6160">Lists and opened cards are named, focusable scroll regions</a>. Thanks to ProdLead and xet7.</summary>Each list body and card-details window now exposes the standard region role,
a translated accessible name and keyboard focus. Speech-recognition software
can therefore target the intended scrollbar instead of guessing among the
several independent regions on a board, and keyboard users gain the same
control. Four focused positive, negative and wiring checks pass. A live Meteor
development stack compiled and started, and Chromium focused overflowing list
and card regions and scrolled each with Page Down. A Dragon installation was
not available for its proprietary voice-command layer. FerretDB is not involved
in client focus or scrolling.
Container logging - startup remains independent of obsolete transports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/cae55dae4">The removed Winston logger can no longer be mistaken for a Docker option</a>. Thanks to zagoa, huneau and xet7.</summary>The Winston/Zulip database observer that replaced Meteor's default console transport was removed in 2017, restoring normal database startup. Docker guidance no longer advertises that obsolete integration. Focused regressions protect the removed startup files and dependencies while requiring the active container database URL; live Meteor runs also started logging and MongoDB together.
</details>REST authentication - clients can invalidate bearer tokens explicitly.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6e4e1a5db">REST logout proves that a revoked token immediately loses access</a>. Thanks to Gobliins, MonkeyNinja, AbdullahAlAsad and xet7.</summary>The existing logout endpoint revokes either the presented token or every session with authenticated-user scoping. All 12 positive and negative planning cases pass. A new end-to-end API regression logs out with a real bearer token, verifies its removal from the user's resume tokens and requires that same token to receive HTTP 401 on its next request.
</details>The board archive - card actions stay visually attached to their card.
<details> <summary><a href="https://github.com/wekan/wekan/commit/41d6a8a28">The grouped Restore and Delete action report is explicitly closed</a>. Thanks to fxkr and xet7.</summary>The earlier fix wraps every archived card together with its own Restore and Delete links and separates that complete group from the next card. Structural, spacing, hover, narrow-sidebar, RTL and theme-neutral regressions all pass, and the browser restore flow remains registered. The negative case prevents the old equal spacing above and below the actions from returning.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d6cf4016d">Swimlane archive and restore are confirmed to preserve their cards</a>. Thanks to Maevabus, chirrut2 and xet7.</summary>The existing cascade archives a swimlane's active cards with the lane and restores exactly those cards later. Eleven positive, negative, legacy-data and repeated-round-trip cases pass: cards archived independently remain archived, other lanes remain untouched and no active card can be stranded beneath an archived swimlane.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/97e7b3158">Its item collection stays separate from All Boards</a>. Thanks to ocdtrekkie and xet7.</summary>The current board menu opens its card, list and swimlane archive in the board sidebar, while archived boards remain a distinct section of All Boards. The strengthened 12-case navigation suite now names issue 1280 and prevents those destinations from being conflated again. The archived-card browser restore flow also remains registered.
</details>Board members - invitation decisions and team grants stay consistent.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f861c92de">The removal, decline and team-access report is explicitly closed</a>. Thanks to UCS, rainbowjaw, DVNBLMHC and xet7.</summary>The earlier invitation fix deactivates removed or declining members, clears the pending invitation and refuses to reactivate anyone who no longer has a real invite, while preserving the valid acceptance path. All 11 lifecycle cases and 16 team-to-board propagation cases pass. Browser coverage also registers both administrator removal and the negative non-admin permission path.
</details>Subtasks - their own stored board remains a usable navigation target.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d6eaaa737">The missing subtask board-ID navigation report is explicitly closed</a>. Thanks to xet7 and TNick.</summary>The existing navigation helper derives the card and board route from the subtask itself and falls back to its stored board ID when a cross-board deposit board is not loaded in the browser. Eleven loaded, unloaded, plain-document and malformed-data cases pass; truly broken references warn instead of throwing or routing to an undefined card.
</details>Linked cards - safe snapshots remain usable across board boundaries.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9cdbe1a53">The private-source visibility report gains an authorization regression</a>. Thanks to TiibCD, t0mcat1337 and xet7.</summary>A member of the linking board must not inherit access to the private source board. Current source already filters the real card and all children by board visibility while falling back to the linked card's stored snapshot, avoiding the historical blank and frozen view. Unit coverage pins both sides of that boundary, and a browser scenario proves an unauthorized viewer can open and close the snapshot normally without weakening source-board authorization.
</details>Board imports - old Sandstorm exports retain their cards and dates.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bb925445b">The missing Sandstorm card and date report is explicitly closed</a>. Thanks to xet7.</summary>The existing importer restores created, received, start, due and end dates and
falls back to the exported card when an old or pruned board has no creation
activity. Thirteen focused cases prove valid dates, missing fields and corrupt
values; invalid optional dates become null rather than aborting insertion and
hiding the entire card. Both WeKan import browser flows remain registered.
Old Sandstorm exports can omit the board permission. The importer now preserves
an exact explicit public or private value while missing, malformed and
lookalike values fail closed to private. Five focused positive and negative
cases pass, and a browser regression imports a permission-less Sandstorm-style
board and verifies its stored visibility.
Sandstorm grains - preserved iframes recover and board sharing works across the grain.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9bc1896ff">Hidden grains remeasure the viewport when their iframe resumes</a>. Thanks to neynah, dwrensha and xet7.</summary>A grain that finished rendering while its iframe was hidden could retain the hidden frame's narrow dimensions because returning did not reliably emit a resize. Visibility, page-show and focus restoration now coalesce into the ordinary resize path after the iframe has rendered, refreshing the sidebar, header, popups and responsive layout. Hidden-frame, timing, wiring and hot-reload regressions pass, and a live Chromium test confirms that restoring a preserved page publishes a fresh viewport measurement.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3c57cb876">Existing grain users can be added independently to every board</a>. Thanks to Mini-pen, drseussofporn, jpretori, ocdtrekkie, wellgoodthen, stoltenhoff and xet7.</summary>The member button no longer depends exclusively on the Powerbox native module, which is unavailable in the Node 24 bundle and never attached its claimed identity to the selected board. It now opens WeKan's maintained, permission-checked search for identities that have visited the grain. Local email-account creation stays hidden because those accounts cannot authenticate through Sandstorm. Focused coverage protects the Sandstorm branch and admin mapping; live Chromium adds a user to the second of two boards, leaves the first unchanged and opens the second board as that user.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9cfc1d8d">Local member removal warns that Sandstorm grain access remains</a>. Thanks to soerenk, ocdtrekkie, rautamiekka, kentonv, ertanalytics, sjoki, lslamp and xet7.</summary>Revoking a user's Sandstorm share prevents access but cannot notify the app, so the old WeKan member avatar can remain. Board administrators can remove that stale local member and all of their card roles, while a Sandstorm-only warning now states that this action does not revoke grain access and points them to the platform's Share access dialog. Four positive, negative and wiring regressions pass, the complete client compiles, and both live member-removal permission scenarios pass in Chromium. A real revoke callback cannot be tested because Sandstorm exposes none; FerretDB is not involved in grain authorization.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eca1095f2">Username collisions are confirmed safe across case and concurrent claims</a>. Thanks to mitar and xet7.</summary>The earlier Sandstorm account fix probes candidate usernames with an anchored, escaped, case-insensitive match and retries the next numeric suffix if a concurrent insert wins the unique-index race. Eleven positive, negative, race and integration-wiring regressions pass. This closing commit records the verified implementation against issue 574.
</details>Labels - sidebar drag-and-drop follows cards added after initial render.
<details> <summary><a href="https://github.com/wekan/wekan/commit/712ce25d0">Late-rendered cards remain valid sidebar label drop targets</a>. Thanks to marco-aa, ocdtrekkie and xet7.</summary>The existing reactive dependency reinitializes droppable behavior whenever a card enters the board after its lists first render. Three focused wiring cases pass. A new browser regression inserts a late card, performs a real pointer drag from the sidebar label and verifies the resulting label assignment in the database, reproducing the navigation-dependent failure from issue 1554.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6f6fe229a">Authorized source-member avatars remain visible after linked-card reloads</a>. Thanks to theonafor and xet7.</summary>The linked-source publication already sends the minimal username, name, initials and avatar fields only after source-board visibility filtering. Three field-parity and authorization checks pass. The cross-board browser regression now assigns a custom avatar to a second source member, leaves the picker subscription, reloads through the linking board and requires that image to remain rendered in card details.
</details>Card copying - duplicated conversations remain recognizable as history.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8dff96d7b">Copied comments preserve their original author and time</a>. Thanks to ocdtrekkie, 2martens, frmwrk123 and xet7.</summary>Comment authorship was already retained, and the copy dialog already prefilled the source title, but schema auto-values still replaced every copied comment's date with the copy time. Copies now re-home immutable comment data without mutating the cached source or emitting false new-comment activity. Unit tests cover preserved history and invalid input; the REST browser regression checks the original author, timestamp and destination board together.
</details>Email notifications - translated messages follow their recipient's profile.
<details> <summary><a href="https://github.com/wekan/wekan/commit/46609d3e8">French notification delivery is confirmed against the recipient-language fix</a>. Thanks to Sophalso and xet7.</summary>The earlier server-language loading fix ensures the recipient's translation bundle is present before notification subjects and activity descriptions are rendered, then carries that same language into buffered delivery. Six focused checks cover exact French prose, placeholder preservation, load order, the send-language handoff, supported-language fallback and rejection of the server default in this path; seven invitation-language tests also pass. The live Meteor development stack compiles and starts. An SMTP server was not required to verify content generation before transport, and FerretDB is not involved in localization.
</details>User invitations - email-derived accounts remain distinct and login-safe.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d3f227586">The duplicate invitee collision report gains exact regression coverage</a>. Thanks to ghost, rvalle and xet7.</summary>The production correction landed earlier in commit 0f2179988 for duplicate
issue #619: email addresses are normalized to lowercase, and a repeated local
part receives the first free numeric suffix before account creation. New tests
pin #825's exact john.doe addresses at two domains and its mixed-case account
scenario, proving they produce distinct, lowercase-login-safe usernames rather
than the old raw 403 response.
Verification emails were enabled, but the accounts package's token route was
never configured, so links fell through to WeKan's signed-in guard without
changing the account. /verify-email/:token and the companion resend route are
now installed explicitly. Routing and mail regressions pass; live Chromium
coverage proves a valid token verifies the address, is consumed and signs the
user in, while an invalid token leaves both account and session unchanged.
CAS login - first-time identities follow their own validated account shape.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ca59a68fc">A first CAS login no longer requires nonexistent OAuth service data</a>. Thanks to faelys, Meeques, jgribonvald and xet7.</summary>The shared account-creation hook assumed every login handler supplied an OIDC
services object. CAS instead supplies its validated username, verified email,
profile and authentication method as top-level fields, so a first login crashed
before insertion. OIDC normalization now runs only when that service data
exists. Focused coverage proves the missing-services path and complete CAS
identity wiring while retaining the negative conflict check that prevents a CAS
identity from silently taking over a non-CAS username. Provider validation
still requires a deployment's external CAS server.
User profiles - accounts without email metadata can add their first address.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ac0d9a3cd">An account with no email field can save its own first address</a>. Thanks to jpfleury and xet7.</summary>Sandstorm and other SSO accounts may have no emails array. Profile submission
now treats that state as an empty address instead of dereferencing a missing
element, and the server honors the configured Allow Email Change setting for
the authenticated user's own account. Global administrators retain their
existing authority; cross-user and disabled-setting writes are rejected, and
new addresses begin unverified. Focused regressions and live Chromium cover all
three authorization paths and the complete missing-field save.
Mobile navigation - full-screen panels return readers to their board.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7d67f80df">Search Back returns directly to the card wall</a>. Thanks to 1ubuntuuser and xet7.</summary>On a mini screen, Search occupies the board area, but its Back arrow used to replace Search with the sidebar Home/settings content. It now resets Search for the next opening and closes the sidebar, uncovering the board immediately. Desktop Search and every other sidebar view retain their existing transitions. Positive and negative unit coverage protects that boundary, and a mobile browser regression checks the full Search-to-board interaction.
</details>Card details - open views follow remote card lifecycle changes.
<details> <summary><a href="https://github.com/wekan/wekan/commit/951b072b5">They close when another client removes or relocates the card</a>. Thanks to rptl and xet7.</summary>A deleted, archived or cross-board-moved card no longer leaves an empty details shell open on mobile. Each details instance observes the card that created it and clears desktop windows, mobile routes and popup state when that card disappears or changes boards. Positive and negative unit coverage pins the presence decision and cleanup wiring; a mobile browser regression removes an open card directly from the database and expects the board canvas to return.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1ad5d2ba3">Opening Activities is confirmed to show persisted card history</a>. Thanks to franksiler and xet7.</summary>Activities starts collapsed to keep card opening light, but its caret now asks for the complete card history regardless of obsolete per-card visibility flags. All 26 section-state, keyboard, subscription and negative regressions pass. A new browser scenario seeds a non-comment activity, opens the card's Activities section and requires that exact persisted entry to render.
</details>Card movement - drag-and-drop remains visually stable during reactive work.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b687f3727">Dropped cards stay visible while large boards reconcile the move</a>. Thanks to mimZD and xet7.</summary>The required jQuery UI cancellation used to draw a card back in its source list until Blaze finished rendering the database move, producing the intermittent one-second flicker reported on large boards. A presentation-only copy now holds the target slot until the real reactive card arrives; it cannot receive pointer events and is removed immediately on arrival or failure, with a safety timeout. A Chromium drag regression deliberately delays the model move and verifies the target remains occupied throughout before the real card replaces the preview.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6c9ba4745">Fast touch drags move cards without opening them</a>. Thanks to xator91 and xet7.</summary>The touch bridge synthesized a click for every gesture shorter than 500 ms, even after jQuery UI had completed a sortable drag. That click followed the minicard link, making the board appear to reload with the moved card open. The adapter now suppresses only the synchronous post-drag click; normal taps and later independent clicks remain unchanged. Positive and negative unit coverage pins that boundary, and a Chromium regression performs a raw fast touch drag, checks the database move and verifies that card details stay closed.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/500452f78">Cross-board moves keep unique finite positions and every destination card visible</a>. Thanks to rlach and xet7.</summary>The synchronous client no-cache lookup already prevents destination sort
values from becoming NaN, overlapping existing cards and making them appear
or disappear during later reordering. Eight focused positive and negative
cases pass. A new browser regression moves a card into a populated list on
another board, requires three distinct finite positions and verifies that all
destination cards remain visible.
The current drag handler scrolls the list body under the pointer, refreshes sortable geometry after scrolling, and keeps the board still until the list reaches its end. Focused source regressions cover list selection, both scroll directions, the end boundary and geometry refresh. A Chromium regression drags through a long list, observes vertical scrolling and requires the persisted card position to change.
</details>Snap database restore - backup archives replace existing data predictably.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d7a225eb0">It drops existing collections and validates the supplied archive</a>. Thanks to ram19890 and xet7.</summary>The restore command retains its existing --drop behavior, preventing the
duplicate-key collisions caused by merging a backup into populated collections.
It now also requires exactly one existing file and preserves an archive path
containing spaces as one argument. Three command-level positive and negative
regressions execute the real script around a mocked Snap environment and
mongorestore; the related 13 old-database recovery checks and shell syntax
validation pass. Building and installing a complete Snap was not required to
exercise this command boundary. FerretDB is not used by this MongoDB archive
restore path.
The earlier startup fix gives an explicitly configured mongo-url priority
over the internally constructed address, including its database name, while an
empty setting still receives the local port and oplog defaults. Four executable
positive and negative regressions run the real setting reader with a mocked
snapctl and evaluate the startup selection block. They prove that both the
external database and its explicit oplog URL survive unchanged; shell syntax
validation also passes. A complete Snap install and external MongoDB were not
needed to exercise the configuration boundary. FerretDB is bypassed when this
external URL is selected.
Outgoing webhooks - activity notifications retain their translated values.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c2b7c4e8">The stale raw-placeholder report is closed against its completed canonical fix</a>. Thanks to sfahrenholz, chotaire and xet7.</summary>The canonical report, #2226, is closed and the current activity builder resolves
members, checklist items, cards, lists, swimlanes, boards and labels before the
complete parameter object reaches TAPi18n. Focused regression coverage now
protects that wiring and verifies that representative German activity strings
remain translated while preserving the exact replaceable-token inventory.
The current activity hook starts each outgoing delivery without awaiting it, and its guard resolves both synchronous and asynchronous failures instead of propagating them into the database operation. New regressions hold a receiver promise open and prove the caller continues immediately, verify rejected delivery is isolated, and pin the non-awaited Meteor callback wiring. This closes the older custom-field report against the completed non-blocking path.
</details>Apache deployments - reverse-proxy examples cannot inherit open forwarding.
<details> <summary><a href="https://github.com/wekan/wekan/commit/577ccb9f8">Both documented virtual hosts explicitly disable forward proxying</a>. Thanks to bastos77 and xet7.</summary>The abusive third-party traffic came from Apache's ProxyRequests On, not from
WeKan or its database. Both reverse-proxy examples now set ProxyRequests Off
inside the virtual host and no longer grant wildcard proxy access, preventing
an unsafe global setting from leaking into the WeKan host. The guide also
explains why ProxyMaxForwards does not close an open proxy. Three positive and
negative configuration regressions protect those boundaries.
and improves the following translation:
Fulah - direct whole-file translation using established Pulaar vocabulary.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3d02d1d87">Activity history gains its first 50 Fulah values</a>. Thanks to xet7.</summary>Board membership, comments, dates, attachments, checklists, custom fields and imports now use Fulah. Named and percent placeholders and HTML tags remain exact, and whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0efb942af">Activity summaries and workspaces gain 50 Fulah values</a>. Thanks to xet7.</summary>Card movement, activity summaries, checklist events and workspace controls now use Fulah. Named and percent placeholders, HTML tags and Markdown remain exact, leaving 2,066 values.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: FerretDB board loading and database searches do substantially less work through adaptive loading, narrower publications, bounded result windows and indexed shared discovery. LDAP profiles retain Active Directory display names even with restricted attributes. Cherokee, Central Kurdish, Chuvash, Dzongkha and Ewe translations are complete, with whole-locale integrity checks preserving code tokens.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
Board loading - bounded, indexed work for FerretDB publications.
<details> <summary><a href="https://github.com/wekan/wekan/commit/00026ff23">Board publications avoid repeated whole-collection polling</a>. Thanks to xet7.</summary>FerretDB launchers now default to adaptive card loading instead of forcing every board into eager mode. All Boards publishes only boards related to the signed-in user rather than every public board on the instance; direct public links and the Public Boards page remain available. Linked-card and parent discovery now uses two narrow indexed queries shared only among one composite parent evaluation, with no TTL or board-id authorization cache. Compound indexes cover those card queries and the common All Boards filter and ordering. Positive and negative coverage preserves assigned-only scoping, share revocation, direct public access, pagination parity and explicit loading-mode overrides.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c69bf503c">Searches and Due Cards transfer bounded result windows</a>. Thanks to xet7.</summary>Attachment and checklist discovery is restricted to the authorized board scope,
and filename searches retrieve only the cardId they consume. Text ranking
retains only skip + limit candidates instead of every match. Due Cards uses
reachable Previous/Next pages rather than an unlimited live cursor or a
truncating cap. MongoDB receives compound indexes covering lazy card windows and
due-card selection. The companion
FerretDB change selects the
declared compound filter index and bounds sorted-query memory while preserving
MongoDB BSON ordering. Regression coverage checks projections, board scoping,
pagination, index order and positive and negative ranking behavior.
LDAP profiles - directory names shown to signed-in users.
<details> <summary><a href="https://github.com/wekan/wekan/commit/59f9b3116">They keep the configured display name</a>. Thanks to Nissulya and xet7.</summary>An LDAP user-attribute allowlist could omit LDAP_FULLNAME_FIELD even though
displayName was configured separately. Active Directory then returned the
login attribute but not the full name, leaving the profile header showing
sAMAccountName. Restricted searches now merge in every configured identity
attribute, including fields inside templates, with case-insensitive
deduplication. Regression coverage requires the positive displayName request
and rejects the old allowlist-only query.
and improves the following translation:
Cherokee - direct whole-file translation in Cherokee syllabary.
<details> <summary><a href="https://github.com/wekan/wekan/commit/70875a2f8">Activity history and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card moves, membership changes, activity history, checklist events, dates and workspace controls now use Cherokee syllabary. Named and percent placeholders, HTML tags and Markdown remain exact, and whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4095accd5">Board layout and Home controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home-board controls, due and end dates, list widths, keyboard shortcuts, swimlane heights, templates, subtasks and checklist controls now use Cherokee syllabary. Percent placeholders remain exact, and whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3fa2fa8b2">Board administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administrator announcements, public boards, offline recovery, board archives, templates, attachments, backgrounds, member and assignee views and private board notices now use Cherokee syllabary. Named and percent placeholders and HTML tags remain exact, and whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a144862bc">Board views and card controls gain 50 values</a>. Thanks to xet7.</summary>Public-board notices, board movement, colors and backgrounds, desktop and mobile views, zoom, calendar and statistics views, archive recovery and card labels now use Cherokee syllabary. Named and percent placeholders and HTML tags remain exact, and whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6523fc818">Voting and imports gain 50 values</a>. Thanks to xet7.</summary>Card membership and dates, voting, Planning Poker, dependencies, organizations, teams, accounts, backgrounds and checklist, swimlane, list, card and board imports now use Cherokee syllabary. Whole-locale placeholder, markup and script coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8269f2288">Member settings and appearance gain 50 values</a>. Thanks to xet7.</summary>Member popups, archive restoration, imported-user mapping, permissions, themes, fonts, text colors, avatars and language controls now use Cherokee syllabary. Digits and whole-locale placeholder, markup and script invariants remain exact, leaving 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f61d0fd6b">Navigation, card aging and colors gain 50 values</a>. Thanks to xet7.</summary>Board and page starring, automatic list widths, card-aging tiers, keyboard-style movement, dialogs, archive recovery and twenty-three color names now use Cherokee syllabary. Whole-locale placeholder, markup and script invariants leave 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8a4e8ea31">Permissions and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment-only, read-only and worker roles, deletion confirmations, clipboard actions, linked cards, templates and custom-field types now use Cherokee syllabary. The bulk-card example remains valid JSON with exact field names, and whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9c43e8a59">Account emails and import errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, profiles, WIP limits, account enrollment, invitation, password-reset and verification emails and authorization, JSON, CSV, TSV and board-import errors now use Cherokee syllabary. Named email tokens and paragraph breaks remain exact, leaving 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/761fc439c">Card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account errors, attachment-free board export, PDF and Excel card exports, attachment metadata, list sorting and due-date, label and member filters now use Cherokee syllabary. File-format and product names remain intact, and whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dd2592766">Advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced custom-field filters, activity and imported-member states and Kanboard, NextCloud, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Cherokee syllabary. Operators, regex, API paths, JSON properties, file extensions and named tokens remain exact, leaving 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/504183ee9">Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello archive validation, workspaces, API credentials, board selection, progress, cancellation and recovery, imported-member mapping, date validation, keyboard shortcuts and label controls now use Cherokee syllabary. URLs, file extensions, format names, percent tokens and digits remain exact, leaving 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2e55fc2fa">List actions and board roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, list links, archive and deletion, user, team and organization settings, swimlane actions, calendar, login, multi-selection, muted boards, archives and assigned-only roles now use Cherokee syllabary. The board-title token and file-format names remain exact, leaving 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/99bad85a0">Privacy, search and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watched-board notices, private-page login, image previews, board visibility, member removal, card-description rescue, search, WIP limits, keyboard shortcuts, sidebars, signup and starred boards now use Cherokee syllabary. Named and percent tokens and link markup remain exact, leaving 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c1597fecf">Tracking, branding and welcome boards gain 50 values</a>. Thanks to xet7.</summary>Starred boards, subscriptions, time tracking, numeric shortcuts, upload state, custom logo, help and URL settings, imported usernames, watching, welcome and template boards, WIP limits and attachment/API limits now use Cherokee syllabary. Digits and URL/API terms remain exact, leaving 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/66bc7e464">Attachment limits, SMTP and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar blocking, registration, invitations, SMTP configuration and testing, authorization, outgoing and bidirectional webhooks and package, database, Node and Meteor metadata now use Cherokee syllabary. Invitation tokens and protocol/product names remain exact, leaving 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f6f2b1ebf">Runtime metrics and organization tenancy gain 50 values</a>. Thanks to xet7.</summary>Database and FerretDB revisions, reactivity and DDP modes, OS metrics, time units, custom-field display, account changes, visibility and organization/team templates, authentication synchronization, tenancy, domains and administrators now use Cherokee syllabary. Environment variables, hostnames and product names remain exact, leaving 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/121400d80">Card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, colors, requester and assigner labels, board and notification deletion, duplicate-list cleanup, subtask destinations, minicard fields, attachment and checklist counts, parent paths and label activity now use Cherokee syllabary. Named and percent tokens and counters remain exact, leaving 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/166388340">Automation rules and activity gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, rule creation and selection, visual workflow construction, card, label, member, checklist, attachment and scheduled triggers and JSON, CSV and Trello Butler rule imports now use Cherokee syllabary. Named and percent tokens and format names remain exact, leaving 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b0f8eead1">Scheduled automation gains 50 values</a>. Thanks to xet7.</summary>n8n and Node-RED workflow imports, scheduled and button triggers, daily, weekly
and monthly timing, due-date conditions, card/list duration, sorting, completion,
bulk movement and relative dates now use Cherokee syllabary. Named tokens,
product names, weekday notation and the literal N remain exact, leaving 1,116
values.
Card movement and archives, label, member, attachment and checklist conditions, checked states, card placement, member and label removal, colors, checklist actions and email subjects now use Cherokee syllabary. The composable rule fragments remain internally consistent, leaving 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a06cc3483">Automation and web manifests gain 50 values</a>. Thanks to xet7.</summary>Automation email, archive, labels, card creation, member, checklist and swimlane actions, relative date fields, authentication, product naming, custom head tags, web manifests, asset links, layout and member displays now use Cherokee syllabary. Comma-separated examples and HTML/JSON names remain exact, leaving 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/20307961d">Reminders and layout controls gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication, board duplication, swimlane deletion, date and due-time reminders, mentions, account, team and organization deletion, minicard labels, drag handles and editor behavior now use Cherokee syllabary. HTML and named and percent tokens remain exact, leaving 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/65c0eeeb4">Roles and calendar settings gain 50 values</a>. Thanks to xet7.</summary>Multi-card windows, Enter-key editor behavior, organization, team and user editing, notification state, rename and invitation permissions, board-role status, weekdays, ownership and linked-card deletion safeguards now use Cherokee syllabary. Keyboard combinations remain exact, leaving 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/437a622d7">Shared templates and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, people and time scopes, My Cards sorting, Due Cards and global-search views and missing board-element errors now use Cherokee syllabary. Domain examples, Markdown emphasis and percent tokens remain exact, leaving 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2e60bec01">Search operators and predicates gain 50 values</a>. Thanks to xet7.</summary>Missing organizations, teams, comments and cards, global-search result counts, search operators and date, state and card-content predicates now use Cherokee syllabary. Percent and pagination tokens remain exact, leaving 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d679f4673">Global-search guidance gains 50 values</a>. Thanks to xet7.</summary>Global-search validation, paging, operators, status filters, sorting and search examples now use Cherokee syllabary. Named tokens, Markdown, pseudo-tags and literal search examples remain exact, leaving 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7227fa01c">Dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Board and card sorting, completion, stickers, dependency graphs, board backgrounds and card locations now use Cherokee syllabary. Import counters, JSON, SVG, dimensions and sort arrows remain exact, leaving 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9dc4293d">Locations and administrator reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, swimlanes, string templates, file, security and performance reports and office-login details now use Cherokee syllabary. Commands, entities, template tokens and IP versions remain exact, leaving 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ef85bcb2c">Recovery and support requests gain 50 values</a>. Thanks to xet7.</summary>Office and REST API reports, automatic data recovery, wait indicators, organization safeguards and support-request states now use Cherokee syllabary. API settings, product names and punctuation remain exact, leaving 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9ac58afe5">Organizations and attachment storage gain 50 values</a>. Thanks to xet7.</summary>Teams, organizations, Node memory metrics, legal notices, checklist actions and attachment-storage movement now use Cherokee syllabary. URL, Node, filesystem, GridFS and S3 terminology remains intact, leaving 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e161bb31">Attachment repair and storage statistics gain 50 values</a>. Thanks to xet7.</summary>Bulk attachment movement, location repair, storage statistics and MongoDB compaction now use Cherokee syllabary. Filesystem, GridFS, cloud, MongoDB, Meteor and identifier terminology remains intact, leaving 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/15d06c361">Custom translations and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing, uploads, custom translations, checklist folding, support and accessibility settings now use Cherokee syllabary. Workspace tokens and PDF, ISO 8601, Markdown, ZIP and JSON names remain exact, leaving 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0884ace8b">Account protection and scheduled jobs gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force lockouts, administrator user filters, attachment paths and scheduled board operations now use Cherokee syllabary. Whole-locale token and markup invariants remain exact, leaving 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4680fe36f">Storage and database migrations gain 50 values</a>. Thanks to xet7.</summary>Scheduled migration recovery, filesystem, S3 and Azure storage, MongoDB and FerretDB migration and Sandstorm conversion now use Cherokee syllabary. URLs, ports, environment variables and database tokens remain exact, leaving 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b262f9661">Security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, lazy card loading, safe rich-text rendering, import/export privacy controls, activity suppression and backups now use Cherokee syllabary. HTML, environment variables, formats and identity-provider names remain exact, leaving 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0abe0ba0b">Backup scheduling and cloud storage gain 50 values</a>. Thanks to xet7.</summary>Backup restoration and scheduling and Google Cloud, AWS S3 and Azure storage configuration now use Cherokee syllabary. Time formats, JSON fields, console paths, provider names and secret labels remain exact, leaving 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/97f2ac4eb">Storage migrations gain 50 values</a>. Thanks to xet7.</summary>GridFS state, migration controls, S3 authentication, scheduled board operations and attachment monitoring now use Cherokee syllabary. MongoDB, CollectionFS, MinIO, regions and transport protocols remain exact, leaving 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6e8d52689">Board repair migrations gain 50 values</a>. Thanks to xet7.</summary>Board-integrity analysis, lost-card recovery, archive restoration and file URL repair now use Cherokee syllabary. Swimlane and list identifiers, URLs and confirmation punctuation remain exact, leaving 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d97034a3">Migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Board-conversion steps, scheduled-job monitoring, CPU and memory metrics and filesystem and GridFS statistics now use Cherokee syllabary. URLs, backend names, numeric intervals and identifiers remain exact, leaving 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0b713688a">Migration controls gain 50 values</a>. Thanks to xet7.</summary>Bulk attachment migration, throttling, monitoring and storage distribution now use Cherokee syllabary. Filesystem, GridFS, S3, units and numeric ranges remain exact, leaving 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3de527c06">Repositories and problem repair gain 50 values</a>. Thanks to xet7.</summary>OTP account access, repository management, administrator problem status and broken-card repair now use Cherokee syllabary. API, CPU and named repair tokens remain exact, leaving 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a61b14f2e">The whole-file translation is complete</a>. Thanks to xet7.</summary>Event network addresses, scoped import/export and number-search guidance now use Cherokee syllabary. IP versions, file formats, product names, named tokens and pseudo-tags remain exact. All 2,166 formerly untranslated values are now translated, with zero English placeholders remaining.
</details>Central Kurdish - direct whole-file translation in Sorani Kurdish script.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5ffd135b5">Activity and board controls gain 50 values</a>. Thanks to xet7.</summary>Board, list, swimlane, card, checklist, comment, member and archive activity now uses Central Kurdish. Named and percent placeholders remain exact, and new whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/386a70f28">Activity and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card moves, checklist history, received and start dates and All Boards workspace controls now use Central Kurdish. Named and percent placeholders and Markdown terminology remain exact, leaving 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a60036138">Workspace and layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, list widths, keyboard shortcuts, swimlane heights, templates, subtasks and checklist controls now use Central Kurdish. Percent placeholders and numeric limits remain exact, leaving 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6c11c8fe8">Board administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administrator announcements, offline recovery, archives, templates, attachments, backgrounds, member and assignee views and private-board notices now use Central Kurdish. Named and percent tokens and HTML tags remain exact, leaving 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8389975c8">Board views and card controls gain 50 values</a>. Thanks to xet7.</summary>Public-board notices, board movement, backgrounds, desktop and mobile modes, zoom, calendar and statistics views, archive recovery and card controls now use Central Kurdish. Named and percent tokens and HTML tags remain exact, leaving 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1381fd30f">Voting and imports gain 50 values</a>. Thanks to xet7.</summary>Card membership and dates, voting, Planning Poker, dependencies, organizations, teams, backgrounds and checklist, swimlane, list, card and board imports now use Central Kurdish. Product names remain exact, leaving 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eaf27f26b">Member settings and appearance gain 50 values</a>. Thanks to xet7.</summary>Member popups, archive restoration, imported-user mapping, permissions, themes, fonts, text colors, avatars and language controls now use Central Kurdish. CAS, digits and whole-locale invariants remain exact, leaving 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/183b1dd96">Navigation, card aging and colors gain 50 values</a>. Thanks to xet7.</summary>Starring, automatic list widths, card-aging tiers, keyboard-style movement, dialogs, archive recovery and twenty-three color names now use Central Kurdish. Digits and whole-locale invariants remain exact, leaving 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2efd120c8">Permissions and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment-only, read-only and worker roles, deletion confirmations, clipboard actions, linked cards, templates and custom-field types now use Central Kurdish. The bulk-card example remains valid JSON, leaving 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2d7364439">Account emails and import errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, profiles, WIP limits, account emails and authorization, JSON, CSV, TSV and board-import errors now use Central Kurdish. Named email tokens and paragraph breaks remain exact, leaving 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9702a2f5">Card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account errors, attachment-free board export, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Central Kurdish. File-format names remain exact, leaving 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a1ae1d7d1">Advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced custom-field filters and Kanboard, NextCloud, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Central Kurdish. Operators, regexes, API paths, file extensions and named tokens remain exact, leaving 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8dd33301c">Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello archive validation, workspaces, API credentials, board selection, progress, cancellation and recovery, imported-member mapping, date validation, keyboard shortcuts and label controls now use Central Kurdish. URLs, percent tokens and digits remain exact, leaving 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/359ef2e37">List actions and board roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, list archive and movement, user, team and organization settings, swimlane actions, calendar, login, multi-selection, muted boards, archives and assigned-only roles now use Central Kurdish. The board-title token and file-format names remain exact, leaving 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ab238b816">Privacy, search and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watched-board notices, private-page login, image previews, board visibility, member removal, card-description rescue, search, WIP limits, keyboard shortcuts, sidebars, signup and starred boards now use Central Kurdish. Named and percent tokens and link markup remain exact, leaving 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/42389b281">Tracking, branding and welcome boards gain 50 values</a>. Thanks to xet7.</summary>Starred boards, subscriptions, time tracking, numeric shortcuts, uploads, custom branding and URL settings, imported usernames, watching, welcome and template boards, WIP limits and attachment/API limits now use Central Kurdish. Digits and URL/API terms remain exact, leaving 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4991caf56">Attachment limits, SMTP and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar blocking, registration, invitations, SMTP configuration and testing, authorization, outgoing and bidirectional webhooks and package, database, Node and Meteor metadata now use Central Kurdish. Invitation tokens and protocol names remain exact, leaving 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/021f94d4e">Runtime metrics and organization tenancy gain 50 values</a>. Thanks to xet7.</summary>Database and FerretDB revisions, reactivity and DDP modes, OS metrics, time units, custom-field display, account changes, visibility, shared templates, authentication synchronization, tenancy, domains and administrators now use Central Kurdish. Environment variables, hostnames and product names remain exact, leaving 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ff176c2f4">Card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, colors, requester and assigner labels, board and notification deletion, duplicate-list cleanup, subtask destinations, minicard fields, attachment and checklist counts, parent paths and label activity now use Central Kurdish. Named and percent tokens and counters remain exact, leaving 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/387a30bdf">Activity and board rules gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, rule creation and selection, workflow editing, card/member/checklist triggers and JSON, CSV and Trello Butler rule transfer now use Central Kurdish. Named and percent tokens and product and format names remain exact, leaving 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/862915878">Workflow schedules and actions gain 50 values</a>. Thanks to xet7.</summary>Visual-workflow imports, scheduled and button triggers, recurrence, due-date and list-duration conditions, sorting, completion, bulk movement and relative dates now use Central Kurdish. Named tokens, product names and weekday ranges remain exact, leaving 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f5ffef518">Rule conditions and card actions gain 50 values</a>. Thanks to xet7.</summary>Moves, archives, label, member, attachment and checklist conditions, card placement, member and label removal, colors, checklist actions and email actions now use Central Kurdish. Rule terminology remains consistent, leaving 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2f01c6785">Rule details and custom branding gain 50 values</a>. Thanks to xet7.</summary>Rule emails, archives, labels, cards, members, checklists, swimlanes and date fields, authentication, custom product metadata and manifests, layout and board member lists now use Central Kurdish. Delimiters, HTML, JSON and filenames remain exact, leaving 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/511daa2e8">Reminders and layout settings gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication, board duplication, counts, swimlane deletion, date changes and reminders, card placement, account deletion and desktop layout controls now use Central Kurdish. HTML tags, line breaks, named and percent tokens remain exact, leaving 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/532ea2565">Roles and notification controls gain 50 values</a>. Thanks to xet7.</summary>Multi-window cards, editor submission, organization, team and user dialogs, notification filters, renaming, board-role permissions and status, weekdays, ownership, activity, voting and linked-card deletion now use Central Kurdish. Keyboard combinations remain exact, leaving 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b6a028541">Shared templates and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, people and time units, My Cards sorting, due-card and global-search views and missing-object messages now use Central Kurdish. Domain examples, Markdown and percent tokens remain exact, leaving 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6dcd4e3ea">Search operators gain 50 values</a>. Thanks to xet7.</summary>Missing comments, organizations and teams, card-result counts, search operators for board fields and predicates for status, dates and content now use Central Kurdish. Search tokens remain space-free and percent and range tokens remain exact, leaving 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dc14ce1aa">Global-search guidance gains 50 values</a>. Thanks to xet7.</summary>Search predicates and validation, paging, operator syntax and examples, board, list, swimlane, label, user, organization, team, date, status, content, sorting and limit guidance now use Central Kurdish. Pseudo-tags, named tokens, examples and line breaks remain exact, leaving 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/45874bb50">Dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Label metadata, board and card sorting, completion, stickers, dependency types and transfer, board backgrounds and card-location fields now use Central Kurdish. Sort arrows, format names, named tokens and size syntax remain exact, leaving 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/27150da4c">Locations and reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, sorting, links, board activity, swimlanes, string templates, creator fields and file, security, speed, test, database, rule, board, card, impersonation, recovery and office reports now use Central Kurdish. Commands, line breaks, entities and tokens remain exact, leaving 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/089c2eaec">Recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>Office and API reporting, automatic recovery, swimlane copying, card creators, wait animations, card sizing, organization and team deletion warnings, ticket fields and statuses, requests, card sorting and details now use Central Kurdish. API settings and product names remain exact, leaving 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2b17697e5">Administration and attachments gain 50 values</a>. Thanks to xet7.</summary>Team and organization assignment, invitations, Node heap and memory metrics, legal notices, checklist copying and line conversion, card/subtask actions and attachment movement between filesystem, GridFS and S3 now use Central Kurdish. Runtime and storage names remain exact, leaving 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bf5f2f0e6">Storage maintenance gains 50 values</a>. Thanks to xet7.</summary>Attachment moves and location repair, avatar storage, default upload storage, file counts and identifiers, MongoDB GridFS compaction and board metadata now use Central Kurdish. Storage, database, runtime and oplog names remain exact, leaving 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fedce366b">Uploads and support gain 50 values</a>. Thanks to xet7.</summary>Board time statistics, upload progress, login, file limits and validation, workspace assignment, custom translations, checklist folding, board ZIP import and support and accessibility pages now use Central Kurdish. Named tokens, formats and standards remain exact, leaving 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/241af1871">Account lockout and scheduling gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force protection, locked-user administration, active-user filters, scheduled jobs and migrations, attachment/avatar paths and scheduled board archive, backup and cleanup now use Central Kurdish. Durations and status distinctions remain explicit, leaving 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cb41f10df">Database migration gains 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration recovery, filesystem and S3 settings, Azure credentials, MongoDB/FerretDB text-data migration and Sandstorm grain migration now use Central Kurdish. URLs, ports, environment variables, paths and database and platform names remain exact, leaving 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3fd930059">Security and backup controls gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card-loading modes, safe link and code rendering, import, export, avatar and identity privacy, activities, notifications, watching and instance/organization backups now use Central Kurdish. HTML, code, environment variables, paths and cloud-provider names remain exact, leaving 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aace17067">Backup and cloud storage gain 50 values</a>. Thanks to xet7.</summary>Backup schedules and restore modes, Google Cloud service accounts, S3, Azure and GCS configuration paths, secret handling, connection tests and attachment movement to cloud storage now use Central Kurdish. Time formats, console labels, key names and provider names remain exact, leaving 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4c820d4dc">Storage migration gains 50 values</a>. Thanks to xet7.</summary>GridFS, general migration progress and controls, CollectionFS moves, S3 authentication and connectivity, scheduled board operations, writable paths and attachment and board migration settings now use Central Kurdish. Storage, region and transport names remain exact, leaving 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/efc61040a">Board repair gains 50 values</a>. Thanks to xet7.</summary>Comprehensive board migration, duplicate-list cleanup, lost and archived item recovery, missing-list and file/avatar URL repair, confirmations, progress and structural migration steps now use Central Kurdish. Field identifiers and URL terminology remain exact, leaving 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b5a958668">Migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Board repair steps, conversion notices, CPU and memory usage, database migrations, job schedules and queues, filesystem and GridFS monitoring and minicard list visibility now use Central Kurdish. Identifiers, intervals and storage names remain exact, leaving 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ae1eb8ad4">Migration controls gain 50 values</a>. Thanks to xet7.</summary>Bulk storage migration, batch size, CPU and delay thresholds, background migration state, monitoring, schedules, progress and attachment totals and minicard list/checklist visibility now use Central Kurdish. Numeric ranges, units and storage names remain exact, leaving 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/574627c33">The whole-file translation is complete</a>. Thanks to xet7.</summary>Accounts and repositories, problem and repair reporting, CPU and event details, filesystem integrity, scoped import/export and number-search guidance now use Central Kurdish. OTP, API, IP versions, product and format names, named tokens and pseudo-tags remain exact. All 2,166 formerly untranslated values are now translated, with zero English placeholders remaining.
</details>Chuvash - direct whole-file translation in Chuvash Cyrillic script.
<details> <summary><a href="https://github.com/wekan/wekan/commit/546f5ed18">Activity and board controls gain 50 values</a>. Thanks to xet7.</summary>Board, list, swimlane, card, attachment, subtask, label, checklist, comment, custom-field, member, archive and import activity now uses Chuvash. Named and percent placeholders remain exact, and new whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fe2bea3c6">Activity and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card moves, member changes, generic and checklist activity, comment and date changes and All Boards workspace controls now use Chuvash. Named and percent placeholders and Markdown terminology remain exact, leaving 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ee4546120">Workspace and layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, list widths, keyboard shortcuts, swimlane heights, templates, subtasks and checklist controls now use Chuvash. Percent placeholders and numeric limits remain exact, leaving 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/da80a33a6">Board administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administrator announcements, offline recovery, archives, templates, attachments, backgrounds, member and assignee views and private-board notices now use Chuvash. Named and percent tokens and HTML tags remain exact, leaving 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/78ddf054d">Board views and card controls gain 50 values</a>. Thanks to xet7.</summary>Public-board notices, board movement, backgrounds, desktop and mobile modes, zoom, calendar and statistics views, archive recovery and card controls now use Chuvash. Named and percent tokens and HTML tags remain exact, leaving 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cc21db893">Voting and imports gain 50 values</a>. Thanks to xet7.</summary>Card membership and dates, voting, Planning Poker, dependencies, organizations, teams, backgrounds and checklist, swimlane, list, card and board imports now use Chuvash. Product names remain exact, leaving 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f3eb5045d">Member settings and appearance gain 50 values</a>. Thanks to xet7.</summary>Member popups, archive restoration, imported-user mapping, permissions, themes, fonts, text colors, avatars and language controls now use Chuvash. CAS, digits and whole-locale invariants remain exact, leaving 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c41801d99">Navigation, card aging and colors gain 50 values</a>. Thanks to xet7.</summary>Starring, automatic list widths, card-aging tiers, keyboard-style movement, dialogs, archive recovery and twenty-three color names now use Chuvash. Digits and whole-locale invariants remain exact, leaving 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f561a0fd9">Permissions and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment-only, read-only and worker roles, deletion confirmations, clipboard actions, linked cards, templates and custom-field types now use Chuvash. The bulk-card example remains valid JSON, leaving 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/310178f2e">Account emails and import errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, profiles, WIP limits, account emails and authorization, JSON, CSV, TSV and board-import errors now use Chuvash. Named email tokens and paragraph breaks remain exact, leaving 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b0799af4b">Card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account errors, attachment-free board export, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Chuvash. File-format names remain exact, leaving 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dd5e03645">Advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced custom-field filters and Kanboard, NextCloud, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Chuvash. Operators, regexes, API paths, file extensions and named tokens remain exact, leaving 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b1511b57">Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello archive validation, workspaces, API credentials, board selection, progress, cancellation and recovery, imported-member mapping, date validation, keyboard shortcuts and label controls now use Chuvash. URLs, percent tokens and digits remain exact, leaving 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c2ebc314e">List actions and board roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, list archive and movement, user, team and organization settings, swimlane actions, calendar, login, multi-selection, muted boards, archives and assigned-only roles now use Chuvash. The board-title token and file-format names remain exact, leaving 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d29a5b185">Privacy, search and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watched-board notices, private-page login, image previews, board visibility, member removal, card-description rescue, search, WIP limits, keyboard shortcuts, sidebars, signup and starred boards now use Chuvash. Named and percent tokens and link markup remain exact, leaving 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/615213404">Tracking, branding and welcome boards gain 50 values</a>. Thanks to xet7.</summary>Starred boards, subscriptions, time tracking, numeric shortcuts, uploads, custom branding and URL settings, imported usernames, watching, welcome and template boards, WIP limits and attachment/API limits now use Chuvash. Digits and URL/API terms remain exact, leaving 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e6c13ffbc">Attachment limits, SMTP and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar blocking, registration, invitations, SMTP configuration and testing, authorization, outgoing and bidirectional webhooks and package, database, Node and Meteor metadata now use Chuvash. Invitation tokens and protocol names remain exact, leaving 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8be90c927">Runtime metrics and organization tenancy gain 50 values</a>. Thanks to xet7.</summary>Database and FerretDB revisions, reactivity and DDP modes, OS metrics, time units, custom-field display, account changes, visibility, shared templates, authentication synchronization, tenancy, domains and administrators now use Chuvash. Environment variables, hostnames and product names remain exact, leaving 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ef259d8f0">Card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, colors, requester and assigner labels, board and notification deletion, duplicate-list cleanup, subtask destinations, minicard fields, attachment and checklist counts, parent paths and label activity now use Chuvash. Named and percent tokens and counters remain exact, leaving 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a2b80b889">Activity and board rules gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, rule creation and selection, workflow editing, card/member/checklist triggers and JSON, CSV and Trello Butler rule transfer now use Chuvash. Named and percent tokens and product and format names remain exact, leaving 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/172365825">Workflow schedules and actions gain 50 values</a>. Thanks to xet7.</summary>Visual-workflow imports, scheduled and button triggers, recurrence, due-date and list-duration conditions, sorting, completion, bulk movement and relative dates now use Chuvash. Named tokens, product names and weekday ranges remain exact, leaving 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7067c50d">Rule conditions and card actions gain 50 values</a>. Thanks to xet7.</summary>Moves, archives, label, member, attachment and checklist conditions, card placement, member and label removal, colors, checklist actions and email actions now use Chuvash. Rule terminology remains consistent, leaving 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/844b8af71">Rule details and custom branding gain 50 values</a>. Thanks to xet7.</summary>Rule emails, archives, labels, cards, members, checklists, swimlanes and date fields, authentication, custom product metadata and manifests, layout and board member lists now use Chuvash. Delimiters, HTML, JSON and filenames remain exact, leaving 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c5300e01f">Reminders and layout settings gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication, board duplication, counts, swimlane deletion, date changes and reminders, card placement, account deletion and desktop layout controls now use Chuvash. HTML tags, line breaks, named and percent tokens remain exact, leaving 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/588c2c5ce">Roles and notification controls gain 50 values</a>. Thanks to xet7.</summary>Multi-window cards, editor submission, organization, team and user dialogs, notification filtering, board-role permissions and status, weekdays, ownership and linked-card deletion guards now use Chuvash. Keyboard chords and weekday terminology remain exact, leaving 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/375e51a2c">Templates and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, display names, calendar units, My Cards, due-card and global-search views and missing-object messages now use Chuvash. Domain examples, Markdown emphasis and percent tokens remain exact, leaving 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fcc08752d">Search vocabulary gains 50 values</a>. Thanks to xet7.</summary>Missing comments, organizations and teams, card-result counts and the search operators and predicates for boards, people, dates, content and card state now use Chuvash. Percent and result-range tokens remain exact, leaving 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ce66d24e4">Global-search guidance gains 50 values</a>. Thanks to xet7.</summary>Search predicates, validation, paging, operator syntax, statuses, sorting, limits and combination guidance now use Chuvash. Named tokens, pseudo-tags, Markdown examples and line breaks remain exact, leaving 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b708055c2">Dependencies and backgrounds gain 50 values</a>. Thanks to xet7.</summary>Label metadata, board sorting, card completion, stickers, dependency relations and imports, board backgrounds and location fields now use Chuvash. JSON, SVG, named and size tokens and sort arrows remain exact, leaving 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d2ffa905">Locations and reports gain 50 values</a>. Thanks to xet7.</summary>Map locations, server-error diagnostics, activity sorting, string templates, file and security reports, impersonation, recovery and office-login details now use Chuvash. Commands, HTML entities, format tokens, line breaks and IP versions remain exact, leaving 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74d6d9701">Recovery and support workflows gain 50 values</a>. Thanks to xet7.</summary>Office and API reports, recovery events and maintenance, swimlane copying, loading animations, organization guards, tickets, requests and card details now use Chuvash. REST API, environment, database and email-copy terms remain exact, leaving 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/360944727">Organizations and storage gain 50 values</a>. Thanks to xet7.</summary>Team invitations, Node memory statistics, organizations, legal notices, checklist transformations and attachment-storage movement now use Chuvash. Node, heap, malloc, URL, GridFS and S3 terminology remains exact, leaving 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/89f3e3875">Storage repair gains 50 values</a>. Thanks to xet7.</summary>Bulk attachment movement, storage discovery and repair, default backends, file counts and MongoDB compaction now use Chuvash. GridFS, S3, IDs, MongoDB, oplog, Meteor and Compact terminology remains exact, leaving 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cd23afbae">Uploads and support gain 50 values</a>. Thanks to xet7.</summary>Board timing, uploads, file restrictions, custom translations, checklist display and support and accessibility pages now use Chuvash. Workspace tokens, PDF, ISO 8601, Markdown, JSON and ZIP terminology remains exact, leaving 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6b9844d08">Account protection gains 50 values</a>. Thanks to xet7.</summary>Accessibility, brute-force lockouts, administrator user filters, scheduled jobs, storage paths and scheduled board archive, backup and cleanup operations now use Chuvash. Security distinctions and time units remain explicit, leaving 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ea587e1b">Migrations and storage gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job recovery, filesystem and cloud storage and MongoDB, FerretDB and Sandstorm migrations now use Chuvash. URLs, ports, environment variables, commands, paths, database names and the destination token remain exact, leaving 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dff10fc4c">Security and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, adaptive card loading, safe text rendering, import/export controls, identity anonymization, activity and notification controls and backup scope and storage now use Chuvash. HTML, environment variables, formats, paths and cloud-service names remain exact, leaving 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2646a0ae8">Cloud backups gain 50 values</a>. Thanks to xet7.</summary>Backup schedules and restoration plus GCS, S3 and Azure credentials, permission paths and connection tests now use Chuvash. Time formats, JSON fields, provider names, console paths, roles and file extensions remain exact, leaving 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/355853811">Migration controls gain 50 values</a>. Thanks to xet7.</summary>GridFS and S3 storage, migration lifecycle controls, scheduled board operations, attachment monitoring and board migration now use Chuvash. MongoDB, GridFS, CollectionFS, AWS, MinIO, region and TLS terminology remains exact, leaving 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/582ef3cca">Board repair gains 50 values</a>. Thanks to xet7.</summary>Comprehensive board migration, duplicate-list cleanup, lost-card restoration, archive recovery and broken list, avatar and attachment repair now use Chuvash. Database field names, IDs and URLs remain exact, leaving 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cb9006385">Migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Board-repair steps, conversion progress, CPU and memory monitoring, schedules, filesystem and GridFS statistics and job queues now use Chuvash. IDs, URLs, CPU, GridFS and numeric intervals remain exact, leaving 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/84f7240d6">Migration tuning gains 50 values</a>. Thanks to xet7.</summary>Attachment migration destinations, batch and CPU thresholds, background status, monitoring controls, schedules, storage distribution and system-resource totals now use Chuvash. GridFS, S3, CPU, percentages and numeric ranges remain exact, leaving 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/42c58ae71">Account and diagnostics complete all 2,166 values</a>. Thanks to xet7.</summary>OTP and account flows, repositories, problem and repair reporting, CPU and event details, filesystem integrity, scoped import/export and number-search guidance now use Chuvash. Named tokens, pseudo-tags, IP versions, product names and file extensions remain exact. All 2,166 formerly untranslated values are translated, with zero English placeholders remaining.
</details>Dzongkha - direct whole-file translation in Bhutanese Tibetan script.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8f31871de">Activity and board controls gain 50 values</a>. Thanks to xet7.</summary>Board, list, swimlane, card, attachment, subtask, label, checklist, comment, custom-field, member, archive and import activity now uses Dzongkha. Named and percent placeholders remain exact, and new whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/98d10bee6">Activity and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card moves, member changes, generic and checklist activity, comments, dates and All Boards workspace controls now use Dzongkha. Named and percent placeholders and Markdown terminology remain exact, leaving 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09b6e6d70">Workspace and layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, list widths, keyboard shortcuts, swimlane heights, templates, subtasks and checklist controls now use Dzongkha. Percent placeholders and numeric limits remain exact, leaving 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0930e60b5">Board administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administrator announcements, offline recovery, archives, templates, attachments, backgrounds, member and assignee views and private-board notices now use Dzongkha. Named and percent tokens and HTML tags remain exact, leaving 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d69443c4c">Board views and card controls gain 50 values</a>. Thanks to xet7.</summary>Public-board notices, board movement, backgrounds, desktop and mobile modes, zoom, calendar and statistics views, archive recovery and card controls now use Dzongkha. Named and percent tokens and HTML tags remain exact, leaving 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2cfe41ec2">Voting and imports gain 50 values</a>. Thanks to xet7.</summary>Card membership and dates, voting, Planning Poker, dependencies, organizations, teams, backgrounds and checklist, swimlane, list, card and board imports now use Dzongkha. Product names remain exact, leaving 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c4be0d9b8">Member settings and appearance gain 50 values</a>. Thanks to xet7.</summary>Member popups, archive restoration, imported-user mapping, permissions, themes, fonts, text colors, avatars and language controls now use Dzongkha. CAS, digits and whole-locale invariants remain exact, leaving 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f35ecb497">Navigation, card aging and colors gain 50 values</a>. Thanks to xet7.</summary>Starring, automatic list widths, card-aging tiers, keyboard-style movement, dialogs, archive recovery and twenty-three color names now use Dzongkha. Digits and whole-locale invariants remain exact, leaving 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/67d562029">Permissions and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment-only, read-only and worker roles, deletion confirmations, clipboard actions, linked cards, templates and custom-field types now use Dzongkha. The bulk-card example remains valid JSON, leaving 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fb4013754">Account emails and import errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, profiles, WIP limits, account emails and authorization, JSON, CSV, TSV and board-import errors now use Dzongkha. Named email tokens and paragraph breaks remain exact, leaving 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d76ccde6c">Card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account errors, attachment-free board export, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Dzongkha. File-format names remain exact, leaving 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/698eef836">Advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced custom-field filters and Kanboard, NextCloud, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Dzongkha. Operators, regexes, API paths, file extensions and named tokens remain exact, leaving 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c501dfcdf">Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello archive validation, workspaces, API credentials, board selection, progress, cancellation and recovery, imported-member mapping, date validation, keyboard shortcuts and label controls now use Dzongkha. URLs, percent tokens and digits remain exact, leaving 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6a8549b4">Lists, archives and roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, card and list links, archive actions, user, team and organization settings, imports, navigation, list movement, multi-selection, notifications and board roles now use Dzongkha. The named board-title token and whole-locale markup invariants remain exact, leaving 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2debb0e2d">Board access and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watching, public and private access, image previews, member removal, rescue dialogs, search, WIP limits, keyboard shortcuts, sidebars and starred and default boards now use Dzongkha. Named and percent tokens and HTML link markup remain exact, leaving 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b89e4ed83">Tracking, uploads and templates gain 50 values</a>. Thanks to xet7.</summary>Time tracking, assignments, subscriptions, uploads, custom branding, URL schemes, watching, welcome and board templates and WIP-limit controls now use Dzongkha. Product names, numeric ranges and whole-locale invariants remain exact, leaving 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d38d376e0">Attachment limits and invitations gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar restrictions, registration, invitations, SMTP configuration and tests, authorization, webhooks and software version labels now use Dzongkha. Email tokens and paragraph structure and whole-locale markup invariants remain exact, leaving 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2d5e88d77">Diagnostics and organizations gain 50 values</a>. Thanks to xet7.</summary>Database, FerretDB, reactivity and operating-system diagnostics, time units, custom-field display, account options, visibility, shared templates, multitenancy, organization domains and scoped administrators now use Dzongkha. Environment names and example hostnames remain exact, leaving 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/03bf26e24">Cards, deletion and subtasks gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, colors, requester and assigner labels, destructive board and notification actions, duplicate lists, subtask destinations, minicard fields, card hierarchy and label activity now use Dzongkha. Named and percent tokens and numeric checklist formats remain exact, leaving 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ee59a1ac8">Automation rules and imports gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, visual rule construction, card, label, member and checklist triggers and JSON, CSV and Trello Butler rule imports now use Dzongkha. Percent and named automation tokens remain exact, leaving 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/02a4b764d">Scheduled automation gains 50 values</a>. Thanks to xet7.</summary>Visual-workflow formats, scheduled and due-date triggers, card and board buttons, list sorting, completion actions, relative dates and automation time units now use Dzongkha. Workflow product names, counts and schedule parameters remain exact, leaving 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/60b32029c">Automation actions gain 50 values</a>. Thanks to xet7.</summary>List and archive conditions, label, member, attachment and checklist triggers, card movement and restoration, membership, color, checklist and email actions now use Dzongkha. Whole-locale token and markup invariants remain exact, leaving 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/668c9cd32">Rule details and branding gain 50 values</a>. Thanks to xet7.</summary>Card creation, archive, membership, checklist and date-field actions, automation notes, authentication, custom product names, head tags, web manifests, Android asset links and layout controls now use Dzongkha. Example lists and HTML, JSON and filename literals remain exact, leaving 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ec14acf70">Custom HTML and reminders gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication display, duplication, destructive swimlane and account actions, date activity, due reminders, mention notifications, drag-resizing and multi-card controls now use Dzongkha. HTML tags, named and percent tokens and keyboard names remain exact, leaving 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e74b5fce6">Roles and interface settings gain 50 values</a>. Thanks to xet7.</summary>Multi-card and keyboard submission behavior, organization, team and user dialogs, notification controls, renaming and board-role permissions, weekdays, status fields and linked-card deletion warnings now use Dzongkha. Keyboard chords and whole-locale invariants remain exact, leaving 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88d31cf17">Shared templates and search gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, domain-scoped shared templates, My Cards sorting, Due Cards and global-search views and board, swimlane, list, label and username errors now use Dzongkha. Domain examples, Markdown emphasis and percent tokens remain exact, leaving 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cfa2ad9ac">Search operators gain 50 values</a>. Thanks to xet7.</summary>Comment, organization and team errors, result counts and board, swimlane, list, label, user, date, status, attachment, checklist and custom-field search operators and predicates now use Dzongkha. Percent and result-range tokens remain exact, leaving 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7160601ef">Search instructions gain 50 values</a>. Thanks to xet7.</summary>Public, private, selector and projection predicates, validation errors, pagination and the complete global-search operator, status, presence, sorting, limit and composition guidance now use Dzongkha. Named tokens, pseudo-tags and Markdown examples remain exact, leaving 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a22a0e77c">Dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Labels, board and card sorting, completion, stickers, dependency relationships and JSON/SVG imports, board backgrounds and location fields now use Dzongkha. Sort markers, named import counts and the background-size token remain exact, leaving 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6f4b0129">Maps and reports gain 50 values</a>. Thanks to xet7.</summary>Map detection and providers, server troubleshooting, sorting, board activity, swimlane movement, string templates, file, security, speed, test, database, rule, recovery and office-login reports now use Dzongkha. Shell commands, entities, format tokens and IP versions remain exact, leaving 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/160ac04b6">Recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>Office and REST API reporting, data recovery and maintenance, swimlane copying, wait spinners, card sizing, organization and team deletion constraints and ticket and help-request workflows now use Dzongkha. API settings, database names and mail headers remain exact, leaving 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/be69998f4">Diagnostics and attachments gain 50 values</a>. Thanks to xet7.</summary>Team and organization controls, Node heap and memory diagnostics, legal notices, checklist movement and copying and filesystem, GridFS and S3 attachment storage actions now use Dzongkha. Technical storage and memory names and the checklist line example remain exact, leaving 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8b42afd56">Storage repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment relocation and location repair, avatar scopes, default storage, move progress and file counts, storage identifiers and MongoDB collection compaction now use Dzongkha. Backend names, identifiers and oplog terminology remain exact, leaving 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/acb7fb1ce">Settings and support gain 50 values</a>. Thanks to xet7.</summary>Board timing and uploads, file restrictions, workspace assignment, custom translations, subtasks, ISO week display, Markdown conversion, board ZIP imports, checklist folding, support and accessibility now use Dzongkha. Named tokens and format names remain exact, leaving 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b3f5c4085">Account protection and scheduling gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force account protection and unlocking, active user filters, attachment and avatar paths, scheduled board archive, backup and cleanup operations and cron migrations now use Dzongkha. Whole-locale token and markup invariants remain exact, leaving 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9b286414f">Migrations and storage gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration recovery, filesystem, S3-compatible and Azure storage, MongoDB/FerretDB text-data migration and Sandstorm grain conversion now use Dzongkha. Database URLs, ports, environment names, commands, named tokens and grain paths remain exact, leaving 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e69482ddd">Security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, adaptive card loading, plain-text link and code controls, import/export and avatar restrictions, user anonymization, activity, notification and watch controls and scoped backups now use Dzongkha. Environment names, markup, examples and backup paths remain exact, leaving 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b7771030c">Cloud storage and restores gain 50 values</a>. Thanks to xet7.</summary>Backup frequency and restoration, Google Cloud credentials and permissions, AWS/S3, Azure and GCS setup paths, secret handling, connection tests and cloud attachment movement now use Dzongkha. Time formats, ranges, credential fields, roles, provider names and filenames remain exact, leaving 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b835cb4be">Storage backends and migrations gain 50 values</a>. Thanks to xet7.</summary>GridFS storage, migration start, pause, stop and status controls, CollectionFS movement guidance, S3 credentials and connections, scheduled board operations, attachment monitoring and board migrations now use Dzongkha. Backend, region and transport names remain exact, leaving 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/30553c491">Board repair gains 50 values</a>. Thanks to xet7.</summary>Comprehensive board integrity checks, duplicate-list cleanup, lost-card and archive restoration, missing-list repair, avatar and attachment URL repair, confirmation dialogs, progress and migration steps now use Dzongkha. Schema identifiers and URL and ID literals remain exact, leaving 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b690edcf6">Migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Final board-repair steps, conversion status, CPU and memory reporting, scheduled intervals, filesystem and GridFS monitoring, board scans, job queues and migration state now use Dzongkha. Identifiers, metric names and numeric intervals remain exact, leaving 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e406bae6">Migration tuning gains 50 values</a>. Thanks to xet7.</summary>Attachment migration destinations, batch and CPU thresholds, background status, monitoring controls, schedules, storage distribution and system resources now use Dzongkha. GridFS, S3, CPU, percent signs and numeric ranges remain exact, leaving 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1a9a258aa">Repository and diagnostics translation completes Dzongkha</a>. Thanks to xet7.</summary>Repository authentication and uploads, problem monitoring, event diagnostics, broken-card repair and import/export controls now use Dzongkha. Named tokens, angle-bracket examples, inline code, emphasis, file extensions and product names remain exact. This final 66-value batch completes all 2,166 Dzongkha placeholders.
</details>Ewe - direct whole-file translation using established Ewe vocabulary.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bf2a68e13">Activity and board basics gain 50 values</a>. Thanks to xet7.</summary>Organization and team membership, comments, due dates, attachments, subtasks, labels, checklists, boards, lists, cards, swimlanes and archive and import activity now use Ewe. Percent and named activity tokens remain exact, leaving 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/997ec2f92">Movement and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement and restoration, member removal, general activity sentences, checklist events, received and start dates and workspace creation and settings now use Ewe. Named and percent activity tokens and Markdown terminology remain exact, leaving 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fd0c42f89">Workspace and layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, board selection and starring, Home-board behavior, list widths, keyboard shortcuts, swimlane height and checklist and card controls now use Ewe. Percent placeholders, pixels and the 270-pixel limit remain exact, leaving 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/51b6341b3">Board administration gains 50 values</a>. Thanks to xet7.</summary>Administration and announcements, public boards, archive and restore controls, templates, attachments, backgrounds, All Boards settings, members and assignees now use Ewe. Named and percent tokens, URL terminology and strong markup remain exact, leaving 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ca486e019">Board views and archives gain 50 values</a>. Thanks to xet7.</summary>Board rearrangement and workspace assignment, visibility and watch controls, desktop and mobile views, zoom, calendar navigation, archives and card editing now use Ewe. Named and percent tokens, strong markup and the zoom range remain exact, leaving 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/068c2fdef">Voting and imports gain 50 values</a>. Thanks to xet7.</summary>Card membership, voting, Planning Poker, dependencies, organization and team assignment, backgrounds, account deletion, domains and board-part imports now use Ewe. Product terminology remains exact, leaving 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/08423a6e8">Membership and appearance gain 50 values</a>. Thanks to xet7.</summary>Member dialogs, card and board links, imported-member mapping, themes, fonts, text colors, avatars, language and permission controls now use Ewe. CAS, email and the numeric font sample remain exact, leaving 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/efb1bb4b2">Navigation and colors gain 50 values</a>. Thanks to xet7.</summary>Settings, starring, automatic list widths, card aging, movement, dialogs, accessibility navigation and twenty-three color names now use Ewe. Numeric tiers and whole-locale invariants remain exact, leaving 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9a2751381">Roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment and read-only roles, deletion confirmations, link copying, multi-card templates, labels and custom-field types now use Ewe. The translated card template remains valid JSON and keyboard terminology remains exact, leaving 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b30572ced">Email and validation gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, WIP limits, dates, reactions, notifications, account emails and board, user and import validation now use Ewe. Named email tokens, line breaks, WIP, JSON, CSV, TSV and WeKan remain exact, leaving 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0b709945c">Exports and filters gain 50 values</a>. Thanks to xet7.</summary>User and account errors, card exports, attachment metadata, free-space errors, sorting and date, label and member filters now use Ewe. PDF, Excel and disk terminology remain exact, leaving 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/efaf9537a">Advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced filtering, activity visibility, imported members and Kanboard, NextCloud Deck, OpenProject, Asana, ZenKit, Trello, Jira, Excel and WeKan board imports now use Ewe. Operators, regex, tokens, API paths, extensions and product names remain exact, leaving 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cf9255edd">Trello and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello ZIP validation, workspace placement, API imports, credential storage, bulk selection and cancellation, member mapping, date validation, keyboard shortcuts and labels now use Ewe. The API URL, key and token names, year example and percent token remain exact, leaving 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d63bb85c3">Roles and list actions gain 50 values</a>. Thanks to xet7.</summary>Last-administrator protection, leaving boards, list archives, role settings, swimlane actions, card imports, login, selection tools and notification roles now use Ewe. Named board-title tokens and import formats remain exact, leaving 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8a8613a77">Access and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watch notifications, private and public access guidance, member removal, description rescue, search, WIP limits, keyboard shortcuts, sidebars and default-board behavior now use Ewe. HTML links, named and percent tokens and product and keyboard terms remain exact, leaving 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5a0e600f4">Tracking and customization gain 50 values</a>. Thanks to xet7.</summary>Starred boards, time tracking, label and assignee shortcuts, uploads, custom logos and URLs, welcome templates, WIP errors and attachment/API limits now use Ewe. Numeric ranges, dimensions and technical names remain exact, leaving 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4aa0418be">Transfers and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment transfer limits, avatar uploads, registration, invitations, SMTP/TLS settings and test mail, webhooks and package, database, Node and Meteor labels now use Ewe. Named email tokens and technical names remain exact, leaving 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/24f2c2ee6">System and multitenancy labels gain 50 values</a>. Thanks to xet7.</summary>Database and FerretDB metadata, reactivity, DDP and operating-system metrics, custom fields, visibility, organizations, teams and multitenancy now use Ewe. Technical modes, environment names and example domains remain exact, leaving 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ab595a8f8">Card settings and deletion gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, color dialogs, permanent board and duplicate-list deletion, subtask and card settings, minicard fields, parent paths and label activity now use Ewe. Named and percent tokens and count examples remain exact, leaving 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/54c02b7e6">Automation workflows gain 50 values</a>. Thanks to xet7.</summary>Attachment and custom-field activity, visual rule workflows, triggers, actions, rule selection and JSON, CSV and Trello Butler imports now use Ewe. Percent and named tokens and format and product names remain exact, leaving 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bd54f339f">Automation scheduling gains 50 values</a>. Thanks to xet7.</summary>n8n and Node-RED workflow imports, scheduled and button triggers, due-date conditions, list sorting, completion, relative dates, units and board and card actions now use Ewe. Product names, count tokens and variables remain exact, leaving 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/08ad8fa82">Automation actions gain 50 values</a>. Thanks to xet7.</summary>Rule predicates and actions for lists, labels, members, attachments, checklists, card movement, colors and email now use Ewe. Whole-locale token and markup invariants remain exact, leaving 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ea996b8d2">Automation and manifests gain 50 values</a>. Thanks to xet7.</summary>Automation actions, checklist construction, date-field updates, card links, authentication, custom HTML and JSON, web manifests, assetlinks and layout settings now use Ewe. Comma-separated examples, extensions and format names remain exact, leaving 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/462bb64cd">Authentication and activity gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, LDAP and OIDC authentication, duplication, swimlane deletion, due-time activity and reminders, mentions, destructive confirmations and drag resizing now use Ewe. HTML tags, named and percent tokens, line breaks and keyboard terminology remain exact, leaving 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/23bba0578">Roles and calendar gain 50 values</a>. Thanks to xet7.</summary>Multi-card windows, editor submission, organization, team and user dialogs, notification state, role permissions, weekdays, status, voting and linked-card deletion guidance now use Ewe. Keyboard combinations remain exact, leaving 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9ab13a132">Domains and search views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, My Cards, Due Cards, global search views and not-found messages now use Ewe. Domain examples, Markdown emphasis and percent placeholders remain exact, leaving 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e6ad5fc69">Search operators gain 50 values</a>. Thanks to xet7.</summary>Search result counts, operators and predicates for boards, swimlanes, lists, users, dates, attachments, descriptions and checklists now use Ewe. Named result-range and percent tokens remain exact, leaving 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e753a9c1c">Global search guidance gains 50 values</a>. Thanks to xet7.</summary>Search predicates, validation, pagination and the complete global-search instruction set now use Ewe. Named operator and predicate tokens, pseudo-tags, inline-code examples, quoted values and Markdown emphasis remain exact, leaving 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f752f4cca">Dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Sorting, card completion, stickers, dependency relations and imports, board backgrounds and locations now use Ewe. Sort directions, JSON and SVG, named import counts and size tokens remain exact, leaving 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d77da6cb4">Reports and location detection gain 50 values</a>. Thanks to xet7.</summary>Location detection, map settings, server troubleshooting, swimlane movement, string templates, system reports, impersonation, recovery and office login data now use Ewe. Shell commands, placeholders, HTML entities, IPv4 and IPv6 remain exact, leaving 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/297673768">Recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>REST API reporting, recovery and database health, swimlane copying, wait spinners, card sizing, deletion constraints, tickets, requests and card details now use Ewe. Environment, database, spinner and mail-header names remain exact, leaving 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/73cf5b016">Diagnostics and attachments gain 50 values</a>. Thanks to xet7.</summary>Teams and organizations, invitations, Node heap and memory diagnostics, legal notices, checklist movement and copying and filesystem, GridFS and S3 attachment moves now use Ewe. Technical storage and diagnostic names remain exact, leaving 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d35e7dcf8">Storage repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment movement and location repair, default storage, progress and counts, storage identifiers and MongoDB GridFS collection compaction now use Ewe. Backend, identifier, database and oplog terminology remain exact, leaving 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0b6bb374b">Settings and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing and uploads, file restrictions, workspace assignment, custom translations, subtasks, ISO week display, Markdown conversion, board ZIP imports, checklist folding, support and accessibility now use Ewe. Named tokens and format names remain exact, leaving 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/285ad0dae">Account protection and scheduling gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force account protection and unlocking, active user filters, attachment and avatar paths and scheduled board archive, backup and cleanup operations now use Ewe. Whole-locale token and markup invariants remain exact, leaving 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/61ba6f3f9">Migrations and storage gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration recovery, filesystem, S3-compatible and Azure storage, MongoDB/FerretDB text-data migration and Sandstorm grain conversion now use Ewe. Database URLs, ports, environment names, commands, named tokens and grain paths remain exact, leaving 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/61ec79340">Security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, adaptive card loading, plain-text link and code controls, import/export and avatar restrictions, user anonymization, activity, notification and watch controls and scoped backups now use Ewe. Environment names, markup, examples and backup paths remain exact, leaving 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d51c30ef3">Cloud backups and restores gain 50 values</a>. Thanks to xet7.</summary>Backup frequency and restoration, Google Cloud credentials and permissions, AWS/S3, Azure and GCS setup paths, secret handling, connection tests and cloud attachment movement now use Ewe. Time formats, ranges, credential fields, roles, provider names and filenames remain exact, leaving 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/17a1c5f2f">Storage backends and migrations gain 50 values</a>. Thanks to xet7.</summary>GridFS storage, migration lifecycle controls, CollectionFS movement guidance, S3 credentials and connections, scheduled board operations, attachment monitoring and board migrations now use Ewe. Backend, region and transport names remain exact, leaving 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8808b2928">Board repair gains 50 values</a>. Thanks to xet7.</summary>Comprehensive board integrity checks, duplicate-list cleanup, lost-card and archive restoration, missing-list repair, avatar and file URL repair, confirmations, progress and migration steps now use Ewe. Schema identifiers, IDs and URLs remain exact, leaving 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0967f16b2">Migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Final board-repair steps, conversion status, CPU and memory reporting, scheduled intervals, filesystem and GridFS monitoring, board scans, job queues and migration state now use Ewe. Identifiers, metric names and numeric intervals remain exact, leaving 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9c154db44">Migration tuning gains 50 values</a>. Thanks to xet7.</summary>Attachment migration destinations, batch and CPU thresholds, background status, monitoring controls, schedules, storage distribution and system resources now use Ewe. GridFS, S3, CPU, percent signs and numeric ranges remain exact, leaving 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09279e95d">Repository and diagnostics translation completes Ewe</a>. Thanks to xet7.</summary>Repository authentication and uploads, problem monitoring, event diagnostics, broken-card repair and import/export controls now use Ewe. Named tokens, angle-bracket examples, inline code, emphasis, file extensions and product names remain exact. This final 66-value batch completes all 2,166 Ewe placeholders.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release closes five security and privacy gaps, improves board creation and filtering, and makes denied attacks visible to administrators. It also completes substantial translation and locale-integrity work, strengthens cross-browser testing, updates dependencies, and documents Haxe, Go and Free Pascal alternatives for WeKan's future architecture.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following HIGH AND MODERATE SECURITY ISSUES:
User search - login, identity fields and literal query boundaries.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4ce5cf6e1">User lookup requires a login and exposes only public identity fields</a>. Thanks to Char0n1507, Reload3d and xet7.</summary>UserSearchBleed and MiniProfileBleed allowed logged-out profile enumeration and let any authenticated user retrieve instance-wide email, administrator, account-state and organization metadata. Both publications now require authentication; the general search returns only public identity fields. User-controlled search text is escaped before becoming a regular expression, and both DDP search paths are rate-limited. Regression coverage pins the positive identity projection and the negative sensitive-field and raw-pattern cases. The logged-out mini-profile denial is summarized as MiniProfileBleed in Admin Panel → Problems with its source address. UserSearchBleed has no runtime event because its fixes change what legitimate search responses carry and how ordinary punctuation is interpreted; logging those calls would record normal use.
</details>Position history - authorization for recorded moves and undo.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4ce5cf6e1">Undo cannot move a card into a board outside the caller's membership</a>. Thanks to Char0n1507 and xet7.</summary>PositionHistoryBleed trusted a client-created history entry's previous board. Inserts now require membership on both named boards, the undo method rechecks the history board, the model rechecks destination membership immediately before moving a card, and clients cannot rewrite trusted history after insertion. The test covers allowed same-board history and rejected cross-board data. Denied inserts, rewrites and undo moves are summarized as PositionHistoryBleed in Admin Panel → Problems with the account and source address.
</details>Board exports - keeping subtask data inside the exported board.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4ce5cf6e1">Every export format scopes subtask queries to its board</a>. Thanks to Char0n1507 and xet7.</summary>SubtaskExportBleed allowed a card from a private board to appear in another board's JSON, ZIP, Excel or PDF export when its parent identifier named a card there. All six subtask query paths now include the exporting board identifier. Regression coverage checks every board and card export implementation and rejects the old parent-only selectors. There is no runtime event because the fix changes the contents of a legitimate export instead of denying an attributable attack.
</details>CAS login - explicit ownership of matching local accounts.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4ce5cf6e1">CAS cannot silently take over a matching non-CAS account</a>. Thanks to Char0n1507, crypto-nidh and xet7.</summary>CasBleed allowed a validated CAS
username to receive the session of an existing password or other non-CAS
account with the same name. New CAS users are marked with their authentication
method; a conflicting account is rejected unless the administrator explicitly
sets CAS_MERGE_EXISTING_USERS=true. Positive CAS reuse and negative implicit
linking are pinned by regression coverage. Refused implicit links are summarized
as CasBleed in Admin Panel → Problems.
Security reporting - denied attributable attacks reach Admin Panel Problems without turning ordinary use into noise.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a1dfa88b4">Blocked security attacks produce bounded administrator events</a>. Thanks to xet7.</summary>Denied MiniProfileBleed, PositionHistoryBleed and CasBleed attempts now create attributed, rate-bounded entries in Admin Panel → Problems. UserSearchBleed and SubtaskExportBleed remain silent because their safe paths are indistinguishable from normal searches and exports. Clients are also prevented from rewriting validated position-history records after insertion. Canary, advisory and negative authorization tests cover the reporting boundary.
</details>and updates the following dependencies:
Dependencies - storage, authentication, build and lint packages stay current.
<details> <summary><a href="https://github.com/wekan/wekan/commit/343340444">The dependency lockfile is refreshed</a>. Thanks to dependency developers and xet7.</summary>The first refresh advances the resolved AWS, Azure authentication, MongoDB, Rspack diagnostics, SWC, TypeScript ESLint, browser-compatibility and supporting packages without changing WeKan's declared dependency ranges.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7783497a6">The latest compatible transitive dependency fixes are included</a>. Thanks to dependency developers and xet7.</summary>A follow-up lockfile refresh takes the compatible patch releases published after the first batch, including the final AWS SDK, Smithy, parser, lint and browser-data resolutions used by this release.
</details>and fixes the following bugs:
All Boards - the overview's Lists and Table layouts.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aae4e8183">Table view offers the same board creation action as Lists view</a>. Thanks to rmb82 and xet7.</summary>The All Boards Table layout displayed existing boards but offered no way to create one. It now shows the same create action and uses the same handler as the Lists tile, including the Template Container wording and workspace context. Archive and the special Home section continue to exclude creation. Source, negative and browser tests cover the available and excluded sections.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a495d7d89">Existing All Boards layouts remain covered after adding board creation</a>. Thanks to xet7.</summary>The source-level Lists and phone-layout tests now recognize the shared Add Board action introduced in Table view while continuing to pin scrolling, tile and layout behaviour. This keeps the feature's new control from being mistaken for an unexpected duplicate by the older regression assertions.
</details>Board views - alternate ways to display one board's cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bdec1744f">Table view applies the active board Filter to its card rows</a>. Thanks to rmb82 and xet7.</summary>The Table layout queried every active card directly and ignored label, member, assignee, date and other criteria from the board Filter. Its reactive query now ANDs the Filter selector with immutable board and archive boundaries before the view's own text search and pagination. Unit, wiring and browser tests cover both unfiltered and filtered rows.
</details>and improves the following developer tooling:
Complete tests - suites use repository tools and available CPU browsers.
<details> <summary><a href="https://github.com/wekan/wekan/commit/af1924567">Complete-test regressions are repaired after recent feature and translation work</a>. Thanks to xet7.</summary>Translation subprocesses now use the suite's own Node executable, invariant and backlog expectations follow the current translation helpers, and the Table-view browser test reapplies its filter after the view-change reload. The complete runner therefore checks current behaviour instead of failing on stale harness assumptions.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c31e64059">Node E2E uses the Playwright container when no compatible Chromium is installed</a>. Thanks to xet7.</summary>The Puppeteer-based Node regression harness now routes through the existing
Playwright container when its browser must come from Docker. Linux arm64 no
longer falls back to a missing /usr/bin/chromium, and selection tests pin both
the local and container paths.
Browser isolation - one test's teardown cannot revoke another test's login.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b72e0e99f">Every Red Strings browser test receives its own session</a>. Thanks to xet7.</summary>Each Red Strings case now creates a separate resume token. Closing an earlier Chromium or WebKit connection can no longer revoke the session a later Firefox case is about to use, eliminating the cross-browser ordering failure while retaining normal session teardown.
</details>Release notes - readers get a brief overview before topic and commit detail.
<details> <summary><a href="https://github.com/wekan/wekan/commit/403a0ad7a">The release-summary hierarchy and topic grouping are explicit and tested</a>. Thanks to xet7.</summary>AGENTS.md and CLAUDE.md now define three levels: a release-wide In short
paragraph of at most about 120 words, one high-level summary for each topic, and
commit-specific expandable details. The format regression caps the introductory
word count and checks that the documented grouping vocabulary remains present.
and improves the following documentation:
Multiverse - alternative implementation languages and dependency mappings.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b649189f4">Haxe rewrite options and dependency equivalents are documented</a>. Thanks to xet7.</summary>The Multiverse design compares incremental Haxe/JavaScript modules, a Haxe browser client, a Haxe/Node server and native HashLink, C++ and JVM servers. It maps Meteor platform facilities and WeKan dependencies to Haxe libraries or explicitly custom replacements, and recommends a tested vertical slice before any complete rewrite.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/037eb3144">Go rewrite options and dependency equivalents are documented</a>. Thanks to xet7.</summary>The Go design maps the server, browser, data, authentication, storage, export,
operations, testing and distribution stack to standard-library facilities,
maintained packages or custom compatibility work. It separates Pug as Jade's
JavaScript successor from Go-native html/template and templ rewrites.
The Free Pascal design derives its stack from Omi and Wami: FCL fphttpapp,
httproute, fpjson, linked SQLite, server-rendered HTML, optional
interact.js, Caddy at the TLS edge and mORMot 2 or Brook only when measured
scale requires them.
Development sandbox - the documented editor sandbox uses its maintained, open-source distribution consistently.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4bf68e8c3">The VS Code sandbox is renamed and updated for VSCodium</a>. Thanks to xet7.</summary>The sandbox directory, launcher and documentation now consistently name VSCodium. Obsolete VS Code-specific setup is removed so paths and commands match the editor distribution the sandbox actually installs.
</details>and improves the translation workflow:
Translation policy - correct-language and placeholder integrity.
<details> <summary><a href="https://github.com/wekan/wekan/commit/775c56df9">Every locale file must use the language declared by its tag</a>. Thanks to xet7.</summary>Human-preference protection now applies only to translations written in the locale tag’s language. Mixed or wrongly seeded values must be replaced directly, including same-script cases such as Russian text in Mongolian, with vocabulary review and regression coverage where script detection cannot distinguish them.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/65d86b442">Translation placeholders must match the English source exactly</a>. Thanks to xet7.</summary>Underscore-delimited tokens such as __board__ and percent-prefixed format
tokens such as %s are code, not translatable prose. They must retain their
English spelling, case, count and format. A translated, renamed, missing or
malformed token is restored from the same key in en.i18n.json, even inside an
otherwise human translation, and regression coverage compares token inventories
with English.
The invariant-source regression now follows the completed-locale list and the current fill helper instead of retaining obsolete exclusions. It continues to prove that already completed locales stay at zero fillable English placeholders as translation work advances.
</details>Placeholder repairs - exact named and printf token inventories.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0aa3b9784">Eleven locale files restore their last mismatched placeholder</a>. Thanks to xet7.</summary>Spanish (Argentina), Galician, Gujarati, Hungarian, Polish and Portuguese variants now preserve the same named and printf tokens as their English keys. The repair keeps exact spelling and case, updates obsolete Hungarian prose and replaces copied Portuguese wording with Galician. Whole-file regression coverage proves all eleven locale files are clean and rejects the malformed, missing and wrong-language forms. The direct Galician and Hungarian wording welcomes human review. Another 1,331 mismatched keys across 108 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b3f4007e0">Seventeen locale files restore their remaining placeholder tokens</a>. Thanks to xet7.</summary>Welsh, Greek, Spanish, Basque, Finnish, Croatian and Slovenian variants now preserve exact named, printf and HTML placeholders. The repair covers 34 counted token mismatches plus four Welsh machine-placeholder remnants, removes a duplicated English sentence from Greek and replaces Catalan prose in Basque. Whole-file regression coverage proves all seventeen locale files are clean and rejects the corrupt or wrong-language forms. The direct Welsh, Basque, Finnish, Croatian and Slovenian wording welcomes human review. Another 1,297 mismatched keys across 91 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ce93ac388">Eighteen locale files restore exact tokens and their declared languages</a>. Thanks to xet7.</summary>Asturian, Bulgarian, Czech, Danish, Persian, Hebrew, Japanese, Georgian, Macedonian, Slovak, Serbian, Tamil, Venda and Xhosa now preserve their exact named and printf placeholders. The repair covers 54 counted mismatches plus a hidden Xhosa machine remnant, and replaces same-script wrong-language carryovers according to each locale tag. Whole-file regression coverage proves all eighteen files are clean and rejects the corrupt tokens and copied neighbouring-language forms. These direct translations have mixed confidence and welcome native-speaker review. Another 1,243 mismatched keys across 73 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c844eeb81">Fourteen locale files restore exact placeholders and script variants</a>. Thanks to xet7.</summary>Esperanto, Japanese hiragana, Korean, Malay, Swedish, Telugu, Uzbek and Vietnamese variants now preserve the exact spelling, case and multiplicity of their English placeholders. The repair covers 56 mismatches, replaces copied Spanish in Esperanto, and writes the hiragana and Arabic-script variants in their declared scripts. Whole-file regression coverage proves all fourteen files are clean and rejects translated token names, duplicate prompt values and wrong-language forms. These direct translations have mixed confidence and welcome native-speaker review. Another 1,187 mismatched keys across 59 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0edb3db90">Eleven French, German and Hindi variants restore exact placeholders</a>. Thanks to xet7.</summary>The coherent part of the five-mismatch tier now preserves exact activity,
email, count and search tokens across eleven locale variants. Search examples
such as user:<name> and has:-due remain literals instead of becoming extra
runtime placeholders. Whole-file regression coverage proves all eleven files
are clean and rejects embedded spaces, incomplete named tokens and invented
operator tokens. Another 1,132 mismatched keys across 48 locale files remain;
the four wrongly seeded files from this tier are handled separately in the next
entry.
Breton, Walloon and Wolof no longer use copied French in the five affected activity and search strings, and Klingon no longer uses copied German. Their twenty mismatches now preserve the exact English token inventory while search examples remain literals. Whole-file regression coverage proves all four files are clean and rejects the French and German seed wording. These direct translations have low confidence and welcome native-speaker review. Another 1,112 mismatched keys across 44 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6ce409f05">Ten Azerbaijani, Catalan and Russian tags restore exact placeholders</a>. Thanks to xet7.</summary>The coherent part of the six-mismatch tier restores 60 logical locale values
across nine tracked data files and the ru-RU symlink alias. Azerbaijani
activities retain every board location, Catalan search help restores its full
predicate inventory, and Russian email subjects and errors use exact named and
printf tokens. Whole-file regression coverage proves all ten tags are clean and
rejects corrupted predicates, %1 fragments and substituted URL tokens. The
direct Azerbaijani wording welcomes human review. Another 1,052 mismatched keys
across 34 locale files remain for audited batches.
Italian restores its six activity placeholders, while Venda and Volapük replace wrong-language seed prose as they restore the same exact token inventories. Whole-file regression coverage proves all three files are clean, checks every due-time value and rejects the Italian, Esperanto and French seed wording. The direct Venda and Volapük translations have low confidence and welcome native-speaker review. Another 1,034 mismatched keys across 31 locale files remain for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bacbfa3d5">Numbered Transifex machine placeholders are restored repository-wide</a>. Thanks to xet7.</summary>A deterministic, transactional utility maps PH0, PH1 and later markers to
the protected backtick, HTML, angle, named and printf spans in each matching
English source key. It repaired 1,208 markers in 539 translated values across
Igbo, Odia, Turkmen, Uyghur and Yoruba without replacing their surrounding
prose. Repository-wide negative coverage rejects every marker spelling variant.
Igbo and Yoruba are now placeholder-clean; another 546 mismatched keys across
29 locale files remained for audited batches.
The remaining 32 malformed activity, member, range and search-help values restore every named and printf placeholder while retaining prose in each declared language. Whole-file coverage proves all three locale inventories are clean, and focused negative tests reject the final Odia marker fragment and missing Turkmen and Uyghur values. The direct translations welcome native-speaker review. Another 514 mismatched keys across 26 locale files remained for audited batches.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fdacd27a1">Four languages complete the seven-mismatch placeholder tier</a>. Thanks to xet7.</summary>Acehnese, Indonesian, Turkish and Traditional Chinese restore 28 named and
printf inventories. Acehnese replaces Indonesian seed wording with its existing
kad, senarai and papan terminology; focused negative tests also reject
case-damaged Turkish tokens and translated Chinese token names. All four files
are clean. The direct Acehnese prose has low confidence and welcomes
native-speaker review. Another 486 mismatches across 22 locale files remained.
Latvian, Norwegian Bokmål, Occitan and Hong Kong Traditional Chinese restore 37 named and printf inventories. Occitan also replaces French-seeded prose in its affected values. Whole-file coverage and focused negative tests reject mistyped, translated and wrong-language identifiers. All four files are clean; the direct Occitan prose welcomes native-speaker review. Another 449 mismatches across 18 locale files remained.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e4659eea3">Afrikaans, Romanian and Chinese families restore every placeholder</a>. Thanks to xet7.</summary>Ten locale files restore 145 named and printf inventories. Romanian replaces Italian-seeded activity prose, while Simplified and Traditional Chinese variants reuse only asserted placeholder-clean sibling translations. Family-wide and focused negative coverage proves every file clean and rejects translated or wrong-language identifiers. Another 304 mismatches across eight locale files remain; direct translations welcome native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/baafa9872">Both Ukrainian variants restore their search placeholders</a>. Thanks to xet7.</summary>Sixty-eight values restore exact activity arities plus every operator and predicate in Ukrainian search help. Whole-file and focused coverage proves both variants clean. Direct wording welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4e15e3d88">Estonian restores every translated identifier</a>. Thanks to xet7.</summary>Thirty-five activity, email, due-date, range and search values restore their named and printf inventories. Whole-file negative coverage rejects translated identifiers. Direct wording welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f800c6b73">Four Arabic-family files restore every placeholder</a>. Thanks to xet7.</summary>Arabic, Algerian Arabic, Egyptian Arabic and Moroccan Arabic restore 160 activity and email inventories. Script and identifier regressions cover every file. Moroccan Arabic wording has low dialect confidence and welcomes review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/70bac688e">Tamazight completes the placeholder repair backlog</a>. Thanks to xet7.</summary>The final 41 mismatches now use Latin-script Tamazight prose instead of mixed Arabic and French seed text. Whole-file coverage proves the repository-wide placeholder mismatch count is zero. These translations have low confidence and welcome native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/269404954">Fill reports exclude values that intentionally stay invariant</a>. Thanks to xet7.</summary>Numbers, symbols, empty values, placeholder-only formats, product names and storage backends no longer appear as impossible translation work. Real sentences containing placeholders remain listed. The corrected direct-fill backlog is 202,628 values across 210 locales, with CLI regression coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1e6b0e73f">Five single-value locales complete their direct fills</a>. Thanks to xet7.</summary>Finnish, two Hindi variants and two Ukrainian variants translate their final genuine English values with exact-value and target-script coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e6942e6f">Vietnamese and Hebrew complete their direct-fill tails</a>. Thanks to xet7.</summary>Both Vietnamese variants translate checklist, font-preview and storage terms. Date-format masks are now classified as invariant, completing both Hebrew tags. CLI and language regressions cover both outcomes.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aa7f62ffe">Both Greek variants complete their storage terminology</a>. Thanks to xet7.</summary>Connection-string, container, backup and project-ID values now use Greek, with Greek-script and zero-backlog coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eca9301e9">Four Indic locales complete board-selection messages</a>. Thanks to xet7.</summary>Gujarati, Odia, Punjabi and Telugu translate 28 board-selection and permanent-delete values. Target-script and English-remnant coverage verifies all four completed files.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/78daa9961">Six eight-value locale tails are complete</a>. Thanks to xet7.</summary>Belarusian, Armenian, Georgian, Mongolian, Serbian and Tamil translate their remaining board controls and technical labels. Zero-backlog and script coverage verifies all six files.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9e61400e2">Four nine-value locale tails are complete</a>. Thanks to xet7.</summary>Bulgarian, both Persian variants and Macedonian translate their remaining board controls and technical labels while preserving product and JSON identifiers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/41af211f8">Khmer completes its direct translation fill</a>. Thanks to xet7.</summary>Thirteen board-selection, permanent-delete and wait-spinner values now use Khmer across all tags, with Khmer-script and English-remnant coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4a1d4587f">All Portuguese tags complete their direct fills</a>. Thanks to xet7.</summary>Three tags translate 57 color, menu, location, Office and API report values, using explicit Portuguese alternatives for valid cognates. Zero-backlog and report coverage verifies every tag.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a3bf82281">Thai completes its direct translation fill</a>. Thanks to xet7.</summary>Nineteen board-selection, permanent-delete, Office and API report values now use Thai, with target-script, English-remnant and API-literal coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3bd4ea9f3">Venda and Zulu complete separate direct fills</a>. Thanks to xet7.</summary>Three Tshivenda and two isiZulu tags translate 95 values with distinct language mappings. Zero-backlog and language-distinction coverage verifies every file. The direct translations have low confidence and welcome native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5b9f6874a">Esperanto completes its direct translation fill</a>. Thanks to xet7.</summary>Twenty board, clipboard, Office and API report values now use Esperanto, with zero-backlog, vocabulary and English-remnant coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/64bda540f">Nine Spanish tags complete their direct fills</a>. Thanks to xet7.</summary>The shared 20-value tail completes 180 UI, Office, API report and ticket values. Explicit Spanish alternatives replace valid cognates, and report coverage verifies every tag.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/442065048">Turkish completes its direct translation fill</a>. Thanks to xet7.</summary>Twenty checklist, typography, Office, API report and storage values now use Turkish, with zero-backlog, terminology and English-remnant coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e6924f68">Hungarian completes its direct translation fill</a>. Thanks to xet7.</summary>Twenty-one board, archive, Office and API report values now use Hungarian, with zero-backlog, terminology and protected-literal coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7874d769b">Latvian completes its direct translation fill</a>. Thanks to xet7.</summary>Twenty-one board, archive, Office and API report values now use Latvian, with zero-backlog, terminology and protected-literal coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ac1ce0295">Basque and Uyghur complete their direct translation fills</a>. Thanks to xet7.</summary>Forty-two board, admin, Office, API report and search-help values now use Basque
and Uyghur. Zero-backlog and target-language coverage preserves %s,
__operator_number__, IPv4, IPv6, REST API and WITH_API=true exactly.
The Uyghur translations have low confidence and welcome native-speaker review.
Sixty-six board, admin, Office, API, storage and search-help values now use their target languages. Zero-backlog, terminology and script coverage preserves every protected API and search placeholder. Breton and Yiddish have low confidence and welcome native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6d3662b3">Galician and Xhosa complete their direct fills</a>. Thanks to xet7.</summary>Both Galician tags and Xhosa translate 69 board, admin, Office, API, storage and typography values. Xhosa has low confidence and welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7af96193">Swahili completes its direct translation fill</a>. Thanks to xet7.</summary>Twenty-four board, colour, admin, Office, API and CPU values now use Swahili, with zero-backlog, terminology and protected-literal coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/46351547a">Asturian, Welsh and Uzbek complete their direct fills</a>. Thanks to xet7.</summary>Seven tags translate 175 values without replacing existing human Uzbek text. Zero-backlog coverage preserves network and configuration literals. Asturian and Uzbek have mixed confidence and welcome native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6b3563555">Seven files complete the 26-value translation tier</a>. Thanks to xet7.</summary>Azerbaijani, Croatian, Polish and Slovak translate 182 values with zero-backlog, sibling-consistency and protected-token coverage. Azerbaijani has mixed confidence and welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a518c6990">Four files complete the 27-value translation tier</a>. Thanks to xet7.</summary>Estonian, Romanian and Walloon translate 108 values with zero-backlog, sibling-consistency and protected-token coverage. Walloon has low confidence and welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/762a778aa">Five files complete the 28-value translation tier</a>. Thanks to xet7.</summary>Indonesian, Occitan, Brazilian Portuguese, Turkmen and Tamazight translate 140 values with embedded-JSON, zero-backlog and placeholder coverage. Occitan, Turkmen and Tamazight have low confidence and welcome review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1c34ec39a">Three files complete the 29-value translation tier</a>. Thanks to xet7.</summary>Acehnese and both Czech tags translate 87 values with embedded-JSON, sibling-consistency and protected-token coverage. Acehnese has low confidence and welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0e1cbc1c5">Three files complete the 30-value translation tier</a>. Thanks to xet7.</summary>Both Slovenian tags and Volapük translate 90 values with mixed-language, sibling-consistency and protected-token coverage. Volapük has low confidence and welcomes native-speaker review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9faa361b">Valencian completes its direct translation fill</a>. Thanks to xet7.</summary>Thirty-one board, admin, Office, API and storage values now use Valencian, with zero-backlog, terminology and protected-token coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e8be40c24">Walloon and Yoruba complete the 32-value translation tier</a>. Thanks to xet7.</summary>Sixty-four values now use their target languages with zero-backlog, mixed-language and protected-placeholder coverage. Both have low confidence.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cf9a4ce5f">Italian completes its direct translation fill</a>. Thanks to xet7.</summary>Thirty-three interface, Office, API and storage values now use explicit Italian alternatives for valid cognates, with zero-backlog coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c3debaa3a">Catalan completes its direct translation fill</a>. Thanks to xet7.</summary>Thirty-five values now use standard Catalan terminology distinct from Valencian, with zero-backlog and protected-token coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6eeffd572">Catalan-Spain and Klingon complete their 35-value tails</a>. Thanks to xet7.</summary>Seventy values now use their target languages with zero-backlog, foreign-script and protected-placeholder coverage. Klingon has very low confidence; its older mixed-language seed text remains for the whole-file audit.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f84ba9d2b">Igbo and Swedish complete the 36-value translation tier</a>. Thanks to xet7.</summary>Seventy-two values now use their target languages with zero-backlog, storage-ID and protected-token coverage. Igbo has low confidence and welcomes review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3d55ef1df">Both Afrikaans tags complete their direct fills</a>. Thanks to xet7.</summary>Seventy-four values now use explicit Afrikaans alternatives for valid cognates, with zero-backlog and sibling-consistency coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cbae558f2">Both Malay tags complete their direct fills</a>. Thanks to xet7.</summary>Seventy-six values now use Malay with executable-JSON, zero-backlog and sibling-consistency coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/819f24fdc">Danish completes its direct translation fill</a>. Thanks to xet7.</summary>Forty interface, Office, API and storage values now use explicit Danish alternatives for valid cognates, with zero-backlog coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d50690692">Norwegian Bokmål completes its direct translation fill</a>. Thanks to xet7.</summary>Forty-two interface, Office, API and storage values now use explicit Norwegian alternatives for valid cognates, with zero-backlog coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a28e1df7e">Wolof completes its direct translation fill</a>. Thanks to xet7.</summary>Forty-four values now use Wolof with zero-backlog, terminology and English-remnant coverage. The translations have low confidence.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/87b49dc12">All five French tags complete their direct fills</a>. Thanks to xet7.</summary>Two hundred seventy values now use explicit French phrases for valid cognates, with zero-backlog and sibling-consistency coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1e36d074b">Both West Frisian tags complete their direct fills</a>. Thanks to xet7.</summary>One hundred ten values now use West Frisian with zero-backlog, storage-ID and sibling-consistency coverage. The translations have mixed confidence.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6a9b669d0">All four German tags complete their direct fills</a>. Thanks to xet7.</summary>Two hundred thirty-two values now use established German UI terms instead of English loanword placeholders, with zero-backlog and sibling coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1f14d95b8">Both Dutch tags complete their direct fills</a>. Thanks to xet7.</summary>One hundred thirty-two values now use established Dutch UI terms instead of English loanword placeholders, with zero-backlog and sibling coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cff765a79">Flemish completes its direct translation fill</a>. Thanks to xet7.</summary>Seventy-three values now use verified Dutch terminology plus regional Flemish board and admin wording, with zero-backlog coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c37a15bac">Amharic begins its whole-file direct translation</a>. Thanks to xet7.</summary>The first 50 activity and board-history values now use Amharic. Progress, Ethiopic-script and whole-locale placeholder coverage verifies the batch; 2,116 values remain. The translations have mixed confidence and welcome review.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4af04a4e4">Amharic activity and workspace history gains 50 values</a>. Thanks to xet7.</summary>The second direct batch completes the remaining activity-history and initial workspace strings. Whole-locale placeholder coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7a3465e29">Amharic board interface gains another 50 values</a>. Thanks to xet7.</summary>Workspace, board-selection, list-sizing and checklist controls now use Amharic. Progress, Ethiopic-script and placeholder coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09a05b33f">Amharic admin and board information gains 50 values</a>. Thanks to xet7.</summary>Admin, archive, attachment and board-information values now use Amharic. Whole-locale coverage now also preserves HTML tags; 1,966 values remain.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d5ba00104">Amharic board views and card editing gain 50 values</a>. Thanks to xet7.</summary>Visibility, display-mode, archive and card-editing values now use Amharic. Placeholder, markup and Ethiopic-script coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b70b264d9">Amharic voting and import controls gain 50 values</a>. Thanks to xet7.</summary>Membership, voting, Planning Poker, dependency and import values now use Amharic. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9a7965ef9">Amharic member and typography controls gain 50 values</a>. Thanks to xet7.</summary>Popups, imported-member mapping, themes and typography now use Amharic. Placeholder, markup and Ethiopic-script coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74b4cf093">Amharic navigation and color controls gain 50 values</a>. Thanks to xet7.</summary>Settings, subtasks, starring, card aging, movement dialogs and most color names now use Amharic. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2e9aced92">Amharic roles and custom-field controls gain 50 values</a>. Thanks to xet7.</summary>Colors, board roles, deletion confirmations, copying and custom fields now use Amharic. Percent-token coverage now protects every letter-style placeholder; 1,716 values remain.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/17f7dc538">Amharic email templates and errors gain 50 values</a>. Thanks to xet7.</summary>Profile, email-template, WIP and error values now use Amharic while retaining every message placeholder. Whole-locale invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/68b20172f">Amharic card exports and filters gain 50 values</a>. Thanks to xet7.</summary>User errors, card exports, sorting and initial filters now use Amharic. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8e052624b">Amharic advanced filters and imports gain 50 values</a>. Thanks to xet7.</summary>Advanced filters, activities and board-import instructions now use Amharic while preserving grammar, JSON terms, paths and placeholders. Focused syntax coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/27cfa82d0">Amharic Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello API imports, member mapping, validation and labels now use Amharic while retaining the API URL, year example and percent placeholder. Whole-locale invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/337592476">Amharic board actions and roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, list and archive actions, selection tools, notifications and roles now use Amharic while retaining the board-title placeholder. Whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1ba438326">Amharic privacy and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Notifications, privacy, removal, search, WIP and shortcuts now use Amharic while retaining login markup and member placeholders. Whole-locale invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/14344e047">Amharic tracking and branding gain 50 values</a>. Thanks to xet7.</summary>Time tracking, uploads, custom branding, welcome templates and WIP warnings now use Amharic while retaining URL, API, WIP and numeric terms. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3f4380a52">Amharic limits and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment limits, registration, SMTP, invitations, webhooks and runtime versions now use Amharic while retaining invitation placeholders and technical terms. Whole-locale invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0d96fcf16">Amharic diagnostics and tenancy gain 50 values</a>. Thanks to xet7.</summary>Diagnostics, custom-field display, visibility, organizations and teams now use Amharic while retaining environment variables, protocols, hostnames and multitenancy syntax. Whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ef061a85d">Amharic card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Card dates, colors, deletion, subtasks, minicard display and activity labels now use Amharic while retaining board and percent placeholders. Whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7e4508f23">Amharic activity and automation gain 50 values</a>. Thanks to xet7.</summary>Activity messages and automation workflow/import controls now use Amharic while retaining repeated percent argument order, named tokens and format terms. Focused positional coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b2e8a2747">Amharic scheduled automation gains 50 values</a>. Thanks to xet7.</summary>Visual workflows, schedules, due-date triggers, buttons, sorting and relative dates now use Amharic while retaining workflow product names and structural values. Whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/039266234">Amharic automation phrases gain 50 values</a>. Thanks to xet7.</summary>List, label, member, checklist, card and email automation fragments now use consistent Amharic rule-builder terminology. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/115e6151b">Amharic automation and custom head gain 50 values</a>. Thanks to xet7.</summary>Automation actions, authentication, custom-head settings and layout controls now use Amharic while retaining comma-separated examples, format names and assetlinks.json. Whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/129a95c11">Amharic authentication and reminders gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication, duplication, deletion, positioning, due reminders, drag and editor controls now use Amharic while retaining HTML tags, newlines and activity placeholders. Whole-locale coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/de8263812">Amharic roles and editor behavior gain 50 values</a>. Thanks to xet7.</summary>Multi-card and editor behavior, organizations, notifications, role permissions, weekdays and linked-card warnings now use Amharic while retaining keyboard chords. Whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8cf649e74">Amharic domains and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, domains, shared templates, My Cards, Due Cards, global search and lookup errors now use Amharic while retaining domain examples, Markdown and percent placeholders. Whole-locale coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88f4e76f4">Amharic global-search vocabulary gains 50 values</a>. Thanks to xet7.</summary>Result counts, search operators and predicates now use Amharic while retaining percent tokens and range-placeholder order. Focused order coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8ede9a641">Amharic search help gains 50 values</a>. Thanks to xet7.</summary>Search predicates, validation messages and full operator help now use Amharic while retaining backticked expressions, metavariables, Markdown, newlines and placeholders. Whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/40da7ceed">Amharic dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Sorting, stickers, dependencies, board backgrounds and locations now use Amharic while retaining JSON/SVG, named tokens and double-brace templates. Expanded token coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5f9176edc">Amharic maps and reports gain 50 values</a>. Thanks to xet7.</summary>Maps, server troubleshooting, string templates, reports and office locations now use Amharic while retaining shell commands, HTML entities, IP terms and percent-brace tokens. Expanded token coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/508125bf8">Amharic reports and tickets gain 50 values</a>. Thanks to xet7.</summary>Office, API and recovery reports, loading indicators, organization warnings and ticket workflow terms now use Amharic while retaining REST API, WITH_API=true and Cc. Whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/44be0dc7e">Amharic administration and attachments gain 50 values</a>. Thanks to xet7.</summary>Team and organization controls, Node memory diagnostics, legal notices, checklists and attachment-storage moves now use Amharic while retaining Node, URL, GridFS and S3. Whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/093975714">Amharic storage and repair controls gain 50 values</a>. Thanks to xet7.</summary>Attachment migration and repair, storage statistics, MongoDB compaction and board metadata now use Amharic while retaining GridFS, S3, MongoDB, Meteor and ID. Whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/635af5a0c">Amharic support and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing and upload status, file constraints, custom translations, checklists, support and accessibility now use Amharic while retaining the workspace placeholder, ISO 8601, PDF, JSON and ZIP terms. Whole-locale invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/91a82565b">Amharic lockout and scheduling controls gain 50 values</a>. Thanks to xet7.</summary>Accessibility, brute-force lockouts, user-state filters, scheduled jobs, attachment paths and board maintenance scheduling now use Amharic. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/822f4ba28">Amharic database migration gains 50 values</a>. Thanks to xet7.</summary>Scheduled-job results, filesystem and cloud storage, MongoDB/FerretDB migration and Sandstorm status now use Amharic while retaining the database placeholder, URLs, environment variables and product names. Whole-locale invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/925fd424b">Amharic security and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card loading, security switches, anonymized import/export and backups now use Amharic while retaining markup, environment variables, service names and backup paths. Whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88daa17da">Amharic cloud storage gains 50 values</a>. Thanks to xet7.</summary>Backup scheduling and restoration plus AWS, Azure and Google Cloud setup guidance now use Amharic while retaining JSON, IDs, filenames, field labels and HH:MM. Whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c3378401c">Amharic attachment migration and S3 gain 50 values</a>. Thanks to xet7.</summary>GridFS and attachment migrations, S3 settings, scheduled board operations and storage navigation now use Amharic while retaining storage products, SSL/TLS and the region example. Whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d08603ac">Amharic board migration gains 50 values</a>. Thanks to xet7.</summary>Lost-card recovery, duplicate-list cleanup, archive restoration, URL repairs and migration steps now use Amharic while retaining field identifiers, IDs and URLs. Whole-locale invariant coverage leaves 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc5bcc88e">Amharic migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Remaining migration steps plus job, CPU/memory, filesystem/GridFS and monitoring labels now use Amharic while retaining technical identifiers. Whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1a42efd7b">Amharic migration controls gain 50 values</a>. Thanks to xet7.</summary>Bulk attachment migration, throttling, monitoring, pagination, schedules and progress counters now use Amharic while retaining numeric ranges, percent, ms, CPU, GridFS and S3. Whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eb8d8724f">Amharic repositories and repairs gain 50 values</a>. Thanks to xet7.</summary>Repository login/upload, authentication errors, problem monitoring, broken-card repair, CPU status and event columns now use Amharic while retaining repair placeholders in source order. Whole-locale invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d8ef7ad8a">Amharic completes its whole-file translation</a>. Thanks to xet7.</summary>The final 16 integrity, export, import and number-search values now use Amharic while retaining IP versions, file extensions, product names and exact search metavariables. The fill tool and focused regression now require zero English placeholders across the whole locale.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1ee1df2ca">Assamese begins its whole-file translation</a>. Thanks to xet7.</summary>The first 50 activity and board-history values now use Assamese while retaining every named and percent placeholder. New whole-locale regression coverage checks the exact remaining count, token and HTML inventories, Assamese script and representative activity placeholders, leaving 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/648d42691">Assamese activity and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement, activity phrases and All Boards workspace controls now use Assamese while retaining named placeholders, percent multiplicity and Markdown terminology. Whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7bdc49228">Assamese board layout gains 50 values</a>. Thanks to xet7.</summary>Workspace selection and home-board controls, due dates, list widths, swimlane heights, keyboard shortcuts and card/checklist actions now use Assamese while retaining percent placeholders. Whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c81798a34">Assamese administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administration, archives, templates, attachments, board appearance, members and privacy now use Assamese while retaining named and percent placeholders, URL terminology and the exact strong-tag pair. Whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5444c182a">Assamese board views and archives gain 50 values</a>. Thanks to xet7.</summary>Public-board markup, board navigation and view modes, zoom, calendar, archive/delete guidance and card editing now use Assamese while retaining markup, the workspace placeholder, percent values and comments. Whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d46008c9c">Assamese voting and dialogs gain 50 values</a>. Thanks to xet7.</summary>Card membership, voting, Planning Poker, dependencies and administration, domain, import and export dialogs now use Assamese. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/265675c1c">Assamese members and typography gain 50 values</a>. Thanks to xet7.</summary>Member and dialog titles, imported-member mapping, themes, fonts, colors, avatars, language and permissions now use Assamese while retaining CAS and numeric preview content. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6b9a7b6a8">Assamese navigation and colors gain 50 values</a>. Thanks to xet7.</summary>Starring, automatic list widths, clipboard and card-aging controls, movement, navigation dialogs and the first color vocabulary set now use Assamese. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d4511ccce">Assamese roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Board roles and comment permissions, deletion confirmations, clipboard and link copying, multi-card templates and custom-field types now use Assamese. The translated multi-card example remains valid JSON. Whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4e963dab3">Assamese email and errors gain 50 values</a>. Thanks to xet7.</summary>Custom fields, permanent deletion, WIP and profile dialogs, email templates, validation and import errors now use Assamese while retaining placeholders, paragraph breaks and JSON/CSV/TSV terminology. Focused email-template coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/104f3f12e">Assamese exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account-name errors, card export fields, disk-space messages, list sorting and date, label and member filters now use Assamese while retaining PDF, Excel and file-format names. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b95aa961b">Assamese advanced filters and imports gain 50 values</a>. Thanks to xet7.</summary>Advanced filters and board-import instructions for multiple services and formats now use Assamese while retaining operators, regex, escapes, JSON field names, API paths, extensions and placeholders. Focused syntax coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5179eb764">Assamese Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Safe Trello ZIP/API imports, job controls and results, member mapping, validation, keyboard shortcuts and labels now use Assamese while retaining the Trello URL, API terminology, year example and percent placeholder. Whole-locale invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/95e763454">Assamese list actions and roles gain 50 values</a>. Thanks to xet7.</summary>Board departure, list archive/move/delete actions, settings dialogs, multi-selection, notification modes and normal/assigned roles now use Assamese while retaining the board-title placeholder. Whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1febef555">Assamese navigation and members gain 50 values</a>. Thanks to xet7.</summary>Watched updates, private-page login links, board visibility, member removal, rescue/search/WIP controls, shortcuts, sidebars and starred/home boards now use Assamese while retaining exact HTML and member placeholders. Whole-locale invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/454fe1e71">Assamese tracking and branding gain 50 values</a>. Thanks to xet7.</summary>Subscriptions, tracking, time and shortcut labels, uploads, branding URLs, welcome/template boards, WIP errors and attachment/API limits now use Assamese while retaining numeric ranges and technical terms. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/65eabb5b2">Assamese limits and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment/API transfer limits, registration and invitations, SMTP settings, test mail, webhooks and runtime versions now use Assamese while retaining technical terms and the invitation template's placeholders and paragraph structure. Focused coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8a0c79ca2">Assamese diagnostics and tenancy gain 50 values</a>. Thanks to xet7.</summary>Database/FerretDB/reactivity diagnostics, OS metrics, custom-field display, account visibility, organization tenancy/domains/admins and team propagation now use Assamese while retaining modes, hostnames and environment variables. Whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/504950321">Assamese card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received/end dates, colors, destructive cleanup, subtask/card settings, minicard display, parent paths and label activity now use Assamese while retaining the board and quoted percent placeholders. Whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fa5e543a8">Assamese activity and automation gain 50 values</a>. Thanks to xet7.</summary>Activity phrases, rule management, workflow/list views, triggers, actions, scheduled time and JSON/CSV/Trello Butler import/export now use Assamese while retaining all placeholders and formats. Focused repeated-percent coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bcdc4775e">Assamese scheduled automation gains 50 values</a>. Thanks to xet7.</summary>n8n/Node-RED workflows, schedules, due-date triggers, buttons, sorting, relative dates, units and movement conditions now use Assamese while retaining the count placeholder, N, product names and schedule semantics. Whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/94f492fcb">Assamese automation phrases gain 50 values</a>. Thanks to xet7.</summary>Rule-builder vocabulary for lists, labels, members, attachments, checklists/items, card movement, colors and email actions now uses consistent Assamese automation terminology. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0b44314ae">Assamese automation settings gain 50 values</a>. Thanks to xet7.</summary>Automation actions, authentication, custom-head, manifest and asset-link settings plus layout controls now use Assamese while retaining HTML, JSON, assetlinks.json and product names. Whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3928c5675">Assamese activity settings gain 50 values</a>. Thanks to xet7.</summary>Custom HTML placement, authentication errors, board duplication, activity dates and reminders, account cleanup and drag controls now use Assamese while retaining HTML tags, named activity tokens and percent arguments. Whole-locale invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a4c1ef2e2">Assamese roles and weekdays gain 50 values</a>. Thanks to xet7.</summary>Card-window and editor preferences, organizations, users, notification states, board-role permissions, weekdays and linked-card deletion warnings now use Assamese. Whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b00d2effa">Assamese shared views gain 50 values</a>. Thanks to xet7.</summary>Domains, shared templates, My Cards, Due Cards and global search now use Assamese while retaining example.com, emphasis markers and positional percent arguments. Whole-locale invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b53c5fdc">Assamese search syntax gains 50 values</a>. Thanks to xet7.</summary>Search result counts, operators and predicates now use Assamese while retaining positional and range placeholders. Whole-locale invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b010bf75c">Assamese search help gains 50 values</a>. Thanks to xet7.</summary>Advanced-search validation, paging, operator instructions and combination notes now use Assamese while retaining every code fragment, markup marker, named operator token and positional argument. Whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b2c749c43">Assamese dependencies gain 50 values</a>. Thanks to xet7.</summary>Sorting, completion, stickers, card dependencies, board backgrounds and locations now use Assamese while retaining JSON, SVG and named size/import tokens. Whole-locale invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/db4002442">Assamese diagnostics gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, custom-field formatting, problem reports and Office login metadata now use Assamese while retaining shell commands, HTML entities, the value placeholder and IP protocol names. Whole-locale invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eac3fc68b">Assamese recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>Office/API reports, automatic data recovery, swimlane copying, wait spinners, organization safeguards and support tickets now use Assamese while retaining REST API, WITH_API, MongoDB and mail-header terms. Whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2aa069c88">Assamese administration gains 50 values</a>. Thanks to xet7.</summary>Team and organization administration, Node memory metrics, legal notices, checklist actions and attachment-storage movement now use Assamese while retaining Node, GridFS, S3 and URL terms. Whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fc472e4eb">Assamese attachment storage gains 50 values</a>. Thanks to xet7.</summary>Attachment storage movement and repair, file statistics and MongoDB compaction now use Assamese while retaining filesystem, GridFS, S3, MongoDB, Meteor, oplog and identifier terminology. Whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/448f7d840">Assamese support settings gain 50 values</a>. Thanks to xet7.</summary>Board timing, upload restrictions, custom translations, checklist display, support and accessibility settings now use Assamese while retaining the workspace token, PDF, ISO 8601, JSON and archive formats. Whole-locale invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4abde2c3e">Assamese account protection gains 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force lockouts, user-state filters, scheduled jobs, attachment paths and board maintenance now use Assamese. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7869792ee">Assamese database migration gains 50 values</a>. Thanks to xet7.</summary>Scheduled migration controls, filesystem, S3 and Azure storage, MongoDB to FerretDB migration and Sandstorm migration status now use Assamese while retaining the database token, URLs, environment variables, paths and product names. Whole-locale invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/05f6b9f54">Assamese security controls gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card-loading modes, safe rich-text rendering, import/export controls, user anonymization, activity/watch controls and backup scopes now use Assamese while retaining HTML/markdown examples, environment variables, paths and product names. Whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a24986db5">Assamese cloud backup gains 50 values</a>. Thanks to xet7.</summary>Backup schedules and restore modes plus Google Cloud, AWS S3 and Azure credential guidance now use Assamese while retaining console paths, JSON field names, time syntax and storage product names. Whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/55793c200">Assamese storage migration gains 50 values</a>. Thanks to xet7.</summary>GridFS and S3 configuration, attachment and board migration controls, scheduled board operations and writable storage paths now use Assamese while retaining storage product and protocol names. Whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f193c3089">Assamese board migration gains 50 values</a>. Thanks to xet7.</summary>Board-integrity checks, lost-card recovery, duplicate-list cleanup and avatar/attachment URL repair now use Assamese while retaining ID field names, URLs and storage terminology. Whole-locale invariant coverage leaves 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/48785ba59">Assamese monitoring gains 50 values</a>. Thanks to xet7.</summary>Board-conversion steps, database cleanup, CPU/memory metrics, recurring schedules, export monitoring and job queues now use Assamese while retaining ID, URL, GridFS and CPU terms. Whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6db145bbd">Assamese migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Attachment migration destinations, batch/CPU/delay tuning, logs, progress, monitoring controls and storage statistics now use Assamese while retaining GridFS, S3, CPU, percent and millisecond syntax. Whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/07061ad92">Assamese problem reporting gains 50 values</a>. Thanks to xet7.</summary>OTP/account access, repository management, problem summaries, broken-card repair, CPU load and diagnostic events now use Assamese while retaining named repair tokens, API, OTP and IP terminology. Whole-locale invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e14b77043">Assamese completes its whole-file translation</a>. Thanks to xet7.</summary>The final sixteen event-address, filesystem-integrity, scoped import/export and card-number search values now use Assamese. Zero-backlog coverage retains the operator token, markup and file-format names across all 2,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b78cf2d98">Bashkir begins its whole-file translation</a>. Thanks to xet7.</summary>Fifty activity values for boards, cards, attachments, labels, checklists, comments, custom fields and imports now use Bashkir while retaining every named activity token. Focused Cyrillic and whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/727fd0308">Bashkir activity and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement, positional activity phrases, checklist states and workspace management now use Bashkir while retaining repeated percent order and named location tokens. Whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/52c6d1731">Bashkir workspace controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, card insertion, personal/shared list widths, swimlane heights and checklist actions now use Bashkir while retaining positional activity arguments. Whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ad6520f22">Bashkir board settings gain 50 values</a>. Thanks to xet7.</summary>Administrator announcements, archives, templates, attachments, board backgrounds and member/assignee summaries now use Bashkir while retaining count, size and HTML emphasis tokens. Whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4f098a105">Bashkir board views gain 50 values</a>. Thanks to xet7.</summary>Board visibility, icon ordering, backgrounds, desktop/mobile modes, zoom, calendar navigation, archive guidance and card-edit labels now use Bashkir while retaining workspace, comment, percent and HTML emphasis tokens. Whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c00ec4492">Bashkir card dialogs gain 50 values</a>. Thanks to xet7.</summary>Card membership, voting, Planning Poker, dependencies, organization/team/domain membership and component import/export dialogs now use Bashkir. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/229e4d17c">Bashkir appearance gains 50 values</a>. Thanks to xet7.</summary>Member mapping, theme categories, fonts and sizes, text colors, avatar actions, language and permission dialogs now use Bashkir. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5c2436823">Bashkir navigation and colors gain 50 values</a>. Thanks to xet7.</summary>Starred boards/pages, automatic list width, clipboard input, card aging, movement/dialog accessibility and twenty-two color names now use Bashkir. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/de74b644d">Bashkir roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Board role restrictions, deletion confirmations, clipboard links, JSON card templates and custom-field types/options now use Bashkir while retaining JSON structure. Whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d4dfa3b32">Bashkir account email and errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field dialogs, permanent deletion, profiles, dates, account emails, WIP limits and board/import errors now use Bashkir while retaining email paragraph structure, named tokens and JSON/CSV/TSV terms. Whole-locale invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8bd69df66">Bashkir exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account validation, card PDF/Excel exports, attachment metadata, list sorting and date/label/member filters now use Bashkir while retaining export format names. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a70fcc603">Bashkir advanced filters and imports gain 50 values</a>. Thanks to xet7.</summary>Advanced filters and Kanboard, Deck, OpenProject, issue, Asana, ZenKit, Trello, CSV/TSV, Jira, Excel and WeKan import guidance now use Bashkir while retaining operators, regex, endpoint tokens, API paths and data-format examples. Whole-locale invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0c34816ed">Bashkir Trello imports gain 50 values</a>. Thanks to xet7.</summary>Trello ZIP safety, workspace placement, API credentials, multi-board job progress/cancellation and member mapping now use Bashkir while retaining the API URL, file formats and percent label argument. Whole-locale invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b8bda89b9">Bashkir board roles gain 50 values</a>. Thanks to xet7.</summary>Last-admin safeguards, board departure, list/archive actions, settings dialogs, multi-selection, archive states and assigned-only notification roles now use Bashkir while retaining the board-title token. Whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6cfd6593b">Bashkir privacy and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watched updates, private and public pages, member removal, unsaved-description recovery, card search, WIP controls and keyboard shortcuts now use Bashkir while retaining the HTML login link and member/board tokens. Whole-locale invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/98e649f40">Bashkir tracking and branding gain 50 values</a>. Thanks to xet7.</summary>Tracking, time and overtime, uploads, custom branding and URL schemes, welcome/template boards, WIP warnings and attachment/API limits now use Bashkir. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9323a2c9d">Bashkir administration settings gain 50 values</a>. Thanks to xet7.</summary>Attachment transfer limits, avatar blocking, registration, invitations, SMTP and outgoing/two-way webhooks now use Bashkir while retaining four invitation placeholders and its paragraph structure. Whole-locale invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/af1db66d1">Bashkir diagnostics and tenancy gain 50 values</a>. Thanks to xet7.</summary>Database and operating-system diagnostics, time units, custom-field display, account changes, visibility, shared templates and organization/team tenancy now use Bashkir while retaining configuration names, domain examples and MULTITENANCY=true. Whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/768226375">Bashkir card settings gain 50 values</a>. Thanks to xet7.</summary>Received/end dates, colors, destructive board/notification/list actions, subtask routing, minicard badges and parent-card paths now use Bashkir while retaining the board token and positional label-activity arguments. Whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3377c35cc">Bashkir visual rules gain 50 values</a>. Thanks to xet7.</summary>Attachment/custom-field activity, rule management, the visual workflow builder, event triggers and JSON/CSV/Trello Butler imports now use Bashkir while retaining time/count and positional activity arguments. Whole-locale invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fe28d09c5">Bashkir scheduled rules gain 50 values</a>. Thanks to xet7.</summary>n8n/Node-RED workflow imports, scheduled triggers, due-date and list-duration conditions, card/board buttons, sorting and relative dates now use Bashkir while retaining the unmapped-count token and integration names. Whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c8eeced26">Bashkir rule actions gain 50 values</a>. Thanks to xet7.</summary>Movement, archive, label/member/attachment/checklist conditions and actions, card positioning and rule-email subjects now use Bashkir. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a869ab850">Bashkir customization and rules gain 50 values</a>. Thanks to xet7.</summary>Rule email/checklist/swimlane/date actions, authentication, product naming, custom HTML/manifest/assetlinks metadata and layout controls now use Bashkir while retaining HTML/JSON and assetlinks.json literals. Whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7e74d4c12">Bashkir reminders and layout gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication display, board/swimlane duplication and deletion, date activity, due reminders, mentions, account deletion and resize controls now use Bashkir while retaining body tags, positional arguments, named tokens and multiline structure. Whole-locale invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/341305be8">Bashkir board roles and weekdays gain 50 values</a>. Thanks to xet7.</summary>Multi-card/editor behavior, organization/team/user dialogs, notification management, renaming, board-role permissions/status, weekdays and linked-card deletion safeguards now use Bashkir while retaining keyboard shortcut names. Whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f832359b4">Bashkir shared templates and views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, domains, shared templates, My Cards sorting, due-card views, global search and missing-object errors now use Bashkir while retaining domain examples, Markdown emphasis and percent arguments. Whole-locale invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9fee1dc6">Bashkir search operators gain 50 values</a>. Thanks to xet7.</summary>Search result counts and board/swimlane/list/label/user/member/assignee, status/date/comment/content operators and predicates now use Bashkir while retaining positional and pagination tokens. Whole-locale invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9823596f">Bashkir search guidance gains 50 values</a>. Thanks to xet7.</summary>Operator validation, pagination and the complete advanced-search operator, status, field, sorting, limit and combination guidance now use Bashkir while retaining code spans, Markdown emphasis, angle-bracket parameters, examples, newlines and every search token. Whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/11921ffbf">Bashkir dependencies and backgrounds gain 50 values</a>. Thanks to xet7.</summary>Labels, board/card sorting, completion, stickers, dependency relationships and JSON/SVG imports, board backgrounds and location fields now use Bashkir while retaining imported/unmatched and maximum-size tokens. Whole-locale invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c31b0ecce">Bashkir locations and reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, sorting, activity visibility, string templates and administrative file/security/performance/database/rules/board/ card/impersonation/recovery/office reports now use Bashkir while retaining commands, the value placeholder, HTML entities and IPv4/IPv6 names. Whole-locale invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b32595edb">Bashkir recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>Office/API usage, automatic recovery, swimlane copying, wait indicators, organization/team deletion safeguards and support tickets now use Bashkir while retaining REST API, WITH_API=true, MongoDB and spinner-style names. Whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7539f6a11">Bashkir diagnostics and storage gain 50 values</a>. Thanks to xet7.</summary>Team/organization administration, invitations, Node heap/memory diagnostics, legal notices, checklist/subtask actions and filesystem/GridFS/S3 attachment moves now use Bashkir while retaining runtime and storage names. Whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9fa620a28">Bashkir attachment repair gains 50 values</a>. Thanks to xet7.</summary>Attachment storage moves, location repair, storage statistics/defaults and MongoDB GridFS compaction now use Bashkir while retaining filesystem, GridFS, S3, MongoDB, Compact, oplog, Meteor and identifier names. Whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b36b7f633">Bashkir support and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing/upload status, upload limits, card details, workspace assignment, custom translations, checklist visibility, board ZIP imports and support/ accessibility pages now use Bashkir while retaining the workspace token and ISO/PDF/JSON/Markdown/.zip names. Whole-locale invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cf5b97b9e">Bashkir account protection gains 50 values</a>. Thanks to xet7.</summary>Accessibility metadata, brute-force protection/lockouts, administrator user filters, scheduled jobs, attachment/avatar paths and scheduled board archive, backup and cleanup operations now use Bashkir. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/59702f662">Bashkir storage and migration gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration recovery, filesystem/S3/Azure storage, MongoDB ↔ FerretDB text-data migration and Sandstorm grain migration now use Bashkir while retaining URLs, environment variables, commands, database/version names, paths and the database token. Whole-locale invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d50461797">Bashkir security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card-loading modes, plain-text security rendering, import/export/avatar/activity/watch controls, user anonymization and instance/ organization backups now use Bashkir while retaining markup examples, environment variables, integration names, usernames and backup paths. Whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9225018cc">Bashkir cloud backup setup gains 50 values</a>. Thanks to xet7.</summary>Backup scheduling/restoration and GCS, AWS S3, MinIO, Azure, Cloudflare R2, Backblaze B2, Wasabi and DigitalOcean Spaces setup guidance now use Bashkir while retaining console menu labels, JSON fields, key names, HH:MM and .csv. Whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5a3aaed21">Bashkir migration controls gain 50 values</a>. Thanks to xet7.</summary>GridFS and S3 storage, migration start/pause/stop controls, scheduled board operations, writable paths and attachment/board migration settings now use Bashkir while retaining MongoDB, GridFS, CollectionFS, AWS, MinIO, SSL/TLS and region names. Whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b310f52e8">Bashkir board repair gains 50 values</a>. Thanks to xet7.</summary>Comprehensive board-integrity migration, duplicate-list deletion, lost/archive restoration, list/avatar/file repair, confirmations, progress and migration steps now use Bashkir while retaining swimlaneId, listId, URL and ID names. Whole-locale invariant coverage leaves 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4173a30cc">Bashkir conversion monitoring gains 50 values</a>. Thanks to xet7.</summary>Board-repair steps, conversion status, CPU/memory/filesystem/GridFS monitoring, scheduled frequencies, job queues and export monitoring now use Bashkir while retaining CPU, GridFS, URL and ID names. Whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/97db47bb1">Bashkir migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Bulk storage migration, batch/CPU/delay tuning, background migration guidance, monitoring refresh/export, schedules, operation progress and system-resource totals now use Bashkir while retaining GridFS, S3, CPU, percentages and units. Whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4a37d3d5f">Bashkir repositories and problems gain 50 values</a>. Thanks to xet7.</summary>OTP/account/repository access, API endpoints, problem summaries, repair status, CPU load and diagnostic event fields now use Bashkir while retaining fixed/ unfixable tokens and IP/CPU names. Whole-locale invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0cb7c0ec2">Bashkir completes its final 16 values</a>. Thanks to xet7.</summary>IP address diagnostics, filesystem integrity, scoped card export/import and number-search guidance now use Bashkir while retaining IPv4/IPv6, WeKan, archive/data-format names, the search token and angle-bracket parameter. The authoritative missing-value list is empty, and whole-locale token, markup and key-order coverage verifies all 2,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/85d354077">Bhojpuri activity history gains 50 values</a>. Thanks to xet7.</summary>Board membership restrictions and board/card/list/swimlane creation, deletion, archive, import, attachment, subtask, label, checklist and comment activity now use Bhojpuri while retaining percent and named location/content tokens. New whole-locale regression coverage checks every translated token inventory and HTML tag, representative Bhojpuri terminology and activity placeholders, leaving 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e3e0feb9f">Bhojpuri movement and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement/restoration, member removal, positional activity phrases, checklist states, dates and workspace creation/editing now use Bhojpuri while retaining every positional and named argument. Whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/32ea617a9">Bhojpuri board layout gains 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, card insertion, personal/shared/fixed list widths, keyboard shortcuts, swimlane heights and checklist actions now use Bhojpuri while retaining positional date arguments. Whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19625f136">Bhojpuri board settings gain 50 values</a>. Thanks to xet7.</summary>Administration announcements, archives, templates, attachments, automatic watching, board backgrounds, All Boards display and member/assignee summaries now use Bhojpuri while retaining count/size/percent tokens and HTML emphasis. Whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/458d8bc3b">Bhojpuri board views gain 50 values</a>. Thanks to xet7.</summary>Board visibility, icon ordering, backgrounds, desktop/mobile modes, zoom, calendar/table/statistics views and card/list/swimlane archive/deletion guidance now use Bhojpuri while retaining the workspace token, percent argument and HTML emphasis. Whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e2445e186">Bhojpuri card dialogs gain 50 values</a>. Thanks to xet7.</summary>Card membership, voting, Planning Poker, dependencies, organization/team/ domain management and checklist/swimlane/list/card/board import/export dialogs now use Bhojpuri while retaining the Planning Poker name. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/51a89024e">Bhojpuri member mapping and appearance gain 50 values</a>. Thanks to xet7.</summary>Member/sticker/invite/sorting dialogs, archive restoration, imported-member mapping, themes, fonts, text colors and avatar/language/permission dialogs now use Bhojpuri while retaining CAS, Markdown and preview digits. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5e394b294">Bhojpuri navigation and colors gain 50 values</a>. Thanks to xet7.</summary>Starred boards/pages, automatic list width, clipboard input, card aging, movement/dialog accessibility and twenty-two color names now use Bhojpuri. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4aac0b48a">Bhojpuri permissions and custom fields gain 50 values</a>. Thanks to xet7.</summary>Comment/read/worker permissions, deletion confirmations, clipboard actions, copy/import dialogs and custom-field basics now use Bhojpuri while retaining the copied-card JSON structure. Whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d7b8b1acf">Bhojpuri account emails and errors gain 50 values</a>. Thanks to xet7.</summary>Custom fields, profile/date dialogs, account emails, WIP settings and import/ authorization errors now use Bhojpuri while retaining every account, site, board, inviter and URL placeholder. Whole-locale invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b81f241e7">Bhojpuri card exports and filters gain 50 values</a>. Thanks to xet7.</summary>User/account errors, attachment-free and card exports, Excel fields, sorting, and date/label/member filters now use Bhojpuri while retaining Excel and PDF names. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a0fefe979">Bhojpuri board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced filters and board imports from Kanboard, Deck, OpenProject, issue trackers, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan now use Bhojpuri. Operators, examples, API paths, JSON fields, extensions and source/endpoint placeholders remain exact. Whole-locale invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2d1f0a938">Bhojpuri Trello jobs and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello ZIP/API imports, job controls, imported-member mapping, validation
messages and labels now use Bhojpuri while retaining the API-key URL and %s
label placeholder. Whole-locale invariant coverage leaves 1,516 values.
Board departure, archive/list actions, settings, multi-selection, notification states and normal-role permissions now use Bhojpuri while retaining the board title placeholder. Whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c4e81b0de">Bhojpuri privacy and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watching/privacy, member removal, search/WIP controls, keyboard shortcuts,
sidebars, signup and starred/default boards now use Bhojpuri while retaining
the login anchor and its %s placeholder. Whole-locale invariant coverage
leaves 1,416 values.
Subscriptions, time tracking, assignee/label shortcuts, uploads, custom branding URLs, welcome/template labels, WIP errors and attachment/API limits now use Bhojpuri. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a6381700a">Bhojpuri server settings gain 50 values</a>. Thanks to xet7.</summary>Attachment/API limits, registration invitations, SMTP, webhooks and system version labels now use Bhojpuri while retaining all invitation placeholders and the SMTP, TLS, API, Node and Meteor identifiers. Whole-locale invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/86a4d9897">Bhojpuri diagnostics and tenancy gain 50 values</a>. Thanks to xet7.</summary>Diagnostics, custom-field display, account and board visibility, organization tenancy/admin controls, teams and timestamps now use Bhojpuri while retaining environment variables, modes, sample domains and technical identifiers. Whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/60d228c7e">Bhojpuri card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Card dates/colors, destructive board and notification actions, duplicate-list cleanup, subtask/card/minicard settings, parent paths and label activity now use Bhojpuri while retaining the board and positional activity placeholders. Whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eba96421c">Bhojpuri automation rules gain 50 values</a>. Thanks to xet7.</summary>Activity messages, rule management, workflow builder triggers/actions and JSON, CSV and Trello Butler imports now use Bhojpuri. Positional custom-field values, time and imported-count placeholders retain their source order and spelling. Whole-locale invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7dbac7b00">Bhojpuri scheduled automation gains 50 values</a>. Thanks to xet7.</summary>Workflow formats, scheduled/button triggers, due-date timing, list sorting, card completion, bulk moves and relative dates now use Bhojpuri while retaining the imported-count placeholder and n8n, Node-RED and WeKan names. Whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a4dd2ee13">Bhojpuri automation actions gain 50 values</a>. Thanks to xet7.</summary>List/archive movements, label/member/attachment/checklist triggers, card and checklist actions and email actions now use consistent, compact Bhojpuri sentence-building fragments. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/922fc392f">Bhojpuri automation configuration gains 50 values</a>. Thanks to xet7.</summary>Archive/member/checklist/date automation actions, authentication, custom HTML/ JSON head configuration and layout controls now use Bhojpuri while retaining manifest and assetlinks filenames/formats. The checklist example is translated rather than left as English. Whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b82b2f74c">Bhojpuri activity and interface settings gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML placement, authentication, duplication/counts, swimlane deletion, date/reminder activities, mentions, destructive account/team/ organization actions and UI behavior now use Bhojpuri. Body tags, placeholders, percent arguments and line breaks remain exact. Invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5c32b8e69">Bhojpuri board roles gain 50 values</a>. Thanks to xet7.</summary>Multi-window cards, editor key behavior, organization/team/user dialogs, notifications, board-role permissions/status, weekdays, voting and linked-card deletion guards now use Bhojpuri while retaining keyboard combinations and the Admin Panel name. Whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8c8a2acae">Bhojpuri shared templates and searches gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, domain/template sharing, My Cards and Due Cards views, global search and lookup errors now use Bhojpuri while retaining the domain example, Markdown emphasis and positional lookup arguments. Whole-locale invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2c68b10de">Bhojpuri global-search labels gain 50 values</a>. Thanks to xet7.</summary>Search result counts and global-search operator/predicate labels now use Bhojpuri while retaining percent and start/end/total pagination placeholders. Whole-locale invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ab65b72ec">Bhojpuri global-search help gains 50 values</a>. Thanks to xet7.</summary>Search validation, pagination and full operator/status/note documentation now use Bhojpuri while retaining every named placeholder, Markdown code/emphasis, angle-bracket metavariable, operator example and deliberate line break. Whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/80b9efc62">Bhojpuri dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Sorting/completion, stickers, dependency relationships/imports, board backgrounds and card locations now use Bhojpuri while retaining import counters, the background-size placeholder and JSON/SVG terminology. Whole-locale invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e2f0cc4ca">Bhojpuri maps and reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server-error troubleshooting, sorting, swimlane movement, string-template fields and admin reports now use Bhojpuri while retaining shell commands, the value placeholder, space entities, IPv4 and IPv6. Whole-locale invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ca480f5e">Bhojpuri recovery and tickets gain 50 values</a>. Thanks to xet7.</summary>Office/API/recovery reports, recovery maintenance, swimlane copying, wait spinners, card sizing, organization/team deletion guards and tickets now use Bhojpuri while retaining REST API and WITH_API=true. Whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3834d7804">Bhojpuri diagnostics and storage gain 50 values</a>. Thanks to xet7.</summary>Team/organization invitations, Node heap diagnostics, legal notices, checklist, subtask and attachment actions and storage moves now use Bhojpuri while retaining Node, GridFS, S3, filesystem and URL terminology. Whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b72cec826">Bhojpuri storage repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment/avatar storage moves and repair, default storage, file statistics and MongoDB GridFS compaction now use Bhojpuri while retaining filesystem, GridFS, S3, MongoDB, replica-set, oplog and ID terminology. Whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/45553b91c">Bhojpuri upload and support settings gain 50 values</a>. Thanks to xet7.</summary>Board status, upload progress, password/login prompts, file restrictions, PDF fallback, workspace assignment, custom translations, checklist collapsing, support and accessibility now use Bhojpuri while retaining the workspace placeholder, ISO 8601, PDF, JSON and .zip. Invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6b60492f9">Bhojpuri account security and scheduling gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force login lockouts, admin user filters, scheduled jobs, attachment/avatar paths and scheduled board operations now use Bhojpuri. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/91260e7d1">Bhojpuri storage and database migration gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job/migration control, filesystem/S3/Azure storage, MongoDB/FerretDB text-data migration and Sandstorm migration status now use Bhojpuri while retaining URLs, ports, environment variables, configuration literals, versions, paths and the database placeholder. Invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6f5d8d8f3">Bhojpuri security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card-loading modes, secure rendering, import/export/avatar/ anonymization policies, activity/watch controls and backups now use Bhojpuri. Anchor/comment syntax, environment variables, formats, providers and backup paths remain literal. Whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f3f7a810d">Bhojpuri cloud storage setup gains 50 values</a>. Thanks to xet7.</summary>Scheduled backup restore plus GCS, AWS/S3, Azure, MinIO and compatible cloud storage setup now use Bhojpuri while retaining console paths, credential field names, JSON/CSV formats, time syntax, identifiers and product names. Whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a0f1a3869">Bhojpuri migration controls gain 50 values</a>. Thanks to xet7.</summary>GridFS/S3 storage, migration controls and monitoring, scheduled board operations and attachment settings now use Bhojpuri while retaining MongoDB, GridFS, CollectionFS, AWS S3, MinIO, SSL/TLS, region examples and paths. Whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1525d0182">Bhojpuri board-integrity migrations gain 50 values</a>. Thanks to xet7.</summary>Duplicate/absent lists, lost cards, archive restoration, avatar/file URLs, confirmations, progress and validation steps now use Bhojpuri while retaining swimlaneId, listId, IDs and URLs. Whole-locale invariant coverage leaves 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/62398f85e">Bhojpuri conversion monitoring gains 50 values</a>. Thanks to xet7.</summary>Board-migration steps, conversion status, cleanup, CPU/memory diagnostics, schedules, export monitoring and filesystem/GridFS statistics now use Bhojpuri while retaining IDs, CPU, GridFS and numeric intervals. Whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/813969eaf">Bhojpuri migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Migration tuning, background progress/logs, monitoring refresh/export, schedules, storage distribution and system resources now use Bhojpuri while retaining CPU percentages, millisecond units, numeric ranges, GridFS and S3. Whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/faf451531">Bhojpuri problem monitoring gains 50 values</a>. Thanks to xet7.</summary>OTP/account access, repository management, API endpoints, problem monitoring, broken-card repair, CPU status and event fields now use Bhojpuri while retaining OTP/API/CPU names and fixed/unfixable counters. Whole-locale invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/16fe17bb4">Bhojpuri completes its final 16 values</a>. Thanks to xet7.</summary>IP/IPv4/IPv6 event labels, filesystem integrity, card/scoped export, WeKan file import and the card-number search operator now use Bhojpuri while retaining the named operator placeholder, number metavariable, formats and product names. Whole-locale invariant coverage proves zero English placeholders across all 2,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/063ea0160">Bambara activity and checklists gain 50 values</a>. Thanks to xet7.</summary>Board/card/list/swimlane activity, attachments, subtasks, labels, checklists, comments, custom fields and archive/import actions now use Bambara. New whole-locale regression coverage checks all token/tag inventories and leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/37faf9fea">Bambara movement and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement, membership, compact activity/checklist messages and All Boards workspace controls now use Bambara while retaining every positional and named placeholder in source order. Whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4111b6f7d">Bambara board and list controls gain 50 values</a>. Thanks to xet7.</summary>Workspace settings, board selection and Home-board controls now use Bambara, along with list and swimlane sizing, keyboard shortcuts and common card, checklist and member actions. Positional placeholders remain exact, and whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e43499da">Bambara archives and board visibility gain 50 values</a>. Thanks to xet7.</summary>Archive operations, attachments, templates, board backgrounds, All Boards settings, member and assignee labels and private-board messaging now use Bambara. Named and positional placeholders and the strong-emphasis tags remain exact, and whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/be89ad924">Bambara board views and archive guidance gain 50 values</a>. Thanks to xet7.</summary>Board appearance and view modes, zoom and calendar controls, card and list archive guidance and common card editing labels now use Bambara. Named and positional placeholders, percentages and emphasis tags remain exact, and whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c5de4e783">Bambara voting and import dialogs gain 50 values</a>. Thanks to xet7.</summary>Voting, Planning Poker, card dependencies, organizations, teams, account and background administration and import/export popup titles now use Bambara. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2a0c101c8">Bambara member mapping and appearance gain 50 values</a>. Thanks to xet7.</summary>Popup labels, archived-item restoration, imported-member mapping, themes, fonts, avatars, language selection and permission controls now use Bambara. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5b0d1b728">Bambara navigation and color names gain 50 values</a>. Thanks to xet7.</summary>Automatic list width, card aging, keyboard navigation, accessible close labels, board restoration guidance and the first color-name set now use Bambara. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d9b50b13b">Bambara roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Board roles and permissions, destructive confirmations, clipboard and linking actions, bulk-template copying and custom-field controls now use Bambara. The embedded JSON example retains valid syntax, and invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1cc76f885">Bambara email templates and errors gain 50 values</a>. Thanks to xet7.</summary>Account enrollment, invitation, password-reset and verification emails now use Bambara, along with WIP controls and board, JSON, CSV, import and linked-card errors. Every email placeholder remains exact, and invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aff357749">Bambara card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account-name conflicts, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Bambara. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4e2632fc1">Bambara advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced-filter help and Kanboard, Deck, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Bambara. Operators, regex, escapes, JSON fields, API paths and named placeholders remain exact, and invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f4cbf2c57">Bambara Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello API credentials, workspace and board selection, import-job lifecycle, member mapping, date and user validation, shortcuts and label controls now use Bambara. The API URL and positional placeholder remain exact, and invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9a575561">Bambara archives and multi-selection gain 50 values</a>. Thanks to xet7.</summary>Board departure, list and card archive actions, user, team and organization settings, multi-selection, muted notifications and normal-role permissions now use Bambara. The board-title placeholder remains exact, and invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2c6be34a6">Bambara shortcuts and board visibility gain 50 values</a>. Thanks to xet7.</summary>Watch notifications, public and private pages, member removal, rescue dialogs, search and WIP controls, keyboard shortcuts, sidebars, signup and starred and default boards now use Bambara. Markup and all placeholders remain exact, and invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9a17c8ac9">Bambara tracking and custom branding gain 50 values</a>. Thanks to xet7.</summary>Time tracking, assignee and label shortcuts, uploads, custom branding URLs, welcome and template boards, WIP errors and attachment limits now use Bambara. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0035fcd0c">Bambara attachment limits and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API size limits, registration, invitations, SMTP configuration, invitation emails, authorization, webhooks and diagnostic version labels now use Bambara. Every email token remains exact, and invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/889f15ee5">Bambara diagnostics and organization settings gain 50 values</a>. Thanks to xet7.</summary>Runtime, OS and memory diagnostics, time units, custom-field display controls, account and board visibility settings and organization tenancy, domains and administration now use Bambara. Environment names, modes and example hostnames remain literal, and invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/98aec264b">Bambara card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, assignment metadata, board and notification deletion, subtask and card settings, minicard fields, parent and source relationships and label activity now use Bambara. Named and positional placeholders remain exact, and invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6e649e940">Bambara automation workflows gain 50 values</a>. Thanks to xet7.</summary>Attachment and custom-field activity, automation rule controls and the visual workflow and JSON, CSV and Trello Butler import/export paths now use Bambara. Every positional and named token remains exact, and invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f16b1dac0">Bambara scheduled automation triggers gain 50 values</a>. Thanks to xet7.</summary>Visual-workflow formats, scheduled, due, list and button triggers, relative dates, sorting, completion and card movement now use Bambara. The named count, day marker, product names and weekday range remain exact, and invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/19ca9756b">Bambara automation actions gain 50 values</a>. Thanks to xet7.</summary>List, label, member, attachment and checklist triggers and card movement, membership, color, checklist-item and email actions now use Bambara. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d5d40e758">Bambara rule and custom-head settings gain 50 values</a>. Thanks to xet7.</summary>Archive, label, card, member, checklist, swimlane and date-field rule actions now use Bambara, along with authentication, custom product, head, manifest, assetlinks, layout and board-list settings. JSON and HTML formats remain literal, and invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/16fb66179">Bambara activity reminders and layout controls gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication, board duplication, destructive actions, received, start, due and end activities, reminders, mentions, drag resizing, editor submission and multi-card behavior now use Bambara. Markup, multiline structure and every token remain exact, and invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/84e821ac2">Bambara role status and weekdays gain 50 values</a>. Thanks to xet7.</summary>Multi-window and editor behavior, organization, team and user dialogs, notification controls, board-role permissions and status, weekdays, ownership, voting and linked-card deletion guards now use Bambara. Whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/411663d0d">Bambara shared templates and search views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, domains, shared templates, My Cards, Due Cards, global search and lookup errors now use Bambara. Every positional placeholder and emphasis marker remains exact, and invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cfd6b435">Bambara search operators and predicates gain 50 values</a>. Thanks to xet7.</summary>Global-search lookup and result-count messages and board, list, user, status, date, organization, content and existence operators and predicates now use Bambara. Every positional and range placeholder remains exact, and invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/93a39f417">Bambara global-search help gains 50 values</a>. Thanks to xet7.</summary>Operator validation and the complete global-search operator, status, field, sorting, limit and composition documentation now use Bambara. Code examples, metavariables, formatting, semantic literals and every named and positional token remain exact, and invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/185221c6c">Bambara dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Label and board sorting, card completion, stickers, dependency relationships and JSON/SVG imports, board backgrounds and location fields now use Bambara. Import counts and the background-size placeholder remain exact, and invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eaa812a7f">Bambara maps and administrative reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, card and swimlane sorting, string templates, file, security, speed, test, CPU, database, rule, board, card, impersonation, recovery and Office reports now use Bambara. Commands, entities, format placeholders and IP names remain exact, and invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ff6eefc3e">Bambara recovery and ticketing gain 50 values</a>. Thanks to xet7.</summary>Office, REST API and recovery reports, recovery maintenance, swimlane copying, wait indicators, card sizing, organization and team deletion guards, tickets, requests, sorting and card details now use Bambara. Technical flags and service names remain literal, and invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/df05278e7">Bambara diagnostics and attachment storage gain 50 values</a>. Thanks to xet7.</summary>Team and organization assignment, invitations, Node heap and memory diagnostics, legal notices, checklist and subtask actions and filesystem, GridFS and S3 attachment movement now use Bambara. Technical storage and runtime names remain recognizable, and invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5f5f4c78b">Bambara storage repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment and avatar storage migration and location repair, storage defaults, progress and statistics, identifiers and MongoDB GridFS compaction now use Bambara. Storage, replica, oplog and identifier terminology remains recognizable, and invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74faf789d">Bambara uploads, support and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board status and time summaries, upload progress and restrictions, login fields, PDF preview, workspace dragging, custom translations, checklist collapsing, support and accessibility now use Bambara. The workspace token and technical format names remain exact, and invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/115860bbb">Bambara account protection and scheduled jobs gain 50 values</a>. Thanks to xet7.</summary>Accessibility content, brute-force account lockout settings and user controls, Admin Panel people filters, scheduled jobs, attachment and avatar paths and board archive, backup and cleanup scheduling now use Bambara. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2552328f4">Bambara storage and database migration gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration controls, filesystem, S3 and Azure storage, MongoDB-to-FerretDB text-data migration and Sandstorm migration status now use Bambara. URLs, ports, environment names, commands, product names, paths and the database placeholder remain exact, and invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6cd50d1e8">Bambara security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, card-loading performance, safe rich-text rendering, global import, export, avatar, activity, notification and watch controls, user anonymization and instance or organization backups now use Bambara. Environment variables, markup examples, paths and counter examples remain exact, and invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/869778143">Bambara backup schedules and cloud storage gain 50 values</a>. Thanks to xet7.</summary>Backup schedules and restores and GCS, S3 and Azure storage configuration now use Bambara, including the provider-console guidance for MinIO, R2, B2, Wasabi and Spaces. Time formats, ranges, JSON and CSV names, console paths, roles, keys and product names remain exact, and invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6352abab1">Bambara migration controls and S3 settings gain 50 values</a>. Thanks to xet7.</summary>GridFS enablement, CollectionFS movement, S3 settings, archive, backup and cleanup schedules, attachment monitoring and board migration controls now use Bambara. Paths and technical storage names remain recognizable, and invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/db205e56b">Bambara board-repair migrations gain 50 values</a>. Thanks to xet7.</summary>Comprehensive board migration, duplicate-list cleanup, lost-card and archive
restoration, missing-list repair, avatar and attachment URL repair and migration
progress now use Bambara. The swimlaneId and listId identifiers remain exact,
and invariant coverage leaves 166 values.
Board conversion and repair steps, cleanup, CPU and memory diagnostics, filesystem and GridFS monitoring, schedules and job details now use Bambara. Technical storage and runtime names remain recognizable, and invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fb5eab7b5">Bambara migration monitoring gains 50 values</a>. Thanks to xet7.</summary>Attachment-storage migration targets, resource thresholds, batch tuning, monitoring exports, schedules, progress controls and storage totals now use Bambara. Numeric ranges and technical storage names remain exact, and invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bb7d42fca">Bambara accounts and event monitoring gain 50 values</a>. Thanks to xet7.</summary>OTP and account access, repository management, problem summaries, broken-card repair, CPU status and event metadata now use Bambara. Named repair counters remain exact, and invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/926ff59d2">Bambara whole-file translation is complete</a>. Thanks to xet7.</summary>IP labels, filesystem integrity, scoped board import and export guidance and the number-search operator now use Bambara. The named operator, markup, file formats and product names remain exact. The final 16-value batch completes all 2,166 source values, with whole-locale token and tag regression coverage.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8b4168928">Bengali activity and checklists gain 50 values</a>. Thanks to xet7.</summary>Board, card, list and swimlane activity, attachments, subtasks, labels, checklists, comments, custom fields and archive/import actions now use Bengali. New whole-locale regression coverage checks all token and tag inventories and leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88cf43f3a">Bengali movement and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement, membership, compact activity and checklist messages and All Boards workspace controls now use Bengali. Named and positional placeholders remain exact, and whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3792cc922">Bengali board and list controls gain 50 values</a>. Thanks to xet7.</summary>Workspace settings, board selection and Home-board controls now use Bengali, along with list and swimlane sizing, keyboard shortcuts and common card, checklist and member actions. Positional placeholders remain exact, and whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e7f7d95ff">Bengali archives and board visibility gain 50 values</a>. Thanks to xet7.</summary>Archive operations, attachments, templates, board backgrounds, All Boards settings, member and assignee labels and private-board messaging now use Bengali. Named and positional placeholders and the strong-emphasis tags remain exact, and whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c7d67ee34">Bengali board views and archive guidance gain 50 values</a>. Thanks to xet7.</summary>Board appearance and view modes, zoom and calendar controls, card and list archive guidance and common card editing labels now use Bengali. Named and positional placeholders, percentages and emphasis tags remain exact, and whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1e57724fe">Bengali voting and import dialogs gain 50 values</a>. Thanks to xet7.</summary>Voting, Planning Poker, card dependencies, organizations, teams, account and background administration and import/export popup titles now use Bengali. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4645d6a12">Bengali member mapping and appearance gain 50 values</a>. Thanks to xet7.</summary>Popup labels, archived-item restoration, imported-member mapping, themes, fonts, avatars, language selection and permission controls now use Bengali. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/33a240462">Bengali navigation and color names gain 50 values</a>. Thanks to xet7.</summary>Automatic list width, card aging, keyboard navigation, accessible close labels, board restoration guidance and the first color-name set now use Bengali. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a5d30b791">Bengali roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Board roles and permissions, destructive confirmations, clipboard and linking actions, bulk-template copying and custom-field controls now use Bengali. The embedded JSON example retains valid syntax, and invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6014df2be">Bengali email templates and errors gain 50 values</a>. Thanks to xet7.</summary>Account enrollment, invitation, password-reset and verification emails now use Bengali, along with WIP controls and board, JSON, CSV, import and linked-card errors. Every email placeholder remains exact, and invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/89eb712c5">Bengali card exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account-name conflicts, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Bengali. Whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/30a566ec6">Bengali advanced filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Advanced-filter help and Kanboard, Deck, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan imports now use Bengali. Operators, regex, escapes, JSON fields, API paths and named placeholders remain exact, and invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/52bad0ab2">Bengali Trello imports and member mapping gain 50 values</a>. Thanks to xet7.</summary>Trello API credentials, workspace and board selection, import-job lifecycle, member mapping, date and user validation, shortcuts and label controls now use Bengali. The API URL and positional placeholder remain exact, and invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b60072a2f">Bengali archives and multi-selection gain 50 values</a>. Thanks to xet7.</summary>Board departure, list and card archive actions, user, team and organization settings, multi-selection, muted notifications and normal-role permissions now use Bengali. The board-title placeholder remains exact, and invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d4cfc2cf1">Bengali shortcuts and board visibility gain 50 values</a>. Thanks to xet7.</summary>Watch notifications, public and private pages, member removal, rescue dialogs, search and WIP controls, keyboard shortcuts, sidebars, signup and starred and default boards now use Bengali. Markup and all placeholders remain exact, and invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d1b061c2">Bengali tracking and custom branding gain 50 values</a>. Thanks to xet7.</summary>Time tracking, assignee and label shortcuts, uploads, custom branding URLs, welcome and template boards, WIP errors and attachment limits now use Bengali. Whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3e5c9fb22">Bengali attachment limits and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API size limits, registration, invitations, SMTP configuration, invitation emails, authorization, webhooks and diagnostic version labels now use Bengali. Every email token remains exact, and invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bb33a9c63">Bengali diagnostics and organization settings gain 50 values</a>. Thanks to xet7.</summary>Runtime, OS and memory diagnostics, time units, custom-field display controls, account and board visibility settings and organization tenancy, domains and administration now use Bengali. Environment names, modes and example hostnames remain literal, and invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7be653a05">Bengali card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, assignment metadata, board and notification deletion, subtask and card settings, minicard fields, parent and source relationships and label activity now use Bengali. Named and positional placeholders remain exact, and invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d5bcdc722">Bengali automation workflows gain 50 values</a>. Thanks to xet7.</summary>Attachment and custom-field activity, automation rule controls and the visual workflow and JSON, CSV and Trello Butler import/export paths now use Bengali. Every positional and named token remains exact, and invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4e0b0361c">Bengali scheduled automation gains 50 values</a>. Thanks to xet7.</summary>Workflow formats, scheduled, due, list and button triggers, relative dates, sorting and card-completion actions now use Bengali. The unmapped-count and time tokens remain exact, and invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bafe6ee33">Bengali automation actions gain 50 values</a>. Thanks to xet7.</summary>Card movement, archive, label, member, attachment, checklist and email conditions and actions now use Bengali. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/59b153d0d">Bengali automation and custom-head settings gain 50 values</a>. Thanks to xet7.</summary>Further automation actions, authentication, custom HTML/JSON configuration, assetlinks, layout and board-list settings now use Bengali. JSON and HTML names remain literal, and invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a44514ff7">Bengali reminders and layout controls gain 50 values</a>. Thanks to xet7.</summary>Board duplication, swimlane and account deletion, date activities, reminders, mentions, card positioning, drag controls and editor behavior now use Bengali. HTML tags and every activity placeholder remain exact, and invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aac0cc636">Bengali roles and editor behavior gain 50 values</a>. Thanks to xet7.</summary>Multi-window and editor behavior, organization, team and user dialogs, notification controls, board-role permissions, weekdays and linked-card deletion guards now use Bengali. Invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/94be3426f">Bengali templates and global search gain 50 values</a>. Thanks to xet7.</summary>Checklist display, domains, shared templates, My Cards, Due Cards and global search now use Bengali. Markdown emphasis and positional lookup placeholders remain exact, and invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8f3c7ffc7">Bengali global-search vocabulary gains 50 values</a>. Thanks to xet7.</summary>Search result counts and board, swimlane, list, label, user, status, date, sorting, organization, team, custom-field and existence operators and predicates now use Bengali. Named and positional count tokens remain exact, and invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/633c3115d">Bengali global-search help gains 50 values</a>. Thanks to xet7.</summary>Operator validation, pagination and the complete global-search syntax and notes now use Bengali. Named tokens, angle-bracket fields, Markdown emphasis, code examples and line breaks remain exact, and invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/868db2640">Bengali dependencies and locations gain 50 values</a>. Thanks to xet7.</summary>Card and board sorting, completion, stickers, dependency types and imports, board backgrounds and card locations now use Bengali. Named import counters and the background-size placeholder remain exact, and invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9330bf65d">Bengali maps and admin reports gain 50 values</a>. Thanks to xet7.</summary>Map detection, server troubleshooting, sorting, board activity, string-template fields and file, security, speed, test, CPU, database, rule, board, card, impersonation, recovery and Office reports now use Bengali. Commands, HTML entities and the value placeholder remain exact, and invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7400196ca">Bengali recovery and ticketing gain 50 values</a>. Thanks to xet7.</summary>Office, REST API and recovery reports, recovery maintenance, swimlane copying, wait indicators, card sizing, organization and team deletion guards, tickets, requests, sorting and card details now use Bengali. Technical flags and service names remain literal, and invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e2d839b63">Bengali diagnostics and attachment storage gain 50 values</a>. Thanks to xet7.</summary>Team and organization assignment, invitations, Node heap and memory diagnostics, legal notices, checklist and subtask actions and filesystem, GridFS and S3 attachment movement now use Bengali. Technical storage and runtime names remain recognizable, and invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/df7ab3c63">Bengali storage repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment and avatar storage migration and location repair, storage defaults, progress and statistics, identifiers and MongoDB GridFS compaction now use Bengali. Storage, replica, oplog and identifier terminology remains recognizable, and invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8657b0f22">Bengali uploads, support and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board status and time summaries, upload progress and restrictions, login fields, PDF preview, workspace dragging, custom translations, checklist collapsing, support and accessibility now use Bengali. The workspace token and technical format names remain exact, and invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/75ca2fd57">Bengali account protection and scheduled jobs gain 50 values</a>. Thanks to xet7.</summary>Accessibility labels, brute-force login protection and locked-user controls, scheduled jobs, attachment and avatar paths and scheduled board operations now use Bengali. Storage and cron terminology remains recognizable, and invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/66456dd38">Bengali storage and database migration gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job and migration controls, filesystem, S3 and Azure storage and MongoDB, FerretDB and Sandstorm migration guidance now use Bengali. URLs, environment-variable names and the database placeholder remain exact, and invariant coverage leaves 366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8af5acc9d">Bengali security controls and backups gain 50 values</a>. Thanks to xet7.</summary>Sandstorm cleanup, adaptive card loading, safe rich-text rendering, disabled imports, exports, activities, notifications and watches, anonymized users and backup scope now use Bengali. HTML and technical configuration names remain exact, and invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/73a108ed4">Bengali cloud backup setup gains 50 values</a>. Thanks to xet7.</summary>Backup schedules and restore modes plus Google Cloud, S3 and Azure credential, permission and console guidance now use Bengali. JSON, product names and menu paths remain recognizable, and invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/621729fd4">Bengali storage migrations gain 50 values</a>. Thanks to xet7.</summary>GridFS and S3 settings, migration lifecycle controls, scheduled board operations, writable storage paths and attachment and board migration settings now use Bengali. Product and protocol names remain exact, and invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9bf2bc791">Bengali board integrity repair gains 50 values</a>. Thanks to xet7.</summary>Board structure checks, duplicate-list cleanup, lost-card and archive recovery, missing-list repair, avatar and attachment URL repair and migration progress now use Bengali. Data identifiers remain exact, and invariant coverage leaves 166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/392246c63">Bengali conversion and job monitoring gain 50 values</a>. Thanks to xet7.</summary>Board conversion and repair steps, cleanup, CPU and memory diagnostics, scheduled-job frequencies, export monitoring and filesystem and GridFS statistics now use Bengali. Technical identifiers remain exact, and invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c8b62e6e6">Bengali migration tuning and monitoring gain 50 values</a>. Thanks to xet7.</summary>Bulk attachment migration, batch, CPU and delay tuning, migration logs and warnings, monitoring controls, schedules, progress and system-resource totals now use Bengali. Storage names and numeric ranges remain exact, and invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/685eab566">Bengali accounts and event monitoring gain 50 values</a>. Thanks to xet7.</summary>OTP login, accounts, repository access, problem summaries, broken-card repair, CPU diagnostics and event details now use Bengali. Card-repair placeholders and technical names remain exact, and invariant coverage leaves 16 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f75e20562">Bengali translation completes its final 16 values</a>. Thanks to xet7.</summary>IP-address labels, filesystem integrity, scoped card export and WeKan file and board import guidance now use Bengali. The global-search operator placeholder, format names and angle-bracket value remain exact. All 2,166 values are now translated and whole-locale invariant coverage reports zero placeholders.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cb16fe49d">Tibetan activity and checklists gain 50 values</a>. Thanks to xet7.</summary>Board, card, list and swimlane activity, attachments, labels, checklists, comments, custom fields, archives and imports now use Tibetan. Named and positional placeholders remain exact, and whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/efd5a918c">Tibetan movement and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card movement, membership, concise activity messages, checklist activity and workspace controls now use Tibetan. Named and positional placeholders remain exact, and whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1c977c7b0">Tibetan board layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, multi-board selection, Home boards, templates, list widths, keyboard shortcuts, swimlane heights and common card controls now use Tibetan. Positional placeholders remain exact, and whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9de5c5fd">Tibetan archives and board visibility gain 50 values</a>. Thanks to xet7.</summary>Administration announcements, public boards, archives, attachments, board appearance, member and assignee summaries and private-board guidance now use Tibetan. Named and positional placeholders and strong-emphasis tags remain exact, and whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b58d68794">Tibetan board views and archive guidance gain 50 values</a>. Thanks to xet7.</summary>Public-board guidance, appearance, desktop and mobile views, zoom, calendar and table views, archive guidance and common card editing labels now use Tibetan. The workspace token, positional placeholder and strong-emphasis tags remain exact, and whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/82254ec7f">Tibetan voting and import dialogs gain 50 values</a>. Thanks to xet7.</summary>Card membership and custom fields, voting, Planning Poker, dependencies, organization and team controls, account and background deletion and import and export dialogs now use Tibetan. Whole-locale invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2f46492df">Tibetan member mapping and appearance gain 50 values</a>. Thanks to xet7.</summary>Member dialogs, archive restoration, CAS login, linked cards and boards, imported-member mapping, themes, fonts, avatars, language and permission controls now use Tibetan. Numeric preview text remains exact, and whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/69eccdbde">Tibetan navigation and color names gain 50 values</a>. Thanks to xet7.</summary>Star controls, automatic list widths, clipboard actions, card aging, movement, dialog navigation, board restoration guidance and the first color-name set now use Tibetan. Whole-locale invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2868da302">Tibetan roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>Board roles, destructive confirmations, list movement, clipboard actions, multi-card JSON examples, labels and custom-field types now use Tibetan. JSON structure remains valid, and whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/337ebfdac">Tibetan account email and errors gain 50 values</a>. Thanks to xet7.</summary>Custom-field controls, permanent deletion, profile and WIP settings, account enrolment, invitation, password-reset and verification emails and board, import and linked-card errors now use Tibetan. Named placeholders and technical format names remain exact, and whole-locale invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7f3542b17">Tibetan card export and filtering gain 50 values</a>. Thanks to xet7.</summary>User and account errors, card PDF and Excel export fields and storage errors, list sorting and date, label and member filters now use Tibetan. Technical format names remain exact, and whole-locale invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d70168bcd">Tibetan advanced filters and imports gain 50 values</a>. Thanks to xet7.</summary>Assignee and custom-field filters, the complete advanced-filter syntax and Kanboard, Deck, OpenProject, issue, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan import guidance now use Tibetan. Operators, endpoints, JSON keys, paths and format names remain exact, and whole-locale invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2cba98533">Tibetan Trello API imports gain 50 values</a>. Thanks to xet7.</summary>Trello archive safety, workspace placement, API credentials, board selection, import-job controls and results, member mapping, validation, invitations and labels now use Tibetan. The API URL, technical names and positional placeholder remain exact, and whole-locale invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ebf852267">Tibetan archives and multi-selection gain 50 values</a>. Thanks to xet7.</summary>Board departure, card and list links, list archives, user, team and organization settings, swimlane actions, imports, selection movement and copying, muted and normal notification roles and archived-item states now use Tibetan. The board title token remains exact, and whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0ed929eb9">Tibetan search and shortcuts gain 50 values</a>. Thanks to xet7.</summary>Watch notifications, private and public guidance, previews, member removal, card-description rescue, search, WIP settings, keyboard shortcuts, sidebars and Home-board controls now use Tibetan. The login link, positional placeholder and member tokens remain exact, and whole-locale invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c4d4607ab">Tibetan uploads and templates gain 50 values</a>. Thanks to xet7.</summary>Time tracking, assignee and label shortcuts, uploads, custom branding URLs, watching, welcome content, card, list and board templates, WIP errors and attachment/API limits now use Tibetan. Technical names and numeric ranges remain exact, and whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/906580d92">Tibetan transfer limits and webhooks gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar upload blocking, registration, invitations, SMTP configuration and test mail, webhooks and basic runtime version fields now use Tibetan. Email tokens and product and protocol names remain exact, and whole-locale invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5fade33b4">Tibetan diagnostics and organization tenancy gain 50 values</a>. Thanks to xet7.</summary>Database, FerretDB, reactivity, DDP and operating-system diagnostics, time units, custom-field display settings, account changes, visibility and organization and team tenancy administration now use Tibetan. Environment variables, hostnames and technical runtime names remain exact, and whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d88ef0509">Tibetan card and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, colors, assignment provenance, destructive board and notification actions, duplicate-list cleanup, subtask and card settings, minicard fields, parent relationships and label activity now use Tibetan. Named and positional placeholders remain exact, and whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e2f29fe27">Tibetan automation workflows gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, rule management, workflow and list views, trigger and action construction, card, label, member, checklist, attachment and daily triggers and JSON, CSV and Trello Butler import/export now use Tibetan. Tokens and technical format names remain exact, and whole-locale invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9b2849798">Tibetan scheduled automation gains 50 values</a>. Thanks to xet7.</summary>n8n and Node-RED workflow imports, scheduled, due-date, list-duration and card and board button triggers, list sorting, card completion, bulk movement and relative-date actions now use Tibetan. The count token, technical names and numeric marker remain exact, and whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3c32da738">Tibetan automation rule actions gain 50 values</a>. Thanks to xet7.</summary>List, card, label, member, attachment, checklist and checklist-item trigger phrases plus card movement, archive restoration, color, member, checklist and email actions now use Tibetan. Whole-locale invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eef4af62c">Tibetan configuration and authentication gain 50 values</a>. Thanks to xet7.</summary>Remaining automation actions, authentication, custom-head metadata, manifest and asset-link settings, board duplication and layout, card counters and member lists now use Tibetan. Technical metadata names remain exact, and whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/729433cdf">Tibetan due-date activity and interaction settings gain 50 values</a>. Thanks to xet7.</summary>Custom HTML placement, login errors, authentication display, duplication and deletion confirmations, date activity, due reminders, mentions, card positioning and drag, editor and multi-card settings now use Tibetan. HTML, named and positional placeholders remain exact, and whole-locale invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/099eb4c1b">Tibetan notifications and board roles gain 50 values</a>. Thanks to xet7.</summary>Multi-card and inline-editor behavior, organization, team and user dialogs, notification management, board-role permissions and status, weekdays, activity metadata and linked-card deletion guards now use Tibetan. Keyboard and technical names remain exact, and whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6126599fa">Tibetan shared templates and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, My Cards, Due Cards, global search and missing-item errors now use Tibetan. Domain examples, emphasis markers and positional placeholders remain exact, and whole-locale invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b4836376">Tibetan global-search vocabulary gains 50 values</a>. Thanks to xet7.</summary>Missing-result messages, paginated result counts and the complete set of global-search operator and predicate labels now use Tibetan. Positional and named range placeholders remain exact, and whole-locale invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/49c7d6827">Tibetan global-search help gains 50 values</a>. Thanks to xet7.</summary>Operator validation, pagination, search syntax, every operator explanation, status and field predicates, sorting and limit guidance and query-composition notes now use Tibetan. Backticked examples, emphasis, angle-bracket metavariables and named and positional placeholders remain exact, and whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a0e5a4054">Tibetan dependencies and board backgrounds gain 50 values</a>. Thanks to xet7.</summary>Board and card sorting, completion, stickers, dependency relationships and imports, board backgrounds and the first location controls now use Tibetan. JSON and SVG names and named size and import placeholders remain exact, and whole-locale invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/068e47ff4">Tibetan maps and administrative reports gain 50 values</a>. Thanks to xet7.</summary>Location detection, maps, server troubleshooting, activity controls, string templates, invisible-filename diagnostics and security, performance, database, impersonation, recovery and office reports now use Tibetan. Shell commands, template placeholders, HTML entities and IP protocol names remain exact, and whole-locale invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9c4d24b2">Tibetan recovery and ticket controls gain 50 values</a>. Thanks to xet7.</summary>Office and API reports, automatic recovery reporting and maintenance, swimlane copying, wait indicators, card sizing, organization and team deletion guards, tickets and card details now use Tibetan. REST API, MongoDB, environment configuration and mail-header notation remain recognizable, and whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eafdb9926">Tibetan administration and attachment storage gain 50 values</a>. Thanks to xet7.</summary>Team and organization administration, invitations, Node heap and memory diagnostics, legal notices, checklist and subtask actions and filesystem, GridFS and S3 attachment moves now use Tibetan. Runtime, allocator, URL and storage names remain exact, and whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/85415ec40">Tibetan attachment repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment and avatar storage migration, location repair, default storage, progress and file statistics plus MongoDB GridFS compaction now use Tibetan. Storage names, IDs, replica-set, oplog and Meteor terminology remain recognizable, and whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d521f2918">Tibetan uploads, support and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing, upload progress and limits, account prompts, PDF previews, workspace assignment, custom translations, Markdown and ZIP imports, checklist collapsing, support and accessibility now use Tibetan. Named placeholders, format names and standards remain exact, and whole-locale invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d8745d70a">Tibetan account lockouts and scheduled jobs gain 50 values</a>. Thanks to xet7.</summary>Accessibility metadata, brute-force login protection and locked-user administration, people status filters, scheduled jobs, attachment and avatar paths and board archive, backup and cleanup scheduling now use Tibetan. Whole-locale invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2b413847a">Tibetan migration diagnostics and storage settings gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job results, migration errors and warnings, filesystem, S3 and Azure
storage settings, MongoDB/FerretDB database migration and Sandstorm migration
status now use Tibetan. MongoDB, FerretDB, SQLite, MinIO, AWS, URLs, environment
variables and __db__ remain exact, and whole-locale invariant coverage leaves
366 values.
Sandstorm cleanup, adaptive card loading, plain-text rendering, import/export and activity/notification controls, identity anonymization and backup settings now use Tibetan. Markdown and HTML examples, environment variables, storage paths, product names and counter examples remain exact, and whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/939a7f0b9">Tibetan backup schedules and cloud-storage guidance gain 50 values</a>. Thanks to xet7.</summary>Backup frequency, restore modes, Google Cloud Storage credentials and permissions, AWS S3, Azure and GCS console paths, connection tests and cloud attachment moves now use Tibetan. Time formats, JSON field names, console menu names, storage products and secret-key terminology remain exact, and whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8a8fb4a23">Tibetan GridFS and storage-migration controls gain 50 values</a>. Thanks to xet7.</summary>GridFS selection, migration lifecycle and progress, CollectionFS moves, S3 credentials and connection tests, scheduled board operations, writable paths and attachment/board migration settings now use Tibetan. MongoDB, GridFS, CollectionFS, AWS, MinIO, SSL/TLS and the example region remain exact, and whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/acc9a3e96">Tibetan board-integrity migration controls gain 50 values</a>. Thanks to xet7.</summary>Comprehensive board checks, duplicate-list cleanup, lost-card restoration,
archive recovery, missing-list repair, avatar/file URL repair and migration
progress now use Tibetan. swimlaneId, listId, IDs and URLs remain exact, and
whole-locale invariant coverage leaves 166 values.
The remaining board-conversion steps, cleanup, database migrations, run intervals, export and attachment monitoring, filesystem/GridFS statistics, job queues, CPU and memory use now use Tibetan. ID, URL, CPU and GridFS remain exact, and whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/05ea0c742">Tibetan migration tuning and monitoring controls gain 50 values</a>. Thanks to xet7.</summary>Attachment storage targets, batch size, CPU thresholds and delays, migration logs and lifecycle, monitoring refresh/export, schedules, progress, resource totals and minicard list/checklist settings now use Tibetan. GridFS, S3, CPU, percent and millisecond ranges remain exact, and whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4c2b4312b">Tibetan whole-file translation is complete</a>. Thanks to xet7.</summary>The final 66 values cover repository login, problem reporting, broken-card
repair, CPU and event diagnostics, scoped import/export and search guidance.
OTP, API, CPU, IP versions, file formats, __fixed__, __unfixable__,
__operator_number__ and <number> remain exact. Zero-backlog and whole-file
invariant coverage now protect all 2,166 Tibetan translations.
Board, card, list and swimlane deletion and creation, comments, due dates, attachments, subtasks, labels, checklists, custom fields, archiving and imports now use Buryat. Named and percent placeholders remain exact, and whole-locale invariant coverage leaves 2,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/be39bd01e">Buryat activity history and workspaces gain 50 values</a>. Thanks to xet7.</summary>Card moves, activity history, checklist changes, board membership, dates, subtasks, custom fields and workspace creation and settings now use Buryat. Named and percent placeholders remain exact, and whole-locale invariant coverage leaves 2,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5b46ffce0">Buryat workspace and layout controls gain 50 values</a>. Thanks to xet7.</summary>Workspace deletion, board selection and home boards, list widths, swimlane heights, keyboard shortcuts, dates, templates and checklist actions now use Buryat. Percent placeholders remain exact, and whole-locale invariant coverage leaves 2,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b542de4e0">Buryat board administration and archives gain 50 values</a>. Thanks to xet7.</summary>Administrator notices and permissions, board archives and restoration, attachments, templates, board backgrounds, member views and assignments now use Buryat. Named, percent and HTML placeholders remain exact, and whole-locale invariant coverage leaves 1,966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/84950690a">Buryat board views and card archives gain 50 values</a>. Thanks to xet7.</summary>Board visibility, backgrounds and views, desktop and mobile display, zoom, calendar navigation, card, list and swimlane archives and card editing now use Buryat. Named, percent and HTML placeholders remain exact, and whole-locale invariant coverage leaves 1,916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9ece9b210">Buryat voting and import controls gain 50 values</a>. Thanks to xet7.</summary>Card membership and dates, voting and planning poker, dependencies, organizations, teams, backgrounds, accounts and board-element imports and exports now use Buryat. Whole-locale token and tag invariant coverage leaves 1,866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d64a6cc65">Buryat member and theme controls gain 50 values</a>. Thanks to xet7.</summary>Members, invitations, archived-item restoration, rule transfer, linked cards and boards, imported-member mapping, themes, fonts, text colors, avatars, languages and permissions now use Buryat. Whole-locale invariant coverage leaves 1,816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a0f2dc4f1">Buryat layout and color controls gain 50 values</a>. Thanks to xet7.</summary>Subtasks, starred boards and pages, automatic list widths, card aging, card and list movement, dialogs, board closure and 23 interface colors now use Buryat. Whole-locale token and tag invariant coverage leaves 1,766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8bb6d9222">Buryat roles and custom fields gain 50 values</a>. Thanks to xet7.</summary>The remaining colors, restricted board roles, deletion confirmations, clipboard actions, card and list copying, template containers, labels and custom-field types and options now use Buryat. The translated JSON example remains valid, and whole-locale invariant coverage leaves 1,716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4378d2c83">Buryat email and validation messages gain 50 values</a>. Thanks to xet7.</summary>Custom-field text, permanent deletion, profile and WIP controls, card dates, notifications, account enrollment, invitation, password and verification emails and board, user, JSON and CSV errors now use Buryat. All email tokens remain exact, and whole-locale invariant coverage leaves 1,666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/60295a7b9">Buryat exports and filters gain 50 values</a>. Thanks to xet7.</summary>Account and name conflicts, attachment-free board exports, PDF and Excel card exports, attachment metadata, list sorting and date, label and member filters now use Buryat. Whole-locale token and tag invariant coverage leaves 1,616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/88498383b">Buryat filters and board imports gain 50 values</a>. Thanks to xet7.</summary>Assignee and custom-field filters, activities, imported members, advanced filter syntax and board imports from Kanboard, Deck, OpenProject, issue trackers, Asana, ZenKit, Trello, CSV, Jira, Excel and WeKan now use Buryat. Named tokens and filter operators remain exact, and whole-locale invariant coverage leaves 1,566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/af8cef75c">Buryat Trello import controls gain 50 values</a>. Thanks to xet7.</summary>ZIP validation, Trello workspaces and API credentials, board selection, progress, cancellation and results, imported-member mapping, date validation, keyboard shortcuts and label creation now use Buryat. The API URL, year example and percent placeholder remain exact, and whole-locale invariant coverage leaves 1,516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/32993b317">Buryat membership and selection controls gain 50 values</a>. Thanks to xet7.</summary>Administrator safeguards, leaving and linking boards, list archiving and deletion, user, team and organization settings, element movement, multi-select, archive states, board roles and participation notifications now use Buryat. The board-title token remains exact, and whole-locale invariant coverage leaves 1,466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/815439608">Buryat visibility and shortcut controls gain 50 values</a>. Thanks to xet7.</summary>Watch notifications, private and public pages, previews, board membership, unsaved-description recovery, search, WIP limits, keyboard shortcuts, sidebars, signup and default and starred boards now use Buryat. Named and percent tokens and the login link remain exact, and whole-locale invariant coverage leaves 1,416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eb8a33114">Buryat tracking and branding controls gain 50 values</a>. Thanks to xet7.</summary>Starred boards, time tracking, assignee and label shortcuts, uploads, custom logos and links, URL schemes, watching, welcome and template boards and WIP and attachment limits now use Buryat. Numeric examples remain exact, and whole-locale invariant coverage leaves 1,366 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cdacd37cc">Buryat attachment and webhook settings gain 50 values</a>. Thanks to xet7.</summary>Attachment and API transfer limits, avatar blocking, registration and invitations, SMTP configuration and testing, authorization errors, outgoing and global webhooks and runtime version labels now use Buryat. Invitation email tokens remain exact, and whole-locale invariant coverage leaves 1,316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3d8afbc41">Buryat diagnostics and organization controls gain 50 values</a>. Thanks to xet7.</summary>Database, FerretDB, reactivity, DDP and operating-system diagnostics, time units, custom-field display, account and board visibility settings and team and organization tenancy, domains, administrators and member synchronization now use Buryat. Environment-variable names remain exact, and whole-locale invariant coverage leaves 1,266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1615d1543">Buryat card dates and subtask settings gain 50 values</a>. Thanks to xet7.</summary>Received and end dates, card and selection colors, board and notification deletion, duplicate-list cleanup, subtask destinations, minicard fields, parent-card display and label activity now use Buryat. Named and percent tokens and count examples remain exact, and whole-locale invariant coverage leaves 1,216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fd835b65c">Buryat activity and automation rules gain 50 values</a>. Thanks to xet7.</summary>Attachment, label and custom-field activity, rule creation and editing, workflow and list views, card, label, member, checklist, attachment and daily triggers and JSON, CSV and Trello Butler rule transfer now use Buryat. Named and percent tokens remain exact, and whole-locale invariant coverage leaves 1,166 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/31e730efa">Buryat scheduled automation gains 50 values</a>. Thanks to xet7.</summary>Visual workflow formats, n8n and Node-RED imports, scheduled and button triggers, daily through monthly schedules, due-date and time-in-list triggers, list sorting, completion actions, relative dates and time units now use Buryat. The import count remains exact, and whole-locale invariant coverage leaves 1,116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c8206e0ce">Buryat automation actions gain 50 values</a>. Thanks to xet7.</summary>Card, label, member, attachment, checklist and checklist-item conditions, archive transitions, card movement, membership and color actions, checklist updates, email actions and their generated descriptions now use Buryat. Whole-locale token and tag invariant coverage leaves 1,066 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c06f1d46a">Buryat rule details and branding gain 50 values</a>. Thanks to xet7.</summary>Generated email, archive, label, member and checklist actions, card creation, swimlanes, date-field actions, card links, authentication, product names, custom HTML, manifests, asset links and layout settings now use Buryat. Format names and comma-separated examples remain exact, and whole-locale invariant coverage leaves 1,016 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1a79586b1">Buryat customization and reminders gain 50 values</a>. Thanks to xet7.</summary>Custom body HTML, authentication display, board duplication, swimlane deletion, card placement, due-time reminders and mentions, account, team and organization deletion, minicard labels, drag handles and editor behavior now use Buryat. HTML and named and percent tokens remain exact, and whole-locale invariant coverage leaves 966 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/589e4aea6">Buryat roles and calendar settings gain 50 values</a>. Thanks to xet7.</summary>Multi-card windows, Enter-key editor behavior, organization, team and user editing, notification state, rename and invitation permissions, board-role status, weekdays, ownership and linked-card deletion safeguards now use Buryat. Keyboard combinations remain exact, and whole-locale invariant coverage leaves 916 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/941ab8b1d">Buryat shared templates and card views gain 50 values</a>. Thanks to xet7.</summary>Checklist visibility, tasks, domains, shared templates, people and time scopes, My Cards sorting, Due Cards and global-search views and missing board-element errors now use Buryat. Domain examples, Markdown emphasis and percent tokens remain exact, and whole-locale invariant coverage leaves 866 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7808b8873">Buryat search operators gain 50 values</a>. Thanks to xet7.</summary>Missing comment, organization and team results, card-result counts and search operators for board structure, people, state, dates, text, attachments and checklists and their archived, open, overdue and time predicates now use Buryat. Result-boundary tokens remain exact, and whole-locale invariant coverage leaves 816 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c66e107db">Buryat global-search guidance gains 50 values</a>. Thanks to xet7.</summary>Public and private predicates, operator validation, pagination and the full global-search guide for board structure, people, dates, state, field presence, sorting, limits and operator composition now use Buryat. Search examples, named tokens and pseudo-tags remain exact, and whole-locale invariant coverage leaves 766 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e41b0e463">Buryat dependencies and backgrounds gain 50 values</a>. Thanks to xet7.</summary>Label metadata, board and card sorting, completion state, stickers, dependency types, filtering and JSON/SVG import, board backgrounds and location names, addresses and latitude now use Buryat. Import counts and the image-size token remain exact, and whole-locale invariant coverage leaves 716 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74f535f09">Buryat maps and reports gain 50 values</a>. Thanks to xet7.</summary>Longitude, map-link detection and providers, server-error troubleshooting, sorting, activity visibility, swimlane movement, string templates and file, security, speed, test, CPU, database, rule, board, card, impersonation, recovery and office reports now use Buryat. Shell commands and the template placeholder remain exact, and whole-locale invariant coverage leaves 666 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/60d0ef5e6">Buryat recovery and support reports gain 50 values</a>. Thanks to xet7.</summary>Office sharing, REST API usage, data recovery status and maintenance, swimlane copying, wait-spinner styles, organization and team deletion safeguards, support tickets and card sorting and details now use Buryat. API and database configuration names remain exact, and whole-locale invariant coverage leaves 616 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cc477e3e0">Buryat teams and attachment storage gain 50 values</a>. Thanks to xet7.</summary>Team and organization assignment, invitations, Node heap and memory diagnostics, legal notices, checklist and subtask actions and filesystem, GridFS and S3 attachment moves now use Buryat. Runtime, allocator, URL and storage names remain exact, and whole-locale invariant coverage leaves 566 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d4fb16622">Buryat attachment repair and compaction gain 50 values</a>. Thanks to xet7.</summary>Attachment and avatar storage migration, location repair, default storage, progress and file statistics plus MongoDB GridFS compaction now use Buryat. Storage names, IDs, replica-set, oplog and Meteor terminology remain recognizable, and whole-locale invariant coverage leaves 516 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/edd68f23e">Buryat uploads and accessibility gain 50 values</a>. Thanks to xet7.</summary>Board timing, upload progress and limits, account prompts, PDF previews, workspace assignment, custom translations, Markdown and ZIP imports, checklist collapsing, support and accessibility now use Buryat. Named placeholders, format names and standards remain exact, and whole-locale invariant coverage leaves 466 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/337c8f6b9">Buryat lockout and scheduled jobs gain 50 values</a>. Thanks to xet7.</summary>Accessibility metadata, brute-force login protection and locked-user administration, people status filters, scheduled jobs, attachment and avatar paths and board archive, backup and cleanup scheduling now use Buryat. Whole-locale token and tag invariant coverage leaves 416 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8506731e9">Buryat migrations and storage settings gain 50 values</a>. Thanks to xet7.</summary>Scheduled-job results, migration errors and warnings, filesystem, S3 and Azure
storage settings, MongoDB/FerretDB database migration and Sandstorm migration
status now use Buryat. MongoDB, FerretDB, SQLite, MinIO, AWS, URLs, environment
variables and __db__ remain exact, and whole-locale invariant coverage leaves
366 values.
Sandstorm cleanup, adaptive card loading, plain-text rendering, import/export and activity/notification controls, identity anonymization and backup settings now use Buryat. Markdown and HTML examples, environment variables, storage paths, product names and counter examples remain exact, and whole-locale invariant coverage leaves 316 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/53cdf8095">Buryat backup and cloud-storage guidance gain 50 values</a>. Thanks to xet7.</summary>Backup frequency, restore modes, Google Cloud Storage credentials and permissions, AWS S3, Azure and GCS console paths, connection tests and cloud attachment moves now use Buryat. Time formats, JSON field names, console menu names, storage products and secret-key terminology remain exact, and whole-locale invariant coverage leaves 266 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c8f5fc627">Buryat storage-migration controls gain 50 values</a>. Thanks to xet7.</summary>GridFS selection, migration lifecycle and progress, CollectionFS moves, S3 credentials and connection tests, scheduled board operations, writable paths and attachment and board migration settings now use Buryat. MongoDB, GridFS, CollectionFS, AWS, MinIO, SSL/TLS and the example region remain exact, and whole-locale invariant coverage leaves 216 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dd2b4c5af">Buryat board-integrity migrations gain 50 values</a>. Thanks to xet7.</summary>Comprehensive board checks, duplicate-list cleanup, lost-card restoration,
archive recovery, missing-list repair, avatar and file URL repair and migration
progress now use Buryat. swimlaneId, listId, IDs and URLs remain exact, and
whole-locale invariant coverage leaves 166 values.
The remaining board-conversion steps, cleanup, database migrations, run intervals, export and attachment monitoring, filesystem and GridFS statistics, job queues, CPU and memory use now use Buryat. ID, URL, CPU and GridFS remain exact, and whole-locale invariant coverage leaves 116 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3c4e7cea1">Buryat migration tuning and monitoring gain 50 values</a>. Thanks to xet7.</summary>Attachment storage targets, batch size, CPU thresholds and delays, migration logs and lifecycle, monitoring refresh and export, schedules, progress, resource totals and minicard list and checklist settings now use Buryat. GridFS, S3, CPU, percent and millisecond ranges remain exact, and whole-locale invariant coverage leaves 66 values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/add0ed2d9">Buryat whole-file translation is complete</a>. Thanks to xet7.</summary>The final 66 values cover repository login, problem reporting, broken-card
repair, CPU and event diagnostics, scoped import/export and search guidance.
OTP, API, CPU, IP versions, file formats, __fixed__, __unfixable__,
__operator_number__ and <number> remain exact. Zero-backlog and whole-file
invariant coverage now protect all 2,166 Buryat translations.
Board, card, list and swimlane deletion and creation, comments, due dates, attachments, subtasks, labels, checklists, custom fields, archiving and imports now use Cherokee syllabary. Named and percent placeholders remain exact, and whole-locale invariant coverage leaves 2,116 values.
</details>and improves the following translations:
Cyrillic translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7a64f8bd7">Office and API reports use Macedonian and Serbian</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use distinct Macedonian and Serbian vocabulary in Cyrillic script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, Cyrillic content, distinct vocabulary and the intentionally universal API labels. These direct translations have low confidence and welcome review by Macedonian and Serbian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/293568999">Office and API reports use Mongolian</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Mongolian vocabulary in Cyrillic script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, Cyrillic content and the intentionally universal API labels. This direct translation has low confidence and welcomes review by Mongolian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/56f270fa2">Mongolian archive and board controls no longer use Russian</a>. Thanks to xet7.</summary>Twenty-eight exact Russian copies in archive, template and board-view controls
now use Mongolian vocabulary. The shared Архив loanword remains valid.
Regression coverage compares the repaired values directly with Russian, checks
established Mongolian board terms and rejects common Russian UI words. Another
1,146 exact-match candidates remain for later audited batches. This direct
repair has low confidence and welcomes review by Mongolian speakers.
Forty-three exact Russian copies across card guidance, dates, editing controls, voting, templates, sorting and popup titles now use Mongolian. Regression coverage compares every repaired key with Russian, preserves placeholders and established card vocabulary, and rejects common Russian card terms. Another 1,103 exact-match candidates remain for later audited batches. This direct repair has low confidence and welcomes review by Mongolian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/27ac02b41">Mongolian attachment controls no longer use Russian</a>. Thanks to xet7.</summary>Fourteen exact Russian copies across attachment deletion, actions, storage moves
and renaming now use Mongolian. The shared Файл loanword remains valid and
GridFS remains recognizable. Regression coverage compares the repaired values
with Russian and rejects common Russian attachment terms. Another 1,089 exact
matches, including shared loanwords, remain for later audit. This direct repair
has low confidence and welcomes review by Mongolian speakers.
Fifty exact Russian copies across activities, dialogs, settings, automation, search and export now use consistent Mongolian checklist and subtask vocabulary. Regression coverage compares every value with Russian, preserves named and printf placeholders, keeps the search operator space-free and rejects common Russian terms. Another 1,039 exact matches, including shared loanwords, remain for later audit. This direct repair has low confidence and welcomes review by Mongolian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5aa7cb978">Mongolian filter labels no longer use Russian</a>. Thanks to xet7.</summary>Eleven exact Russian copies across date, list-title, label, member, assignee, custom-field, advanced and card-title filters now use consistent Mongolian filter vocabulary. Regression coverage compares every repaired value with Russian and rejects common Russian filter terms. Another 1,028 exact matches, including shared loanwords, remain for later audit. This direct repair has low confidence and welcomes review by Mongolian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e71c827ec">Mongolian label text no longer uses Russian</a>. Thanks to xet7.</summary>Thirty exact Russian copies across label activities, deletion, multi-selection, automation, search, display settings and validation now use consistent Mongolian label vocabulary. Regression coverage compares every value with Russian and preserves named, printf and Markdown placeholders. Another 998 exact matches, including shared loanwords, remain for later audit. This direct repair has low confidence and welcomes review by Mongolian speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/24bc8efb0">Mongolian translations restore their exact English placeholders</a>. Thanks to xet7.</summary>Three email subjects now keep __siteName__ instead of substituting __url__,
and the unknown-operator message keeps %s instead of malformed %1. Their
surrounding Russian prose now uses Mongolian. Whole-file regression coverage
compares every Mongolian placeholder inventory with en.i18n.json and rejects
the repaired Russian wording. This direct repair has low confidence and
welcomes review by Mongolian speakers.
Caucasian translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/71c6d0be6">Office and API reports use Armenian and Georgian</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Armenian or Georgian script and each language's own vocabulary. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, rejects the neighbouring script and preserves the intentionally universal API labels. These direct translations have low confidence and welcome review by Armenian and Georgian speakers.
</details>Indic translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4e19994a5">Office and API reports use Telugu and Tamil</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Telugu or Tamil script and each language's own vocabulary. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, rejects the neighbouring script and preserves the intentionally universal API labels. These direct translations have low confidence and welcome review by Telugu and Tamil speakers.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7fac8a9cc">Office and API reports use Odia and Punjabi</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Odia or Gurmukhi script and each language's own vocabulary. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, rejects the neighbouring script and preserves the intentionally universal API labels. These direct translations have low confidence and welcome review by Odia and Punjabi speakers.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: a CRITICAL SECURITY ISSUE, ImportBleed, allowed a logged-out DDP client to write imported board data directly into the database; both import methods now reject unauthenticated callers after mandatory argument validation and before import processing. Below that: bounded legacy E2E login waits, restored Transifex locale aliases, Office and API report translations, visible obsolete English placeholders and focused regression coverage.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of ImportBleed:
<details> <summary><a href="https://github.com/wekan/wekan/commit/2898635df">Board imports reject logged-out DDP callers before import processing</a>. Thanks to Char0n1507 and xet7.</summary>The importBoard method reached direct collection writers without first requiring an authenticated user, allowing a network client with no account or token to create imported board data and placeholder users. importBoard and importScoped now complete Meteor’s mandatory type checks, then reject logged-out callers before feature checks, parsers or creators can run. Scoped imports carry the authenticated method user explicitly. Denied attempts are attributed by connection address in Admin Panel / Problems. Source-level and logged-out browser regression tests cover the guard and no-write outcome. See GHSA-qp32-wqxw-wq3h and ImportBleed.
</details>and has the following developer-tooling fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1ccb71bae">Legacy E2E login and suite waits are bounded</a>. Thanks to xet7.</summary>The Puppeteer list-regression suite now waits for a connected DDP session before resume-token login and bounds the token callback, reactive identity settlement and entire suite. A stalled fresh second session fails with its URL, DDP status, user id and login state instead of pausing all tests indefinitely. Regression coverage keeps every wait bounded and diagnostic.
</details>and improves the following translations:
Translation tooling - placeholder safety and same-language vocabulary reuse.
<details> <summary><a href="https://github.com/wekan/wekan/commit/2d966f157">Transifex locale aliases remain symlinks to their translation targets</a>. Thanks to xet7.</summary>The Khmer and Russian hyphenated locale aliases again point to their underscored Transifex targets. Translation pulls therefore update the files loaded by the app instead of leaving materialized copies stale. The former copies were byte-identical to their targets, so no translation was lost; lazy-loading and new-language wiring tests pin both aliases as symlinks.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eec7ea495">IPv4 and IPv6 labels follow each language’s own IP-address vocabulary</a>. Thanks to xet7.</summary>The placeholder-only local-memory tool now derives versioned protocol labels from each language’s established IP-address translation, preserving its word order and changing only the universal protocol token. It filled 282 rendered labels across 141 language tags (268 stored values plus seven locale aliases). Two obsolete English sentences were reset to the current source so future fills can see them, and wrong-script analysis now ignores only exact IPv4 and IPv6 identifiers while continuing to inspect surrounding prose. Regression tests pin the ambiguity guard, placeholder protection and narrow protocol-token exception.
</details>East Asian translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/df7bb6976">Office and API reports use simplified and traditional Chinese</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now cover eleven Chinese language tags, using simplified or traditional script to match each existing locale. Product and protocol terms such as REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both scripts and the intentionally universal API labels.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/18ce975c7">Office and API reports use Japanese and Korean</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover three Japanese and two Korean language tags, following each family’s established login and address vocabulary. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both writing systems and the intentionally universal API labels.
</details>Cyrillic translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/58b510e49">Office and API reports use Russian and Ukrainian</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover four Russian and two Ukrainian language tags, following each family’s established login and address vocabulary. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both languages and the intentionally universal API labels.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aac41827e">Office and API reports use Belarusian and Bulgarian</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now use distinct Belarusian and Bulgarian vocabulary in Cyrillic script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both languages and the intentionally universal API labels.
</details>Right-to-left translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c2235fbda">Office and API reports use Arabic across four locale tags</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover the Arabic, Algerian, Egyptian and Moroccan tags in Arabic script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, all four right-to-left tags, Arabic-script content and the intentionally universal API labels.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ebfcbd986">Office and API reports use Hebrew and Persian</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover two Hebrew and two Persian language tags in their respective right-to-left scripts. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both scripts and the intentionally universal API labels.
</details>Indic translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/31946787b">Office and API reports use Hindi and Gujarati</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover two Hindi tags in Devanagari and one Gujarati tag in Gujarati script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, both scripts and the intentionally universal API labels.
</details>Greek translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6d0e5b845">Office and API reports use Greek across both locale tags</a>. Thanks to xet7.</summary>All twelve report labels and descriptions now cover both Greek language tags in Greek script. REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, Greek-script content and the intentionally universal API labels.
</details>Khmer translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d233b4fd4">Office and API reports use Khmer</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Khmer vocabulary across the three Khmer language tags. API, REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, Khmer script, variant consistency and the intentionally universal literals.
</details>Vietnamese translations - the Office and REST API usage reports.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5bd71c07d">Office and API reports use Vietnamese</a>. Thanks to xet7.</summary>All twelve translatable report labels and descriptions now use Vietnamese across both language tags, following their established login, report and endpoint vocabulary. API, REST API, IPv4, IPv6 and WITH_API=true remain recognizable. Regression coverage checks every translated key, variant consistency and the intentionally universal literals.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Playwright authentication now waits for the resumed user after navigation, and local WebKit runs retry once with a fresh worker after rare renderer failures. Below that: regression coverage for both safeguards.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release improves the following developer tooling:
<details> <summary><a href="https://github.com/wekan/wekan/commit/47b277932">Playwright waits for resumed users and retries unstable WebKit renderers</a>. Thanks to xet7.</summary>Token login now waits for the expected Meteor user after the final navigation, preventing an authorized method call from racing Accounts token resumption. Local WebKit runs retry once in a fresh worker when its renderer fails internally after many tests; persistent application and assertion failures still fail. Regression tests require the identity wait, its bounded timeout, the local retry and the unchanged two-retry CI policy.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: six coordinated reports harden REST authorization, board ownership, administrator token auditing and error responses. Build and release tooling now supports macOS zsh, Alpine, Arch, Fedora, RHEL and Oracle Linux, keeps companion data under the repository's ignored .tools directory, provides sandbox-local tools, bounds build, test and runtime resources across every platform, reports resource failures in Admin Panel Problems, offers three bounded, cleanly interruptible complete-test execution modes, and includes four dependency updates.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following HIGH AND MODERATE SECURITY ISSUES:
REST board mutations - cards, checklists, checklist items and comments.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">Checklist mutations require board write access</a>. Thanks to Char0n1507 and xet7.</summary>Checklist and checklist-item create, edit and delete routes accepted read-only board members because they checked only whether the caller could view the board. Every mutation now requires the canonical board write capability; read-only members retain GET access. See GHSA-5r4m-5xx6-96jf and ChecklistWriteBleed.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">Every REST mutation follows its canonical board-role capability</a>. Thanks to senti-man and xet7.</summary>Checklist, checklist-item and comment routes had drifted from the role capability table: some mutations needed only read access, while comment creation required full write access and incorrectly rejected Comment Only members. Mutation routes now require write access and comment creation uses the comment capability. See GHSA-cp24-5m9m-wm97 and RoleBleed.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">Card and checklist creation requires board write access</a>. Thanks to Char0n1507 and xet7.</summary>Card and checklist creation reused the comment permission, allowing Comment Only members to create board content. Both single and bulk card creation and checklist creation now require the canonical write capability. See GHSA-qf5c-63jx-mpv4 and CommentWriteBleed.
</details>Boards - ownership assigned by the board-creation API.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">REST board creation cannot choose another owner</a>. Thanks to Char0n1507 and xet7.</summary>The board-creation route trusted the owner and role flags in the request body, so an authenticated caller could create a board attributed to another user. The authenticated caller is now always the initial active administrator and owner. See GHSA-6jvj-85q3-6q2m and OwnerBleed.
</details>Administrator API - issuing login tokens for another account.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">Administrator-created login tokens require an audited reason</a>. Thanks to Char0n1507 and xet7.</summary>The administrator token endpoint could create a reusable login token for another user without producing the impersonation audit record used by the normal UI. It now requires a reason, verifies the target, records the administrator and target before inserting the login token, and rejects an unaudited request. See GHSA-5r57-9vj7-c64f and TokenAuditBleed.
</details>REST responses - safe status codes and public error messages.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1fa0b158c">REST failures return sanitized errors and correct HTTP statuses</a>. Thanks to Char0n1507 and xet7.</summary>Board and user routes returned raw Error objects as successful HTTP 200 responses, which exposed implementation details and made failures look successful. Shared response handling now preserves expected 4xx statuses while replacing unexpected 5xx details with a generic message. Regression tests cover both paths and ensure raw errors do not return from the affected handlers. See GHSA-h59p-76c3-8345 and ErrorBleed.
</details>and updates the following dependencies:
Thanks to dependabot.
and has the following developer-tooling improvements:
Build and release tooling - host setup and repository-local working data.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6ec619e43">Every platform bounds runtime memory and reports resource failures</a>. Thanks to xet7.</summary>Source builds, Linux and Windows bundles, Docker, Snap and Sandstorm now derive Node and FerretDB limits from available host or cgroup memory while preserving explicit administrator overrides. Test and Go compiler floors no longer exceed small containers, Playwright installs repository-local browsers and uses a matching Docker fallback when immutable host libraries are missing, and build dependency stderr remains visible in both the console and timestamped log.
Runtime self-checks proactively report low disk space and V8 heap pressure in Admin Panel Problems. The database classifier now gives actionable reports for memory and file-descriptor exhaustion, read-only volumes, corruption and oversized documents. The remediation documents record which protections work on every platform and distinguish implemented FerretDB telemetry from follow-ups.
</details>Browser runs reuse cached binaries and report only their selected project. Thanks to xet7.
Complete-mode wiring checks cover all three test schedules. Thanks to xet7.
The first three Tests menu choices in both build.sh and build.bat now run the
whole matrix. The default runs one stage at a time with two Playwright workers
per browser for a safe speed increase; one-by-one minimizes memory use; and
at-once runs WeKan jobs concurrently. Database backends and FerretDB stages stay
sequential in every mode to avoid port conflicts and overlapping compiler load.
The Windows helper forwards the selected mode to the shared shell implementation,
and parity tests pin the menu order, mode mapping and Playwright worker limit.
Every EVERYTHING run now owns a repository-local process lock. Starting another one first stops the older run and all descendants, frees and verifies ports 3000 and 3001, and removes tagged Playwright and database-conformance containers. Ctrl-C uses the same cleanup for the interrupted run. Cleanup tries graceful termination before a bounded forced stop; if a process, port or container still survives, the replacement exits with an actionable error before creating logs, building WeKan or starting any new tests. PID start tokens prevent stale lock files from targeting an unrelated reused PID on Linux, macOS and Windows.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e8388314a">Sandbox tasks install version-matched local tools under .tools</a>. Thanks to xet7.</summary>The agent instructions now treat the repository-local, ignored .tools tree as
the installation root when the Flatpak sandbox lacks a required command. Node.js
and npm versions are read from Dockerfile, Meteor from .meteor/release, and
other tools from their repository-owned version sources instead of stale copied
examples. The instructions link the tested sandbox bootstrap, keep environment
overrides scoped, and document a local RapidOCR virtual environment for reading
timestamped screenshots when bubblewrap prevents the normal image viewer from
creating a user namespace.
Fedora screenshots showed available memory falling from 28.6 GiB to 371 MiB
while the CPU-exec negative regression created a large group of short-lived
bash processes. The test now captures stdout and stderr from one direct helper
invocation instead of launching a duplicate nested shell. The following
FerretDB stages limit Go package compilation to two through four workers with a
separate managed-heap target, and conformance no longer downloads the root,
integration and tools module graphs before building its single binary. Focused
positive and negative regressions pin these resource boundaries.
The adaptive 8-16 GiB heap ceiling needed while Meteor compiles WeKan was also
inherited by the long-lived bundle server and every Node, E2E and Playwright
process. A leaking test could therefore consume nearly all workstation RAM and
swap before Linux killed it. Runtime processes now use a separate quarter-RAM
allowance clamped to 2-4 GiB, while Meteor compilation retains the larger heap.
WEKAN_TEST_NODE_OPTIONS provides a test-only override. Regression coverage
pins both the bounded processes and the deliberately unbounded compiler.
Host-side dependency installers now choose Fedora's dnf commands and package
names, including Fedora 44's 7zip, gcc-c++, glibc-langpack-en and snapd
socket setup. Debian/Ubuntu continue to use apt and macOS continues to use
Homebrew. Mocked installer-path tests cover both Fedora and Debian selection.
Host-facing shell installers now identify Linux families through /etc/os-release
and select apk, pacman, dnf, yum or apt-get with each distribution's
package names. Shared compiler and archive setup covers bundle conversion, docs,
Sandstorm, release downloads, Snap prerequisites and forge tooling. Alpine and
Arch report the manual Snap prerequisite instead of incorrectly running apt. RHEL
and Oracle Linux configure their respective EPEL repositories before installing
snapd.
Mocked detection tests cover Alpine, Arch, Fedora, RHEL, Oracle Linux and Debian,
and every migrated script is syntax-checked.
Host-facing Bash scripts now hand direct zsh invocation to macOS's system Bash before parsing Bash-specific syntax. The shared installer detects Darwin without depending on the caller's shell and maps command names to the correct Homebrew formulae for Python, GNU awk, GCC, 7-Zip and Node.js. Regression tests cover the handoff, Darwin detection and every differing formula name.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/299f943f2">Scripts keep website and log directories under .tools</a>. Thanks to xet7.</summary>Shell and Windows batch scripts now use .tools/wekan.fi for the website
companion checkout and .tools/log for build and test output. This removes the
legacy sibling ../w/wekan.fi and parent ../log assumptions while preserving
the CI environment-variable overrides.
A fresh shell did not include .tools/.meteor on PATH, so EVERYTHING stopped
before building with meteor: command not found even though setup had installed
the exact release there. build.sh now prefers the repository-local Meteor and
the Node version and architecture named by the release configuration. The
companion FerretDB test runner
also generates current version metadata before unit packages initialize and
writes standalone logs under .tools/log. Its unit, vet and SQLite integration
stages pass together.
The direct FerretDB test entry uses .tools/FerretDB. Thanks to xet7.
Repository instructions use the same .tools paths. Thanks to xet7.
The database-conformance regression test expects .tools/log. Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the TenantBleed, AssignedBleed and CalendarBleed security fixes restrict Organization/Team writes to site admins and make REST and iCalendar card creation follow canonical board-role capabilities. Below that: card-history, destination-picker and riscv64 release-build fixes. The binary table is the v11.06 baseline and will be replaced by this release's verified provenance when its platform builds run.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following MODERATE SECURITY ISSUES:
Organizations and Teams - tenant configuration and its DDP permissions.
<details> <summary><a href="https://github.com/wekan/wekan/commit/216146edc">Organization and Team DDP writes require a site administrator</a>. Thanks to Char0n1507 and xet7.</summary>Any authenticated user could insert, update or remove an Organization or Team
document over Meteor/DDP when its _id equalled their user id. The collection
allow rules treated document identity as authority without requiring site-admin,
tenant-admin or membership privileges, exposing tenant configuration and
deletion. All six operations now share one site-admin-only decision; legitimate
scoped and internal writes continue through their authorization-enforcing server
methods. Refused authenticated attempts are rate-limited, attributed and shown
in Admin Panel / Problems. See
GHSA-p4cq-83j9-7g73
and TenantBleed.
REST authorization - the shared permission gate for board mutations.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d1c75e995">REST mutations enforce the canonical board write capability</a>. Thanks to Char0n1507 and xet7.</summary>An authenticated Only Assigned Comment member could modify any card through the
REST API, even when not assigned to it. The shared REST authorization helper
duplicated a list of excluded role flags and omitted isCommentAssignedOnly, so
every card mutation route using it accepted a role whose canonical policy says
write: false. The helper and both parallel attachment APIs now use the shared
role-capability decision. All non-writing roles are denied, while No Comments
and the other legitimate writing roles retain access. Refused attempts are
rate-limited, attributed and shown in Admin Panel / Problems. See
GHSA-f396-42fx-vr88
and AssignedBleed.
iCalendar import - creating cards from calendar events through DDP.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e062042c1">ICS imports require the canonical board write capability</a>. Thanks to Char0n1507 and xet7.</summary>An authenticated Comment Only member could call importIcsToBoard over
Meteor/DDP and create arbitrary cards. The method checked board membership and
excluded only read-only roles, so Comment Only, Only Assigned Comment and Worker
members reached card insertion despite the role policy denying them write
access. The DDP path now uses the same canonical write-capability helper as its
REST sibling. Regression coverage denies all five non-writing roles and confirms
that the four legitimate writing roles retain access. Refused attempts are
rate-limited, attributed and shown in Admin Panel / Problems. See
GHSA-fpm6-r5fg-2mrg
and CalendarBleed.
and fixes the following bugs:
Card details - activity history and destination selection.
<details> <summary><a href="https://github.com/wekan/wekan/commit/394a89fbc">Opening Activities shows the complete card history and REST returns every comment</a>. Thanks to rmb82 and xet7.</summary>The removed Activities eye left its old per-card showActivities: false value
in control, so opening the new caret could still request comments only. Opening
the section now always requests its complete history. The comments REST endpoint
also validates the card against the requested board before selecting all records
by their authoritative cardId, so older and imported comments with missing or
stale denormalized board metadata are no longer omitted without weakening board
isolation. Positive and negative regression tests cover both retrieval paths.
The destination picker treated a list without a swimlaneId as shared only by
the default swimlane. It now combines board-global lists with the selected
swimlane's own lists for every swimlane, while continuing to exclude lists owned
by another swimlane. Regression tests cover both cases and the shared picker
used by Move Card and Copy Card.
and fixes the following developer-tooling bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d11987fad">Queued riscv64 snap builds survive the GitHub job limit</a>. Thanks to xet7.</summary>The Release All log showed no riscv64 compiler or recipe failure: Launchpad kept it pending until GitHub cancelled the runner at its six-hour limit. The workflow now gives its local waiter five hours, then leaves the named Launchpad build running and ends cleanly. Its flattened source commit has deterministic dates, so a later job re-run reconnects to that same build instead of adding another one to the queue. Store and GitHub Release publishing remain gated on a downloaded, architecture-checked squashfs snap. Regression tests cover the stable snapshot identity, the pending hand-off and the no-artifact publishing guard.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: card details once again offer every valid list and label, in every swimlane and through linked cards, and use compact accessible icons for person-field actions, while LDAP profiles consistently retain the directory's full display name. Below that: regression coverage for all four reported bugs, linked-card label writes and the icon controls, plus corrected Node and browser expectations for those controls and card dates.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Card details - choosing placement and labels on an opened card.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3a87e3378">It offers board-wide lists in every swimlane and logs moves through reactive cache gaps</a>. Thanks to mimZD, rmb82 and xet7.</summary>The list, move and copy selectors treated a legacy board-wide list as shared only in the default swimlane. They now include it for every swimlane. Activity logging also records a temporarily unavailable list or swimlane with an empty name instead of throwing after the valid card update.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3a87e3378">Its label picker falls back to the visible card board</a>. Thanks to ClemStrummer and xet7.</summary>The linked-card label refactor could resolve no source board and render only the Create label action. It now prefers the source board but falls back through the placement board to the current visible board, retaining existing labels and newly created ones.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e633f3c74">Adding a label through a linked card updates its source</a>. Thanks to xet7.</summary>The label row is a nested popup context, while the routed board contains the linked representation. The popup now retains the card as its explicit mutation target and resolves the label catalogue and new-label board from the source, so selecting or creating labels through a linked card works consistently.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/96e598059">Requested By and Assigned By use an icon instead of Add text</a>. Thanks to xet7.</summary>Empty person fields now use the same compact icon-only language as description editing: a plus to add and a pencil to edit. Both states keep localized hover tooltips and accessible names, while the decorative icons stay hidden from screen readers.
</details>LDAP profiles - directory names shown to signed-in users.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3a87e3378">They normalize displayName values to text before saving the full name</a>. Thanks to Nissulya and xet7.</summary>Active Directory attributes can arrive as a scalar, a one-element array or a
buffer. The full-name sync now converts all three shapes to text, so the user
header and profile do not fall back to sAMAccountName while the Admin Panel
still sees directory data.
and updates the following developer tooling:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d61772c08">Regression suites follow accessible Add icons and Card-wrapped dates</a>. Thanks to xet7.</summary>The all-tests run still expected visible Add text after those actions became icon-only controls, and one date test expected the direct context used before the v11.05 card-date repair. Node and Playwright coverage now checks the localized accessible name, tooltip and plus icon, and the date assertion matches the Card explicitly passed through the Blaze argument context. The complete plain-Node run passes all 495 suites.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: opened cards with saved dates render normally again, retain their labels and remain editable instead of stopping Blaze reactivity with a date-template context error. Launchpad snap builds now wait for their release bundles and preserve valid artifacts when Snapcraft only fails during post-download cleanup.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/e909b475b">Cards with saved dates open and remain editable</a>. Thanks to ClemStrummer and xet7.</summary>Named Blaze arguments had replaced each opened-card date template's Card
context with a plain argument object. Reactive date helpers then called Card
methods on that object, stopping card-detail rendering and leaving the board in
a refresh loop after a date save. Card details now passes the Card explicitly,
while the shared date code accepts both that wrapped context and the direct
context used by minicards and Table view. Browser coverage opens a labeled card
with all four dates, verifies every badge and label, edits its title, and checks
that no date-context exception occurs.
and fixes the following developer-tooling bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/2be7aac2b">Launchpad waits for release bundles and keeps validated snaps</a>. Thanks to xet7.</summary>Launchpad snap jobs used to start alongside the extra-architecture bundle jobs, so s390x and ppc64el repeatedly downloaded release assets that did not exist yet. They now wait for those jobs and verify the exact architecture-specific asset before starting a remote build. A missing optional bundle is skipped with its real cause instead of spending hours retrying a permanent 404.
A Snapcraft SSL error during cleanup also used to discard an armhf snap that had already downloaded successfully. The workflow now keeps an artifact only after checking its minimum size and squashfs magic, regardless of the later cleanup status. Release-workflow tests cover the dependency, bundle-name mapping, missing-asset path, step gates, successful cleanup-failure path and invalid-artifact rejection.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: card date editors now preserve edited date and time values through reactive redraws and wait for persistence before closing, fixing an intermittent Chromium failure when changing an existing start date. Meteor moves to 3.5.2-beta.0 together with its matching Rspack integration and core package prereleases.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c118812f">Date editors preserve edits through reactive redraws</a>. Thanks to xet7.</summary>The shared date editor now synchronizes valid native date and time input changes with its reactive draft and awaits every card, vote and planning-poker date write before closing. This prevents a redraw from restoring the old value between editing and submission. The Chromium start-date regression passed ten consecutive runs, and a node guard covers every asynchronous callback.
</details>and updates the following dependencies:
accounts-base, Babel, DDP client, ECMAScript, JavaScript minifier,
MongoDB driver wrapper, Rspack, tools-core and TypeScript packages move to
their beta352.0 builds, while @meteorjs/rspack moves from 2.1.0 to
2.2.0-beta.0. Update.Thanks to Meteor developers.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: accessibility gives shared tabs, dialogs, images, password controls and card edit targets coherent names and keyboard order throughout WeKan, with one common keyboard utility keeping those behaviors consistent. Card details restore checkbox custom fields, keep their saved values separate from field visibility, save currency values, provide independent opened-card and minicard visibility settings, make every custom-field value copyable, use compact accessible pencil icons for Edit actions, restore the grid/one-per-row layout switch, restore cross-board card links, omit deleted custom fields from exports, and make attachment previews use the available viewport. JFIF image uploads receive portable JPEG download names. Linked cards mirror every visible source field across boards and authorized members can edit that shared content from either board. Card locations recognize both map URLs and plain coordinate pairs. Positive, negative and browser regression coverage keeps each interaction working. Existing boards also receive the new default-on opened-card custom-fields setting during schema upgrade. Opened cards can be resized wider as well as narrower on desktop. All Boards keeps the complete invitation message and its actions visible on phone-sized layouts. Developer tooling keeps long-running Rspack development watchers from retaining cache state until they exhaust the JavaScript heap, and lets Flatpak terminals run the Firefox/WebKit matrix through host Docker. Dependencies refresh S3 storage, build analysis, keyboard shortcuts, CSV parsing and browser automation. Admin Panel / Problems / Offices groups login addresses by person and shows each address family, available location, per-person login count and available initials instead of empty avatar circles. Admin Panel / People / People summarizes each person's login countries and opens country-by-country city, IPv4, IPv6 and login-time details. Problems pagination no longer mixes a specialized pane's state with the shared reports. The complete browser run restores inline title and date editing, popup focus, loading-state accessibility and phone board scrolling while bringing its selectors in step with the current UI.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
The Admin Panel - reports about people and where they log in from.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3539f2bbf33a5481f2cc0cf4569d725e19c509c9">Offices groups login addresses by person with locations and counts</a>. Thanks to xet7.</summary>The Offices report was inverted: each row represented an address and placed all people using it inside one cell. It now pages people and keeps every address for one person together. Each row has separate IPv4 and IPv6 columns, the latest country flag and city supplied by Cloudflare or another supported proxy, and that person's successful-login count and first/last times for that address.
Location is recorded when a login succeeds; existing address tallies are not
retroactively geolocated. With Cloudflare, the proxied hostname must enable the
Add visitor location headers Managed Transform so CF-IPCountry, CF-IPCity,
CF-Region, CF-IPLatitude and CF-IPLongitude reach WeKan. Caddy passes
these request headers through by default; any header_up override belongs
inside its reverse_proxy block.
People sharing one address remain separate groups with separate counts. Search still matches names, addresses and locations, while location metadata for a page is fetched in one batch. Positive and negative coverage checks both IP families, shared addresses, supported location headers and absent geography.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/480074969b5aa80cf776a44c5b48196b536884f0">Offices shows available initials instead of empty avatar circles</a>. Thanks to xet7.</summary>The server already computed initials for every person, but person grouping dropped that field and the shared table tried to find the user again in the client cache. When the full user document was not published there, the fallback had no letters to draw and rendered an empty grey circle.
Initials now travel with the person through the response, grouped rows and shared table cell. The existing avatar template uses them directly and retains its reactive user lookup as the fallback everywhere else. Regression coverage checks both the Offices path and the generic table conversion.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ff9d0bab8b63bee6ef70c70decf82a852f5bb2ba">People shows country login counters with city and address details</a>. Thanks to xet7.</summary>A Location column immediately before Status now shows each person's country flags and successful-login counts. Selecting a counter opens that person's location report: countries are the left menu, busiest first, and the selected country's shared Table.md table lists city, IPv4, IPv6 and the person's first and last login times for every address.
The detail view has the shared search, pagination and Back controls. Location data is fetched once per People page and restricted through the same site-admin or tenant-admin scope as the People list. Unknown locations are not guessed, stale page responses are discarded, and new logins retain the available proxy location on the person's own address tally.
Positive and negative coverage checks country totals and ordering, both IP families, city rows, timestamps, menu and table wiring, authorization limits and absent geography.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2c034643b82785b4197fee7a1a810799280ac912">Problems pagination keeps each pane's state separate</a>. Thanks to xet7.</summary>Pagination events from specialized Problems panes bubbled into the containing shared report handler. Their pane ids intentionally have no shared report configuration, so clicking Next could try to read a count from undefined and stop the requested action with a browser exception.
Event-stream and Offices controls now keep their events inside their own pane. The shared Previous, Next and search handlers also safely ignore missing or transitional report state. Regression coverage exercises both specialized pagers and the defensive shared-handler path.
</details>Card details - fields, attachments and links on an opened card.
<details> <summary><a href="https://github.com/wekan/wekan/commit/491781c449c1e1e03bc3620472dd04f91ecd741f">Existing boards keep default-on opened-card custom fields</a>. Thanks to xet7.</summary>The board schema made opened-card custom fields visible by default, but the
schema-upgrade list omitted that new flag. Boards created before the setting
therefore retained no stored default when their other default-on feature flags
were repaired. The upgrade now backfills allowsCustomFields: true while still
preserving an administrator's explicit false choice.
The complete 490-suite Node run also exposed guards that still described old
custom-field, date-control, card-width, initials and publication layouts, plus a
security scan entering a downloaded Go toolchain under .tools. Those guards
now pin the current intended behavior and scan only maintained FerretDB source.
Content detection already identified a JFIF upload as JPEG, but the MIME
database did not recognize the .jfif filename suffix. The generic correction
therefore appended .jpeg and stored names such as photo.jfif.jpeg.
Detected JPEG content named with .jfif now replaces that suffix with .jpeg.
Downloads consequently use a conventional filename that desktop file
associations can open directly, while valid .jpg and .jpeg names remain
unchanged. Positive and negative tests cover replacement, non-appending and the
Security Report's sanitization reason.
Visible Edit controls on cards now use the same Font Awesome pencil-square icon as Description instead of repeating the word. This covers custom fields, Requested By, Assigned By and comments; Add remains text where it is a distinct action. Every icon is decorative to assistive technology, while its control retains the localized Edit tooltip and accessible name.
Focused coverage rejects textual Edit controls, checks the icon and accessibility attributes, and keeps the separate Add state intact.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/46b276e7582c1bd020ccb2fe4d01a88b8ee177ae">Opened cards can be resized wider as well as narrower</a>. Thanks to xet7.</summary>The desktop card already had a bottom-right resize handle, but its 520px opening width was also a hard maximum, so dragging could only make the card narrower. It still opens at the compact 520px width and can now grow up to the viewport's eight-pixel margins. Mobile remains full-screen and non-resizable.
Placement and maximized-card regression coverage checks both resize directions, the initial width, viewport ceiling, mobile behavior and maximized geometry.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0ca913d477b8583b19d5ea5c60fd27a9d4ba0ff">The Custom Fields layout toggle changes and remembers the layout</a>. Thanks to xet7.</summary>The toggle continued saving the user's customFieldsGrid preference after the
card-section refactor, but the rendered container no longer read it, so nothing
visibly changed. The off state now produces a compact wrapping grid and the on
state makes each custom field occupy a full row, preserving the toggle's
original behavior.
Focused positive and negative coverage checks both layout classes and the persisted method wiring. A browser regression switches the layout and reloads the card to verify that the selected mode remains active.
</details>Wider opened cards fit more compact-grid custom field columns. Thanks to xet7.
The Custom Fields layout selector shows Grid and List icons. Thanks to xet7.
The layout selector sits between the Custom Fields title and menu. Thanks to xet7.
Changing the Custom Fields layout no longer collapses the section. Thanks to xet7.
Selecting a custom field and changing a checkbox now use acknowledged server methods with board-write and field-definition checks. A rejected optimistic client write can therefore no longer make a checkbox appear inert or make a deselected field spring back. PDF and Excel export also omit an orphan field whose definition has been deleted instead of exposing its internal ID.
Unit tests cover successful writes, authorization and field-type failures, and the orphan export case. The browser test checks a checkbox and removes its field from an opened card.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6791ade7fc8fc40b2978055687b3c8a451e155c0">Opened-card custom checkboxes save values without hiding fields</a>. Thanks to xet7.</summary>The checkbox rendered as a card field borrowed the checklist item's event class and derived its next state from the template context. It now has a dedicated control, reads the persisted field value, and saves its boolean without allowing the click to reach visibility or checklist handlers. The Custom Fields menu remains the separate place that shows or hides the field.
Static coverage keeps the value and visibility event paths distinct. The
browser regression saves both true and false from the opened card and
verifies that the field remains visible after each change.
Saving the checkbox already changed the card's boolean and its minicard badge, but the opened-card square read a nested template path that did not react to that change. It now reads the custom field's direct persisted value, so the opened card and minicard show the same checked state immediately.
The regression test requires every opened-card checkbox checkmark to bind to
value and rejects the stale data.value path.
The checkbox editor calculated its second click from the Card object captured
when the template was created, so it could keep saving true instead of
unchecking. It now reads the reactive field context and saves either boolean.
Custom-field values also no longer appear automatically on minicards. Card menu / Show on Card gains a Custom Fields setting that defaults to enabled; Minicard menu / Show on Minicard gains its independent Custom Fields setting that defaults to disabled. Enabling the minicard option shows assigned fields without changing their values or the opened-card setting.
Positive and negative tests cover both defaults, both menu handlers, both rendering gates and repeated checkbox toggles. Existing browser coverage that expects minicard custom fields explicitly enables the opt-in setting.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6a437ef31d93afbf20dd9555cb3646922088fc21">Currency custom fields save and provide an X beside Save</a>. Thanks to xet7.</summary>The currency editor issued an unacknowledged direct card update and closed without knowing whether the value was accepted. It now parses dot or comma decimals, rejects non-finite input, and awaits a server method that verifies card-edit permission, the board's currency-field definition and the assigned field before storing the number. Linked cards use the same source-card route.
Its input reads the direct current value, and the standard X close control now sits immediately after Save. Static tests cover authorization, field type, finite-number validation and control order; the browser regression enters a comma-decimal amount and verifies the stored numeric value.
</details>Custom Field Dropdown places an X close control beside Save. Thanks to xet7.
Custom Field Number places an X close control beside Save. Thanks to xet7.
Custom Field Text places an X close control beside Save. Thanks to xet7.
Custom Field String Template places an X close control beside Save. Thanks to xet7.
Text fields retain the copy button supplied by their rich-text editor. Number, checkbox, currency, date, dropdown and string-template fields now use a shared copy control beside their editable value. Date values are copied in ISO format and multi-part values are separated by newlines.
Focused interaction coverage verifies that every field type exposes a copy control and that the shared handler normalizes scalar, date and array values.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9eb0c76cbd3c8ffdfbc25e3fdfbc1047f1508927">Custom field titles open editing without taking over field controls</a>. Thanks to xet7.</summary>Clicking a custom field's title or displayed value now opens its value editor. Checkbox fields keep their square as an immediate on/off control, while their title and the empty area beside the square open a staged editor with Save and Close.
Copy is hidden while a field is only being viewed. Every field editor provides the action while editing, and it copies the currently edited input rather than the previously saved value. Focused positive and negative coverage checks the view/edit boundary; the browser regression checks its editing-only visibility.
</details>Custom field titles and values open editing while checkbox squares toggle. Thanks to xet7.
Checkbox custom fields can be checked and unchecked while editing. Thanks to xet7.
Non-Date custom field value clicks work across their nested Blaze templates. Thanks to xet7.
Date custom fields restore their direct datetime popup opener. Thanks to xet7.
The Date custom field popup keeps Date, Time and Copy on one row without a second X. Thanks to xet7.
Dropdown custom fields preselect their saved value when editing. Thanks to xet7.
Every custom field editor starts with its saved value. Thanks to xet7.
Text custom fields align Convert to Markdown immediately left of Copy. Thanks to xet7.
Their Blaze helpers run inside each customFieldsWD, where the current context
is already a custom-field row. They tried to call the Card method
customFieldsWD() on that row, throwing on every render and interrupting the
surrounding card UI. Both helpers now format the row's resolved trueValue
directly, preserve numeric zero, leave empty values empty and use safe fallbacks
for invalid definitions or values.
Focused regression coverage rejects the invalid Card call and checks the value, empty, numeric and String Template paths.
</details>A linked card displayed the source fields but several editors still wrote its empty placement placeholder, while server methods required write access on the source board. A member who may edit the current board can now edit the shared source content through a visible, active link. Revoking source visibility, archiving the link, or assigning a read-only current-board role removes that delegation. Moving and archiving still affect only the linked representation.
The same source route covers titles, descriptions, dates, colors, people, labels, stickers, locations, dependencies, custom fields, checklists, subtasks, attachments, covers, watchers, minicard settings, votes and estimates. Label and custom-field definitions come from the source board, while permission to edit the card comes from the board on which the linked card is visible.
Positive and negative tests inventory the content mutators, method arguments and active-link authorization boundary. The browser regression edits the opened linked card, verifies both stored representations, then edits the source card from its own board.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e3588f2a0eaad73c1468361e55c7d0cae77e9a33">Attachment previews use the available viewport</a>. Thanks to rmb82 and xet7.</summary>Desktop PDF and text previews were fixed at 560 pixels wide, while a misplaced media query changed them to 840 pixels only on narrower screens. The overlay is now a viewport-filling flex layout: document viewers take the space between the navigation controls, images retain their aspect ratio, and mobile controls keep their compact layout. Static positive and negative tests reject another fixed desktop document width, and a browser test measures the rendered preview.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/798390c6fcc7c905b12c1afda57606ec3328c5ba">Cross-board card links are created and their dialog closes</a>. Thanks to ClemStrummer and xet7.</summary>The Link dialog directly inserted its pointer card from the client, so a server rejection could undo the optimistic insert and leave the dialog open without an explanation. It now awaits one server-authoritative operation and closes only after success. The server verifies read access to the source, write access to the destination, the selected list and swimlane, and rejects archived, same-board, template and link-pointer targets.
Method tests cover the acknowledged path and invalid targets. The browser test follows the reported board, swimlane, list, card and position selection, then checks both the closed dialog and the stored linked card.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a830cae64742bfcf99ab500a19872413e35de802">Link placement survives asynchronous popup confirmation</a>. Thanks to xet7.</summary>The linked-board confirmation asked Blaze for the popup's top or bottom
position after awaiting the next card number. By then its event view was no
longer current, so Blaze threw There is no current view before inserting the
link. The popup now captures its position when it is created and its sort
calculation uses only that stored value after asynchronous work. The board
selector is scoped to the same popup as well.
The regression test rejects any later Template.currentData() call inside the
sort calculation and covers both placement choices.
Detect location already recognized provider URLs such as Google Maps
@latitude,longitude,zoom, but a coordinate pair copied directly from a map or
GPS application produced no result. The same field now accepts a complete
latitude, longitude pair with optional surrounding whitespace and retains its
range checks. The match is anchored to the entire input so prose that happens
to contain two numbers is not mistaken for a location.
Parser tests cover both supplied formats, exact precision and the prose negative case. The browser regression enters each format through the location popup and verifies the detected latitude and longitude fields.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8fac3ab89127cc0d7d2164664152dffc0fbc32df">Linked cards mirror every visible field from their source</a>. Thanks to xet7.</summary>A linked card mixed a few source-aware getters with raw fields from its empty placement placeholder. Titles, dates and comments worked, while labels, stickers, custom fields, locations, dependencies, subtasks, colors, creator details and other direct fields could disappear. After reload, the source board's label and custom-field definitions were also absent because only the source card document and children were published.
Linked cards now resolve displayed content through one live source-card path. The current-board publication supplies the authorized source metadata, definition records, relevant users, subtasks and dependency targets; all remain behind the existing source-board visibility check, and child-card queries are constrained to those authorized boards. Placement itself still belongs to the linked placeholder on the current board.
Parity tests cover every directly rendered collection and the negative publication boundaries. The browser regression adds a source label, sticker, custom field and location, then verifies them on both the linked minicard and its opened details.
</details>Accessibility - keyboard order, control names and dialog focus across pages.
<details> <summary><a href="https://github.com/wekan/wekan/commit/692207bf5ec42ba97fc189beb4135975b6d20f31">Tabs and dialogs follow a coherent keyboard and screen-reader model</a>. Thanks to xet7.</summary>Shared tabs now expose tablist, tab and tabpanel relationships, keep only the active tab in normal Tab order, and support arrow, Home, End, Enter and Space keys. Popups and modals move focus inside, contain forward and reverse Tab navigation, and return focus to their opener when closed. Password visibility is no longer skipped by Tab, card and minicard edit targets are focusable, and all template images explicitly provide meaningful or intentionally empty alternative text.
Application-wide static coverage rejects positive tabindex values and images
without alt, while focused tests pin names, roles, relationships, keyboard
handlers and focus restoration. The browser regression audits representative
pages for natural order, unnamed controls and missing image alternatives, and
exercises both directions of the popup focus loop.
Popup, modal and board-menu focus previously discovered controls independently,
while shared tabs and Archive tabs separately implemented the same six-key
navigation rule. Focusable-element discovery, initial focus, Tab trapping and
tab-key calculation now live together in client/lib/accessibility.js; each
consumer supplies only its own state transition. The board-only menu observer
also disconnects with its template instead of remaining attached after leaving
a board.
Regression coverage requires both tab implementations and every dialog consumer to delegate to the shared helpers, and rejects copies of the old key and focus selector branches.
</details>All Boards - the overview, its Archive actions and phone-sized layouts.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0aad02722">Archived boards can be deleted and selected cards can be archived reliably</a>. Thanks to Nissulya and xet7.</summary>The archived-board half of #6608 is handled by the new Global-Admin-only, explicitly enabled and confirmed multi-selection Delete action. For cards, the sidebar previously fired direct client updates and closed immediately, so a refused write looked successful while every card stayed in place. It now sends one ordered selection to an awaited server method. The server validates the board, write access and every live card before archiving the first; a failure reports its reason and keeps the selection open. Unit tests cover positive and negative client/server paths, and a browser test selects and archives two cards from one list.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dc12789b43006a6fc1d48c2df07c17275728f95c">Phone board invitations show their message and actions</a>. Thanks to mimZD and xet7.</summary>The compact phone layout forced every board icon to exactly four rem, including an invitation that also contains its explanatory message and two touch-size buttons. The board tile clipped that overflow, leaving only the title visible. Invitation tiles now carry an explicit state class and retain the ordinary four-rem floor while growing naturally around the message, Accept and Decline. Ordinary board icons remain compact and equal-height.
Static coverage keeps the invitation exception attached to its template state. The phone browser regression creates a real pending invitation and verifies that its message and both buttons remain visible and inside the tile.
</details>Complete browser regression run - cross-page interactions exercised by all three browser engines.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c476ee45a9162bb09b2c4937ccc048d20e4f98af">Browser regressions restore editing, focus and phone scrolling</a>. Thanks to xet7.</summary>The all-browser run exposed interactions that narrower source tests could not: the minicard wrapper stopped its sibling inline-title handler, shared opened-card date badges lost the explicit edit-permission argument, popup Tab handling used Blaze's inconsistent delegated event container, and the stock logo briefly lost its alternative text while settings loaded. Those paths now retain editing permission, focus containment and accessible loading states.
The shared table search has an accessible name and a phone's board list grows inside the single page scroller. Browser coverage now opens the attachment viewer where that template exists, scopes the linked-card and custom-field controls to their real DOM owners, waits for reactive options and fields, and checks the icon-only Edit control by its accessible name. Focused positive and negative source tests pin the permission, focus, table and scroll contracts.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9f9fa4c2d53d1ad6592d142f4cc547649639348">Browser regression corrections now pass against a fresh production bundle</a>. Thanks to xet7.</summary>Linked-card creation now restarts the board subscription so source labels, stickers and custom fields are available with the new link. Date writes are awaited and refresh the same subscription, popup focus trapping uses the real dialog element, and the phone board grid leaves vertical scrolling to the page.
The browser tests now use stable card identities and the actual interactive DOM targets after titles change, distinguish visible and compact label markup, seed existing dates before subscribing, and measure scroll ownership instead of relying on a browser's normalized overflow keyword. The complete Node suite and the modified Chromium browser group cover the positive and negative paths.
</details>and updates the following dependencies:
Thanks to dependabot.
and improves developer tooling:
Development builds - local builds and long-running watchers.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e1f842eb56e20b2033cd999214fec10d187ca929">Long-running Rspack watchers no longer exhaust the JavaScript heap</a>. Thanks to xet7.</summary>Rspack's experimental persistent cache retained JavaScript-side serialization
state while meteor run repeatedly rebuilt WeKan's large Blaze graph. After a
long development session that retained state could consume the full 16 GB V8
heap and abort the bundler.
Development client and server watchers now run without the persistent cache. Short-lived production builds keep caching, and unrelated Rspack experiments still merge normally. Focused coverage checks both watchers, the production negative case and the merge over Meteor's cache defaults.
</details>Browser test containers - running the complete Playwright matrix from the documented VS Code sandbox.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5ae63a53417123c9a1686f94a69178a5bc412203">Flatpak terminals can run Firefox and WebKit through host Docker</a>. Thanks to xet7.</summary>The ARM64 browser runner correctly selected Playwright's official Docker image,
but a VS Code Flatpak terminal could not see the host docker executable and
reported that Docker was not installed. The build script now discovers Docker
through flatpak-spawn --host and routes image pulls, browser containers and
conformance cleanup through the same host-aware wrapper.
Regression coverage pins both direct and Flatpak-host discovery. The complete Firefox and WebKit matrices were run against a fresh production bundle; all 255 runnable tests passed in each engine, with the eight Chromium-only drag harness tests intentionally skipped.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: release metadata advances WeKan to v11.02 and records the binary provenance carried by its platform bundles.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release updates release metadata:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b82ecfdd34903c38d92b111e2204da3ea4e8ddbd">Release metadata advances to v11.02</a>. Thanks to xet7.</summary>The release preparation records v11.02 and carries forward the verified Node.js and FerretDB binary provenance for every built platform.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: Admin Panel / Problems gains the missing Delete settings pane, where a Global Admin can enable the existing, default-off permanent-delete gate. The pane sits above Notifications, has a stable address of its own, and explains that enabling the gate does not delete content by itself. Its checkbox also stays checked after saving because the reactive settings publication returns the stored value. Below that: regression coverage keeps the menu order, pane rendering, URL, setting handler and publication together, with the corresponding Admin Panel documentation and English source strings. The newest All Boards and permanent-delete controls are also translated into 69 languages and regional variants. Permanent-delete setting changes and board-purge attempts, including unauthorized and failed ones, are audited in Recovery with Boolean status, acting user, proxy-aware IP address, board IDs and titles; coloured icons distinguish success, failure and deleted data, and a dropdown filters the audit to All, Done, Failed or Deleted events. A second description below the database-recovery explanation names the permanent-delete events and fields recorded here. The Delete settings pane repeats that audit explanation below its existing text, so an administrator sees what Recovery will record before changing the gate. Board Archive removes permanent delete from individual board icons and offers it as one confirmed red action on a multi-selection, with the same gate enforced again on the server without Meteor argument-audit failures. When that gate is disabled, the sidebar explains where to enable it instead of showing an inapplicable selection instruction. Its enabled Delete button and server method both require the site-wide Global Admin flag. Select All and Select None above the icons make that selection explicit and quick. Dragging that archived selection now highlights only Remaining and existing Workspaces as valid restore targets. Archived tiles no longer show an action-looking archive glyph at their lower-left corner. The same themed controls now cover Remaining, Starred, Home and Templates, always following the visible section and search result. While that mode is on, its action sidebar now stays visible until Multi-Selection is turned off, and its actions remain available before the first board is checked. Closing that sidebar with its X also turns Multi-Selection off. Setting a Home board now requires exactly one checked board, and the Home section offers only the actions that make sense for its current board. Dragging a selection onto Home follows the same one-board rule. Remaining can now drag boards onto Starred or Archive as green targets. Home's empty state also states its one-board limit before dragging begins. All Boards and board loading now publish only dashboard board fields, keep templates separate, omit empty share branches, paginate in the database and snapshot lazy card windows on FerretDB. Workspace boards can also be dragged additively to Starred or, after confirmation, moved to Archive, and existing Workspace views now show Select All and Select None while Multi-Selection is active. Selected cards are now archived by one acknowledged server operation, so a failure remains visible and leaves the selection available to retry. The CPU governor also observes FerretDB before acting and never slows its read path when its configured cap is zero or an idle WeKan sees FerretDB itself busy.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release adds the following features:
The Admin Panel - server-wide safety settings.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4ab3161b4">The Delete settings pane appears above Notifications</a>. Thanks to xet7.</summary>The removed Features page left no place to operate the existing
enablePermanentDelete gate. Problems now has a directly linkable Delete pane
whose checkbox exposes that default-off setting to Global Admins. Turning the
gate on does not delete anything by itself; it only permits an explicit purge.
Menu-order, pane-rendering, URL and setting-handler tests cover the addition.
Enabling or disabling Admin Panel → Problems → Delete now goes through a strict Global Admin server method and records the actual changed state in Recovery with the actor's username and user ID. Each successfully and permanently removed archived board records its ID and JSON-quoted title with the same actor. No-op setting writes, unauthorized calls and failed removals cannot create misleading success records. Tests pin the ordering of write before audit, stable event types and every required detail.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7a0b7b7cd">Recovery shows every permanent-delete attempt and its outcome</a>. Thanks to xet7.</summary>Unauthorized and failed setting changes or board purges are now audited beside successful ones, without swallowing the original error. Structured fields keep Boolean Done/deleted-data state, user ID, username, trusted-proxy-resolved IPv4 or IPv6, and bounded requested board IDs and titles. Done is the report's first column: success is a green check, failure a red warning, and a successful physical deletion adds a yellow trashcan. Partial batches show both the boards already deleted and the failed whole-batch attempt. Tests cover storage, address classification, negative paths, icon rendering and colours.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5f1d42211">Recovery can filter events by outcome</a>. Thanks to xet7.</summary>The shared controls row now offers All, Done, Failed and Deleted filters above the Recovery table. The server applies the selected outcome together with search before counting and pagination, so page totals and rows stay consistent. Legacy events without the newer Boolean field remain under Done. Positive, combined and negative selector tests cover the filter and its UI wiring.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b418b0df">Recovery explains its permanent-delete audit trail</a>. Thanks to xet7.</summary>A second description below the existing database-recovery explanation now tells admins that this report also logs permanent-delete setting changes and successful, failed or unauthorized purge attempts. It names the recorded Done status, user ID, username, trusted IPv4 or IPv6 address, and attempted board IDs and titles. A UI wiring test pins both the content and its position below the original paragraph.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/71abf3f48">Delete settings explain their Recovery audit trail</a>. Thanks to xet7.</summary>The Recovery audit explanation now also appears below the existing text in Admin Panel → Problems → Delete, so the administrator sees exactly what will be recorded before enabling or disabling permanent deletion. Delete and Recovery use one shared sentence to prevent their descriptions from diverging. Tests pin its placement after the current Delete guidance and its reuse in both panes.
</details>Board Archive - restoring or permanently removing archived boards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d3034d81e">Permanent delete acts on selected archived boards</a>. Thanks to xet7.</summary>Board icons no longer carry a trashcan that can permanently delete one board by accident. With Multi-Selection active, a Global Admin who enabled Admin Panel → Problems → Delete sees one red Delete button in the Archive's right sidebar. It shows the existing irreversible board-and-content warning before sending the whole selection to one server method.
The server trusts none of those display conditions: it independently requires a Global Admin, the enabled feature flag, a bounded string-id selection and only archived boards. It validates every selected board before deleting the first, so an invalid or live-board id cannot leave a half-applied batch. Positive and negative tests cover the missing per-board control, both UI gates, confirmation, successful reset, retained selection on failure and every server-side gate.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/76e429493">The disabled Delete gate explains how to enable it</a>. Thanks to xet7.</summary>When permanent delete is disabled, Archive's Multi-Selection sidebar no longer shows "Click checkboxes to select boards" without an action beneath it. It instead explains that enabling Admin Panel → Problems → Delete makes the Delete button visible. When enabled for a Global Admin, the normal selection instruction and red Delete action return together. Positive and negative tests pin both branches, and the new ordered translation key is available in every language file without replacing human translations.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2eb3e0d4b">Only a Global Admin sees and can use Delete</a>. Thanks to xet7.</summary>The Archive Delete action now requires the site-wide Users.isAdmin flag to be
exactly true in both its sidebar helper and server method. A board-admin role,
a missing flag or a truthy non-Boolean value can neither expose the button nor
authorize a forged method call. The permanent-delete setting and archived-only
validation remain additional required gates, with regression coverage for the
strict client and server checks.
When Multi-Selection is active in Board Archive, two buttons now sit in the right page between its heading and board icons. Select All checks every icon in the exact Archive list currently rendered after searching; it does not silently select boards outside that visible result. Select None clears the shared board selection. Tests pin their Archive-only visibility, placement, translated names, compact layout and both selection operations.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9db74d849">Select None uses the same theme colour as Select All</a>. Thanks to xet7.</summary>Both Archive multi-selection controls now use the primary themed button style. The regression test requires the same class on both buttons, so one cannot fall back to the browser's unthemed grey while the other follows the site theme.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9be167742">Archived selections target only Remaining and existing Workspaces</a>. Thanks to xet7.</summary>While Multi-Selection is active in Archive, only Remaining and existing Workspace rows receive the green valid-target highlight. Home, Archive, sharing targets and the other board sections refuse the drag before accepting an HTML5 drop. A Workspace drop restores every archived board and assigns it there; regression tests cover the allowed targets, rejected targets and green styling.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fb13d01bc">Archived tiles have no lower-left archive glyph</a>. Thanks to xet7.</summary>The archive glyph inside the "Archived at" metadata inherited broad board-tile hover styling, which moved it to the lower-left corner and made it look like a per-board action. Both tile variants now keep the useful archived date as plain text without that icon. Permanent deletion remains exclusively in the right sidebar, with regression coverage for the absent glyph and retained date.
</details>All Boards - its named sections and their shared multi-selection controls.
<details> <summary><a href="https://github.com/wekan/wekan/commit/2547c5216">Every named section can select all or none of its visible boards</a>. Thanks to xet7.</summary>Remaining, Starred, Home, Templates and Archive now show the same themed Select All and Select None buttons above their board icons while Multi-Selection is on. Select All reads the exact section/search result used to render those icons, so it cannot silently include a hidden board; Select None clears the shared board selection. Home deliberately keeps Multi-Selection too: Select All simply checks its one visible Home board when present. Tests cover all five sections, both entry points, both operations, placement and matching theme classes.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7974c3e66">Existing Workspaces show Select All and Select None</a>. Thanks to xet7.</summary>An existing Workspace now shows the same themed Select All and Select None
buttons as Remaining, Starred, Home, Templates and Archive whenever
Multi-Selection is active. Select All uses the exact Workspace/search result
currently rendered by boardsForView, so hidden boards are not selected;
Select None clears the shared selection. Positive tests cover visibility,
rendered-board scope and clearing.
Close, toggle, Escape, Search and Home actions can no longer hide or replace the pane that operates selected boards. Every sidebar state change resolves back to the visible Multi-Selection view until the mode is turned off; its off controls then unlock and close the pane in that order. Positive and negative tests cover both sides of the transition.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/782200a0d">The sidebar X turns Multi-Selection off and closes it</a>. Thanks to xet7.</summary>The right-sidebar X previously called the guarded close operation while Multi-Selection was active, so the guard immediately kept the sidebar open. The X now disables selection mode first and then closes the unlocked sidebar, matching the explicit Multi-Selection Off action in one click. Other sidebar views retain their ordinary close behavior, and regression coverage pins the required operation order.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c704b65cc">Selection actions remain visible before a board is checked</a>. Thanks to xet7.</summary>The instruction, every action applicable to the current section and the way to turn Multi-Selection off are now always present in the right sidebar. An action clicked with no checked boards reports "You did not select any boards" and stops before opening a confirmation or calling the server. One shared guard and regression tests cover star, Home, archive, duplicate and permanent delete.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ddb0ab4a2">Set as Home board accepts exactly one selected board</a>. Thanks to xet7.</summary>The action no longer silently chooses the first board from a larger selection.
Only one selected id reaches toggleDefaultBoard, matching the fact that login
can open only one Home board. Regression tests cover the accepted single-board
selection and both rejected selection counts.
Set and Unset as Home board now use the same "Please select only one board" message for both invalid counts: zero and several selected boards. The other bulk actions retain their separate empty-selection warning. The Home action still stops before calling the server unless exactly one board is checked.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aebc7d9de">Home rejects a drag containing multiple boards</a>. Thanks to xet7.</summary>Dropping several selected boards onto Home now reports "Please select only one board" instead of silently making the first one Home. The rejected drop changes nothing and keeps Multi-Selection intact so the user can narrow it. A one-board drag continues to set Home normally, with regression coverage for both paths.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3edc723f2">Remaining boards can be dragged to Starred and Archive</a>. Thanks to xet7.</summary>Dragging one or several boards from Remaining now highlights Starred and Archive in green alongside its existing valid targets. A Starred drop adds only missing stars, so every board stays in Remaining and an already-starred board is not accidentally unstarred. An Archive drop keeps the existing confirmation and moves the whole drag into Archive. Tests cover the source marker, target hints, additive starring and confirmed archive path.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f9a0b3e9a">Workspace boards can be dragged to Starred and Archive</a>. Thanks to xet7.</summary>Dragging one or several boards from an existing Workspace now highlights Starred and Archive as green valid targets. A Starred drop adds only missing stars, leaves every board assigned to its current Workspace and makes it appear in Starred too. An Archive drop asks for confirmation and then archives every dragged board, removing it from the Workspace. Regression tests cover the source marker, both hints, additive stars, retained assignments, confirmation and batch archive calls.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74c387356">The empty Home asks for only one dragged board</a>. Thanks to xet7.</summary>The empty-state instruction now reads "Drag only one board here to open it after login", making Home's one-board limit visible before a drag starts. The existing translation key remains in place, and the Home regression suite pins the exact English wording.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9c6febe1d">The Home section offers Unset without Archive or Duplicate</a>. Thanks to xet7.</summary>The Home section's Multi-Selection sidebar now calls its toggle "Unset as Home board", matching what clicking its current Home board does. Move Board to Archive and Duplicate Board are hidden in that section while remaining available for the other live-board sections. A regression test pins both the Home-specific label and the absent actions.
</details>and fixes the following bugs:
The Admin Panel - server-wide safety and performance settings.
<details> <summary><a href="https://github.com/wekan/wekan/commit/43d592590">The permanent-delete checkbox stays checked after saving</a>. Thanks to xet7.</summary>The server accepted and stored enablePermanentDelete, but the settings
publication omitted the field. The next reactive render therefore read
undefined and immediately replaced the optimistic checkmark with an unchecked
box. The publication now returns the stored value, with a negative regression
test that ties the checkbox helper, update handler and published field together.
A high host-CPU event used to apply the first FerretDB delay before its status response could say which process was busy, and a maximum delay of zero still applied that first delay. The first request is now status-only. A zero cap is a clean monitoring-only setting, and an idle WeKan does not escalate delays while FerretDB's own process CPU is above its threshold. The existing backoff, recovery logging and labelled WeKan-operation mitigation remain in place.
</details>All Boards - loading and filtering the overview.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7b52a6313">The global publication sends board summaries without cards or lists</a>. Thanks to xet7.</summary>The overview previously opened composite child cursors for every board, making its first paint wait for lists and cards it does not render. It now publishes a projected set of board documents only. Template-container boards have their own projected subscription, active in Templates and cross-category search, so the dashboard retains the appearance, access, ordering and sharing data it uses without turning into a second board view.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fd0369b89">Empty share lists no longer add non-selective visibility branches</a>. Thanks to xet7.</summary>The shared board-visibility selector used to emit organization, team and domain
branches even when the caller had no ids for them. Those empty branches could
not match, but still complicated every dashboard query. They are now omitted;
non-empty branches retain the same-element $elemMatch and isActive: true
requirements, with negative coverage against revoked-share access.
The page method no longer fetches every visible board, filters and sorts the
array in Node.js, and slices only at the end. Section, workspace and search
conditions are encoded before a database count and a title/id-sorted query with
skip and limit. Cross-category search subscribes to template summaries too,
so moving the work into the database does not hide template results.
Archive actions - permanently deleting boards and archiving selected cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5a5832846">Permanent deletion checks its argument before asynchronous work</a>. Thanks to xet7.</summary>The bulk-delete method looked up its caller before handing boardIds to
Meteor's check(). With audit-argument-checks enabled, that asynchronous
boundary made Meteor report “Did not check() all arguments” and reject the
operation even though validation appeared later in the method. Validation now
runs before the first await; malformed attempts still resolve their actor in
the failure path and are written to Recovery without masking the original
error. Positive ordering and audit-path tests cover the regression.
Board views - lazy loading of a board's cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/35825c540">FerretDB card windows use bounded snapshots</a>. Thanks to xet7.</summary>The lazy window publication previously returned a limited live card cursor. FerretDB's polling observer could repeatedly scan and diff that moving window, including while a board was otherwise idle. On FerretDB the bounded card batch is now fetched once and published as a snapshot; MongoDB keeps its live cursor, and the other child publications remain reactive on both databases.
</details>and improves translations:
<details> <summary><a href="https://github.com/wekan/wekan/commit/34f8b000f">Recent board controls are translated into ten major languages</a>. Thanks to xet7.</summary>Seven new All Boards and permanent-delete strings are translated directly into Arabic, Chinese, Finnish, French, German, Italian, Japanese, Brazilian Portuguese, Russian and Spanish. They cover the permanent-delete setting and disabled hint, empty and one-board selection errors, unsetting Home, and Select None. The guarded fill writes only English placeholders, so it preserved each language's existing human translation of the Home-board drag instruction.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6cab5c7a4">Recent board translations expand to 69 locales</a>. Thanks to xet7.</summary>The same seven All Boards and permanent-delete strings now cover 69 locale files. The expansion adds Czech, Danish, Dutch, Greek, Hebrew, Hindi, Indonesian, Korean, Norwegian, Polish, Swedish, Turkish, Ukrainian and Vietnamese, plus the appropriate Arabic, Chinese, European, Japanese, Portuguese, Russian and other regional variants of the completed languages. Existing non-English values remain untouched. A regression test checks every string in every covered locale and distinguishes permanent deletion from ordinary archiving.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: RouteBleed, found by GitHub CodeQL, removes an incompletely escaped dynamic regular expression from board-export route coverage. All Boards on phones uses one native page scroller in mobile and desktop UI modes, so ordinary boards, invitation controls and table rows remain reachable with the same vertical swipe; the shared layout extends that behavior to every page. Snap database recovery can read retained MongoDB 4.x, 5.0, 6 and 7 data and merge it into the live FerretDB without opening SQLite twice. Helm containers size the Node.js heap from their memory limit, and the official chart supplies enough memory for startup plus native allocations. Minicard titles save again from their inline editor, and card dates can be reopened, changed or deleted again after they have been saved. Full-suite repairs keep the shared date form registered before its events, preserve the one mobile page scroller in mobile-view mode, and make the standalone E2E browser selection architecture-safe. Translations reuse 3,838 unambiguous same-language values, and the new reports are complete in Finnish. Regression coverage now exercises inline minicard title editing in the browser and guards the Finnish Office and API report vocabulary directly. Requested By and Assigned By say Edit when their free-text value already exists and Add when it is empty.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/ed4b8cd64">RouteBleed: route coverage compares exact text instead of an incompletely escaped pattern</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>RouteBleed - code scanning alert
#434, rule js/incomplete-sanitization (CWE-20, CWE-80 and CWE-116), in
tests/boardExportScope.test.cjs: an export path was interpolated into a
dynamic regular expression after escaping only forward slashes. Backslashes
and every actual regular-expression metacharacter remained active, so the test
could match a different route, fail to match the intended one or fail to
compile.
The code was test-only, read a hardcoded route table and is never shipped in a
WeKan bundle, so there was no runtime or user-input exposure. There is no
denied operation to attribute in Admin Panel → Problems. The fix removes the
pattern rather than adding another sanitizer: the test wants an exact route
literal and now checks that exact string with includes().
Positive and negative cases cover backslashes and the full metacharacter set, and a repository-wide guard rejects the reported slash-only escape shape in tracked JavaScript. The same sweep removed a second partial dynamic pattern from release-bundle coverage.
</details>and fixes the following bugs:
All Boards - scrolling the overview on a phone.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ae2cfd8aa">One vertical swipe reaches every board and invitation control</a>. Thanks to mimZD and xet7.</summary>The page had nested vertical overflow panes on #content, its wrapper, the left
menu, the icon grid and the table view. A swipe could be captured by the wrong
pane, and an invited-board tile's Accept and Decline controls made its grid row
taller than the percentage-height chain expected, leaving boards below it
unreachable.
#content is now the one native vertical scroller. The wrapper, both columns,
the board grid and the table page contribute their natural height to it, so a
gesture has one owner and invitation controls are not clipped. Regression tests
cover ordinary tiles, invitations, table view, viewport sizing and reject a
second nested vertical scroller.
The narrow-device rule made the body a viewport-sized, non-scrolling box, but
made #content non-scrollable too. Mobile UI mode happened to restore its
overflow through a more specific selector; explicitly selecting desktop UI
mode did not, so the bottom of All Boards and other pages was clipped behind the
browser controls.
The phone viewport and content-scroller contract now lives in the shared page
layout rather than the All Boards stylesheet. The top header stays separate
while #content scrolls in both UI modes on every route. All Boards continues
to use that one scroll owner for Starred, Remaining, Public, Archived,
Workspaces and every other left-menu section. Source and negative tests reject
a hidden content pane, and the phone browser test toggles to desktop mode before
checking that its final board remains visible.
Snap database recovery - comparing and merging the retained database copy.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9cea2f796">Recovery uses the compatible MongoDB reader and the live FerretDB</a>. Thanks to waltermhl and xet7.</summary>database-compare and database-merge tried only the current MongoDB 7
executable, although the migration carries MongoDB 5.0 and 4.2 readers for
older WiredTiger formats. A retained MongoDB 4.x or 5.0 database was therefore
reported as unreadable, and merge had no source from which to recover the
missing work.
Both recovery phases now use the same 7, 5.0 and 4.2 compatibility ladder as migration, safely skipping readers absent on an architecture and naming the startup log when all of them fail. Merge also reuses a running FerretDB target; it no longer starts a second FerretDB against the already-open SQLite database. Only temporary processes are stopped afterwards, so a live database borrowed for the operation remains running.
</details>Helm containers - the memory available while the server starts.
<details> <summary><a href="https://github.com/wekan/wekan/commit/794771ed8">The Node.js heap fits its container and the official pod fits the heap</a>. Thanks to titver968 and xet7.</summary>The official chart limited WeKan to 1 GiB while its own comments claimed the Docker image supplied a 4 GiB heap setting. It did not: Node.js 24 derived a roughly 640 MiB V8 ceiling from the cgroup, and the v10.96+ server bundle could exhaust it while linking and creating startup indexes, before ordinary application logging began.
The container now gives V8 three quarters of its cgroup memory, from 768 MiB up
to the documented 4 GiB ceiling, leaving one quarter for native allocations.
An administrator's explicit NODE_OPTIONS always wins. The
official chart now requests
512 MiB and limits the WeKan pod to 2 GiB, providing a 1536 MiB heap plus 512
MiB of native headroom by default.
Minicards - editing a card directly on the board.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f9934ebde">Saving an inline title edit renames its minicard again</a>. Thanks to Heart1010 and xet7.</summary>The title editor is a nested inlinedForm, whose submit event receives the
form arguments as this, not the Card document. Saving therefore called
getTitle() on { classNames: "js-minicard-title-form" }, threw a TypeError
and restored the old title.
The handler now takes the Card from its enclosing minicard template instance, compares and saves through that document, and never treats the nested event context as a Card. Empty and unchanged titles remain no-ops.
</details>Opened cards - editing dates that are already stored on a card.
<details> <summary><a href="https://github.com/wekan/wekan/commit/fd41970ed">Saved card dates can be changed and deleted again</a>. Thanks to Alishara and xet7.</summary>The common date form became a child Blaze template, but its Save, Delete and validation event map remained on each parent popup. Blaze does not dispatch a child template's events to its parent's event map, so the form rendered its existing value while every control inside it was inert.
The common form now owns its common event handlers and receives the parent popup's state and field-specific callbacks explicitly. Received, start, due, end, vote, planning-poker and custom-field dates retain one shared form, and a browser regression test changes a previously stored due date.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/14b04575b">Clicking any saved card date reopens its editor with the Card context</a>. Thanks to xet7.</summary>The first date could be added because its plus button lives directly in the
opened card's data context. Once saved, however, the date became a shared badge
whose child-template data contains only display arguments. Clicking that badge
passed those arguments to the popup as though they were a Card, so calls such
as getStart() and getReceived() threw and no editor appeared.
Every received, start, due, end, vote and planning-poker date badge now opens its popup explicitly with the surrounding date template's Card and stops the click before the opened-card handler can create a second popup. Source-level positive and negative tests cover all four ordinary card dates, and the browser suite adds a saved start date, reopens it, changes it and verifies the new date.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bf4c00459">Populated Requested By and Assigned By text fields say Edit instead of Add</a>. Thanks to xet7.</summary>The free-text control previously said Add regardless of whether the field was empty or already displayed a value. Both opened-card fields now use the existing translated Edit label when text exists and retain Add only for an empty value; their separate plus button continues to open the board-member selector.
The source guard checks both branches for both fields, including the empty-value negative case. Browser coverage seeds populated Requested By text beside an empty Assigned By field and verifies that the two controls render Edit and Add respectively.
</details>The full test run - client startup, mobile scrolling and portable test execution.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a2ba64c0f">The newest full-suite regressions are repaired and diagnosed at their source</a>. Thanks to xet7.</summary>The shared date form registered its Blaze event map before its Jade template
existed, throwing on client startup and leaving Node E2E and every browser test
on a blank page. The forms feature now creates editDateForm before the
datepicker library attaches its events, with an import-order regression guard.
On phone-sized All Boards pages, the more-specific .board-list.mobile-view
rule overrode the new natural-height list and restored a nested vertical
scroller. The phone rule now covers both selectors, so #content remains the
one swipe owner in either view.
The standalone E2E runner now discovers current Playwright cache revisions and rejects a Chromium binary for the wrong CPU architecture. Failed page renders also report browser exceptions and failed requests instead of only an empty body. The remaining completed failures were stale guards updated for the shared export document, server-supplied download names, reorganized LDAP documentation, the translation-memory helper and explicit bundle-smoke Node binary.
</details>and adds the following developer-facing test coverage:
<details> <summary><a href="https://github.com/wekan/wekan/commit/bb9680f00">Upcoming minicard and Finnish translation changes have direct regression tests</a>. Thanks to xet7.</summary>The browser suite now edits a minicard title from the board and verifies that the card is renamed without opening its details. Its negative case submits only whitespace and verifies that the original title remains visible.
A plain Node.js suite inventories every Office and API report translation in
Finnish, rejects missing, empty and unintended English placeholders, and keeps
the universal API, REST API and WITH_API=true terms recognizable. The
existing Upcoming tests continue to cover RouteBleed, phone scrolling, Snap
recovery, container heap limits, card dates and same-language translation
memory, including their negative cases.
and improves translations:
Translation completeness - filling only English placeholders, without an external translation service.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f94d5cbda">Repeated source text reuses unambiguous translation memory from the same language</a>. Thanks to xet7.</summary>WeKan has many keys with identical English text. When a language already has exactly one non-English translation for that source, the same value can safely fill its still-English copies without guessing or crossing a language boundary. This filled 3,838 placeholders across 227 language files.
The reusable pass is dry-run by default, writes only when explicitly asked, never replaces an existing target translation, and leaves a source untouched when its translations disagree. Tests pin all four constraints; the human-preference and wrong-script checks remain clean.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/73c6bc059">The Office and API reports are translated into Finnish</a>. Thanks to xet7.</summary>All fourteen translatable strings added by the two Admin Panel → Problems reports now have Finnish labels, descriptions and empty-state messages. Product names, protocol acronyms, numbers and symbols remain unchanged because those values are already the same in Finnish.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: the unified export dialog can finally be changed, visibly shows its selection with the Admin Panel's own checkbox, and passes that selection to every export format. PDF and Excel exports now share one card document, use the saved user language or browser fallback, keep the opened card's date format, preserve multilingual text, and name downloads for the localized board, swimlane, list or card exported. Their shared card layout now includes locations, stickers, dependencies and sort position; PDF also embeds JPEG and PNG attachment previews plus Unicode-plane fonts. Admin Panel / Problems keeps avatars at avatar size, and All Boards keeps its Add Board and Home placeholder tiles as tall as the boards beside them. Requested By and Assigned By can select board members while retaining their free-text fields. Below that: fourteen export fixes, one export-layout consolidation, one people-picker fix, two shared-checkbox fixes, two UI sizing fixes, restored subtask creation, and the documentation move into its feature and platform hierarchy with every local link checked.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release adds the following new feature:
Opened cards - the people responsible for requesting and assigning work.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bb5275e97">Requested By and Assigned By can select board members</a>. Thanks to xet7.</summary>Their round + now opens a searchable board-member picker like Members and
Assignee. Selected people appear as avatars, while the Add link stays below
the + and continues to open the existing free-text editor. The two forms are
independent, so a card can retain an external name or note beside its selected
members.
Cards store selected people in requesters and assigners user-ID arrays,
with assign, unassign and toggle operations matching assignees. WeKan JSON and
scoped imports preserve them, whole-board imports remap their user IDs, and
user removal cleans them up. PDF, detailed and table Excel, and CSV resolve the
selected people beside the original text fields. The implementation completes
the existing Requested By / Assigned By design document.
and fixes the following bugs:
Opened cards - the people responsible for requesting and assigning work.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c183b5bf8">The new people pickers have titles and stay below their buttons</a>. Thanks to xet7.</summary>Requested By and Assigned By opened headerless popups because their generated title keys did not exist. They now reuse the fields' existing translated names, so every language gets a title without adding a second English placeholder to all translation files.
The general popup geometry also chose whichever side of an opener had more
space. That made Assigned By jump above the card while Members and Assignee
happened to open below. All four card-people pickers now anchor directly below
their + button, use the remaining space there, and retain the same member-list
body and styling.
Subtasks - creating their hidden helper-board records.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f3ca168e2">A new subtask is created even before its helper-board cache refreshes</a>. Thanks to xet7.</summary>Creating the first subtask also creates its hidden helper board and default swimlane. The server immediately looked for that swimlane through a reactive cache which could still hold the earlier empty result, so it returned without inserting the subtask and the form appeared to do nothing.
The async server lookup now reads the authoritative collection, and its default-swimlane self-heal falls back to that collection after inserting. The form retains the entered title and reports the actual error when creation really fails instead of silently clearing it.
</details>Exporting - choosing what goes in the file.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ec9f9e76d">Board Excel exports now use the detailed card layout they request</a>. Thanks to xet7.</summary>The export popup sent the card-details layout choice, but the server's shared
field allowlist omitted that key and silently removed it. Every board, swimlane
and list Excel request therefore selected the old one-row-per-card streaming
table even though Card details was checked.
The layout key now survives validation, so the detailed exporter draws each card with the same renderer as Export card to Excel. Attachment metadata and embedded image galleries are included along with the other selected card fields, while the existing Board → Swimlane → List → Card order remains. The streaming table is still available by deliberately unticking Card details for very large boards.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1b0f2075e">Every PDF and detailed Excel scope carries the complete card fields</a>. Thanks to xet7.</summary>The shared card layout omitted locations, stickers, dependencies and numeric sort position. Locations now include place name, address, latitude and longitude for every current location entry, and retain the legacy single- location fields used by older and Trello-imported cards. The other missing fields have their own selectable sections or metadata row.
Because card, list, swimlane and board exports all draw this one document, the fix applies to both PDF and detailed Excel at every scope. The hierarchy is pinned as Board → Swimlane → List → Card, Swimlane → List → Card, and List → Card, without repeating an ancestor above a smaller export. The Excel and PDF format documentation now lists the complete shared card data.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/57c806705">Detailed PDF and Excel exports follow the board hierarchy</a>. Thanks to xet7.</summary>A board export now reads in the same order as the board: board name, members, creation and modification times, then each swimlane, each list within it, and each card within that list. Even the only visible swimlane is named instead of being silently flattened away.
Smaller exports start at the level selected rather than repeating unrelated ancestors. A swimlane export begins with its swimlane and continues through its lists and cards; a list export begins with that list and its cards; a card export remains the card. PDF and detailed Excel use the same ordering, and the format documentation records it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b27f4fc4">PDF and Excel downloads are named for the object exported</a>. Thanks to xet7.</summary>A swimlane export no longer arrives with the board title in PDF or Excel's
generic default filename. Download names now use the localized object type and
its number: for example, the first English swimlane is Swimlane-1.pdf or
Swimlane-1.xlsx. Lists and cards follow the same convention, while a whole
board pairs the localized board label with its title.
The saved profile language wins; when it is absent, the browser language sent
with the export request supplies the label. The response includes the UTF-8
content-disposition filename, so scripts outside ASCII remain intact, and the
browser no longer overrides it with a title-derived download attribute. The
low-memory Excel table exporter also keeps list and swimlane scope while naming
its result.
Reported as #6586: "with v10.98 we have that unified export dialog 👍 But I can't select/deselect those arrows here", and confirmed as "clicking a checked option, like labels, does not uncheck it". Two faults, and either one alone makes the list useless.
It could not be changed, and the first fix was not enough. The handlers were
a template event map, and a click on a row did nothing; moving them onto
exportScopeSelect — the template that actually draws the rows — did not help
either, and that was built and shipped in both bundles before the answer came
back that the list still could not be changed. What the built bundle shows is
that the templates are registered, their helpers run and both event maps are
attached; the click simply never arrives. This list is drawn inside FIVE popups,
each rendered into its own Blaze view tree, so the toggle is now bound where
nothing in that chain can drop it: one native listener on the
document, in the capture phase — capture so a stopPropagation() between the
row and the document cannot eat it, native so an absent window.jQuery cannot
fail silently, and exactly one so it cannot toggle twice and cancel out.
And no row said whether it was ticked. Each drew an unconditional
i.fa.fa-check on a li.active, which is the OTHER convention in popup.css:
that tick is hidden and shown only for an active row by a selector needing a
nested list carrying checkable, and this list was neither. So the tick was
never hidden and active was never styled — every row looked ticked whatever it
was. Each row now carries .materialCheckBox, the checkbox Admin Panel /
Settings / Announcement uses, which needs no ancestor to be right, and the box
aligns with the first line of a label that wraps.
What is ticked is what the file contains — checked rather than assumed. Every format builds its URL through one function that appends the selection, and every route parses it: the JSON, .zip and Kanboard exports, Excel, PDF, the CSV (where the selection lands on columns), and the ten external-tool exports, which share one handler. A test reads the format table, so a format added later is covered without editing it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/58bf9774a">A card sent to PDF came back as an HTML file</a>. Thanks to Heart1010 and xet7.</summary>Not a broken PDF — WeKan's own page, saved as <card>.pdf. Every export in the
interface is a download from an /api/… address, and the server refuses every
/api request unless WITH_API is exactly true. It refused by answering
301 Location: /, so the browser followed it to the front page and the
download link's download="<card>.pdf" wrote the HTML it got there to that
name. Reproduced against a running instance: GET /api/boards/x/exportPDF
answered 301.
Two faults, either one enough on its own. The bundle launchers did not set
WITH_API — the snap has defaulted it to true for years and every
docker-compose*.yml here sets it, so the bundle was the one platform where
exporting was off by default, and therefore the one platform where an export
came back as HTML. Both launchers now default it to true, overridable, and say
why: the name reads like a developer feature, and somebody switching it off to
harden an instance should know they are turning off every export in the
interface.
And the refusal was a redirect. Whatever the setting, "the API is off" must not arrive as a page. It is now a 403 in plain text, saying that exports use the API too and naming the variable to set — an answer that cannot be mistaken for the file that was asked for.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/74888f1ed">PDF exports carry their JPEG and PNG attachment previews</a>. Thanks to xet7.</summary>PDF listed image attachments by name while Excel embedded their previews. Card and detailed board PDFs now read JPEG and PNG attachments from the configured file store and put real image XObjects into the document. JPEG keeps its original DCT-compressed bytes; PNG scanlines are decoded, their filters removed and their transparency composited onto white before the RGB pixels are deflated into the PDF. Images keep their aspect ratio and are only scaled down.
A missing object, unsupported format or corrupt image is still listed by name and cannot fail the rest of the export. Tests inspect both filters and the page's XObject references, exercise transparent PNG pixels, and pin that failure-safe path. The format design and current progress moved from TODO Later to reciprocal Excel and PDF documentation pages. The shared Excel renderer described there is completed by the multilingual export entry below.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c796fc6c6">PDF and Excel exports preserve every language and share one card layout</a>. Thanks to xet7.</summary>PDF exports now subset and embed the OFL-licensed GNU Unifont BMP and supplementary-plane fonts. Finnish, Greek, Cyrillic, Hebrew, Arabic, Indic and CJK text, plus supplementary characters such as emoji, therefore remain visible, searchable and portable without fonts installed on the reader's device. The former WinAnsi writer remains as a failure-safe fallback.
Printable Excel cards now render every block from the same medium-independent
card document as PDF while retaining their six-column metadata, colored labels,
checklist progress, attachment table and image placement. Excel cells already
store real Unicode text; .xlsx cannot portably embed an OpenType font, so the
spreadsheet application chooses an installed font or fallback rather than
turning editable cells into pictures.
Both formats now resolve locale in one explicit order: a logged-in user's saved language first, the current browser language when none is saved, then English. Public card Excel no longer hard-codes English. Their export links continue to carry the date format displayed by the opened card and the browser's timezone, and the routes validate that format before rendering it.
Tests parse and subset both shipped font files with multilingual text, pin the
locale precedence and opened-card date-format handoff, and exercise the shared
Excel renderer with ordinary metadata and enough colored labels to wrap onto a
second row. The reciprocal Excel
and PDF pages record the implementation
and the .xlsx portability boundary.
The production bundle resolved markdown-it as an ES-module namespace, while
bare Node resolved the same package as its constructor. The shared PDF and
Excel Markdown parser constructed the value directly, so unit tests passed but
the bundled server stopped during module initialization with is not a constructor, restarted, and repeated the same failure.
The parser now normalizes both module shapes before constructing MarkdownIt. A regression test supplies the CommonJS and Meteor production-bundle shapes and requires both to resolve to the same constructor.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9ff542b9b">PDF dates remain complete, and attachment previews share rows</a>. Thanks to xet7.</summary>Three metadata cells fit across the printable PDF width, but long translated labels and full date/time values were shortened with an ellipsis. Each cell now grows onto additional lines, keeping the complete value visible.
Attachment previews formerly occupied one full-width row each and carried a
synthetic [image: filename] line. Up to three previews now share a row, with
the real filename and human-readable file size above each image and no image:
prefix. The row is one pagination unit: when its caption and previews do not
fit, all of them move together to the next page.
Tests pin the complete translated dates, three-column captions, absence of the old prefix, multiple images in one row, image XObjects and atomic page break.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4deff691e">The production bundle uses its embedded Unicode PDF font</a>. Thanks to xet7.</summary>PDFKit initialized its built-in Helvetica before WeKan selected the bundled
Unicode fonts. That reads data/Helvetica.afm relative to PDFKit's own module,
but Meteor's production bundle rewrote the lookup to
/_build/main-prod/data/Helvetica.afm, where no such application asset exists.
Every Unicode export therefore logged the missing file and fell back to the
WinAnsi writer.
PDFKit now starts with the already-loaded GNU Unifont buffer as its default font, so initialization performs no Helvetica AFM filesystem lookup. A regression PDF begins from that buffer, embeds its Unicode map and contains no Helvetica reference; the normal named BMP and supplementary-plane fonts remain available for all subsequent text runs.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7eb0d5ac5">An image attachment is named once in PDF and Excel exports</a>. Thanks to xet7.</summary>An attachment image with a displayed preview was named in the bullet list and again in the caption above its image. Successfully loaded previews are now removed from that list and keep their filename and human-readable size in the caption.
The decision follows the preview that was actually loaded, not only the file's declared type. Non-image files and images whose stored object is missing, unreadable or corrupt therefore remain listed with filename and size instead of disappearing from the export. A regression card contains one previewed image and one ordinary file and pins that each is named in exactly its proper place.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fb52149e9">PDF and Excel cards share their visual attachment layout</a>. Thanks to xet7.</summary>Excel stacked every preview vertically despite having six worksheet columns. It now uses three two-column preview cells per row and starts the fourth image on the next row. PDF uses the same three-column arrangement. In both formats, only the filename appears below each image.
The attachment details above those previews include EVERY file, images
included, in the same six fields: row number, filename, human-readable size,
media type, upload date/time and uploader. PDF now resolves attachment uploaders
alongside card members and comment authors, and the Excel headings use their
real translated attachment keys instead of displaying uploaded-at and
uploaded-by when those generic keys do not exist.
PDF also consumes the presentation data the shared card document already gives Excel: labels use their actual background and contrasting text colors, metadata keeps the same positions, and checklist completion is a six-part blue progress bar with its completed/total count. Tests exercise a real ExcelJS worksheet with four images and pin the corresponding PDF label, progress, detail-table, preview-caption and pagination objects.
This intentionally follows the preceding duplicate-name fix with the complete details requested here: an image is present in the all-attachments details table, while its filename-only preview caption identifies the image below.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ef66347f0">Six attachment previews fit across an Excel row</a>. Thanks to xet7.</summary>The first shared attachment layout put three Excel previews in two-column cells, but the worksheet has six usable columns and the screenshots showed the images still consuming far more vertical space than necessary. Each preview now occupies one column: images one through six share an image row, their filenames sit in cells A through F directly below, and image seven begins the next image row followed by its filename row.
An actual ExcelJS worksheet test places seven PNGs, checks that the first six have the same row coordinate, the seventh has the next image-row coordinate, and verifies the filename cells below both rows. PDF keeps three previews on an A4 row because six would make them too small to read; the shared details, colors, field positions and progress styling remain the same.
</details>Checkboxes - the one square WeKan draws everywhere.
<details> <summary><a href="https://github.com/wekan/wekan/commit/58bf9774a">A checkbox keeps its shape, wherever it is put and whatever is beside it</a>. Thanks to xet7.</summary>In the export popup an unchecked box drew a thin vertical sliver beside a long label and a proper square beside a short one: the row is a flex container, the box is a flex item, and a flex item shrinks.
That is the third time this shape has broken — settingBody.css already
carries a fix for a 41px min-height that "turns a 13px box into a tall
rectangle", and another for a height: 100% that stretched it — so the fix
goes in the rule that DEFINES the checkbox: flex: none, for all 90 of them
across 19 templates, rather than one more local patch. The local patch went with
it: it had set box-sizing: border-box, which would have made that one popup's
boxes 13px including their border while every other checkbox in WeKan is 13px
plus 2px.
The rest was audited rather than assumed: every rule in the client that sizes a checkbox gives it equal width and height, including the two "clean" board themes that deliberately draw theirs at 24px and 18px. A test now pins that for every rule, so the fourth one fails a suite instead of a screenshot.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5723b5692">The custom-field picker uses WeKan's shared animated checkbox</a>. Thanks to xet7.</summary>The checkbox beside each custom-field name was two Font Awesome square icons, switched according to state. It looked like the browser's tiny native checkbox and had none of the behavior used by Admin Panel / Settings / Announcement.
The picker now uses the same .materialCheckBox element and is-checked state
as those settings: a 13px grey square whose shared 0.2-second CSS transition
morphs it into the rotated green tick. No local copy of its dimensions, colors
or animation was added, so future changes to the common checkbox reach this
picker too. Tests pin the shared markup and its transition, rotation and green
checked-state borders, and reject the old icon imitation.
Admin Panel / Problems - how a person is shown.
<details> <summary><a href="https://github.com/wekan/wekan/commit/580b03e17">A user's avatar is avatar-sized, in every pane that names one</a>. Thanks to xet7.</summary>Offices and the Impersonation Report drew a user's photograph at its NATURAL size — a 300px portrait in a table row, a screen tall, with the login counts scattered around it — and it was every pane that names a user, because they all draw one the same way.
Every avatar rule in userAvatar.css is scoped to .member, which is what
makes an avatar 24px and round and crops the image to fill it. The shared table
page's cells had no such box: the image and the initials had been copied, and
the thing they belong in had not. The cells use .member now rather than a
fourth private copy of "how big is an avatar" — there were already three, which
is how the three came to disagree — with the two board-specific declarations it
carries turned off for a table.
All Boards - the size of a tile.
<details> <summary><a href="https://github.com/wekan/wekan/commit/580b03e17">Two tiles that were shorter than the boards beside them</a>. Thanks to xet7.</summary>On Starred, the grey "+ Add Board" tile hung short. min-height: 114px is a
FLOOR, and a board whose title wraps to three lines grows past it; the grid
stretches every other tile in that row to match, but the grey comes from the
label INSIDE the list item rather than from the item itself, so it kept its
114px while its row grew. The item is a flex box now and the label grows into
it.
On Home, "Drag a board here to open it after login" was padding around a line of text, about 85px, on a page whose entire content is that box. It stands where a board tile will be, so it is a board tile's height.
Templates and the workspaces were checked and were already right. Every view — Starred, Remaining, Home, Templates, Archive and each workspace — is the same list with a different set of boards in it, so they share one rule, and every tile variant computes to the same 114px border-box floor. A test pins that there is one list and that no variant sets a height of its own, so that question keeps having one answer instead of six.
</details>and has the following developer-tooling improvement:
<details> <summary><a href="https://github.com/wekan/wekan/commit/0885b9a10">Every PDF and Excel scope maps cards through one shared adapter</a>. Thanks to xet7.</summary>Board, swimlane, list and card hamburger menus already use one export popup, selection and URL builder, and the detailed board exporters reuse their card renderers. One duplicate layer remained: PDF and Excel separately converted database records into the shared card document, including separate people, date, checklist, comment, attachment and file-size mappings.
Both formats now call one pure adapter for every card at every scope. Scope only selects the surrounding Board → Swimlane → List → Card hierarchy; PDF-specific code draws pages and Excel-specific code draws worksheet cells. The Excel and PDF feature documents describe this design and its format-specific boundary.
</details>and reorganizes the following documentation:
<details> <summary><a href="https://github.com/wekan/wekan/commit/c4cb87345">Documentation follows its feature and platform hierarchy, and its links resolve</a>. Thanks to xet7.</summary>Feature documentation that had grown into separate top-level Date, DragDrop,
Email, ImportExport, Login, Theme, Translations and Webhooks trees now lives
under docs/Features; webserver documentation lives under docs/Platforms.
The move also puts related pages and images beside their subject instead of in
the broad Features directory.
All references were recalculated from their source page's old and new location, including links from pages that moved themselves. The documentation link test now walks every Markdown page recursively instead of checking only the flat DeveloperDocs directory, so a future move cannot silently leave links or images pointing at paths that no longer exist.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: one CRITICAL fix, and the whole of logging in reworked
around it. WeKan's brute-force lockout counted an attacker's failed logins
against the victim's account rather than against the address they came from,
so anyone who knew a username could lock its owner out from every address,
repeatably — and a correct password was refused while the lock held, and
counted as another failure. Usernames are public, so any account was a target
and an administrator was as easy to lock out as anyone else. Reported by daniais
as JamBleed. Two GitHub CodeQL alerts on one line of release tooling go with
it. Around that fix: increasing delays after a wrong password, per source
address rather than per account; a record of who logs in from where, both
directions, which is what the new Problems / Offices pane groups into the
offices an admin recognises — "London", with the flag, rather than
100.100.100.100; and the reason it exists, which is that blocking an ADDRESS
would take a whole office off WeKan at once, so WeKan blocks the account.
Admin Panel / Problems now keeps ONE summary row per problem — a count, a
window, and who tried it how often — instead of a document per event that grew
with the attack it was recording, and gains an API pane answering the
opposite question: not what went wrong, but who called which REST endpoint and
how often. Then: v10.97 shipped a bundle that could not
start, the third release in a row stopped by the same habit. Trimming what a
bundle carries is measured by a graph of what the server can reach, and that
graph read require() only. Meteor compiles an ESM import to module.link(),
so every ESM import in every Meteor package was invisible to it: it called
live code dead, and the bundle shipped without nodemailer-openpgp, which
packages/email.js links on its first tick. The reachable count goes from 211
to 450 with the fix — the measurement was wrong rather than merely optimistic —
and the category it justified is withdrawn: 61.3 MiB becomes 40.0 MiB. What
changes beyond that one fault is the check: a release now has to start the
bundle it built and see it reach its database before it may carry it.
That fix had a cost nobody saw for a day: its package loaded on the CLIENT
too, so require('crypto') reached the browser bundle and every page died
on load with Cannot find module 'stream' — fixed here, and the whole class
is now guarded. CHANGELOG.md is 2.5 MB lighter of history, keeping the
current MONTH while older months and years move to old-CHANGELOG/. And
Build WeKan release bundle is a menu entry now, building what a release
would publish rather than
a plain meteor build — so "does it start at all" no longer takes a release to
answer. Below that: the Sandstorm pack that was throwing its own trim away,
Admin Panel / People showing who is locked again, a location in an admin table
opening a map through the card's own chooser, the Problems route and template
finally called what the menu calls them, and the documentation refiled to match
the menu — including Directory-Structure.md, which had been describing the
tree as it was in 2017.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of JamBleed:
Logging in - who the brute-force lockout is protecting, and from whom.
<details> <summary><a href="https://github.com/wekan/wekan/commit/78478c39d">JamBleed: the account lockout counted an attacker's failures against the victim</a>. Thanks to daniais and xet7.</summary>wekan-accounts-lockout kept ONE counter per user —
services.accounts-lockout.failedAttempts — with no notion of where the
attempts came from. Any unauthenticated attacker who knew a username could spend
three wrong passwords and lock that account out from every address,
repeatably, for as long as they cared to keep going. Usernames are public in
normal WeKan use — board and card members are listed — so choosing a target was
trivial, and an administrator was as easy to lock out as anyone else. Reproduced
on v10.91:
attacker, address A : Incorrect / Incorrect / Too many attempts
victim, address B : (correct password) Too many attempts
Affected from v10.59, and not before, for a reason worth keeping in view.
The flat counter is much older, but until the LockoutBleed fix
(GHSA-2g94-9x3m-hv37)
the hooks gated on English error strings that Meteor's ambiguousErrorMessages
had already rewritten, so the counter never moved and no account ever locked.
Making the lockout WORK is what made this reachable — a fix that turns on a
mechanism inherits whatever that mechanism gets wrong.
Two faults, and the second is the one that hurts:
Both are fixed in the decision itself, now a pure module. The counter is per
(user, source address): the address comes from X-Forwarded-For under
HTTP_FORWARDED_COUNT, the same rule server/lib/loginAttemptThrottle.js uses
so a lockout and a throttle cannot disagree about who somebody is, and only the
position hops from the right is read, so a forged header cannot pick its own
bucket. The key is a SHA-256 prefix — an IPv4 address is all dots and cannot be
a Mongo field name, and a locked account should not carry a list of the
addresses that attacked it.
A correct password is allowed FIRST, before anything reads the lock, and clears the state behind it — including the pre-fix flat fields, so an account left locked by the old counter is freed by its owner's next correct login rather than by a wait with no visible end. Hammering during a lock no longer extends it either, or the denial of service returns inside the mechanism meant to stop it.
The three methods that wrote the flat counter are removed rather than left
unreachable. A lockout firing is now recorded and shows in Admin Panel →
Problems — on the lock only, not on every refused attempt during one, or an
attacker could fill that page by holding down a key.
tests/lockoutPerSourceAddress.test.cjs is 19 tests, driving the decision as
arithmetic rather than through a server: the reported attack, the correct
password during a lock, that the lock still fires and still expires, the
forwarded-header rules, that malformed state reads as nothing yet rather than
throwing — a lockout that threw on an unexpected document would lock everybody
out of a database that had one — and that every construction of
AccountsLockout passes the reporter, since there are two and a reload that
dropped it would stop recording attempts while the guard kept working.
and fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/78478c39d">The release version is no longer built into a regular expression</a>. Thanks to xet7.</summary>Two alerts on one line of releases/changelog-open-next.mjs, and both were
right:
js/incomplete-sanitization — version.replace(/\./g, '\\.')
escapes dots and not backslashes, which is the classic half-escape: a version
containing a backslash would have escaped the backslash and left the next
character bare.js/regex-injection — the version is an argv value, so it reached
new RegExp as a pattern.Neither is exploitable with a version release-all.sh computed from the
CHANGELOG, and the script is release tooling rather than anything a user
reaches. But the fix worth making is the one that removes the question instead
of answering it, which is CodeQL's own first recommendation: design so that
sanitization is not needed. The heading is found with startsWith on the exact
text now, so there is no pattern to escape and nothing to inject into, and a
guard fails if a RegExp is built there again.
Shipped in the same commit as the JamBleed fix above.
</details>and adds the following new features:
Logging in - what happens between a wrong password and the next attempt.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9476cdc0f">Increasing delays after a wrong password, per source address</a>. Thanks to xet7.</summary>Checked before any of it was written, because a second mechanism beside an
existing one is worse than no mechanism: the DDP login already has
wekan-accounts-lockout, the REST login has
server/lib/loginAttemptThrottle.js per address, and
server/lib/loginTimingDefense.js equalises timing so a missing user cannot be
told from a wrong password. Nothing anywhere did increasing delays. So this
extends the lockout decision rather than adding a rival to it.
The lockout on its own is a STEP FUNCTION: two failures cost nothing, the third costs sixty seconds. A guesser spends the free attempts of every window and waits, and somebody who mistyped their password gets no sign they are one attempt from being locked out. A delay that GROWS - 1s, 2s, 4s, 8s, capped - costs a guesser far more than it costs a person, and it degrades instead of slamming shut: the account is never unavailable, only slower to try again.
It is per (user, source address), like the counter it sits beside and for the same reason - an attacker must not be able to slow down the account's owner - and a correct password is still allowed immediately, delay or no delay. Somebody who did not have to guess has proved they are not who this is for. An attempt refused as too early is not counted, either: letting it count would let an attacker lock an address out FASTER by trying faster.
</details>Admin Panel / Problems - what the page records, and what it shows.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3d08c65bb">One summary row per problem, with who tried it and how often</a>. Thanks to xet7.</summary>A guard on a path an attacker controls fires as fast as they can send. One document per event meant the database grew WITH the attack, the Problems page became a scroll of near-identical lines, and the one event that mattered was buried under ten thousand that did not. The admin's question is never "list every attempt" - it is what is happening, how much, since when, and who.
So each problem is ONE row that accumulates: a count, the firstAt … at
window it covers, and the actors, each with a count of their own — username1 25, 100.100.100.100 30. The actor list is capped with an overflow count, so an
attacker rotating addresses cannot turn the summary back into the log it
replaced.
Existing per-event rows are folded into their summary on read, so an instance upgrading does not lose what it recorded, and does not keep paying for it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d8d1863db">Who logs in from where, with each account as its initials or avatar</a>. Thanks to xet7.</summary>Checked first, and reused rather than rewritten: the REST throttle and the
lockout each already resolve a client address, models/users.js has
getInitials() and profile.avatarUrl, the board sidebar and cards show
members through +userAvatar / +userAvatarInitials, cardDetails.js builds
"open in map" links for a dozen providers, and the Admin Panel tables already
had an edit-user handler and a userId column. Only reading a location from CDN
headers is new.
One successful login writes a tally in BOTH directions - which addresses this account uses, and which accounts use this address - because the second is what says an address is an office, a VPN or a carrier's NAT rather than one person. Both are capped with an overflow count.
The location comes from a header something in front of WeKan already set (Cloudflare, Fastly, CloudFront, Vercel, Google Cloud, or a hand-configured proxy). WeKan geolocates nothing itself: no database to ship, no lookup of a user's address against a third party. And because anything a client can send it can forge, a location is display only - a name beside an address and a map link, never a decision. Nothing blocks, allows or rate-limits on the strength of one.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/961ff6cb4">Problems / Offices groups those logins into the places they come from</a>. Thanks to xet7.</summary>A pane at /admin/problems/office, through the shared table page every other
report here uses (Table.md) - same layout, same
search, same paginator - so it needed no design of its own.
The columns are what an admin actually asks. Location: the country flag and
the city, "London" rather than 100.100.100.100, so somebody recognises their
own offices at a glance and the flag says WHICH London; empty when nothing
resolved it. Address: the IPv4 or IPv6 it really is. People: each
account as its initials, or its avatar where it has one, with its own login
count beside it - the same way the board sidebar shows members, and clicking one
opens the same edit-user popup as everywhere else. Logins and the window
they fall in.
WHY IT EXISTS AT ALL: an address that many accounts log in from is an office, and anything that reacted to a security event by blocking the ADDRESS would take all of them off WeKan at once - the admin would see "one address blocked" rather than "eighty people locked out". WeKan blocks the ACCOUNT that caused the event. This pane is what lets an admin see the shape of their own users, and what would make an address-level action visibly reckless if one were ever proposed.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6dc2651b4">Problems / API: who called which endpoint, and how often</a>. Thanks to xet7.</summary>Every other pane under Problems answers what went WRONG. This one answers what
is being DONE: POST /api/boards called 34 times by alice last month, 812 times
yesterday by nobody with an account. Without it the only record of REST API use
is whatever the reverse proxy happens to keep, and most instances keep none. The
table is username, endpoint, calls, the window they fall in and the addresses,
sorted by calls - a usage report's question is what is used most, where a
problem report's is what happened last.
The name is the route pattern, not the path. /api/boards/:boardId/lists is
one endpoint; /api/boards/abc123/lists and ten thousand of its siblings are
that one endpoint being used. Naming rows after paths would put a row per board
in the collection - the one-row-per-event cost this page exists to remove - and
a request that matched NO route is counted under a single (no route) name
rather than under the path it invented, because a 404 sweep is an attacker
walking a wordlist and a row per guess would let them fill the database.
One row per account and endpoint, which makes the account part of a row's identity - the one deliberate exception to the rule every other stream follows. There the question is "what is happening" and the caller would multiply the rows; here "who called what" IS the report, and the cardinality is bounded by real accounts times real endpoints. The account is stored by ID, so a rename does not split its history in two.
Nothing per request. Ordinary API traffic is not rare the way a guard firing is rare, so calls are counted in memory and folded on a timer - a thousand requests become one write. Counting hooks the middleware chain rather than the routes, so a route cannot be added without being counted. The pane is the shared event-stream report with a different column list, not a second table page, and the api stream is deliberately not one of the "problem" streams: an instance serving its API would otherwise report thousands of new problems.
Also fixed while there: the summary rows have had ipv4 and ipv6 fields since
the summaries were written, and not one of the four loggers ever filled
them, so the two columns the design asked for could not have
worked. The fold splits
the address now, once, for every stream, and both reports use one shared pair of
columns that falls back to classifying the stored ip - so rows written before
today display correctly instead of showing two empty columns for all of history.
Offices names the places accounts log in from - "London", with the country's flag - and a name is only useful if it leads somewhere. Clicking one now asks which map to open it at, from the same eleven providers a card's location offers.
The same chooser, not a second one. The provider list was eleven options
inside cardDetails.jade; it is one template both callers include now, with its
helper and its styles, because two lists would be eleven places to add a
provider to instead of one and they would disagree the first time only one was
edited. What the two do NOT share is the rest: a card's popup EDITS a location,
and an office's arrived in a CDN header - WeKan did not ask for it and cannot
correct it.
A cell is a link only when the row HAS coordinates: a city name is not a position, and a map URL built from one would either search for the word or invent a place. The link follows the selection before it is saved, because choosing a provider and finding the link still pointing at the old one reads as broken.
The handler lives on the shared table page rather than on the report - which is also where "clicking a user opens the Edit user popup" went, from the three identical copies each report had written for itself.
</details>and fixes the following bugs:
Logging in - and what the lockout was costing everybody else.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5ce9f181c">The lockout package is server-only: it was breaking every page in the browser</a>. Thanks to xet7.</summary>The JamBleed fix above hashes the source address of a login attempt with
require('crypto'), and its package declared
api.mainModule('accounts-lockout.js') with no architecture — which loads
it into the CLIENT as well as the server.
So the browser bundle pulled in crypto-browserify, which pulls in cipher-base,
which does require('stream'), and the page died on load:
Uncaught Error: Cannot find module 'stream'
before WeKan drew anything at all. The server started perfectly and answered HTTP 200 with a page that could not run.
It is server-only now, which it always should have been: nothing in client/
imports it, and shipping a brute-force lockout's decision to the browser would
hand an attacker the rules even if it cost nothing. A rebuilt client bundle is
758 KB smaller and contains neither crypto-browserify nor cipher-base.
Why nothing caught it. bundle-smoke-boot.sh starts the bundle and waits
for it to reach its database, which proves the SERVER image loads — every
package linked, every map read. This was the client, and no check looked there.
Both crashes that check was written for were server-side, so it answered the
question it was asked and the question next door went unasked.
tests/packagesLoadOnTheRightArch.test.cjs pins the class rather than the one
package: it reads each package.js for the entry files NOT restricted to the
server, follows their imports, and fails when any requires a Node builtin — and
does the same for every file under client/. Verified to fail on the real
fault. Meteor's default being both architectures is what makes this silent:
the code works, the tests pass, and the cost lands in a browser bundle nobody
reads.
Bundles and images - what a build carries, and what it can start without.
<details> <summary><a href="https://github.com/wekan/wekan/commit/71ff74c6d">The reachability graph must read Meteor's module.link, not only require()</a>. Thanks to Heart1010 and xet7.</summary>Reported as #6602: a Docker instance upgraded from v10.91 to v10.97 would not come up at all. The entry above shipped in a bundle that crash-looped:
Error: Cannot find module ".../nodemailer-openpgp/lib/nodemailer-openpgp.js"
at packages/email.js:347
packages/email.js does not require() that package. Meteor compiles an ESM
import to its own linker call:
module.link('nodemailer-openpgp',{openpgpEncrypt(v){openpgpEncrypt=v}},6);
and the scanner only ever looked for require(). So it missed every ESM import
in every Meteor package — which is most of them — and reported live code as
dead. The reachable count goes from 211 to 450 with the fix: the measurement was
badly wrong, not marginally. Four forms count now: require(), module.link(),
module.watch(require()) and module.dynamicImport().
The safety worked, which is the one good part. With the corrected graph the
policy still named openpgp, the graph VETOED it, and the tool refused and said
so rather than deleting it. That entry is removed outright now rather than left
to be vetoed every run — the reason is what grants the permission, and this
reason was never true.
And the check that was missing both times. v10.96 died on a source map
deleted but not un-named; this died on a linked module. Both were reasoned about
by reading the code, and reading the code is how both mistakes were made. So
releases/bundle-smoke-boot.sh starts the bundle with a database address that
cannot answer and requires it to get as far as trying to reach it — which proves
the whole server image loaded, because the database is the first thing WeKan
needs that the check does not provide. The amd64 build runs it after the trim
and the prunes, and every other architecture's bundle derives from that one.
It is verified against both real failures — a bundle with nodemailer-openpgp
removed, and a manifest naming maps that are not there — and each fails with its
own diagnosis, because the fix for each is a different one. A bundle that exits
quietly or hangs is not a pass either: a smoke test whose failure mode is
passing when it learned nothing is worth less than none.
v10.98's log shows the trim doing exactly what it was meant to, and then being thrown away:
--- .meteor-spk/bundle: 856M total (first pack, fails)
bundle-trim: removed 5236 files, 355 MiB
prune-unreachable-npm: removed 28 package(s), 49.9 MiB
--- .meteor-spk/bundle: 424M total (424M + 336M deps = 760M, under 1 GiB)
Building Meteor app... <- pack rebuilds it
App exceeds uncompressed size limit of 1 GiB
meteor-spk pack runs the Meteor build first, so the retry regenerated the
bundle and packed the untrimmed one - 405 MiB of trimming discarded between the
measurement and the pack. Sandstorm's own spk pack only packs what is there,
so the retry prefers it and falls back to meteor-spk when it is not installed.
A comment in the retry claimed pack REUSED the bundle. It did not, and the log above is what disproved it.
</details>The CHANGELOG and its tooling - a file that grew faster than it was read.
<details> <summary><a href="https://github.com/wekan/wekan/commit/43341fc75">Releases are frequent, so the CHANGELOG tooling stops assuming they are rare</a>. Thanks to xet7.</summary>The maintenance loop here is build.sh option 1 (git pull + git push) and
releases/release-all.sh with no arguments, several times a day when a fault is
being chased. A release landing in the middle of a piece of work is the normal
case, not a special one — and release-all.sh renamed
# Upcoming WeKan ® release to # v<NEW> … and left nothing behind, so the
next entry written had nowhere correct to go. It landed above the closing
Thanks to above GitHub users … line, which is now INSIDE the release just
published.
v10.96 and v10.97 both ended up that way. The second was worse than misplaced: an entry already published was EDITED afterwards, so the notes described a smaller, tidier change than the one that shipped — and the one that shipped was the one that stopped the bundle starting.
releases/changelog-open-next.mjs opens the next section as soon as a release
is named, carrying the **In short:** placeholder and the binaries table the
format guard requires, so the file is valid the moment release-all.sh
finishes. Re-running it is a no-op, since release-all.sh can be run again
after a failure. The format guard now allows an Upcoming that is empty AND still
carries the placeholder — and fails one that has entries and still carries it,
because that is a summary nobody replaced.
tests/changelogEntriesBelongToTheirRelease.test.cjs is the check that the home
was used: git knows which commits a release contains, so an entry linking a
commit that is not an ancestor of its release is in the wrong section. Scoped to
the newest three releases on purpose — over the whole file it flags 83 entries
back to v2.99, from old release practices and history rewrites, and a guard
reporting 83 things nobody will act on is a guard people learn to skip.
CLAUDE.md says all of this where the release instructions are, including the
rule the second mistake broke: a released section is a record, not a draft.
Reported as #6580: the file had reached 2.6 MB and 51,365 lines across 1,070 releases going back to 2015, which is slow to open and slower to read on the web.
The current year stays in CHANGELOG.md; every older year moves out whole to
old-CHANGELOG/<year>.md, with a bullet in # Platforms linking each one.
798 releases move, 272 stay, and the file goes from 2.6 MB to 1.9 MB —
2026 is a big year on its own, because releases here are frequent.
Nothing is deleted and no entry is rewritten. An archived section reads exactly
as it did before, for the same reason a released section is never edited in
place: it is a record. That git blame is less useful on the split file is
accepted rather than worked around — the history is still in git for anyone who
wants it, through gitk, git-gui or git log --follow, and being small
enough to open is worth more.
Each archive opens with a count of releases per month: a
| 2025 | Releases | table over rows of 01, 02 and so on. How busy was
2019 is the first thing a year file is asked and the last thing 159 collapsed
sections answer.
Only months that had releases get a row: a fixed twelve rows would put ten
zeroes in 2015's table. The table is regenerated from each file's own headings
on every run, so it cannot drift from the sections beneath it, and
tests/changelogArchive.test.cjs checks the two against each other.
releases/changelog-archive-years.mjs does it, and it is a January job: run
it once when a year turns over. It is idempotent, so a stray run does nothing.
Cutting by YEAR rather than by a count of releases is what makes a link into the
archive stable — once a year is over, nothing in it moves again.
One thing the script had to learn: eleven years of headings do not agree on their own wording. Of 1,070, 539 say Wekan release, 524 say WeKan ® release, and the rest are one-offs — Sandstorm-only Wekan release, Wekan relase, and one that explains it was NOT released. Matching the strict modern form found only half of them and silently absorbed the others into the section above, so the version and the DATE are what is matched and whatever follows is left alone. Verified by counting headings before and after: 1,070 both times, none lost and none duplicated.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cc3201292">And then the current MONTH, because a year of these releases is still 1.9 MB</a>. Thanks to mimZD and xet7.</summary>Moving whole years out left 1.9 MB, which was still too large, because releases here are FREQUENT: 2026 alone is 272 releases over eight months, and July was 80 on its own. A year is not a small enough unit when a year is that busy.
So the cut is by month. CHANGELOG.md holds the current month plus the
Platforms, TODO Later and Upcoming sections; earlier months of the current year
go to old-CHANGELOG/<year>/<MM>.md; years that are over stay one file each,
because at 30 to 107 KB they are already small and splitting them further would
trade a size problem for a "which of thirty files is it in" problem.
2.5 MB becomes 822 KB. Each archive opens with a table of how many releases it holds, per month, so the file says what is in it before a reader scrolls.
</details>Admin Panel / People - who is locked out, and who can undo it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6e208c615">People shows who is locked again, and why</a>. Thanks to xet7.</summary>Moving the lockout counter to one per (user, source address) - the JamBleed fix
above - broke three readers still looking at the flat field it replaced: the
People table's lock icon, its unlock click handler, and the lockedUsers
methods behind them. Every account would have shown as unlocked, and no admin
could have unlocked one.
That is the "and nowhere else" half of the rule this release adds to CLAUDE.md,
missed on the very next fix. models/lib/accountLockout.js is now the one place
that knows the shape - locked or not, since when, how many addresses and how
many failures between them - so the client helper, the click handler and the
server methods cannot drift apart again, and the negative test fails if anything
reads the flat field.
The People row says which ADDRESSES are locked and until when, rather than a bare padlock, because "locked" now means something narrower than it used to and an admin should not have to guess how much narrower.
</details>and has the following developer-facing changes:
Admin Panel / Problems - how a pane knows it is the open one.
<details> <summary><a href="https://github.com/wekan/wekan/commit/760bd0904">One active pane, instead of eleven booleans saying the same thing</a>. Thanks to xet7.</summary>Every pane on the Problems page had a ReactiveVar of its own -
showSummary, showSecurity, showIntegrity and eight more - on top of
activeReport, which already held the id of the open pane. Each one could only
ever mean "activeReport equals my id", so they were forty-four lines restating
one value, kept in step by hand: opening a pane reset all eleven and set one.
That made a pane four wiring points instead of one, and missing any of them
failed SILENTLY. Filesystem integrity got three of the four - menu entry, setter
and template branch - and no helper, and in Blaze an undefined helper is not an
error but a falsy value, so the pane drew a blank
page while Summary went on
counting the problems it could not show. The Offices pane added in this release
had the same hole somewhere else: it set no loading.set(false), so opening it
would have spun for ever.
The template asks else if isPane 'report-integrity' now, against one helper.
Panes that fetch through a method rather than a subscription are a list, and
that list is also what fixes Offices. Adding a pane is a menu entry, a branch,
and one line saying how it loads.
The guards moved with it. tests/adminPaneHelpers.test.cjs, written when the
integrity pane was blank, pins both halves of the new mechanism: the helper must
exist, and every id the template branches on must be an id the menu sets - a
typo either way is dead template or a blank pane, and neither says anything at
runtime. tests/problemsMenuOrder.test.cjs now checks EVERY pane rather than
three named ones: each menu entry must be rendered, and must either load itself
or have a report config, so a pane that spins for ever cannot ship again. A
negative test fails if a per-pane ReactiveVar comes back.
The pane is called Problems in the menu, its address is /admin/problems, and
docs/Features/Admin-Panel/Problems is where it is documented. Two things still
called it Reports, from before it was renamed: the route NAME was
admin-reports, where its three siblings are the bare page key setting,
people and attachments - so the one route whose name did not match its own
address - and the template and its three files were adminReports. Both are
problems / adminProblems now, across 41 files.
What did NOT change is legacyBase: '/admin-reports'. That is an address people
have in bookmarks, and it still redirects.
Security fixes - what one is required to come with.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c3299c490">A test, a negative test, and the attempt visible in Problems</a>. Thanks to xet7.</summary>Two rules added to CLAUDE.md, both learned from fixes in this repository rather than proposed in the abstract.
A test AND a negative test, written so the fault cannot exist ANYWHERE in the codebase rather than only where it was reported. A test that pins one call site leaves the same mistake free to live in the other five, and that is how most of these arrive: SignupBleed's guard read an option nothing sets and the same shape sat in a second endpoint; the source-map trim was safe on the client and fatal on the server. So the test proves the fix does what it claims - driving the decision itself, as arithmetic, where it can - and the negative test proves the fault is GONE rather than moved, by searching the tree for the shape of it.
And the attempt is visible in Admin Panel / Problems, whenever the fix is one that DENIES an operation. A vulnerability that is fixed silently tells an admin nothing about being attacked through it, and "nobody is trying" and "somebody tries every four seconds" are not the same instance to run. Recorded as a summary, never per event.
</details>The local build - what build.sh produces, and what a release produces.
"Build WeKan" ran meteor build .build --directory and stopped there, and that
bundle is not the one a release publishes. A release adds the server's npm
modules, three prunes, the sockjs / legacy-client / source-map trim, a verified
Node.js, FerretDB, the eight MongoDB Database Tools and a launcher — and three
releases running broke in exactly that difference: v10.96 on a source map the
trim deleted and left named, v10.97 on a package the prune's graph could not
see, v10.98 on a Sandstorm pack that rebuilt the bundle and threw the trim away.
None of it was reproducible locally, because locally there was only meteor build, so "does the bundle a release would publish start at all" took a
release, a workflow run and a download.
releases/build-release-bundle.sh runs the workflow's OWN steps, in its order,
with its arguments, for whichever platform it is run on. Not a second
implementation of the release — the same scripts, so a fix to one is a fix to
both. It makes no zip, no checksum and no provenance row, because those describe
a published artifact and this one is a directory to start:
cd .build/bundle && ./start-wekan.sh
Verified by running it on Linux arm64: an 850M bundle, trim −349 MiB, prune
−40.0 MiB, the boot check passed, Node.js v24.19.0 verified against nodejs.org's
SHASUMS256.txt, FerretDB and the eight tools fetched per-arch, 686M with all
of it embedded — then started, with FerretDB on SQLite and WeKan answering
HTTP 200 fifteen seconds later.
The Setup menu offers it as Build WeKan release bundle, beside Build WeKan
development bundle — the plain meteor build that entry used to be, kept
because the two answer different questions at very different cost, and the
development one now says what it is NOT when it finishes.
Downloads are cached under .tools/bundle-binaries/, and a cache HIT re-checks
the published SHA256 rather than trusting a file for having been there before.
The test path is deliberately unchanged: it runs the bundle under its own node
and mongod, so a hundred megabytes of binaries it will not use is the wrong
trade. tests/releaseBundleMatchesWorkflow.test.cjs FINDS the release scripts
the workflow runs rather than listing them, so a step added there and not here
fails the suite instead of quietly putting the difference back.
and improves the documentation:
docs/ - where a page lives, and how a reader finds it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/daaf56391">Implemented designs move to docs/Features, filed the way the menu is</a>. Thanks to xet7.</summary>A design that describes what WeKan already DOES belongs with the feature it
describes, not in the folder for things being proposed. Fifteen docs move -
Accessibility, Original-Positions, the thirteen Page/* designs that exist,
and the Admin Panel Problems design.
What stays in docs/Design is what the folder is for: the principles
(Design-Principles, Monkey-Proof-Software), the comparisons, the roadmap, and
the proposals not yet built.
It described the tree at commit e2f768c and had gone on describing it for
years. Fourteen links pointed at nothing - five files that had moved, seven
that no longer exist anywhere - and every one of the fourteen was written as a
full https://github.com/wekan/wekan/tree/main/… URL rather than a relative
path, which is exactly why none was noticed: a broken relative link is visibly
broken in an editor and on GitHub, while an absolute one looks like a link and
404s only for the reader who follows it. All 144 relative links were fine.
And two thirds of the repository was not mentioned at all — imports/,
packages/, releases/, tests/, docs/, migrations/, server/lib,
server/methods, server/routes, models/lib, client/features. The page
walked through four directories out of twenty and did not say so, which left a
reader unable to tell "not here" from "does not exist". It opens with a table of
the whole tree now, and gains the sections those directories should have had.
tests/docsLinksResolve.test.cjs checks both link forms against the tree, and
fails when the page stops mentioning a top-level source directory - the silence
being the failure that lasted longest.
The menu is the structure a reader already has in their head, so the docs match
it: one directory per tab, one page per pane, and a README in each listing the
panes IN MENU ORDER with the URL slug beside them - so somebody with
/admin/problems/integrity in the address bar can search for integrity and
find the page.
Settings (7 panes) and People (9) already had a page each and were already in order; they gain the slug column. Problems (17) and Attachments (10) listed almost none of theirs, so their READMEs now index every pane.
WHAT IS NOT DONE is now visible instead of invisible: 15 of the 17 Problems
panes and all 10 Attachments panes have no page of their own. They are dashes in
the table, and tests/adminPanelDocsMatchMenu.test.cjs counts the dashes
against the sentence that states how many there are - so the gap cannot grow
quietly, and a pane added to the menu and not to the docs fails the suite.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: v10.96 shipped a bundle that could not start. Trimming what a
bundle carries went one file too far: boot.js reads every source map NAMED in
programs/server/program.json, unconditionally, so removing the maps left 63
dangling names and the server died before opening its port. The names go with
the files now, and the fix was checked by BOOTING a trimmed bundle rather than
by reading the code again. Then the snap, which had been taking itself
offline at every restart: the startup comparison of the two database copies ran
unbounded with nothing on the web port, on an ambiguity that its own reading of
MongoDB kept recreating. Below that: 61 MiB off every bundle
from packages nothing can reach, and a guard that keeps all 246 translations
loading one at a time.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following bugs:
Bundles and images - what a build carries, and what it no longer does.
<details> <summary><a href="https://github.com/wekan/wekan/commit/dc19f0661">Dropping a source map must un-name it too, or the server will not boot</a>. Thanks to xet7.</summary>The entry above removed the source maps from every platform. A released image then crash-looped:
Error: ENOENT: no such file or directory,
open '/build/programs/server/packages/ecmascript.js.map'
at /build/programs/server/boot.js:101:29
"Nothing on any loading path reads a .map" was true of the client and false
of the server. boot.js reads every map NAMED in
programs/server/program.json, at boot, unconditionally:
serverJson.load.forEach(function (fileInfo) {
if (fileInfo.sourceMap) {
var rawSourceMap = fs.readFileSync(
path.resolve(serverDir, fileInfo.sourceMap), 'utf8');
63 of the 102 load entries name one — 60 MiB — so deleting the files left 63 dangling names and the server died before it opened its port.
The names now go with the files: the same pass deletes sourceMap and
sourceMapRoot from every load entry. The client was never affected and still
is not — its program.json names no maps at all (678 manifest entries, zero
sourceMap fields) and webapp reads only program.json itself at startup.
Verified by BOOTING a trimmed bundle rather than by reading the code again: with
uWebSockets.js, the legacy client and all 4766 maps removed, node main.js
loads the whole server and reaches AccountsServer.init, failing only on the
deliberately unreachable MONGO_URL it was given. That is the check that was
missing the first time, and tests/bundleTrim.test.cjs now pins the invariant
boot.js actually requires — every map the manifest names exists on disk — for
both settings of --keep-maps.
The snap's two copies of the database - after a migration both stay on disk, and starting is where that gets decided.
<details> <summary><a href="https://github.com/wekan/wekan/commit/da221c549">Reading MongoDB to compare the copies must not look like writing to it</a>. Thanks to xet7.</summary>An instance serving FerretDB printed this at every restart:
WeKan: BOTH databases have been written to since the migration.
MongoDB last written 2026-08-16 01:41 (WiredTiger.wt)
FerretDB last written 2026-08-16 01:38
Nothing had opened that MongoDB in a month. The giveaway is in the report itself: the MongoDB timestamp is the minute the snap started, three minutes AFTER the FerretDB it is being compared against.
bin/ferretdb-migration-stale decides MongoDB has been written to from the
mtimes of the WiredTiger data files, and its own header admits an mtime cannot
tell "somebody used this database" from "this database was started".
bin/database-autopick is the answer to that — it reads both copies and
compares their contents. But to read MongoDB it STARTS mongod, and starting
mongod does recovery and a checkpoint, which stamps exactly the files the
staleness check reads. So the diagnostic manufactured its own evidence: after
one run, MongoDB looked freshly written forever and the ambiguity could never
resolve, however long nobody touched it.
The read now notes the newest data-file mtime before mongod starts and puts anything newer back afterwards — files mongod CREATED during the read included, since a new journal file is the newest thing in the directory and reports as a write on its own. mongod does not use mtimes, and what it checkpointed is not user data, so the metadata is made to say what is true. Every way out of the read restores, including the failures, because a failing read is exactly when the ambiguity gets reported. A write that happened BEFORE the read still survives it: the point is to hide WeKan's own read, never somebody's work.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/062d4fede">The database comparison at startup cannot take the site down any more</a>. Thanks to xet7.</summary>The same instance went OFFLINE at every restart. wekan-control ran the
comparison synchronously and unbounded before anything opened the web port — and
it starts each database on a temporary port to read it, which on a real instance
is minutes. Until it returned a browser got a connection timeout, and the reason
was in snap logs, the last place somebody whose site is down thinks to look.
That is #6592 one step earlier than where it was fixed. The database WAIT already serves a waiting for its database page; the helpers for it were defined 200 lines BELOW the comparison that needed them. They move up, and the comparison uses them — after the same grace period, so a comparison that finishes in seconds does not flash a page up and teach proxies to cache a 503 for a healthy site.
And a bound. WEKAN_AUTOPICK_TIMEOUT is 900 seconds by default; when it runs
out WeKan says so, changes nothing, and starts on the database already selected.
Stopping the comparison is safe — it never deletes anything on either side, and
the merge it may be in the middle of only INSERTS what is missing, so a partial
one is fewer documents copied and the next start finishes the job. Serving the
site beats finishing the comparison.
WEKAN_AUTOPICK was an env var and nothing else, so an admin whose site was
down had no supported way to skip the comparison. It is a snap config key now,
with the bound beside it, and the timeout message names both — somebody reading
it is somebody whose site just came up late:
snap set wekan autopick=false
snap set wekan autopick-timeout=1800
This is the other half of the entry above. Before that fix the comparison ran at EVERY start, so every restart took the site down for a comparison that could never conclude. One stops it recurring; this one makes it survivable.
</details>and changes what every platform ships:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d6e21202f">Prune what no require() in the server bundle can reach</a>. Thanks to xet7.</summary>programs/server/npm/node_modules is 347 MB of an ~850 MB bundle, and measuring
its require graph says 206 MB of that is unreachable from any server entry
point. Two things cause it, neither a bug in Meteor:
programs/server/app/app.js
requires only 76 bare specifiers because rspack inlined the app's real
dependencies into it. But this tree is what Atmosphere packages declare with
Npm.depends() and load through Npm.require(), which rspack cannot follow.
It is largely the INPUT to a build whose OUTPUT ships beside it.npm install, devDependencies included.Duplication is the smaller half: 586 distinct packages exist as 815 copies, but
the redundant copies are only ~28 MB, because Meteor keeps per-package
node_modules on purpose so packages can pin conflicting versions.
releases/prune-unreachable-npm.mjs removes 61.3 MiB, in four categories whose
reason is PROVABLE rather than merely plausible — the standard the
uWebSockets.js removal met: typescript (23.2 MiB, a devDependency of 196
packages here and a runtime dependency of none), openpgp with
nodemailer-openpgp (21.3 MiB, reachable only through an optional nodemailer
plugin nothing requires), @types/* (9.6 MiB across 24 copies, verified to
contain no .js at all) and sinon (7.2 MiB, a test framework).
One of those four was wrong, and this release cannot start because of it.
packages/email.js links nodemailer-openpgp on its first tick through
module.link(), which the graph did not read as a reference — so the bundle
ships without a package it needs and dies with Cannot find module. Withdrawn
and fixed in the release above, where the whole story is.
The remaining 145 MB of unreachable packages STAYS. jquery, hotkeys-js and
the @azure storage adapters are almost certainly dead too, but almost
certainly is not the standard, and the tail of 590 packages is where a static
scan is most likely to be wrong.
Two independent safeties, because this is riskier than the uws removal was. The
reachable set is recomputed from the bundle every run and deliberately
OVER-approximated — once a package is reached, every file in it is scanned and
every require() string literal counts. The policy then only proposes; the
graph has a VETO, so an entry whose package is actually required is refused and
said out loud rather than applied. And after deleting, every path in the
reachable set must still exist or the run fails. Verified on a real bundle:
473M to 410M, with the reachable count 211 before and after.
and adds the following guard:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1619a6c37">Every language stays lazily loaded, and a guard keeps it that way</a>. Thanks to xet7.</summary>WeKan ships 246 languages, 37 MB of JSON. What makes that affordable is one
character in each of 246 near-identical blocks in imports/i18n/languages.js:
load: () => import('./data/ace.i18n.json'),
A dynamic import() is a SPLIT POINT. rspack emits each language as its own
chunk — measured on a built bundle, 238 chunks and 34 MiB of JS — and a browser
fetches exactly one, about 145 KB for the language in use. Written instead as
import data from './data/ace.i18n.json', or with require(), the same line
stops splitting and 34 MiB joins the main bundle for every user, in every
language. That edit is made by hand each time a language is added, which is why
it wants a guard rather than a convention.
Seven checks: every entry has a load:, every load: is the dynamic form, none
uses require(), no static import of a data file, nothing outside
languages.js imports one, every file is claimed and every claim has a file,
and a Transifex pull writes the file the app actually loads.
The last two are symlink-aware, and that is the point of them. .tx/config's
lang_map renames most of Transifex's underscored locales to WeKan's hyphenated
files; for the two it does not — km_KH and ru_RU — the hyphenated name is a
SYMLINK to the file Transifex writes. Two names for one file, not two copies.
Reading it the other way costs a language its real translations, so both checks
compare through realpath and say so.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: one CRITICAL fix. With registration turned OFF in the Admin
Panel, POST /users/register created accounts anyway — for anybody who asked,
on an instance whose administrator had decided nobody else may join. The guard
that was supposed to stop it read a Meteor option WeKan never sets, so it was
always false and the endpoint had never refused anyone. It was found while
reviewing a pull request about the opposite symptom. Then the release
workflow: pressing Cancel did not stop a run, so docker went on building
an image for a release being abandoned, and the Sandstorm .spk gets under
its 1 GiB limit at last, now that its size report says what filled the gigabyte.
What that measurement found ended up changing every platform, not just
Sandstorm: uws is not reliable enough yet, so every default is now
sockjs, and no bundle carries uWebSockets.js (121M), the legacy client
(81M) or source maps (152M) — around 354M a bundle. Below that: two AWS SDK
updates for the S3 attachment path.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following CRITICAL SECURITY ISSUE of SignupBleed:
Account creation - who may make an account, and who decides.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a3f1b626292cc8cb16d3cfd71ba30009cfc5f935">SignupBleed: registration disabled, and the REST API created accounts anyway</a>. Thanks to AhmedLukman and xet7.</summary>Turning Registration off in the Admin Panel closes the sign-up form. It did
not close POST /users/register, which went on creating accounts and handing
back login tokens to anyone who asked. A closed instance was open.
The endpoint did have a guard. It asked
Accounts._options.forbidClientAccountCreation — and nothing in WeKan ever
sets that:
Accounts.config() call, in server/accounts-common.js, sets
loginExpirationInDays and nothing else;forbidClientAccountCreation: disableRegistration in config/accounts.js is
passed to AccountsTemplates.configure(), which is the useraccounts package's
own options object, not Meteor's Accounts;disableRegistration is only assigned inside an async
Meteor.call('isDisableRegistration', …) callback that fires AFTER
configure() has already run — something the file's own comment records, for
a different reason, a few lines above.Three near-misses, and the condition was always falsy. The guard read as though it worked, which is why it lasted: the name says exactly what it ought to do.
It reads the setting from where the setting lives now - the same
getCurrentSetting().disableRegistration === true that the
isDisableRegistration Meteor method behind the sign-up form uses, so the form
and the API can no longer disagree about whether the door is open. An instance
with no Settings document yet still allows registration rather than locking
itself out.
Every call that reaches the refusal is recorded and shows in Admin Panel /
Problems under authz.register. That is safe to log without drowning the page
precisely because there is no legitimate caller: the administrator has turned
registration off.
How it was found. #6598 by AhmedLukman changed this guard to read WeKan's setting, filed against #4774 — a 403 Forbidden from this endpoint that nobody had been able to reproduce, and which this line cannot produce, since it never returned 403 at all. The pull request was closed and the fix written here with a security log entry and the tests the route had never had; the finding is the reporter's.
Nine tests, four of them negative, on an endpoint that had none: that the setting is read where the Meteor method reads it, that the dead option is not consulted again AND is still set nowhere — so a later change cannot quietly reintroduce two sources of truth that disagree — that an enabled instance still creates the user and answers with its token, and that a missing Settings document does not refuse everybody.
</details>and updates the following dependencies:
Thanks to dependabot.
and fixes the following build failures:
The release workflow - what a release builds, and who can stop it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0f374a0d4">Cancelling a release run stops it, and a stuck job no longer needs a human</a>. Thanks to xet7.</summary>Pressing Cancel on a release-all run did not stop the docker job. It kept
going for another twenty minutes and would have pushed a multi-arch image for a
release that was being abandoned.
The cause is always(). It is true while a run is CANCELLING, which is exactly
why it was chosen in v10.80: a build-mac-x64 queued on a runner label GitHub
had retired sat for two hours, was cancelled by hand, and that cancellation
SKIPPED charts, ucs and nextcloud — so the Helm chart for an already
published WeKan was never pushed, and no error anywhere said why. always()
fixed that by ignoring cancellation entirely, and ignoring cancellation is also
what disabled the Cancel button on a job that takes hours.
!cancelled() keeps the half that matters — do not skip me because a SIBLING
failed — and drops the half that overrides a person. Nine job-level conditions
change, across release-all.yml, release-all-missing.yml, Flatpak.yml and
AppImage.yml. The 22 step-level if: always() reporting steps do not: those
correctly print CANCELLED.
That alone would reopen v10.80, so the other half of the fix is that no job
needs a hand cancellation any more. The eight jobs that had no timeout-minutes
now have one, and every job in the release workflow is bounded. A stuck job
FAILS on its own, and a failure satisfies !cancelled() the same way always()
let it through — Cancel is left meaning only what a person meant by it.
v10.93, v10.94 and v10.95 all failed to pack with App exceeds uncompressed size
limit of 1 GiB. v10.95 was the first run whose size report worked, and it named
the gigabyte: .meteor-spk/bundle 852M plus .meteor-spk/deps 336M, so 1188M
against a 1024M ceiling. It also answered the question that report was written
to ask — a second meteor-spk pack REUSES the bundle rather than rebuilding it,
since the pruned tree stayed pruned across the retry (852M to 833M). So trimming
between the two attempts works. 19M was simply never going to close a 165M gap.
Measuring the bundle rather than guessing at it turned up two passengers that are large, unreachable at runtime, and safe to drop:
require('./uws_' + process.platform + '_' + process.arch + '_' + process.versions.modules + '.node'). A machine is one platform running one
Node, so the other sixteen files can never be opened by it. Keeping every ABI
of the target platform and CPU, so a Node major bump still finds its binary,
frees ~93M. The entry below drops the remaining 28M as well..map degrades a stack trace at worst.releases/bundle-trim.mjs does both, measured at 281 MiB on a real bundle, and
the Sandstorm leg runs it beside the existing prune before the retry pack.
An architecture with NO uWebSockets.js prebuild at all — ppc64le, s390x,
riscv64, where ddp-server falls back to sockjs — is left completely alone, since
deleting the other platforms' files there would free nothing that matters and
could only break the fallback. tests/bundleTrim.test.cjs pins that, the kept
ABIs, that a directory merely ending in .map is not a source map, and that the
trim runs before the retry pack rather than after it.
Trimming uWebSockets.js down to the prebuilds a grain's own platform can open freed 93M of its 121M. All of it can go, because a grain loads NONE of it.
The uws transport is OPTIONAL in Meteor 3. ddp-server's transports/index.js
resolves the transport from Meteor.settings.packages['ddp-server'].transport,
then DDP_TRANSPORT, then DISABLE_SOCKJS, and DEFAULTS to sockjs — and
Npm.require('uWebSockets.js') sits inside the uws transport's setup(), which
runs only for the transport that was actually chosen. sandstorm-pkgdef.capnp
sets none of those, and its environ is, by its own comment, the app's ENTIRE
environment. So a grain has been running sockjs all along while carrying 121M of
a module it never required.
The pkgdef now PINS DDP_TRANSPORT=sockjs, because the removal should rest on a
stated fact rather than on an upstream default staying put, and
bundle-trim.mjs takes --transport sockjs, which removes the module whole
instead of thinning its prebuilds. 120 MiB measured, 27 MiB more than the
platform trim: 1188M becomes ~860M, with over 160M of headroom against the
1 GiB limit.
It is a flag rather than the default because WeKan asks for uws nearly
everywhere else — docker-compose, start-wekan.sh, build.sh — and those
bundles keep the module, trimmed to the prebuilds their own platform can open.
tests/bundleTrim.test.cjs ties the grain's pinned transport to the flag the
.spk is trimmed with. If those two ever disagreed, the grain would require a
module that was left out and fail to boot, which is the one way this can go
wrong.
and changes what every platform ships:
Bundles and images - what a build carries, and what it no longer does.
<details> <summary><a href="https://github.com/wekan/wekan/commit/33c867f4d">Every platform talks sockjs, and no bundle carries uws or the legacy client</a>. Thanks to xet7.</summary>uws is not reliable enough yet to be what a default points at, so nothing WeKan ships selects it any more, and nothing carries the module. Three halves, and each is useless or harmful without the others.
The defaults. Every docker-compose*.yml, start-wekan.sh,
start-wekan.bat, build.sh, build.bat, the image's own ENV block, the
snap config and the Sandstorm grain now say sockjs. The commented-out
#- DDP_TRANSPORT=uws alternatives beside them are replaced by a note saying
why there is no alternative, rather than advertising a value that no longer
works.
The bundles. Every place a built bundle is post-processed — the amd64
build, each per-arch rebuild, the repack container,
releases/install-node-for-arch.sh and the Dockerfile — now runs
bundle-trim.mjs beside the prune it already ran, dropping two things:
uWebSockets.js, 121 MiB, for the reason in the entry above: a sockjs server never requires the module at all.
programs/web.browser.legacy, a whole second copy of the client built
for browsers without modern JS. 81 MiB measured. Meteor supports running with
architectures excluded and says so in webapp's categorizeRequest(): "If our
preferred arch is not available, it's better to use another client arch that
is available than to guarantee the site won't work". An old browser is served
web.browser; the 404 branch below that is reached only when NO arch matches,
which cannot happen while web.browser is there, and autoupdate iterates the
programs that actually loaded rather than a fixed list.
The files are only half of it. The arch is NAMED in
programs/server/config.json and star.json, and boot.js builds a
dynamic-import root for every name in the first — so the name is removed with
the files. Those manifests are mode 444 as Meteor writes them, so they are
made writable, rewritten, and set back: failing there would leave the one
state that actually breaks a server, files gone and manifests still naming the
arch.
The upgrade. An existing docker-compose.yml that says
DDP_TRANSPORT=uws would ask the new image for a module it does not have and
crash-loop on the require. So the Docker entrypoint and the bundle's own
start-wekan.sh coerce uws back to sockjs before starting anything, and
print why — a setting silently ignored is worse than one that fails.
tests/sockjsEverywhere.test.cjs pins the three halves together: a default
without the coercion is an upgrade trap, a coercion without the trim is dead
weight, and a trim without both is a crash.
The .zip bundles and the Docker image were trimmed with --keep-maps, so only
Sandstorm — which had a 1 GiB ceiling to get under — dropped them. Every
platform drops them now: 152 MiB per bundle, measured. That is 188 MiB
across 4766 files, less the 36 MiB inside web.browser.legacy that the entry
above already takes with it.
A source map translates a position in built code back to the source that
produced it — this bundle's server side is one 117 MiB
programs/server/app/app.js, and its 58 MiB app.js.map is what turns
app.js:1284531 into a file and a line. It is read by browser devtools, which
fetch the .map only while they are open, and by Node stack traces through
source-map-support. A released bundle has neither attached to it.
A map the server manifest NAMES is not optional, and this release did not
know it. boot.js reads every map listed in programs/server/program.json
at boot, unconditionally, so removing the files without removing the names left
the server unable to start — fixed in the release above, where the whole story
is. The client is unaffected: its manifest names no maps at all, and
a client .map is found through the //# sourceMappingURL comment, which is a
comment — a missing target means devtools show compiled positions and a server
stack trace prints bundle offsets. Debugging a production crash goes back to
reproducing it against a development build, which is where the maps still are.
The guard pins that no call site keeps them, so one platform cannot quietly
drift back to carrying them.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the Sandstorm .spk still will not pack - Sandstorm refuses an
app over 1 GiB uncompressed - and v10.94 was supposed to say what filled it.
It said nothing, because the size report ran BEFORE meteor-spk pack, and pack
is what builds the trees it was trying to measure. The sizes are taken after
pack now, on failure and on success, dereferencing the symlink that hid most of
them; and the bundle is pruned of its build-only toolchain and packed once more
before the job gives up, which is the same pruning every other bundle already
gets and the one reduction available without guessing.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following build failures:
The release workflow - the bundles a release is supposed to carry.
<details> <summary><a href="https://github.com/wekan/wekan/commit/671da7c1666c2a0dd1e1089333dcc3f8bc949487">The Sandstorm .spk fails on a size limit, and now says what filled it</a>. Thanks to xet7.</summary>build-sandstorm fails with App exceeds uncompressed size limit of 1 GiB and
nothing else. Because Sandstorm refuses to pack, no .spk is written, so there
is no artifact to open and no list of what filled it.
The first attempt at that added a size report before meteor-spk pack, and
the next run showed it printing nothing at all:
--- packing (Sandstorm refuses over 1 GiB uncompressed)
Building Meteor app...
meteor-spk pack is what BUILDS the Meteor app - Building Meteor app... comes
after it starts - so .meteor-spk/deps and .meteor-spk/bundle do not exist
until it has run. The report was measuring two directories that were not there,
found neither, and skipped both silently. A diagnostic that runs before the
thing it diagnoses says nothing at all.
The sizes are taken AFTER pack now: on failure, where they are the whole point,
and on success, where the headroom against 1 GiB is worth knowing before the
next thing is added. du -shL DEREFERENCES, because deps is a symlink to the
tree sandstorm-src/build-deps.sh assembles and a plain du -sh on a symlink
reports the link rather than the gigabyte behind it. The bundle's server
packages are listed separately, because they are the part every other bundle
prunes.
And one bounded RETRY, which is an attempt at the fix rather than more looking.
meteor-spk pack runs npm install inside the bundle's programs/server -
that is where [email protected] and node-gyp's tree come from in the log - and the
Sandstorm leg is the only one that never removes them afterwards.
prune-build-only-modules.mjs drops 83 of 120 packages everywhere else, and
nothing in a packed app runs any of them. Whether it is enough turns on
something no log has answered yet: does a second pack REUSE
.meteor-spk/bundle, or rebuild it and undo the prune? Both answers are useful
and neither is worse than the hard failure that is there now - if it rebuilds,
the second failure is identical and the log says the prune was undone; if it
reuses, the .spk packs. It runs once, only after a failure, and only if the
bundle is actually there.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the Windows bundles are back. v10.93 built them, compiled
their native modules, and then threw both away on one line: npm on Windows is
npm.cmd, a batch script, and Node applies no PATHEXT when it spawns, so
execFileSync('npm', …) in the bundle's security-bump step resolved to nothing
and build-win64 and build-win-arm64 died with spawnSync npm ENOENT after
all the work was done. npm's own CLI is run with the Node already running now,
which needs no PATH lookup and no shell. The Sandstorm .spk also failed, on
Sandstorm's 1 GiB uncompressed limit, and it failed silently - no list, no
sizes, no .spk to inspect - so the pack step now says where the gigabyte is
before it packs.
The table below carries only the four platforms this run recorded a complete,
verified provenance for; the release job regenerates it from every build job's
provenance.tsv.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.53.0 | cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.53.0 | d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3 |
This release fixes the following build failures:
The release workflow - the bundles a release is supposed to carry.
<details> <summary><a href="https://github.com/wekan/wekan/commit/21363baf6d4e80760073388a29763d03b3025480">Windows builds died on spawnSync npm ENOENT, after all the work was done</a>. Thanks to xet7.</summary>build-win64 and build-win-arm64 both failed in v10.93, and both had already
built the bundle and compiled its native modules. What killed them was one line
of releases/bump-bundle-npm-deps.mjs:
Error: spawnSync npm ENOENT
npm on Windows is npm.cmd, a batch script, and Node applies no PATHEXT when it
spawns - so execFileSync('npm', …) resolves to nothing. build-win32 was
skipped that run for want of a published Node.js build, so it never reached this
and looked fine; the fault was never architecture-specific.
npm's own CLI is run with the Node already running instead:
execFileSync(process.execPath, [npm-cli.js, …]). No PATH lookup, no PATHEXT,
no shell - and the same npm either way. shell: true would have found the
.cmd and broken differently, because with a shell Node joins the arguments and
quotes NOTHING, so the first Windows temp path containing a space would corrupt
the install. A bare npm on PATH remains as the last resort, for a Node with no
npm beside it, and on Windows it now says which case that is rather than letting
ENOENT speak for itself.
Verified end to end: a bundle-shaped tree holding qs 6.0.0 is bumped to 6.15.3
through the new path, with the dependencies the new version needs copied in
beside it.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: Docker did not work in v10.92, and this is that fixed. The "waiting for database" page added in that release stood in front of a database that was answering everyone else: the probe behind it looked for the MongoDB driver where a production bundle does not keep one, so it never asked the database anything, and the login page did not load for the ten minutes the page was allowed to hold the web port. The snap was never affected - it serves its own page and never runs that probe. Two further faults found while proving it are fixed with it: the probe forced a connection option a replica-set URL refuses, and it said nothing at all about why it had failed, which is now printed and put on the page itself.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bugs:
Starting up - what a browser sees while WeKan cannot yet serve.
<details> <summary><a href="https://github.com/wekan/wekan/commit/df41f61c2600bcbe52dca3b260f7219992946228">The waiting page hid a healthy WeKan: the probe never found a driver to ask with</a>. Thanks to Alishara and xet7.</summary>Reported against this release, with a screenshot: the login page never loads,
the container logs The database is not answering yet and serves the waiting
page, while mongosh on the host talks to the same MongoDB without complaint -
a Meteor 3 production setup from
docs/Platforms/FOSS/Container/Docker/Meteor3,
MongoDB 7.0.40, replica set rs0, network_mode: host, MONGO_URL a single
host with authSource. 10.91 worked and this did not, which is the whole
story: the page is new here, and it is what broke.
The database was never the problem. db-ready.mjs asked for the driver at
programs/server/node_modules only - and mongodb is a devDependency, not a
dependency, so a production bundle has NOTHING there. Meteor's own driver
lives under programs/server/npm/node_modules/meteor/npm-mongo. require threw
MODULE_NOT_FOUND on every ask, in every container, whatever the database was
doing; the catch turned that into "not ready", the entrypoint sent the reason
to /dev/null, and WeKan sat behind the page for the whole ten-minute window
before starting.
The snap already carries this scar. Its db-eval.mjs says so in a comment -
"made WeKan loop 'MongoDB not ready' forever" - and resolves the driver from a
list of bundle paths; db-ready.mjs now uses the same list. That is also why
the snap was unaffected by any of this: it serves its own page from
wekan-control and never runs db-ready.mjs at all.
No driver no longer means a page. It exits 2, distinct from 1, and the entrypoint starts WeKan without the page: "I could not ask" is not evidence that anything is wrong, and a page shown on that basis hides a WeKan that would have served fine. Docker with an external MongoDB needs no waiting screen, and it no longer gets one it has not earned.
Two more faults were found while proving it, both of which could hold the page in front of a working database on their own:
directConnection: true. For one host that is harmless; a
replica set is normally a SEED LIST, and the driver refuses that outright -
MongoParseError: directConnection option requires exactly one host. The
throw happened while the client was being CONSTRUCTED, outside the try, so
the probe died with an unhandled error. The options come from the URL now,
which is also the more correct question: WeKan connects with the URL as
written, so a probe that quietly connects DIFFERENTLY can report ready for a
database WeKan cannot reach - dropping the page and leaving the port closed,
which is the exact fault the page exists to prevent.docker logs. MongoServerSelectionError: connect
ECONNREFUSED wekan-db:27017 names the host that could not be reached.WEKAN_DB_WAIT_PAGE=false still turns the whole thing off.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this was tagged v10.92 and never published: its release job
died before running a line of its own script, with Argument list too long,
because the release notes had outgrown the size a single environment variable
may hold — so the release, and every job that needs it, never happened. The
amd64 and arm64 bundles built and verified; only the publishing failed. The
notes now travel from file to file and never become a shell value, which is the
one shape that neither runs a backtick as a command nor has a size limit at all.
Everything below was written for v10.92 and ships here instead. It begins with
what the published Docker image SHIPS, cleaned up against a container scan of
ghcr.io/wekan/wekan:v10.91. That scan reported 80 findings against "Node.js",
three of them CRITICAL, and not one of them was in code WeKan runs: npm and
node-gyp are build tools that were left in the finished image, and their
trees are where tar 6.2.1, sigstore, ip-address and the rest came from.
Both go now, after the install that needs them - 83 of the 120 packages in
programs/server, and npm itself. The rest of that list is the npm packages
Meteor's own packages bundle, which no package.json here can reach:
nodemailer, openpgp, svgo, postcss, nanoid, lodash,
qs, body-parser, cookie, on-headers, tmp, diff,
@babel/runtime and underscore are raised inside the built bundle, to
fixed versions in the same major, by a manifest the release jobs and the
Dockerfile share. Below that: the guard suites that keep both from coming
back, and what could NOT be fixed and why. Then card export: the PDF and
Excel exports of a card were two different answers to "what is on this card",
and are now one - the same fields under the same translated labels, dates in the
reader's own time zone and in the date format the opened card shows, and
a description's markdown drawn as bold and italic rather than stripped. On
top of that, #1173 after eight years: a board, a swimlane or a
list exports to PDF and Excel in that same card layout, from one selection
popup that says what to include. And titles are edited where they are
written: a card's title on the board (#4990, asked in 2022), a board's by
clicking its name in the header bar instead of a pencil beside it. Above all
of it, though: five CRITICAL REST API fixes reported by ybsun0215, the worst
of which let any user with write access to one board destroy the comments,
checklists and history of every card on every board in the instance. Below that:
twelve bug fixes - among them Custom Fields, which the browser tests caught
being unreachable on a card that had none, which is exactly where it is needed,
and a field made from a card that was silently never created - a test that pins
that a browser downloads one language file and not all 246 of them, and 81
languages taken past the words on the board into the menus and the login page,
beside the Export row that read as the lowercase key export in every one
of them because that key had never existed. And then the translations turned out
to have a much older problem than any missing string: 8,716 values were
written in the wrong language entirely, which no count had ever reported
because nothing was looking. Korean held Japanese, Georgian Russian,
Hindi Gujarati, Tamil Telugu — and, once a second check asked about the
Latin alphabet inside a language that is not written in it, Greek held
Italian, Thai Vietnamese and Algerian Arabic French. All of it is
translated now, and the scan that found it stays as the guard, reporting zero
for both of its checks across all 246 files. Below that: the search operators a
user TYPES, in the language they read; the one-letter shorthands beside them,
each derived from that language's own word; and the panels a file never had
because they were added after it was last touched. Then dependency updates,
thirty-odd bug fixes, the developer-facing changes, and the rest of the
translation work.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.53.0 | eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.53.0 | bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following CRITICAL SECURITY ISSUES:
The REST API - what an endpoint authorises, and what it then acts on.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9cc4139699ef8d6b6e51efb45b9a06f66e4340b4">PurgeBleed: the single-card DELETE destroyed the contents of any card in the instance</a>. Thanks to ybsun0215 and xet7.</summary>PurgeBleed is the severe one of the
five. DELETE /api/boards/{boardId}/lists/{listId}/cards/{cardId} authorised
the caller on the board in the URL, and then fetched the card by its id ALONE.
The order is what makes it destructive. cardRemover runs BEFORE the card is
removed - it has to, so the children's before.remove hooks still find their
parent - and it removes strictly by card id: checklists, checklist items,
comments, the activity history and the whole subcard tree. So it erased those
for whatever card the bare lookup returned, which was any card in the instance,
on boards the caller cannot read, irreversibly.
The removal that follows uses a triple key, {_id, listId, boardId}, which a
foreign card never matches. The card SHELL therefore survived and the endpoint
answered 200 with the card id - so nothing in the response said that the
contents of somebody else's card had just been destroyed, and the attack is
repeatable for every card id an attacker learns.
Any authenticated user with write access to ONE board - their own is enough -
could reach every board on the instance. All deployments with WITH_API=true
are affected.
The bulk endpoint had always constrained its lookup with {_id, boardId}; the
single-card path is the sibling that was missed, the same shape as
PassBleed. It uses the constrained
lookup now, so a card outside the authorised board does not resolve, and
cardRemover is never reached.
HashBleed: GET /api/users/{userId}
and PUT /api/users/{userId} serialised the whole Meteor user document with no
projection, so every answer carried services.password.bcrypt - an
offline-crackable password hash - and services.resume.loginTokens, the hash of
every live session with the time it began.
The authorisation was never the problem: both endpoints are admin-only and stay
that way. The payload was. Walking the ids that GET /api/users returns
harvested the credential material of the whole instance, and a password hash is
an attack that continues offline long after the export.
The two sibling endpoints in the same file show what was intended: the list
projects down to _id and username, and the self view runs delete data.services before answering. Nothing in the code, the CHANGELOG or the
documentation ever said the subtree was meant to be exposed. One helper strips
services and sessionData now, and both endpoints answer through it - the PUT
as well, which returned the same unprojected document after every action.
GuestBleed is two paths that are
each defensible alone. The members and assignees ARRAYS on card create and
card update were stored exactly as given, with nothing checking that those ids
belong to the card's board. And GET /api/user/cards answered by CARD
membership, without re-checking the caller's access to the card's BOARD.
Together they are a channel: a member of a private board writes an outsider's id onto a card, and the outsider's own my cards feed then returns that card's title, its board, list and swimlane ids, its dates and its co-members - for as long as the id stays on the card. Their direct read of the board stayed Forbidden the whole time, which is what made it quiet.
The invariant already existed, documented on the merge endpoint POST .../cards/{cardId}/members/{memberId}, which has refused a non-member with 400
since #5998. It covers the array
shapes now - single create, bulk create and update - and an id that may not be
assigned is dropped rather than the request refused, so a bulk edit does not
fail over one stale id. The listing is filtered by board visibility as well,
because fixing only the write path would leave every card placed before this
release still answering.
StaleBleed: GET /api/users/{userId}/boards selected boards with a dotted 'members.userId'
match, which ignores the membership's isActive flag. Removing a board member
does not delete their entry - it sets isActive: false and isAdmin: false and
keeps it - so a removed member's own board listing went on showing that board's
id and title, indefinitely.
Reading the board itself was already refused, which bounds this to the id and the title. The id is the part that matters, because it is what every other endpoint in the API is addressed by, and a private board's title is often the thing it is private about.
A dotted path cannot express this: in Mongo, 'members.userId' and
'members.isActive' may be satisfied by DIFFERENT entries of the array.
$elemMatch is what ties them to the same entry, and it is what the rest of
WeKan uses - the single builder introduced by the fix for
GHSA-gwc4-fw7p-gw58,
whose header note reads "A share entry counts only while it is active,
everywhere". This listing predates that consolidation and was never converted.
AuthorBleed: six server paths took
the actor's identity from the request body's authorId field, checking only
that such a user exists. An existence check is not an authentication check - it
confirms that the name in the envelope belongs to somebody, and says nothing
about who wrote the letter.
So a board member could record "victim created this card" and "victim deleted
this card" on any board they may write to, and the card document itself
recorded the forged userId as its creator. The six are single card create,
bulk card create, the linked-card form, single card delete, bulk card delete and
custom-field create.
WeKan had already accepted this exact class as a vulnerability and fixed it for
card comments in 8.19, and again for the card PUT handler - whose inline note
still reads "use req.userId consistently (it previously read req.body.authorId
here)". These six were missed, which makes it an incomplete fix rather than a
decision. All six read req.userId now, the session the request authenticated
as, which is the only identity the server can vouch for.
and fixes the following SECURITY ISSUES found by container scanning:
The published image - what it carries that it never runs.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6157094ab8e633e65427c7cd046537af77956bff">npm and node-gyp's tree are not shipped any more, after the install that needed them</a>. Thanks to xet7.</summary>Three CRITICAL findings and most of the HIGH ones were three copies of one
package - tar 6.2.1 twice and tar 7.5.11 once - and none of them is
reachable from boot.js:
node-gyp and @mapbox/node-pre-gyp are dependencies of Meteor's
meteor-dev-bundle, there to COMPILE native modules during the npm install
in programs/server. WeKan compiles nothing at run time: uWebSockets.js,
bcrypt and argon2 all ship prebuilt .node files that node-gyp-build picks
at require time. Their tree is 83 of the 120 packages that install leaves
behind, and it brought tar 6.2.1 (via node-gyp and cacache),
brace-expansion, minimatch and npm's networking stack with it.npm itself runs exactly once in the image, for that install. The container
starts bash /build/wekan-entrypoint.sh, which never calls it. Shipping it
shipped its own bundled tar 7.5.11, sigstore 4.1.0, @sigstore/verify,
@sigstore/core, ip-address 10.1.0 and brace-expansion 5.0.4 as image
content no code path can reach.releases/prune-build-only-modules.mjs removes the first, and it is a
REACHABILITY walk rather than a list of 83 names - start from every dependency
of programs/server/package.json except those two, follow each package's own
dependencies, keep the closure - so it cannot go stale the next time Meteor
changes its dev-bundle. The Dockerfile deletes npm and npx in its cleanup
step; node stays, because that is what runs WeKan.
It runs in every place a bundle is made, not only in the image: the amd64 build,
the arm64 container, the three Windows legs, both macOS legs, and
install-node-for-arch.sh for the emulated arches - each one reinstalls
programs/server, so each one has the tree to remove. The pruned bundle was
booted before and after to prove nothing needs what it takes:
identical failure at the database, no missing module.
The rest of that scan was nodemailer 8.0.3 and openpgp 5.11.1
(meteor/email),
svgo 2.8.2, postcss 8.5.1 and nanoid 3.3.15 (meteor/minifier-css), qs
6.13.0, cookie 0.4.1, on-headers 1.0.2 and tmp 0.2.3 (meteor/webapp),
lodash 4.17.21 and diff 3.5.0 (ostrio:files), body-parser 1.20.3
(meteor/oauth), @babel/runtime 7.20.7 and underscore 1.13.7.
Npm.depends names an EXACT version, and meteor build copies that version
into
programs/server/npm/node_modules/meteor/<package>/node_modules. Nothing in
this
repository's package.json is consulted for it - not a dependency, not an
overrides entry - so the only place those versions can be raised is the
bundle,
after it has been built. releases/bump-bundle-npm-deps.mjs does that from the
minimums in releases/bundle-npm-security-bumps.json, installing with
--ignore-scripts so no prebuilt native module is rebuilt by a version bump,
and
replacing only copies BELOW the minimum.
A minimum stays inside the major the Meteor package was built against, and that
rule was learned rather than assumed: uuid 8.3.2 and 9.0.1 are both affected
and the lowest fixed release is 11.1.1, which moved its entry point to
dist/cjs/index.js - the bundle records dist/index.js at build time, and the
server died on boot with Cannot find module .../uuid/dist/index.js. It is in
the manifest's notFixable list with that error, beside lodash.template,
which has no fixed release at all.
One pass on the amd64 bundle reaches every architecture, because every other
bundle is that bundle with programs/server reinstalled. That reinstall is also
why every leg runs it again: meteor-dev-bundle pins underscore 1.13.7
(CVE-2026-27601) and puts it back over the bumped copy - including in the
Dockerfile, which reinstalls from the .zip.
tests/imageBuildOnlyModules.test.cjs pins that the pruner is
reachability-driven
and never touches npm/node_modules, that the image prunes AFTER the install
and
before the bundle is moved, that npm and npx are removed and node is not, and
that every leg which installs programs/server prunes afterwards.
tests/bundleNpmSecurityBumps.test.cjs pins the manifest's versions, the
--ignore-scripts install, the below-the-minimum-only replacement, and that
uuid is NOT in minimums and says why.
Not everything in that report can be fixed here, and it is worth saying which:
debian 13.6 target's 176 findings are ALL Fixed in: - - unfixed
upstream, in perl-base, util-linux, ncurses, glibc and the rest of a
base system. No upgrade closes them; only carrying fewer packages helps, which
is what the build-dependency purge and the pebble removal already do.lodash.template 4.5.0 has no fixed release; it reaches the bundle through
aldeed:simple-schema.nodemailer's fifth advisory needs 9.0.1, a major the Meteor email package is
not written against; 8.0.11 fixes the other four.build/<tool> binaries and build/ferretdb are other repositories'
builds - wekan/mongo-tools-patches and wekan/FerretDB - and are fixed there.and adds the following new features:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d11f5ffdb768f5ebcb6bc447c385d77147e0c59a">Import into a swimlane, a list or a card, beside the one whose menu was used</a>. Thanks to xet7.</summary>The other direction of the same popup. Importing used to mean one thing: a whole new board, from the full-width page at All Boards. A swimlane's menu now imports a swimlane BELOW that swimlane, a list's menu imports a list after it - which is to its RIGHT in English and to its LEFT in Arabic - and a card's menu imports a card below it, into the board that is already open.
The RTL side of that is ONE rule and not two, which is worth saying because it
looks like a missing branch: the page carries dir, so the board's row of lists
mirrors itself and "after in sort order" is already "the other side". A
direction
branch in the placement would mirror it twice and put the list back where it
started.
Placement is a FRACTION between the target's sort and the next one's, not a renumbering of every sibling - renumbering is what a board with ten thousand cards cannot afford, and what two clients doing it at once get wrong. Several imported items spread evenly through that gap, so importing a swimlane of ten lists does not put nine of them in the same position.
The file is what the export writes, at any scope, and the SAME checkboxes decide
what comes in: a document full of comments imported with Comments unticked
brings the cards and leaves the comments. A .zip is unpacked in the browser
and
its wekan.json handed to the same method, so there is one import path rather
than two. Nothing is merged - everything created is new, because an import that
half-updated a board would be an edit nobody could undo - and a custom field is
matched by NAME, since an id from another board matches nothing here.
Importing WRITES, so unlike the exports beside it, it asks whether you may change the board rather than whether you may see it, and a read-only member is not offered it at all.
</details>All Boards - the overview and the tiles in it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6c591968024d8889accdd9a1c342d05db7d6738e">A starred page and a Template Container are the same height as a board and as "+ Add Board"</a>. Thanks to xet7.</summary>In All Boards / Starred, a bookmark tile stood 8px taller than the board tile beside it and than the grey "+ Add Board" box, and pulled its whole row up with it.
Two of the four tiles in that grid carry border: 4px solid #fff - a bookmark
and the Template Container - to say they are not an ordinary board. The tile's
height was a 72px floor with 42px of padding added outside it, which comes to
the same 114px for a plain tile and to 122px for a bordered one, because on a
content-box element a border is added to the height rather than taken out of it.
The heights all said 72px, so nothing in the stylesheet looked wrong.
The tile now states the height it actually renders at - 114px, with the padding
and the border folded into it (box-sizing: border-box), which is what the
mobile rules in the same file already do for the same reason. Nothing moves
except the two bordered tiles, which lose the 8px they were never meant to have.
The guard compares RENDERED heights now, borders included, instead of comparing
the declared min-height of two tiles that were both content-box - which is how
this went unnoticed while a test watched it.
The last piece of #1173: "combine all import options to same template". They were fourteen - a link per source in a pop-over, each going to its own address - and the page each one landed on never said which other sources existed, so "where do I import a Jira export" was answered by a menu somewhere else, if you knew it was there.
All Boards / + Add Board / Import is now one full-width page that lists every source it can read - Trello, Jira, CSV/TSV, Excel, Kanboard, NextCloud Deck, OpenProject, GitHub, GitLab, Gitea, Forgejo, Asana, Zenkit, and a previous export of this WeKan, named with the Product name this instance is branded with rather than with a product the reader has never seen.
Under the source picker are the same what to include checkboxes every export
offers, from the same list, and on this side they say what comes IN. That works
for every source without teaching five different creators a selection each: the
parts that were not ticked are taken OUT of the parsed document before any
creator sees it, and a creator that never sees a comment cannot import one. A
section that was not ticked is EMPTIED rather than removed, because the creators
read board.comments directly and an undefined array is a crash where an empty
one is "there were none". A source's own name for a part is pruned with it -
Trello calls its comments actions - and a key this list does not know about is
left alone rather than silently dropped.
/import/:source still works, under its own route name, so every existing link,
bookmark and back button lands exactly where it did.
The round trip was half a round trip: a card imported from a swimlane, list or card menu arrived with its checklists and comments and without its files.
A .json export carries each attachment's bytes as base64, and the importer now
writes them the way the board import always has - the server-side Meteor-Files
writeAsync, one attachment at a time. A .zip carries them as the files they
are, named attachments/<id>-<name>; the archive is unpacked in the browser and
each file is put back on the metadata row its id names, so the server sees the
same document either way and there is one import path rather than one per
container.
An attachment lands where its CARD landed - the list and swimlane it is in now,
not the ones it was exported from - and one unreadable file is warned about and
skipped rather than losing the rest of the import. A url attachment from an
older export is still fetched through the downloader that validates and pins
every hop, so FollowBleed stays
fixed on this path too.
The whole-board import on the new import page takes a .json or a .zip
the same way, through the same reader, which is the case the .zip exists for:
a board whose attachments are too large to sit inside one JSON string.
Reading an archive in the browser and sending its attachments as base64 over DDP is fine for a card and wrong for a board: 2 GB of attachments become 2.7 GB in one message, in the browser's memory and then in the server's.
A .zip now goes to POST /api/import/zip as the file itself, and nothing is
ever whole in memory on the way in. The request body is streamed to a temp file
as it arrives; unzipper.Open.file reads the archive's central directory, so
entries are opened on demand rather than inflated together - the same approach
the backup restore already takes; and each attachment is piped from the archive
into the attachments collection by addAttachmentFromStream, which writes it to
a temp file and hands Meteor-Files the PATH rather than a Buffer. That helper is
lifted out of the attachment-copy code that already did exactly this, so there
is one way to add an attachment from a stream rather than two.
Where the files END UP is not decided by the import: addFile fires the
collection's onAfterUpload, which validates the file and moves it to the
default storage configured in the Admin Panel, exactly as an ordinary upload
does.
The upload is capped as it ARRIVES rather than after
(WEKAN_IMPORT_ZIP_MAX_BYTES,
5 GB by default), so an oversized archive never lands, and the temp file is
removed whatever happens. An entry's name is data and never a path: only the
attachment id before the first dash is read from it, the temp file is named by
WeKan, and the path is built through the same safeEntryPath containment check
the backup restore uses - so an entry called ../../etc/cron.d/x can only ever
be an attachment with a strange name (ZipBleed).
A .json still travels as a document over DDP, which is what it is.
Three things about the page that lists the sources. The pop-over that opens it said "Import board (Trello, Jira, WeKan export, CSV, Excel, ...)" - a list of five of the fourteen sources, in a menu whose only job is to get to the page that lists all of them - and now says Import board and nothing else.
Import from: and Select what to include: were rows of grey ticks that
were never anything else: a fa-check drawn once per row, the same shape
whether that row was chosen or not, so the answer to "which one did I pick" was
a shade of grey. Both are the app's own animated checkbox now
(.materialCheckBox, the one Admin Panel / Announcement uses), which grows its
green tick out of the empty square over 0.2s, so a click is visible as it
happens. The box and its label are spaced apart by the same amount ticked or
unticked - the checked box is a narrower shape shifted left, so the two margins
differ by exactly that shift - and the row itself declares no alignment of its
own, because the tick's rotation is written for the plain flow and an
align-items: center on top of it lands the tick on the first word.
The two questions sit side by side when the window has room for two 280px columns and stack when it does not, so the second one is not below the fold on a page whose first list is fourteen rows long. No source is chosen to begin with, and choosing one un-chooses the last, because an import reads one file in one format; every part is ticked to begin with, because an import that silently left things out would be worse than one that asks.
And Import without mapping members (map later) is a primary button like the Import it sits beside. It was the one unstyled button on the page, which reads as disabled - and it is not a cancel, it is the same import with one question skipped.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/05ddc83cb786f7ca3baede3e80bf925a72e0e696">A board tile in All Boards is the board's whole theme, and light themes are readable on it</a>. Thanks to xet7.</summary>A board on the clearpink theme was a pink rectangle floating inside a grey tile, while the flat-coloured board beside it filled its tile edge to edge.
Two lists decided what a tile is coloured with. boardColors.css painted
.board-list .board-color-<name> a - the LINK inside the tile, which sits
inside the tile's own 24px/18px padding and so can never reach its edges - and
boardsList.css carried a hand-copied list of seventeen flat background
colours on the tile itself, which is what made the flat themes look right. The
five colour SLIDE themes added later were never copied into that list, so their
tiles kept the default grey and only the inset link showed the slide.
clearblue was in the list, flattened to one hex, so it did not show its
slide either, and Clean Dark and Clean Light were in neither list.
There is one list now. Each theme paints .board-list li.board-color-<name>
where it paints its header bar and its Public Boards row, the copy in
boardsList.css is gone, and a tile is the whole tile at every theme.
The other half of a theme is the text on it. A tile writes its title in
#f6f6f6, which is right on the fifteen dark themes and invisible on a light
one: Apple Glass Pastel's tile is a pastel wash from #f6f7fb, so its name
and description were white on near-white and only the tile's shape said a board
was there. Both light themes now write their title, description and archive
line in their own dark ink, and darken the card-count pill and the unstarred
star that sit on the same tile - in ONE block, named as the place a third light
theme goes, because a per-theme copy is what caused the first half of this.
The Clean Light theme's top bar is #F1F1F3, and header.css paints every
icon in that bar white by id (#header-quick-access i.fa). The buttons inside a
ul li escaped that through a more specific color: inherit and took the
theme's own text colour; the house and the notification bell are in no
ul li, so they stayed white on near-white - the bell's LABEL was readable
beside a bell that was not there, which is how it was noticed.
Both are dark now, and so is everything beside them: the bar's labels were
rgba(10, 10, 20, 0.5) and its board title full black, so fixing the two icons
alone would have left the bar at three darknesses. There is ONE shade in it now.
The current entry used to be the dark one among pale ones - a difference
that no longer exists - so it is the full-black, heavier one instead. A bell
with something to report keeps its white glyph, because it is on the red circle
then and not on the bar.
The dividers between the bar's groups - the seam that separates the page's own controls from your account - are white too, a background in the first bar and a border in the second, so on this theme they were not there at all. They are in ink now, at the divider's own weight rather than the text's: a hairline as dark as the label beside it stops reading as a seam and becomes a stroke. The starred group's outline - the box around the caret, the count and the star - was white for the same reason, so the three sat loose on this theme; it is the same 0.7 in ink now, which is the dark outline the phone/desktop toggle beside it already draws for itself and why that one looked right and this one did not.
Hovering an icon lightens it - #header-quick-access i.fa:hover goes to
#ccc - which on a dark bar reads as "brighter, so you know you are on it" and
on this one made the house fade towards the bar it sits on. On this theme it
goes the other way, to full black, which is the same message read the right way
round for a light bar.
And with Member Settings / Change color / All Boards on, a tile is the
theme's own fill rather than the accent under a flat white veil. The veil made
every tile one shade lighter than the selected row in the left menu beside it,
which reads as two colours rather than as one page. It reads
--theme-accent-fill now, not --theme-accent: the accent is ONE colour - the
solid end of a colour-slide theme - so on clearorange the tiles came out
flat beside a menu row that slid. The fill is the theme's slide where it has
one and its accent where it does not, which is what every other themed control
already reads. The popup's own All Boards button is filled from the same
variable, because the button and the tiles it turns on are one decision and
were two looks.
On the Modern theme the popup that asks all this was a single narrow column
of swatches. That theme set width: 260px on every popup's content, so any
popup that asks for width by name - Change Color, Export board, Show on Card,
Show on Minicard - was pinned to 260px whatever it had asked for. A theme
decides what a popup looks like; how wide it is belongs to the popup, and the
declaration is gone.
And on the Dark theme that popup had no title bar at all. That theme hid every popup's header outright, and the header is not decoration: it carries the popup's title, the back arrow into the popup it came from, and the X - so "Change Color" was an untitled panel that could only be left with Escape or a click outside. No other theme does that, and Dark no longer does either.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d5b69137cc3090e5b0ca503d1a516b6518d401c7">Member Settings / Font / Size moves all of the text, not the part written in rem</a>. Thanks to xet7.</summary>Picking Largest grew the page heading and the minicards and left the header bar, the left menu, the lists, the swimlane header and every popup exactly as they were - a setting that half-works, which is harder to use than one that does nothing.
The preset was a percentage on the root element, and a root percentage only
reaches text whose size is written in a RELATIVE unit: rem is measured against
the root, em against its parent. WeKan writes most of its sizes in px, and
px is absolute - no root percentage moves it. So exactly the rem-sized parts
scaled. The base rule made it worse: html, body, input, select, textarea, button re-stated font: 14px …, so the body took the stock size straight back
off the scaled root, and everything inheriting from the body with it.
The preset is now published as a NUMBER as well
(--wekan-ui-font-scale), and every px font-size and line-height in the
client stylesheets - 433 declarations across 37 files - is written
calc(14px * var(--wekan-ui-font-scale, 1)). One preset moves the whole
interface. line-height scales with the size on purpose: 21px of type in an
18px line box is the same setting half-applied.
Nothing renders differently until a preset is chosen: the fallback in every one
of those declarations is 1, and the variable is REMOVED for Default, so an
instance where nobody touched the setting computes exactly the sizes it always
did. The base font rule is split into font-family and font-size because the
shorthand's size was the one thing the setting had to be able to move. A guard
fails on any bare px font-size or line-height added later, so a new one
cannot quietly opt out of the setting.
Colour is inherited, and it was set on <body> and the form controls, so it
only ever reached text that had no colour of its own - and WeKan gives most of
its text one: the header bar's buttons, the left menu's rows, a minicard's
title, a list header. Choosing green recoloured the page heading and the menu
and left the rest exactly as it was. It is set on every element now.
.fa is left out, because those are Font Awesome GLYPHS rather than text - a
red alert and a green tick mean something by being that colour - and the icons
that are meant to follow their label already say color: inherit, so they
follow this anyway.
A colour painted behind the text needs elements to sit on, and neither choice of
them looks good. On the boxes - which is what it did, from <body> - it painted
the whole window: the board canvas and the empty space under the lists, which is
a page background and not a text background. On the elements that actually carry
text it striped every heading, menu row and paragraph with a full-width band. A
setting nobody can make look good is worse than no setting, so it is removed
rather than tuned a third time.
The wheel and its Unset button are gone from the popup, nothing reads
profile.uiTextBgColor, and no stylesheet rule paints it. A profile that
already HAS a colour is cleaned rather than left dormant: setUiColors unsets
the field on every call, whatever it is passed - and it still accepts the
argument, so an older client cannot fail against a newer server. The schema key
stays declared for exactly one reason: a modifier touching a key SimpleSchema
does not know is rejected, which would leave those profiles the only ones that
could not be cleaned.
The text colour beside it stays.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/024036f4ee4b976f51bf6a032aa9b5d8c3916138">A custom colour can be chosen in Member Settings and the Admin Panel, not only on a board</a>. Thanks to xet7.</summary>All three places render the same picker, so the Custom color wheel was already written for all three - it was just never reachable in two of them. It appeared only once a named theme had been SELECTED, and a board always has a colour whose first entry is flat, so Board Settings / Change Color always showed it; Member Settings / Change Color and Admin Panel / Settings / Visibility open on Default (no override) with nothing selected, so both looked as though they had no custom colour at all.
The wheel is offered from that state too, as the flat category's single colour - which is what a custom colour means with no theme under it - and choosing one applies it over the first flat theme. That fallback was already in the code that saves; it is now written into the picker's own selection as well, because otherwise the wheel would store a theme the page did not show as chosen and the next click would read the selection back as none.
Picking a clear theme first still gives the two wheels its colour slide needs. One helper answers "which category's custom colours is this picker offering", so the wheel, the preview and what gets saved cannot disagree.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/df2913744e382eaeaa36b3cdc407998895124c27">Every export popup is one popup with one scope, and looks like it</a>. Thanks to xet7.</summary>There were two export menus. The board popup wrote nineteen formats of its own under four subheadings, in two panes across the width of the window; the swimlane, list, card and checklist popups wrote five in one narrow column with no panes. So "the export popup" looked like two different features depending on which menu opened it, and a format added to one was missing from the other.
They are one template now, drawing one TABLE of formats. Each entry says what it
is - an icon, a label, and either a path to download or a class to click - and
which scopes it belongs to: BOARD_ONLY for the ones that only make sense for a
whole board (the HTML archive, the dependency graph, the CSV columns and the
exports written for Trello, Jira, Kanboard and the rest), and nothing at all for
the ones that work anywhere, because exportUrl already carries the scope - a
swimlane, a list and a card differ from a board by a query parameter, not by a
route. The board popup is now the same one-line include the other four are, with
no markup and no URL builders of its own; the nineteen helpers that built those
URLs are gone with the list that called them.
All five get the board's panel: what to include in one pane and what to export it to in the other, side by side when the window has room and stacked when it does not, pinned to the viewport so the header and its X are always reachable. The layout is written against the panes' own class rather than against one popup's name, so it cannot be true of one menu and not another.
Which scope a popup is, is asked in ONE place - and every scope is named there, because a scope left out would be read as "a whole board" and offered a board's formats.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e948acfdd31e48d2544b81268a8309352581d1f0">Every menu on a board has an Export row and an Import row</a>. Thanks to xet7.</summary>The card, list, swimlane and board menus each had ONE row, and each named itself differently: Export card, Export list / Import, Export swimlane / Import, Export board. Two of them mentioned an import that was a section inside the popup rather than a thing you could go to, and the card and board menus did not mention it at all - so importing into a list was discoverable and importing into a card was not.
Each of the four now has Export and Import, in that order. The rows are named for what they do and not for what they act on: the menu already says whether it is a card, a list, a swimlane or the board, so Export card inside the card menu said "card" twice.
Both rows open the same popup - the same panes, the same table of formats, the same what to include selection - with one difference: the second pane offers the formats to write out, or the file to read in. The selection means the same thing in both directions, which is why it is the pane they share.
Import writes, so it is offered only to somebody who may change the board. That question is asked once, in one function, registered as a helper the four menus use - and asked AGAIN inside the popup, because a row that is merely hidden is not a permission check.
</details>Search - finding a card by what people call it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/530ef32c16c41dfa52f408e31da21a2d32cbb93e">Cards can be searched by their card number</a>. Thanks to mfilser and xet7.</summary>#5006, open since 2023: "A customer has just hinted us that we are not able to search cards by its number (#)". A card carries a number, the board shows it, people quote it to each other in meetings and in tickets - and the one thing that could not be done with it was find the card again.
number:12 now does, in the global search and everywhere else that parses the
same query. It is an EQUALITY match on a number rather than a regular expression
on text, which is the whole difference between finding card 12 and finding cards
12, 120 and 312. number:abc is refused with the same "expected a number" error
limit: already gives, because a string never equals a numeric field and a
search that quietly finds nothing teaches nobody anything.
The issue's title says "number/#", and both of those forms work too: #12 and a
bare 12 search for a label called 12 AND the card numbered 12. A board calls a
card "#12" and a label can be called anything, so which of the two somebody
means cannot be read off the text - answering with both is the only reading that
never hides what they were looking for.
The two halves are joined with OR, which is what makes that safe rather than
destructive: a search that used to find a label called 2024 still finds it,
and the card numbered 2024 is ADDED to the answer instead of replacing it. An
AND would have found nothing at all in almost every case and looked like the
feature working. Only a term that is entirely digits gets the second half, so
#red is the label red exactly as before. And #12 on a board with no label
called 12 no longer reports "label not found" - the card-number half is a real
answer, and a not-found message beside the card it just found contradicts the
screen.
Board, swimlane and list export - printing a board, and what goes in it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bb6f18c469810c787ce1ac6b2872cc39b5dc1ae5">The export selection reaches every format, and the menu opens wide enough to read at once</a>. Thanks to xet7.</summary>Two halves of the same menu.
The selection now reaches all of it. PDF, Excel, JSON and .zip took the
parts checkboxes; CSV, TSV, Kanboard and the eleven JSON dialects built their
own
addresses and could not carry them, so ticking "no comments" changed four
downloads out of nineteen. Every board export link is built by the one url
helper now, so ?fields= rides along with all of them.
What a format can DROP depends on what it has, and that is the honest part. A CSV has no comments to leave out - what it has is columns, so the selection lands there: unticking People removes five of them, Dates removes eight, Custom Fields removes the trailing block. One mask filters the header and every row, so the two cannot drift apart. A Trello, Jira or GitHub export carries a title, a description, a due date and labels; those three parts are gated and nothing pretends to gate what is not there.
It is a full-width panel, and its X is always reachable. It was anchored to
the button that opens it and clamped into the window using an assumed width -
and the assumption and the stylesheet disagreed by a few percent, so on a narrow
window the panel's trailing edge, with the pop-over's own X on it, ended up past
the edge of the screen: Escape or a click away were the only ways to shut it. It
is pinned to the top of the viewport now, at the 10px gutter every popup keeps,
and is calc(100vw - 20px) wide - the whole window, less that gutter on each
side - so the header, the X and both panes are always on screen.
And the menu is a menu again. Twenty-odd entries under three subheadings
were a single column you scrolled past. On a window with room it is now TWO
PANES - what to include on the left, what to export to on the right - each
filling its own width with as many columns as fit, so the whole menu is visible
at once. The panes are grid COLUMNS, which is what makes the right-to-left case
free: a mirrored page puts the first column on the right, so the selection lands
on the right and the formats on the left with no second rule to write or forget,
and the divider between them is a border-inline-start for the same reason.
Below 800px they stack, because popup.css already lays every popup out as a
full-screen sheet there, which is what a phone should get. The 1100px width is
mirrored in client/lib/popupOffset.js, which places a popup using its width:
left at the default 380 a wide menu opened near the right edge lands most of the
way off the screen.
A rule sits above each subheading, so a group's name says where the group before it ended - one rule per heading and no stray separators, which a test counts.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b77299c8063859f11855467bbfd59e0a6097c7bc">A component that others import loads its own template, so the client bundle cannot die at startup</a>. Thanks to xet7.</summary>Template.exportScopeBody.helpers(...) runs at module scope, and it throws when
that template is not defined YET. That does not break one popup - it stops the
module evaluating, so every template registered after it never registers either.
The visible symptom was the SIGN-IN page: a blank form and
Uncaught TypeError: can't access property "helpers", Template.exportScopeBody is
undefined
[UserAccounts] Warning no template passwordInput found!
The central lists in client/features/*.js import a component's .jade before
its .js, which is enough for a component nobody else imports. The export popup
body is imported by the sidebar, by the card details and by the import page, and
whichever of those is reached first evaluates it - long before the feature list
gets to the template. It imports its own .jade now, so the order is a fact
rather than a hope.
A guard in tests/clientBundleImports.test.cjs checks the whole rule, and found
a second component with the same fragility that had not fired yet -
migrationProgress.js, imported by boardBody.js. Fixed the same way.
It was nineteen entries in one list, each spelling out its whole family: "Export / CSV (,)", "Export / CSV (;)", "Export / TSV", then eleven lines beginning "Export / JSON /". The part that differed - the only part worth reading - started two thirds of the way along every line.
A subheading names the family once and the entries under it say only what they
are: PDF, Excel and HTML, then Dependencies (JSON, SVG), CSV
((,), (;), TSV) and JSON (JSON, JSON without attachments, .zip, Kanboard,
Trello, Jira, NextCloud Deck, OpenProject, GitHub, GitLab, Gitea, Forgejo,
Asana, Zenkit). The .zip sits with the JSON it is a container for.
The parts checkboxes stay at the top, from the shared popup body: the board
passes hideFormats because it lays the formats out itself, and borrows the
selection rather than growing a second copy of it. Its links are built by the
same url helper every other menu uses, so a menu cannot send a different
selection than the one on the screen. Nothing was dropped in the regrouping, and
a test walks the whole list in order to say so.
The board's Export menu had JSON; nothing else did, and there was no .zip at all. Both are now offered wherever an export is - the board, a swimlane, a list, a card and a checklist - from the same popup, with the same checkboxes deciding what goes in.
They are the same export in two shapes, not two exports. The document is written
by models/exporter.js's streaming writer either way, so a .zip's
wekan.json
and a .json download of the same scope are the same bytes. What differs is
where the attachments are: base64 INSIDE the document for JSON - or omitted,
with
the existing "without attachments" option now offered at every menu - and beside
it under attachments/ as the files they are for the .zip.
Both halves stream. The JSON writer already wrote a document at a time from raw
cursors with backpressure; the .zip gives it a PassThrough that archiver
compresses as it fills, and pipes every attachment from the file store rather
than reading it into a Buffer. A board with a gigabyte of attachments costs a
gigabyte of disk reads and not a gigabyte of RAM - which is what makes the
.zip
the shape to use when the JSON is too large to hold as one string.
A SCOPED export is the same wekan-board-1.0.0 document with fewer rows in it,
plus the lists and swimlanes its cards refer to, so what comes out can be
imported back into somewhere. A section the popup did not tick is an EMPTY array
rather than a missing key, for the same reason. A checklist scope exports the
card that holds it, because a checklist alone has nowhere to land.
#1173 "Add Feature: Print Board with Params" has been open since 2017, and two things were missing. There was no CHOICE of what to print: the board's Excel and PDF exports took everything they knew how to render and nothing else, while the card export already had a popup with a checkbox per section. And they did not LOOK like the card export - the board's Excel export was a spreadsheet table, one row per card and eighteen columns, which is a data dump rather than a printed board.
Both now render a board as the board's own header followed by every card as the
CARD export's block, drawn by the card export's own code - cardBlockLines()
for the PDF, ExporterExcelCard.renderCardBlock() for the Excel one - so a card
looks the same whether it was exported alone or as part of its board, and the
two cannot drift into two layouts again. Each card starts on its own page in the
Excel export, because a printed board is read a card at a time.
The swimlane and list menus offer the same export, which is the board export with one more parameter saying which cards are in scope. They sit beside "Copy link" rather than below the permission checks, because exporting is reading. The file is named for what was exported - a PDF titled with the board that holds one list is a file nobody can place afterwards.
Rendering a card block needs the whole board in memory, and
models/server/ExporterExcel.js STREAMS on purpose - it was rewritten that way
after the in-memory version ate gigabytes on boards with thousands of cards. So
that exporter is still there and still reachable: unticking Card details
asks for it. That is a checkbox in the popup, not a silent fallback nobody can
see.
The card export's popup had a list of sections, and the server had another one, under a comment reading "Must match ALL_FIELDS in models/server/ExporterExcelCard.js" - which is a comment, not a mechanism. A section added on one side and forgotten on the other is either a checkbox that does nothing or a section nobody can turn off.
Both now import models/lib/exportFields.js, and
so do the board, swimlane and list popups, which are ONE shared body -
exportScopeBody - included with a different scope each. The selection is
remembered for the session, because somebody printing a board rarely wants a
different shape for each list of it.
The same ?fields= gates the same sections in both formats: the card export's
checkboxes used to be labelled "fields to include in Excel export" and did
nothing to the PDF, so one popup meant two things. A section a request does not
name is not rendered and, where the export is the only reason to read it, not
even fetched.
Titles - renaming a thing where its name is written.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c6ad79562955d1993d703e9d57885ae5db20088f">Clicking a card's title on the board edits it there</a>. Thanks to bonnebulle and xet7.</summary>A list's title has always been edited in place: click the heading, type, save. A card's title could only be changed by opening the card, so correcting a typo on a board of forty cards was open, edit, close, forty times - which is what #4990 asked about, in 2022.
The minicard's title text is now an inlinedForm, the same component the list
heading uses, with the same textarea, Save button and X. Only the TEXT opens it,
and only for somebody who may write: the complete checkbox, the linked-card
icons and the card number beside it keep doing what they did, and everything
else on the card still opens the card. A title nobody may edit still opens it
too.
The minicard sits inside the link to the card, so a click in the open editor would have navigated away mid-rename. That default is cancelled - except on the Save button, whose own default IS the submit, and which the browser picks over the link around it because the innermost element with an activation behaviour is the one that runs. An empty save is a no-op rather than a way to end up with a card that has nothing to click.
Half of the title edits; the other half drags. With drag handles OFF a card is dragged by its own body, so a title that is entirely an edit target leaves nowhere on that line to take hold of - a grab that moves a few pixels is a click, and the editor opens instead of the card moving. The edit target is the LEADING half (left in English, right in Arabic, from one logical edge) and the trailing half is there to drag from. With handles ON the handle is the only drag source, so nothing has to be reserved and the whole title edits.
The opened card's title splits the same way, and its drag handle now appears only when drag handles are on - with them off, the title bar is what moves the window. The drag surface there is the header ROW rather than the heading: a heading is only as wide as its own text, so on a short title "the trailing half" was a few pixels and the empty space beside it - the obvious place to take hold of - belonged to nothing. The heading fills the row now, and the handler steps aside for the buttons in it, for the drag handle when there is one, and for the half that edits.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6ad79562955d1993d703e9d57885ae5db20088f">A board is renamed by clicking its name, and the pencil beside it is gone</a>. Thanks to xet7.</summary>The board's name in the first header bar had a pencil next to it. Two targets for one job, and the smaller of the two was the one that did it.
The name itself now opens the rename popup - the SAME boardChangeTitlePopup
that the pencil opened, with the title and the description in it, so only what
you click to get there changed. It is opened with the board as its data context,
because that bar's context is the page rather than the board. Board admins only;
everybody else sees a plain title, as before.
A board whose title is EMPTY renders no text at all, and an element with no content is zero pixels wide - so there would be nothing to aim at, and an empty title could never be given one. The clickable title carries a minimum width and height for exactly that.
</details>Card and minicard menus - the settings about a thing, in that thing's menu.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ec8ba5875357860ab65d00efc350b713c490c747">Subtasks Settings is in the menu of the subtask it is about</a>. Thanks to xet7.</summary>What subtasks DO on a board - whether they are shown at all, which board and list a new one is deposited in, and how a parent is named on a minicard - was in Board Settings only: open the sidebar, open the board menu, find Subtasks Settings. Somebody who wants to change it is usually looking at a subtask on an opened card at that moment.
It is behind a hamburger at the end of the Subtasks heading on an opened card - where the card and the minicard already keep theirs - and clicking it opens the same popup Board Settings did. Board Settings no longer has the entry: the settings are where subtasks are.
The heading IS the control that folds the section, so the hamburger's click has to stop there. Without that, opening the menu would close the thing it was opened from.
It is the SAME template in both places, with its own state, helpers and handlers, so neither place needs code of its own and the two lists cannot drift. That is what the move needed: a template included in two places cannot reach its parent's helpers, because a helper is looked up on the template it is written in.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/02025aa6c6dd504869595dc0ff7065579b24fa23">Show on Card and Show on Minicard do not print their own title again</a>. Thanks to xet7.</summary>The heading row named the columns of a ROW - Show on Card over the checkbox and Description over the setting's name - which is what they labelled, but not what they looked like. The rows flow into as many columns as the window fits, so the two headings sat above the FIRST of three or four columns and read as if they named those: Show on Card over one column of settings and Description over another, when both columns hold the same kind of row.
The first heading also said what the popup's own title says. Show on Card opened a popup titled Show on Card and repeated it a line below, and the same for Show on Minicard.
Both are gone, and the two-pixel rule under them with them - one more line across a popup that is a list of lines. A row is a checkbox and the name of a setting, which needs no heading over it. One template serves both popups, so both lose it together, and the sticky positioning that existed only to keep those headings in view while the rows scrolled goes with it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0204ae014d1b312355be89bba09717ffeb28d47">Show on Card and Show on Minicard lead the card's and the minicard's menus</a>. Thanks to xet7.</summary>Board Settings / Card Settings is a table of twenty-four settings with two columns - what a card shows, and what a MINICARD shows - and it lived in one place only: the right sidebar, four clicks from the card whose fields it decides.
It is now also the first entry of the two menus it is about. The hamburger on an opened card offers Show on Card, the hamburger on a card on the board offers Show on Minicard, each followed by a rule like every other group in those menus. Both open the SAME popup with the other column hidden by a class, so there is still one list of settings and one set of handlers: a setting added to the table appears in all three places by itself, and none of them can drift from the others.
Both are wide, and lay their rows out in columns. Two dozen settings in one
column is a list nobody sees the end of - "is Description on?" was somewhere
below the fold. With one of the two checkbox columns hidden each row is half as
wide as it was, so the popup is min(90vw, 900px) and the rows flow into as
many columns as fit: one on a narrow window, three or four on a wide one, with
the headings still spanning the width because they name the columns of a ROW
rather than of the grid.
The permission is the one Board Settings already used - a board admin - asked
in the menu and again where it acts. The one menu is opened from two places and
its data context is the card either way, so the opener says which hamburger it
was in client/lib/cardMenuSource.js: a
module-level reactive value, not a field on the card document, which is gone the
next time Blaze re-renders the popup with a fresh copy of it.
It was in the right sidebar under the People and Organizations tabs, which is not where anything else about a minicard is. It is a row of the same table now, directly under Labels, named "Labels text" with the Labels icon and the Description Text icon in front of it - because it is about what those labels look like on a minicard: coloured words, or coloured bars with the words left out.
It is ticked when the text is SHOWN, which is what a board does out of the box. The stored flag is a "hidden" one, so reading it straight through would have left the box unticked for the default behaviour, which reads as a broken checkbox.
It is the one PERSONAL row of a board-wide table - the user's own profile, or this browser when nobody is logged in - so it appears in the minicard's view and nowhere else, and it is offered to EVERYBODY. A reader who is not a board admin gets that row alone rather than a table of checkboxes the server would refuse.
The move found a bug. The setting was written out three times, and the
minicard's own copy only ever wrote localStorage: a logged-in user toggling it
set something nothing reads, because for a user the value is read from the
profile. One module reads and writes it now
(client/lib/minicardLabelText.js), so the
two halves cannot disagree again.
The board's LIST of custom fields - where a field is created, renamed or deleted - was in Board Settings only, and the picker for which of those fields are on THIS card was down among Voting, Planning Poker and Spent time. Two halves of one subject, three groups apart.
They are one group now, under Watch and its rule: Custom Fields first, because a field has to exist before a card can be given it, then Edit custom fields, then a rule like every other group in that menu. A board admin's, the same as in Board Settings.
Card Settings is gone from Board Settings. Every setting in that table is in the menu of the thing it is about now, so a board-wide entry as well would be a third way to one list - and the way that is furthest from what it changes. The table itself is unchanged; the two menus include it.
Everything about custom fields is on the section that shows them. It was spread over three places: Board Settings had the board's LIST of fields behind a right-sidebar view, the card menu had an entry for that list and another for the picker of which fields are on this card, and the picker had a cog that jumped back to the sidebar - closing the menu and the card pane on the way.
One place now: the hamburger at the end of the card's own Custom Fields heading. It opens every field the board has, ticked when it is on THIS card, with a pencil each and, under a rule, Add custom field. Edit and Add open in the same pop-over on top of that list, so the back arrow returns to it and the card stays open behind; they are the board's own forms, not second copies of them. The card menu's entry and the Board Settings row are both gone, and so is the wrapper popup that nothing opened any more.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9d9b8ae93b3876845510d28c8905e1ae63c6c03f">Show list on minicard became the List title row of Show on Minicard</a>. Thanks to xet7.</summary>It was a line in the card's menu that read "Show list on minicard", or "Hide list on minicard" once it was on - a menu entry that changes its own name is the shape a checkbox has, drawn as a sentence. It is a checkbox now, the FIRST row of Show on Minicard, above Received, named List title and unchecked by default, which is what the field itself has always been.
It shows the name of the LIST the card is in on its minicard, which is worth having where a card is read away from the column it lives in: a swimlane, search results, My Cards. The board-wide "Show lists" row further down the same table turns that on for EVERY card; this one is for a single card that needs it, and either is enough for the name to show.
It belongs to the card rather than to the board, so it is drawn only in the minicard's own view - Board Settings has no card to act on - and only for somebody who may change that card, which is the permission the menu entry had. The card is passed into the popup and re-read from the collection when it is toggled: a minicard's menu is opened from the board, where there is no "current card" to fall back on.
On the minicard itself the name sits on its own line under the badges. It was
landing BESIDE them: the badge row is float: inline-start, so the line after
it shared its row and read as one more badge rather than as the line it is.
Two popups of the same menu answered one question. Change Background Image asked for a URL; Board backgrounds listed the images uploaded to this board and carried the upload button. So the popup that reads as the place to set a background had half of the ways to set one, and the other half was behind a different entry.
The upload is under the URL field now - a picture from the web and a picture from this machine, read together. Board backgrounds keeps what it is named for: the images already there, to set active, download or delete. The uploader moved as its own template, once, rather than being copied.
In the same popup, Unset was five blank lines and a rule below the Save
it belongs beside. They are one row, Save first, sharing the width. Unset is
type="button": inside that form a button with no type is a SUBMIT button, so
beside Save it would have saved the URL it is meant to clear.
The two sections at the top of the right sidebar could not be folded at all. The People / Organizations / Teams / Domains tabs with their avatars, and every label of the board, were always open and pushed everything under them down the panel.
Both have a caret in front of their heading now, and the heading is the button
The choice is the reader's and lasts the session, not the board's: neither
decides anything on the server, so folding them for yourself must not fold them
for everybody. Activities is the exception and stays as it is - its caret
writes board.showActivities, which also decides what the publication sends.
A caret leads the group, at its leading edge - left of the lock in a left-to-right page, right of it in Arabic, because its direction comes from the same rule every other caret uses. Folded, the seven controls after it are gone
The buttons a board's own Rules put in that bar are NOT folded with them: somebody added those to this board on purpose, so the standard controls fold around them. It is named by the words the app already has - Collapse and Uncollapse - so no key was added to 147 language files to say them again, and it answers Enter and Space like the button it says it is.
All three folds share one store, one helper and one class
(client/lib/foldState.js): a caret in the header
and a caret in the sidebar pointing different ways in one language is the bug
that avoids.
The heading read "Activities" and beside it sat an eye and the words "Show
activities" - one setting with two controls and its own name said twice. It is a
caret in front of the heading now, and the heading is the button: click it and
the activities appear under it, click it again and they are gone. That is the
same control every collapsible section of an opened card has, from the same
function (client/lib/sectionCaret.js), so the
caret cannot point one way on a card and another in the sidebar of the same
language - it points down when open, and toward the text when closed, which is
right in English and left in Arabic. It carries role="button" and a tabindex,
so Enter and Space do what a click does.
and fixes the following bugs:
The release workflow - what reaches the Release page.
<details> <summary><a href="https://github.com/wekan/wekan/commit/38c6ee29fd7487630ed18afe001e59ae5b8440f2">Three snaps built, published, and then fell off the Release: one file listed twice</a>. Thanks to xet7.</summary>v10.91's release run failed in four jobs, and three of them - s390x, ppc64el and riscv64 - had done all the work: each snap BUILT on Launchpad and each was published to the Snap Store. What failed was attaching it to the GitHub Release:
HTTP 404: Not Found (https://uploads.github.com/repos/wekan/wekan/releases/370103352/assets?label=&name=wekan_10.91_s390x.snap)
The step collected the file with two patterns - wekan_${VERSION}_<arch>*.snap
and *_<arch>.snap - and a snap called wekan_10.91_s390x.snap matches BOTH,
so the same path was passed to gh release upload --clobber twice. Asked to
attach one name twice, it deletes the asset it has just uploaded and then 404s
on it. The two other places in the same job that build this list were already
deduplicated; this one was missed.
It is deduplicated now, and the attach is confirmed from the other side the way the native snap job already did it: read the release's assets back and fail if this snap is not among them. An upload that reports success and leaves nothing behind is the failure nobody notices until somebody's download 404s - and here every job that lost an architecture had already said the snap was built and published.
The fourth failure, armhf, is not this: snapcraft died with
SSLEOFError while downloading the build log from Launchpad, on all three
attempts, and produced no snap. That one is Launchpad's side of the wire.
v10.92's release job failed before running a line of its script, and with it every job that needs the release - snap, docker, AppImage, the Windows and macOS bundles - so nothing published at all:
##[error]An error occurred trying to start process '/usr/bin/bash' with
working directory '/home/runner/work/wekan/wekan'. Argument list too long
Nothing in the step was wrong. The notes are the whole newest CHANGELOG
section - prepare measured them at 172,458 characters - and they reached
the step as env: CHANGELOG:. Linux caps a SINGLE argv/envp string at
MAX_ARG_STRLEN, 128 KiB, so execve refused to start the shell. The amd64
and arm64 bundles had already built and verified; only the publishing died.
The environment was itself the fix for the PREVIOUS failure. Interpolated
inline as ${{ }}, the notes become part of the shell SOURCE, so a backtick in
any code span runs as a command: that is how v10.59 published nothing and
printed Incorrect: command not found. One shape is unsafe and the other does
not scale, and this release was the first big enough to find the second wall.
So the notes stop being a shell value at all. releases/release-notes.sh prints
the CHANGELOG section for a version, reading the file itself and taking its
arguments from the environment inside a QUOTED heredoc, and each job appends its
stdout to release-notes.md. Only file PATHS are passed around, which has no
size limit and leaves the text as data that no shell ever parses.
prepare keeps the validation - a missing section is still cheap to fix there -
but no longer publishes the text as a job output, because an output nobody can
safely consume is a trap for the next person to find. The release job and the
notes-rewrite job check out ref: main, the same ref prepare read.
The guard test is rewritten around the new shape: no changelog job output, no
CHANGELOG in an env:, no interpolation into a script, and the extraction
script is what all three consumers run. It passed throughout this failure,
because it only knew about the injection.
Starting up - what a browser sees while WeKan cannot yet serve.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b4499b0ddc5d8059b8feac1802e1417d3ee1c8ef">A container waiting for its database says so, instead of timing out</a>. Thanks to Alishara and xet7.</summary>"We upgraded to 10.91 ... Gateway timeout appears" - #6595, from the same reporter as #6592, whose fix shipped in 10.91. It could not reach them: that fix is the SNAP's, and this is a container.
WeKan does not open its web port until the database answers, and in a container nothing else was listening while it waited - so a reverse proxy in front returned a gateway timeout, and that is the same symptom for two completely different faults: WeKan is broken, or the database has not come up yet.
The entrypoint now asks whether the database answers (one ping, with the
driver already in the bundle), and while it does not it serves the same bridge
page the recovery case uses, saying WeKan is waiting for its database and
where to look. What is bounded is the PAGE, not the wait: a database can take
minutes to come up after an update, and giving up on it would be worse than
waiting - so when the window ends the page stops, WeKan starts, and WeKan keeps
waiting exactly as it did before. WEKAN_DB_WAIT_PAGE=false turns it off.
Performance - what the database is asked, and what it has to walk.
<details> <summary><a href="https://github.com/wekan/wekan/commit/569dd4af541b797c8df763aef38cf0db820bf9ea">Cards, activities, comments and checklists are indexed at last</a>. Thanks to xet7.</summary>Reported by email against 10.91: "Still slow on loading cards. Test server with me as only user." One user is the part that says what it is NOT - with nobody else on the server there is no contention, no queue and no lock, so the time is going into the queries themselves.
It was. cards, activities, cardComments, checklists and
checklistItems had no index at all - everything a board draws and everything
an opened card pulls in. "The cards of this list", "the comments of this card",
"the newest activities of this board" each walked the whole collection. That is
invisible on a demo board and expensive on a real one, and worse on FerretDB,
whose SQLite backend has to walk the same documents.
The activities publication even explains that it keeps its selector flat "so both push down to FerretDB v1 (SQLite)'s index instead of forcing a full-collection scan" - and there was no index for it to push down to.
Each new index matches a selector the app really makes WITH the sort it really
uses, so it serves both the filter and the order: a filter-only index still
leaves an in-memory sort of everything it matched, which on a board with a year
of history is the slow half. They are created through the same idempotent
ensureIndex every other index here uses, so a restart does not rebuild them
and a backend that refuses one logs it instead of stopping the server.
Attachments and the snap's databases - what can be read, and what cannot.
<details> <summary><a href="https://github.com/wekan/wekan/commit/146a5184d90028b3ccf917069b3007ef520fcb59">Moving attachments out of CollectionFS no longer deletes a binary two records share</a>. Thanks to sbruckmueller and xet7.</summary>Moving from MongoDB CollectionFS to File-System stopped on some attachments
with FileNotFound: file 66336fc372e64200010f4832 was not found, and the
reporter had it exactly: it is the identical files.
A CollectionFS filerecord points at its binary by copies.<coll>.key, the id of
a file in the cfs_gridfs.<coll> bucket, and TWO filerecords can carry the SAME
key - the same file attached twice, or a board copied with its attachments. The
migration deleted the binary as soon as it had moved the FIRST of them, so the
second read a file that was no longer there. The binary now goes only when no
other filerecord still names it, and when the question cannot be asked the
binary stays: a file left behind can be removed later, an attachment deleted out
from under another record cannot be brought back.
A binary that really is missing - metadata restored without the chunks - is no longer a MongoDB stack trace naming a GridFS id. It names the attachment, says where it was looked for, and counts as SKIPPED rather than failed, because there was nothing to move. The Admin Panel's summary line shows how many were skipped and how many failed, so a run that leaves attachments behind cannot look like a run that moved everything.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ff21ac962c671bb9cf18a3c07db9ccf7877d30c">A snap whose MongoDB files no reader can open stops comparing them with FerretDB</a>. Thanks to mueschel and xet7.</summary>An instance whose MongoDB data was written by a MongoDB older than any mongod
the snap carries printed this at every start, before the site came up:
BOTH databases have been written to since the migration and then [autopick] reading both databases to see which one holds the work ....
mongodb_has_data answers "are the files there", not "can anything here open
them", and on that instance those are different answers - mongod 7, 5.0 and
4.2 each refused the files in turn. So the comparison ran on a copy that cannot
be served, and it is not a cheap question: it starts a mongod that cannot open
the data, and then a SECOND FerretDB against the SQLite directory the running
one already holds.
migration-control has already tried every reader in the snap and left
.mongodb-data-too-old behind when none could open the files. With that marker
present the MongoDB copy is no longer a candidate, and WeKan serves the FerretDB
that has the data. Nothing is deleted and the marker stays: it is true, and it
is right again on a snap that can read those files.
snap run wekan.database-compare answered MongoDB: <unreadable> and
FerretDB: <unreadable> on a live instance whose wekan.sqlite was 85 MB and
whose site was up.
FerretDB was unreadable BECAUSE it was up: SQLite has one writer, so the second FerretDB the comparison started against the same directory did not get the data. The tool then reported the database it had been talking to all along as unreadable, and refused to choose.
The live one is asked first now - evidence only counts and sorts - and a
second copy is started only for a database that is not running. Both speak the
MongoDB wire protocol on the same port, so which one is answering is asked
rather than assumed: FerretDB names itself in buildInfo and a mongod does
not. Reading one as the other would be a wrong answer given with confidence,
which is worse than "unreadable".
The page sidebar - the controls of a page that has no sidebar of its own.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9ef05970d3cb20f9babbcb03dd31cc58f598fe11">The controls in the page sidebar are rows, not three links run together</a>. Thanks to xet7.</summary>Rules, My Cards, Due Cards and Global Search each keep their controls in the
shared page sidebar, and each draws them as .board-header-btn - which is what
they were when they lived in that page's own second header bar.
Every rule for that class is scoped to #header-quick-access, so inside the
sidebar they had NO styling at all. On the Rules page that is three bare links
jammed onto one line - "← Board 🔀 Workflow view ⇄ Import / export" - with the
icons run into the words and nothing to click but the text itself.
They are rows now: one per line, the width of the panel, with the icon in a fixed column so every label starts at the same x, and a hover to click against - the shape the board sidebar's own rows have. Both sidebars built on that shell get it, All Boards' as well as the page one.
</details>Minicards - what a click on one does.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f525b391a649e7b9cb2c683a3d3f622ec9a385be">The stickers popup is wide enough to see the stickers</a>. Thanks to xet7.</summary>A hundred and fifty icons, eight to a row, in a popup 380 pixels wide: a column taller than the screen, so choosing a sticker meant scrolling past most of them.
It is as wide as the colour pickers now - min(90vw, 720px) - and the picker
is a grid that fits as many columns as the width allows instead of stopping at
eight. The clamp that places a popup knows the new width too, or a popup opened
near the right edge would have landed half off screen.
Clicking a label opened the card's labels popup AND the card details behind it: the click reached the minicard too, so one click did two things and the one nobody asked for was underneath the one they did.
It stops at the label now. A click in the labels AREA that is not on a label is
still the card's, as before - and which label was clicked is read from the
EVENT rather than from :hover, which answers about the pointer and on a touch
screen can still be true for whatever was tapped last.
Card details - the card as it is opened and edited.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aba09a087935423d8ec209cb67ff5d956b3e336d">Custom Fields is reachable on a card that has none, which is where it is needed</a>. Thanks to xet7.</summary>Everything about custom fields moved to the hamburger at the end of the card's own Custom Fields heading - the picker of which fields are on this card, a pencil each, and Add custom field - and the card menu's entry, the Board Settings row and the wrapper popup went with it. But the heading itself was drawn only when the card already HAD a custom field on it.
So the one way in existed only once you were already through it. A card with no custom field had no heading and therefore no hamburger; a board that had never used them had no way in anywhere, because the sidebar view that still holds the create and edit forms is opened by nothing. It is invisible on any board that already has a field - which is every board a developer tests on - and the browser tests found it on a seeded board that had none.
The heading is drawn for every card its reader may write to now, the same condition the rest of the editable card uses. The FIELDS below it still come from the card's own values, so a card with none shows an empty section rather than a phantom row per board definition, and a reader who may not write sees no heading at all.
The anchor that used to open the old popup went too: the move took away its
label text and left the tag, so it rendered nothing, could not be clicked, and
still had two handlers bound to it. tests/customFieldsSectionMenu.test.cjs
gains the two checks that would have caught this - the heading is gated on who
may write rather than on what the card already has, and no empty anchor or
orphaned handler is left behind.
Two things wrong with the popup the Custom Fields heading opens.
A new field made from it never appeared. Add custom field sits OUTSIDE the
list of fields, so its data context is the popup's own - which is the CARD - and
the create and edit forms are one form that decided which it was doing by
whether its context had an _id. A card has one, so creating a field from a
card ran the UPDATE branch against a custom field whose id was a card's: no such
document, nothing written, and nothing said so. The same form works from the
board sidebar, where the context has no _id, which is why this only ever
happened from a card.
Both halves are fixed. The popup hands the form an empty context, because a new
field is made from nothing. And the form no longer trusts a bare _id: it asks
whether that id NAMES a custom field, so a context arriving from anywhere cannot
make it update one that is not there.
The pencil that edits a field sat on a line of its own under the field's name. Every other pop-over list has ONE anchor per row, so the row is a block and the anchor fills it - and two anchors in a block stack. This list has two by design, the name with the checkbox that puts the field on this card and the pencil that edits the field itself, so the row is the flex container now and the name takes the space the pencil does not. Scoped to this popup, so no other list moves.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/47099b8baadc0bdfcefe5006bf16ed8e7e7ca565">Ten popups had no header, and so no close button</a>. Thanks to xet7.</summary>A pop-over draws its header from its title, and with no title it renders as
no-title: no header, no X, no back arrow - Escape or a click away are the only
ways out. Noticed on the question "Are you sure you want to delete this
background image?", which is the worst place for it.
Ten of them were in that state, and each is titled the way the other 151
already were: a <name>Popup-title key. Delete Background Image, Delete
Duplicate Lists, Delete Account, Add Domain, Remove Domain, Map to existing
user, Export swimlane, Export list, Export checklist, and the member popup.
They were added to EVERY language file at the same position, as the English placeholder a pull would produce - the files are one key order, and a key inserted in some and appended in others makes every later diff unreadable.
The other mechanism, Popup.open(name, { titleKey }), is not a second way but
the same one pointed at a phrase the app HAS - "Custom Fields", "Sort Boards",
"Show on Card" - so a word already translated 147 times is not copied into a
new key that would start as English in all of them.
tests/popupTitles.test.cjs walks every popup
template and fails if one resolves no title at all, so the next one cannot ship
without a header.
"Upload background image" picked a file and then nothing arrived. Two fields of an upload's config are not optional in the way they look, and the board background's config - written by hand - had neither:
fileId, and the same id copied into meta.fileId. Attachments'
namingFunction is what decides the name a file is STORED under, and on the
client it reads that id out of meta and deletes it. Without it the name is
undefined.transport. HTTP everywhere, because Meteor-Files' default DDP floods
the WebSocket and makes Safari reconnect at ~95%; DDP on Sandstorm, whose
http-bridge strips the x-* upload headers so every chunk comes back "Can't
continue upload, session expired" [408].Both come from one builder now, in
attachmentUploadConfig.js, used by the
card's file picker, the card's pasted image and the board background alike - so
the next uploader gets them by asking for a config rather than by knowing two
things nothing would have told it.
One popup answers the whole question now - the URL, the upload, and the pictures uploaded to this board - and Board Settings / Board backgrounds is gone with the popup it opened: a second entry to a list that is already on screen is a second place to look for one thing. Clicking a PICTURE, or its name, puts that one behind the board; it used to be a 14px check icon in the row under an 80px picture of the thing it applies, so the obvious click did nothing. Download and delete stay in that row, being the two things a picture cannot say.
Three things around the upload made the failure impossible to read, and are
fixed with it. insertAsync can reject BEFORE there is an uploader to listen
to, and that rejection went nowhere: the spinner stopped, no message appeared,
and the picture simply never turned up. It is caught and shown now. Each
picture is listed with its NAME, because two photos are the same picture at 80
pixels and nothing said which one had arrived. And a finished upload puts
itself behind the board: "add background image" is asked for by somebody who
wants that picture there, and an upload that only lands in a list, with the
board unchanged, reads as one that did not work.
getInitials() ended with this.username[0], so a user document that arrived
without a username threw a TypeError - from a Blaze helper, mid-render. Two
helpers call it, so each avatar threw twice, and the second throw left the SVG's
viewBox half-written (0 0 15), which Firefox refuses outright: the avatar
drew nothing and took the rest of that render pass with it.
It is total now: initials, then a fullname, then the first letter of a username if there is one, then an empty string - a blank circle rather than a broken page. A fullname of only spaces used to spell the literal word "undefined" in the circle, because an empty word still contributed its missing first letter; empty words are skipped.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/443e65b6d0eb2f6b5131d56f4659a4510d1a3039">The Date Format menu is back: two collapse mechanisms were folding one field</a>. Thanks to xet7.</summary>The dropdown that chooses a card's date format disappeared, and nothing brought it back - the Received, Start, Due and End dates under the same heading were there, but the selector above them was gone.
Two things were folding it. #1591 gave every FIELD of an opened card a caret on its own title and hid everything after that title when it was clicked, storing the fold per card in the user's profile. The section carets that replaced it put ONE handle on each section's heading - and that heading is drawn on the first field of the section, so a click on it was also a click on a field title. The old handler ran too, folded that field, and stored it. Reopening the section brought the dates back but not the field the heading sits on, and the per-field caret had been suppressed inside a group, so there was nothing left to open it: the Date Format select was hidden for good.
The #1591 half is gone - the handler, the caret it drew, the rule that hid the siblings, and the per-card state it re-applied on render. Every field lives in a section now, and the section's heading is the only handle. The same per-user store still belongs to the CHECKLISTS, which key their own entries by checklist id and are untouched; that is why the store itself stays.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7a75779b2cba58de149b99f308360b9944a50dd3">What the card restructuring nearly took with it, and a Copy card link button that can be read</a>. Thanks to xet7.</summary>Moving eleven sections with a script is how markup disappears without anything failing: the file still compiles, the tests still pass, and a card is quietly missing a row. Comparing the card against its state before the section work turned up four things, three of them lost:
+cardButtons) - a slice used it as a boundary and
swallowed it;+cardCustomField renders the value and nothing
else, so a card with three custom fields showed three values with nothing to
say what they were;All four now have a test of their own, because each was invisible to everything else.
The Copy card link to clipboard button carried only .btn, so it fell back
to the plain grey button whose dark label is nearly unreadable on a dark theme.
It is a .primary now - the board's accent with white text - named in the same
rules as the other themed buttons rather than given a copy of them.
Grouping the fields gave each group a header LINE of its own, and for a group named after its first field that line said the same word twice, one above the other: "Labels" and then "Labels, Stickers, Location".
There is one row of titles now, with the caret at the start of it: Labels Stickers Location, Members Assignee Creator, and their + buttons on the row below. The caret belongs to the first field's title rather than to a header above it, and everything it folds - that field's own content and every field beside it - is behind the same switch, so a collapsed group is exactly one line.
The rule moved with it, from inside a field to the group, where it spans the card. The Checklists icon is a plain check again rather than a check in a box, which is what it was before it became a section.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/050df89f06ab07a17899a4b032beeae04676c270">Requested By and Assigned By are set with a + like Members, and survive an import</a>. Thanks to xet7.</summary>Requested By and Assigned By were set by clicking the word "Add". They have the round + the two fields beside them use now, opening the same editor the text does.
Checking where else those two live turned up a round trip that lost them, which is the worst shape this kind of bug takes - the export file looks complete and the import reports success. They were exported by the card PDF, the card Excel and the board CSV, and imported by NOTHING: a card exported and imported back came home having forgotten who asked for it and who assigned it. Both importers put them back now, our own JSON and .zip and the per-menu one.
The board's Excel TABLE did not export them at all, so that one gained two columns - header and value together, since a header without its value shifts every column after it.
And from other trackers, where the same idea has another name: Jira's Reporter is who asked for the work, and an issue's author on GitHub, Gitea, Forgejo and GitLab is the same thing. Both arrive as Requested By, as free text, so they survive an import from a tracker nobody on this board has an account on. A source with no such field - Trello, CSV - gains nothing, which a negative test pins.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/050df89f06ab07a17899a4b032beeae04676c270">Assignee and Creator sit beside Members, with their + buttons on one line</a>. Thanks to xet7.</summary>Grouping the fields left two things crooked. Members, Assignee and Creator were each a full-width row, so Assignee and Creator fell to the line below Members - the class that made a FIELD full width was from before the groups, and the group is the full-width row now. And a field whose title had been taken over by the group header had its + on the first line, level with its neighbours' titles, instead of on the second line with theirs.
Every field in a group keeps its own title again. The group's header names the family and folds it; the field's title names the field, and its content - the +, the avatars - starts on the line under it, level all the way across.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/83147dcc3e357ccfce9a1702568484f2d70bcb3d">A card's fields fold in GROUPS, from one caret each, and Members reads Members, Assignee, Creator</a>. Thanks to xet7.</summary>The caret beside Labels folded the Labels field and left Stickers and
Location sitting under it, and every field had a caret of its own on the right -
#1591 draws one on each
.card-details-item - so a row could have two handles saying the same thing in
two sizes.
The fields belong to FAMILIES, and the families are what fold now:
One caret per group, on its title, at the reading direction's start. The fields inside a group have none: #1591's per-item caret is turned off there, and its title is no longer a handle. Outside a group it is untouched, so nothing that folded before stopped folding. A collapsed group is one line - the caret, the icon and the name - because everything else is inside the fold.
Members reads Members, Assignee, Creator, which is the order it was asked for. The loose rule the layout drew above the users block is gone with it: the group's own rule is above the whole group, so there is one line there instead of two.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/365cfdc4df5187e9cbfc8f83acab39df755a19c7">The rules between card sections are the page's own line, and there is one where there should be</a>. Thanks to xet7.</summary>Four small things about those separators, all of them visible at a glance and none of them visible in the source.
The rule drew a border of its own, darker than the line the card already had
above Creator. It is that same lighter hr now - the section rule sets only its
spacing, so there is one line style on the card rather than two.
Labels had a rule above it. It is the first section: there is nothing above it to be separated from. Members had one too, immediately under the rule the layout already draws above Creator - two lines with a heading between them. Neither draws one now, and every other section still does.
And the rule above Activities was to the LEFT of the heading rather than
above it, because .activity-title is display: flex and a rule inside it is a
flex item. The heading sits outside that row now.
Two things the collapsible sections got wrong the first time, both visible the moment a card was opened.
Short rules everywhere. .card-details-items is a wrapping flex row, and
the
rule was drawn INSIDE a section - so it was as wide as that box, which is a stub
of a line beside a heading rather than a separator across the card. The six
sections that live in that row are now rows of their own, so the rule in them
spans the card, and the items that were laid out side by side on purpose -
Stickers, Location, the four dates, Creator, Assignees - still share rows as
they
did.
Two headings for one section. The Checklists and Subtasks templates draw their own title, and the new section header drew it again, so a card showed each of those names twice, one above the other. The templates keep what belongs to the LIST - add a checklist, add a subtask - and the section header is the only thing that names the section.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/384f03b359a47592e08a16060675e330319ec36c">Every section of an opened card collapses from its own caret, with one rule between sections</a>. Thanks to xet7.</summary>A card had ONE caret, in its header, which collapsed the whole card - and Activities had an eye beside its heading. Every other section was a heading with no control at all, and the rules between them were written by hand wherever somebody remembered one.
Labels, Date Format, Members, Dependencies, Sort, Custom Fields, Description, Checklists, Subtasks, Attachments, Comments and Activities each carry a caret that collapses that section, and one rule above it. Both come from ONE template, so eleven sections cannot end up with ten separators and nine carets. The heading is a real button - it announces itself as one and Enter or Space works it - rather than a click target only a mouse can find. The caret points DOWN when a section is open and toward the way the reader reads when it is closed: right in English, left in Arabic, Hebrew and Persian.
Activities starts collapsed and everything else open, because a card is opened to read the card and its history is the thing you go looking for.
The eye is gone, and that needed one piece of care: it was NOT the same control
as the caret. showActivities: false - its default - subscribed to
activityType: 'addComment' only, so the eye chose between "comments" and "the
whole history" as much as it showed and hid. Removing it without more would have
quietly turned an opened Activities section into a comments-only list. So a card
section that is OPEN now asks for the whole history, and one that is closed
subscribes to nothing at all - which is the cheaper half of what the flag was
for, without a second control to keep in step. A card that had the flag
explicitly set to false keeps it.
Custom Fields is one section with one caret however many fields a card has: its header was briefly inside the loop that draws them, which would have been six sections sharing a caret on a card with six fields.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0c7b3e8df36d185d97a7cbe442f0eba76ecd897e">A dependency's card title wraps instead of being cut off at the pane edge</a>. Thanks to xet7.</summary>In an opened card's Dependencies section each row is an icon, the linked card's title and its controls, and the title was held on one line with an ellipsis. The card details pane is narrow and a card title is a sentence, so most of a real title - everything to the right of the coloured icon - could not be read at all.
It wraps now, and the row grows as tall as it needs to. Two properties, and the
second is the one that is easy to miss: white-space: normal alone would have
changed nothing, because a flex item's default min-width: auto refuses to
shrink below its content, so the text overflowed the row instead of wrapping
inside it. min-width: 0 is what lets it. A title that is one long token - a
URL, an id - breaks inside itself rather than pushing the type, colour and
remove controls off the row, and the icon and those controls now sit beside the
FIRST line instead of floating halfway down a three-line block.
Clicking a card's title opens the title editor in place of the card header - and the header is where the card's own close X lives, so the X disappeared the moment you started editing and Save became the only visible way out. Escape still worked; nothing on the screen said so.
The editor draws the X itself now, reusing the header button's own class, so it
is the same size in the same place and does not jump as the editor opens and
closes. That class floats to inline-end, which is the right in English and the
left in Arabic, Hebrew and Persian without a second rule - so the mirroring
needs no direction branch and must not grow one.
The same one-line omission was in the Requested by and Assigned by editors right below it: the close anchor was there, its icon was not, so the click target existed and was invisible. They have their X now too.
</details>Card export - what it says, and in whose language, format and time zone.
<details> <summary><a href="https://github.com/wekan/wekan/commit/698e980e27522b53f60ad6fce347727cb11eb017">A card's PDF and Excel exports carry the same fields, under the same translated labels</a>. Thanks to Heart1010 and xet7.</summary>#6586 was reopened for what the
first fix left: the PDF export of a card had grown separately from the Excel
export of the same card, and every point the reporter came back with was a
symptom of that. The labels were hard-coded English - "Assignee, Labels, due,...
these titels should be in the user set language". They were not even consistent
with each other: the card export wrote Due: , the board export wrote due
with no colon and a lowercase letter. And a card carries more than either export
printed - "I think all those other things we set in a card should be also
present
in the pdf? Location, Voting, Checklists, Subtasks, Custom Fields, Attachments,
Comments...".
Two exports of one card that disagree about what is on it are one bug reported
twice, so the fix is to stop them being two things. Both now carry the same
fields, in the same order, under the same i18n keys, in the language the request
carries: title, labels, creator, assignees, members, board, swimlane, list, card
number, requested by, assigned by, the six dates, spent time, description,
custom
fields, checklists, subtasks, comments, attachments, voting and Planning Poker.
The three sections neither export had - custom fields, voting and
poker - are new on both sides, selectable like the others in the Excel
export's field checkboxes, and appended to that list rather than inserted, so a
saved ?fields= link still asks for what it always asked for.
Inside the PDF exporters one field() helper writes every Label: value, which
is what makes the board export's "due" and the card export's "Due:" impossible
to
have at once again. Every label carries its English text as the fallback, so a
language that has not translated a key shows the word rather than the key.
"The marked due date - here the time is not in the user set timezone (-2h wrong for Europe/Berlin)." It was worse than -2h: the PDF export printed UTC and the Excel export printed whatever zone the server process was started in, so the same card could come out with two different times on it and neither was the reader's.
Dates are stored in UTC, and a WeKan profile carries no time zone at all, so the
only place the reader's zone exists is the browser. Every export link now sends
it - Intl.DateTimeFormat().resolvedOptions().timeZone, the IANA name the
server's own Intl wants back - together with the date format the opened card
is showing, which for a reader who is not logged in lives in localStorage
where no server-side lookup can reach it. An export that printed 2026-08-14
for
a card showing 14-08-2026 was the same card in two formats.
Both exports format through one helper, formatDateByUserPreference, which
gained an optional zone; without one it still renders in the process's own zone,
which is what every client-side caller - the card view itself - wants. A
server-built export that is given no zone renders UTC and SAYS UTC, rather than
printing the server's and looking like the reader's. The route accepts only the
three formats that helper understands, and a zone name is length-bounded: they
are request parameters, not free text. 12:00Z now prints as 14:00 for
Europe/Berlin, as 00:00 on the 15th for Pacific/Auckland, and a zone the
runtime does not know falls back instead of failing the download.
"Would it make sense to support markdown formated text in description? (so it
gets transformed correct in the pdf output with bold, underline,....)" The first
fix removed the syntax and kept the words, because **bold** in a PDF is four
stray asterisks; this is the other half.
A description is now cut into RUNS - **bold** into a bold run, *italic* into
an italic one, ***both*** into both, nested emphasis keeping both - and each
is
drawn in the matching Courier face: Courier, Courier-Bold, Courier-Oblique,
Courier-BoldOblique, all base-14, so no font binary is embedded. Nothing is
measured to place them: consecutive Tj operators continue at the current text
position, so a font switch between two of them lands the next run exactly where
the last ended, whatever the glyph widths are. Wrapping counts characters across
the whole line, indent included, so an emphasised word does not push a line past
the page.
What has no face is not invented: ~~strikethrough~~ and `code` keep
their
words and lose their markers, because a base-14 Type1 font has no strike and
there is no fifth Courier to give code. Block markdown is still flattened either
way - a heading loses its # and is drawn in the bold font, a bullet keeps one
shape, a fence keeps its code. An underscore inside a word stays an underscore:
file_name_here is an identifier, not three-quarters of an italic.
and has the following developer-facing changes:
Language loading - which of the 246 language files a visitor is sent.
<details> <summary><a href="https://github.com/wekan/wekan/commit/59c742e6c95b42529b53ec33d2f2d075108e78d9">The browser downloads one language, and a test says so</a>. Thanks to xet7.</summary>imports/i18n/data/ is 37 MB across 246 files, every one of them reachable from
the client. The only thing keeping them out of the initial bundle is that each
entry in imports/i18n/languages.js loads its file through
() => import('./data/<tag>.i18n.json') - a call Meteor's dynamic-import
package code-splits into a module fetched on demand - and that tap.js imports
exactly ONE of them statically, English, so the interface stays readable when
dynamic import is broken (#6503).
That was true when checked and nothing pinned it. A single
import data from './data/xx.i18n.json' added anywhere on the client would
quietly ship that language to every visitor, and nothing about the app would
look wrong - it would just be a heavier download, which no other test measures.
tests/i18nLazyLoading.test.cjs checks the six things that have to hold
together: every registered language has a dynamic loader, languages.js pulls
in no data itself, tap.js statically imports English and nothing else, the
loader is called once for the single resolved tag rather than mapped over the
registry, no client file bundles a language file, and dynamic-import is still
in .meteor/packages. A negative test proves the detector really sees a static
import, so the other checks cannot pass by failing to look.
Browser tests - the guards that drive a real browser, and what they say.
<details> <summary><a href="https://github.com/wekan/wekan/commit/acaaa3c03fa67e1130f694ac2dbbba3802daad3d">Three page objects still described the markup as it was before it moved</a>. Thanks to xet7.</summary>A run failed the same four tests in Chromium and in Firefox, and three of the four were the guard describing the OLD markup rather than the app being wrong.
editTitle waited for both js-card-title and js-open-inlined-form on ONE
element. The title splits now - the leading half opens the editor and the rest
of the heading drags the window - so the class is a DESCENDANT of the title, and
the old selector matched nothing and waited out its timeout.
The Activities heading in the board sidebar carries TWO icons since the caret
became shared with the card sections: the caret that says whether the section is
open, and the section’s own comment icon. A bare i.fa matches both, which
Playwright fails as a strict-mode violation rather than picking one. The spec
asks for the caret specifically now - the three directions caretClassFor can
produce - which is also a stronger assertion, since the caret is the part that
indicates state.
The fourth was not a guard at all: see the Custom Fields fix above.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7a08f081241336aedcd7a57da1678e8d104d8f53">Wait for the client bundle before calling a Meteor method</a>. Thanks to xet7.</summary>Four Firefox-only failures, all the same cause: Meteor is not defined, and in the fourth a count that came back as the string error: can’t access property "callAsync", window.Meteor is undefined. Chromium and WebKit passed all four.
Waiting for networkidle says the NETWORK went quiet, not that the client
bundle has finished executing. Under the three-browser parallel run against one
shared server, Firefox reached the evaluate with Meteor still undefined - the
bundle is large and had been fetched but not yet run.
helpers/auth.js has had waitForMeteor for exactly this since WebKit needed
it; these two specs simply never called it. It is idempotent and returns at once
when Meteor is already up, so it costs the browsers that were passing nothing.
Shared templates - one piece of markup, or one component, not many copies.
<details> <summary><a href="https://github.com/wekan/wekan/commit/9c288c3d4d8045cdd1d76b68b64fc91c414288c9">The date markup is written once instead of twenty-two times</a>. Thanks to xet7.</summary>Two shapes were copied across three files. The edit-a-date form - date,
time, Save, Delete - existed eight times, letter for letter: the card's
Received, Start, Due and End, a vote's end date, a planning poker's end date, a
date custom field, and a ninth datepicker template that nothing included and
no popup could open. The date badge - the coloured date on a card and on a
minicard - existed fourteen times.
The JavaScript was already shared: client/lib/datepicker.js holds the state
and the handlers, and each popup differs only in the field it stores. It was
only the markup, so a change to the form meant eight edits and a change to the
badge meant fourteen, with nothing to say so.
Each is one template now. They take what they draw as ARGUMENTS, because a
helper is looked up on the template it is written in and not on the one
including it - which is what lets one piece of markup serve them all while every
popup keeps its own state, its own click and its own name. cardDate.jade went
from 289 lines to 91.
The badge's baseClass is the trap this had to avoid: three of the fourteen -
the custom-field dates - were never .card-date and must not become one, so the
class each caller carried is passed in rather than baked into the shared markup.
Both pairs duplicated their whole component, not only their markup. The
selection dialog was 152 lines twice over and 145 of them were the same: the
four reactive selections, the four selects' helpers, the change handlers and the
sort-index maths. The seven lines that differed are what each does to a card
once the destination is known - move it, or copy it and move the copy - which is
one applyToCard passed to one registration now. The swimlane pair is the same
story with one difference instead of seven: the method called on Done.
The markup is one template each too, handed the popup's own instance as
dialog, because a helper is looked up on the template it is written in. What
the two copies really differed in was the ids their labels point at, so those
are passed in - and the title's id has to arrive as an id= attribute, since a
literal id cannot hold a mustache.
sidebarFilters.js lost 106 lines, sidebarFilters.jade 17 and
swimlanes.jade 10, and the scan for near-duplicate templates is at 7 pairs
from the 74 it started at. tests/sharedFormTemplates.test.cjs covers both.
The test harness - what a test run does before the tests.
<details> <summary><a href="https://github.com/wekan/wekan/commit/92650b782ec4ea427dc39f783b5e1a26869e395f">Reuse the test database only when it answers, not when the port is open</a>. Thanks to xet7.</summary>A run reported "WeKan tests FAILED" while the node suites, mocha and the import
regression had all passed. What failed was the test server, on its first query -
MongoTopologyClosedError: Topology is closed - because the harness had decided
to reuse a database that was not there: it asked whether the port was open, and
something else was holding it.
An open port is not a database. The check is a query now, so a port held by anything else means the harness starts its own rather than handing the server a socket that answers and then closes.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/590ccf3e81d173516281c86b12dc97bface9afcc">The test database moves off a port another program owns</a>. Thanks to xet7.</summary>The next run stopped where the previous one had died, and the port said why: 127.0.0.1:3001 on that machine is an "Omi Server" answering HTTP. A test database that cannot have the port it wants now takes the next free one and tells the rest of the run which it took, instead of failing at the first query against whatever was already listening.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/53c3123de5dc82f9c010792989adf55cc15bffb6">Three forms that were written ten times over are written once</a>. Thanks to xet7.</summary>Three more shapes were copied, and in each of them the JavaScript was already one piece while the markup was not - so a behaviour change was one edit and a markup change was four:
Each is one template now, and what it shows is passed in. The two pickers are
handed the dialog and read it from the template INSTANCE: inside each boards the data context is a board, so a helper reaching into the context for
it would find nothing there.
The events stay with the popups. An event inside an included template bubbles to the one that includes it, which is the one holding the state - that is what lets four popups do four different things with one form.
Two more went the same way, and those had their whole COMPONENT duplicated as
well: Move selection / Copy selection - 145 of the dialog's 152 lines were
identical, the other seven being what each does to a card once the destination
is known - and Move swimlane / Copy swimlane, where the only difference is
the method called on Done. Both are one registration now, taking that
difference as an argument, and sidebarFilters.js lost 106 lines.
cardDetails.jade lost 71 lines, boardHeader.jade 58, sidebarFilters.jade
17, listHeader.jade 10 and swimlanes.jade 10, and a scan for near-duplicate
templates went from 74 pairs to 7.
What is left of that scan is deliberately left: attachmentSettings and
storageSettings share a shape but only a third of their code, the two
Change Avatar popups differ in who they act on, and the three mini* templates
are eight lines each in the three folders they belong to - indirection would
cost more than the fifteen lines it saved.
and improves the translations:
Files written in another language - and the scan that found them.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f225b3245df2d29d77fb49f9744628f4d405c9de">Korean was Japanese and Georgian was Russian, in 1,143 strings between them</a>. Thanks to xet7.</summary>A missing translation shows English, which says plainly that nobody has done it
yet and invites the person who can. These files said something else: ko opened
a board labelled ボード and ka one labelled Доска - confident text in a
script the reader may not read at all, and the merge rules protect it forever,
because a Japanese word is not equal to the English source and so is never
treated as a placeholder.
Neither file was wholly wrong, which is why nobody had noticed: both had save
right and board in the wrong language. ko had 354 kana values and ka
789 Cyrillic ones, and they are Korean and Georgian now - the card details
pane, the Admin Panel, the rules engine, global search and the error strings.
Found by comparing each value's Unicode script
against the one the language is written in, which needs two exclusions to be
usable: the danda । is shared across the Indic scripts and so is not evidence,
and hanja in a Korean string is legitimate Korean.
The same fault, larger: hi held 1,204 values in Gujarati script and ta
held 1,637 in Telugu and Devanagari - two thirds of the whole Tamil file.
Both are translated in place
rather than blanked back to English, because blanking three thousand strings
trades one unreadable file for one empty one.
The search operators needed a decision, because they are the words a user TYPES
rather than labels a user reads. They are native words - operator-board is
बोर्ड in Hindi and பலகை in Tamil - and the instruction text names those same
words through its __operator_*__ placeholders, so the two can never drift
apart. Georgian was done the same way, and the choice is reversible in one file
if search syntax should stay ASCII.
Three values stay in Latin on purpose. excel-font is the font family handed to
the spreadsheet writer, ldap is a protocol and gridfs-storage a product; the
transliterations they carried were wrong in a way no reader could act on.
node releases/translations/wrong-script.mjs --count now reports zero
across all 246 files, and stays as the guard that catches the next file seeded
from a neighbour before it ships.
A guard that reports zero is only worth what it looked at. wrong-script.mjs
listed the script for a BASE tag - hi, ko, ja, el, ru - so hi-IN and
ko-KR were never checked at all, and both are full copies of what hi and
ko used to be: 1,204 values in Gujarati and 354 in Japanese kana, sitting
behind a green count for the whole time their parents were being fixed.
Every file whose tag reduces to a known base is now checked under that base's script, so a variant cannot hide behind its parent again. The two that surfaced are fixed from their corrected base, key by key, and only where the base actually has a translation.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4be57c2ad5756807b36a77fab16b38756e809cd3">Korean: 80 values per file that were Chinese or Japanese, not hanja</a>. Thanks to xet7.</summary>The scan excluded CJK entirely for a Korean file, on the correct grounds that hanja beside hangul is legitimate Korean. That exclusion was too wide: it also passed a value with NO hangul in it at all, which is not Korean under any reading - it is whatever the file was seeded from. 看板 for a board, 拡大 and 縮小 for zoom, 担当者 for an assignee, 賛成 and 反対 for the two sides of a vote, and the three date formats written 年-月-日.
The rule is now the narrower one it should always have been: hanja is fine WITH
hangul, and a hangul-script value carrying CJK and no hangul is flagged. That is
80 values in ko and the same 80 in ko-KR, translated here - including the
search operators a user types, 담당자, 마감 and 조직.
Two more blind spots, both found by asking the scan a question it could not answer before.
A value written entirely in the LATIN alphabet, inside a language that is
not. The Unicode-block comparison could never see it, because Latin is not one
of the blocks it compares against - so el and el-GR held 926 values of
Italian each, th 685 of Vietnamese, ar-DZ 510 of French and ka
45 of Turkish, and the count read zero throughout. Product names are Latin
too, so a value is only suspect when it says something: five letters or more,
and not the English source wearing different punctuation.
English that stopped looking untranslated. The two keys
map-to-existing-user-desc and -none were REWORDED in en.i18n.json, and
every other file kept the old English. Old English is not equal to the new
source, so the fill tooling counted it as a translation and stopped offering the
key - in 145 files. The current source is written back, which changes nothing a
reader sees and makes 290 values countable again.
The detector's first harvest, smallest first: 43 values of Turkish in ka, 508
of French in ar-DZ, and 44 in mn that were never translated at all but had
drifted far enough from the English source that the fill tooling no longer
offered them. Plus the tail - six in ug, two in fa, one each in or_IN,
pa and uk. ar-DZ is fixed from ar, key by key and only where ar has a
translation, so the variant is never ahead of its base.
The script map also grew the languages it had never listed: ary, the nine
Turkic and Mongolic languages written in Cyrillic, and Chinese under every tag
it ships as. Of those only mn had anything to report, which is the answer
worth having.
A third of th.i18n.json was written in Vietnamese - Bảng for a board, Thẻ
for a card, Danh sách for a list - beside 1,594 values of real Thai. All 683
are Thai now, under terms fixed once and used throughout: บอร์ด, การ์ด, รายการ,
สวิมเลน, เช็คลิสต์, คลังเก็บ.
One value was not a translation at all: act-withCardTitle read
__kartu__[__Panel__], two Indonesian words in placeholder syntax, so the
notification it formats could never substitute a board or a card. It is
[__board__] __card__ again.
el and el-GR are the same file twice, and 924 of their values were Italian -
lista rinominata in, si è unito a, Bacheca for a board, and Couloir,
French, for a swimlane. All of it is Greek now: the activity feed, the archive
dialogues, Planning Poker, the export fields, the rules engine, the Admin Panel,
the migrations and the whole of global search.
The terms follow what the file already had right - Πίνακας, Κάρτα, Λίστα, Ετικέτα, Μέλος, Λίστα ελέγχου, Αρχείο - and fill the two it did not: Διάδρομος for a swimlane and Υπεύθυνος for an assignee.
That empties the backlog the detector found: 3,174 values across six files and a
tail of small ones. wrong-script.mjs --count now reports zero for BOTH of its
checks - no value in another script, and none in the Latin alphabet inside a
language that is not written in it.
Panels added since a file was last touched - strings a language never had.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3eb1631d5429fa8d7dd7c894dc61e15a6747e28f">The Home page and starred pages, in the 33 languages that still showed English</a>. Thanks to xet7.</summary>A key added last month is missing everywhere that has not been visited since, which is not the same problem as a language nobody has worked on. The All Boards Home strings - Remove from Home, its confirmation, Click to star this page, Click to unstar this page, Starred Pages and the empty-Home hint - were the newest in the file, so 33 otherwise-complete languages showed six English rows in the middle of their own page. 197 values are filled.
Four of those files are not written in the language their name claims, and are
completed in the language they are ACTUALLY in rather than left half English:
ace is Malay, ast-ES is Spanish, ve is Zulu, and every uz variant
including uz-AR is Latin Uzbek.
km_KH already had human translations for all six and kept them, and sr kept
the one it had. That is the fill rule doing its job rather than a special case:
it writes only where the value is still the English source, so it reports
skipped 6 instead of overwriting them.
The same shape, two panels wider: the board status summary (card loading, time spent, total, cards with time, overtime cards) and the roles status table - what each board role may do, under its Invite / Sees cards / Create and edit / Board settings columns. 307 values across 47 files.
Five of them are written in a different language from their name and are
completed in that one: ro is Italian, ast-ES Spanish, ve Zulu, wo
French, and sr uses case-file vocabulary throughout - a board there is
Списи, so its board status is Стање списа rather than a literal translation
of the English. Two are low confidence and want a speaker: Klingon, whose
lexicon has patlh for a rank but no idiom for a board role, and Volapük.
roles-status-role is deliberately left alone in Czech, Spanish and Walloon.
Role and Rol are those languages' own words, and the fill step ignores a
value equal to the English source rather than pretending a translation happened.
The same is true of far more of the backlog than it first looked: magenta and
indigo are magenta and indigo in nearly every language that "misses" them.
The search operators - the words a user types, rather than reads.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d62c15aedc4b260b54340e7de3c2f073b679e059">The rest of the operators, in the 107 files that had done the others</a>. Thanks to xet7.</summary>A file that translated board:, list: and member: and left org:, title:
and customfield: in English contradicts itself: half the search syntax is in
the reader's language and half is not, and nothing on screen says which half is
which. 217 operator words, filled only in files that had already
translated at least two of the core three - so this never invents a search
vocabulary for a language that has not chosen one.
An operator NAME is matched before the colon, so it can never contain a space. Languages that write these as two words run them together, the way Greek already writes προσαρμοσμένοπεδίο: Slovak vlastnépole, Hungarian egyénimező, and the same in Thai and Vietnamese.
72 are deliberately left alone. status, limit, team, selector, projection, description, week and open are those languages' OWN words in Dutch, Swedish, Spanish, French, Catalan, Czech, Polish, Turkish and Malay.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/774d7a032b1bd17a3ee721ff4611317b6b8f7efe">Each one-letter shorthand is the language's own letter, in 89 files</a>. Thanks to xet7.</summary>Each search operator has a one-letter shorthand beside it, and the shorthand is
the first letter of THAT LANGUAGE'S word: French couloir is c, Russian
доска is д, Georgian დაფა is დ. A file that translated the word but kept
the English letter contradicts itself - board: works, and b: stands for
nothing the reader can see.
277 shorthands, derived rather than guessed: the first grapheme of the file's
own word, extended while it collides with another shorthand in the same file,
and written only where it differs from the English letter. Portuguese lista
and German Liste both begin with an l, so l is already correct and is left
alone. Every file ends with five distinct shorthands - Welsh aelod and
aseinai are a and as, Hungarian Tábla and tag are t and ta.
Three orderings had to be right or the result was worse than what it replaced:
case-fold BEFORE the collision test (Tábla and tag are both a T), seed the
taken set from the shorthands that are NOT being changed (Frisian kept l for
lijst while lid was handed the same l), and test the English letter AFTER
the collision loop rather than before it, or Welsh keeps a twice on the
grounds that a is what English uses.
Skipped where the operator WORD is itself in another language, because a
shorthand derived from it carries that one step further and the word is what
wants fixing: tlh is German, th is Vietnamese, br is French, ve-PP is
Finnish, and one key each in mn, sk, lv, vo and zgh. Latin-script
contamination like that is invisible to wrong-script.mjs, which can only
compare Unicode blocks.
Words filled by key - one key across many files, not one at a time.
<details> <summary><a href="https://github.com/wekan/wekan/commit/da8d02315a806e9e8a5f5ff4f92e950f6181be61">Statistics, package and the region names, where the word is not the English one</a>. Thanks to xet7.</summary>128 values across 67 files, chosen by asking of each key whether the language actually has a different word - because most of what the count calls a prose backlog is not one. Status is Status in Danish, German, Dutch, Swedish and Polish; Server is Server almost everywhere; Normal, Ticket, Menu and Logo are themselves in most of Europe. Those are correct as they stand.
What is filled is where the English word is visibly foreign or the language plainly has its own: the non-Latin scripts (Κάδος and Στατιστικά, דלי, سطل, Кофа, Корпа, Багц, and America in seven Indic and East Asian files), the African languages that had been handed English (IMelika, Umlawuli, Iphakethe, Iphakheji, Ngwugwu, Idì, Marekani), and Statistics in the twelve European languages that had not translated it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4a950b80bbfafc5d1a9a3bb107ad8b7b472cc6d4">The import-mapping dialogue, in the 48 files where English is a foreign alphabet</a>. Thanks to xet7.</summary>An untranslated string is not equally visible everywhere, and counting them as if it were is what makes the backlog look shapeless. In a Latin-script language Status reads as a word; in a Greek, Arabic, Thai or Devanagari interface an English paragraph is a different alphabet in the middle of the page. Measured that way the non-Latin files are nearly done - 235 real words still in English across 52 of them, against 1,927 product names and symbols that will never stop counting - and 102 of those 235 were two keys.
They are the strings the import dialogue shows when it asks which real user an imported member is: the pair whose English source was reworded, so every file kept the old English. 96 values, in 24 languages from Arabic to Chinese in both scripts. Serbian gets the case-file vocabulary the rest of that file uses - a board is списи and a member a сарадник - so the sentence reads as the same document the rest of the interface describes.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e53e895f6ba0e875dc14524ba58317fddeff8575">The import-mapping dialogue, in the other 85 files</a>. Thanks to xet7.</summary>Finishes the pair the non-Latin files got: 170 more values, from Afrikaans to Zulu. These are the two longest strings in the file - a paragraph explaining that mapping an imported member onto a real user moves that member's cards, comments and activity, and that the user is added with the imported member's OWN role, so mapping can never grant more permission than the import did. That last clause is the one worth getting right in every language, because it is the answer to "what am I about to give this person".
Seven files are written in a language other than the one their name claims and
get that one: ace is Malay, ast-ES Spanish, ro Italian, ve Zulu, vl-SS
Dutch, and vo, wo, zgh and wa-RR are French.
tlh is deliberately left in English. Klingon has words for a board, a card and
a rank, and this paragraph needs none of those - it needs three subordinate
clauses about permission, and inventing them would produce something that reads
as though somebody meant it. An English placeholder says plainly that nobody has
translated it yet, which is the more useful thing for the one reader who could.
The next twenty keys by how many files share them - Dialog, Container, Tests, Filter, Version, Pause, Repository, Detail, Type, Color, Error, Roles, Latitude, Longitude, orange, Admin - were offered to the 56 Latin-script files that still had them in English. 48 were filled and 202 were ignored, because the value offered was the English word and the English word is what that language uses: Filter is Filter in German, Dutch, Danish, Frisian and Estonian, and Container, Tests, Version, Pause, Repository, Detail, Type and Latitude are themselves across most of western Europe.
What did change is where a language really has its own: Spanish Funciones for roles, Administrator / Administrateur / Frisian Behearder for admin, Slovak Podrobnosť and Dialóg, Italian Contenitore and Finestra di dialogo, Vietnamese Vùng chứa, Portuguese Pausa, and Yoruba Ìwọ̀n ìhà and Ìwọ̀n gígùn for latitude and longitude.
The 202 are the result worth recording. They count as untranslated only because the tool's test for it is "still equal to English", no amount of work will ever reduce them, and they are why the backlog number is several times the size of the backlog.
</details>Deeper translation - languages taken past the words on the board.
<details> <summary><a href="https://github.com/wekan/wekan/commit/72fd4aa1278ba3a827ca6193d6a358e22fe9f63c">Eighty-one languages go past the board, into the menus and the login page</a>. Thanks to xet7.</summary>A language file starts here with the words a board is made of - board, list, card, swimlane, member, the buttons - about 50 to 77 strings. Eleven of the largest new ones went further already, to roughly 112, and the difference is what somebody meets in the first minute rather than the first second: Log Out, Forgot password, Email Address, Create Board, Move to Archive, Restore, Copy Card, Move Card, Delete Card?, List Actions, Board Settings, Member Settings, Search All Boards, Custom Fields, Add Attachment, Accounts, People, Organizations, Teams - and, for the ones that had not reached it yet, Activities, Attachments, Checklists, Assignee, Due Date, Register, Change Password, Admin Panel, Profile, Watch and Export list.
Every other language still at that first tier now carries the second one too: Maltese, Luxembourgish, Shona, Albanian, Bosnian, Hawaiian, Latin, Luganda, Assamese, Irish, Icelandic, Javanese, Kurmanji, Kyrgyz, Malagasy, Maori, Marathi, Chichewa, Oromo, Pashto, Kinyarwanda, Sindhi, Sinhala, Sesotho, Tajik, Tigrinya, Setswana, Xitsonga, Tatar, Friulian, Ladin, Neapolitan, Romansh, Aragonese, Corsican, Sardinian, Sicilian, Aromanian, Kashubian, Upper Sorbian, Silesian, Faroese, Greenlandic, Northern Sami, Bislama, Tok Pisin, Fijian, Samoan, Tongan, Haitian Creole, Papiamento, Inuktitut, Aymara, Quechua, Nahuatl, Cherokee, Manx, Cornish, Scottish Gaelic, Guarani, Bashkir, Buryat, Chuvash, Sakha, Tibetan, Dzongkha, Bhojpuri, Maithili, Konkani, Kashmiri, Sorani, Akan, Bambara, Ewe, Fula, Northern Ndebele, Northern Sotho, Kirundi, Swati, Tigre and Wolaytta.
Each string was written from that file's own existing vocabulary, so the new menu rows use the same word for board, list and card that the board already uses. The ones written with least confidence are Cherokee, Inuktitut, Nahuatl, Wolaytta, Tigre and Kashmiri - a speaker who sees an error there should correct it, and the merge rules guarantee that a human translation arriving on Transifex REPLACES a filled one and is never overwritten by it.
That is 4,082 strings across 81 files, all of them into placeholders that were
still English: releases/translations/verify-human-preference.mjs passes, and
nothing here is pushed to Transifex as if it were human. Every file is still a
small fraction of 2,384 keys, deliberately - the rest falls back to English
exactly as before.
A new language file started with the words a board is made of - board, list, card, swimlane, member, the buttons. Enough to recognise the app, not enough to use it: the menus, the popup titles and the login page were still English.
Eleven of the largest new ones - Bengali, Urdu, Filipino, Hausa, Amharic, Kannada, Malayalam, Nepali, Somali, Kazakh and Burmese - now carry the second tier as well: Log Out, Forgot password, Email Address, Create Board, Move to Archive, Restore, Copy Card, Move Card, Delete Card?, List Actions, Board Settings, Member Settings, Search All Boards, Custom Fields, Add Attachment, Accounts, People, Organizations, Teams - the strings somebody meets in the first minute rather than the first second.
They are at about 112 strings each now, from 77. That is still a small fraction of 2,384, and deliberately so: these are the ones where being in English is most visible, and the rest falls back to English exactly as before.
</details>New languages - the ones WeKan had no file for at all.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a7332f233637fa9716d276eac967fed300a0017d">Twenty more, including the ones I was least sure of - and which those are</a>. Thanks to xet7.</summary>246 languages, from 154 when this release started.
Confident enough to write without hedging: བོད་སྐད་ (Tibetan), རྫོང་ཁ (Dzongkha), Kalaallisut, ᐃᓄᒃᑎᑐᑦ (Inuktitut), ᏣᎳᎩ (Cherokee), Nāhuatl, Буряад хэлэн, Kaszëbsczi, Ślōnskŏ gŏdka, Aragonés, Ladin, Armãneashti, मैथिली, भोजपुरी, कोंकणी and کوردیی ناوەندی (Sorani Kurdish, right-to-left).
Written with less to go on, and said so here rather than quietly: کٲشُر (Kashmiri, right-to-left), Pulaar, ትግረ (Tigre) and Wolaytta. For these four the sources are thin - a wordlist and a grammar sketch rather than a dictionary with a UI vocabulary in it - so the terms for board, swimlane and checklist are built from the ordinary words for a writing-board, a path and a list of things to check. They are the strings most likely to be wrong, and the most likely to be corrected by the first speaker who opens the language picker and finds their language in it at all.
That is the whole argument for doing them: the alternative was not a better translation, it was no language. Cherokee has around 2,000 fluent speakers; Tigre, Wolaytta and Pulaar have millions between them and almost no software. A wrong word invites a correction. An English placeholder invites nothing.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0cca649b7dd37ea5bc59c5afa2659950f0e1c543">Fourteen more: the Pacific, the Caribbean, and the languages of Italy</a>. Thanks to xet7.</summary>226 languages, from 154 at the start of this release.
Kreyòl ayisyen, Papiamentu, Tok Pisin, Bislama, Gagana Sāmoa, Lea faka-Tonga, Na Vosa Vakaviti, Corsu, Sardu, Sicilianu, Napulitano, Furlan, Rumantsch and Hornjoserbsce.
Four are creoles - Haitian, Papiamentu, Tok Pisin and Bislama - which between them are the everyday language of about fifteen million people and are almost never what software is written in. Five are languages of Italy that Italian does not cover: Sardinian and Sicilian each have more than a million speakers. Rumantsch is one of Switzerland's four national languages, and Hornjoserbsce is one of Germany's.
Each is the same shape as the rest: the words a board is made of, the buttons, the menus, the dates - and English underneath until somebody corrects it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a3ee0262d68ce0d10d4466018a9c5d3b9b44c233">Eighteen more languages: the Americas, the Sahel, Siberia and the Celtic ones</a>. Thanks to xet7.</summary>212 languages. WeKan had 154 when this release started.
This block: Bamanankan, Akan, Eʋegbe, Ikirundi, isiNdebele, Sesotho sa Leboa, siSwati, Башҡортса, Чӑвашла, Саха тыла, Runasimi (Quechua), Avañe'ẽ (Guaraní), Aymar aru, Føroyskt, Davvisámegiella, Gàidhlig, Kernewek and Gaelg.
Three of them are indigenous languages of the Americas with millions of speakers between them and no project-management tool in any of them. Three are Siberian and Volga languages of the Russian Federation. Four are Celtic - and Cornish and Manx are revived languages, which is exactly the case where the wording being imperfect matters least and the language existing at all matters most.
Scottish Gaelic and Cornish take the Scottish and English regional flags rather than the Union Jack - the emoji tag sequences, not the state's flag - and Manx takes the Isle of Man's own. A language gets the flag of where it is spoken, and for these three that is not the same thing as the flag of the state.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2395dfd0840b39f584c0338fc4683e560a9af0e8">Fifteen more languages, most of them African</a>. Thanks to xet7.</summary>194 languages. This block is the one WeKan was furthest from having: Татарча, ʻŌlelo Hawaiʻi, chiShona, Ikinyarwanda, Chichewa, Sesotho, Setswana, Xitsonga, Luganda, Afaan Oromoo, ትግርኛ, Kurdî, سنڌي, অসমীয়া and Malagasy.
Nine of the fifteen are African languages with tens of millions of speakers
each and no kanban tool in them at all. Sindhi is rtl: true; Kurdish
takes the Iraqi flag, because Kurdistan has no emoji and that is where most of
its speakers are - the same rule as any language spoken across a border.
The three edits are one script now - releases/translations/add-language.mjs.
It writes the strings file as a full copy of the
English one with the translations swapped in, inserts the registry entry in
alphabetical order with the name in the language's own script, and adds the flag
only if the map does not already anticipate it. That is why this block took
three commands rather than thirty, and why nothing was left half-wired.
179 languages. This block adds Íslenska, Gaeilge, Shqip, Bosanski, Malti, Lëtzebuergesch, Кыргызча, Тоҷикӣ, Māori and Latina - the same three edits each, the name written in its own language, and a flag.
Two of them say something about the policy. Māori and Latin have nobody waiting to translate a kanban board, and Latin has no country at all; it takes the Vatican's flag because that is the state that still uses it, which is the same reasoning that gives a language spoken across a border the flag of where it is spoken. A constructed language keeps the globe instead - Esperanto, Klingon and Volapük are nobody's country, and borrowing one would be a claim rather than a hint.
Latin is a real translation and not a joke: tabula, charta, index, semita, titelli, sodales, and the imperatives a menu is written in - Adde, Dele, Serva, Quaere.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/244710e91ec29a30093428ee6fbf64c5791aee26">Nine more languages: Kannada, Malayalam, Burmese, Sinhala, Nepali, Javanese, Somali, Pashto and Kazakh</a>. Thanks to xet7.</summary>WeKan is at 169 languages. These nine were missing entirely, and between them they are spoken by something like 250 million people: ಕನ್ನಡ, മലയാളം, မြန်မာ, සිංහල, नेपाली, Basa Jawa, Soomaali, پښتو and Қазақша.
Each arrives the same way as the last six: the strings file, the entry naming
the language in its own script, and the flag. Pashto is rtl: true, and the
RTL guard's list grew with it - that list is the one place the direction of a
script is written down, so a new right-to-left language that is not added to it
fails rather than laying itself out backwards in silence.
Each starts with the words a board is made of - board, list, card, swimlane, label, member, the buttons, the menus, the dates - about eighty strings, and the rest falls back to English until somebody who speaks it says otherwise. That is the point of the policy: a language that is 3% translated is a language somebody can start using and correcting, and 0% is not.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1fedfb26256b2cace1d058d93931d94ad223c930">Six languages WeKan did not have: Bengali, Urdu, Marathi, Filipino, Hausa and Amharic</a>. Thanks to xet7.</summary>WeKan had 154 language files and not one of them was Bengali - about 270 million speakers - or Urdu, or Marathi, Filipino, Hausa or Amharic. The list of languages WeKan supports was never a judgement about which languages matter; it is the list somebody happened to start.
Each is three edits, and all three are needed or the language is invisible: the
strings file, the entry in languages.js naming the language in that
language - বাংলা, اردو, मराठी, አማርኛ - and the flag in the picker. Urdu is
rtl: true, so the whole interface lays itself out right-to-left for it.
A new file is a full copy of en.i18n.json with the translated values swapped
in, which is what every other language file here is: the key-order guard reads
absolute positions, so a file holding only the strings it has translated would
put every later key at the wrong index. Each of the six starts with the words a
board is made of - board, list, card, swimlane, label, member, the buttons, the
menus, the dates - and grows from there.
tests/newLanguageWiring.test.cjs holds the three edits together, and found
that fourteen languages were showing a globe instead of a flag (Acehnese,
Moroccan Arabic, Asturian, Breton, Frisian, Norwegian Bokmål, Occitan, Turkmen,
Walloon, Wu, Yiddish, Tamazight and two Chinese variants), which they no longer
do. Constructed languages - Esperanto, Klingon, Volapük - keep the globe on
purpose rather than borrowing somebody's country.
It also learned what a symlinked language file is. km-KH.i18n.json and
ru-RU.i18n.json are symlinks to km_KH.i18n.json and ru_RU.i18n.json: those
two locales are the ones .tx/config's lang_map does not rename, so Transifex
writes the underscored name and the hyphenated link is what languages.js
loads. A guard that reads only the import paths sees the target as an orphan and
is wrong about it; this one resolves the link, and checks that every link points
at a file that exists and that one end of each pair is loaded.
Another eight languages, from their own vocabulary: the export and import menus, the roles table, the board-status pane, the starred pages, the search operators, and for Igbo the last of its report and recovery strings.
The list sort letters are translated too. A list header shows (N), (M)
or (L) for whether it is sorted by NAME, by your MANUAL order or by LAST
access - initials of the ENGLISH words, so on any other language they were three
letters that stand for nothing. They are that language's own initials now:
German (N)/(R)/(Z) for Name, Reihenfolge, Zugriff; Finnish
(N)/(J)/(K) for Nimi, Järjestys, Käyttö; Russian (И)/(П)/(Д) -
twenty-five languages, and their regional variants after them.
What is left is mostly not translatable. Of the strings still flagged in German, French and Dutch, three quarters are a word of one or two syllables that those languages spell exactly as English does - Filter, Team, Text, Details, Layout, Person, Pause, Actions, Date, Description, Notifications, Type, Database, Help, Repository. They are counted as untranslated because the only test available is "is this still equal to the English source", and by that test a correct translation that happens to be identical can never pass.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/da022efe627fc3298e21db7ecc4f3a37a5b40ce3">Frisian, Igbo, Yoruba, Wolof, Breton, Walloon, Volapük and Tamazight, from their own words</a>. Thanks to xet7.</summary>Eight languages nobody has volunteered for. Five of their files were not
untranslated at all - wo, wa, br, vo and zgh were written in
French (Tableau, Carte, Liste, Supprimer), with Esperanto in the
Volapük file and Arabic in the Tamazight one, seeded from whatever was to hand.
As with Klingon, that is not a translation to protect: the core vocabulary is
replaced with each language's own words - Breton Taolenn, Kartenn, Roll;
Walloon Tåvlea, Cåte, Djivêye; Wolof Tablo, Karta, Limu; Volapük
Bod, Kad, Lised; Tamazight Tafelwit, Takarḍa, Tabdart.
On top of that each got the strings a board actually shows: the export and import menus, the roles table, the board status pane, the starred pages, and the sentence that explains what dragging a board onto Home does.
They are imperfect, and that is the point: CLAUDE.md now says so outright
What is left in the big files is mostly not translatable at all: German Filter, Team, Text, Details, French Actions, Date, Description, Notifications and Dutch Filter, Type are the words those languages use, and they are counted as "untranslated" only because the tool's test for it is "still equal to English".
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3b5f2b24cc16547c50e36fd5308b0c7fde5c67a3">Klingon from its own lexicon, and every regional variant inherits its language</a>. Thanks to xet7.</summary>Klingon. tlh.i18n.json was not untranslated - it was written in FRENCH and
GERMAN: Tableau, Karte, Liste, Löschen, Board exportieren. Somebody
had seeded it from whatever file was to hand. The words are looked up now, in
the boQwI' lexicon - the Klingon
Language Institute's own data, ~5800 entries - and Klingon has words for most of
what a kanban board is: 'echlet is a board, 'echletHom a card,
tetlh a list, per a label, chuD a member, mI' a number,
Dotlh a status, yer a domain, mIllogh a picture. The actions
are imperatives, which is what a menu entry is in Klingon: yISuq (acquire it)
for import, yIngeH (send it) for export, yIQaw' (destroy it) for delete,
yIchel (add it), yIteq (remove it). So Export list is tetlh yIngeH and
Import card is 'echletHom yISuq.
Regional variants. A variant file that had not translated a string carried
the English source while its base language had a perfectly good translation two
directories away - de-AT in English beside German, es-MX beside Spanish,
zh-Hant beside Traditional Chinese. Each variant now inherits its base for the
strings it has not translated itself, and only for those: a variant's OWN
wording always wins, which is the same rule that protects a human translation
from a filled one.
Three files were in another language entirely, and are completed in the
language they are actually written in rather than left half-English: vl-SS is
Dutch throughout, ace is Malay, ve is Zulu.
Two of the languages that had nothing - Igbo and Yoruba - had real translations under the English placeholders, so they are filled from their own vocabulary rather than from a neighbour's.
</details>New strings - what this release added, in every language.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f12095efab438128b73a9cf01bec4e769289ec65">The 31 strings this release adds, in 127 languages</a>. Thanks to xet7.</summary>Everything added to en.i18n.json since 10.91 - the new popup titles, the
export and import wording, the card-number search operator, the filesystem
integrity report - shipped as English placeholders in every other language file.
Translated directly, as CLAUDE.md requires: no external translation service,
API or key, and each language's OWN existing strings as the reference, so the
new wording matches what that file already says rather than being invented
beside it.
The vocabulary each file already had is what decided the wording: its word for a swimlane, a list, a card and a board, and whether it says export with a verb or a noun. The four import titles are the four export titles with that language's own word for importing, because in a menu that already says what it is about they are the same sentence pointed the other way.
operator-number is a special case: it is not a label but a word somebody
TYPES into the search box, beside operator-board and operator-list - so it
is that language's plain word for a number, in the form those two are already
in.
Nothing overwrote a human translation. The fill step writes only into keys that
are still the English source, and a translation already committed for a key was
kept - Finnish's own Poista ylimääräiset lista kopiot stayed exactly as it
was. verify-human-preference.mjs proves that property rather than asserting
it, and none of this is pushed to Transifex, so a filled string can never
masquerade there as a human one.
Fifteen languages are deliberately left as English placeholders rather than guessed at: Klingon, Volapük, Acehnese, Breton, Igbo, Uyghur, Venda and its two variants, Walloon and its variant, Wolof, Yoruba, Tamazight, and vl-SS. A wrong translation reads as though somebody meant it; an English placeholder says plainly that nobody has translated it yet.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9417e35d0b2f5c9925df514f33b9fbd17a989e5c">Export is a word in every language, not the key "export" in lowercase</a>. Thanks to xet7.</summary>Every menu on a board grew an Export row and an Import row, and the Export one
was written {{_ 'export'}} - a key that had never existed in en.i18n.json,
or in any of the other 245 files. i18next answers a key it does not know with
the key itself, so the row read export: lowercase, untranslated, in every
language including English. It was reported from Finnish, where every other word
in that menu is Finnish and this one was not.
It is a key now, and translated into all 245 other files. For 225 of them the word is the shared part of that file's own Export list and Export swimlane - the word the language already uses, Vie in Finnish, Exportieren in German, Экспортировать in Russian.
The rest are written out, because a shared prefix is the wrong answer for them. Where it took an article along - Portuguese Exportar a, Irish Easpórtáil an, Welsh Allforio'r - the article is dropped. Where the verb is a circumfix that only appears with its object between the halves, both halves are written: Voer uit in Afrikaans, Flytja út in Icelandic, Yi adi in Akan, Salim i go aut in Tok Pisin. Hungarian takes the nominative Exportálás rather than the possessive, and Klingon keeps its own capitalisation, yIngeH with the lowercase y, which a capital-first rule got wrong. The twenty-one languages that put no spaces between words had nothing to derive from at all and are written from their own phrases - the two Chinese forms by script, and Japanese, Khmer, Thai, Burmese, Tibetan and Dzongkha.
import already existed and was translated everywhere. In seven files it was
still the bare English noun while their Export was a verb, which read as a pair
that did not match: Czech gets Importovat beside Exportovat, Turkmen
Import et, and the four Uzbek files Import qilish. Malay and the English
variants keep Import, which is the word those languages use.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: four reports from admins who could not tell what their own WeKan was doing, and two pieces of theming. Member Settings / Change color gained an All Boards switch beside "Default (no override)" - the overview's tiles take the theme's lighter colour instead of eleven different board colours, if that is what you want - and a highlighted popup row now follows the theme like the left menu does instead of being a fixed navy. Clicking a minicard again did not close the card it had opened - the toggle was there and had a test, and it was closing the wrong thing, so it was the one part of this that nobody could see was broken. A snap waiting for its database answered nothing at all on the web port, so an upgrade that left the database down looked like WeKan itself loading forever; the wait is a page now, with the commands that say why, and it carries the product name and the schema-upgrade dashboard's colours. The Admin Panel reports were full of things that never happened - an ordinary restart written up as a crash (and, because of the same bug, actually turned into minutes of downtime), and a reverse proxy written up as a spoofing attempt. And a snap serving the older of its two copies of the data was told "No problems detected", because the status report reads only the database WeKan is connected to and never said WHICH one that is; it now ends with a section that does, and names the two recovery commands. Below that: two open issues that the current code already answers, closed by reading it.
The binaries below are carried over from v10.90 and have NOT been checked
against a newer build; releases/provenance-table.sh prints the real table
from the provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bugs:
Cards on the board - opening one, and closing it again.
<details> <summary><a href="https://github.com/wekan/wekan/commit/664cc6b95">Clicking a minicard again really closes the card, wherever the card was opened from</a>. Thanks to Heart1010, csonkaoszimt and xet7.</summary>"you now can click outside your mini card and the popout will close. Clicking the mini card again to close the popout is still not possible I think" - #6465, on v10.90, which already had the toggle and a test suite pinning it.
The toggle was closing the wrong thing. On a desktop-sized screen the card
details are not the address you are at: clicking a minicard writes the card id
into the openCards session list and the board renders one draggable window per
id in it, leaving the URL on the board. The toggle asked whether the card was
currentCard and closed it by navigating to the board - which clears
currentCard and leaves the window on screen, because nothing had taken the
card OUT of openCards. Only the window's own X button did that, which is why
closing worked from the card and not from the minicard.
Both questions are now the card details' own close logic, so both ways of
closing a card do the same thing. "Already open" is asked of the list that
renders the window, which also fixes it for Open many cards at once: with
several open, currentCard is only the last one clicked, so every earlier
window was impossible to close from its minicard. Closing navigates back to the
board only when the card really IS the address - a card opened by a click is
not, and navigating would reset the board view for nothing.
Member Settings - the theme, and what it reaches.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e86e7abe0">All Boards tiles can take the theme's lighter colour, and a highlighted popup row follows the theme too</a>. Thanks to khuongsatou and xet7.</summary>Two things in Member Settings / Change color.
Beside Default (no override), on the same row, a second switch: All Boards. Turned on, every tile in the All Boards overview takes the theme's lighter colour — a white veil over the theme accent, which is one shade up from whatever the theme is — with white text on it, instead of the colour its own board was given. Turned off, which is the default, the overview is exactly what it was. A board with a background image keeps its image: that is a picture somebody chose, not a colour.
That is #6593's observation as a per-user choice rather than a stylesheet. The pull request paints every tile white for everybody, and what it noticed is right — a wall of boards in eleven colours reads as a palette rather than a list — but "the tile colours are noise" and "the tile colours are how I find my board" are both true, of different people. So it lives where the rest of that user's theme is chosen, and only there: a board has no overview of its own, and a site admin does not choose this for everybody.
And the highlighted row in a popup: it was filled with a fixed dark navy that appears nowhere else in WeKan and stayed navy however the user had themed it. The All Boards left menu fills its selected row with the theme accent and turns the label and icon white, and so does the Admin Panel — the popup does now too, so there is one selected-thing look rather than three.
</details>Starting up - and what a browser shows while WeKan cannot yet serve.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4254dfa39">A snap waiting for its database says so in the browser, instead of timing out</a>. Thanks to Alishara and xet7.</summary>"We upgraded from 10.85 to 10.89 and later to 10.90 - a reload of wekan got a timeout (loading forever)" - #6592.
WeKan does not open its web port until the database answers, and the snap's two
waits are endless on purpose: a database can take minutes to come up after an
update, and giving up on it would be worse than waiting. They are not silent
either - after two minutes each prints what to check. But nothing was listening
on the web port while they waited, so the browser got a timeout and everything
printed went to snap logs, which is the last place somebody whose site is down
thinks to look. It also makes the two possible faults look the same: "WeKan
does not load" is the report whether WeKan is broken or FerretDB simply did not
start.
The wait now serves a page saying WeKan is waiting for its database, naming
the database it is waiting for and, in the browser, the commands that answer
why: the service's own log (an exec format error is the bundled binary not
running on this CPU), wekan.problems for which copy of the data is served,
snap start --enable for a service left stopped by a failed migration, and
snap revert back to the revision that worked. It refreshes itself away when
WeKan starts, appears only after 30 seconds so an ordinary restart never shows
it, and is stopped before anything else binds the web port.
This does not say why the reporter's database did not come up - the issue has no logs yet - but the next person sees the reason instead of a timeout.
</details>The Admin Panel reports - what they say happened, and whether it did.
<details> <summary><a href="https://github.com/wekan/wekan/commit/034a23ede">A restart is not a crash, and a reverse proxy is not a spoofer</a>. Thanks to xet7.</summary>Two reports from a server running 10.90 Snap, both full of things that never happened.
Filesystem integrity, over and over: "the previous run STOPPED WITHOUT
SHUTTING DOWN CLEANLY, and this server was down for about 4 minute(s)",
severity high — on a snap that had been refreshed, not crashed. Two faults, and
the second made the downtime real rather than merely reported.
IntegrityKeys.update() is not synchronous in Meteor 3: it starts a write and
hands back a promise nobody waited for, so the clean-shutdown mark was never on
disk when the process went. And registering ANY listener for SIGTERM replaces
Node's default behaviour, which is to terminate — nothing in that listener
exited, so WeKan ignored SIGTERM outright, systemd waited out its stop timeout
and used SIGKILL. That is both the minutes of "downtime" in those rows and a
genuinely unclean kill on every ordinary restart. The mark is written with
updateAsync now and the handler exits: with 0, after at most two seconds, and
exactly once however many signals arrive. What a crash IS was not touched — the
rows were wrong because the input was wrong.
Security Report, over and over: a medium-severity spoofing row for
/metrics denied "with X-Forwarded-For present", from 127.0.0.1. Every
reverse proxy adds that header to everything it forwards, so a Prometheus scrape
through a local proxy on a server whose allowlist does not cover it was being
written up as an attack, in the report where a real one would have to be
noticed. The spoof has a signature and it is asked for now: the header NAMES an
allowlisted address while the connection is not from one. The 401 is unchanged
and gained the sentence the admin needs — the address in it is the proxy, not
the scraper, unless METRICS_TRUST_PROXY says how many hops to trust.
The snap's standalone pages — under maintenance, recovering data, database too
old, and the new one above — are a small HTTP server with no database
connection, which is the whole point of them. The product name therefore comes
from a file that wekan-control cached once per start, which leaves the case
that matters: the name is set in the Admin Panel and the snap is not restarted
before the next outage, so a rebranded WeKan tells its users "WeKan is waiting
for its database" — a word they have never seen. WeKan is the only thing that
knows the name the moment it changes, so WeKan writes it, at startup and
whenever the setting changes.
They also wear the schema-upgrade dashboard's colours now — the same #111
ground, #7bf blue and monospace face — because that dashboard and these pages
are the same thing to a reader: the product saying what it is doing while it
cannot show them the app.
Recovering a snap that has two copies of its data - and finding out that it has.
<details> <summary><a href="https://github.com/wekan/wekan/commit/504e450a1">The status report says which copy of the data is being served, and how to compare them</a>. Thanks to waltermhl and xet7.</summary>"error: cannot find app "database-compare" in "wekan". It seems, that database compare is not included in wekan 10.82" - #6583. It is not: the two recovery commands are snap apps, so they exist only in the revision that ships them, v10.90. That is the smaller half.
The larger half is what happened before it. The same instance, serving data from
a migration done in July, ran snap run wekan.problems and was told "No
problems detected" - which was true of everything it checks, because it reads
the one database WeKan is connected to and every check inside it passed. Nothing
asked WHICH of the two copies that is, while the documentation already claimed
this command answered it.
The report now ends with a Databases on this machine section: which copy is
being served and why, whether MongoDB files and a FerretDB database both exist,
and - when they do - the two commands to run. It reads the files rather than a
database, so it answers with WeKan down, and it declares no fault: two copies is
the normal state of a migrated snap. A revision without the commands says to
refresh rather than leaving snapd's "cannot find app" as the last word, and says
why a refresh is safe here - it does not import an old MongoDB over a FerretDB
already in use, and neither copy is ever deleted. wekan.help and
Migration-to-FerretDB.md
list both commands with the release they arrived in.
Reported behaviour that the current code already gets right
<details> <summary><a href="https://github.com/wekan/wekan/commit/b9e00dbb8">Two open issues answered by reading the code, and pinned so they stay answered</a>. Thanks to xet7.</summary>Both sat in TODO Later as "needs the running app", and both are decided by
files that can simply be read.
#5052 — "Attachments cannot be
opened (.eml)", a blank page in the browser and nothing usable in Thunderbird
after a board was copied. Three things could produce that, and each is handled
now. The NAME: an unknown MIME used to append .bin
(#6589), and Thunderbird will not
open a .bin; every type a mail file arrives as now keeps its extension, and
the board copy names copies with the same rule. The SERVING: message/rfc822
is in neither the dangerous-types nor the safe-inline list, so it takes the
"unknown types" branch, which forces the download under the file's own name —
inline is what shows a browser a blank page. The FILE: an attachment whose
recorded path and on-disk name had diverged is found by the same search reading
already used (#6589), so a copied board's attachments open even when the
database's idea of the path is stale.
#5081 — "Owner is on the very
left, followed by members (if there are any) and on the very right there are
the assignees", wrapping to a second right-aligned row when they do not fit.
That is what the current minicard renders, and the reason is the float: three
groups that float to the inline end are laid out RIGHT to left in DOM order, so
the markup's assignees, members, creator renders as creator | members |
assignees. Each avatar floats too, so a row that does not fit wraps and stays
right-aligned, and an empty group is display: none rather than a gap.
tests/openIssuesVerifiedFromCode.test.cjs holds both, so neither can quietly
stop being true.
In short: things that were reported this week, and one of them is data
coming back from the dead: previously archived cards, some years old,
reappeared in the top swimlane because the schema upgrade treated an archived
swimlane as breakage. The board Excel export answered nothing at all — it
had been broken since a dependency bump, and the route swallowed the failure so
the browser waited forever. A .drawio attachment was stored as .bin,
unopenable, and could not even be renamed back. The board's watch popup did
nothing for anybody who reaches a board through an organisation, a team or an
email domain, and said nothing either. And when the snap cannot read an old
MongoDB, it now prints what each reader actually said and where to download a
MongoDB that can. Below that: two release-tooling fixes from the v10.89 run.
The binaries below are carried over from v10.89 and have NOT been checked
against a newer build; releases/provenance-table.sh prints the real table
from the provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bugs:
Recovering a snap that has two copies of its data
<details> <summary><a href="https://github.com/wekan/wekan/commit/073a327d6">Two commands for a snap that is serving the older of its two copies</a>. Thanks to waltermhl, lukechao and xet7.</summary>From #6583: "The migration and the update to 10.83 startet at 11.08.2026 at 6:35 pm and migration failed. Now we just see the old data from a migration we did in july 2026. … Which steps exactly could we do, to restore the database with our most recent data?"
Everything needed to answer that already existed — db-eval evidence,
database-choose.mjs, database-merge-missing.mjs, database-autopick — and
none of it was a command anybody could run. snap run wekan.problems answered
"No problems detected", which is true of the things it checks and no help at
all here.
sudo snap run wekan.database-compare # what does each copy hold?
sudo snap run wekan.database-merge # bring the missing documents across
compare starts each database on a temporary port, counts its documents and finds the newest moment its data carries, and prints both sides — the running WeKan is not disturbed, and nothing is written. A file timestamp cannot answer this question: starting a database moves its files, and a file written a minute ago may hold nothing anybody typed.
merge inserts the documents that exist in the MongoDB copy and not in the
FerretDB one. It overwrites nothing, deletes nothing, and reads the MongoDB
files only — so it is safe to run without first knowing which copy is "right".
That is WeKan's own design doing the work: the history is append-only, so
merging can only ADD to what a card shows, and the work that was stranded
becomes readable in that card's History. What it does not do is reconcile two
edits of the same card; the served copy's version stands, and the other stays
where it is. It asks for a copy of $SNAP_COMMON first, with the command, and
takes --dry-run.
The removed snap run wekan.database switch is gone from the core26 snapcraft
file as well, where it had been left behind.
Boards - what shows on them, and what quietly does not.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8ec78bb14">Cards archived years ago no longer reappear in the top swimlane</a>. Thanks to xet7.</summary>Reported by email: "Previously archived cards (some several years old) have reappeared. These cards have incorrectly been placed in the top swimlane."
Archiving a swimlane is how a whole swimlane is put away: its cards stay where
they are, archived: false, out of sight because the swimlane is. The schema
upgrade's swimlane rescue read that as breakage and moved every such card to
the board's first VISIBLE swimlane — so work anybody had ever archived that way
came back, years later, at the top of the board.
A card is orphaned when its swimlane does not exist, or belongs to another
board. That is #1959, and it is
still rescued. The other issue the sweep cited,
#1971, is about cards added in
List view landing in an archived swimlane — and that is fixed where cards are
created, by getDefaultSwimline() picking a non-archived swimlane. It never
needed a sweep over data somebody archived on purpose.
The two guards that pinned the sweep now pin the opposite, each carrying the reason, and a board whose every swimlane is archived is left exactly as it is.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8ec78bb14">The watch popup works for everyone who can open the board, and says so when it refuses</a>. Thanks to xet7.</summary>Reported by email, with a screenshot of the Ändra bevaka popup: "Silent does not respond. If we try to change it does not change. Nothing happens."
Driven against a running WeKan, the popup works for a board member — the level is written, the check mark moves, the popup closes — and does nothing at all for anybody else:
login as non-member admin: ok
watch -> ERROR error-board-notAMember
A board is shared four ways: membership, an organisation, a team, and (since
#5850) an email domain. Only the
first puts anybody in members, and the watch method asked hasMember().
Everyone reaching a board through an org, a team or a domain could open it, see
the button, and be refused the moment they used it. It asks whether the user may
see the board now, through the same selectors the publications use — so a
watch can never be granted where the board is not visible, and a revoked share
still is not.
The other half is why nobody could tell: the popup closed on success and did
nothing otherwise, so a refusal was indistinguishable from a dead button. It
reports the reason now — the watch feature being off in the Admin Panel, or the
board not being visible — and error-watch-disabled, thrown since #5820 but
never translated, exists as a string.
Cards and attachments - what a card holds, and getting it back out.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a712db947">An unknown file type is no longer renamed to .bin, and a stuck attachment can be repaired</a>. Thanks to rmb82 and xet7.</summary>#6589: a .drawio upload was
stored and displayed as .bin, could not be opened, and could not be renamed
back either — renameAttachment threw ENOENT. Two faults.
The name. A browser sends application/octet-stream for a type it does not
know, and the upload-time "correct the extension to the type" step took that
literally: mime.extension('application/octet-stream') is bin, so
sso-proconnect-keycloak.drawio became …drawio.bin. Every unrecognised format
— .drawio, .kdbx, .ova, anything new — went the same way. An uninformative
type now yields no extension at all, while a type that does say something still
corrects the name, which is what that step is for.
The rename. The recorded versions[].path and the file on disk had
diverged, and rename used the recorded path alone:
Error: ENOENT: no such file or directory, rename
'/data/files/attachments/6a7d66369c6aee799e857d36.drawio' -> ...
while the READER already searched every layout WeKan has used and found the file. That search is a method now, and reading, renaming and deleting all use it — so an attachment that can be read can also be repaired. When there really is no file, the error names the attachment instead of a path nobody recognises.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1f83020b4">A card containing an onenote: link no longer stops a whole board from rendering</a>. Thanks to titver968 and xet7.</summary>#6590: "A board gets stuck
indefinitely on the loading animation (three dots) for all users", traced to
one card whose description and checklist item held
onenote:///path/to/file.one#section-id={GUID}.
It is #6588 from the other end —
the same this.__schemas__[...].validate is not a function out of linkify-it 6,
reported as a board nobody could open rather than a card nobody could open — and
it was fixed on 2026-08-12. The regression test now renders that exact string,
in a description and in a checklist item, and pins that the {GUID} stays text
rather than being swallowed into a link.
Exporting a board - the format that answered nothing.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a712db947">The Excel export produces a file again, and a failure answers instead of hanging</a>. Thanks to titver968 and xet7.</summary>#6591: "Board Settings -> Export board -> export/Excel didn't work". Reproduced against a running WeKan — CSV and JSON of the same board answered 200, and Excel never answered at all:
csv: HTTP 200 4758b
json: HTTP 200 4491b
excel: Operation timed out after 30002 ms with 0 bytes received
with nothing in the server log. Two faults, either of which hangs the browser on its own.
The zip. exceljs 4.7.3's streaming writer calls archiver the way archiver 7
was called — Archiver('zip', opts) — and WeKan moved to archiver 8 for the
low-memory backup zips. archiver 8 is ESM and exports classes, so that is
TypeError: Archiver is not a function, and the export has been broken since
the bump. Supplying the missing factory does not rescue it either: archiver 8's
readable-stream then refuses the objects exceljs appends. So the export asks
what archiver exports and, when it cannot stream, it uses a buffered writer
with the same API — the path this export used before it was made streaming.
Bounded memory is what is lost, not the export.
The silence. The route called exporterExcel.build(res) without awaiting
it, so the rejection went nowhere: no 500, no log line, and a response that was
never written or ended. Every sibling route awaits; this one did not. It does
now, and a failure answers 500 with the reason.
The snap - when it cannot read the database it is asked to migrate.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1f83020b4">Each reader says why it refused the data, and the page says where to get an old MongoDB</a>. Thanks to mueschel and xet7.</summary>From #6585, a log that says everything except the useful part:
[migration] mongod 7 could not open the data; trying the bundled mongod 5.0 ...
[migration] mongod 7 could not open the data; trying the bundled mongod 4.2 ...
[migration] The database files were made by an older MongoDB
(MongoDB 4.2 or earlier can still read them).
Every reader was tried, each of them said something, none of it was shown — and the conclusion recommends the version that had just failed. mongod 7 names the version that can open the FORMAT; it cannot know the files are also damaged, or left locked by an unclean shutdown.
So each reader now prints its own last words when it does not open the data, the
"trying the bundled X" lines name the reader being tried instead of blaming
mongod 7 for all of them, and when mongod 7 asks for a version this snap
carries, the report says it was tried too and that --repair on a copy is
the usual next step.
And the other half of that report — "If you need us to run some external tools,
like an old mongodb, it would be good to provide a source for them" — the log
and the explanatory page now link mongodb.com's download page and
fastdl.mongodb.org, name the docker run mongo:<version> one-liner, and spell
out mongod --dbpath / mongodump on a copy, never the original.
and fixes the following release-tooling bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d38ca4680">A snap that reached the Snap Store also reaches the GitHub Release</a>. Thanks to xet7.</summary>The v10.89 run published the armhf, ppc64el and s390x snaps and then failed on the next line:
Revision 3661 created for 'wekan' and released to
'beta', 'candidate', 'edge', and 'stable'
no git remotes found
Error: Process completed with exit code 1
Those jobs flatten history so the Launchpad push stays small, and the git remote
goes with it — so gh had nothing to infer the repository from. The snap was in
the store and not on the release, which reads like a failed build.
Every gh release upload, view and edit in every workflow now names the
repository, so the call does not depend on what the checkout looks like, and a
bare one fails the guard.
v10.89's riscv64 leg ran five hours and fifty minutes — its cap — with Launchpad
still printing Building: riscv64, and the run showed CANCELLED and nothing
else. What is true at that moment is worth saying: the Launchpad build is not
cancelled with the job, it keeps its name, and re-running the job reconnects to
the same build and downloads the snap rather than starting another one.
The cap is now 360 minutes, the maximum a hosted runner allows, and a cancelled job prints that explanation plus any Launchpad URL its logs carry.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: the snap stops asking which database it runs on. It runs on
FerretDB — every platform — and MongoDB is in the amd64/arm64 snaps to be
read while a migration is owed, so snap set wekan database=… and
snap run wekan.database are gone: the data decides, and it cannot
contradict itself the way a setting could. With them go the three ways a snap
could stay on MongoDB for good — a 5.0 database no reader could open, a
migrated copy that had fallen behind being answered by switching back to
MongoDB ("WeKan changed to old MongoDB data") instead of merging, and a failed
migration that never tried again. Below that, the release workflow: a repo
script the job could not see, and an hour of emulated build thrown away on a
push that was never going to be authorized.
The binaries below are carried over from v10.88 and have NOT been checked
against a newer build; releases/provenance-table.sh prints the real table
from the provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bugs:
The snap - which database it runs on, and how everything gets into it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e63ac0e77">A migration never runs over the database it already produced</a>. Thanks to lukechao and xet7.</summary>From #6583: "the migration
re-ran yesterday (even though it had already run successfully a few weeks ago).
The .migration-to-ferretdb-done file is time stamped yesterday … That explains
why I'm seeing old data."
That is the worst version of this bug. The instance had been migrated and had
been serving from FerretDB for weeks; the marker went missing, the old staleness
guard put it back on database=mongodb, and the migration ran again — importing
the MongoDB copy it had been made from, over the database holding the work
since. discard_partial_ferretdb could delete that database outright, because
"partial" was assumed rather than checked.
Two locks on that door now, and the same fact opens both: the importer writes
migration-progress.json as it goes and resumes from it, so a FerretDB with
data and no checkpoint beside it is a finished database, in use — never a
migration to continue.
migration-control checks that before it probes, reads or deletes anything.
If it finds one it marks the migration done, starts FerretDB, stops MongoDB
and exits. Nothing is imported.discard_partial_ferretdb checks it again before removing a SQLite, and says
so when it declines. The MongoDB data is never touched either way.bin/migration-pending already answered the same question through
bin/database-role, so neither branch should be reachable — which is why they
are there. The cost of being wrong in this direction is somebody's data.
Three ways a snap could stay on MongoDB for good, all of them reported. The snap runs on FerretDB on every platform — MongoDB is bundled to be READ during a migration, and is not what WeKan runs on — so each of these is a bug.
A database nothing could open. A MongoDB server starts only on data whose
featureCompatibilityVersion is at most one major behind it, so the readers
covered FCV 6.0/7.0 (mongod 7), 4.0/4.2 (mongod 4.2) and 3.x (the 3.2 tools) —
and nothing covered 4.4 or 5.0. That is not a hypothetical rung: the WeKan
snap shipped MongoDB 5 in February 2023 and 6.0.6 only in May, so a site that
stayed on it has 5.0 files, and every reader refused them. Those instances got
.mongodb-data-too-old and an explanatory page while their boards sat in a
database nobody could read. mongod 5.0 is bundled now, as a fourth read-only
reader, tried between 7 and 4.2 — and through cpu-exec, because MongoDB 5.0
requires AVX on x86_64 and a CPU without it should read the database under
emulation rather than die on a SIGILL.
"WeKan changed to old MongoDB data." When the migrated FerretDB copy had
fallen behind the MongoDB beside it, the snap answered by switching itself to
database=mongodb. That is the mail this came from: the site is put back on
the database the snap is migrating away from — and when the detector guessed
wrong (#6583), onto a copy that
was weeks behind. The repair is the merge, not the switch: the documents
MongoDB has and FerretDB does not are copied into FerretDB — inserting what is
missing, overwriting nothing — and WeKan carries on there. WeKan's history is
append-only, so the work done on MongoDB after the migration lands in the card
History instead of a database nobody opens. Switching to MongoDB is now only
the fallback for when the merge cannot run, because serving a copy that is
behind is exactly the complaint.
A failed migration that never tried again. A failure set migrate=off so
it would not loop, and nothing ever set it back on. The snap stayed on MongoDB
until an admin read snap logs and typed a command, and most never do. A
failure is recorded now — how many attempts, when, and which snap revision —
and retried by itself: immediately after the next snap refresh, since the
next release is the most likely thing to have fixed it, and otherwise after a
wait that doubles from an hour up to a day. The same record replaces
migrate=off on the unreadable-database path, which is what makes this
release's 5.0 reader reach the instances that were already given up on.
snap set wekan migrate=off still stops it completely — an admin saying "not
now" is a decision, not a failure.
None of this deletes anything: the MongoDB data stays in $SNAP_COMMON and a
snap set wekan database=mongodb is still the way back.
snap set wekan database=mongodb|ferretdb is removed, and so is
snap run wekan.database. WeKan runs on FerretDB — every platform, every
install — and MongoDB is in the amd64/arm64 snaps to be read while a
migration is owed, not to be run on.
A setting could say something the data did not support, and each way it could was a report:
mongodb, it kept a site on the database the snap migrates away from,
for good, because nothing ever set it back — including the instances a failed
migration or a wrong staleness guess had put there;ferretdb with no FerretDB present, it would have served an empty
site, so the guard against that had to exist anyway;snap-src/bin/database-role replaced it: one helper, asked by wekan-control,
mongodb-control, ferretdb-control, migration-pending, attachment-repair
and the configure hook, that answers from the data — is there a FerretDB with
something in it, and has the migration that fills it finished? An interrupted
migration is told from a finished one by the importer's own checkpoint, so a
partial FerretDB resumes and a finished one whose marker went missing is not
migrated over again (#6585). A
snap that still carries the old setting is told once that it is ignored, and it
is unset.
The explanatory page stopped being a dead end too. When the MongoDB files cannot be read by this snap but a FerretDB copy is there, that copy is now served instead of the page — older beats unreadable — and the page's first instruction, which used to be a command to type, says so. The rest of it now opens with the fact that the snap keeps trying by itself.
Migration-to-FerretDB.md is the whole design in one page: what moves (all text data to SQLite, CollectionFS and Meteor-Files attachments to the filesystem, the card History with it), which MongoDB versions can be read, when it runs, what happens when it fails, and how two copies are reconciled. The Admin Panel, Snap and CPU-platform docs point at it instead of describing a setting that is gone.
</details>The release workflow - what it needs to be there before it runs.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e533829f5">Only the wekan Docker image is published; the two variant names are commented out</a>. Thanks to xet7.</summary>wekan-ondra and wekan-gantt-gpl are snap names — they exist because a
snap name cannot be changed once people have it installed — and as Docker images
they were only ever a second name for the same image. The release tagged them on
all three registries for two versions; it does not any more, and
docker pull wekanteam/wekan (or quay.io/wekan/wekan, or
ghcr.io/wekan/wekan) is the image, as it always was.
Six extra repositories across three registries, each with its own visibility and its own push permission, is six new ways for a release to fail in order to publish a copy of something already published — and v10.88 failed exactly that way, an hour into an emulated build:
ERROR: failed to push quay.io/wekan/wekan-ondra:v10.88:
unauthorized: access to the requested resource is not authorized
Quay grants push per repository and that repository had just been created by the release itself.
The -t lines are commented out, not deleted, with what it would cost to
uncomment them written beside them — a line that vanishes is a line somebody
re-adds next year — and the same for the names in the two verification loops and
the push preflight. The manual
docker-variant.yml
stays for publishing one out of band; it is workflow_dispatch only and no
release calls it.
Nothing is deleted from any registry: ghcr.io/wekan/wekan-ondra up to v6.99.2,
quay.io/wekan/wekan-gantt-gpl to v4.41 and wekanteam/wekan-gantt-gpl to
v5.62 keep working for whoever pinned them. They stop gaining versions. The
snaps keep both names and are still built and published, which is the point
of having them.
The v10.89 run failed four more jobs, all of them the same two mistakes one step further along.
The Windows jobs. They check this repository out to path: src, so the
scripts were addressed as src/releases/… — correct until the bcrypt step does
pushd "$TMP", after which a relative path resolves against a temp directory:
bash: src/releases/npm-retry.sh: No such file or directory,
The location is fixed now BEFORE anything moves — SRC="$PWD/src" at the top of
the step, then "$SRC/releases/…" — in all eighteen blocks that need it, and
the same for the UCS job's univention/.
The downloads that are not in a workflow. snapcraft.yaml builds the snap
in its own container, and sandstorm-src/build-deps.sh runs on the runner; both
still used a bare curl, and github.com's 503s took them out:
:: curl: (56) Connection died, tried 5 times before giving up
:: caddy: no linux/arm64 archive in Caddy 2.11.4 - nothing left to try.
==> [4/7] FerretDB v1 (amd64) at deps root
curl: (56) Connection died, tried 5 times before giving up
Both go through releases/fetch.sh now — the snap parts reach it through
CRAFT_PROJECT_DIR, since snapcraft mounts the project into the build — so the
caddy, MongoDB, mongod 4.2/5.0 and OpenSSL downloads, the meteor-spk and Node.js
tarballs and the FerretDB binary all wait an outage out. The Caddy version
lookup stays a plain curl: when it fails the pinned version is used, which
is what it is for. curl https://install.sandstorm.io | sudo bash became a download and a
run, because a pipe cannot be retried.
tests/workflowRepoScripts.test.cjs grew the two checks that would have caught
these: a repo-script path that is relative in a step which changes directory,
and a bare download in the snap build or the Sandstorm deps.
The v10.88 run lost seven jobs to two mistakes of the same kind: a step that needs something and does not check whether it is there.
The scripts were not on disk yet. Moving the downloads and the package
installs behind releases/fetch.sh and releases/apt-install.sh turned steps
that needed nothing into steps that need this repository:
bash: /home/runner/work/wekan/wekan/releases/apt-install.sh:
No such file or directory,
bash: D:\a\wekan\wekan/releases/npm-retry.sh: No such file or directory,
The first is build-extra-arches, where "Install dependencies" was the FIRST
step of the job, before actions/checkout — fine while it was a plain
apt-get. The second is the Windows jobs, which check this repository out to
path: src, so $GITHUB_WORKSPACE/releases is a directory that does not exist
there; they already called the other scripts as src/releases/…. The same two
shapes were in the Flatpak job (no checkout at all), Release All Missing's
extra-arches and its charts job (path: wekan), and the UCS job
(path: univention).
tests/workflowRepoScripts.test.cjs now reads every workflow and reports a step
that runs releases/… before its job checks out, or through a prefix that does
not match where that job put the repository. It also checks that every script a
workflow names exists here.
And the push that was never going to work. The docker job built every architecture, emulated, for the best part of an hour, and threw it all away on the last line:
ERROR: failed to push quay.io/wekan/wekan-ondra:v10.88:
unauthorized: access to the requested resource is not authorized
The credentials were fine — the login check passed. Quay grants push per
repository, and wekan-ondra had just been created, so the account that
pushes wekan and wekan-gantt-gpl had no rights on it. A registry will say
whether it would grant a push token in one request, so the job now asks — for
all nine images, before building anything — and fails in seconds with what to
change, naming the per-repository setting. A registry that does not answer is a
warning: that is the network, not the rights.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the rest of the afternoon github.com spent returning 503,
and one repository that had nothing to do with WeKan at all. Two more release
runs died: one downloading FerretDB, where curl --retry 5 --retry-delay 10 is fifty seconds of patience, and one on apt-get update, which fails as a
whole when any configured repository — the runner's Google Chrome one,
here — serves an index mid-republish. Both wait the outage out now. The other
half of both fixes is that a real failure is still immediate: a 404 is an
answer, not an outage, and an existence check that reads a 503 as "that
binary was never published" would drop an architecture that is sitting right
there on the release.
The binaries below are carried over from v10.87 and have NOT been checked
against a newer build; releases/provenance-table.sh prints the real table
from the provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following release-tooling bugs:
<details> <summary><a href="https://github.com/wekan/wekan/commit/a6edf86bc">A package index that is mid-republish no longer ends a release</a>. Thanks to xet7.</summary>The bump job of the same afternoon died on a repository the release does not
use:
E: Failed to fetch https://dl.google.com/linux/chrome-stable/deb/dists/stable/main/binary-amd64/Packages.gz Hash Sum mismatch
E: Some index files failed to download.
Error: Process completed with exit code 100.
It was installing python3 and curl. A GitHub runner comes with
google-chrome, microsoft-prod, azure-cli and docker repositories configured,
and apt-get update fails as a whole when any one of them serves an index
that does not match its own hashes - which is what a mirror looks like while it
is being republished.
releases/apt-install.sh installs the packages instead. It retries the update,
clearing the cached lists first - a Hash Sum mismatch is a cached index
disagreeing with the server, so re-reading it reports the same thing - and if
it still fails it moves the third-party lists aside and updates from the
distribution archive alone, which is where every package a release job installs
comes from. Both steps say what they did: a silent change of package sources
would be worse than the failure. A mirror that never comes back still fails the
job, saying it is the mirror.
Every apt-get update + apt-get install pair in Release All, Release All
Missing, the Sandstorm, meteor-spk and Flatpak workflows, and the emulated
build container, goes through it. tests/releaseAptInstall.test.cjs drives it
with a fake apt-get that mismatches on demand and a fake sudo that records
rather than runs - a test must not move the package sources of the machine it
runs on.
The second run of the same afternoon died one step later than the first, in
build-amd64, on the FerretDB binary:
curl: (22) The requested URL returned error: 503
Warning: Problem : HTTP error. Will retry in 10 seconds. 5 retries left.
...
curl: (56) Connection died, tried 5 times before giving up
Nothing was wrong with WeKan, and nothing was wrong with nodejs.org either -
the bundled Node.js downloaded and verified in the same step, seconds earlier.
It was github.com, and --retry 5 --retry-delay 10 gives it fifty seconds.
releases/fetch.sh is now what downloads a file in a release. It retries
5xx, 429, 408 and the connection errors on a backoff that adds up to
about fifteen minutes, and every download in Release All, Release All Missing,
the preflight scripts and the emulated build containers goes through it - with
the Dockerfile carrying it alongside resolve-node-source.sh, which now asks
it which Node.js builds exist.
The distinction it adds is the one a longer --retry cannot: a 404 is not
an outage. Several callers here legitimately ask "is this published for this
CPU?" and get "no" - the preflight that skips an architecture with no Node.js
build yet, the MongoDB Database Tools that are not built for every platform,
the .sha256sum a source may not publish. Those fail immediately and quietly.
Everything else waits.
And an existence check now has three answers instead of two: present,
absent, or the server would not say. That third one used to be
indistinguishable from "absent", which is how an outage could silently drop a
platform from the Docker image or skip an architecture whose binary was
published all along - a ::warning:: nobody reads until somebody on ppc64le
asks where their image went. It now stops the job and says to re-run it.
tests/releaseDownloads.test.cjs runs the script against a local server that
503s, 404s and 429s on demand, and reads the workflows for a download that
still goes straight to curl.
and has the following test-tooling fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/4c68a8a49">One browser test logging in no longer logs the other tabs out</a>. Thanks to xet7.</summary>The last WeKan test run failed one test in all three browsers - a test that had passed for a month:
02-cards-open-view.e2e.js:66 copy-link button produces a URL that
opens the card in full-screen view
Error: Token login failed: You've been logged out by the server.
Driving the running server over DDP with a token seeded the way the fixtures seed one shows what it is:
session A: ok tokens: [CfgBWImyytla]
session B: ok tokens: [CfgBWImyytla] <- two sessions, one token
after B logged out tokens: [] <- logout removed it
session C (same token): ERROR You've been logged out by the server.
A seeded test user has one resume token, and Meteor.logout() deletes it on
the SERVER — for every session using it. The login helper called it when a page
was logged in as somebody else, so switching users in one page stranded every
other page of that test. Only the copy-link test logs a second page in, which is
why it was the one that failed.
The helper now ends the previous session in the CLIENT instead: it drops the
three Accounts keys and reloads, which the helper already knows how to do for
its own first load. The token is untouched, and the page still arrives with no
user on it. logout() stays as its own helper, because logging out is a real
thing to test — 05-admin-users logs out and back in with a password.
Two pages are two browsers, so they now get two tokens: db.addResumeToken()
adds one to an existing user, and the second tab uses it. That tab also stopped
waiting for networkidle before looking for the card — a card is rendered when
the subscriptions land, which is not a network event a browser can be idle
about, and on a loaded machine the wait ended before the card existed.
tests/e2eSessionTokens.test.cjs pins both rules, including a scan of every
spec for two logins sharing one token.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the release workflow stops throwing a release away when
somebody else's server has a bad minute. Every download in it already retried;
the npm installs did not, so five minutes of 503 Service Unavailable
from github.com ended a release run in its first job and skipped everything
derived from it — eleven bundles, the Docker images, the snap. They now retry
with backoff, and a real npm error still fails on the first attempt.
The binaries below are carried over from v10.86 and have NOT been checked
against a newer build; releases/provenance-table.sh prints the real table
from the provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following release-tooling bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/644e61f6a">A five-minute outage at github.com no longer costs a whole release</a>. Thanks to xet7.</summary>The v10.86 run failed in build-amd64, installing the bundle's server modules:
npm error code E503
npm error 503 Service Unavailable - GET https://github.com/meteor/node-source-map-support/tarball/81bce1f9...
Nothing was wrong with WeKan. github.com was returning 503 for a few minutes,
and npm install gave up: npm's own fetch-retries is two quick attempts, and
it does not cover a git tarball fetch at all. Because every other Linux bundle
is repacked from the amd64 one, that one job took the whole release with it —
eleven architectures, the Docker images, the snap, all skipped — and it had to
be started again by hand. Every curl in that workflow has carried
--retry 5 --retry-delay 10 for years; the npm installs carried nothing.
releases/npm-retry.sh runs an npm command and retries it on backoff (15s, 30s,
60s, 120s, five attempts), and every npm install, npm pack and
meteor npm install in Release All now goes through it — including the two that
run inside docker run, which get the script mounted the way the arch builds
already mount releases/. Release All Missing repacks through that same
container script, so it inherits the retry.
The half that keeps it honest is what is NOT retried. A dependency conflict, a
404 for a package that does not exist, a gyp compile error: those fail on the
first attempt with not for a network reason, because five attempts at an
emulated arm64 install that was never going to work is half an hour spent to
print the same message. Only HTTP 5xx/429 and the socket errors — ECONNRESET,
ETIMEDOUT, EAI_AGAIN, socket hang up, a git clone whose remote hung up —
count as transient. An outage that outlasts all five attempts still fails the
job, saying it is an outage rather than WeKan.
The Meteor installer is fetched to a file and then run, instead of
curl https://install.meteor.com/ | sh: a pipe cannot be retried, because by
the time the download fails sh is already half way through the script.
tests/npmRetry.test.cjs runs the script against a fake npm to pin both halves,
and reads the workflow for a bare npm install — one unretried install is all
it took the first time.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a snap release and a batch of reported bugs. The snap half is
one theme in four places: a copy made at one moment being used as if it were
current. A MongoDB to FerretDB migration interrupted weeks ago and finished
by an update copied only what was left, so an overnight refresh could serve
boards and cards as they stood weeks earlier; an instance already running on
FerretDB was dragged back to the old MongoDB files it was migrated from, and
shown "Wekan cannot open the existing database" instead of its own working site;
a MongoDB 3.x database on any CPU without the 3.2 reader waited for a
database that was never coming, with the reason only in snap logs; and the
upgrade documentation let an admin copy the old database directory back over a
running database, which destroys the restore they had just made. Two copies
of one database are also reconciled automatically now — the newer is served
and the older is merged into its history — so an instance being shown the wrong
copy repairs itself instead of waiting for somebody to type two commands. Then:
a file:// link no longer makes a card impossible to open, a Worker can
finally move a card and assign themselves to it, an archived card is still
named in its own history, the PDF export writes umlauts instead of
question marks and no longer prints markdown at a reader, minicards follow
the Member Settings font size, and unchecking "Show on minicard" on a
checklist finally hides it. Below that: dependency updates, the Helm chart
moving to FerretDB with the release that publishes it, a repo-wide guard that
asks whether an already-fixed vulnerability exists anywhere ELSE - which found
one - and the tests for all of it. The binaries below are carried over from
v10.85 and have NOT been checked against a newer build;
releases/provenance-table.sh prints the real table from the provenance each
build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release updates the following dependencies:
@aws-sdk/* credential, signing and presigner packages it pulls in — the
optional S3 attachment storage. Unused unless S3 is configured.puppeteer-core and
@puppeteer/browsers 3.1.0 → 3.2.0) — the headless browser some tests drive.
It ships in no WeKan bundle.Thanks to dependabot.
and fixes the following bugs:
The snap - which database it serves, and what it says when it cannot.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c78382982">A migration interrupted weeks ago no longer resumes onto a database MongoDB has outgrown</a>. Thanks to Alishara and xet7.</summary>"Our snap updated automatically from 10.81 yesterday to 10.85 this morning. Data in the DB is from about 2 - 3 weeks ago. Many cards and work is lost."
Nothing was lost. The MongoDB data lives in $SNAP_COMMON and was untouched;
what the update completed was a migration that had been interrupted weeks
before.
The migration is resumable because it has to be: it can run for hours, and a
snap refresh, a snap stop or a reboot part-way through is normal rather than
exceptional. So the importer records every finished collection in
migration-progress.json and skips those on the next start. That checkpoint was
only ever checked against the TARGET - it is deleted whenever a partial FerretDB
is discarded, so it can never name collections that are not there. Nothing
checked it against the SOURCE, and between an interruption and the retry the
snap hands WeKan back to MongoDB and people go on using it. A migration
interrupted in July and resumed in August therefore skipped every collection it
had finished in July, copied only the rest, and switched the snap onto the
result.
A new snap revision is usually what sets a stalled resume going again, because the per-revision failure counter starts at zero - which is why this reads as "the update lost my data". The update is when the weeks-old copy finally got served. It is the same family as #6583 and has the same answer: a copy made at one time may not be used as if it were current.
Two checks now stand in the way, because they fail differently. Before the migration starts, and before it starts a mongod of its own - starting one rewrites the files the question is asked of - the checkpoint is compared against the MongoDB data files' timestamps. Newer MongoDB means the collections listed as done are copies of an older database, so that half of the checkpoint is dropped and they are copied again. The FILE half is kept: attachments are written once and re-verified on disk, and re-extracting gigabytes is the slowest part of a resume. Afterwards, when both databases are quiet, each collection is COUNTED on both sides; one whose copy holds fewer documents is copied again from the source as it is now. Only a shortfall is acted on - a copy holding more documents is a resume carrying documents deleted from MongoDB since, and deleting on that evidence is the guess this whole family of bugs is made of - and a shortfall that survives the second copy is reported rather than fatal, so one document FerretDB will not accept cannot leave the snap in a migration it can never finish.
The tests run the detector against synthesised $SNAP_COMMON directories - the
reported case, an ordinary resume minutes later, the margin that covers the
migration stopping its own temporary mongod, and a mongodb.log that must not
count as somebody having used the database - and exercise the count check
extracted from both importers.
"It somehow tries to access Mongodb again instead of Ferretdb. I don't even have an old version, but just this" - and the screenshot is the page from #6471, "Wekan cannot open the existing database", on a snap whose data was in FerretDB and perfectly readable.
The migration never deletes what it copied from, so a migrated snap keeps its
old MongoDB files in $SNAP_COMMON forever. Two places treated their presence
as something WeKan had to act on. The migration was called "pending" for any
$SNAP_COMMON holding those files without the completed-migration marker - the
database setting was never consulted - so an instance whose marker is gone (a
forced re-migration cleared it, or it was migrated by a snap old enough never to
have written one) probed the old files on every start, and when no reader could
open them the site was replaced by an explanation of a database it does not use.
Both now ask what the snap is actually running, and "already on FerretDB" means
the setting AND real data: database=ferretdb with an empty files/db is
exactly the case the migration exists for and must still run. Nothing is
switched automatically - #6583 is
what choosing between two copies on the snap's own initiative costs - but where
a FerretDB copy exists, the page now says so and gives the one command that
serves it, the one that undoes it, and the warning that a migrated copy is only
as new as the migration that made it.
"It turns out the mongodb version on my installation was even older. It was running on mongodb 3.2, this is why your 4.2 check was not doing anything." Their site never showed the explanatory page either - it waited for MongoDB forever, which is the loop #6471 was opened about.
The MongoDB 3.2 tools are staged for amd64 only, because MongoDB published no
3.2 build for anything else. So on every other architecture a MongoDB 3.x
database has no reader in this snap at all - and that case was handed back to
mongod 7, the binary that has already refused the files. It fails, the migration
re-runs, and the site sits on "MongoDB not ready yet, retrying in 5 seconds..."
with the reason only in snap logs.
A missing reader and an unreadable database are not the same thing, and only one
of them can usefully be retried. Where mongod has already said "too recent to
start up on the existing data files", the reader not being in this snap is a
final answer: stop, keep every byte, serve the page. The page also stops leaving
the way out as an exercise - it gives the four steps in this snap's own paths,
ending in snap run wekan.database-restore, rather than "move the data across
with a MongoDB that can read it".
An email report, on an instance being served the older of its two copies: "some users are unable to log in (error: 'user not found'), and boards created after mid-July appear to be missing." That is what serving a copy looks like from the outside — the accounts and boards made after the copy was taken are simply not there.
Until now the snap handled that by handing it back to the admin.
#6583 taught it not to switch on a
guess, so when both MongoDB and the migrated FerretDB have been written to since
the migration it printed the two snap set wekan database=... commands and
stayed where it was. That message lives in snap logs; most people never see
it, and their site meanwhile shows the wrong copy.
A file timestamp cannot answer "which copy holds the work" — an mtime says when a file was touched, and starting a database touches its files. But both copies can be READ. Each is started on a temporary port, asked how many documents it holds and what the newest moment in its data is, and the copy holding the work is served. Where both hold something the other does not, the documents that exist only in the other one are copied across, and where the two cannot be told apart nothing is changed and the old message stands.
The merge is what makes this safe to do automatically, and WeKan's own design is
what makes the merge safe: the history is append-only, so activities, comments
and the coming change-history rows can only be ADDED to. Every document whose
_id is absent from the chosen copy is inserted and nothing else happens — what
is already there is never overwritten, so a card edited on both sides keeps the
newer version; nothing is deleted on either side; and the copy that was not
chosen stays on disk, so switching back is still one command, now a choice
rather than a repair. The work done on the copy that is not being served becomes
readable in the served copy's card History instead of sitting in a database
nobody opens.
For the reported symptoms that means the missing users and boards are inserted
rather than left behind. Reconciling two edits of the same field is still not
attempted: that is a decision about somebody's work. WEKAN_AUTOPICK=false
turns the whole thing off and database-autopick --dry-run shows what it would
do.
docs/Features/Reports/History/History.md
gains the section that states the append-only invariant this depends on.
From an upgrade report by email. The admin upgraded 6.09 to 10.85 exactly by the
documented route - dump, move /var/snap/wekan/common aside, refresh, restore -
and it worked: the boards were back. Then, to get their attachments, they undid
that step the way it reads, with cp -pR /root/common/* /var/snap/wekan/common/, and seconds later mongod aborted (status=134/n/a).
"After that, Wekan was still running, but all the boards were missing."
Everything beside files/ in that directory is the raw database, and copying it
over a RUNNING mongod replaces the files it has open underneath it. The
documentation is where this is decided, because the mistake is made before any
WeKan code runs: the step already said to copy back only files, and it now
says what the obvious inversion costs, which files those are, and what the
failure looks like in snap logs wekan.mongodb so somebody who has already done
it recognises their own log. mongodb-control recognises that abort too - exit
134 gets a case of its own beside the AVX one, naming the cause and the one way
out.
The board - what a card looks like, and what an export says.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7b35cb5f0">A Worker can move a card and assign themselves to it, which is what the role is for</a>. Thanks to rptl and xet7.</summary>"User with Worker permission can't assignee card if it has been assigned to someone else before" — and in fact could not assign themselves at all. The card showed their name for a moment and then showed the previous assignee again, which is what a rejected optimistic write looks like.
The board schema defines the role as "only allowed to move card, assign himself to card and comment". Both of those are card updates, and the capability table gives Worker no write access — so the role defined by two specific writes was allowed neither, while the client already offered the UI for it: the assignee popup shows a Worker exactly one name, their own.
Widening write access was never the fix; that hands a Worker every field of
every card. Moving and self-assigning are their own capability now, enforced
field by field on the server: a Worker may write listId, swimlaneId, sort
and their OWN id in assignees, and nothing else. The policy allows only what
it recognises, so a title, a label, somebody else's name, a whole-document
replacement or an operator added by a future MongoDB are all refused by default.
Roles.md
gains the column, and its "Known gaps" section is now empty.
"Activities for archived card displayed as undefined on board settings." Move a card around, archive it, open the board sidebar — and the sentences that named that card name nothing.
The feed asked for the card document and rendered its title, and an archived
card is not published to the client: a card that still exists, still has a
title, and whose activities are right there on the page went nameless in its own
history. The activity itself already recorded the title in most cases, so that
is what is read now — the card's current title when the card is here, marked
[archived] when it says it is, the recorded title when it is not, and "this
card" when neither exists, rather than a gap in the middle of a sentence. The
link survives all of it, because a card URL can be built from the ids the
activity carries.
The two activities that were NOT recording a title were the two about archiving, which are exactly the ones guaranteed to be about a card the client can no longer look up. They record it now. A card that is merely absent is not called archived: with lazy card loading it may just be outside the window this client was sent, and that would be a claim the feed cannot support.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/75a23b76a">A file:// link in a card no longer makes the card impossible to open</a>. Thanks to rmb82 and xet7.</summary>"A card whose description or a comment contains a file:// URL cannot be
opened. Clicking the minicard plays the open animation but the card details
panel never mounts." There was no visible error, because Blaze swallows a render
exception; captured, it was TypeError: this.__schemas__[...].validate is not a function, thrown out of markdown-it's linkify pass.
WeKan registers eight custom URL schemes — file:, thunderlink:, onenote:
and five more — and registered them the linkify-it 4/5 way, passing the string
'http:' to mean "behave like that one". linkify-it 6 removed string aliases
and builds the definition by spreading it, so that string became
{0:'h',1:'t',2:'t',3:'p',4:':'} — an entry with no validate — and the
recogniser then called .validate(...) on it. Every one of those schemes was a
landmine in any card's text.
Each scheme carries a validate of its own now, which is all the alias stood for. The guarantee behind the fix is worth more than the fix: nothing a card contains may make that card impossible to open, so the render is wrapped and a throw from any future plugin, formula or upgrade shows the text as written — escaped, sanitised, unformatted and readable — instead of a panel that never mounts.
The schemes still do not produce clickable links, and never did: markdown-it's
own link validation refuses file:, and the viewer's sanitiser allows only
http/https/ftp/ftps/mailto/tel/callto/cid/xmpp. Making them clickable means
relaxing both for schemes that launch local applications, which is the security
decision #3218 asks for; it is in
TODO Later, and the tests pin today's answer so that changing it is a decision.
"The font size setting in the user menu (Settings - Font Size) has no effect on mini cards, while it correctly applies to other UI elements."
Two halves, and both had to be wrong for this. The preset was applied to
<body>, and rem is measured against the ROOT element, so anything sized in
rem never heard about the setting. And a board's type is sized in px - the list
heading, the minicard title and the "Add card" link were pinned to 16/14/13px in
#6465 - which follows nothing at
all, so the board was the part of the UI the setting could not reach whichever
element carried it.
The preset moves to <html> and those sizes become rem: the same 16/14/13px at
the default root size, so the type scale is unchanged, but a 130% preset now
scales it and 80% shrinks it. The size goes on one element only, because on both
html and body a 130% preset compounds to 169%.
Reported by email with a screenshot: the switch is off and the checklist is on
the minicard anyway. It was off for everybody, because the minicard asked
board.allowsChecklistsOnMinicard || checklist.showChecklistAtMinicard and the
board flag defaults to true. An OR cannot be argued with: while the board
setting is on, no value of the checklist's own field changes the answer. The
popup made it look like a working switch - it drew the state from the raw field,
which starts false, so it read OFF beside a checklist that was plainly showing,
and clicking it changed nothing visible in either direction.
The two settings are a DEFAULT and an OVERRIDE now, which needs three states:
the field loses its defaultValue: false, because with every checklist born
false "hidden" and "not chosen" are the same value. Checklists written under the
old default carry a false that meant "follow the board", so a schema-upgrade
step clears exactly those - once ever, not once per version, since afterwards a
stored false is somebody's choice.
"the umlauts (german, ä, ü, ö,...) are corrupt", "all the text in this PDF file is markdown formatted - this doesn't make sense in a pdf file, does it?", and "I can't see in which swimlane a card is in that export, no tags".
The umlauts were removed on purpose, one line before anything was written: every
character outside printable ASCII became a question mark, so "Grüße" left the
server as "Gr??e". That was a workaround rather than a choice - text in a PDF is
bytes plus an encoding, and the exporter declared neither, so there was nothing
safe to write those letters into. The font now declares WinAnsiEncoding, the
text is encoded to those single bytes, and the file is assembled as binary; the
cross-reference offsets are measured in bytes for the same reason, since
measured as UTF-8 every offset after the first accented character is wrong.
Markdown is flattened to its words instead of printed as syntax, list headings
and card titles are drawn in the bold font rather than with ##, and the board
export now names swimlanes, labels, members, assignees and dates. What
Windows-1252 has no room for is transliterated rather than erased; a script the
base-14 fonts cannot draw at all still degrades to ?, and an embedded Unicode
font for those is in TODO Later.
and changes what the Helm chart installs and how it is published:
The Helm chart - the database it installs, and the index that lists it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5d1c9d0a3">This release switches the Helm chart to FerretDB, and its release job is what publishes it</a>. Thanks to salcinad, ouvry-ems and xet7.</summary>wekan/charts drops its bundled MongoDB for
FerretDB (ghcr.io/wekan/ferretdb), installed by the chart itself as one
StatefulSet and one ClusterIP Service. That answers
charts#55 — WeKan runs on FerretDB
and the chart did not — and
charts#54: the chart built its
MONGO_URL out of a different chart's naming, Bitnami's per-pod
<release>-mongodb-0.<release>-mongodb-headless against the services
groundhog2k actually creates, so WeKan dialled a host that does not exist. The
database is the chart's own now, so the Service in the URL is the Service the
chart creates.
The chart reaches people with THIS release, not before it. The charts job of
the Release All workflow publishes chart <version>.0 from the charts repo's
main branch, and that branch now holds the FerretDB chart — so the release
publishes it, with an image that exists and an index entry written by the script
that owns the index. Nothing was published out of band, and no existing entry in
the index is touched: charts already in it keep their package and their digest.
The image is one that can be PULLED, which took finding out. Artifact Hub's scan
of the chart said error scanning image ghcr.io/wekan/ferretdb:latest: image not found — because a GHCR package is private until somebody makes it public, and
the WeKan organisation had public packages disabled entirely, so the setting was
greyed out. In a cluster that is ImagePullBackOff, not a scanner complaint.
The organisation policy and the package are public now, so
ghcr.io/wekan/ferretdb is the default, with quay.io/wekan/ferretdb and
Docker Hub's wekanteam/ferretdb beside it — all three verified to serve the
full multi-arch set. The chart keeps the story, because "check the package's
visibility" is the first thing to try if a pull ever fails that way again.
Chart.yaml also declares its images and its changes to Artifact Hub now, written
at release time from the version being released, so the scanner reads a list
instead of inferring one.
The chart carries what WeKan needs on FerretDB rather than what it needed on
MongoDB — polling reactivity, sockjs, WRITABLE_PATH, WITH_API, no
MONGO_OPLOG_URL, and directConnection=true in the URL
(#6582) — and each of those says,
where the setting is, what it would be on MongoDB instead, with both of WeKan's
compose files linked and the production notes
(docs/Platforms/FOSS/Container/Docker/Meteor3)
pointed at from values.yaml, the README and the URL helper. The image comment
names the three registries that carry FerretDB and the Docker Hub mongo:7 it
replaced. Plus every setting docker-compose.yml documents, commented, so a
Helm user has the same reference a Docker user has.
Two release-path guards come with it. The version bump a release performs cannot
touch tag: latest, so a WeKan version bump can never rewrite the database
image tag. And filling holes in the index for OLD releases
(releases/backfill-charts.sh) stops at WeKan 10.00, the release FerretDB
became the default in: it packages today's chart, and giving a v6.09 image a
FerretDB chart would publish an install nobody has ever run.
wekan-ondra and wekan-gantt-gpl are the same WeKan as wekan: those
repositories are byte-identical to wekan/wekan apart from the snap name: in
snapcraft.yaml. Their Docker images have therefore never been rebuilt — they
were retags of the released manifest, published when somebody remembered to
start docker-variant.yml by hand, which is how an image comes to name a
version newer than the bits inside it.
They are now tagged in the release's own docker buildx build --push, beside
wekan itself, on GHCR and Docker Hub and Quay — eighteen tags from one
build. Being in the same build is the point: the variant tags carry the
release's own digests for every architecture, there is no second emulated build
to go wrong, and there is no window in which a variant image can differ from the
release it names. docker-variant.yml stays, for republishing one out of band
when a registry was down or a repository was created after the fact, and that is
work a human should start.
Two guards, because a tag that is pushed is not a tag that anyone else can pull.
The existing verification asks the registry about all nine images with the
release's own credentials; a new step then asks for each one anonymously,
the way a user does, and a 401/404 there fails the job with what to fix. It
matters on the first release after this: Quay creates a new repository
private, so quay.io/wekan/wekan-ondra will exist, will pull for the
pusher, and will not pull for anybody else until its visibility is changed —
the same trap ghcr.io/wekan/ferretdb fell into. A registry that cannot be
reached at all is a warning, not a failure: that is the network, not the
release.
Release All Missing has no Docker part to change: its jobs are plan, extra-arches, appimage, flatpak, charts and done, and it fills in artifacts for versions whose image already exists.
If a variant repository ever stops being identical to wekan/wekan, those two
tags become a lie and that variant needs its own build — which is written where
the tags are, and in
Snap-Ondra-Gantt.md.
Two guards on the release path, found by checking the Release All and Release All Missing workflows against the chart change rather than assuming they still fit.
A release rewrites exactly three things in the chart: appVersion, the chart
version, and the WeKan image tag. That last substitution matches tag: v<digits> and every other image in the chart is tag: latest — FerretDB and
the two busybox images — so a WeKan version bump cannot reach the database
image. The test asserts that against the pattern itself rather than a copy of
it, and the whole release was simulated for a hypothetical 10.86: no
dependencies, FerretDB enabled, both its templates in the package, and the
FerretDB URL helper.
releases/backfill-charts.sh fills holes in the published index by packaging
TODAY's chart with an old release's numbers on it, and today's chart installs
FerretDB — which WeKan did not default to until v10.00. A backfilled chart for
v6.09 would pair that image with a database nobody ever ran it against,
published under a version number that says it is that release's chart. It now
stops at 10.00, reports the older ones rather than dropping them silently, and
CHART_FERRETDB_FLOOR overrides it.
From an Artifact Hub scan report of the chart repository:
error scanning image ghcr.io/wekan/wekan:v9.62: image not found
(package wekan:9.62.0)
and six more like it. Checked against the live registry, the index itself is
already clean — every one of its 230 entries points at an image that exists,
because releases/reindex-charts.py asks the registry about every image a
package pins and leaves out the ones whose image is gone.
The hole was in the backfill: it rebuilt the index with helm repo index --merge, and helm indexes what it FINDS. 135 packages on that branch have an
image that no longer exists — six WeKan images that were never published, and
129 charts vendoring a Bitnami MongoDB image Bitnami has since deleted — so one
run would have put all of them back and produced the same report again. It now
uses reindex-charts.py, so "a package whose image is gone stays out of the
index" is one rule in one place rather than two tools that disagree. A registry
that cannot be REACHED is still never read as "image gone": that would drop good
entries on a network hiccup.
The duplicate-entry repair that followed the merge went with it — --merge was
what produced the duplicates, and one entry per package cannot duplicate.
and has the following test coverage work:
<details> <summary><a href="https://github.com/wekan/wekan/commit/eb47e0465">A guard that asks whether an already-fixed mistake exists anywhere else</a>. Thanks to xet7.</summary>Every entry in the Hall of Fame has a suite pinning the place it was found, and none of them could answer the other half of the question: is the same MISTAKE somewhere else, in WeKan or in the FerretDB fork WeKan ships as its default database? A per-site regression test knows one file, and the next occurrence is written months later by somebody who never read the advisory.
The new guard asks it as a class, over the whole tree and over .tools/FerretDB
when the clone is there: an archive entry naming its own destination, a client
selector carrying operators that execute JavaScript, TLS verification switched
off (including Go's InsecureSkipVerify), eval, an SVG served inline
unsanitised, and a secret from a non-cryptographic source or folded onto an
alphabet with a biased %. Comments and strings are stripped before matching,
or the notes explaining a fix would trip the check enforcing it, and every check
was confirmed to FAIL on the mistake before being kept.
It found one immediately: the selector guard rejected $where and nothing else,
but $where stopped being the only way a find filter runs JavaScript in MongoDB
4.4 - $expr with $function, and $accumulator, do the same through the same
client-supplied selector. Both are rejected now. A release entry that had
shipped with no test at all - the two Admin Panel / Problems database bugs -
gets one too.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: one fix, and it is to the browser console rather than to
anything a user sees. Firefox logged a warning for every Font Awesome glyph
whose stored bounding box was tighter than its own outline, on every page load -
819 of 2163 glyphs across the four font files - which buried anything else
worth reading there. The boxes are recomputed, with a script to redo it after a
font upgrade and a guard so the warnings cannot come back unnoticed. The
binaries below are carried over from v10.83 and have NOT been checked against a
newer build; releases/provenance-table.sh prints the real table from the
provenance each build job records.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f368c1a2a">Font Awesome: state the bounding box each glyph actually has</a>. Thanks to xet7.</summary>Firefox logged one warning per glyph, on every page load, for every Font Awesome file WeKan serves:
downloadable font: glyf: Glyph bbox was incorrect; adjusting (glyph 19)
(font-family: "Font Awesome 6 Free" ...) source: .../fa-regular-400.woff2
819 of the 2163 glyphs were affected - 540 in fa-solid-900, 204 in
fa-brands-400, 59 in fa-regular-400, 16 in fa-v4compatibility - so the
console filled with hundreds of lines and stopped being useful for spotting
anything else.
Every TrueType glyph stores its own bounding box in the glyf table. Font
Awesome ships boxes that are TIGHTER than the outline: they bound the on-curve
points only, while the box has to bound the control points too, because a
quadratic curve can bulge past its endpoints. Firefox's OpenType sanitiser
notices, corrects each box in memory and says so. Nothing rendered wrongly - the
warning was the whole of the damage - but the numbers in the file were wrong,
and they are wrong upstream, in Font Awesome's own build.
releases/fix-font-bboxes.py recomputes them, and writes the .ttf and the
.woff2 from one corrected font so the pair cannot drift. Only head and
hhea differ from what Font Awesome shipped, and both follow from the boxes;
every other table compiles identically, which the script verifies by reading
back what it wrote. It keeps Font Awesome's own head.modified, so two runs
over the same input agree byte for byte rather than churning a committed binary.
tests/fontGlyphBounds.test.cjs parses the fonts with its own reader rather
than the tool that wrote them, and that guard matters more than the fix: these
fonts are VENDORED, so the next Font Awesome upgrade drops fresh upstream files
straight back into the tree and the warnings would return silently, months from
anything that would explain them. The failure message names the script to
re-run.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: one fix, to the snap, and it is a fix to the previous
release's fix. The guard v10.82 added so an out-of-date FerretDB copy could not
be served had the opposite failure of the bug it fixed: it decided which copy
was newer by comparing MongoDB's file timestamps against the migration marker,
and STARTING mongod rewrites those files - so a single service start during a
refresh made a frozen MongoDB look newer than the FerretDB that had been live
for two weeks, and the snap was switched onto the frozen one. It now asks the
question of BOTH copies, and when both have been written to since the migration
it switches nothing and says so, because a timestamp says when a file was
touched and not how much is in it. Below that, the three newest interface
strings are translated into 133 languages. The binaries below are carried
over from v10.83 and have NOT yet been checked against this release's own build;
releases/provenance-table.sh prints the real table from the provenance each
build job records, and it heads this release's GitHub release notes.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/1993d7d3c">Snap: a started mongod is not a used mongod, so stop calling the live copy stale</a>. Thanks to lukechao, markusst1982 and xet7.</summary>The staleness guard added for #6583 had the opposite failure of the bug it fixed:
A couple of weeks ago, I did a snap revert ... but then completed the migration successfully. Today, my database suddenly reverted to an old version from what looks like weeks ago. Upgrading to 10.83 did not fix the problem automatically.
Their FerretDB was the live database and had been for two weeks; MongoDB was the
frozen one. The guard decided otherwise because it compared exactly two things:
the newest mtime under the MongoDB data directory, and the migration marker.
Starting mongod rewrites those files - recovery, and the checkpoint it writes
on startup - so one service start during a refresh put MongoDB's newest mtime at
today against a marker from two weeks ago. The guard called the live copy stale,
wekan-control set database=mongodb, and what came up was the data as it
stood on the day of the migration. Upgrading could not help, because the upgrade
was the cause: this runs at every start, so every start re-applied it.
An mtime cannot tell somebody used this database from this database was started, so asking it of one copy cannot answer the question. Asking it of both can, because the case the guard exists for has a shape the mistaken one does not. The migrated copy untouched since the migration while MongoDB moved on is STALE - nothing has been using FerretDB. The migrated copy moved on while MongoDB did not is CURRENT, the normal state after a successful switch. Both moved on is AMBIGUOUS: two databases have been written to since they were copies of each other, and there is no answer there, only a choice, and it is the admin's.
Only the first may be acted on automatically. The ambiguous case switches
nothing, prints both databases' last-written times and the two commands to look
at each, and says that nothing was changed or deleted - both copies live in
$SNAP_COMMON, which snap revert does not roll back. That restraint matters
most in the branch of mongodb-control that DELETES files/db to migrate again
when mongod cannot start at all: on ambiguity the SQLite holds work of its
own, so wiping it would destroy the very copy in doubt.
The message has no database=ferretdb condition on it, deliberately. An
instance the old guard already moved to database=mongodb is sitting on the
wrong copy now and that setting persists, so speaking up only when FerretDB is
selected would leave it there silently. Whichever side is selected, the admin
hears that the other one holds writes of its own.
tests/ferretdbMigrationStale.test.cjs gains the reported regression - a
two-week-old migration, a FerretDB written to a minute ago, a mongod started
an hour ago - and pins that ambiguity can never reach the deletion.
and improves the translations:
<details> <summary><a href="https://github.com/wekan/wekan/commit/639214574">The three new Version-pane and checklist strings, in 133 languages</a>. Thanks to xet7.</summary>invalid-year, collapse-checklist and expand-checklist shipped in
en.i18n.json with the card-date fix and the collapsible checklists; every
other language file carried them as English placeholders. Translated directly,
as CLAUDE.md requires - no external translation service, API or key - from
each language's OWN existing strings, so the wording matches what that file
already says rather than being invented beside it.
Three anchors did most of the work. checklist and collapse / uncollapse
give each language its established terms, and invalid-domain is the same shape
of sentence as the new one - a rejection, then an instruction with an example -
so its phrasing, punctuation and register carried over directly.
Where an anchor was itself wrong the correct term was used instead of copying the mistake forward. Several files have terms that drifted in from another language: Italian Non collassare in the Greek and Romanian files, Russian in the Georgian and Mongolian ones, Vietnamese in the Thai one, Serbian in the Slovenian and Bulgarian ones. Others use a literal sense of "collapse" that is not the UI one - Azerbaijani Yıxılma, Estonian Kokkupõrge, Khmer ដួលរលំ and Chinese 崩溃 are structural collapse, a building falling down. The new strings use the folding sense each language actually uses for this control.
Nine languages are deliberately left as English placeholders rather than guessed
at: Klingon, Volapük, Tamazight, Walloon, Wolof, Uzbek in Arabic script, and the
three ve files, whose contents disagree with their own locale tags - ve-CC
reads as Venetian and ve-PP as Veps, so which language to write is a question
about the file, not about the string. A placeholder says "nobody has translated
this yet", which is true; a fabrication would say something false in a shipped
product.
Applied with fill-translations.mjs --apply, which writes ONLY into
placeholders: every language reported filled 3, skipped 0 existing human
translation(s), so no human translation was touched. Key order and the 2-space
indent are preserved, all 154 files still parse, and
verify-human-preference.mjs passes 10/10.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a CRITICAL SECURITY ISSUE, PassBleed: the single-card
Excel export authorised against the board named in the URL and then read the
card named in the URL, with nothing tying the two together. Any authenticated
user could create their own public board, name it as the board, and export any
card from any private board on the instance - including the bytes of its image
attachments. The identically shaped PDF route had always resolved its card
correctly, which is what showed this was an omission rather than a decision, and
it is what the Excel exporter now does. It also fixes broken avatar images,
seen after upgrading from v6 but never actually working: the route that serves
them asked Meteor.userId(), which throws in a plain HTTP handler rather than
answering "nobody", and the handler turned that into a 500 - and, once that was
fixed, that the same route had always ignored the boardId the client appends
so a public board can show its members' pictures to visitors.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.49.0 | 7c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5 |
| armhf | Node.js | wekan/node-patches | v24.19.0 | b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa |
| armhf | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| armv6 | Node.js | wekan/node-patches | v24.19.0 | 128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1 |
| armv6 | FerretDB | wekan/FerretDB | v1.49.0 | 7c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | 8dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45 |
| armv7 | FerretDB | wekan/FerretDB | v1.49.0 | 144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4 |
| i386 | Node.js | wekan/node-patches | v24.19.0 | 3b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac |
| i386 | FerretDB | wekan/FerretDB | v1.49.0 | 1f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.49.0 | 37d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.49.0 | 7c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.49.0 | bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.49.0 | no checksum published |
| win-arm64 | Node.js | nodejs.org | v24.19.0 | 8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f |
| win-arm64 | FerretDB | wekan/FerretDB | v1.49.0 | 792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.49.0 | f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478 |
This release fixes the following CRITICAL SECURITY ISSUE of PassBleed:
The single-card Excel export - which card it is allowed to read.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5269d0ba5">PassBleed: the export authorised against one board and read a card from another</a>. Thanks to TWPaMWang and xet7.</summary>PassBleed -
GHSA-6p5m-f9p2-wqm5,
Moderate, CWE-639, CVSS 6.5. GET /api/boards/:boardId/lists/:listId/cards/:cardId/exportExcel checked whether
the caller could see the board in :boardId, then resolved the card by
:cardId alone. Nothing confirmed that the card was on that board, so the two
identifiers came apart: one decided the authorisation, the other decided the
data.
The pass is self-service. POST /api/boards takes permission straight from
the request body, so any authenticated user could mint their own PUBLIC board,
name it as :boardId, and pass the id of a card in somebody else's private
board as :cardId. :listId was never used in a query at all and could be any
string.
What came back was the card: title, full description, members and assignees,
every comment with its author, checklists and checklist items, subtask titles,
attachment metadata - and, because image attachments are read through
getReadStream() and embedded with workbook.addImage, the attachment BYTES.
The same board could be reused while :cardId was substituted, which made it a
scriptable bulk read rather than a single disclosure. The REST API is on by
default in the shipped Docker configuration.
The fix was already in the codebase one file away: the identically shaped PDF
route has always resolved getCard({ _id, boardId, listId }) and 404s a
cross-board id. That control is what shows the Excel exporter's omission was a
defect rather than a decision, and it is what the Excel exporter now does.
Constraining the QUERY matters more than a check after it - the exporter fans
out on the same card id for checklists, subtasks, comments and attachments, none
of which carry a board constraint of their own, so a card that cannot resolve
outside the authorised board makes all of them safe by construction.
The route binds the two identifiers as well, before either branch builds - deliberate duplication, because that is where both arrive together and it covers the public-board branch, which skips authentication entirely. A card that is not on the named board is a 404 rather than a 403, so the difference does not reveal whether a card id exists.
</details>and fixes the following bugs:
Avatars - the routes that serve a profile picture, and who they serve it to.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bf047d53c">Ask the request who it is, because Meteor.userId() cannot</a>. Thanks to markusst1982 and xet7.</summary>Following the same upgrade as #6583, profile pictures came back as broken images - initials rendered fine, and the Admin Panel showed a user's picture while a board showed the missing-picture icon for the same person.
Nothing was lost, and the migration is not at fault. The avatar files migrate,
and the Meteor-Files record made from a CollectionFS filerecord even reuses its
_id, so a 6.x URL still names the right object. What broke is the request for
it. A 6.x install stores profile.avatarUrl as /cfs/files/avatars/<id>; that
route serves the legacy bytes if they are still there and otherwise redirects to
/cdn/storage/avatars/<id>, which asked who was asking with Meteor.userId().
That reads the current DDP invocation's environment, which exists inside a
method or a publication and NOT in a WebApp handler - where it does not return
"nobody", it THROWS. The handler wraps its body in a try/catch that answers
500, so the throw was swallowed into a broken image, and no avatar served
through that route ever reached anybody on any install. The upgrade did not
cause it; it moved every avatar URL onto the route where it already applied.
server/routes/legacyAttachments.js had the identical call, so legacy
attachment URLs failed the same way.
An HTTP request carries its identity in the request: a bearer token, an
X-Auth-Token header, an ?authToken= parameter, or the login cookie - and on
Sandstorm, a platform-injected user id and no Meteor token at all.
server/routes/universalFileServer.js has always resolved it that way and
serves attachments correctly today. server/lib/requestUser.js lifts that
resolution out so the two routes that were guessing share it rather than grow a
third copy. It never throws: a caller deciding whether to serve a file wants an
answer, not an exception its own catch will turn back into a 500.
tests/requestUserAuth.test.cjs pins that neither route calls
Meteor.userId(), that both await the resolver - an unawaited Promise is
truthy and would authorise everybody - that all four token carriers and the
Sandstorm path are handled, and that the migration still reuses the id the old
URL names. Confirming the served image needs an upgraded instance.
Found while checking why the Admin Panel showed a picture that a board did not.
The two URLs differ in one thing: the avatarUrl helper in
client/components/users/userAvatar.js appends ?boardId=<id>, and says why in
its own comment - "so public viewers can access avatars on public boards". The
Admin Panel uses profile.avatarUrl raw.
/cdn/storage/avatars/:fileName never read that parameter. It required a
signed-in user and nothing else, so on a public board every visitor who was not
logged in got a 401 and the missing-picture icon - the exact case the parameter
was added for. Fixing Meteor.userId() alone would have left that half broken.
The named board must now exist, be public, AND have the avatar's owner as a member. The last part is not ceremony: without it, naming any public board would unlock any avatar on the instance, and a public board publishes its own members, not everybody.
The legacy redirect keeps the query string too. /cfs/files/avatars/<id> 301s
to /cdn/storage/avatars/<id>, and that is the path EVERY migrated 6.x avatar
URL takes, so dropping ?boardId= there would 401 exactly the installs the
entry above sets out to fix.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a CRITICAL SECURITY ISSUE, WhereBleed: eight Admin Panel
handlers took a query selector from the client and checked only its type, so a
$where in one made the database run the caller's JavaScript - a repeatable
denial of service, reachable by a per-tenant admin. The detector for it was
already in the codebase and wired into one publication; the eight siblings never
called it, and now share the one copy. Then the snap, where two permanent
markers meant an instance that had failed on an older revision never retried on
the fixed one, so the MongoDB 4.2 reader added for it never ran.
Notifications grew an unbounded array inside the user document that SQLite
was rewriting on every addition, which is the slow login and the pinned CPU.
Clicking an open card closes it again, and a focused Admin Panel checkbox is
no longer drawn as a diamond. It also adds the first new feature in this
release: checklists and card feature groups fold away, on the opened card
and on the minicard, asked for since 2018. Below that: a typed two-digit year
refused rather than stored as the year 26, the Helm chart index listing only
charts that can be installed, and a way to remove the Templates containers made
for accounts that never used them. And two developer-facing fixes: the
database-conformance stage no longer opens a debug port nothing in it uses - one
taken by an unrelated FerretDB made every backend report a database problem that
was not one - and two more snap give-up paths that deleted the directory their
own stage filter names. The binaries below are v10.81's: nothing here rebuilds
them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.48.0 | 2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.48.0 | 06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.48.0 | 4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.48.0 | 0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6 |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.48.0 | d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.48.0 | 6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.48.0 | ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2 |
This release fixes the following CRITICAL SECURITY ISSUE of WhereBleed:
The Admin Panel's People, Org, Team and Translation panes - what a query from the client is allowed to be.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b4ebe48d7">WhereBleed: eight Admin Panel handlers ran the caller's selector unchecked</a>. Thanks to TungNGo02 and xet7.</summary>WhereBleed -
GHSA-phm4-4v26-j2vq,
Moderate, CWE-943, CVSS 5.8. The people, org, team and translation publications
and their companion count/page methods take a query selector from the client and
validate only its TYPE - check(query, Match.OneOf(Object, null)) - which is
not validation, because a MongoDB selector is executable data. $where makes
the database run the caller's JavaScript once per document scanned, so
Meteor.subscribe('team', { $where: 'while(true){}' }, 25, 0) pins a database
worker for as long as the caller likes, repeatably: denial of service for every
tenant on the instance from one narrowly-scoped account.
The reporter demonstrated both halves on v10.81 against a real MongoDB 7:
$where: 'sleep(2000) || true' made the subscription take 2.03s and return the
document, $where: 'false' returned nothing in 0.00s - the caller deciding, in
JavaScript, which documents come back.
It needs an authenticated admin session, so no board member or visitor can reach
it. It matters at this severity because the people and org surfaces are open to
a per-tenant Global Admin, a role meant to be confined to one Organization,
and those two wrap the caller's selector as { $and: [query, restriction] }
rather than stripping execution operators out of it - so merging the tenant
restriction never removed the $where, and a role scoped to one tenant reached
instance-wide impact.
What makes this one particular is that the defence was already in the
codebase. classifySelector and hasWhere were written for exactly this
class, are unit-tested, and were wired into the card-window publication. Eight
sibling handlers taking the identical shape of selector simply never called
them. So the fix adds no new detection logic: that publication's own helper
moves unchanged into a shared module and all nine call sites use the one copy -
a second copy would be the same bug set up to happen again.
Each handler refuses with the "match nothing" selector the card window already
uses in production, { _id: { $in: [] } }, so a refused request returns an
empty result instead of throwing at an admin mid-page. Ordinary behaviour is
untouched: none of the searches, filters, regexes,
$or/$and/$in/$elemMatch or date ranges those panes send carries an
execution operator.
FerretDB, WeKan's default database, rejects $where itself, so this degrades to
a rejected query there; the supported MongoDB path is where it was reproduced.
MongoDB 7 also happens to reject $where inside the aggregation pipeline the
count methods use - but that is an engine accident for one operator on one call
path, so those methods are guarded like the rest rather than left to it.
and adds the following new feature:
Cards - folding away what you are not reading.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1cef5ede9">Checklists and card feature groups collapse, on the opened card and on the minicard</a>. Thanks to czinkos, MikeRatcliffe, JannetGen and xet7.</summary>Asked for in 2018: "It would be great to have collapsable checklists on cards", and again this week - "Long checklists can make a card pretty cluttered, so being able to collapse them and expand only when needed would keep the board much cleaner."
WeKan had something adjacent and it was not this. A checklist carries
hideAllChecklistItems, reachable through a toggle switch inside the checklist
actions popup - but that is a field ON THE CHECKLIST, so flipping it changes
what everyone on the board sees, and it is an edit to the card rather than a
view preference. It is untouched; it has its own uses.
Collapsing is per-user, which WeKan already says twice in its own models for lists and swimlanes, so this follows them: one map in the user profile keyed by card. A feature group uses its own section name, an individual checklist uses a key of its own - which is why folding a checklist on the opened card folds it on the minicard too.
The control is a caret on the title rather than another entry in a menu, since the point is to fold at a glance while reading the card. It carries aria-expanded and answers Enter and Space. For the sixteen feature groups on the opened card it is done once, with a delegated handler and CSS: they all open with a title but only three wrap what follows in a content element, so folding hides every sibling after the title, which works whatever a section puts there.
A checklist's progress bar stays visible when folded - it is the summary of what was folded away - and the fold survives reopening the card.
</details>and fixes the following bugs:
The snap, upgrading from an old MongoDB - and why a fixed version changed nothing.
<details> <summary><a href="https://github.com/wekan/wekan/commit/097ad9160">A new snap revision is a new chance, so the MongoDB 4.2 reader actually gets to run</a>. Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7.</summary>Reported against 10.81: "I still have the exact same issue, MongoDB cannot start. The web interface is still unreachable, and I do not see the messages you added in the last commits." The messages were missing because the code that prints them never ran.
Two markers in $SNAP_COMMON stop the snap doing work, and both were PERMANENT.
.mongodb-data-too-old is written when no reader in the snap could open the
data, after which mongod is not started at all; .mongod-start-failures is a
counter that, past three, stops the migration being re-run so a migration/mongod
restart loop cannot form. Both are right, and both record a conclusion about
what THAT snap could do.
An instance that had already failed on 10.79 or 10.80 - before mongod 4.2 was bundled - carried a marker saying "no reader can open this" and a counter far past three. So 10.81 never started mongod, never attempted the migration, and printed nothing new. Upgrading to the version with the fix changed nothing, which is exactly what their log shows: the database-selection line, then "Waiting for MongoDB replica set primary..." forever.
Each marker now records the revision that wrote it, and one from a different revision is ignored and cleared - a marker with no revision recorded at all is stale by definition, which is precisely what the affected instances carry. Within one revision nothing changes, so the loop protection still holds; and not knowing the revision is never taken as evidence that it changed.
</details>Notifications - and the database write behind a slow login.
<details> <summary><a href="https://github.com/wekan/wekan/commit/184e1713d">The notification tray is capped, so SQLite is not rewriting an ever-growing array</a>. Thanks to Nissulya and xet7.</summary>An instance reports FerretDB at 737% CPU, logins over a minute, boards not
appearing, and logs full of database is locked (5) (SQLITE_BUSY). The stack
names the same write every time: addNotification.
That is an $addToSet on profile.notifications, an array inside the user
document - so adding one entry reads the whole document, scans the array and
writes the document back, at a cost proportional to the array. FerretDB on
SQLite has a single writer, so those rewrites queue and start failing, and
login, which also writes to the user document, queues behind them.
It grows without limit because the existing cleanup only removes notifications
that have been READ. A user who never clears their tray accumulates entries
forever. The same pass now also keeps the newest
NOTIFICATION_TRAY_MAX_PER_USER (default 1000) and drops the rest. It is
applied to what is left after the expiry pull, a user needing no change is not
written to at all, and it stays one write per user. This bounds the array; it
does not make SQLite a multi-writer engine, and a busy instance still wants the
PostgreSQL backend.
Cards and the Admin Panel - two things people asked for in the same thread.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6420819cf">Clicking an open card closes it, and a focused checkbox is not drawn as a diamond</a>. Thanks to csonkaoszimt, Heart1010 and xet7.</summary>Closing a card by clicking it again was not a missing feature - it was an unreachable one. The handler already ended with a branch that closed the open card, but the TITLE branch above it returned first, and a minicard's title covers most of the minicard. So the second click almost always re-opened the card that was already open, and the toggle worked only if you managed to miss the title. The title branch now makes the same decision, and on a phone, where the card is a popup, the second click closes that.
The Problems page glitch in the screenshot is the focus ring. The Admin Panel draws its checkboxes as a square that morphs into a tick, and the tick IS a 40-degree rotation of the element - so a browser draws its focus ring around a rotated box, and a checkbox that is both checked and focused (which is what one you just clicked is) comes out as a blue diamond. The ring moves to the row that contains it, which is not rotated; keyboard focus stays visible.
</details>Card dates - what a typed date actually becomes.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0c5cb99b7">A typed two-digit year is refused instead of stored as the year 26</a>. Thanks to xet7.</summary>From email feedback: "If i write the expiration date with the keyboard it turns red, if i choose it with the date picker it is yellow. Can you please tell me the difference?" The colour was never the difference - the YEAR was.
<input type="date"> reports its value as YYYY-MM-DD, but a browser lets the
year sub-field be typed as two digits and reports exactly that: entering
31-12-26 gives "0026-12-31", the year 26 AD. That is a valid Date, so nothing
refused it, and the card was saved with a due date two thousand years in the
past. Red is what an overdue date looks like. The attached screenshot shows it:
the yellow badges read "31-12-2026" and the red ones "31-12-26".
Saving now refuses a year outside 1000-9999 and says which digits are missing. It refuses rather than silently correcting 0026 to 2026, because that would be a guess about a date other people's reminders hang off.
</details>Old template containers - the boards nobody asked for.
<details> <summary><a href="https://github.com/wekan/wekan/commit/80f551872">Remove the Templates containers that were made for accounts which never used them</a>. Thanks to xet7.</summary>From email feedback: FerretDB at 190-350% CPU on an instance with 14490 boards, of which 13404 are template containers, for 9264 accounts of which 478 have ever logged in.
Before v10.00 every new account got a "Templates" container board at signup whether or not the person ever saved a template. v10.00 made that lazy (#2339, #5850), so no new account creates one - but nothing removed the ones already made, and they are not visible enough for anybody to delete by hand. On that instance they are 13x the boards collection, and every query that touches boards carries it.
Because this deletes boards, the rule for what may go is narrow: only a container nobody ever used. A template saved into it, a list, swimlane or card, a second member, a rename, a star or a manual archive all keep it, and every board that is kept reports why. A rename is judged only against the titles the app itself used, so a container whose default name is in another language is not deleted for it. The default is a dry run showing what WOULD go; deleting takes a second, explicit request.
</details>The snap database - which copy of the data it serves.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b1e7b1e53">A FerretDB copy older than the MongoDB beside it is never served</a>. Thanks to markusst1982 and xet7.</summary>After upgrading from v6 the reporter saw "the state of the data from days ago"
and suspected a snap revert done four weeks earlier. They were right about the
cause, and nothing was lost.
The MongoDB to FerretDB migration copies MongoDB into SQLite once and writes
.migration-to-ferretdb-done. That copy is a snapshot; nothing keeps it in
step. Revert the snap to a revision that runs mongod and WeKan carries on
writing to MongoDB - for four weeks here - while the finished SQLite sits frozen
at the date it was made. Refresh forward again and the snap saw a marker plus a
non-empty SQLite, called that a completed migration and switched onto it. Every
board and card written during the revert was still on disk and simply not being
served.
The data was never in danger: it lives in $SNAP_COMMON, which is shared across
revisions and is not rolled back by a revert (unlike $SNAP_DATA, which is
per-revision). What was wrong was which of the two copies got served, and
nothing compared their ages.
A new check answers exactly that: mongod rewrites its WiredTiger files on
every commit, so the newest mtime among them is when MongoDB was last written
to, and later than the marker means the copy is behind. It counts data files
only - a newer mongodb.log means the snap was started, not that the database
changed - and allows a margin, because the migration stops its own temporary
source mongod moments after writing the marker. Anything it cannot tell is
reported as NOT stale, since the callers act on a yes.
Where mongod runs, the snap now stays on MongoDB, which has the newest data.
Where mongod cannot start at all - the case that forces the migration in the
first place - it migrates again from scratch rather than serve the old copy; the
migration reads with its own temporary mongod and the 4.2/3.2 readers, so it
still reaches everything written since. And wekan-control gains the second
half of a guard it already had: it refused to start an empty FerretDB while
MongoDB had data, and a full but out-of-date one looks worse, because WeKan
comes up with everything present except the last weeks.
Board and card drag - what scrolls while a card is held.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8456170d4">Dragging a card down scrolls the list, not the whole board</a>. Thanks to markusst1982 and xet7.</summary>"Upwards is no problem, the Line scrolls automaticly up, but this does not work downwards. The whole Page/Site scrolls down and not ne Line".
The card drag auto-scrolls at the edges. Horizontally it picks the lane under
the pointer (#443); vertically it picked
nothing, and always scrolled .board-canvas - which holds the swimlanes -
rather than the .list-body under the pointer, which is overflow-y: scroll
and holds the cards. Scrolling the canvas moves the whole board.
The asymmetry is what makes it reproducible. Dragging up, the canvas is usually already at the top, so the handler did nothing and jQuery UI's own scroll option - which acts on the placeholder's scroll parent, the list body - scrolled the list, which is why up always worked. Dragging down, the canvas nearly always has room left, so the handler fired first and scrolled the board instead.
The list under the pointer is scrolled first now, and the board only once that list cannot go further - so a drag down a long list scrolls the list, and a drag past the end of it moves on to the board, which is what dragging a card into another swimlane needs.
</details>The Helm chart index - which charts it lists.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a442959dd">List only the charts whose container images still exist</a>. Thanks to xet7.</summary>Backfilling the index taught this within the hour: Artifact Hub scans every entry and mailed a list of errors.
error scanning image ghcr.io/wekan/wekan:v9.62: image not found error scanning
image docker.io/bitnami/mongodb:7.0.14-debian-12-r3: image not found
That was this side's doing. The rebuild listed every package on gh-pages, and a
chart is a POINTER TO CONTAINER IMAGES - one whose images have been deleted
installs and then fails at the pull, so listing it says the repository is broken
when the repository is fine and the images are gone. 135 of the 360 packages are
in that state, from two unrelated causes: six WeKan images were never pushed
(v8.30, v9.12, v9.14, v9.38, v9.39, v9.62 - releases whose own docker job
failed, and exactly the six Artifact Hub named), and 129 older charts vendor
the Bitnami mongodb subchart and pin tags Bitnami has since deleted. Charts
from 8.41 on vendor groundhog2k's mongodb, which uses the official mongo image
and is unaffected.
The index now holds 225 entries: every one of the 216 that were listed before -
none dropped - plus the 9 backfilled packages whose images all resolve. The
exclusions are recorded in unindexed.txt beside the packages, not decided per
run, so a rebuild during a release cannot depend on reaching two registries, and
an image that comes back is one deleted line away from being listed again. The
.tgz files stay, so direct URLs keep working.
Two things this shook out. --check-images asks each registry with ITS OWN 401
challenge instead of a hard-coded token URL per host - the first attempt
reported every quay.io image as missing, including quay.io/wekan/wekan:latest,
which plainly exists. And an image that cannot be checked is never treated as
missing, only a definite 404, so a registry hiccup cannot silently unpublish
charts. release-charts.sh now refuses to publish a chart at all when
ghcr.io/wekan/wekan:v<version> does not exist, which is what created these six
in the first place.
and has the following developer-facing fixes:
The test run - a stage that failed for a reason that was not about WeKan.
<details> <summary><a href="https://github.com/wekan/wekan/commit/09a525ae8">The database-conformance run no longer opens a debug port nothing in it uses</a>. Thanks to xet7.</summary>Every backend of the conformance stage failed before a single query was compared: "Failed to create debug handler ... listen tcp 127.0.0.1:8088: bind: address already in use", then "FerretDB did not start on this backend" for each of them.
FerretDB opens a debug handler for metrics and profiling at 127.0.0.1:8088 by default and EXITS when that address is taken, so an unrelated FerretDB running on the machine made the whole stage report a database problem that was nothing of the sort - as it would for anyone with anything on that port.
The script already takes this seriously for the two ports it knows about: it picks a free wire port, makes both overridable, and says in its own comment that they are chosen so it can run while something else is running. The debug port was simply never passed. Nothing in the run queries it, so it is not opened at all - which is also what FerretDB's own integration tests effectively do, choosing a random debug port rather than the default.
</details>The snap build - the part that could end it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b6d0be0f4">Two more mongo42 give-up paths deleted the directory their stage filter names</a>. Thanks to xet7.</summary>The earlier fix for this covered one of the three ways the mongo42 part gives up
stage: mongo42 naming a path that is not
there, and snapcraft ends the build on that rather than skipping it.Those two are reached on amd64 and arm64, where the binary IS downloaded: OpenSSL 1.1 unavailable for the architecture, or the staged mongod 4.2 failing the check that it actually runs. Either would have ended the snap build for the two architectures that matter most, the same way it ended all four Launchpad ones. Both now clear the CONTENTS and keep the directory; an empty one still means "no 4.2 reader", because every use is guarded on the binary rather than the directory.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: the release pipeline, and two bugs that stopped WeKan starting at
all. FerretDB advertises its own listen address as the member list of a
one-node replica set, so the MongoDB driver threw away the host in MONGO_URL
and dialled 0.0.0.0:27017 instead - a fresh docker compose up could not
reach its database, and every FerretDB compose file now says
directConnection=true. All Boards on a phone could not be scrolled to its
last boards, its Table view could not be scrolled at all, single rows of board
icons were drawn several times their proper height, and the icons sat too close
together - four separate causes, one of them a calc(100dvh - 48px) in
layouts.css that guessed at a header height which is measured at runtime
precisely because it is never one number. On the release side: the Mac x64
bundle has never once been built, because its runner label was retired by
GitHub and a retired label queues forever instead of failing - and cancelling
that queue is what silently skipped the charts, ucs and nextcloud
jobs. The armv6 bundle was being assembled in a soft-float ARMv5 userland
that its own hard-float Node.js cannot start in, and every Launchpad snap
was ended by an optional MongoDB-migration part that stages a path it does not
build outside amd64 and arm64. Below that: a report of the WeKan releases the
Helm chart index is missing, and the duplicate entries found in it. The
binaries below are v10.80's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.48.0 | 2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.48.0 | 06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.48.0 | 4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.48.0 | 0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6 |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.48.0 | d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.48.0 | 6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.48.0 | ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2 |
This release fixes the following bugs:
FerretDB - the default database, and how WeKan is told to reach it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e1c6221f2">Connect with directConnection=true, so a fresh docker compose up starts</a>. Thanks to Dandrass and xet7.</summary>A new install with nothing changed but the port and ROOT_URL could not reach
the database:
MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017
reason: TopologyDescription { type: 'ReplicaSetNoPrimary',
servers: Map(1) { '0.0.0.0:27017' => [ServerDescription] },
setName: 'rs0', ... }
0.0.0.0 is in no compose file. It is FerretDB's own listen address, and the
driver was handed it by the server. The ferretdb service runs with
--repl-set-name=rs0 - added in
#6480 so Meteor can tail an OpLog
instead of poll-and-diff - so FerretDB answers the hello handshake as a
one-member replica set and fills hosts, me and primary with its
--listen-addr. A driver not in direct-connection mode reads that as an
invitation to do replica-set discovery: it adopts the advertised member list and
drops the seed it was given, because the server reports a name other than the
one that was dialled. mongodb://ferretdb:27017 became 0.0.0.0:27017, which
inside the wekan-app container is that container itself.
Measured against FerretDB v1.49.0 with the driver the bundle ships:
without the parameter the topology ends up ReplicaSetWithPrimary with the seed
discarded, with it the topology is Single on the host that was given.
It costs nothing else - the handshake still reports setName: rs0, the only
thing Meteor checks before it will tail an OpLog. All five FerretDB v1 compose
files carry it; the MongoDB ones deliberately do not, being real replica sets
whose members are reachable under the names they advertise.
All Boards on a phone - the scroll, the tiles and the spacing between them.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c0a5c2b7c">The board list scrolls to its last board, the Table view scrolls at all, and the tiles are their own size</a>. Thanks to mimZD and xet7.</summary>Four things at once, in an issue reopened against 10.10, 10.37, 10.38, 10.72 and 10.77.
THE SCROLL. Every earlier fix removed viewport arithmetic from boardsList.css.
The last piece of it was in layouts.css:
body.mobile-mode #content { height: calc(100dvh - 48px) }. 48px is a guess at
the height of the header, and the header is not 48px and is not any one number -
--wekan-header-height is published from a ResizeObserver for exactly this
reason. On a phone whose bar wraps, #content was taller than the room under
the header, its bottom sat below the screen, and body.mobile-mode is
position: fixed and overflow: hidden, so that strip is unreachable. Nothing
needs computing: body is a flex column of one viewport and #content is its
flex: 1 item.
THE TABLE VIEW. The right column has two branches - the board icons and
+tablePage - and only the icons were ever given a scroller, so in Table view
the rows past the fold were clipped with nothing to scroll.
THE 4x-TALL TILES. A grid defaults to align-content: stretch, so a list
shorter than its column has the leftover height divided among its rows and each
tile grown into it - and a board tile paints its colour over the whole cell.
This was fixed once, but only on .board-list.mobile-view; the phone media
query builds a grid too, and a narrow window that is not a mini screen took
that path.
THE SPACING. An 8px grid gap plus a margin-bottom: 0.5rem per tile: 16px
between rows and 8px between columns, which reads as crowded sideways. One value
now, in both directions. The mobile full-screen popup had the same class of bug
as the first and states dvh now too. Desktop is unaffected.
and fixes the following release-pipeline failures:
The release workflow - which jobs run, and on what.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7b8c37709">The retired Mac runner, the jobs a cancellation skipped, and the armv6 userland</a>. Thanks to xet7.</summary>build-mac-x64 asked for macos-13, and GitHub has retired that image -
actions/runner-images publishes macos-14, macos-15 and macos-26 only. A
retired label does not fail: the job QUEUES, for a runner that is never coming.
It has been sitting there every release until cancelled by hand, and no WeKan
release has ever carried a wekan-<version>-mac-x64.zip because of it. Intel
macOS was renamed, not removed: macos-15-intel, which is what TSC already
builds on.
Cancelling it is what skipped charts, ucs and nextcloud: a cancelled job
cancels the run, and a cancelling run skips every job that has not started - so
all three were skipped the second the docker job they wait on succeeded, after
everything had already shipped. A job whose if is always() still runs while
a run is cancelling, so the three now say always() and name the needs that
must have succeeded.
The armv6 bundle died on
qemu-arm: Could not open '/lib/ld-linux-armhf.so.3'. Debian has no ARMv6 port
debian:trixie publishes arm/v5 and arm/v7 - and containerd treats a lower
ARM variant as compatible, so --platform linux/arm/v6 quietly resolved to
arm/v5: Debian armel, soft-float, with no hard-float loader in it. The preflight
compared the architecture and ignored the variant, so it never said so; it
compares both now, and armv6 is built in Debian's arm/v7 container, which is
armhf and runs node-armv6 fine. Nothing ARMv7 reaches the bundle - the container
compiles nothing, and node, FerretDB and the MongoDB tools are all downloaded
already built for ARMv6 - except the bundled qemu-user, which is copied out of
the container and is therefore skipped for that one bundle.The snap - what the four Launchpad architectures were really failing on.
<details> <summary><a href="https://github.com/wekan/wekan/commit/18da0d261">An optional part was ending every Launchpad build</a>. Thanks to xet7.</summary>armhf, s390x, ppc64el and riscv64 all failed, three attempts each, and the job reported it as a Launchpad problem: "often an OOM in the Meteor npm install, or a transient build-farm reset; re-run". It was neither. The build log said the same deterministic thing every time:
Staging mongo42
Failed to copy '/build/.../parts/mongo42/install/mongo42': no such file or
directory. Build failed
The mongo42 part downloads MongoDB 4.2 so a database from an old MongoDB snap
can still be migrated (#6471), and
MongoDB publishes 4.2 for amd64 and arm64 only. Everywhere else the part prints
"nothing to migrate from there; skipping" and exits 0 having created nothing -
its own comment calls that optional by design. It was not: the part also carries
stage: [mongo42], and snapcraft does not skip a filter whose path is missing,
it ends the build. So a migration helper no exotic architecture has ever needed
took the whole snap down on exactly the four that can only be built on
Launchpad. The directory is created before anything can decide to skip; the
binary is still downloaded only where it exists.
riscv64 additionally lost its last attempt to npm ERR! code ECONNRESET on one
tarball, after half an hour of building - every request there goes through
Launchpad's proxy - so that npm install is retried three times. The
::error:: no longer guesses: it sends the reader to the build log printed
above it.
and improves the release tooling:
The Helm chart index - which WeKan releases it lists.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8248892e3">Report the releases the chart index is missing, and repair its duplicates</a>. Thanks to xet7.</summary>A chart entry is written once, during the release it belongs to, so a release
whose charts job did not run leaves a hole nothing ever fills. Counted against
the live index: 216 of 690 WeKan releases have a chart entry.
releases/backfill-charts.sh answers which releases the index should list - the
ones that exist and can be installed. 216 are kept (never repackaged; that would
change a digest helm clients have seen), 369 could be built, and 162 are OMITTED
because they have no container image on ghcr: a chart is a pointer to an image,
so an entry for one of those is an install that fails at the pull. The index is
rebuilt from the packages actually present, so omission needs no bookkeeping.
It also found something already wrong: the published index has four entries for
9.36.0 and two for 10.30.0, each with a different digest and the same url,
because the release script prepends an entry every time it runs. At most one of
those digests can be the one of the package really served, and a helm client
that picks another fails the integrity check on a good file. The rebuild keeps
the entry whose digest matches the package, falling back to the newest only when
there is no package to compare against. Nothing is written without --apply and
nothing is pushed without --push; the charts job runs it in plan mode only,
into the run summary.
With the charts repository cloned it became clear the index had drifted from its
own packages in four ways at once, all from the same habit: an entry was written
by COPYING the previous entry and substituting a few fields, so the index was
being kept as the record rather than as a description of the .tgz files beside
it.
146 packages had no entry at all - 362 packages, 220 entries. They are
downloadable by URL but invisible to helm search, to
helm install wekan --version 9.63.0 and to Artifact Hub. 9.36.0 had four
entries and 10.30.0 two, each copy with a different digest and the same url,
so a client could pick a digest matching no file and fail integrity on a good
package. appVersion was the CHART version on every entry - 10.79.0 where
the package says 10.79, which is the WeKan version a reader is looking for.
And every entry claimed the mongodb subchart was 0.7.2, a field nobody
substituted, while the packages had long since moved to 0.7.6.
releases/reindex-charts.py derives the index from the packages, reading each
one's Chart.yaml out of the archive, so an entry cannot describe a package
wrongly. Nothing is invented: created is preserved exactly on every entry
whose digest matches its package, and a package being added takes the date
it was committed to gh-pages, which is when it was really published. Two 2023
packaging slips - wekan-1.2.7.tgz containing chart 1.2.6, and wekan-6.96.tgz
containing 6.9.6 - are reported and left out rather than making one version mean
two files; both files stay on the server.
The index went from 220 entries to 360, and was checked after writing: no repeated version, every digest equal to the sha256 of its file, every file present, and no version that was listed before missing.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a434bb4bd">Both release workflows publish the chart, and the index is derived rather than edited</a>. Thanks to xet7.</summary>The charts repository keeps the chart SOURCE on main and the published
packages plus index.yaml on gh-pages, and its own two scripts move between
them: release.sh commits the source, tars wekan/ into
wekan-<version>.0.tgz, checks out gh-pages and drops the package there;
release2.sh commits and pushes. release-charts.sh drives both and owns the
index in between - and that middle part is what changes here.
The index is now rebuilt from the packages instead of being edited. It used to copy the newest entry, substitute a few fields and prepend the result, and all four of the index's defects came from exactly that. Deriving it makes them impossible rather than fixed: one entry per package, digest computed from the file, fields read out of the archive, and any package that was missed picked up on the next run.
The package is checked against its own filename before it is indexed.
release.sh names the tarball from its argument while the version INSIDE comes
from the Chart.yaml that was just edited, so when those drift the repository
gains a file called one version that declares another - which is how
wekan-1.2.7.tgz (containing 1.2.6) and wekan-6.96.tgz (containing 6.9.6)
came to exist. Both are now removed, and a package like them stops the release
with a message instead of being published.
Release All Missing publishes a chart too. It could rebuild any missing bundle but not a missing chart, so a release that never got one never would - its header even said the charts were out of scope. It has a charts job now, guarded twice: nothing happens without a token that can push to wekan/charts, and nothing happens if the chart for that version is already published, because re-publishing would re-tar the package and change the digest of a chart people have already pulled.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: the Admin Panel, in the two panes v10.79 had just changed. Version is one table again rather than five: five tables sized their columns independently, so the values started at a different x in every category. The categories are rows inside one table now - bold, spanning both columns - over two equal 50% columns. Problems / Filesystem integrity drew a blank page: the one piece of its wiring that was missing was a template helper, and Blaze reads an undefined helper as false rather than complaining. The binaries below are v10.79's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.48.0 | 2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.48.0 | 06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.48.0 | 4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.48.0 | 0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6 |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.48.0 | d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.48.0 | 6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.48.0 | ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2 |
This release reorganises the Admin Panel:
Admin Panel / Settings - the Version pane, and how it lays itself out.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aed26c677">Version is one table with combined category rows, over two 50% columns</a>. Thanks to xet7.</summary>The pane arrived at v10.79 as five tables, one per category. Five tables size their columns independently: "WeKan ® Version" made the first one's label column wide and "OS Type" made the next one's narrow, so the values started at a different x in every group and the pane read as five unrelated things.
One table now, and each category is a row in it — a th with colspan=2,
bold and start-aligned, so it says what the rows under it are about instead of
being a label with an empty cell beside it. A colgroup of two 50% columns plus
table-layout: fixed puts every label and every value in the same place down
the whole pane; the 240px header cap the other admin tables carry is undone for
this one, since its width is now stated outright and the cap would fight it.
The category title keeps the table's own font size deliberately: at the pane title's size, five of them would read as five pane titles and "Version" would be lost among them.
Two of the repository's own guards caught mistakes on the way, which is what
they are for. The jade compile check refused th(colspan=2) — the Meteor jade
dialect wants the value quoted, and an unquoted one is a build failure rather
than a rendering difference — and the RTL check refused text-align: left,
because the label column is on the RIGHT in Arabic and Hebrew, so it is
start.
and fixes the following bugs:
Admin Panel / Problems - a pane that drew nothing, and said nothing about it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/01c36852d">Problems / Filesystem integrity showed a blank page</a>. Thanks to xet7.</summary>The pane drew its title and then empty space, while Summary went on reporting "7 new problems" for it.
Everything about it looked right, which is why it survived: the menu has a
report-integrity entry, clicking it is handled, the handler sets
tmpl.showIntegrity, and the template has else if showIntegrity.get with an
integrity event stream under it. The missing piece was the helper.
showIntegrity() was never added beside showDatabase() and the eight others,
and in Blaze an undefined helper is not an error — it is falsy. So the
branch never ran, the page was blank, and nothing anywhere said why.
The guard added with it is the class rather than this one pane: every
show*.get branch in a settings template must have a helper of that name in
that template's own .js, and a ReactiveVar behind it. The templates are
FOUND rather than listed, so a pane added later is covered without editing the
test.
The snap - what it does when it cannot read the database it was upgraded onto.
<details> <summary><a href="https://github.com/wekan/wekan/commit/562fa0271">A database this snap cannot read stops and says so, instead of serving 502 forever</a>. Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7.</summary>A snap upgraded onto a database left by a MongoDB 4.x or 5.x snap served 502
Bad Gateway indefinitely, with the reason only in snap logs:
This version of MongoDB is too recent to start up on the existing data files.
Try MongoDB 4.2 or earlier.
The snap carries two readers — mongod 7, the server it runs, and the MongoDB
3.2 tools for a 6.09-era database — and nothing in between, so 4.x data opens in
neither. What the code did then is the one thing that cannot work: the migration
found that neither reader could open it and handed back to mongodb-control,
which started mongod, which failed the same way, which re-ran the migration —
three times by its own counter — and then exited for snapd to restart. Nothing
in that loop can succeed, because reading those files needs a binary the snap
does not have.
It stops now. The migration tells "no reader for this vintage" from
"unreadable or corrupt" by mongod's own words, records the version mongod named
as still able to read the data, pauses auto-migration and exits 0 — zero,
because snapd restarts a failing service forever and no restart can help here.
mongodb-control will not start a mongod it knows cannot start, and WeKan
serves an explanatory page on the web port, both at startup and from inside the
database wait loop, so an instance already waiting switches over without a
restart.
The page names the MongoDB version that can still read the files and gives the two ways forward — go back to the revision that worked, or dump with a MongoDB that can read it and restore into this version — says plainly that nothing was changed and that attachments and avatars are files on disk, and drops the auto-refresh and the spinner the other two maintenance pages carry: this is a stop, not a wait, and the page should not promise that something is happening.
Nothing is deleted or modified on this path: the source data is exactly as it was, the marker file is the only thing written, and removing it lets the snap try again. The snap documentation gains the section an admin searching for that mongod line will find, with the commands.
The test covers the wiring in all three scripts and then RUNS the page — it is standalone Node with no dependencies — to check what an admin actually sees: 503, the version, "untouched", both remedies, no refresh, no spinner.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/71ba0c2dc">A third MongoDB reader, so a 4.x database migrates instead of stopping</a>. Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7.</summary>The entry above stopped the crash loop and explained it. This removes the reason for it in the case that was actually reported.
A MongoDB server only starts on data whose featureCompatibilityVersion is at
most one major behind it, so what the snap can READ is decided by which servers
it carries: mongod 7 (FCV 6.0, 7.0) and the MongoDB 3.2 tools (3.2). Everything
in between was unreadable — and the WeKan snap has shipped 3.6, 4.0, 4.2, 4.4
and 5.0 over the years. The reported error names the gap exactly: "Try
MongoDB 4.2 or earlier", which is FCV 4.0.
mongod 4.2 is now bundled as a third reader, used only to read the old data during a migration and never as the running database. It opens FCV 4.0 and 4.2, and the modern importer reads it with the bundled driver, which supports servers from 4.2 up — the same importer that reads a 6/7 source, not a second copy of it. The probes run newest-first: 7, then 4.2, then the 3.2 tools, then the page.
amd64 and arm64 only. MongoDB publishes no 4.2 for the others, and they have been FerretDB from their first boot, so there is nothing there to migrate from.
It carries its own OpenSSL 1.1. The 4.2 build links libssl.so.1.1 and
libcrypto.so.1.1, and core24 is Ubuntu 24.04, which ships OpenSSL 3 — without
them the binary does not even load. Both come from one Debian libssl1.1
package, staged beside the binary and put on LD_LIBRARY_PATH exactly as the
3.2 tools already are, with the filename resolved by listing the pool rather
than pinned, because point releases roll and a pinned name 404s the day they
do.
Optional by design: every failure in that part — download, checksum, OpenSSL, or the binary not running — ends it with a message and no binary, and the migration simply does not find one. A release is never failed over a migration aid.
Verified as far as a machine without a snap allows: mongod 4.2.25 aarch64 was downloaded, staged with the Debian libssl1.1 and RUN — "db version v4.2.25, OpenSSL version: OpenSSL 1.1.1w" — then started on a dbpath, forked and listened on a port. That is the whole mechanism, on a 2026 system. The build repeats the check and unstages the binary if it fails.
Still unreadable, and still answered by the page rather than a migration: 3.4, 3.6, 4.4 and 5.0. Bundling mongod 5.0 beside this one would close 4.4 and 5.0 the same way, at the same cost in size.
</details>and improves the translations:
Translations - the new strings, and the languages that keep the English placeholder.
<details> <summary><a href="https://github.com/wekan/wekan/commit/132a64484">The Version pane's new strings, translated into 113 languages</a>. Thanks to xet7.</summary>The pane's five category labels and the packaging row arrived in English only,
so every other language showed them in English. Three of the six needed
translating at all: Platform, package ("Package") and OS. Database
was already translated in 131 languages — the key existed before and this
revived it — and Meteor and Node are product names that stay as they
are in every language, which is also why the filler ignores a value equal to the
English source.
Translated directly, with no external service, using each language's own
existing strings as the reference. Its OS_Type and OS_Platform show the
form that language's translators use — "Typ des Betriebssystems", "Tipo
SO", "Тип ОС", "Käyttöjärjestelmän tyyppi" — so OS is Betriebssystem in
German, SO in Italian and Portuguese, ОС in Russian and
Käyttöjärjestelmä in Finnish, rather than one spelling imposed on all of them.
Two files were deliberately NOT copied from: Greek's OS_Type and OS_Platform
hold Italian, and Korean's hold Japanese. Propagating that would have spread
somebody else's mistake into three more strings, so those two got proper Greek
and Korean instead.
Applied through fill-translations.mjs --apply, which writes only into a
placeholder, so no human translation could be overwritten even by accident — and
the diff shows it: 292 changed lines across 106 files, every one of them
Platform, package or OS. Key order and indentation are unchanged, every
file still parses, and verify-human-preference.mjs passes 10/10.
40 files keep the English placeholder on purpose — ace, ary, br, gu-IN, ig,
km, mn, oc, or_IN, pa, tk_TM, tlh, ug, ve, vl-SS, vo, wa, wo, xh, yi, yo, zgh,
zu and the en-* variants, which are English by design. A placeholder that
says so is better than a translation nobody can stand behind, and Transifex can
still replace any of them with a human one: nothing here is pushed there.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: two new platforms and the snap jobs. WeKan now builds a
win-arm64 bundle - Windows on ARM - which needed no new work anywhere else,
because nodejs.org, FerretDB and the MongoDB tools all publish that
architecture already and nothing here had asked for it. armv6 - Raspberry
Pi 1 and Zero - was the opposite: nobody publishes a Node.js for it any more,
so wekan/node-patches gained an armv6 target, and FerretDB and the MongoDB
tools gained GOARM=6 ones beside their armhf builds. It ships as a bundle zip
and no snap, because the Snap Store has no armv6 architecture at all; the
Docker image is wired for it too, but stays gated until a base image
publishes linux/arm/v6 — Debian has no ARMv6 port, and a request for one
silently resolves to soft-float armel rather than failing. (armv5
and armel still cannot be added at all: V8's accepted --arm-arch list stops at
armv6, so there is no Node.js to build.) On the snap
side, one thing had been quietly failing for months: armhf asked Caddy for
a linux_armhf archive that has never existed - Caddy is built by Go and its
assets carry Go's architecture names -
and riscv64, ppc64el and s390x each built a perfectly good snap that
the Snap Store then refused while processing it, with an error about its own
duplicate check; the upload is retried now, and the message no longer blames
credentials. Beside that, the amd64 bundle had never recorded where its
Node.js and FerretDB came from - not because the directory was wrong, which was
last release's fix, but because the scripts themselves were never found, and the
|| true meant for release notes swallowed the error every run. Below that, the
CPU platforms of the image and of the snap now have a page each, and the
FerretDB v1 page lists every architecture its binary is built for. The binaries
in the table below are v10.78's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.48.0 | 2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.48.0 | 06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.48.0 | 4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.48.0 | 0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6 |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.48.0 | d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.48.0 | 6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.48.0 | ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2 |
This release adds the following new features:
Windows on ARM - a bundle whose every binary was already published.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8bb7b9ba6">Build a win-arm64 bundle</a>. Thanks to xet7.</summary>WeKan could always have shipped this and simply did not. All three suppliers
publish the architecture: nodejs.org builds node-v24.19.0-win-arm64.zip
itself, wekan/FerretDB publishes
ferretdb-win-arm64.exe, and
wekan/mongo-tools-patches
publishes every tool as -win-arm64.exe.
releases/resolve-node-source.sh gains the platform and resolves it to
nodejs.org with a published SHA256, so it needs no node-patches build at all.
The job is the win64 one with the architecture changed: it builds on the x64
Windows runner exactly as win32 does, because the Meteor bundle is JavaScript,
the native modules are installed with --ignore-scripts and are not compiled
for the runner's architecture either way, and the only architecture-specific
things in the zip are the three binaries above, which are downloaded rather
than built.
What cannot be added, checked at the same time: armv5 and armel. FerretDB
and the MongoDB tools publish armel - they are Go, and Go still targets it -
but Node.js does not exist for either, and it cannot be built either: V8's
accepted --arm-arch list stops at armv6. No Node.js means no bundle, so no
Docker image and no snap either. armv6 was checked in the same pass and looked
like the same answer - nodejs.org publishes no 32-bit ARM at all for v24.19.0
and unofficial-builds has no armv6l - but there the SUPPORT was still in the
source and only the build was missing, which is what the next entry does.
Raspberry Pi 1 and Zero - the platform whose Node.js had to be built first.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d9e4cd57c">Build an armv6 bundle</a>. Thanks to xet7.</summary>Every piece of the chain exists now, and the piece that was missing was
Node.js. nodejs.org dropped its ARMv6 binaries after Node 11 and
unofficial-builds has none, so
wekan/node-patches gained an armv6
target - V8 still accepts --arm-arch=armv6 and configure.py still carries
is_arch_armv6() with vfp among its valid ARM FPUs, so only the build was
missing - and wekan/FerretDB and
wekan/mongo-tools-patches
gained GOARM=6 targets beside their armhf ones.
Their armel is GOARM=5 and WOULD run on these boards, which is exactly why
it looks like a substitute and is not one: GOARM=5 does floating point in
software, and an ARMv6 board has VFPv2.
The bundle builds in a linux/arm/v6 container and takes node-armv6, and
releases/resolve-node-source.sh resolves it - naming linux-armv6l as the
upstream spelling even though nothing upstream will ever answer to it, so the
search it prints is honest about where it looked.
No snap, and that is not an oversight. The Snap Store has no armv6
architecture at all - its only 32-bit ARM is armhf, which is ARMv7-A
hard-float and will not run on an ARMv6 board. So armv6 ships as a bundle zip
and a linux/arm/v6 Docker image, and models/lib/snapArchitectures.js
records that reason in NOT_SNAP_ARCHITECTURES beside i386's and armv7's.
Four tests found the four places a new platform has to be registered, which is
what they are for: releases/expected-assets.sh (or "Release all missing"
never notices the asset is absent), the resolver's mapping table, the
non-native bundle list, and the snap-platform exemption. Each list was updated
rather than the guard loosened.
What is not verified: none of these binaries has been built yet. The
Node.js one is a multi-hour ARM cross compile and the first CI run is its test.
Everything checkable from source was checked - V8's accepted --arm-arch
values, configure.py's ARM handling, Go's GOARM semantics, and that zlib's
ARM SIMD is gated on arm_fpu == "neon", so an armv6 build selects the scalar
code by itself and needs none of the NEON patching armv7 does.
TARGETARCH does not identify a 32-bit ARM platform. linux/arm/v6 and
linux/arm/v7 both arrive in the Dockerfile as TARGETARCH=arm; the CPU
generation is in TARGETVARIANT. The architecture case mapped arm straight
to armhf, so the moment armv6 existed as a bundle, an ARMv6 build would have
been handed the armhf zip — ARMv7-A instructions for a CPU that cannot execute
them. It now branches on the variant: v6 takes the armv6 bundle, v7 and an
unset variant take armhf, and anything else exits. v5 is armel: FerretDB and
the MongoDB tools publish it, but Node.js does not exist for ARMv5, so there is
no bundle and refusing is the only honest answer.
linux/arm/v6 is in the docker job's optional platform list beside the others,
and the release then asks a question it never asked before: does the base
image publish this platform? For every other architecture that question is
uninteresting, because a base that lacks one fails the build. For 32-bit ARM it
does not fail — containerd treats a lower ARM variant as compatible, so a
linux/arm/v6 request against a base with arm/v5 and arm/v7 quietly
resolves to arm/v5, Debian armel, soft float. The image would build on a
userland whose loader cannot start the hard-float node-armv6 inside the
bundle. A silent downgrade is worse than a dropped platform.
So the job reads the base name out of the Dockerfile — no second copy of it —
asks docker buildx imagetools inspect what that base publishes, and drops a
candidate it lacks with a warning saying why. Today debian:trixie is 386,
amd64, arm64/v8, arm/v5, arm/v7, ppc64le, riscv64, s390x: no
arm/v6, because Debian has no ARMv6 port — its 32-bit ARM ports are armel
and armhf, and ARMv6 hard-float is Raspberry Pi OS territory. The armv6
bundle zip is unaffected, and that is how an ARMv6 board runs WeKan today; the
image platform is wired end to end and turns itself on the day a base publishes
the variant, with nothing else to change.
Checked against the real registry rather than assumed: debian:trixie's
manifest list was read, and the decide step was run against both that list
(armv6 drops, the other seven build) and a base that does publish arm/v6
(armv6 is included, paired with the armv6 bundle).
tests/releaseDockerPlatforms.test.cjs pins the variant split, the refusal of
v5, the platform-to-bundle pairing, and that both loops ask the base — the
last one so that "just delete the check" cannot quietly become an image whose
Node.js will not start.
and reorganises the Admin Panel:
<details> <summary><a href="https://github.com/wekan/wekan/commit/dcc6c8fe5">Version is five tables with a heading each, and says what WeKan is installed as</a>. Thanks to xet7.</summary>Admin Panel / Settings / Version was ONE table of 38 rows, in which the WeKan version, the OS load average, the DDP transport and a V8 heap counter were the same kind of thing: a flat list read top to bottom, with no way to jump to the part you came for.
It is five tables now, each under a small heading, in the order somebody debugging reads them — Platform (what this is), OS (what it runs on), Meteor (how it talks to the client), Database and Node. Reactivity, reactivity order and the DDP transport moved to Meteor, where they belong: they are how the client is fed, and they were the rows most often read as database settings. Whether an OpLog exists stays with the database, because that is a property of it. Nothing was dropped — a test lists all 38 rows by name and requires each to still be shown.
The new row is what WeKan is installed as: bundle.zip, Snap, Docker or
Sandstorm. It is the first thing a support answer turns on and the pane
never said it: the same version keeps its data somewhere else, carries a
different database and gives the admin different reach in each of the four.
The row's label is translated (package, "Package"); the four values never
are, and none of them is a translation key — they are the names of the things
themselves, not words describing them, so what an admin reads is the same string
they can put in an issue, search the docs for and grep a log with, in every
language.
models/lib/platformPackaging.js decides it most-specific-first — an explicit
WEKAN_PACKAGING wins, then Sandstorm, then snapd's own SNAP/SNAP_NAME,
then a container runtime's marker file, then the bundle. Sandstorm and Snap are
asked BEFORE the container markers on purpose: a grain is a container and a
confined snap can look like one, so the other order answers "Docker" for both.
It claims nothing it cannot know: there is no "source checkout" answer, because
a meteor run and an unpacked bundle are identical from inside the process.
The detection is a pure function — it takes the environment, the Sandstorm flag
and a file-exists callback — so all nine of its tests run in a sandbox with no
snap, no container and no grain: that an empty SNAP is not a snap, that an
empty WEKAN_PACKAGING falls through instead of blanking the field, and that an
unreadable filesystem root answers bundle.zip rather than throwing away an
admin's Version page.
The headings are deliberately smaller than the pane title above them, and a test compares the two font sizes instead of trusting the CSS to stay that way: five headings at the pane title's size read as five pane titles, and "Version" is lost among them.
</details>and fixes the following bugs:
The snap builds - what they download, and what the store does with the result.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4e9be22f9">Caddy has no armhf archive, and a store hiccup is not a bad snap</a>. Thanks to xet7.</summary>Two failures in the v10.78 snap jobs, neither of them a problem with WeKan.
armhf asked for a Caddy architecture that does not exist. With the
libcurl4t64 fix in, the armhf build got past the stage-packages and died
further along:
:: Downloading Caddy 2.11.4 (linux/armhf) from GitHub releases...
:: curl: (22) The requested URL returned error: 404
'override-build' in part 'caddy' failed with code 1.
Caddy is built by Go and its release assets carry Go's architecture names,
not Debian's. There is no linux_armhf archive and there never has been - the
32-bit ARM ones are armv5, armv6 and armv7 - and the case statement had no
armhf branch, so it fell through to a default that passed the Debian name
straight into the URL. The "fall back to the pinned version" path then retried
the same wrong name, so the failure read as "Caddy stopped publishing this
architecture" when it was this file's mapping all along.
armhf maps to armv7, not armv6: Debian armhf's baseline is ARMv7-A with
VFPv3-D16 hard-float, and Go's armv7 build is GOARM=7, which is exactly that.
This is not the armhf/armv7 distinction that matters for Node.js in
wekan/node-patches - that one is about
NEON, and GOARM=7 does not use NEON. Checked against the actual release: all
six mapped URLs answer and linux_armhf 404s. The default branch now names the
problem and stops, instead of guessing a name and letting a 404 blame the wrong
project.
Three good snaps were lost to a store hiccup. riscv64, ppc64el and s390x each built on Launchpad, downloaded, and were then refused:
Status: error while processing
Issues while processing snap:
- binary_sha3_384: Error checking upload uniqueness.
That is the store failing its own duplicate check on a digest it had just computed - a server-side error, not a bad snap - while the message the job printed was about "is not a valid file", credentials and ACLs, none of which applied. The upload is retried three times with a backoff now, and the give-up message says the snap is fine and nothing here needs changing. The retry stays narrow on purpose: a rejected file, unparseable credentials or a missing ACL will be rejected identically three times, and retrying those only buries the one message that says what to fix - so the classifier is tested against all four, not just the one that happened.
</details>The snap on the next base - what snapcraft-core26.yaml builds.
snapcraft.yaml declares six platforms — amd64, arm64, armhf, ppc64el, riscv64,
s390x — and every one is built: two natively, four on Launchpad.
snapcraft-core26.yaml declared five. armhf was missing, and nothing could
notice: that file is not built by any release, it is the next base kept so the
move can be tested before it is made. The day core26 becomes the base, armhf
would simply stop being published, and the only symptom would be a store column
going stale — exactly as wekan-ondra's armhf did at 0.22.
Adding the platform alone would have been nominal. The same file still carried
the pre-t64 stage-package names — libssl3, libcurl4,
libgoogle-perftools4 — and those are precisely what failed armhf on core24
twice: Ubuntu's 64-bit time_t transition renamed them, the 64-bit
architectures kept a compatibility Provides so the old spelling resolves
there, and armhf did not. armhf on core26 would have failed on the first thing
it tried, in the way this repository has already debugged. It has the t64 names
now, and its Caddy branch — which already mapped armhf to Go's armv7 — no
longer claims to be unreachable.
Two guards, both checked in the failing direction as well: the two snapcraft files must declare the SAME set of architectures, and the stage-package check runs over BOTH files instead of only the one the release builds.
Nothing here changes what the release builds today. core24 stays the built base,
and wekan's armhf snap is still waiting on the Caddy armhf fix above.
The release notes - what the provenance table can say about amd64.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e59680519">The amd64 provenance scripts were never found, only never complained</a>. Thanks to xet7.</summary>The amd64 build failed with exit 127, after the bundle had been zipped and checksummed:
a95d331b… wekan-10.78-amd64.zip
bash: releases/record-provenance.sh: No such file or directory,
bash: releases/ferretdb-latest-tag.sh: No such file or directory,
The step runs cd .build first, so nothing relative in it means what it looks
like it means - and that is two bugs, of which only the second had ever been
visible. bash releases/record-provenance.sh resolves against .build/, which
has no releases/, so it had printed "No such file or directory" on every run
since it was written and the || true on the end swallowed it. amd64 had
therefore never recorded any provenance at all, and the only symptom was its
absence from the table - so the missing-directory fix above repaired the half
that showed and left the half that did not.
What made it loud is that the FerretDB tag lookup added alongside it is an
assignment. Under set -e, and every GitHub run: is bash -e,
VAR="$(cmd)" ends the step when the command fails - so a line that had been
quietly doing nothing for months became a hard failure of the whole amd64 build.
Every path in that step is absolute now, and every FERRET_TAG assignment
across all seven sites ends || true: the bundle is the deliverable, and which
version string reaches a markdown cell is not worth failing a good build for.
Only amd64's step cds - the other six run from the workspace root, which is
exactly what the v10.77 run showed, since all six uploaded provenance and amd64
did not.
The guard added with it is the general form rather than this one line: it walks
back from every provenance call to its step header and requires an absolute path
whenever a cd runs inside that step.
and documents which CPU platforms each package is built for: The Docker image and the snap - a page each for what they are built for.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5ec9f3ae5">A page for the Docker image's CPU platforms, beside the one the snap already had</a>. Thanks to xet7.</summary>"Which CPUs is this published for, and why not that one" was answerable for the
snap and nowhere else, so the same page now exists for the image:
docs/Platforms/FOSS/Container/Docker/CPU-platforms.md. It carries the
platform matrix with each platform's TARGETARCH, TARGETVARIANT and bundle,
how the set is decided on every release from what the base image publishes and
which bundles landed, the three names that differ between Docker, Debian and the
WeKan bundles, and one section per platform that is deliberately NOT an image:
loong64 (no base image exists at any tag), armv6 (gated on a base with
arm/v6), armv7 (the one 32-bit ARM slot must carry the armhf baseline) and the
Windows and macOS bundles. It ends with why the FerretDB image covers more
platforms than the WeKan one — FROM scratch around a static Go binary needs no
userland at all.
The snap's page gains the matching armv6 section — the Snap Store has no armv6 architecture, and its only 32-bit ARM is armhf, which an ARMv6 board cannot run — and a table of all fifteen bundles against which six become snaps, so "it is missing" and "it cannot be there" stop looking alike. The two pages link each other, because the answer differs between them.
docs/Databases/FerretDB/1/README.md said the per-architecture FerretDB binary
was embedded in the bundles "for ppc64le, s390x, riscv64". Every bundle carries
one; those three are part of a longer list of platforms where it is the DEFAULT
because MongoDB publishes no server. It now lists all seventeen built binaries
and separates the three 32-bit ARM builds that are not variants of each other:
armhf is GOARM=7, armv6 is GOARM=6, armel is GOARM=5 software
floating point.
The snap store - what it actually holds, and what it cannot.
<details> <summary><a href="https://github.com/wekan/wekan/commit/cf0c347b2">The snap page says what the store holds at 10.78, and why the empty squares differ</a>. Thanks to xet7.</summary>The table of what each snap has was "as of 10.76" and marked every empty square the same way: built by snapcraft.yaml but that snap has no revision for it yet; uploading one is all that is needed. For eight of the twelve squares that is not true, and it sends whoever reads it at the wrong job.
Three marks now, because there are three situations. wekan's armhf is
BUILT by the release and missing from the store: snapcraft.yaml declares it,
snap-launchpad builds it, and it has been failing - most recently on Caddy,
which publishes no linux_armhf archive because its asset names are Go's.
The exotic squares of wekan-ondra and wekan-gantt-gpl are not pending
uploads at all: the snap-variants job has four matrix entries, each variant
on amd64 and arm64, and the ppc64el/s390x/riscv64/armhf builds come from
snap-launchpad, which builds only the wekan name. And wekan-ondra's
armhf 0.22 is a fossil: there is no newer revision to promote, and nothing
currently builds one.
riscv64 moved from missing to published since 10.76, so wekan is five of six.
Every published architecture is on all four channels, which is the
release-to-every-channel work holding.
and documents how to work on these repositories:
CLAUDE.md and AGENTS.md - who maintains them, what is in .tools/, and how
each repository's changelog is written.
Both files opened with an identity CHECK - "maintainer mode only when the identity is exactly Lauri Ojansivu" - which says what to do with an identity but never says whose repositories these are. They are xet7's: WeKan, the FerretDB v1 fork, node-patches and mongo-tools-patches. That is now the first thing both files say, and the commit author follows from it rather than from a conditional.
Two rules are spelled out under it. No commit, pull-request body or CHANGELOG
line ever attributes work to an AI - no Co-Authored-By, no "Generated with",
no assistant or model name - and CODE_OF_CONDUCT.md is cited as where that comes
from: "mention only those participants that are human". And a missing or
wrong git identity in one of these checkouts is to be SET, not worked around:
the .tools/ clones can come up with no user.name/user.email at all -
mongo-tools-patches did - which would author a commit as whatever the machine's
default is.
.tools/ was one sentence naming three clones. It is now the two kinds of thing
that directory holds, because they are treated differently: companion git
repositories - wekan/FerretDB on main-v1, node-patches and
mongo-tools-patches on main, Secretchronicles/TSC on devel,
sandstorm-io/sandstorm - each with their own history, branches, changelog and
release flow, cloned on demand by build.sh's ensure_tool_repo; and
unpacked toolchains that are downloads rather than repositories -
node-v<version>-linux-<arch>, go with its caches, .meteor, the TSC
AppImage - to be deleted and re-fetched freely.
No CLAUDE.md or AGENTS.md under .tools/. node-patches and
mongo-tools-patches each had a pair, and they were removed: the rules are
identical for all of these repositories and a second copy drifts from the first.
A fact true of one repository only belongs in that repository's own README or
docs - and an instruction file found there is to be removed rather than updated.
Each repository's changelog is written in the format its own file already uses, as a table: WeKan, node-patches and mongo-tools-patches use the WeKan format; wekan/FerretDB keeps upstream FerretDB's; and TSC uses GNU ChangeLog
YYYY-MM-DD Name <email> header over tab-indented * Fix: entries in a
file called CHANGELOG with no extension. The reason is the reader: a FerretDB
release is read beside upstream's, and a TSC entry beside a decade of GNU
entries.The commit above read Secretchronicles/TSC's remote, saw it was not a wekan/
repository, and concluded it was somebody else's project xet7 contributes to. It
is his: the organisation is Secretchronicles rather than wekan, and that is the
only difference. His GitHub profile says it in three words - "WeKan and TSC
maintainer" - and the files cite it, so the next reader checks a source instead
of inferring from a remote URL as that one did.
So maintainer mode covers it - commit directly to devel, no pull request, same
author, no AI attribution - and the files name the one .tools/ repository that
really is somebody else's: sandstorm-io/sandstorm, cloned for reference. What
does NOT follow from maintaining it is WeKan's house style: TSC keeps its own
GNU ChangeLog and its own release process.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the AppImage workflow. It built both AppImages, started them,
waited for WeKan to answer on port 8080, uploaded them as artifacts - and then
attached nothing to the release, because the job that does the attaching never
checks the repository out and its two gh calls were the only ones in the file
without --repo. Fixed, and the matrix grows from two architectures to
four: i686 and armhf are the other two an AppImage runtime exists for.
The Flatpak workflow had the same one-line bug and a second beside it - it
was attaching the ostree repository along with the bundles - and it stays at two
architectures, because a flatpak needs a published runtime and only x86_64 and
aarch64 have one. Beside that, the Docker images now carry full SLSA
provenance at every one of the four sites that pushes one, and a test pins
that every bundled binary - FerretDB, the MongoDB Database Tools, Node.js -
is still fetched as the NEWEST one everywhere it is fetched, which is what makes
those projects' security fixes arrive without a commit here - and the table
below is that working: FerretDB v1.48.0 replaces v10.77's v1.45.0 on every
platform, with no change in this repository, because latest now resolves to
it. That is the release carrying go1.25.11 and x/sys v0.46.0, which answers
the Go advisories a Quay scan reported against the v10.77 image. Node.js stays
v24.19.0, the newest 24.x. The rest of this release is the same theme - what a
build REPORTS versus what it did. The armhf snap failed three times on a
package name that has not existed since Ubuntu 24.04 renamed it, while the
message blamed a transient build-farm reset; three snaps that BUILT were
reported FAILED by the step that saves their logs; and the provenance table
printed every row twice, left amd64 out entirely, and gave six platforms the
version "latest".
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.48.0 | 2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.48.0 | 06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.48.0 | 9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.48.0 | 4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.48.0 | 0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6 |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.48.0 | d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.48.0 | 6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.48.0 | ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2 |
This release adds the following new features:
AppImage - which CPUs get one.
<details> <summary><a href="https://github.com/wekan/wekan/commit/97e57e531">Build the other two architectures an AppImage runtime exists for</a>. Thanks to xet7.</summary>x86_64 and aarch64 become four, with i686 and armhf. That is the whole
set: an AppImage BEGINS with a runtime binary the kernel executes, and runtimes
exist for exactly those four. ppc64le, s390x, riscv64 and loong64 have
none, so they cannot have an AppImage however the job is written - they are
served by the bundle zip and the .deb, and the matrix now says so where somebody
would otherwise try.
The two are not equally safe and are not treated as though they were. i686
builds on the x86_64 runner, which runs 32-bit x86 natively: no emulation, no
multiarch, appimagetool-i686 runs as-is. armhf builds on the aarch64
runner, which runs 32-bit ARM only if the kernel has CONFIG_COMPAT and the
32-bit loader is installed - GitHub promises neither - so it is
continue-on-error, the same treatment TSC gives its emulated armhf job. It
cannot decide whether the other three reach the release.
The smoke test needed the distinction too. It starts the AppImage and waits for port 8080, which a 64-bit runner cannot do for a 32-bit build it cannot execute. A check that cannot RUN is not a failed check: when the binary will not execute at all AND it is not this machine's architecture, it warns, says the AppImage is uploaded unchecked, and moves on. A build that DOES start and then does not answer still fails - that is the bug the step exists for.
</details>Docker images - what an image records about how it was built.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5b3169534">Attach full build provenance at every site that pushes an image</a>. Thanks to xet7.</summary>BuildKit attaches MINIMAL provenance on its own, which is where the
unknown on unknown rows beside each real platform on quay.io come from - that
is the OCI marker for "not a runnable image", so docker pull never selects
one. Minimal is a build id and a timestamp, which answers nothing anybody asks
of a supply chain. mode=max records what does: which commit, which base
image, which build arguments, which frontend.
The hazard is not getting it wrong once, it is that WeKan pushes images from
FOUR places - docker-publish.yml, two sites in release-all.yml, and
releases/docker-build.sh - and a fifth added later would silently fall back to
the default. So the test finds the call sites by searching the repository
instead of from a list, and fails when a pushing one lacks the flag.
SBOM stays off, deliberately: it enumerates every OS package and npm dependency, so the attestation grows from tens of kilobytes to megabytes per platform. Turning it on is a decision rather than a default to drift into.
The --load build in docker-build.sh must NOT ask for it - the docker
exporter cannot carry an attestation at all - and that is a test of its own, so
the flag is not added there by symmetry one day.
One more thing the test pins, because it was written the wrong way twice while
this was being done: a # comment sitting among a continued command's
arguments. docker buildx build \ followed by a comment line comments out the
REST OF THE JOINED LINE, so the command becomes a bare docker buildx build
with every platform, tag and flag swallowed. bash -n accepts it - it is valid
syntax, just a different command - and a YAML run: block is a shell script,
which is where it happened the first time.
and fixes the following bugs:
The snap builds - what the Launchpad jobs build, and what they report.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a820b056b">libcurl4 does not exist on armhf, so name the package noble ships</a>. Thanks to xet7.</summary>The armhf snap failed all three attempts in v10.77 while s390x, ppc64el and riscv64 each built on attempt 1. The Launchpad build log says why, one line into the mongodb part:
Stage package not found in part 'mongodb': libcurl4.
Build failed
Ubuntu 24.04's 64-bit time_t transition renamed the library, and libcurl4 is
not a real package on any architecture in noble - the binary is
libcurl4t64 everywhere. On the 64-bit architectures the renamed package keeps
the old name alive, Provides: libcurl4 (= 8.5.0-2ubuntu10), and the armhf
libcurl4t64 has no Provides line at all, because there the ABI really
changed. That is what makes this class of bug reach exactly one architecture -
which is also what makes it look like flakiness on that architecture.
It had already happened, to libssl3 and libgoogle-perftools4 in v10.71, and
the comment above this list describes it exactly; libcurl4 was the same
transition and was left behind. All fourteen stage-packages were checked against
the noble armhf main+universe indices this time, and it was the only one that
did not resolve.
The workflow's own guidance made it worse. snapcraft ends a missing
stage-package as a Stopped build with no artifact, which from the outside is
indistinguishable from Launchpad cancelling a build, so the message offered "an
OOM in the Meteor npm install, or a transient build-farm reset - re-run" for a
failure that will never succeed on a re-run. The step now looks for that line
first, names the missing package, says it is not transient, explains the
Provides asymmetry, and gives the one-line archive query that checks a name.
s390x, ppc64el and riscv64 each printed "Remote build <arch> succeeded on
attempt 1" and downloaded their .snap - and each job then ended FAILED,
because the step that saves the build log could not upload it:
The path for one of the files in artifact is not valid:
/snapcraft-wekan-f82a93c2…_s390x_2026-08-09T11:39:05.txt.
Contains the following character: Colon :
snapcraft names a remote-build log after the recipe and an ISO timestamp, and
upload-artifact refuses a colon because NTFS cannot hold one. The colons
become dashes now, so the timestamp is kept rather than the name thrown away -
and the upload is continue-on-error, because a diagnostic upload must
never be able to fail the build it is diagnosing. The snap is the deliverable,
and the renaming is not the last thing that could ever make a log unuploadable.
The release notes - what the provenance table says each bundle carries.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6dce9dda7">Every row appeared twice, amd64 appeared not at all, and six platforms said "latest"</a>. Thanks to xet7.</summary>Three things wrong at once in the v10.77 table, and every one of them silent - nothing errored, the table was simply not what it claimed to be.
Every row appeared twice. provenance-table.sh globbed
provenance/**/*.tsv provenance/*.tsv, and with globstar a **/ matches
zero or more directories, so the first pattern already covered the second
and every file was read twice. It is one pattern now, and rows are also
deduplicated on the WHOLE line - a retried step records an identical line and
nothing tells the copies apart. Deliberately not sort -u -k1,1 -k2,2: two rows
sharing a bundle and a binary but differing elsewhere are not a duplicate, they
are a real disagreement about which Node.js went into a bundle, and hiding one
at random is worse than showing both.
amd64 was missing - the platform most people download. Its step runs
cd .build before mkdir -p provenance, so the rows went to
.build/provenance while the upload looked at provenance/ from the workspace
root and found nothing. Every other build job records from the workspace root,
which is why only this one was affected. That turned out to be half the story:
the scripts themselves were not being found either, which the next release
fixes.
Six platforms said Version latest. amd64, arm64, win64, win32, mac-arm64
and mac-x64 passed the literal string; only the extra-architecture job asked
what latest actually was, with its own inline curl. "Which FerretDB did
v10.77 ship" is the one question that column exists to answer. All seven sites
now call releases/ferretdb-latest-tag.sh, which asks once per job and caches,
authenticates when there is a token so a shared 60/hour limit is not what makes
it fail, checks the answer is shaped like a tag before printing it into a
markdown cell, and prints nothing and exits 0 when it cannot find out - a
release note must never fail a build that produced a good bundle.
The table's prose also linked wekan/node; the binaries come from
wekan/node-patches, which is what the
rows themselves already linked.
The release upload - what reaches the release page.
<details> <summary><a href="https://github.com/wekan/wekan/commit/fa296d0c8">Attach the built AppImages to the release again</a>. Thanks to xet7.</summary>Both AppImages built. Both passed the smoke test that starts them and waits for WeKan to answer. Both uploaded as artifacts, and both were downloaded by the attaching job with matching SHA256 digests. Then:
gh release upload 'v10.77' assets/* --clobber
failed to run git: fatal: not a git repository
That job does not check the repository out - it has no reason to, it only
downloads artifacts - so gh had no git remote to infer the repository from.
Every other gh call in the workflow already passed --repo; those two did
not.
Every call passes it now, including the ones in jobs that DO check out: a call
relying on an inferred remote breaks the day somebody removes the checkout, and
the error it gives names neither gh nor the workflow.
Worth recording for whoever reads that failed run, because the log looks far
worse than the bug: ::error:: lines appear in the build jobs for the
bundle-file check and the port check, and in the attaching job for "no
architecture produced an AppImage - every build job failed". None of them fired.
They carry the escape codes of the ##[group] header - they are the SCRIPT
being echoed, not output - and a few lines below them ls -lh assets shows both
AppImages sitting there at 225M and 227M. One line in the whole run was a real
error.
The same one-line bug as the AppImage workflow above, from the same cause: the
job that attaches the bundles does not check the repository out - it only
downloads artifacts - so gh had no git remote to infer the repository from and
gh release upload failed with "fatal: not a git repository". Both flatpaks
had built and uploaded as artifacts; nothing reached the release. Every gh
call in that workflow passes --repo now too.
And a second one beside it: assets/* was not the bundles. The artifacts also
carry the ostree repository the flatpak was exported through - config,
objects/, refs/, summaries/, summary, summary.idx - which is build
scaffolding, and attaching it would have put a few hundred directories on the
release beside the two files anyone wants. The upload names the bundles and
their checksums instead.
No architectures could be added here, and the workflow header now says why
rather than leaving it to be rediscovered. A flatpak runs against a RUNTIME, not
the host's libraries, so an architecture exists only if freedesktop.org
publishes org.freedesktop.Platform for it: x86_64 and aarch64, the i386 and
arm runtimes having been discontinued. That is the difference from the AppImage
work above, which could grow from two architectures to four - an AppImage
carries its own runtime binary, and those exist for i686 and armhf as well.
and has the following developer-facing change:
Bundled binaries - keeping "newest" true everywhere it is claimed.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ce69db8c5">Pin that every bundled binary is fetched as the newest one</a>. Thanks to xet7.</summary>WeKan ships binaries other projects publish - FerretDB, the MongoDB Database Tools, Node.js - and fetches each from a release URL. There are many such URLs, across workflows, release scripts and compose files, and they must all agree: one that names a fixed version stops receiving that project's security fixes silently, and nothing about the build fails when it does.
That is not hypothetical. A Quay scan of the v10.77 image reported Go
advisories in the FerretDB binary baked into the bundle - stdlib 1.25.9
wanting 1.25.11, golang.org/x/sys v0.38.0 wanting 0.44.0. The source was
already fixed and v1.48.0 was already published carrying go1.25.11 and
x/sys v0.46.0; the image had simply captured an older latest at build time.
Had any of these URLs been pinned instead, the rebuild would not have fixed it
either.
The URLs are found by searching the repository rather than from a list, so a
build site added later is checked too, and three shapes are accepted:
latest/download/<asset>, the bare latest API endpoint that release-all.yml
asks which version latest resolved to so the provenance table can record it,
and ${VAR} whose default is separately asserted to be latest - that is
FERRETDB_RELEASE in the compose files, which an operator may pin for their own
reasons but which must not freeze everybody who does not.
Node.js is a different mechanism with the same effect, so it is pinned too:
NODE_VERSION is the bare major 24, and releases/resolve-node-source.sh
answers with that CPU's newest 24.x from whichever of nodejs.org,
unofficial-builds or wekan/node-patches
has one.
What makes latest safe rather than merely convenient is the provenance table
above: rebuilding an old release would embed a different FerretDB than it
shipped with, and the only reason that is a trade rather than a hole is that
every release RECORDS the versions and SHA256s it actually shipped. So that is
asserted here as well - releases/record-provenance.sh exists, and the
CHANGELOG still carries the table it produces.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the snap side, which had three problems that looked like one. The helper that put a build on channels released ONE snap, ONE revision, to THREE channels - and a revision number is per architecture, so it could only ever be right for one of them. The page documenting the CPU platforms listed five architectures and omitted armhf, which has been built all along. And the two architectures that are release bundles but NOT snaps - i386 and armv7 - were nowhere, so "missing" and "cannot be there" looked identical. The binaries below are v10.76's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release adds the following new feature:
Snap publishing - getting every snap onto every channel without typing a revision number.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a5d8a1fd9">Release every snap, every architecture, to all four channels</a>. Thanks to xet7.</summary>The helper this replaces was snapcraft release wekan $1 edge,beta,candidate,
which is wrong three ways at once. It names only wekan, leaving wekan-ondra
and wekan-gantt-gpl to be done by hand. It takes ONE revision number, and
revisions are PER ARCHITECTURE - the store shows wekan at 3601 on amd64 and
3600 on arm64 for the same 10.76 - so one number can only ever be right for one
of them. And it leaves out stable, so a build reached three channels of four
and somebody had to remember the fourth.
releases/snap-release-all-channels.sh resolves the revision per (snap,
architecture) from the store itself, so no revision number is ever typed, and
releases it to all four channels in ONE call - a revision reaches all of them or
none. A pair with no revision is reported and skipped rather than failing the
run: the three snaps genuinely have different architecture sets today.
The mapping is the hazard, and it lives in one place now
(models/lib/snapArchitectures.js) with both directions tested. ppc64le and
ppc64el ARE the same hardware - the bundles use the kernel's name, the store
uses Debian's - and it is the only rename. Nothing warns when the wrong one is
used: an unrecognised architecture is simply one the store has never heard of,
so it looks like it worked.
Pass the version to pin it. Without one the newest revision of each architecture
is promoted, and edge is often ahead of stable, so a bare run publishes edge
builds to stable users; --dry-run prints the plan first.
and improves the following documentation:
Snap CPU platforms - which six, why not the other two, and how the names differ.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3c9291338">Say which six architectures are snaps, and why i386 and armv7 are not</a>. Thanks to xet7.</summary>docs/Platforms/FOSS/Container/Snap/CPU-platforms.md listed five architectures
and omitted armhf, which snapcraft.yaml has built all along. It said the
release publishes candidate, beta and edge and that stable "is published
manually
later" - no longer true, and the reason a build reached three channels of four.
The matrix is now the six build-for: entries with the bundle name beside each,
and a new section explains the three ways the two naming systems differ.
armhf and armv7 are not a rename, and getting it wrong ships a snap that
crashes. node-patches builds armhf to
the Debian baseline - hard-float, VFPv3-D16, assuming no NEON - so it runs on
any
ARMv7-A, and armv7 with NEON for boards that have it. The Snap Store has ONE
32-bit ARM architecture serving every such device, so it must carry the BASELINE
build: the NEON one would be an illegal instruction on a board without NEON. So
armv7 ships as a bundle only, and a test cross-checks that explanation against
node-patches' own workflow so it cannot drift from the binaries.
i386 cannot have a new snap at all, and it is categorically different from a
missing Node.js build. node-patches patches SOURCE so a binary can be built;
here
the BASE SNAP does not exist, because Ubuntu 24.04 has no i386 port - no patch
set produces a base Canonical does not publish. The last base with one was
core18, end-of-life. The store still shows an i386 column for wekan-ondra
because it keeps whatever was ever uploaded; that revision is 0.X-ci and
nothing can replace it.
The page also records what each snap has in the store today and what is still to
upload, including the two fossils channel promotion cannot fix - wekan-ondra's
armhf at 0.22 and its i386 at 0.X-ci, which have no newer revision to promote.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: three round-trip counts that were linear in the input, and are not
any more. The bulk label endpoint asked the database twice per card - a
thousand sequential round-trips at its 500-card cap - global search resolved
each named user with its own lookup before the search could start, and
FerretDB skipped every top-level $or when building a WHERE clause, so the
board-list query narrowed nothing in SQL and filtered every row in Go. None of
them was a wrong answer; each was the right question asked one document at a
time. The binaries below are v10.75's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release makes the following faster:
The REST API - how many times one request talks to the database.
<details> <summary><a href="https://github.com/wekan/wekan/commit/90a41dbb2">Bulk label changes read every card in one query, and write them together</a>. Thanks to xet7.</summary>PUT .../cards/labels awaited a getCard per id and then an update per id. At
the BULK_CARDS_MAX of 500 that is one thousand sequential round-trips for
a single request, each starting only once the last one finished.
The reads are all the same question, so they are one $in query indexed into a
Map. The writes genuinely differ - each card merges its own labelIds - so they
stay individual updates, but they are issued together and awaited once instead
of each waiting for the last.
Two details had to survive. The loop still iterates the caller's cardIds
rather than the query result, because a batch read comes back in the database's
order and without the ids that matched nothing: iterating it would silently
reorder updated and lose notFound. And every write is awaited before the 200
is sent, so the response still means what it says.
The bulk DELETE beside it is deliberately left alone. Its per-card work is real
cardRemover runs the sub-item hooks and each card gets its own activity - so
batching the reads would save one query inside a loop that does far more than
query.Global search - what happens before the search itself starts.
<details> <summary><a href="https://github.com/wekan/wekan/commit/90a41dbb2">Every username a query names is resolved in one lookup</a>. Thanks to xet7.</summary>Each user:, member:, assignee: and creator: predicate resolved its name
with its own awaited findOne, so member:ann member:bob member:carol was
three serial round-trips before the search could begin.
They are all the same question - which of these names is an account - so it is
asked once, with $in, and answered from a map. A name typed under two
operators is one lookup now rather than two.
An unknown name is still reported against the operator it was typed under: "ann is not a user" is not useful without saying where ann was typed.
</details>The database - what SQLite is asked, and what is filtered afterwards.
<details> <summary><a href="https://github.com/wekan/FerretDB/commit/dab729eb">FerretDB pushes a top-level $or down to SQL when every branch can be</a>. Thanks to xet7.</summary>FerretDB skipped every top-level $-key when building its WHERE clause, so a
selector whose only SELECTIVE terms sit inside an $or produced a clause that
narrowed nothing: SQLite returned the rows, and every one was decoded and
filtered in Go to return a handful.
That is the shape of WeKan's "which boards may this user see" query, and the
worst possible one for it - archived = false and type = 'board' push down
and match nearly everything, while the membership clauses that actually select
stayed in Go. On an instance with ten thousand boards where a user belongs to
five, that decoded ten thousand documents to return five, on every All Boards
load.
It is all or nothing, and that is the whole subtlety. Every other pushdown
NARROWS: a condition that cannot be expressed is dropped, the WHERE returns a
superset, and the Go filter removes the rest. An OR that drops a branch REMOVES
rows that match it, and the Go filter never sees them. So one unpushable branch
refuses the whole $or, as does a nested-operator branch and an empty one.
See the FerretDB CHANGELOG for the database side.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: two things the full test run turned up, one in WeKan and one in
the scripts that run it. A linked-card cache that could not see a card
added after the first time a subscription asked, and the build script parity
guard that was the one red suite - right twice over, because build.bat really
was missing the two entries and the guard really could not tell a shell function
from a script. The binaries below are v10.74's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release fixes the following bug:
The board publication - what a subscriber is sent, and when.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ada480c93">Linked-card ids are no longer cached for the life of a subscription</a>. Thanks to xet7.</summary>The helper the five linked-card cursors share memoized its result per board, to
spare them the duplicate queries the five copies of that preamble used to make.
That is wrong: publishComposite re-runs a child's find() when the parent
document changes, and a cache living for the whole subscription then serves the
ids computed the FIRST time forever - a linked card added later would never be
published, and one removed would go on being published.
The five cursors each ran these same queries before the helper existed, so computing per call is exactly the cost they always had, and it is correct. The guard that pinned the memoization now pins its absence, with the reason, so the next reader does not put it back.
</details>and has the following developer-facing change:
The build scripts - what each menu offers, and what the guard between them compares.
<details> <summary><a href="https://github.com/wekan/wekan/commit/114d6c059">git pull and git push work on Windows too, and the parity guard knows what a function entry is</a>. Thanks to xet7.</summary>The one red suite in the full test run, and it was right twice over.
The commit that replaced Update git with a git pull and a git push that
finish the job added both to build.sh's menu and left build.bat behind, so
build.bat git-pull answered nothing. It answers to both names now, with its
own :gitpull / :gitpush labels - the ones its interactive menu already used.
The guard also had to learn what a ! entry is. It marks something build.sh
runs ITSELF rather than by executing a file in releases/. Most are raw
commands and the .bat runs those the same way, so those still compare as
before; the exception is an entry naming a build.sh FUNCTION, which is shell
the .bat has no way to call. Comparing those as scripts is what made the suite
fail the moment Update git was replaced. A function entry is exempt from the
script comparison now and checked separately: build.bat must implement a label
of the same name, so one dropping out of a menu still fails.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: four security reports from Alpastx, all of them the same
mistake in four places - a rule that exists, is correct, and is not asked.
PathBleed let an avatar's owner write the on-disk path of their own avatar
and have board export read that file back out, because the guard attachments
have was never copied to avatars. RevokeBleed left a revoked organization,
team or domain share working in the board publication, because that
publication had its own copy of the visibility query and the copy ignored
isActive. ParentBleed let a card on one board name a parent on a private
one and had the publication deliver that private card to everybody, because
nothing checked who may see the parent's board. CommentBleed let any board
member delete anyone's comment over REST, because the object-level rule lived in
a collection hook that cannot see an HTTP caller. Each fix puts the rule in ONE
place that both callers use, and each comes with a plain-node suite that pins
the attack and the negatives. Auditing for more of the same found five more
cursors leaking cross-board content the way ParentBleed did, three more
hand-written copies of the visibility query, and a comment reaction anybody
on the board could put in somebody else's name.
The features under them are two answers to "and then what": canary tokens, which record WHO tried a permission override and from WHERE without telling them they were seen, and a daily filesystem integrity check that asks whether every stored file is still the file WeKan stored - name, date, md5, sha256, sha512 and an ed25519 signature - and warns when one changed with no record saying why. Below that: dependency updates, the two bugs Admin Panel / Problems was itself reporting, and the security tests, which now say WHICH published vulnerability they guard so a new guard can check the whole Hall of Fame list against them - 33 of 58 covered, the other 25 recorded gaps with reasons. The binaries below are v10.73's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release fixes the following CRITICAL SECURITY ISSUES:
Avatars and board export - where an uploaded picture lives on disk, and what an export is allowed to read.
<details> <summary><a href="https://github.com/wekan/wekan/commit/0a9eb0ef4fb3600917ef8b55d4c6afb089873e98">PathBleed: an avatar could name any file on disk, and board export read it</a>. Thanks to Alpastx and xet7.</summary>PathBleed -
GHSA-4mxf-m8pq-xc9p,
High, CWE-22 and CWE-73. Attachments blocked client-supplied
versions.*.path; avatars did not. The avatar allow rule was the single line
update: isOwner - being the owner let you write ANY field of your own avatar
document, versions.original.path included. That field is server-managed: it
says where on disk the bytes are.
Board export then read it. For every member whose avatar is a local WeKan file
the exporter opens that path and embeds the bytes as base64 in
profile.avatarFile, so an authenticated user could point their own avatar at
/etc/passwd - or anything else under WRITABLE_PATH - export a board they
are a member of, and decode the file out of the JSON. Arbitrary file read as
the WeKan OS user, from any account that can own an avatar.
Both halves are closed. The WRITE: the guards attachments had now live in one
module and BOTH permission files import it - avatars had gone without them
precisely because each collection wrote its own copy. Avatars refuse an insert
carrying versions.*.path or .storage, refuse any update touching the
versions subtree, and restrict updates to the same field whitelist attachments
use; being the owner is still required and no longer sufficient. The READ:
nothing is read from a stored path unless it RESOLVES to somewhere inside
WeKan's own storage. That half also holds when a path is poisoned some other way
existsSync, was the last place left and takes the same check -
not the reported hole, since the attachment allow rule refuses a client-supplied
path, but a path is only as trustworthy as every way it could have been written.The board publication - who is sent a board, and which of its cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/08baf7fd1b76a9bb889de2220bdc10c36aad6a65">RevokeBleed: revoking an org, team or domain share did not revoke it</a>. Thanks to Alpastx and xet7.</summary>RevokeBleed -
GHSA-gwc4-fw7p-gw58,
Moderate, CWE-639 and CWE-863. isActive: false is how a board admin REVOKES a
share with an organization, a team or an email domain. All Boards honoured it -
it matches shares with $elemMatch: { ..., isActive: true } - so the board
disappeared from the revoked user's list and everything looked correct.
The board publication did not. That is the publication that sends the board
document and its lists, swimlanes, cards, comments and attachments, and it
matched shares with 'orgs.orgId': { $in: orgsIds } instead. A dotted path
matches an element of the array without saying anything about that element's
other fields, so isActive was never consulted: anyone who still knew the
boardId - a bookmark, a browser tab, a note - could subscribe and receive the
whole private board after their access had been taken away. A revoke the
primary data publication does not honour is not a revoke.
The rule was written out twice and the two copies disagreed, so it is written
once now: one builder makes the $or, both Boards.userBoards and the board
publication call it, and every share kind is matched with $elemMatch
requiring isActive: true. includePublic: false still drops the public clause
and only that clause, which is what the search over all boards needs.
ParentBleed -
GHSA-jvv9-498p-hxrg,
Moderate, CWE-200 and CWE-862. A card's parentId may name a card on ANOTHER
board, and setting it was authorized only against the CHILD board's write ACL.
Nothing asked whether the person setting it, or the people subscribed to the
child board, were allowed to see the other board at all.
The board publication walks the whole ancestor chain, because the prefix-with-full-path subtask setting renders a subtask's complete path (#3453), and it published the complete ancestor card DOCUMENTS to every subscriber of the child board. A member who could write on shared board B and knew a card id on private board A could point a card at it and have A's card - title, description, custom fields - delivered over DDP to people who are not members of A.
Closed at both ends, because either alone leaves half the hole open. The WRITE refuses a parent whose board the actor cannot see - one question, asked in one place with the same selectors the publication and All Boards use, so an ACTIVE org, team or domain share counts and a revoked one does not. It is enforced on the REST card create, on the REST card update, and on DDP by a deny rule covering insert as well as update, beside the cross-board MOVE deny that GHSA-gm7v-pc38-53jr added. The PUBLICATION sends only the ancestors whose board the subscriber may see; the board being published is its own answer, so an ordinary same-board subtask path is unchanged. This is the shape of check the linked-card path already made - creating a linked card requires read access to the source card's board - applied to the field that did not have it.
Auditing for more of the same
found the LINKED-CARD cursors beside the ancestor one with the identical hole
and a wider blast radius: five of them, publishing the linked card, its
comments, its attachments, its checklists and its checklist items. A
cardType-linkedCard names a card by id exactly as parentId does,
and that card may live on any board. They take the same answer - a linked card
whose source board the subscriber cannot see is not sent - and they share one
helper now instead of repeating the same fifteen-line preamble five times, which
is what stops the sixth from being written without the check.
The REST API - what an HTTP caller may do to somebody else's content.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a8fa6cfa3fb6c519245a4425399da3a9838bea95">CommentBleed: the REST API let any board member delete anyone's comment</a>. Thanks to Alpastx and xet7.</summary>CommentBleed -
GHSA-pqr4-rxgp-hv2m,
Moderate, CWE-639 and CWE-863. Over DDP, deleting a comment is
author-or-board-admin, and a board that sets restrictCommentEditing takes even
the admin's ability away (#5906). That rule was enforced in a collection hook
keyed off the Meteor userId, with an early return for the genuine
server-internal callers - board copy, cleanup, migrations - that run with no
authenticated user.
An HTTP request carries no Meteor userId into the invocation context. So
DELETE /api/boards/:boardId/cards/:cardId/comments/:commentId, which checked
only board MEMBERSHIP, reached the collection with no user, the hook took its
"server-internal, trust it" path, and any normal member could delete any comment
on the board: HTTP 200, the comment gone, restrictCommentEditing or not, while
the same deletion over DDP was correctly refused. Harassment and evidence
destruction on a shared board.
The fix does not rely on the hook seeing something it cannot see. The handler loads the comment - 404 when there is none - and applies the same rule itself, with the REST caller's id, before removing anything. That rule is now an exported function the hooks and the handler both call, so DDP and HTTP cannot enforce different things, and it carries a 403 so a refusal answers Forbidden rather than 500. The no-userId path stays, documented for the internal callers it was written for.
</details>and adds the following new features:
Admin Panel / Problems / Security - what an admin is told when somebody probes.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d577f0770">Canary tokens record who tried to override permissions, and from where</a>. Thanks to xet7.</summary>The security event log said what a guard DID - a request blocked, a filename sanitized - which answers "is WeKan defending itself" and not the question an admin asks next: who did that, and from where. It also could not tell a browser that got confused from somebody working through the Hall of Fame one entry at a time.
A canary is a tripwire at a point that only a permission-override attempt reaches. Ordinary use never gets there, so a trip is not noise. Three properties define one, and each is enforced by a test.
Silent. Tripping one changes nothing the caller can observe: tripCanary()
always returns false and tripCanaryDeny() always true, so a call site
reads as the refusal it replaces, and the REST handler re-throws the ORIGINAL
error. This is not politeness - a canary that announces itself is a map of which
paths are watched, and a probe would avoid them.
Bounded. A canary sits where an attacker can loop, so one database row per attempt would be a denial of service they get for free. The first trip of a (canary, actor) pair is recorded at once, the rest of the window are COUNTED, and one summary carrying the total is written when the window closes; a pair that never gives up stops writing after sixty summaries. The tracked-pair map is capped and evicts the least RECENTLY seen, so a long-running attacker is not pushed out by a passing one. A thousand attempts in a minute cost one row, and a suppressed trip costs one map lookup.
Attributed. Every event carries the account, the username, the IP address and the attempt count. The username is stored at write time on purpose
Seventeen canaries sit at the permission checks that refuse the attempts behind
BoardBleed, ParentBleed, ChecklistBleed, PathBleed and CommentBleed, and nine
more cover NoSQL injection (an execution operator in a client selector, or
{"$ne": null} where a typed value belongs), SQL injection (the database's
own guard now marks its refusal so the attempt reaches the admin instead of a
log file), sanitization that removed something dangerous rather than merely
tidying, a forged forwarded-for header, and a login lockout. All
server-side: no browser, nothing to install.
Admin Panel / Problems / Security gains Username, IP address and Attempts columns, both new ones searchable - the thing an admin does with one security event is pivot on it.
</details> <details> <summary><a href="https://github.com/wekan/FerretDB/commit/a260a53e">The database marks the operations WeKan never issues, so an operator sees them</a>. Thanks to xet7.</summary>FerretDB is reached over a local socket by one application, whose driver is a
Meteor 3 one. That makes a class of operations interesting by their mere
presence: server-side JavaScript ($where, eval, $function), an aggregation
writing into a collection ($out, $merge), dropping a database, a
server-administration command. The driver does not send them, so a request that
does is either a bug or somebody who reached the socket and is looking around.
internal/util/canary refuses them with the ordinary "operation not supported
by this build" - the same answer an unimplemented command gets - and appends
canary:<id>, which WeKan reads off the error and records with the account and
the address. The package writes nothing: no file, no table, no counter, so
hammering it costs one string comparison per request. The SQL guard marks
its refusals the same way; it already refused a statement carrying what only
injection produces, but a line in the database's own log is not somewhere
anybody looks.
On MongoDB there is no FerretDB to mark anything and these operations simply never appear, so the feature degrades to nothing rather than misbehaving.
</details>Admin Panel / Problems / Filesystem integrity - whether the stored files are still the files WeKan stored.
<details> <summary><a href="https://github.com/wekan/wekan/commit/2cc0d42dd">A daily paced check of every stored file's name, date and four hashes</a>. Thanks to xet7.</summary>Attachments and avatars are files under WRITABLE_PATH, and the database holds
one document per file. Nothing checked that the two still agree. A file can
be replaced, truncated, back-dated or deleted by anything that reaches the
filesystem - a bad restore, a sync tool, a container rebuild, a shell on the
volume - and WeKan would keep serving whatever is there now.
A baseline per file now lives in the existing WeKan database (no new files under
WRITABLE_PATH): path, size, modification time and md5, sha256 and sha512.
Three, because md5 is what other tools print - so an admin can compare with a
backup using what they already have - and because two digests over the same
bytes cannot disagree: when they do, the bytes were not read the same way twice,
which is a failing disk rather than a substitution, and is its own critical
finding. ed25519 is the fourth check and is not a hash but a signature,
answering what the digests cannot: who says these are the right hashes?
Anybody who can rewrite a file can rewrite a row of hashes, so each entry is
signed and verified on every scan.
The scan runs once a day, never at or above 60% CPU, with a pause between every file (50 ms, plus 20 ms per megabyte) and a fifteen-minute budget after which it stops and continues tomorrow - and reads each file once for all three digests. A run that stopped early does not report what it never reached as missing.
The finding is a change with no record saying why. A change WeKan made is reported once and re-baselined; a change nothing accounts for keeps showing until somebody looks. Crashes and downtime are in the same stream, from a heartbeat the next start reads: a first run and a clean stop record nothing, and a long gap with no clean-shutdown mark says so, with how long the server was down.
</details>and updates the following dependencies:
tests/playwright only; it ships in no WeKan bundle.Thanks to dependabot.
and fixes the following bugs:
Comment reactions - who a reaction says it belongs to.
<details> <summary><a href="https://github.com/wekan/wekan/commit/156121c4bc0428a5a1edf5db9fbda6cac916d3ea">React as yourself, not as somebody else</a>. Thanks to xet7.</summary>The same shape as CommentBleed, one collection over. A CardCommentReactions
document holds
{ cardCommentId, reactions: [ { reactionCodepoint, userIds } ] } and the whole
array is ONE field, whose allow rule was board membership for
insert, update and remove alike. So any member could $set reactions to
anything: add a colleague's userId to a reaction they never made, or remove one
they did. toggleReaction() only ever touches the caller's own id, so no
legitimate client sends anything else - the rule simply never said so.
Integrity rather than confidentiality, since reactions are visible to the whole
board already, but it puts words in another person's mouth. A deny rule now
refuses an update that changes any OTHER user's presence in any reaction. The
decision compares MEMBERSHIP rather than array order, because the client
rebuilds the array on every toggle and a reordered array with the same
membership is the same set of reactions. The modifier forms that cannot be
checked that way - $push, $pull, $addToSet, $unset, a dotted
reactions.0.userIds - are refused outright. Read-only and no-comment members
still may not react at all, as before.
Admin Panel / Problems / Database problems - two of its own reports, acted on.
<details> <summary><a href="https://github.com/wekan/wekan/commit/30e8e38f6">Fix the two bugs the Database problems page was reporting</a>. Thanks to xet7.</summary>The page was doing its job and nobody had acted on it. moveSwimlane threw
"update is not available on the server. Please use updateAsync()" four times
in
a week, because the default-swimlane self-heal called the synchronous
Swimlanes.upsert() that Meteor 3 removed on the server; it starts the async
one
without waiting now, since the getter around it cannot await. moveList threw
"ValidationError: Failed validation, Cannot read properties of undefined
(reading 'title')" when a list had no title: the insert failed schema
validation and collection2's error formatter then crashed on the undefined
field, so the admin saw neither the list nor the real problem. It now says
"This list has no title, so it cannot be moved to another board" before the
insert, and the two console.log lines that printed the title to a log nobody
reads are gone.
Both errors also gained a classifier rule, so neither reads as unknown / unclassified again: they say plainly that this is WeKan's bug rather than the database's or the admin's, and where to report it.
</details>and has the following developer-facing changes:
The test suite - what it claims to guard, and what it actually does.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a4a9c03b1fc75d61760a35c87decbaf7a3081973">Security tests say which vulnerability they guard, and the list is checked</a>. Thanks to xet7.</summary>"Are the security tests enough to prevent what is in the Hall of Fame" was
unanswerable. WeKan publishes 58 named vulnerabilities; some suites named the
one they belong to, most did not, and the Hall of Fame lives in a different
repository that CI never checks out - so answering it meant reading 58 pages
against 300 suites by hand, which means it was never answered. A regression test
that quietly stops existing is worth nothing, which is the failure mode
tests/testsAreRegistered.test.cjs already caught once, when two *bleed suites
had drifted out of the mocha index.
The list lives in the repository that has the tests now, and
tests/securityRegressionCoverage.test.cjs keeps the two in step. Every
published vulnerability is either GUARDED - named by a suite that still exists -
or RECORDED, a gap with a written reason, which is this CHANGELOG's TODO Later
pattern applied to tests. The gap count is pinned, so a vulnerability cannot be
published with neither a test nor a note; a gap that turns out to be guarded
after all fails too, so coverage is never understated; and a file that merely
REGISTERS suites is not accepted as coverage, or the guard would pass itself.
Getting there meant naming vulnerabilities in the suites that already guarded
them. tests/securityMeifukun.test.cjs guards eight reports and named one: its
sections are RedirectBleed, SourceBleed, LiveBleed, CasBleed, OidcBleed,
MetricsBleed, ImpersonateBleed and InviteBleed - seven vulnerabilities that
looked untested and were not. tests/noIdentityReplacement.test.cjs guards
IdentityBleed and PatternBleed. ExportBleed, CrashBleed, MimeBleed and the four
LockoutBleed suites now say so too, and the cross-board suite also checks
BoardBleed's move deny on Lists and Swimlanes, not only Cards.
The count that comes out of it: 29 of 58 published vulnerabilities have a named regression test, and 29 are recorded gaps - mostly older fixes from before WeKan tested its security fixes at all. They are not known to be unprotected; they are known to be unchecked, which is a different and more honest statement, and each one now says what it would take to close it.
</details>The release and setup scripts - what the build menu offers, and what it still carries.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6def3a14d">Setup: "git pull" and "git push" that finish the job, replacing "Update git"</a>. Thanks to xet7.</summary>The build menu's Update git did a git pull and left it there, so a
contributor who used it still had to know the other half by heart. It is two
entries now - one that pulls and one that pushes - and each does the whole
thing, submodules included, rather than the first step of it.
Two scripts in releases/ had been replaced by the release workflow and were
kept only because guards had been written to exempt them. Both the scripts and
their exemptions are gone, so the guards now describe what is really there -
an exemption that outlives its reason is how a check quietly stops checking.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a GitHub CodeQL finding fixed after v10.72 was tagged - PatternBleed, a string replacement that replaced a hyphen with itself, so an escape that looked like one was not there - and a guard that catches the whole class in WeKan's own test run rather than days later in a web interface. The binaries below are v10.72's: nothing here rebuilds them.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release fixes the following SECURITY ISSUE found by GitHub CodeQL code scanning:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f3a12533d756a01472e0d1904eaafb39ec776880">PatternBleed: a string replacement that replaced a hyphen with itself, and a guard for the whole class</a>. Thanks to GitHub CodeQL code scanning and xet7.</summary>PatternBleed - code scanning
alert #431, rule js/identity-replacement (CWE-116), in
tests/releaseNodeSources.test.cjs: a platform name was interpolated into a
regex through p.replace('-', '-'), which replaces a hyphen with a hyphen. It
reads as "escape this before putting it in a pattern" and does nothing at all,
so the value went in raw.
Nothing failed, because a hyphen outside a character class needs no escaping -
but the guard it looked like was not there, and a platform name carrying a .
or a + would have matched the wrong row or thrown. CodeQL is right to flag the
shape: its usual cause is a mistyped backslash escape, where a replacement meant
to double a character silently is that character. The name is escaped for real
now, with the same escapeRegExp the other guards in tests/ use.
tests/noIdentityReplacement.test.cjs catches the class rather than the
instance - code scanning reports these days later in a web UI, the node suites
report in fifteen seconds. Three things it took to make it honest: it compares
the two sides as VALUES rather than as source text, since an escaped quote and a
plain one are the same value and a text comparison would miss the very mistake
it exists for; the two quote styles are separate alternatives rather than one
character class excluding both, because CodeQL's own example puts a double quote
inside a single-quoted literal and the first shape of the pattern could not
match it; and comments are stripped, with the guard skipping its own file,
because this file and the one it was written for both quote the bad line to
explain it. Verified in both directions - the repository is clean, and the same
scan against the previous commit reports the offending line.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a GitHub CodeQL finding is fixed - a string replacement that
replaced a hyphen with itself, so an escape that looked like one was not there -
with a guard that now catches the whole class in the test run rather than days
later in a web UI. Then: the prereleases WeKan was running on become finals -
Meteor
3.5.1 and @meteorjs/rspack 2.1.0 - and two dependencies take a major
version: jQuery 4 and @babel/parser 8. jQuery 4 stopped the server from
starting at all. It throws jQuery requires a window with a document the moment
it is loaded outside a browser, and the CSV importer carried an unused
jQuery import that the server bundle pulled in, so every start died before the
first route existed. That import is gone, and a new guard walks the server's
import graph so no browser-only package can reach it again. The snap builds
are the other half: v10.71 published no snap for armhf, s390x,
ppc64el or riscv64 and no wekan-gantt-gpl amd64, for three unrelated
reasons - a Caddy version lookup rate-limited by the GitHub API, two
MongoDB library packages under names Ubuntu 24.04 does not publish on armhf,
and a bin that is staged when it is not a directory - and the
snap-launchpad job now keeps the whole build log and outlives its own
retries, which is what made those three take two attempts to find. On a board,
picking up a card no longer stretches every list to fill the window: a card
drag switches the board's panning off by removing a class, and in lists view
that same class was the only thing holding the lists at their width. Moving a
card to another board failed for everyone with a 403 from Meteor's
insecure-write rule, and the REST route offered as a workaround left the card
pointing at a list on a board it was not on; both are fixed. Below that: an npm
dependency refresh, Tests -> EVERYTHING in build.sh and build.bat
growing the one check it never ran and one browser log per browser on Windows,
companion repositories moving into .tools/ with the build scripts cloning
them on demand, an LDAP group base for directories that keep users and
groups apart, a REST answer for when a list last changed, guards pinning
what a board export contains, and the usual documentation and translation
work.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release updates the following dependencies:
-beta351.0 builds move to their finals with it: ecmascript
0.19.0, rspack 1.2.0, mongo 2.5.0 and accounts-password 3.3.1.
Update.
Thanks to Meteor developers and xet7.node-polyfill-webpack-plugin and the browser shims it carried
(crypto-browserify, browserify-sign, elliptic, node-stdlib-browser
and their dependencies). body-parser moves 1.20.5 → 1.20.6 in the same
install.
Update.
Thanks to developers of dependencies and xet7.$.isArray, $.isFunction, $.isNumeric, $.type, $.trim,
$.proxy, $.now and $.parseJSON - gone from
node_modules/jquery/dist/jquery.js, and none of them called anywhere in
WeKan's own client code - and it no longer tolerates being loaded where there
is no document, which is the boot crash fixed below.
Update.
Thanks to dependabot, jQuery developers and xet7.package.json so the build resolves a single version of it.
Update.
Thanks to dependabot and xet7.dompurify 3.4.12 → 3.4.13, markdown-it
14.2.0 → 14.3.0, markdown-it-emoji 3.0.0 → 3.1.0, temml 0.13.3 → 0.13.4,
@rsdoctor/rspack-plugin 1.5.11 → 1.6.1, puppeteer 25.3.0 → 25.5.0 and both
halves of typescript-eslint 8.65.0 → 8.66.0. Only the lockfile changes: the
version ranges in package.json stay as they are.
Update.
Thanks to developers of dependencies and xet7.and fixes the following bugs:
Signing in with LDAP - where WeKan looks for the groups.
<details> <summary><a href="https://github.com/wekan/wekan/commit/8b2e0038df5513c6cd9b409e063f1c9e3183514c">Groups may live in their own subtree, not only under the user base</a>. Thanks to leandro-cyberproject and xet7.</summary>#5539: WeKan could not
authenticate LDAP users whose groups sit in a different ou than the users.
Both group searches - getUserGroups, which feeds the login restriction, admin
status sync, group-to-role sync and org/team sync, and isUserInGroup -
searched BaseDN, which is the USER base. A directory that keeps ou=groups
beside ou=people has no groups under it, so every group search came back
empty; with LDAP_GROUP_FILTER_ENABLE on, isUserInGroup concluded "not a
member" and refused the login. Nothing in the package could say where the groups
were.
LDAP_GROUP_BASEDN says where, and falls back to BaseDN when unset or blank,
so a directory with one subtree behaves exactly as before - a present-but-empty
variable is one somebody meant to fill in, and searching "" would silently
search the directory root. The three USER searches keep BaseDN: pointing those
at a group subtree would break login for everyone, so the guard pins which
searches moved and which did not. Documented in docs/Login/LDAP.md and
docker-compose.yml, because a setting nobody can find is one that does not
exist.
The REST API - what a list can be asked about.
<details> <summary><a href="https://github.com/wekan/wekan/commit/50083104c74f4b6ea2c55431e1c0cece2074442c">A list says when it changed, and when its cards last did</a>. Thanks to mimZD and xet7.</summary>#5251 asked for a list's last
change date, to build an offline client that syncs only what moved. Neither half
existed: the list endpoint returned {_id, title} and no dates, and while the
single-list endpoint returns the whole document, its modifiedAt answers the
wrong question - it moves when the LIST changes (title, sort, archived), and a
card being added, edited or archived does not touch it.
GET /api/boards/:boardId/lists reports both now: modifiedAt for the list
itself, and cardsModifiedAt for the newest change among its cards, null when
there is none. One query for the board and a reduction in memory, not a query
per list - which is what made this expensive enough to ask about - and archived
cards count, since archiving is one of the changes named. The reduction is a
pure helper: it reads the legacy dateLastActivity as well as modifiedAt and
takes whichever is newer, skips a card with no usable date rather than counting
it as now, and leaves a list with no dated cards ABSENT so the endpoint reports
null instead of an invented time - a client polling on a wrong date either never
syncs or syncs forever.
Controls and the things they belong to - five reports, five causes.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c687daadf4d357e5653c7807320e3161302a798e">Two misplaced checkboxes, a crowded Save button, and the WIP counter back on the list title line</a>. Thanks to Alishara and xet7.</summary>The screenshots in #6465, each with the fault circled.
"Checkbox is misplaced" - the Admin Panel people table. The select-all box and
the "all users" it labels were two loose inline nodes in a centred table
heading: the box is replaced content with its own baseline, and at that column's
width it landed on the word. One <label for> now, an inline-flex row with a
gap that cannot collapse - which also makes the word click the box.
"This checkbox is crazy" - Member settings. That row carried a b
spacer and .left, which is float: inline-start. A float inside a flex
container is ignored and the spacer became a stray flex item, so the box drifted
up beside the "Card settings" heading instead of sitting with its own text. It
is the same shape every other checkbox row in that file uses now.
"(2/5) move this up here" and "make this same height as the lanes left and
right" - one cause, not two. The WIP counter is already inside the heading
right after the title; the title is a +viewer, and .viewer is display: block with a 22px min-height, so it pushed everything after it onto a second
line AND reserved a band under it, which is what made that list's header taller
than its neighbours'. Inline, the counter stays where the markup already puts
it.
"Please move the button down. This is too close" - the backup schedule's Save sat directly under the 1-28 day buttons, close enough to hit while aiming for a date. The gap is on the group that FOLLOWS the day grid, so it applies where that grid is and nowhere else. And "the alignment is out of place" - the attachment move button - is normalisation rather than a measured fix: the row aligns at the bottom, so a margin of the button's own offsets it from the controls it acts on; that is zeroed and the alignment made explicit. Whether that is the whole of what was circled needs a browser, and the guard says so.
</details>Outgoing webhooks - who a webhook says did something.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5b83e8e256ca1fc77db4bd42e530d1249e63ffb2">A webhook sends the username as well as the display name</a>. Thanks to xet7.</summary>#3113: an outgoing webhook said
who did something by DISPLAY name, and had no field for the login name.
params.user comes from getActivityUserName(), which prefers getName()
because the same params feed the e-mail notification text - "Lauri Ojansivu
commented on ..." is what a person wants to read there. A webhook consumer needs
the identifier instead: it received the full name where it needed xet7, and
matching users by display name is wrong the moment two people share one.
Changing what user means would break every consumer already reading it, which
is why this sat in TODO Later as needing a decision. It needs none: the username
travels as its own field. user is untouched, username is beside it in the
default WEBHOOKS_ATTRIBUTES list, and a deployment that pinned its own list
still gets exactly the fields it asked for.
Two more left the backlog with it. #6542 - "Users imported: 60, but only 25 are listed" - is already true in this source: the People pane paginates and shows its total beside the rows, which is what the report was about. #6500 was closed upstream and was only sitting there. The rest of the backlog stays, each with its reason - an SMTP server, an LDAP directory, a container, a browser to drag in, or a decision on an intended contract, none of which a source reading settles.
</details>Logging in with OIDC - what happens when the provider says no.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a50b483ff92786886acf2faf99e7072e6b2c9607">A provider that refuses the request says so, instead of "Cannot read property 'ocs' of null"</a>. Thanks to Esther125 and xet7.</summary>#5174: an OIDC login against a
provider that rejected the request failed with Error in OAuth Server: Cannot read property 'ocs' of null, which says nothing about the refusal that caused
it - the reporter's actual problem was a scope the provider did not allow.
getTokenContent() returns NULL for a token it cannot parse, and the ADFS/B2C
branch assigns that straight into userinfo. The next line was the Nextcloud
hack, if (userinfo.ocs), so the first thing to touch the failed response was a
property read on null. Every claim read after it had the same problem; that line
was first only by accident of ordering. So the fix is not one null check but an
order: the provider's answer is validated once, as a whole, before anything
reads a field off it, and each failure names what failed.
Checked now: that the token response is an object; that it carries an
access_token or an id_token, with a 200 carrying neither reported by listing
the fields that DID come back - keys only, because the values are secrets; that
userinfo is a non-null object, saying which path produced nothing and pointing
at OAUTH2_REQUEST_PERMISSIONS; that the ocs and metadata hacks unwrap to
something, since ocs without ocs.data used to set userinfo to undefined
and fail one line later; that Azure AD B2C's emails claim is an array before
it is indexed; and that expires_in parses to a finite number, since
parseInt(undefined) is NaN and NaN propagated into the account's expiry
silently. The guard pins the ORDER, because an edit that reads a claim earlier
would restore the bug without touching a check.
The size of things on a board - what is bigger than what.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a95451c67b65eb459d0ad94d3ea7ccec7e4319ed">A list heading is bigger than the cards under it again, and the Add button is a control</a>. Thanks to xet7.</summary>Reported by email with a screenshot: the Add card link and the Add button are too big, and things should be sized in relation to each other the way an h1 is bigger than an h2.
The measurements agreed. A list heading was 14px, while a minicard title and the Add card link set no size at all and inherited the document's 16px, and the composer's Add button was a 50px-tall slab. The column was labelled in fine print, the affordance for making a card was as loud as the cards themselves, and the biggest thing on screen while typing was a button.
The scale is heading 16 > card title 14 >= add-card link 13 now, each stating
its own size instead of inheriting, and the Add button is a 34px control. Mobile
keeps its own larger sizes - the .mobile-view rules and the @media blocks -
because a phone is read at arm's length. The guard pins the ORDER rather than
the numbers, since that is what "in relation to each other" means, and that each
of the three states a size of its own: an inherited size is exactly how the link
and the card title both landed on 16px, level with each other and above their
heading.
Moving a card to another board - the card dialog, and the REST route.
<details> <summary><a href="https://github.com/wekan/wekan/commit/24927c3a7a2d114dc3813068bbe2380082ba8458">A cross-board move no longer fails with "Untrusted code may only updateAsync documents by ID"</a>. Thanks to jullbo and xet7.</summary>#6572: every cross-board move
failed immediately with Not permitted. Untrusted code may only updateAsync documents by ID. [403] - whether or not the card had any dependencies, and
before the move itself ever ran, so the card stayed where it was.
models/cards.js is isomorphic, so its helpers run in the client bundle, and
client code calls card.move() directly. Meteor lets untrusted code update only
BY ID - a bare id, or an { _id: ... } selector. The cross-board branch of
move() cleaned up inbound Red Strings
(#3392) with a compound selector
and multi: true, which the client rejects every time, including when it would
have matched nothing. addDependency, setDependencyProps and
removeDependency in the same file each carry a comment saying updates must be
by _id only - this was the one place that did not follow it.
The card's own dependencies are still cleared by the move; that rides along in
the by-id update. The inbound half - the links pointing AT the card from the
board it left - is a Cards.after.update hook in server/models/cards.js now,
where a selector is allowed, and being server-side it also covers the REST API
and import paths, which never called the helper at all. It pulls both stored
shapes: the { cardId, ... } objects, and the bare id strings older data still
holds, which normalizeDependencies hides on read.
The report also says the REST workaround corrupts the card, and it does. A PUT
of boardId/listId/swimlaneId naming the DESTINATION board is not a board
move - that needs newBoardId, newSwimlaneId and newListId - so the
board-move branch never ran, while the same-board swimlane and list branches
did: the card kept its old boardId and got the other board's listId and
swimlaneId written onto it, pointing at a list and a swimlane on a board it
was not on. It showed on neither board and took a hand-written database update
to undo. Both branches now check that the target belongs to the board in the URL
and otherwise refuse with a 400 naming the parameters to use, and they are
skipped during a board move - they would rewrite listId before the board-move
update, whose selector pins the card's original listId, so that update would
match nothing and silently do nothing: the same broken card by another route.
Dragging a card - what the rest of the board does while one is in the air.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c6b0af16ebae40005f19f28a8bf4dac2c00b0081">Picking up a card no longer stretches every list to fill the window</a>. Thanks to yulqen and xet7.</summary>#6573: the moment a card was picked up, every list on the board expanded horizontally to fill the page, and dropping it snapped them all back - which makes landing a card in the intended list a guess. It happened when dragging inside one list too.
Two changes that are each correct alone met. A list's custom width is an inline
--list-width custom property, and what turns that property into an actual
width is a rule in list.css whose selector needs an ancestor: .js-swimlane,
.dragscroll, or a swimlane-<id> id. In lists view the container is
.swimlane.list-group.js-lists.dragscroll - it has no js-swimlane class and
no such id - so .dragscroll was the only one of the three that matched, and
every list's width on that view hung on that one class. Then
#6558 taught a card drag to stop
the board panning under the same pointer, and the way it does that is to REMOVE
the dragscroll class from the board for the duration of the drag and put it
back on drop.
So the drag deleted the class the width rule was matching on: flex: none and
the three width declarations switched off, the lists fell back to the flex rules
and re-flowed to fill the window, for exactly as long as the drag lasted. It
explains the workaround in the report too - dragging a list's edge first sets
that list's width through the resize path, which is why that one column stopped
jumping while the others still did.
A layout rule may not hang on a class that an interaction removes. .js-lists
is on the container in BOTH views and nothing takes it off, so the width rules,
their mobile-mode counterpart and the resize rules now name it.
tests/listWidthDuringDrag.test.cjs pins that every width rule still matches in
lists view with dragscroll gone - it fails on the previous CSS, and it also
pins the premise, that suspending the pan really does remove that class.
The server bundle - what a client-side import may drag into it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/6cada892d9bff5775995e90e70df027f5bc4e1f3">An unused jQuery import in the CSV importer crashed the server at boot under jQuery 4</a>. Thanks to xet7.</summary>jQuery 4 changed what loading it outside a browser does. Its CommonJS entry
point runs the factory as soon as the module is required - module.exports = factory( global, true ) - and the factory's first statement throws when
window.document is missing. Under jQuery 3 the same import did not stop the
server: it had been sitting in models/csvCreator.js since the CSV importer was
written and WeKan started with it in place every time. Line 3 was import { isEmptyObject } from 'jquery', and the file never called isEmptyObject
anywhere - the import was unused from the day it was added.
server/imports.js loads /models/csvCreator, because the server is what
imports a CSV or TSV board, so that unused import put jQuery in the server
bundle - where there is no window and no document. The bump to jQuery 4 turned
it into a boot crash: the bundle threw while it was still being evaluated,
before a single route, publication or method existed, so there was no error page
and no log entry, only a stack trace ending in Object../models/csvCreator.js
and Module../server/imports.js, and Exited with code: 1.
The import is removed, and nothing else changes, because nothing used it. It was
the only jQuery import anywhere under models/, server/, imports/ and
config/, and the rebuilt _build/main-dev/server-rspack.cjs now contains the
csvCreator module with no reference to node_modules/jquery left in it.
The new tests/serverBundleBrowserImports.test.cjs keeps it that way. It walks
the import graph from server/main.js - 410 files - and fails if any file it
reaches names a package that needs a DOM: jquery, jquery-ui, the touch-punch and
dragscroll add-ons, blaze, bootstrap. Its negative tests pin that the bug as it
actually was is reported, that a deep path such as jquery/dist/jquery.js
counts as the same package, and that an ordinary server package is not flagged.
An unused import is invisible in review and free on the client, so a guard is
what catches the next one.
and has the following developer-tooling fixes:
What the snap is built from - the parts in snapcraft.yaml.
The caddy part resolved the newest Caddy release through
api.github.com/repos/caddyserver/caddy/releases/latest. That API rate-limits
unauthenticated callers by IP address, and a CI runner shares its address with
every other job on the same host, so it answers 403 whenever the neighbours have
been busy. In v10.71 it did, and one line failed the whole wekan-gantt-gpl amd64
snap: curl: (22) The requested URL returned error: 403, then 'override-build' in part 'caddy' failed with code 22.
There was already a pinned fallback for exactly this, on the very next line, and
it never ran. snapcraft executes a scriptlet under set -o pipefail as well as
set -e, so the 403 failed the assignment and ended the part one line above
its own safety net - which is why the job log shows CADDY_VERSION= being set
to nothing and then nothing more. A fallback that the failure it covers skips
over is not a fallback.
The version now comes from github.com/caddyserver/caddy/releases/latest, which
is a redirect to the newest tag rather than an API call and is not rate-limited
the same way; the tag is read out of the URL it lands on. The lookup is allowed
to fail (|| true keeps the substitution's status 0), an empty answer selects
the pin, and a release that publishes no archive for this architecture falls
back to the pin as well instead of failing the snap. Setting CADDY_VERSION in
the environment still overrides everything, for a reproducible build. Verified
by running the scriptlet: it resolves 2.11.4 from the redirect, falls back to
the pin with the lookup pointed at an unreachable host, honours an explicit
CADDY_VERSION - and the old line, under the same shell options, dies before
its fallback exactly as it did in the release.
The armhf snap never got as far as building anything: Stage package not found in part 'mongodb': libssl3. and, on the next attempts, the same for
libgoogle-perftools4. Ubuntu 24.04's 64-bit time_t transition renamed both
packages to libssl3t64 and libgoogle-perftools4t64. On the 64-bit
architectures the renamed package also provides the old name, so the old
spelling resolves there and the mistake stays invisible; on armhf the ABI
genuinely changed, there is no compatibility provide, and the old name does not
exist at all. One architecture failing on a name every other architecture
accepts is what that looks like from the outside.
Both are now spelled the way the archive spells them. Checked against the noble
archive rather than assumed: libssl3t64 is published for amd64, arm64, armhf,
i386, ppc64el, riscv64 and s390x, and libgoogle-perftools4t64 for every one of
those except i386 - which builds no snap, because core24 has no i386 port.
The s390x, ppc64el and riscv64 snaps died in the stage step, right after
Staging mongodb: /build/.../stage/bin: Is a directory, IsADirectoryError.
The part is staged last of the ones that carry a bin/, so stage/bin is
already a real directory by then, and staging something that is not a
directory on top of it fails the whole snap rather than that one part.
This was fixed once, in v10.70, by replacing a bin symlink with a real
directory - the shape the first failure had. The v10.71 logs show that guard
running, its [ -L ... ] test coming out false, and the build dying in the
identical way immediately afterwards. So bin was something else that is not a
directory, the symlink was only one shape of the problem, and a guard written to
one shape passes while the build breaks.
The condition is now the invariant rather than the diagnosis: when this part
carries no mongod - the FerretDB-only architectures, where MongoDB ships no
server and the build exits early - bin becomes an empty real directory,
whatever it was, since rm -rf takes a symlink, a regular file or a directory,
where the old rm -f took neither of the last two, and removing a symlink
leaves what it pointed at alone. An empty real directory merges into stage/bin
and changes nothing. Where mongod really is there, amd64 and arm64, nothing is
touched.
It also prints ls -ld of bin before and after, because the reason this
needed two attempts is that no log ever recorded what the thing actually was.
Verified by running the scriptlet against each shape - symlink, regular file,
missing, empty directory, and a directory holding mongod - and checking what
it leaves behind, including that the symlink case does not delete the directory,
it points at.
The test suite itself - guards that described the world before a change.
<details> <summary><a href="https://github.com/wekan/wekan/commit/55480290884fc3891083b83fbb6b1de4113a66be">Five guards catch up with the companion-repo move and the cross-board card fix</a>. Thanks to xet7.</summary>A whole-run found five failures, all of them consequences of the two changes before it, and every one a guard doing its job.
Three broke on the companion-repo move. Two of them - the .meteorignore scan
scope and the swc helper guard - listed the foreign checkouts BY NAME, one
ignore entry each: FerretDB, node, mongo-tools, TSC, gitea, the two WeKan
variants. A list of names is a list of history; it fails for the ones that moved
and says nothing about the next repo somebody clones. They ask the property now
.tools is excluded, and nothing at the top of the checkout that is its own
git repository is left for Meteor to walk - and the variant check became the
stronger version of itself: no directory holding client/, server/ and
models/ is reachable from the top, since a second WeKan is loaded EAGERLY
rather than merely scanned. The third reads FerretDB's Go source and still
opened the old path; it reads .tools/FerretDB, and skips with a note when the
clone is not there, because that is another repository and not every checkout
has it.The compose guard caught a real omission: the LDAP_GROUP_BASEDN block went
into docker-compose.yml only, and every compose file's wekan service must be
identical - what a user reads while editing their settings must not depend on
which backend they picked.
The mocha failure is the one worth reading twice. Its test pinned move()
issuing the inbound dependency cleanup itself: a multi-document update with a
compound selector - exactly what the cross-board move fix removed, because that
helper runs in the client bundle where Meteor allows updates only by id. The
test was pinning the bug. It asserts the contract that replaced it now:
move() clears the card's own dependencies and makes no update that is not by
id, with { _id: x } still counting as by id - the rule is "by id", not "not an
object".
Two left over, and the second is not a flake.
The swc guard also required the .gitignore comment naming each clone - - node/ : clone of the Node.js ... - which went with the entries it described.
.gitignore says what .tools/ holds now, naming the entries it replaced so
the change is legible, and the guard requires that: an ignore of a whole
directory with no explanation is the kind nobody dares remove later.
One WebKit test of 243 failed, in the shared boardPage fixture rather than in
an assertion: Test timeout of 60000ms exceeded while setting up "boardPage",
then Target page, context or browser has been closed. openBoard retries five
times, waiting up to 20s for a list each time with a second between - about 105
seconds, against a 60 second test timeout. The loop could never reach its own
error: Playwright killed the test first, so the report said the page had closed
instead of saying the board never rendered. Retrying past the point where the
result can still be used is not resilience, it is a worse error message.
It is bounded by a deadline now - 45s, leaving room for the rest of the fixture inside the 60s timeout. The first attempt keeps its full 20s look, later attempts get whatever is left, and it stops rather than starting a wait it cannot finish, so a slow board still gets one long look and a board that will not render fails with "did not render any lists within 45s". Simulated across never-renders, renders-at-4s and renders-at-19s: all inside the test timeout, where the old shape overran it by 45 seconds. The run after it was green - 328 node suites, 518 mocha tests, 249/243/243 in the three browsers, 98 conformance cases with none differing, and FerretDB's own suites.
</details>The build scripts - what Tests -> 1 runs, on both platforms.
wekan/FerretDB is a separate git repository that WeKan's test runs need - the
conformance run builds it from source, and "Run all FerretDB tests" runs its own
suites. It was cloned as a subdirectory of the repo root, which is why
.gitignore and .meteorignore carried an entry per companion repo: nine of
them, each one a chance for a clone to reach a commit or a Meteor rebuild. They
live in .tools/ now, one directory both files already exclude.
build.sh gains ensure_tool_repo: it returns the path to .tools/<name>,
cloning wekan/<name> if it is not there - SSH first, since a maintainer
pushes, HTTPS after, so everyone else still gets a working clone. Its messages
go to stderr, because the path is what it prints, and the directory comes from
the script's own location rather than the caller's cwd. EVERYTHING's FerretDB
stage calls it rather than assuming an earlier stage cloned already - a
whole-run must not depend on the order of its own stages - and build.bat
clones into .tools\FerretDB with the same fallback instead of printing
instructions and stopping.
The other half is that a repo inside .tools still has to find its way back
out. FerretDB's own build.sh writes its logs where WeKan writes its own, and
reached them with $ROOT/../../log - correct from wekan/FerretDB, one level
short from wekan/.tools/FerretDB, where it means wekan/log and nothing else
looks. It walks up until it recognises a WeKan checkout now, then applies
WeKan's own rule: ../log when that is writable, log/ inside the checkout
otherwise. Verified against five layouts, including the old one.
./build.sh -> Tests -> EVERYTHING (sequential) is what a maintainer runs
before a release, so a check that is in the Tests menu but not in EVERYTHING is
a check that runs only when somebody remembers it. The floating-promises
guard was exactly that: it is menu entry 12 and was in no whole-run. It is now
the first stage of four - it takes seconds, so an unawaited permission check is
reported before an hour of browsers rather than after it - and its return code
is part of the verdict like every other stage's.
What it runs there is checks only. The menu entry may install ripgrep and the
@typescript-eslint packages and write the rule into .eslintrc.json, which is
a person setting the guard up; EVERYTHING runs unattended, must not call sudo,
and must not modify the tree it is testing - a run that rewrites
.eslintrc.json is no longer testing the commit it started from. So the two
checks themselves - the rule is configured, and every
Authentication.checkBoardAccess / checkBoardWriteAccess in server/models
is awaited - are one function that both callers share, and it uses grep rather
than ripgrep so it also works where nothing may be installed to make it work. An
unawaited permission check returns a pending promise, and a promise is truthy,
so the call site passes a check that never ran.
On the Windows side, build.bat ran the three browsers as a single Playwright
invocation writing one wekan-alltests-browsers.log, where "which browser
failed" and "what did WebKit print" could not be answered afterwards - and
CLAUDE.md's "check the newest test logs" names the per-browser files. It now
starts chromium, firefox and webkit as three jobs with a log, a status and a
summary row each, as build.sh has always done, with a per-browser --output
because Playwright clears its output directory at startup and three jobs sharing
one would delete each other's traces. EVERYTHING itself is not reimplemented
there: Windows hands the whole run to releases/run-everything.sh, which calls
build.sh --run-everything, so there is one implementation and the new stage
arrives on both platforms at once. The parity guard now pins all of it,
including that the shared checks install nothing.
Renaming the menu entry above broke it, between one commit and the next.
choose() shows the SHORT labels and sets $opt to the FULL DESCRIPTION of the
one picked, and the dispatcher hundreds of lines below matches that description
as a case arm - so the description is an identifier written twice, and the
rename changed only one of the two. With no arm matching and no catch-all, the
case fell through, the for _once loop ended and the script EXITED: choosing
Tests -> 1 printed nothing and ran nothing at all.
The arm carries the same string as the menu entry again. Two guards so it cannot
come back: the parity test now checks every one of the 25 choose() entries
against the case arms and fails on a description that matches none - it fails on
the previous commit - and the dispatcher has a *) arm that names the option
with no handler and says nothing was run, so if one ever does get past the test,
the person at the menu is told rather than dropped back to the shell.
build.bat was checked for the same fault and cannot have it: its menus
dispatch on the NUMBER typed rather than on a sentence, and its EVERYTHING hands
the run to releases/run-everything.sh instead of reimplementing it. Verified
in both directions anyway - every printed menu number has an if-dispatch in all
seven menus, and every goto / call target resolves to a label - and that is
pinned now too.
The EVERYTHING run above found this on its first green pass: the WeKan stage
failed on one node suite, dbConformanceWiring, with "build.bat: playwright-all
must be logged". Splitting the whole-suite browser job into three uncovered a
real gap rather than causing one.
call :onelog playwright-all existed in exactly one place - inside the combined
three-project browser line of the sequential all-tests flow, which is gone now
that each browser is its own job with its own log. Where it did exist it never
worked: the call sat inside the cmd /c string of a STARTED child process,
which has no build.bat labels to call, so %ONELOG% was empty and
Tee-Object was handed an empty path.
Meanwhile the option that name was supposed to cover - Tests -> 11,
"Playwright ALL browsers" - wrote no log at all: one playwright test call with
three --project flags straight to the terminal, nothing left to read
afterwards. That is exactly what the guard is about, and build.sh's same
option has always written one log per browser. It now runs the three browsers
one at a time - still sequential, because three at once against one dev server
exhausts RAM on smaller machines - each through the same :onelog helper as
every other Tests option, and each with its own --output so Playwright does
not clear another browser's traces at startup. The guard drops playwright-all,
which named an implementation that is gone and was broken, and gains what it was
reaching for: that the ALL-browsers option logs, and covers all three.
Board export - what a backup contains, pinned against the source.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ee16a41a9963d2c579f52c62f9fccc14849c7aba">Every section of a board export is pinned, so a backup cannot quietly lose one</a>. Thanks to Tuphal, KhaoulaMaleh and xet7.</summary>#6274 was "export includes only
comments from current year": the exporters selected comments and activities by
boardId, and records written by older WeKan versions carry no boardId, so
every comment from a previous year was dropped from the JSON and the Excel
export alike. #6275 fixed it by
selecting on the cards' ids, which every comment has. Its reporter then came
back with a second one: "the export is now missing the lists part".
Both were checked against the current source rather than assumed. The fix from
PR #6275 is intact and survived the streaming-export rewrite that came after it:
models/exporter.js and models/server/ExporterExcel.js each select comments
by cardId and activities by boardId OR cardId, in all three of the
streaming JSON writer, the in-memory one and the spreadsheet. The missing
lists is not reproducible: result.lists has been written by the JSON
exporter continuously since the CSV/TSV export was added, both JSON writers emit
the same fourteen sections, and the flat formats carry each list's name on the
card's own row.
So nothing needed fixing, and nothing about the export changed - what was
missing was the guard. tests/exportBoardSections.test.cjs pins the section
list, that the two JSON writers agree on it (a key only one of them writes is a
section that appears or not depending on which path served the download), that
no exporter selects comments by board again, and the two smaller fixes from that
PR: the class has no this.boardId, which was the undefined field that ran a
query against no board, and a comment whose author has been deleted falls back
instead of writing undefined into the cell.
One thing that PR also fixed is deliberately gone: the Excel exporter used to LOAD custom fields and render them nowhere, and the streaming rewrite dropped that dead load along with the activities, checklists, subtasks and rules the spreadsheet also never showed. Custom fields survive where an import reads them back - the JSON export - and the guard pins them there.
</details>The mocha test stage - what a suite on the client side may import.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ee16a41a9963d2c579f52c62f9fccc14849c7aba">A test that reads the repository moves to the side that has a filesystem</a>. Thanks to xet7.</summary>client/lib/tests/boardTriggersClass.tests.js is the regression guard for
#5188, the rule trigger that could
not be activated because a class name in the jade template and the one in the
event handler had drifted apart by a letter. It reads both files off disk with
fs and path and compares them - which is a server-side thing to do, and it
ran on the client, where it worked only because node-polyfill-webpack-plugin's
browser shims were in the bundle.
The @meteorjs/rspack 2.1.0 update above drops that plugin, so the shims went
with it and the meteor test client build stopped on one line: Cannot find module 'path' for matched aliased key 'path'. That is not one suite failing -
the client bundle does not build, so every mocha suite is skipped and the stage
reports a crash where its results should be. One file's import took out the
whole stage.
The suite was never wrong, only on the wrong side; it now lives in
server/lib/tests, registered in that index. A new guard,
tests/clientTestsNoNodeBuiltins.test.cjs, walks the client suites for imports
of Node builtins - the mirror of the existing guard that keeps browser-only
packages out of the server bundle - and pins where this one went. It strips
comments before looking, or the sentence explaining the fix would be reported as
the fault.
Running the exotic builds on Launchpad - and reading them afterwards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/3c7926662c5e14dde16465d6a5380746cf298708">A Launchpad build keeps its whole log, and the job outlives its own retries</a>. Thanks to xet7.</summary>Two things about the job made the failures above harder to fix than they should have been.
The build log is downloaded by remote-build and printed as its last 150 lines.
For v10.71's staging failure those 150 lines were the IsADirectoryError and
then lpbuildd's own Python traceback, the proxy-token revocation and the process
scan - everything except the mongodb part's output from an hour earlier, which
is the part that would have said what bin was. The Launchpad build log is
deleted along with the temporary snap recipe, so once the job ended, no copy of
it existed anywhere. The job now prints the lines around every failure marker
as well as the tail, and uploads the complete Launchpad and snapcraft logs as a
snap-launchpad-logs-<arch> artifact - on always(), not failure(), because
a build that succeeds on attempt 2 otherwise hides why attempt 1 did not.
The other is the job timeout, which was 180 minutes for a step that retries
three times. The riscv64 leg spent 2h24m on attempt 1 alone, almost all of it
queueing for a riscv64 builder, failed it on the bin bug above, and was cut
off 35 minutes into attempt 2 - the The operation was canceled. in that job is
this timeout and not Launchpad at all. A retry loop the job does not outlive is
not a retry loop; 350 minutes fits two slow attempts and stays under GitHub's
360-minute per-job ceiling.
The exotic snap architectures (ppc64el, s390x, riscv64, armhf) have no native
runner and cannot be cross-built, so they are built with snapcraft remote-build on Launchpad. When such a build ends as Stopped with no .snap
npm install or a transient build-farm reset - the job scans the downloaded build
log to tell that apart from a real credential failure. The scan pattern was
unauthoriz|401|403, and a bare 401/403 matches any three digits anywhere
in a large build log - a package size, a hash, an attempt 4/6 line - so it
fired on almost every Stopped build and told the maintainer to re-create
LP_CREDENTIALS that were working: the build had reached Building:, which
already needs valid credentials. The pattern now matches those codes only in an
HTTP-error context (HTTP Error 401, 403 Forbidden) or an explicit phrase
(invalid credentials, not logged in, bad credentials), so the credential
hint appears only for a genuine authorization failure. The Launchpad Stopped
builds themselves are a Launchpad-side limit on slow emulated architectures, not
a WeKan bug; the job already retries three times and is continue-on-error, so
it never fails the release.and updates the documentation and translations:
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the bundled MongoDB Database Tools - bsondump, mongodump, mongoexport, mongofiles, mongoimport, mongorestore, mongostat and mongotop - now come from wekan/mongo-tools-patches instead of the wekan/mongo-tools fork, in every place that downloads them: the amd64 base bundle, the per-arch overwrite in each Linux bundle, the Windows and macOS bundles, the preflight and download checks, the Dockerfile, both snapcraft files, the Flatpak and AppImage workflows and the docs. The fork changed none of upstream's Go source and existed only to carry a build; the new repository keeps that build, clones the newest upstream release and applies patches to it, exactly as wekan/node-patches replaced the wekan/node fork. Nothing about the bundles changes - the same asset names, the same per-tool tolerance for an architecture with no binary, the same checksum verification - and the new checkout beside the repository is excluded from git and from Meteor's file scan like its siblings. Below that: the release scripts now run on the bash 3.2 that macOS ships, so a release can be triggered from a Mac.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
This release changes where the bundled MongoDB Database Tools come from:
<details> <summary><a href="https://github.com/wekan/wekan/commit/354d356a0">The MongoDB Database Tools come from wekan/mongo-tools-patches now</a>. Thanks to xet7.</summary>wekan/mongo-tools was a fork of a large Go project that changed none of its
source: its six commits were the build workflow and its changelog, and the 738
directories of upstream Go beside them were kept in a fork only so that workflow
had somewhere to live. wekan/mongo-tools-patches keeps the build and drops the
fork - it clones the newest upstream mongodb/mongo-tools release, applies the
patches in its dist/ (there are none yet, which is the honest state of a fork
that changed nothing), cross-compiles the eight tools for sixteen platforms with
CGO disabled and publishes the same <tool>-<arch>[.exe] assets with a
.sha256sum beside each. It is the same move wekan/node-patches made for the
retired wekan/node fork.
So every download URL here moved with it: the amd64 base bundle and the per-arch
overwrite in the Linux bundle jobs and in releases/repack-bundle-for-arch.sh,
the Windows and macOS bundle jobs, releases/check-arch-binaries.sh,
releases/require-binaries.sh, releases/test-download-urls.sh, the
Dockerfile, snapcraft.yaml and snapcraft-core26.yaml, the Flatpak and
AppImage workflows, the Sandstorm build-deps note and the CPU-platforms
documentation.
Nothing else changes. The asset names are the same, the per-tool tolerance for
an architecture the tools were not built for is the same, and the checksum
verification is the same, because the build script moved across unchanged. What
does change is that the next release needs wekan/mongo-tools-patches to have
published a release first: its Release All has not run yet, and the amd64
bundle's download of the eight tools is not tolerant of a missing release.
and has the following developer-tooling changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/354d356a0">The mongo-tools-patches checkout is excluded from git and from Meteor's scan</a>. Thanks to xet7.</summary>mongo-tools-patches/ is a separate git repository worked on beside WeKan, like
node-patches/, FerretDB/ and the mongo-tools/ clone it replaces. It is in
.gitignore so it is not untracked noise in git status, and in
.meteorignore so Meteor does not walk it during a build - nothing in WeKan
imports it. The guard that checks every git repository cloned in here is
excluded from BOTH files covers it now.
releases/release-all.sh and releases/version.sh built arrays with
mapfile, a bash 4+ builtin absent from the bash 3.2 that macOS still ships,
so ./releases/release-all.sh stopped with mapfile: command not found and
could not trigger a release from a Mac - and brew install mapfile finds
nothing, because mapfile is a shell builtin, not a program. Each
mapfile -t VAR < <(cmd) became the portable read loop
VAR=(); while IFS= read -r line; do VAR+=("$line"); done < <(cmd), which
builds the same array on bash 3.2 and on the bash 5 the Ubuntu release runner
uses. Converted: release-all.sh's RELEASED version list, and version.sh's three
reads - the Node.js files, the MongoDB files, and the two newest CHANGELOG
release lines.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: WeKan takes its Node.js from three sources, in order -
official nodejs.org, then unofficial-builds.nodejs.org, then
wekan/node-patches - instead of only from the retired wekan/node fork,
and a platform that none of the three publishes a Node.js for is simply not
built that release instead of failing it. The order lives in one new script
that the bundle .zip jobs, the extra-architecture preflight and the
Dockerfile all ask, so the image and the .zip of one CPU can never be built
on Node.js from different places; the snap takes its node out of the bundle,
so it follows without a change of its own, and Sandstorm (amd64 only) is
untouched. Below that: the release notes now say which source actually served
each platform instead of a hardcoded name, the 32-bit Windows import library
moves with the runtime, and the guards that pinned the old fork-only rule are
updated to the new one.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
i386, armhf, armv7 and win32 have no rows because they are not built this release: nodejs.org publishes no 32-bit x86 or ARMv7 Node 24 and no 32-bit Windows one at all, unofficial-builds has none of them either, and wekan/node-patches has not published its first release yet. Each returns by itself on the first run after a Node.js for it appears - nothing has to be edited for that to happen.
This release changes where the bundled Node.js comes from:
Bundled Node.js - the runtime inside every bundle, image and snap.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b9eff391b97427f4ed31ff07753a83d1f571f511">Node.js comes from official, then unofficial, then patched builds, and one script decides</a>. Thanks to xet7.</summary>WeKan took its Node.js only from the wekan/node fork, for every platform. That
fork is retired in favour of
wekan/node-patches, which carries
patches rather than a whole Node.js source tree - so the question "where does
this platform's Node.js come from" had to be answered again, and the answer is
now three sources tried in order: nodejs.org, then
unofficial-builds.nodejs.org, then wekan/node-patches.
The order is a preference, not a last resort. Where nodejs.org publishes a build, that is the one WeKan ships: it is the build the rest of the world runs and its checksums are the ones everyone else verifies against. unofficial-builds covers the CPUs nodejs.org does not release - riscv64 and loong64 - and node-patches covers what neither publishes at all: 32-bit x86, 32-bit ARM and 32-bit Windows.
releases/resolve-node-source.sh is the one place that order and the
platform-name mapping live. The bundle jobs (through
releases/embed-verified-node.sh), the extra-architecture preflight
(releases/check-arch-binaries.sh) and the Dockerfile all ask it, so the
image and the .zip of one CPU cannot be built on Node.js from different
places - which is exactly what happened when each of them carried its own copy
of the walk. The snap copies its node out of the bundle, so it follows with no
change of its own, and Sandstorm is amd64-only and untouched.
It answers with the exact file, what shape that file is - nodejs.org and
unofficial-builds publish a tarball or a .zip, node-patches a bare binary -
and the SHA256 that source published for it. Because it only returns a build it
found a published checksum for, the "shipped unverified" path that a missing
.sha256sum used to open is gone.
There is nothing a release run can do about a CPU nobody publishes a runtime for, so it no longer tries: the resolver answers "not found", the preflight in each bundle job turns that into a skip, and every step after it is gated on that. A red job every release for such a CPU is noise, and when it was an error it took the whole extra-architecture matrix with it - and, through that, the Docker image and the jobs that need it.
Nothing has to be edited when that changes. The next run resolves again, and the platform builds the moment a Node.js for it is published at any of the three sources. That is what the four missing rows in the table above are: i386, armhf, armv7 and win32 are waiting for wekan/node-patches to publish its first release, and they come back by themselves when it does.
A lookup that could not be MADE is kept apart from an answer of "nobody publishes it": an unreachable nodejs.org is an error, not a reason to skip every platform and call an empty release normal.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9eff391b97427f4ed31ff07753a83d1f571f511">The release notes name the source that actually served, not a hardcoded one</a>. Thanks to xet7.</summary>Every bundle job recorded its Node.js provenance row with the literal source
wekan/node, whatever had actually served, so the provenance table at the top
of the release notes could not answer the one question it exists for. The rows
now carry what the embed step reported - nodejs.org,
unofficial-builds.nodejs.org or wekan/node-patches - together with the exact
URL and the verified SHA256.
The same table is now part of the CHANGELOG too, at the top of each release
section under the summary, so which binaries a release carries can be read
without a build log that expires. CLAUDE.md describes its shape.
A 32-bit Windows native addon must link against an ia32 node.lib, and
nodejs.org stopped publishing one in Node 23, so node-gyp is pointed at a
nodedir built by hand: headers from nodejs.org plus a node-win32.lib. That
library came from the retired fork; it now comes from wekan/node-patches, which
is also where the node-win32.exe in the bundle comes from. The addons and the
runtime they run on are then from the same build.
and has the following developer-facing changes:
<details> <summary><a href="https://github.com/wekan/wekan/commit/b9eff391b97427f4ed31ff07753a83d1f571f511">The guards pin the three-source order, and no longer pin the fork-only rule they replaced</a>. Thanks to xet7.</summary>tests/releaseNodeSources.test.cjs is new and pins what the change above is
worth: that the three sources are tried in that order, that version is the outer
loop and source the inner one - so a current patched build beats an ancient
official one rather than the other way round - that every consumer asks the one
resolver instead of carrying its own walk, that a platform with no Node.js is a
skip rather than a failure, and that no file downloads from the retired fork any
more.
releaseNodeVerified and releaseArchSkipAndBaseAttach pinned the fork-only
rule, which was the correct rule until this release. They are updated to the new
one and say in the test why, so the next reader can see it was a decision. Each
keeps the assertion that made it valuable: a named version, a verified download,
and no path that ships an unverified Node.js.
releaseSnapArches read release: out of ordinary English in a run: block
and took the following word for a snap channel; it now keeps only the matches
that name one. node-patches, checked out beside WeKan, is added to
.meteorignore for the same reason the other sibling repositories are there.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: two fixes to the release build after the fork-everywhere
Node.js switch in v10.68. The native jobs pass the pinned Node major
(24), but the wekan/node fork tags releases by full version (v24.19.0), so
the embed helper built a …/download/24/node-x64 URL that 404s - it now
resolves the major to the newest fork tag that carries the asset. And the
multi-arch Docker image no longer skips when one exotic CPU fails to build:
it now builds for whatever bundles landed, dropping just the missing arch.
This release fixes the following bugs:
The release build - the fork Node.js download and the Docker platform set.
<details> <summary><a href="https://github.com/wekan/wekan/commit/414594ce99ac7dd44de2affaa15d7b135f6c319e">The native embed resolves the fork's full version from the pinned major</a>. Thanks to xet7.</summary>The native jobs call embed-verified-node.sh … node-x64 "$NODE_VERSION", but
$NODE_VERSION is the pinned MAJOR, 24, while the fork tags its releases by
full version, v24.19.0. The helper built
…/releases/download/24/node-x64, which 404s, so the amd64 bundle failed at the
Node embed step. It now resolves a bare major to the newest fork tag
v<major>.x that carries the asset - the same GitHub-API walk
check-arch-binaries.sh uses - and takes a full tag as given. When no release
has the asset it stops with a message naming the fork asset to build.
The docker job needs: build-extra-arches, a matrix. When one leg failed - a
single exotic CPU like ppc64le - the whole matrix job was "failed", and a job
whose needs dependency failed is SKIPPED, so one failed arch skipped the
entire image. It now runs on always() && needs.release.result == 'success',
and a new "Decide which platforms" step probes which
wekan-<version>-<arch>.zip bundles actually landed: amd64 and arm64 are
required, the exotic arches
(ppc64le/s390x/riscv64/386/arm/v7) are included only if their bundle is present,
and a missing one is a warning that drops just that platform. The decided set
drives the wait loop, --platform and the push-verify list from one place, so a
failed or best-effort-skipped arch drops only itself and returns the next
release that builds it - the image is never skipped and never fails on one CPU.
Three native-bundle failures in one run. mac-arm64 failed the fork major->tag
lookup: embed-verified-node.sh and check-arch-binaries.sh call the GitHub
API to find the newest v24.x fork tag, but no step passed a token, so the
call was UNauthenticated (60/hour per shared runner IP) and 403-rate-limited -
amd64 (run first) resolved, mac-arm64 (later) did not. A workflow-level
GITHUB_TOKEN: ${{ github.token }} authenticates every such call. win64 failed
because the fork published node-win64.exe WITHOUT its .sha256sum, and the
helper 404'd on the sidecar; a missing checksum is now a warning (the binary
still ships, over authenticated HTTPS, UNVERIFIED - a checksum that is present
and does not match is still fatal). win32 failed at setup-node
("Unable to find Node version '24' for platform win32 and architecture x86" -
there is no 32-bit Windows Node 24); it now runs the x64 Node to drive node-gyp
and cross-builds the native modules to ia32 with npm_config_arch=ia32.
Each extra arch is built by running the fork's target-CPU node UNDER QEMU to
rebuild the native modules, and qemu-user does not run every binary perfectly:
ppc64le crashed at node startup deserializing its V8 snapshot
(Check failed: IsFreeSpaceOrFiller(filler) in v8::Isolate::Initialize, then
qemu: uncaught target signal 5), failing its leg. With ppc64le and riscv64
still REQUIRED, that failed the whole build-extra-arches matrix. They join
s390x/i386/armhf/armv7/loong64 as best-effort: a leg that cannot run this
release SKIPS with a warning, so the matrix never fails on one exotic CPU, the
docker job is never dragged down, and the release ships whatever built. amd64
and arm64 (native) remain the required core; ppc64le returns the release it runs
cleanly again.
The win32 and mac-x64 preflights checked
…/releases/download/${NODE_VERSION}/… with NODE_VERSION=24 (the bare MAJOR),
which 404s - the fork tags by full version (v24.19.0) - so both jobs ALWAYS
skipped. They now resolve the newest v24.x fork tag that carries the asset
via a shared helper, releases/resolve-fork-node-tag.sh (authenticated with
the workflow token, so the API is not rate-limited). And the win32
native-module rebuild is finished: npm_config_arch=ia32 alone could not link,
because an ia32 addon needs an ia32 node.lib and nodejs.org dropped 32-bit
Windows in Node 23, so node-gyp cannot fetch win-x86/node.lib. The rebuild now
assembles a nodedir node-gyp can use - node headers (arch-independent, from
nodejs.org) plus the fork's own node-win32.lib (now published beside
node-win32.exe) - and points node-gyp at it, so bcrypt cross-builds to ia32
against the fork's Node.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: WeKan now takes its Node.js from the wekan/node fork for every
platform and from nowhere else - the native bundles, the emulated
cross-builds and the multi-arch Docker image alike - so a Node bug can be
patched and rebuilt from source, and one set of artifacts is never half-built on
a different Node.js per CPU. On top of that, every platform the fork builds a
Node.js for now has a .zip bundle: this adds the missing armv7,
win32 and mac-x64 bundles. Below that: a Docker release-verify fix
that was failing a linux/arm/v7 image that had actually built correctly.
This release takes WeKan's Node.js from the wekan/node fork for every platform:
Node.js sourcing - one source, built from source, for every CPU.
<details> <summary><a href="https://github.com/wekan/wekan/commit/ac899ece2d3c2a5445cbb48e77df545b6b9ac808">Every native bundle takes its Node.js from the wekan/node fork, not nodejs.org</a>. Thanks to xet7.</summary>The four native bundles (amd64, arm64, win64, mac-arm64) embedded a verified
Node.js downloaded from nodejs.org. They now download the wekan/node fork's bare
node-<asset> binary (node-x64 / node-arm64 / node-win64.exe /
node-mac-arm64) and verify it against the node-<asset>.sha256sum the fork
publishes beside it. The reason is control: the fork is Node built from source,
so a Node bug can be patched and rebuilt - which cannot be done with
nodejs.org's opaque binaries - and one source means a set of bundles is never
half-built on one Node.js and half on another. A bundle ships only the node
binary and runs node main.js, so no npm is grafted here (the Docker image and
the emulated cross-builds, which run npm, graft it separately).
tests/releaseNodeVerified.test.cjs pins the fork source for all four.
The emulated cross-builds and the multi-arch Docker image used to prefer
nodejs.org, then unofficial-builds, and fall back to the fork only for the CPUs
neither built. That is now reversed to fork-only, for the same
built-from-source reason as the native bundles.
releases/check-arch-binaries.sh no longer walks nodejs.org - it lists the
fork's own releases for this major, newest first, takes the newest that carries
node-<arch>, and verifies it against that release's .sha256sum;
releases/install-node-for-arch.sh always installs the fork's bare binary
(grafting npm - arch-independent JavaScript - from the official amd64 tarball,
a build tool, not the shipped node); the Dockerfile maps every TARGETARCH to
a fork asset. s390x becomes best-effort (optional) like i386/armhf/loong64:
until the fork has published node-s390x the preflight skips it with a warning
instead of failing the whole matrix and taking docker down with it, and it
returns on its own once the fork publishes it.
and adds the following new .zip bundles:
Platform bundles - every CPU the fork builds a Node.js for gets a bundle.
<details> <summary><a href="https://github.com/wekan/wekan/commit/41d4eaf7e8d03f43aa3a1f7d661ca9f373549dc8">A new armv7 .zip bundle, from the fork's node-armv7</a>. Thanks to xet7.</summary>The wekan/node fork builds a generic ARMv7 Node.js (node-armv7) as well as
Debian's hard-float one (node-armhf); each is a real fork platform, so each
gets its own WeKan .zip. A new armv7 leg of the extra-arches matrix runs in
the same linux/arm/v7 emulated container as armhf, takes its Node.js from the
fork's node-armv7, and shares FerretDB's armhf binary (FerretDB has no
distinct armv7). It is best-effort, like the other 32-bit bundles. The result is
wekan-<version>-armv7.zip alongside the armhf one.
Twelve of the fork's thirteen platforms had a bundle; 32-bit Windows and Intel
macOS did not, though the fork builds node-win32.exe and node-mac-x64.
build-win32 mirrors the win64 job but installs a 32-bit (x86) Node via
setup-node so the native modules it rebuilds (bcrypt) are ia32, matching the
fork's 32-bit node.exe, and takes ferretdb-win32.exe. build-mac-x64
mirrors the mac-arm64 job on a macos-13 Intel runner, taking node-mac-x64
and ferretdb-mac-amd64. Both are best-effort: a preflight step skips the job
with a warning if the fork has not published that platform's node yet, so the
bundle appears the run after the fork publishes it.
tests/releaseBundleCoverage.test.cjs pins that all thirteen fork platforms map
to a bundle.
and fixes the following bug:
<details> <summary><a href="https://github.com/wekan/wekan/commit/d71f030527bbc1c225e0d062113f61f4726be4b3">The Docker release verify reads the CPU variant, so linux/arm/v7 is not misread as linux/arm</a>. Thanks to xet7.</summary>The docker job built and pushed the multi-arch image for all seven platforms
correctly; the step after it, which inspects each registry's manifest to prove
every platform arrived, then failed the run over a manifest that was right.
buildx builds linux/arm/v7, but a registry records that as architecture arm
with variant v7 in a separate field, and the verify's imagetools --format
printed only OS/Architecture - so the entry read back as bare linux/arm and
the check for linux/arm/v7 never matched it. The format now appends
/{{.Platform.Variant}} when a variant is present, and normalises arm64's
implied /v8 away so it still matches linux/arm64.
tests/releaseDockerPlatforms.test.cjs pins the variant-aware format.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: the Docker image gains linux/386 and linux/arm/v7 -
the two 32-bit platforms that were shipping as .zip bundles only. The image
base moves from ubuntu:26.04 to debian:trixie (Ubuntu publishes no i386
image, Debian does, and Debian carries every arch the image targets), and the
Dockerfile installs their Node 24 from the wekan/node fork (node-i386 /
node-armhf) - which nodejs.org and unofficial-builds do not build - grafting
npm from the official amd64 tarball. v10.66 had removed arm/v7 as a
stopgap so the build could pass; this brings it back properly, with i386
alongside.
This release adds the following new Docker platforms:
<details> <summary><a href="https://github.com/wekan/wekan/commit/703527a322d2b1557181bdb36bdce5074c601169">Docker images for linux/386 and linux/arm/v7, on a Debian base with Node from the fork</a>. Thanks to xet7.</summary>Two 32-bit Docker platforms that only ever shipped as .zip bundles now build
as images. The blocker was two-fold and is removed on both sides:
The base image moves from ubuntu:26.04 to debian:trixie. Ubuntu dropped
i386 years ago and its image has no linux/386 manifest, so 386 stopped at the
base; Debian still ships i386, and debian:trixie carries every arch this image
targets (amd64, arm64, 386, arm/v7, ppc64le, riscv64, s390x), so one base covers
them all - and it is the same base the per-arch .zip bundles already build in.
The image installs no MongoDB server (every arch defaults to the bundle's
FerretDB), so nothing was Ubuntu-specific; the sha checks moved from shasum to
coreutils sha256sum.
Node.js for 32-bit x86 and ARM exists on neither nodejs.org nor
unofficial-builds, but the wekan/node fork
builds it - as a bare node-i386 / node-armhf binary plus a .sha256sum, not
a tarball. A new fork branch in the Dockerfile's arch case downloads and
verifies that binary and grafts npm/npx from the official amd64 tarball (npm
is arch-independent JavaScript). Debian's 32-bit ARM port is armhf (ARMv7
VFPv3-D16), which is what linux/arm/v7 runs, so node-armhf is the match.
linux/loong64 still ships as a .zip only - no Docker base publishes it and
the registries do not agree on its manifest yet.
tests/releaseDockerPlatforms.test.cjs pins the Debian base, 386/arm/v7 in and
loong64 out, and that every built platform has a Dockerfile arch-case branch.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: with linux/386 gone, the multi-arch Docker image build got
past the base image and then failed inside the Dockerfile on linux/arm/v7
(armv7l) - the Dockerfile installs Node.js from nodejs.org / unofficial-builds,
neither of which ships a Node 24 for armv7l, so its arch case has no arm
branch and the build stopped with "Unsupported architecture: arm". armv7l is
dropped from the image's platform list, joining 386 and loong64: it ships as a
.zip bundle but not as a Docker image.
This release fixes the following release-build issue:
<details> <summary><a href="https://github.com/wekan/wekan/commit/370c091cf91ba014a8fde026cac7e8313cd56b76">The Docker image drops linux/arm/v7 too, which its Dockerfile has no Node 24 to install for</a>. Thanks to xet7.</summary>Removing linux/386 last release let the multi-arch build get past the
ubuntu:26.04 base, and it then failed inside the Dockerfile's RUN step on
linux/arm/v7: "+ echo Unsupported architecture: arm / + exit 1". Docker's
TARGETARCH for linux/arm/v7 is arm, and the Dockerfile's architecture
case handles only amd64/arm64/ppc64le/s390x/riscv64 - it deliberately has no
arm branch, because it installs Node.js from nodejs.org and
unofficial-builds, and NEITHER ships a Node 24 for armv7l. But linux/arm/v7
was still in the docker buildx --platform list, so the RUN reached the
"Unsupported architecture" guard and the whole build failed. linux/arm/v7 is
removed from the build's --platform list and from the want= list that
verifies the pushed manifest, joining linux/386 and linux/loong64: all three
ship as .zip bundles (armhf's Node.js comes from the wekan/node fork there)
but not as Docker images, because the image sources its Node.js from
nodejs.org/unofficial only. tests/releaseDockerPlatforms.test.cjs pins arm/v7
out of both lists.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release clears the remaining release-build failures.
loong64 has no linux/loong64 base image to build its bundle in, so its
build job failed - and because the docker job waits on the whole
build-extra-arches matrix, that one failed leg skipped docker, and with it
the charts, ucs and nextcloud jobs; loong64 is now best-effort like
i386 and armhf, skipped with a warning instead of failing. With docker
running again, it then dropped linux/386 from the multi-arch image, which
its ubuntu:26.04 base cannot provide. And the
Launchpad snap builds (ppc64el, s390x, riscv64, armhf) could not push
WeKan's large history to git.launchpad.net and timed out mid-upload; the
repository is flattened to a single commit before the push now, so it fits.
Below that, a Launchpad failure hint that wrongly blamed the project licence
This release fixes the following release-build issues:
Extra-architecture bundles - the CPUs the build matrix compiles under emulation, and the Docker image that waits on that matrix.
<details> <summary><a href="https://github.com/wekan/wekan/commit/27da39f9587365003d26cfa6f095a203c67e1eb6">A loong64 with no base image is skipped, not failed, so it stops skipping the Docker image build</a>. Thanks to xet7.</summary>The docker job has needs: [prepare, release, build-extra-arches], and GitHub
skips a job whose needed job failed. build-extra-arches is a matrix, and its
loong64 leg hard-failed: no Docker Hub image publishes linux/loong64
(node-loong64 and ferretdb-loong64 exist, but the container to rebuild the
native modules in does not), so the preflight stopped it. One failed matrix leg
makes the WHOLE build-extra-arches job failure, which skipped docker - and
through docker the charts, ucs and nextcloud jobs that need it. That is
why run #209 left docker skipped, even though every buildable architecture
succeeded. loong64 is now marked optional: true, like i386 and armhf, and
releases/check-arch-binaries.sh skips a best-effort arch whose BASE IMAGE is
missing - not only one whose Node.js is missing - with a warning and exit 0
emitting skip=true, gated the same way as every other best-effort skip. So the
matrix job succeeds and docker runs; loong64 stays visible on every run as a
skip, and returns to a real build the day a linux/loong64 base image is
published. tests/releaseArchSkipAndBaseAttach.test.cjs pins loong64 as
best-effort and that the base-image gate skips it.
With loong64 no longer skipping build-extra-arches, the docker job ran again
for the first time in a while and failed at the base image: "ubuntu:26.04:
failed to resolve source metadata ... no match for platform in manifest". The
image is FROM ubuntu:26.04, and Ubuntu publishes no i386 image, so docker buildx build --platform ...,linux/386,... cannot resolve the base for that one
platform and the whole multi-arch build stops - the workflow's own bundle-build
comment already notes that docker run --platform linux/386 ubuntu:26.04
answers "no matching manifest". linux/386 is removed from the build's
--platform list and from the want= list that verifies the pushed manifest,
exactly as linux/loong64 already was: i386 ships as a .zip bundle (built on
debian:trixie, which has 386) but not as a Docker image.
tests/releaseDockerPlatforms.test.cjs pins that 386 and loong64 are out of
both lists, that the two lists match, and that the base is ubuntu:26.04.
The Launchpad snap builds - the ppc64el, s390x, riscv64 and armhf snaps
built on Launchpad with snapcraft remote-build.
The snap-launchpad builds (ppc64el, s390x, riscv64, armhf) run through
snapcraft remote-build, which pushes the project's git repository to
git.launchpad.net and builds it there. The push failed - "Git operation
failed with: Could not push 'HEAD' to git.launchpad.net/... snapcraft-wekan-
<hash>" - about four to five minutes in, on every one of the three
retries (v10.64 ppc64el, and v10.55 riscv64 before it). remote-build rejects a
SHALLOW clone, which is why the checkout is fetch-depth: 0, but it does not
need the history, and WeKan's full history is large enough that the push times
out or is refused mid-upload. After the full checkout the repository is now
re-initialised as ONE commit of the tagged tree - git rev-parse --is-shallow-repository is still false, so remote-build accepts it, but the
push carries the source tree (tens of MB) instead of the whole history
(hundreds). The snap version comes from snapcraft.yaml, not git describe, so
dropping the history changes nothing about what is built.
tests/releaseSnapLaunchpadFlatten.test.cjs pins the flatten, its order, and
that the checkout stays full-depth.
The Launchpad project the remote builds file under
(xet7-craft-remote-build) is
set to MIT / X/Expat, but the snap-launchpad failure hint printed "check its
Licence, 'None specified' ... set the licence to MIT" on EVERY failure - so an
s390x build that Launchpad had Stopped for another reason read as a licence
problem that was already fixed, and the search went to a correct setting. The
hint now says the licence should already be MIT (re-set it only if the page
shows "None specified") and, for the real remaining case, explains that a
Stopped build whose log is BUILDING (not FAILED) and ends mid-step is
Launchpad cancelling the build as it runs - typically an out-of-memory in the
memory-heavy Meteor npm install, or a transient build-farm reset, both of
which the three retries already cover.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release hardens how the multi-platform release is assembled.
The Node.js and FerretDB inside every platform bundle become named,
checksummed binaries, so the release provenance table can say exactly which
build each platform carries - the thing you need when a Node.js CVE lands;
the four native bundles used to ship whatever Node the GitHub runner carried
(cp $(command -v node)), and arm64 was even shipping Node 22 because
its job had no setup-node. Separately, the base bundles (amd64, arm64)
are now attached to the release loudly and verified, and - the real fix behind
that - the release job checks out before downloading the bundles instead of
after: the after-checkout deleted the just-downloaded zips (that is what
shipped v10.63 and v10.64 with no amd64/arm64 bundle, 404'ing every snap
build). And i386/armhf are skipped when no Node.js exists for them anywhere
instead of failing the run every release. It
also fixes an Admin Panel bug where a report opened by its own URL came up
empty over data that was plainly there, because the subscription was cancelled
by its own count re-render.
This release fixes the following release-build issues:
Bundle provenance - which Node.js and FerretDB binary each platform ships.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e802d52eed3c41bb65071c2c17bb6890c704232b">Every native bundle ships a named, checksummed Node.js and FerretDB instead of the runner's node</a>. Thanks to xet7.</summary>The amd64, arm64, win64 and mac-arm64 bundles embedded Node.js with cp $(command -v node) - the runner's node. A bare binary extracted onto the
runner publishes no checksum, so releases/record-provenance.sh could only
write no checksum published, and the provenance table at the top of the
release could not name the exact Node.js build a platform carried. It was also
the wrong build on arm64: build-arm64 has no setup-node step, so
$(command -v node) was ubuntu-24.04-arm's DEFAULT Node 22.x, not the
pinned Node 24 every other bundle shipped - and nothing recorded the
disagreement. A new releases/embed-verified-node.sh downloads the pinned
Node.js for the bundle's OS+CPU from nodejs.org, verifies the archive against
the published SHASUMS256.txt (fatal on a mismatch), and puts its node into
the bundle; each native job now calls it and records nodejs.org + the exact
version + the verified SHA256. FerretDB is verified the same way, against the
.sha256sum wekan/FerretDB now publishes beside every binary, and win64 and
mac-arm64 - which recorded no provenance at all - now upload a provenance
artifact like amd64 and arm64, so every platform is accounted for. The
emulated arches already did this through install-node-for-arch.sh.
tests/releaseNodeVerified.test.cjs pins that no native bundle can go back to
the runner's node or an unverified download.
Release assembly - attaching the base bundles, and the arches that can be built at all.
<details> <summary><a href="https://github.com/wekan/wekan/commit/55463f685a09f75b07a127ec901b3d564353d46b">The base amd64/arm64 bundles are attached loudly and verified, not silently by softprops</a>. Thanks to xet7.</summary>Every bundle except amd64 and arm64 attaches itself with gh release upload --clobber, which fails on a missing file and is verified from the release
side. The two base bundles were the exception: the central release job
attached them with softprops files:, which does NOT fail on an unmatched
file. So when a run produced no base zip, softprops created the release with
none of them and reported success - and v10.63
shipped with no amd64 or arm64 bundle at all, which 404'd every snap build
(native, wekan-ondra, wekan-gantt-gpl) on wekan-10.63-amd64.zip. softprops
also never listed the .sha256sum, so the base bundles had no checksum beside
them. The release job now creates the release with softprops (so it exists for
the self-attaching jobs) and attaches amd64/arm64 in a following step with gh release upload --clobber - failing the release if a base bundle is missing or
empty, rather than 404'ing ten downstream jobs - and it checksums the exact
bytes it attaches, so the base bundles get a .sha256sum like the rest. (Why
the base zip was missing in the first place is the next entry - the loud,
verified attach is what turned that silent gap into a failed release that
names it.)
The real reason v10.63 (and then v10.64) shipped with no amd64/arm64 bundle was
not softprops - it was the checkout. The release job downloaded the bundles
into the workspace and THEN ran actions/checkout for the provenance script.
The workspace is not a git repository at that point, so checkout's very first
act is "Deleting the contents of '<workspace>'" to make room for a fresh
clone - and it does this even with clean: false, which only skips the
git clean in an already-checked-out repo, not the initial wipe. The
just-downloaded wekan-<version>-{amd64,arm64}.zip were deleted before the
attach step, which then failed with "wekan-<version>-amd64.zip is missing or
empty" - and, because of the loud attach above, that now failed the release
outright rather than shipping an empty one. The checkout runs FIRST now, into
the empty workspace, and the bundles are downloaded on top of the checked-out
tree, where nothing removes them. tests/releaseBundlesSurviveCheckout.test.cjs
pins the order (checkout before the bundle download) rather than clean: false,
which was never enough.
build-extra-arches failed on i386 and armhf because there is no Node.js to
build them against: Node has no linux-x86 build at all, and no source builds
Node 24 for armv7l (nodejs.org and unofficial-builds have neither, and the
wekan/node fork has not built them yet). A red job every release for a CPU
nothing can currently produce a Node for is noise, not news. Both are now
marked best-effort (optional: true), and when a best-effort arch's
Node.js is absent everywhere releases/check-arch-binaries.sh emits
skip=true (a warning, exit 0) rather than a fatal error; every build step in
the job is gated on it, so the arch is skipped cleanly with nothing built. It
returns on its own the first release after wekan/node publishes node-i386 /
node-armhf. A required arch whose Node.js is missing is still fatal, as
before. tests/releaseArchSkipAndBaseAttach.test.cjs pins both this and the
base-bundle attachment above.
and fixes the following Admin Panel bug:
Admin Panel reports - loading a report by its own URL.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c5a40924187545b3f954ba1eca7970346432a314">An admin report opened by its URL keeps the subscription its own count re-render used to cancel</a>. Thanks to xet7.</summary>Opening an admin report by its address - /admin/problems/files typed,
bookmarked or refreshed - drew the column headers, "No results" and a "1 / 1"
pager over data that was plainly there, while the count method reported five.
An earlier fix
addressed one half (re-subscribe once the login lands), but the report still
came up empty. The onCreated autorun opens the pane and subscribes when the
open-pane or the logged-in user changes, and it called openReportPane() /
loadReport() directly in its reactive body - so it became reactive on
cfg.count (which loadReport reads through pageInfo), and loadReport's
own count method then did cfg.count.set(...). That re-ran the autorun, and a
Meteor.subscribe made inside an autorun is AUTO-CANCELLED when the autorun
re-runs; the re-run took the "same user, same pane" path, did not re-subscribe,
and left the report with no subscription - attachments in minimongo: 0. From
the left menu it worked, because that opens the pane from an event rather than a
computation, so the subscribe was never auto-managed; only the URL path hit it.
The autorun now reads only the pane id and the user reactively and runs its body
inside Tracker.nonreactive, so a count re-render no longer cancels the
subscription, whose lifetime is managed explicitly (a new onDestroyed stops
the last one). tests/adminProblemsSubscriptionLifetime.test.cjs pins it.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release stops "release all missing" from rebuilding the
AppImage and Flatpak every run when they are already published. Their
checksum files were named with the extension dropped
(WeKan-<v>-<arch>.sha256sum), but the missing-check looks for the name the
zip bundles use - the asset name plus .sha256sum
(WeKan-<v>-<arch>.AppImage.sha256sum) - so it never found the checksum and
counted the package as missing. The checksums now keep their extension,
matching the check.
This release fixes the following release-build issue:
<details> <summary><a href="https://github.com/wekan/wekan/commit/85d2ec6dadc8a8f04b97d3fa21af3367c7031336">"Release all missing" no longer rebuilds the AppImage and Flatpak that are already published</a>. Thanks to xet7.</summary>releases/expected-assets.sh decides a package is present only when its binary
AND <asset>.sha256sum are both on the release - the same convention the
wekan-<v>-<arch>.zip.sha256sum bundles follow. But AppImage.yml and
Flatpak.yml wrote the checksum with the extension DROPPED
(WeKan-<v>-<arch>.sha256sum rather than
WeKan-<v>-<arch>.AppImage.sha256sum), so the check never matched it and
reported the AppImage and Flatpak as missing on every run - rebuilding and
re-uploading them even when nothing had changed. The checksum (and md5sum) now
keep the .AppImage / .flatpak extension, so the check finds them and only
genuinely-missing packages are built. The same fix went to the wekan-ondra and
wekan-gantt-gpl forks, which name their assets the same way.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release fixes the reason the release shipped no bundles,
which is what starved every downstream job (snap, Docker, AppImage) of the
wekan-<version>-amd64.zip they download - the 404s those jobs kept hitting
were never their own fault. The release job downloaded the bundles and then
checked the repo out, and actions/checkout's default clean deleted the
untracked zips before they could be attached; the checkout now keeps them. The
snap download also stops treating the brief post-upload 404 as fatal.
This release fixes the following release-build issue:
<details> <summary><a href="https://github.com/wekan/wekan/commit/809dc794f2627c6fbc18ea0570128b4a8bac57a6">The release stops deleting its own bundles before attaching them</a>. Thanks to xet7.</summary>The release job downloads the per-arch bundles to the workspace root, then
checks the repo out (for the provenance script) before creating the GitHub
Release. actions/checkout defaults to clean: true, whose git clean -ffdx
deletes those untracked zips - so the "Create GitHub Release" step's
files: wekan-<version>-{amd64,arm64}.zip matched nothing. softprops does not
fail on unmatched files, so the release was created (job "success") with NO
bundles attached, and every downstream job that downloads one 404'd on
wekan-<version>-amd64.zip - the snap, Docker and AppImage failures were all
this. The checkout now sets clean: false, so the bundles survive and are
attached; the same fix went to the wekan-ondra and wekan-gantt-gpl forks, which
had the identical job. Separately, the snapcraft wekan part downloaded its
bundle with a single wget that treated a 404 as fatal, so it also broke on
the brief CDN lag right after an upload; it now retries like the other release
downloads. tests/releaseBundlesSurviveCheckout.test.cjs pins that a checkout
after the bundle download keeps clean: false.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release is all release-build fixes. With the release job
itself working again (v10.60), the per-platform jobs it feeds surfaced their
own breakage: build-win64 ran a bash script under PowerShell,
build-mac-arm64 called the Linux-only sha256sum, build-extra-arches
never unpacked the bundle it downloaded, and an i386 entry in
snapcraft.yaml — which core24 cannot build — was a parse error that failed
every snap. The downstream Docker and AppImage jobs, which only wrap
an already-published release bundle, now skip gracefully instead of failing
when that bundle is not up yet. And a Debian type: base snap is
scaffolded so i386 can eventually ship as a snap at all, since core24 has no
i386 port.
This release fixes the following release-build issues:
The per-platform release jobs - each broke in its own way once the release job started feeding them again.
<details> <summary><a href="https://github.com/wekan/wekan/commit/bbc357874715f1e91e4297a40c7301d5aea354cf">The win64, mac-arm64, extra-arch and snap release jobs each fail on their own bug</a>. Thanks to xet7.</summary>Four independent failures in the v10.60 run, one per job: build-win64's
"Check the binaries" step ran set -euo pipefail under the Windows default
shell and PowerShell answered "A parameter cannot be found that matches
parameter name 'euo'" - it now says shell: bash. build-mac-arm64 ran
sha256sum, a GNU coreutils name macOS does not have, and died exit 127;
shasum -a 256 is the BSD/macOS spelling. build-extra-arches downloaded
wekan-<version>-amd64.zip but never unpacked it, so the container mounted an
empty /bundle and the native-module rebuild died with ENOENT ... /bundle/programs/server/package.json (exit 254) - it now unzips the bundle
like the arm64/win64/mac-arm64 jobs already do. And snapcraft.yaml declared
an i386 platform that core24 (Ubuntu 24.04, no i386 port) rejects with "none
of these build architectures are supported" - a PARSE error that failed
snap-native AND every snap-launchpad arch, not only i386. The i386 platform
and its launchpad matrix entry are removed; i386 users are served by the .deb
and AppImage.
The downstream packaging jobs - Docker and AppImage only WRAP a release bundle that another job builds, so they cannot run before it exists.
<details> <summary><a href="https://github.com/wekan/wekan/commit/43ff0e7a5ca56e3c51560e701146e336449affee">Docker and AppImage skip with a notice instead of failing when the release bundle is not published yet</a>. Thanks to xet7.</summary>dockerimage.yml / docker-publish.yml build a Dockerfile that DOWNLOADS the
prebuilt wekan-<version>-amd64.zip, and AppImage.yml wraps the per-arch
bundle; all of them exited hard when that asset was not on the release -
which it was not while the release was still building, or when a run targeted a
version whose bundle was not published. Each now checks whether the asset it
needs is on the release and SKIPS with a ::notice:: (a green run) when it is
not: the docker jobs gate the build on the pinned version's zip, and AppImage
flips its per-arch BUILD_THIS off and treats "no AppImage, and no base bundle
either" as nothing-to-do. The release image / AppImages are still built by
release-all.yml once the bundles are up; these standalone runs simply stop
failing in the meantime.
and scaffolds an i386 snap on a Debian base:
<details> <summary><a href="https://github.com/wekan/wekan/commit/8790b66806269f26f08c4073ea8d4f7805716ba6">A Debian type:base snap so i386 can ship as a snap at all, since core24 has no i386 port</a>. Thanks to xet7.</summary>core24 has no i386 port, so base: core24 cannot build an i386 snap. Debian
still ships i386 and is glibc, so - unlike an Alpine/musl base - WeKan's
existing binaries run unchanged, and since the snap only assembles the prebuilt
wekan-i386.zip (which already exists), the one missing piece is an
i386-capable base. snap-base-debian/ scaffolds wekan-base-debian13 (type: base), a trimmed Debian trixie rootfs, starting with i386 - which builds
NATIVELY on an amd64 runner, no qemu or Launchpad. It is isolated from the
working core24 snapcraft.yaml and is a documented, UNVERIFIED scaffold: a
custom base snap needs a real snapcraft pack + local install + a WeKan snap
running on it before CI or the (manual) store review, all of which
snap-base-debian/README.md spells out.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release fixes the release workflow that publishes WeKan.
The v10.59 release job built the GitHub Release body from the newest CHANGELOG
section by interpolating it inline into a shell printf, so every backtick
in the notes ran as a command — and v10.59's notes are full of code spans, so
the job died with Incorrect: command not found and
loginFailureDecision.js: Permission denied, and published nothing. The
CHANGELOG now reaches the release-notes scripts through the environment,
where the shell treats it as data. A new test pins that the changelog is never
interpolated into a run: script again.
This release fixes the following release-build issue:
<details> <summary><a href="https://github.com/wekan/wekan/commit/541dc8b7b6499ef39f7812deacdebc5927570b9b">The release notes pass the CHANGELOG through the environment instead of inline into the shell</a>. Thanks to xet7.</summary>The release and release-notes jobs of release-all.yml composed the GitHub
Release body with printf '%s\n' "${{ needs.prepare.outputs.changelog }}".
Interpolated inline with ${{ }}, the CHANGELOG becomes part of the shell
source before bash parses it, so a backtick in an entry — every code span is
one — runs as a command substitution. The v10.59 notes (this file's own
LockoutBleed section) turned into Incorrect: command not found,
User not found: command not found, POST: command not found and
server/apiAuthRoutes.js: Permission denied, the release job failed, and the
release was published with no bundles. Both steps now take the changelog through
env: CHANGELOG: ${{ … }} and write "$CHANGELOG", where the shell treats the
value as data and never parses its backticks, $( ) or quotes.
tests/releaseNotesNoShellInjection.test.cjs pins that outputs.changelog is
only ever consumed as an env: assignment, never inline in a run: script, and
fails on both pre-fix printf lines.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: this release closes LockoutBleed (GHSA-2g94-9x3m-hv37), a
reported two-part authentication weakness that chained into account takeover.
The bundled accounts-lockout brute-force protection had gone completely
inert: its hooks gated on the English failure text Incorrect password /
User not found, but Meteor's ambiguousErrorMessages (on by default) rewrites
every credential failure to one generic sentence before those hooks run, so
the failure counter never moved and no account ever locked. Alongside it the
login path leaked which usernames and emails exist — a real user runs bcrypt
(~50 ms) while a missing one answers in ~2 ms, a timing oracle no uniform error
text can hide. The REST /users/login twin was worse: it named the missing
user outright and never went through the lockout at all. The lockout now counts
on any genuine password failure regardless of wording, a dummy bcrypt
comparison equalises the missing-user path's timing on both the DDP and REST
logins, and the REST endpoint answers missing-user and wrong-password
identically and throttles repeated failures per client. Four new unit suites
pin each half. Below that, a release-build fix: three build jobs ran the
binary pre-check without the repo checked out where the call looked for it, so
the arm64 build died with exit 127 before assembling a bundle.
This release fixes the following CRITICAL SECURITY ISSUE of LockoutBleed:
Login and the brute-force lockout - signing in, the lockout that guards it, and its REST twin.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c3742f2ed7cfeb3dc709997ca9403d3fae5f7188">The bundled brute-force lockout counts failed logins again instead of silently never firing</a>. Thanks to NinjaGPT and xet7.</summary>WeKan bundles wekan-accounts-lockout (default: 3 failures → 60 s lockout), and
it had been doing nothing at all. Both of its Accounts.validateLoginAttempt
hooks decided whether an attempt was a failure by comparing the error's reason
string: loginInfo.error.reason !== 'Incorrect password' for a known user,
!== 'User not found' for an unknown one. That reason never arrives. Meteor's
accounts-base ships ambiguousErrorMessages defaulting to true, so
Accounts._handleError rewrites every credential failure — wrong password, no
such user, no password set — to the single sentence "Something went wrong.
Please check your credentials." before any validateLoginAttempt hook runs. The
literals therefore never matched, both hooks returned early, the counter was
never incremented, and no account ever locked — confirmed in the report by
AccountsLockout.Connections staying empty under unlimited failed logins.
The hooks now decide from the attempt's structural fields instead of a
localized, Meteor-internal string we do not control (new
packages/wekan-accounts-lockout/src/loginFailureDecision.js): a password login
of a known user that carries an error is a countable failure; a password login
with no matched user that carries an error is a countable unknown-user failure.
The one error deliberately not counted is no-2fa-code — accounts-2fa
throws it after the password already checked out, to ask for the second
factor, so it is the normal first leg of every two-factor sign-in and counting
it would lock out legitimate 2FA users. A wrong second factor
(invalid-2fa-code) still counts, because there the password was already
correct. tests/loginFailureDecision.test.cjs pins that the ambiguous reason is
counted (the exact regression), that success still runs the hook so an active
lock is enforced, and that no-2fa-code never locks anyone.
The accounts-password login path runs a bcrypt comparison (~50 ms) only when the user exists and has a local password; for a missing user — or an LDAP/OIDC-only user with no local password — it throws immediately (~2 ms) with no bcrypt work. The two response-time distributions do not overlap, so an unauthenticated attacker can tell whether any given username or email exists with near-100% reliability, regardless of the uniform error text WeKan returns.
The standard mitigation is applied: whenever the real path would skip bcrypt,
one dummy bcrypt comparison against a fixed cost-10 hash is performed so the
missing-user path costs about the same as a real check (new
server/lib/loginTimingDefense.js). On the DDP login method a
timing-normalization login handler runs ahead of the built-in password handler,
looks the user up, and — when there is no local password to check — burns the
compensating time before falling through; it never authenticates
(server/loginTimingNormalization.js).
tests/loginTimingDefense.test.cjs pins the fixed hash's shape, that the
equaliser feeds the dummy user and digest to the injected comparator, and that
it never throws.
POST /users/login in server/apiAuthRoutes.js checks the password directly
with Accounts._checkPasswordAsync and never runs the DDP lockout hooks, so it
had no brute-force protection at all — and it enumerated by message, throwing
a distinct "User with that username or email address not found." for a missing
user while a wrong password threw the ambiguous one. It now fails missing-user
and wrong-password with the same uniform error, runs the same dummy-bcrypt
timing equaliser for a missing or password-less account, and throttles failed
attempts per client address (new server/lib/loginAttemptThrottle.js, default
10 failures / 60 s → 60 s lockout, env-tunable via REST_LOGIN_MAX_FAILURES /
REST_LOGIN_FAILURE_WINDOW_SECONDS / REST_LOGIN_LOCKOUT_SECONDS). Only
failures count and a success clears the client's counter, so correct-credential
clients are never impeded; X-Forwarded-For is honoured only when
HTTP_FORWARDED_COUNT declares the proxy depth, so the header cannot be spoofed
to dodge the throttle. tests/loginAttemptThrottle.test.cjs pins the
time-injected state machine and the key resolver, and
tests/loginBruteForceEnumerationWiring.test.cjs pins that the fragile
reason-string guards stay gone and the REST endpoint keeps its uniform error,
timing equaliser and throttle.
and has the following release-build fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/3e8c5a89e23c7d51cbe53cde23955aabaf74d635">Three release-build jobs check the repo out before running the binary pre-check</a>. Thanks to xet7.</summary>The bash releases/require-binaries.sh pre-check, wired into the release build
jobs, needs the repository on disk — but three jobs did not have it there, so
the arm64 build failed with exit code 127
(releases/require-binaries.sh: No such file or directory) before it ever
assembled a bundle. build-arm64 and build-mac-arm64 download a prebuilt
bundle artifact and never checked the repo out at all; each now checks the tag
out first, before download-artifact drops the bundle into the same workspace
(a root checkout would otherwise wipe it). build-win64 checks the repo out
into src/ for start-wekan.bat and snapcraft.yaml, but called the script at
the workspace root; it now calls src/releases/require-binaries.sh, the same
src/ path its start-wekan.bat copy already uses.
tests/releaseBuildJobsCheckout.test.cjs pins, for every build job that runs
the pre-check, that the job checks the repo out where the call looks for it and
that a root checkout precedes download-artifact; it fails on all three pre-fix
breakages.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: WeKan is downloadable as an AppImage and as a Flatpak now,
both built from the bundle a release already carries rather than from a second
build of WeKan. Everything else here is about builds, which were failing at both
ends, and none of it was about WeKan's
own code. The release build for every architecture that is not amd64 or
arm64 was down: four died on a shell quoting bug that emptied the CPU name
out of the Node.js download URL, two on a base image that is not built for
their CPU, and underneath both, nothing checked that the binaries those
bundles are assembled from had been published at all. Every build checks first
now, and says which file is missing and which repository should publish it. The
local build was running out of memory, which turned out to be .gitignore
against .meteorignore: Meteor reads only the second, so every other
repository cloned in beside the app - the Node.js fork, mongo-tools, TSC,
two more WeKan checkouts - was being walked as if it were WeKan, at one ignore
matcher per directory. And a release can now be finished rather than made
again: release-all-missing.yml builds only what a release is short of, in
every repository WeKan releases from. Below that: an Admin Panel report that
drew "No results" over data that was there, a phone layout with the menu over
the boards, seven SSRF tests failing on a fake response that was not a stream,
where a local run writes its logs, and a build.sh that reported success after
a failed build.
This release adds the following ways to install WeKan:
AppImage and Flatpak - two more formats, from the bundle a release already has.
<details> <summary><a href="https://github.com/wekan/wekan/commit/73e06940c3ee7b597e9f47ce9054dbbb5b69aa82">WeKan is published as an AppImage and as a Flatpak, for x86_64 and aarch64</a>. Thanks to xet7.</summary>Both are built from wekan-<version>-<arch>.zip - the bundle the release
already carries - rather than from a second build of WeKan, so what is inside an
AppImage is the same Node.js, the same FerretDB and the same application code
that the .zip for that architecture contains. Neither needs Meteor to run again.
The AppImage carries its own runtime and starts on any distribution with a
recent enough glibc; the Flatpak runs against org.freedesktop.Platform and is
published with a repository so flatpak update works. Each is checksummed like
the bundles, with a .md5sum and a .sha256sum beside it.
and updates the following dependencies:
{a,b} expansion behind glob
matching.Thanks to dependabot.
and fixes the following release-build bugs:
The extra-architecture bundles - built from binaries other projects publish.
<details> <summary><a href="https://github.com/wekan/wekan/commit/92198b0671ee27545d62c9537aaccb50cabe49f7">An apostrophe in a comment emptied the CPU name out of every Node.js download URL</a>. Thanks to xet7.</summary>Four of the six died with the same 404, on a URL that named no architecture at all:
curl: (22) The requested URL returned error: 404
No Node.js for at official (https://nodejs.org/dist/v24.18.1/node-v24.18.1-linux-.tar.xz)
The container script was passed as bash -c '...' - a single-quoted argument -
and it contained apostrophes, in comments like "where this CPU's Node.js comes
from". A single-quoted shell string cannot hold an apostrophe: the backslash
does not escape it, it ENDS the string. Everything after became separate words,
and ${NODE_ARCH} ended up somewhere the RUNNER's shell expanded rather than
the container's - and the runner has no NODE_ARCH, so it expanded to nothing.
The script is a file now, releases/install-node-for-arch.sh, mounted into the
container. A file has no quoting layer to get wrong and bash -n can check it.
The other two died earlier still, on docker: no matching manifest for linux/386 in the manifest list entries. ubuntu:26.04 publishes amd64,
arm/v7, arm64, ppc64le, riscv64 and s390x - not 386, and not loong64.
debian:trixie publishes 386 as well, so that is the base image now, named
per architecture in the matrix instead of assumed.
loong64 still cannot be built: no image on Docker Hub publishes it at any tag, so there is no loong64 userland to rebuild the native modules in. Its Node.js and its FerretDB both exist; the container does not. It stays in the matrix, and the check below says exactly that on every run, rather than the architecture quietly vanishing from the release.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/92198b0671ee27545d62c9537aaccb50cabe49f7">Every build checks the binaries it needs exist, and stops naming the missing one</a>. Thanks to xet7.</summary>A bundle is assembled out of files other repositories publish: FerretDB from
wekan/FerretDB, the MongoDB Database Tools from wekan/mongo-tools, and on
the CPUs nobody else builds for, Node.js from wekan/node. Any of them can be
absent because a build has not finished, and the build should say so plainly
rather than failing an hour later with a bare 404 in the middle of an emulated
npm install.
releases/check-arch-binaries.sh runs before anything is built and checks the
base image, the Node.js, the FerretDB binary and the tools;
releases/require-binaries.sh does the same for the amd64, arm64, win64 and
mac-arm64 bundles. Each missing file gets a line naming it and the repository
that should publish it. The MongoDB tools stay a warning - FerretDB is the
database, and the launcher does not need them to start.
What a release says about itself - what is in a bundle, and where it came from.
<details> <summary><a href="https://github.com/wekan/wekan/commit/c429ea71d92d945076d25f990599fa0b1fd3d535">Every bundle has a checksum, and the release notes open with where its binaries came from</a>. Thanks to xet7.</summary>A WeKan bundle is assembled out of files other projects publish - a Node.js build, a FerretDB binary, the MongoDB Database Tools - and WHICH of them a given architecture got varies per release: nodejs.org builds some CPUs, unofficial-builds others, the wekan/node fork the ones neither of them does, and not all of them publish a checksum to check the download against.
None of that was written down anywhere a downloader could see. It lived in a build log that expires.
Every wekan-<version>-<arch>.zip now has a .sha256sum beside it on the
release, so a download can be checked. And the release notes OPEN with a
provenance table: for each architecture, what each binary was, which project
published it, at what version, from what URL, and with what checksum - or, when
the publisher offers none, that it could not be verified. It is written by the
build that used the file, not by hand afterwards, so it describes what actually
went into the bundle.
Which Node.js a bundle carries - where it comes from, and if it is checked.
<details> <summary><a href="https://github.com/wekan/wekan/commit/60c9e55d2fa4e5a152c2784caa60bb4db0dc3b21">Each CPU gets the newest that exists for IT, not the newest that exists</a>. Thanks to xet7.</summary>Those are the same thing on amd64 and arm64, and regularly are not anywhere else. nodejs.org builds a handful of architectures, unofficial-builds adds a few, the wekan/node fork builds the rest, and each runs on its own schedule
The check walks the 24.x releases from newest down and asks all three sources at
each one, taking the first hit - which is by construction the newest build that
exists anywhere for that CPU. riscv64 is why it matters: unofficial-builds
publishes it up to v24.18.1 and has not reached v24.19.0, so looking only at
the newest found nothing and stopped the build, while a perfectly good riscv64
build was sitting there one version back.
When the answer is behind, the log says which version it got, from where, which it wanted, and what to build to bring it in line - a warning, not an error, because the alternative is no bundle at all for that CPU. The walk stops after twelve releases: a CPU whose newest build is a dozen releases old is not slightly behind, it is unmaintained, and saying so is more use than quietly shipping something from last year.
Today that gives s390x and ppc64le the newest v24.19.0, and
riscv64, i386 and loong64 v24.18.1. armhf has no build at any
version from any source, and is the one architecture the run still stops on.
The three sources do not offer the same assurances, and this was checked rather
than assumed: nodejs.org publishes a SHASUMS256.txt and signs it with the
Node.js release keys, unofficial-builds publishes the checksums but no
signature, and the wekan/node fork published neither until the change beside
this one. So the order is official, then unofficial, then the fork - descending
verifiability, with the fork as the backstop for what the other two do not
build.
Preferring a source because it publishes a checksum and then not checking it would be preferring it for nothing. The checksum is looked up during the preflight and the download step refuses a file that does not match it.
A mismatch is retried before it is fatal: the likely cause is a truncated transfer, which asking again fixes. After three attempts the build stops, because at that point the file being served is not the file that was published. Where no checksum exists the log says so in as many words, rather than leaving the reader to assume a check was made - which is the case for FerretDB and the MongoDB tools today, neither of which publishes one.
</details>Running the tests locally - what a run is given, and what it leaves behind.
<details> <summary><a href="https://github.com/wekan/wekan/commit/03b677f285ec7ba8c9da9e62e3096ba9efff390c">A build that runs out of heap can be told to leave a snapshot behind</a>. Thanks to xet7.</summary>Three runs have now exhausted the heap in the same phase - after both rspack
compiles report done, while Meteor writes the production bundle. 8146 MB of
8192, then 15526 of 15542 once the limit was worked out from the machine, then
15520 with standard-minifier-js removed on the theory that a second JS
minifier over rspack's already-minified output was the consumer.
That last run settled the theory: taking the minifier out moved the peak by 6 MB, which is noise, and it died in the same place. It was not the consumer, so it is back - an unverified change to the release artifact that demonstrably fixes nothing does not belong in the tree, and with the build never completing there was no way to confirm the bundle was still correctly minified either.
Something else is holding 15 GB, and three guesses is enough. build.sh takes
WEKAN_BUILD_HEAP_SNAPSHOT=1 now, which adds Node's
--heapsnapshot-near-heap-limit=1 so the build writes a heap snapshot just
before it dies instead of only dying. Off by default, because the file is about
as large as the heap; when it is on, the build says where the snapshot lands and
what to open it with.
The build died with
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
at 8146 MB of an 8192 MB limit, on a machine with 30 GiB of RAM. It had
not run out of memory - it had run out of the ceiling build.sh gave it. And
because that ceiling was a constant, the same number was simultaneously too
small on a large machine and too large on a small one.
It is half of total RAM now, clamped to [4096, 16384]. Half is the share that leaves the rest of the machine usable while a build runs; the floor keeps a small machine from being handed something unusable; the ceiling is there because a heap bigger than that means something is wrong rather than something is big. At 16 GiB it works out to exactly 8192 - the value that was hard-coded - so nothing changes on the machine that number was picked for. This one gets 15542.
The chosen size is printed at startup, and exporting TOOL_NODE_FLAGS or
NODE_OPTIONS yourself still wins.
This is also the first failure the new build log caught: the run before it failed the same way and left nothing behind to read. And when it happens again the error says so in words - the limit the build had and the peak it reached - rather than ending on a V8 stack trace, which reads as a crash rather than as the resource limit it is.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/76f278871c62049ad419498bd7dbe4c33f7ed11c">Logs land somewhere writable, so a run inside a sandbox keeps them</a>. Thanks to xet7.</summary>Every log build.sh writes goes into a log/<datetime>/ directory, and the
root of those was hard-coded to ../log/ - one level up from the repo,
which is the right default: a test run then does not show up in git status,
and the release process and the docs read it there.
It is not always there to write into. A Flatpak sandbox shares only the
repository directory, so .. is missing or read-only, mkdir -p ../log fails,
and every redirection after it either failed or quietly dropped its log into the
repo root - which is the worst of both, because the run looks like it logged
something and git status fills with stray files.
WEKAN_LOG_ROOT is resolved once at startup: ../log when the parent is
writable, ./log inside the repo when it is not, and whatever you set if you
set it. Same log/<datetime>/ shape either way, so nothing that reads these has
to care which happened, and the chosen path is printed when a run starts.
releases/db-conformance.sh makes the same choice when run on its own, and
/log/ is gitignored for the case it lands inside.
The newest run ended FAIL WeKan tests (sequential) and its log directory held
nothing about WeKan at all - the FerretDB and database-conformance logs were
there, and not one line about why WeKan never got as far as a test. The build's
output went to the terminal only, so the one question the run raised was the one
question its logs could not answer.
build_wekan tees into wekan-build.log in the run's own directory now, and
says the path before it starts. An interactive build still scrolls past exactly
as before.
Two things found while looking. build_wekan did not check whether the build
worked - meteor build failing left the function returning success, and the
caller found out only later by noticing .build/bundle was missing. And
run_all_tests minted its own ../log/<datetime>/ even when a larger run had
already set WEKAN_LOGDIR, which is exactly the split-across-two-directories
the per-run directory exists to prevent; it happened to land in the same second
this time, so nothing showed. It uses the outer directory when there is one, and
exports its own when there is not.
The steps that got there, each a change of its own:
and fixes the following local-build bugs:
What Meteor is allowed to walk - the app directory, and what has been cloned into it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/cf178e5948f91041af2a02ad2503d6ce37cae105">The build ran out of memory scanning the other repositories cloned beside it</a>. Thanks to xet7.</summary>meteor build died with JavaScript heap out of memory, and raising
--max-old-space-size never helped, because the ceiling was not the variable.
A heap snapshot taken as the build died says what was holding it. Of a 4.1 GB
heap, 980 MB was 14,267,543 IgnoreRule objects, 667 MB sliced strings, 308 MB
concatenated strings, and 274 MB was 945 copies of a single 296 KB pattern
list - about 2.5 GB of ignore machinery, and only 945 directories in.
Meteor builds one ignore matcher per directory it descends into, each carrying the whole accumulated pattern list. There were 6,867 directories under the app and roughly 1,000 of them were WeKan. The rest were repositories cloned in beside it - the Node.js fork checkout alone is 4,132 directories, 45,527 files and 2.3 GB - plus mongo-tools, TSC, the TSC website and two more WeKan checkouts. So the real cost was very nearly seven times what the snapshot managed to catch.
All of them were in .gitignore. Meteor does not read .gitignore; it reads
.meteorignore, which listed only .tools/, FerretDB/ and tests/. The two
variant checkouts were the worst of them, because wekan-ondra and
wekan-gantt-gpl contain client/, server/ and models/, and Meteor loads
server/ and client/ eagerly - a second and third copy of the whole app
pulled into the build.
_build/ and _build-local-test/ are the trap here, and the first attempt
fell into it. They are gitignored, and they were the first two entries of that
296 KB pattern list, so they read as build output that should be excluded too -
and excluding them breaks the build, with an error that never mentions
.meteorignore: Could not find mainModule for 'os' architecture: _build/main-prod/server-meteor.js. They are not leftovers, they are the
handoff. rspack compiles the app INTO _build/main-prod/, and Meteor then reads
server-meteor.js and client-meteor.js from there as the application's main
modules. They are three directories each, so there was nothing to win and a
build to lose; the guard now asserts the opposite for them.
tests/meteorignoreScanScope.test.cjs pins the excludes and that they are
anchored to the repo root rather than matching a directory of that name at any
depth. It also states the general rule, so the next clone dropped in here is
caught by a test rather than by a build running out of memory: it WALKS the tree
for any directory with a .git of its own, rather than comparing against a list
of names, and requires each one to be ignored by git and listed in
.meteorignore. A directory with a .git is another project - not WeKan's
source and not WeKan's history - so it belongs in both files, and being in
neither is the state every one of these arrived in.
build_wekan returns 1 when the build fails, and prints ERROR: the WeKan build failed. The menu called it bare, which throws that status away: the script fell
off the end and exited 0. Anything driving it non-interactively - printf '1\n2\n' | ./build.sh, or CI - saw a green run and a missing bundle.
The first attempt at the fix above excluded _build/ and _build-local-test/
too, and that broke the build outright, with an error that never mentions
.meteorignore:
error: Could not find mainModule for 'os' architecture:
_build/main-prod/server-meteor.js
They look exactly like build output that should be ignored - they are
gitignored, and they were the first two entries of the 296 KB pattern list the
heap snapshot turned up. They are not leftovers, they are the HANDOFF: rspack
compiles the app INTO _build/main-prod/, and Meteor then reads
server-meteor.js and client-meteor.js from there as the application's main
modules. Ignoring them hides the files Meteor is about to be handed. They are
three directories each, so there was nothing to win and a build to lose.
The first guard derived its list FROM .gitignore, so it only caught a clone
that had got half way: one added to NEITHER file was invisible to it, and that
is the state every one of these arrived in.
The check walks the tree for any directory with a .git of its own -
existsSync, because a submodule's .git is a file rather than a directory -
instead of comparing against a list of names, so a clone nobody thought to name
is caught too. Anything it finds must be ignored by git (asked of git check-ignore, not of a hand-parsed .gitignore) and, when it is top-level,
listed in .meteorignore. It does not descend into a repository it has already
found: that one's own submodules ride along with it.
Verified by planting a directory with a .git in it - the guard fails and names
it.
The three clones that arrived during this release, each in both files - git
ignores them so git status stays readable, Meteor ignores them so the build
does not walk them:
and fixes the following bugs:
The Admin Panel reports - how a pane gets its rows.
<details> <summary><a href="https://github.com/wekan/wekan/commit/78b2f9ebcd56b699f218e8da06cd99a8eac81b12">A report opened by its own address drew "No results" over data that was there</a>. Thanks to xet7.</summary>Opening a report BY ITS ADDRESS - /admin/problems/files typed, bookmarked or
just refreshed - drew the column headers, "No results" and a "1 / 1" pager while
the attachments were plainly in the database. Reached by clicking the menu entry
it worked, so this only ever happened to the URL.
A full page load resumes the login from localStorage asynchronously, and
the route sets the open pane before that lands. The subscription was therefore
made with no user; the publication's isAdmin check answered this.ready()
with no rows; and nothing re-subscribed, because the autorun that opens the pane
did not depend on the user. The count METHOD, called later from the same page,
happily reported five - which is what made this look like a publication bug
rather than a timing one.
The autorun depends on Meteor.userId() now. openReportPane() returns early
when the pane is already open, so re-running it after the login would do nothing
at all - hence the second branch, which re-subscribes the report that is already
open now that there is a user to subscribe as.
Mobile All Boards - the phone layout, and what decides a column's width.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d80d8de1a0dbc4abf55cb8831c64ee9017440079">The left menu was drawn over the board icons on a phone</a>. Thanks to xet7.</summary>The menu was 260px wide inside a 157.5px grid track and lay over the boards. The grid was right all along: 42% of a 375px phone is 157.5, plus the 8px gap puts the board column at x=165.5, which is exactly where the boards start. The menu simply did not fit in its track.
260px is --wekan-left-menu-width, the DESKTOP width the drag-grip sets, and
leftMenu.js does not even offer that grip below 800px. The menu carried it on
a phone anyway, because the phone rules never said otherwise and the
max-width: 100% on the base rule does not do what its comment claimed: a
percentage width on a grid item does not resolve against the track, so it capped
nothing. That is not a browser quirk - Chromium, Firefox and WebKit all drew the
same 260px.
width: auto in the phone rules instead. A grid item with an auto width
stretches to its grid area, so it fills the track exactly with no percentage to
resolve. Measured rather than guessed: decoding the failing test's screenshot
pixel by pixel shows the menu background #f7f7f7 running from x=3 to x=255 with
the blue board tiles painted on top of it from x=170, and the page background
only from x=260.
The security test suite - what it stands in for, and how faithfully.
<details> <summary><a href="https://github.com/wekan/wekan/commit/aab81e8ffd4df6b0585f122aea3dac09ded726be">Seven SSRF tests timed out because the fake response was not a stream</a>. Thanks to xet7.</summary>Every ALLOW case of the DnsBleed suite failed with Timeout of 2000ms exceeded,
while every block case passed - which is what hid it.
The stub was at fault, not fetchSafe. It faked http.IncomingMessage with a
bare EventEmitter that emitted data and end from a process.nextTick,
into the void if nothing was listening yet. No real response does that: an
IncomingMessage is a PAUSED Readable that buffers its body until a listener
attaches, so reading late cannot lose data. That only started to matter when
fetchSafe was split into resolving the response and then reading it, which the
redirect handling needs - the nextTick queue drains BEFORE promise microtasks,
so the fake had already fired end by the time the awaited continuation
attached its listeners.
Verified rather than assumed, both halves: against a real server, a request
whose listener is attached two nextTicks, a setImmediate and 20ms late still
receives the whole body; and against the real fetchSafe, driven by each stub
in turn, the old one times out where the Readable-backed one returns the body,
the pinned IP and the Host header. The security assertions still hold through
the new stub - a 302 refused, a host resolving to 127.0.0.1 refused with ZERO
requests sent, each redirect hop pinned, and credentials dropped cross-origin.
and adds the following release tooling:
Completing a release - across every repository WeKan releases from.
<details> <summary><a href="https://github.com/wekan/wekan/commit/d62c4f376635461d7cd59ffc6fa791b5a7f39556">A release can be finished without being made again</a>. Thanks to xet7.</summary>release-all.yml is one run that bumps the version, tags, builds every
platform, and publishes Docker images, snaps, the website and the charts. It is
the right thing for MAKING a release and the wrong thing for FINISHING one. When
the v10.57 run ended with all six extra architectures missing, there was no way
to get those six except to run the whole thing again, version bump and all.
release-all-missing.yml finishes a release that already exists. It never bumps
a version, never tags, and never touches Docker, snap, the website or the
charts.
What makes it possible: only build-amd64 runs Meteor. Every other bundle WeKan
ships is that same bundle with its native modules rebuilt, its Node.js swapped
and its database binaries replaced. So it downloads the PUBLISHED
wekan-<v>-amd64.zip - verifying its checksum, since it is the bundle every
other architecture is cut from - and repacks it with
releases/repack-bundle-for-arch.sh, the same script the full release runs. A
bundle added to a release months later is therefore built exactly like the ones
already on it.
It does not claim to build everything, and says which: amd64 is the Meteor
build, arm64/win64/mac-arm64 each need their own kind of runner, and the
Sandstorm .spk is signed. releases/expected-assets.sh says what a complete
release looks like, in one place, and an asset counts as present only when its
.sha256sum is there too - a bundle whose checksum upload failed is
half-published.
The same pair now exists in wekan/node, wekan/FerretDB,
wekan/mongo-tools, wekan/gitea, Secretchronicles/TSC and both snap
variants, so every repository WeKan releases from can be completed the same way.
Where the full build had another name - node.yml, build-binaries.yml - it is
release-all.yml now.
The only filter was written as a job-level condition, and GitHub refuses to
load a workflow that does that:
Invalid workflow file
(Line: 109, Col: 9): Unrecognized named-value: 'matrix'
matrix is available to a job's runs-on, env, name, container,
services, continue-on-error, timeout-minutes, strategy and steps - but
NOT to jobs.<id>.if, which is evaluated before the matrix is expanded. It
looks entirely reasonable, which is why it was written in five workflows in one
sitting.
It is worse than a job that does not run: a workflow that will not load takes
every workflow that CALLS it with it, so TSC's release-all-missing.yml failed
at startup with "error parsing called workflow" and built nothing at all.
The decision moves to the job's env:, which can see matrix, and every step
asks for it. Steps that already had a condition keep it, ANDed inside
parentheses. Twelve files across five repositories.
tests/workflowExpressions.test.cjs is the guard, and it exists because a YAML
parser is perfectly happy with every one of these - the file is valid YAML, and
only GitHub's expression evaluator rejects it, when the workflow is dispatched.
It pins that no job-level if: reads matrix or steps, that every ${{ }}
is closed, and that a workflow declaring an only input actually consults it.
Its brace check strips complete expressions rather than counting braces, because
three real lines run docker with Go templates full of }} that close nothing of
GitHub's.
uses: ./.github/workflows/X.yml fails at STARTUP - taking the calling workflow
down with it, so nothing runs and there is no job log to read - when X.yml does
not exist, has no workflow_call trigger, or is handed a with: key it never
declared. Same class of failure as the one above, and just as invisible: the
YAML is valid, and only GitHub's loader objects.
Worth pinning now because release-all-missing.yml calls AppImage.yml and
Flatpak.yml here, TSC's calls five workflows, and the only input they all
take was added by hand to each of them. One typo in a with: key would stop a
whole run.
Checked across every repository first - TSC, WeKan, both snap variants,
wekan/node, wekan/FerretDB, wekan/mongo-tools and wekan/gitea - where all
reusable-workflow calls already match. Then verified against all three shapes by
breaking each in turn: an undeclared with: key, a missing file, and a called
workflow with no workflow_call.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: two reported bypasses of the SSRF guard are closed, and they are the same mistake in its two halves. FollowBleed — the import downloads validated the URL and then fetched it with something that follows redirects, so a public URL could answer 302 to 127.0.0.1 and that body became the imported attachment; fetchSafe now validates and pins every hop, not only the one the caller passed. TransitBleed — the shared block-list read an IPv6 address by its spelling, so 6to4, NAT64 and Teredo addresses carried an internal IPv4 destination straight through it; an address is now expanded to its bytes and every embedded IPv4 is re-checked. Below that, the platform documentation is arranged by what each platform is: every page lives under OS, HW, Container, Cloud, Package, Source or SaaS, with the links that had to follow the move, logos stored beside their page instead of fetched from somebody else's server, two new platform pages, and a developer-tooling fix.
This release fixes the following CRITICAL SECURITY ISSUES:
The SSRF guard - what it checks, and what it was deciding from.
<details> <summary><a href="https://github.com/wekan/wekan/commit/4c183b4d5942f09ab30d0dcdad4d6e4df889023d">Import downloads validate every redirect hop instead of only the URL they were given</a>. Thanks to RandomGenerator and xet7.</summary>FollowBleed is a bypass of the fix
that closed LiveBleed /
CVE-2026-30844. The live Trello import did validate the attachment URL with
validateAttachmentUrl() — and then downloaded it with the platform fetch(),
which follows redirects. So the guard only ever saw the request, and the
target gets to answer:
http://<public-host>/attachment.txt on a Trello cardvalidateAttachmentUrl() resolves it, sees a public IP, allows it302 Location: http://127.0.0.1:18080/secretfetch() follows, and the loopback body is stored as the imported
attachment, readable back through WeKanThat is non-blind SSRF against loopback services, internal admin panels, cloud metadata and anything else reachable from the container — the exact thing the validation was added to stop, reached through the response instead of the request.
A guard on the URL alone cannot hold, so fetchSafe() guards every hop.
maxRedirects (default 0) is how many redirects a caller is willing to follow.
0 keeps the old behaviour of refusing any 3xx outright, which is right for
outgoing webhooks and avatar downloads, because a legitimate one never
redirects. A caller that must follow one passes a small number, and each hop
goes through the same protocol allowlist, blocked-range check and DNS pinning as
the original URL before a packet is sent to it. Credentials are dropped on a
cross-origin redirect, so following Trello's 302 to S3 cannot hand the API key
and token to whoever the redirect names.
Refusing every redirect was not an option: Trello's own attachment endpoint answers with a 302 to a signed S3 URL, so that would have meant importing no attachments at all.
The offline importers had the same hole and were not in the report. They
handed the validated URL to Attachments.loadAsync(), and Meteor-Files
downloads with the platform fetch() too, so a pasted Trello or WeKan board
export reached 127.0.0.1 by exactly the same 302. They download through the
guard now, and store the bytes with the same call they already used for an
attachment that arrived inline.
tests/followbleed.test.cjs replays the reported attack against a stubbed
transport and asserts the second hop is never sent, then pins the rest: a
redirect to a hostname resolving to a private IP, to metadata, to a non-http
scheme, a relative Location, the chain limit, credential stripping across
origins, 303/307 method handling — and that a legitimate public-to-public
redirect is still followed with each hop pinned, because the import has to keep
working.
TransitBleed. isIpBlocked() is
the one block-list behind both halves of the SSRF defence — the input-time
validator and the delivery-time guard — and its IPv6 half classified an address
by its spelling: startsWith('::ffff:'), startsWith('2001:db8'), and the
first hextet parsed out of the string.
IPv6 has several standard ways to write "this packet goes to an IPv4 address",
and none of them looks like ::ffff::
2002:a9fe:a9fe:: — 6to4 (RFC 3056) → 169.254.169.25464:ff9b::c0a8:101 — NAT64 (RFC 6052) → 192.168.1.12001:0:… — Teredo (RFC 4380), the IPv4 stored as the complement of the low
32 bits0:0:0:0:0:ffff:7f00:1 — IPv4-mapped, merely spelled out → 127.0.0.1On a host with a 6to4 relay or a NAT64 gateway — ordinary in cloud and
Kubernetes networks — the packet arrives at that IPv4 address. So
http://[2002:a9fe:a9fe::]/latest/meta-data/ read cloud metadata straight
through the guard whose whole job was to stop it.
An address is expanded to its 16 bytes once and every check reads those
bytes, so notation cannot change the answer, and every transition form has its
embedded IPv4 extracted and re-checked with the IPv4 rules: 6to4, NAT64 (the
well-known prefix and the RFC 8215 local-use one), Teredo through both its
server and its obfuscated client address, IPv4-mapped, IPv4-translated,
IPv4-compatible, and ISATAP under any routing prefix rather than only the
link-local one. The deprecated fec0::/10 site-local range is blocked too.
A transition address wrapping a public IPv4 is still allowed, and
tests/transitbleed.test.cjs pins that as carefully as it pins the bypasses: a
guard that blocks everything is a guard somebody switches off.
and has the following developer-tooling fix:
<details> <summary><a href="https://github.com/wekan/wekan/commit/6cd64284dce838fea38dc967a5729246761bc89f">The Sandstorm bridge guard reads the page at the path it moved to</a>. Thanks to xet7.</summary>The documentation reorganisation turned
docs/Platforms/FOSS/Container/Sandstorm from a page into a directory, and this
test reads that page at run time to pin what it documents. So it did not merely
go stale: fs.readFileSync on a directory throws EISDIR, and the suite died
before its assertions ran.
It reads the directory's landing page, README.md, which is where the
migration-bridge documentation ended up. Both assertions are unchanged and both
pass.
and reorganises the documentation:
The Platforms docs - how the pages are arranged, and what points at them.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f628a698890dcbbd965ddee7b3b164523a3acf4a">Every platform page sits under what it is - an OS, container, cloud, hardware or package</a>. Thanks to xet7.</summary>docs/Platforms grew a page at a time, so what a reader met was a flat list:
Android.md, Debian.md, FreeBSD.md and SmartOS.md next to Snap/,
Docker/ and Sandstorm/, next to ppc.md, s390x.md and RaspberryPi/,
with Propietary/Cloud/ holding both rented machines and one-click hosting
services. Nothing said which was which, and the list only ever got longer.
The two halves keep their names and gain a middle layer that says what a thing
is. FOSS has OS, HW, Container, Cloud, Package and Source;
Propietary has OS, HW, Cloud and SaaS. So Debian is an OS, Snap and
Sandstorm are containers, Raspberry Pi is hardware, OpenShift and Helm are
cloud, and PikaPods, Cloudron, Scalingo, Heroku and Uberspace are SaaS rather
than being filed beside AWS and OVH.
A directory's index page is README.md, which is what GitHub renders when
somebody opens the directory, so a link points at the directory and never spells
out README.md. Docker/Docker.md, Snap/Snap.md, Sandstorm/Sandstorm.md
and the other pages named after their own directory became that README.md.
Cloud/OpenShift/ is the one exception, because it already had one.
The old FOSS/Platforms.md index is gone, the directory tree being the index
now, and the pages that linked to it point at docs/Platforms instead.
Two different things break when a documentation tree is rearranged. A link whose
target moved is the obvious one. The other is a link inside a page that
itself moved: Snap/CentOS-7.md went one directory deeper, so all ten of
its links out to Webserver/, Login/, Backup/ and Email/ needed another
../ even though nothing they point at had moved at all. Both kinds were
resolved from where each page used to live, across 213 files.
Not every reference is a markdown link, and those were followed too: the Snap
settings comments in the eight docker-compose*.yml files,
sandstorm-pkgdef.capnp, sandstorm-src/start.js,
server/methods/sandstormMigration.js, snap-src/bin/config,
releases/version.sh and the two ferretdb start-wekan scripts.
One of them is not a comment. tests/sandstormMigrationBridge.test.cjs reads
the Sandstorm page at run time to pin what it documents, so this move would
have failed the test suite rather than merely leaving a dead link behind.
43 links in five files named a path from the repository root, models/users.js
or client/lib/localStorageValidator.js, which resolves to nothing from the
page holding it. They carry the ../ that gets there now:
docs/DeveloperDocs/Directory-Structure.md has 9, where the other link on
each of those same lines was already correct, and the four
docs/Security/PerUserDataAudit2025-12-23/ files have 34.
Only paths that exist in the WeKan repository itself were repointed. FerretDB,
node, wekan-gantt-gpl and wekan-ondra sit inside the working copy but are
separate git repositories, so nothing resolves into them.
Third-party assets - what a reader's browser fetches when a page opens.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f628a698890dcbbd965ddee7b3b164523a3acf4a">Platform logos are stored beside their page instead of fetched from another host</a>. Thanks to xet7.</summary>A logo loaded from somebody else's server hands that server the IP address and user agent of everyone who opens the page. The deploy buttons were doing exactly that, from cdn.zenith.hosting, cdn.scalingo.com and www.herokucdn.com, as was the Sandstorm badge from img.shields.io and the MacStadium and WeKan logos from wekan.fi.
Each is stored next to the page that shows it now, named after its platform:
zenith.svg, scalingo.svg, heroku.png, sandstorm.svg,
MacStadium-developerlogo.png and wekan-logo.svg. PikaPods needed no
download at all, because pikapods.svg was already sitting in its own directory,
unused while the page fetched the same image over the network.
Screenshots are left as they are. This is about the logos, which are small, never change, and are fetched on every single visit to the page.
One was beyond saving: the chat badge at vanila.io answers with an HTML page
rather than an image, so it is left alone rather than replaced by a copy of
something that is already broken.
and adds the following new platform pages:
<details> <summary><a href="https://github.com/wekan/wekan/commit/f628a698890dcbbd965ddee7b3b164523a3acf4a">Packager.io and Zenith Hosting have a page of their own</a>. Thanks to xet7.</summary>Packager.io at FOSS/Package/ records the DEB/RPM build at
packager.io and says plainly that it does
not work yet, which is the useful part: the link exists, and a link that exists
invites the assumption that what is behind it works.
Zenith Hosting at Propietary/SaaS/ is one-click managed WeKan with
storage, backups, email and a free subdomain, and a share of every subscription
goes back to WeKan.
Thanks to above GitHub users for their contributions and translators for their translations.
In short: WeKan is built for every Linux platform its database is. The three architectures that were missing a Node.js - i386, armhf and loong64 - have one now, built by the wekan/node fork for the platforms nodejs.org and unofficial-builds do not publish, so they get a bundle, and i386 and armhf get a snap and a place in the multi-arch image as well. Below that: the snap build that could not finish on any of the FerretDB-only architectures, the All Boards left menu lying across the boards on a phone and its board counts landing at four different x positions, and a remote snap build that failed three times without ever saying why.
This release adds the following new features:
<details> <summary><a href="https://github.com/wekan/wekan/commit/12ae908ddfbcaa0bf59d7c5dc047ea77757bb264">Every Linux platform FerretDB builds for is built, with a Node.js to match</a>. Thanks to xet7.</summary>Every non-amd64, non-arm64 platform already used FerretDB v1 - MongoDB ships no server for any of them - but only three were built, because only three had a Node.js runtime anyone published. The wekan/node fork now builds the rest, so the release follows FerretDB's list instead of Node.js's.
New bundles: i386, armhf and loong64, beside the ppc64le, s390x and
riscv64 already there. i386 and armhf also become snaps and join the
multi-arch image (linux/386, linux/arm/v7); loong64 ships as a .zip
only, because it is not a snap architecture and buildx and the three registries
do not agree on it yet.
Where the Node.js comes from is resolved at build time rather than declared: nodejs.org, then unofficial-builds, then the fork, in that order, and the log says which one served. The first two ship a tarball; the fork ships the bare binary it built, because that is the only part missing - so when the fork serves, npm comes from the official amd64 tarball of the same version. npm is JavaScript and runs on whatever node executes it, so an npm built for one CPU drives a node built for another.
Three vocabularies meet in that matrix and they disagree. Node says x86
and armv7l where Debian, snap and FerretDB say i386 and armhf, and snap
says ppc64el where everyone else says ppc64le. Every row now names all
three, because a row that named one of them would download another CPU's binary
and nothing would notice until somebody ran it.
armel is the one FerretDB target deliberately left out: V8 has not supported ARMv5 for many years, so there is no runtime to put in the bundle and the fork cannot build one either. A bundle with nothing to run it is not a bundle.
The MongoDB Database Tools are per-tool tolerant now - wekan/mongo-tools does
not publish every architecture, and a missing mongodump is a missing
convenience rather than a broken bundle, since FerretDB is the database. It
removes the inherited amd64 tool instead of shipping it, because a tool for the
wrong CPU is worse than no tool. It is written up as
Platforms.
and fixes the following bugs:
The snap - the packages Launchpad builds for the arches with no runner.
<details> <summary><a href="https://github.com/wekan/wekan/commit/2d17d2627a2aef431b40e644805e9cf822d4f958">It builds again on every architecture that has no MongoDB server</a>. Thanks to xet7.</summary>The s390x snap died in the STAGE step of both its Launchpad builds, right after
"Staging mongodb", with IsADirectoryError on stage/bin. ppc64el and riscv64
take the same branch and would have died the same way.
The mongodb part's stage-packages unpack an Ubuntu 24.04 merged-/usr layout,
which leaves bin in the part as a symlink to usr/bin. On amd64 and arm64
the part then downloads MongoDB and copies its binaries in, which replaces that
symlink with a real directory. On the architectures MongoDB ships no server for,
the build exits before that and the symlink survives - and staging a symlink on
top of the real stage/bin an earlier part has already created is what failed
the whole snap, not just that part.
An override-stage turns a bin symlink into an empty real directory before
staging. It tests for a symlink specifically, so the architectures where mongod
really is there are untouched, and it removes before mkdir -p, because
mkdir -p follows a symlink and would have changed nothing.
All Boards - the page and its left menu, on a phone above all.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5b91c99b115c93c27fa2d257972a748ee45054e4">The left menu fits its column on a phone instead of lying over the boards</a>. Thanks to xet7.</summary>On a 375px phone the menu's column is capped at about 157px, and the menu kept the 260px width it carries so it can be dragged - so it lay across the board icons.
max-width: 100% did not fix it, and that is the part worth knowing: a grid
item's default min-width is auto, which is its content's intrinsic minimum,
and a minimum beats a maximum. The cap did nothing until the item was
allowed to shrink to it. min-width: 0 is the same pair the board column beside
it has carried all along, for the same reason. Nothing inside the menu needs the
intrinsic width held open - the workspace name already ellipses through its own
min-width: 0.
The node/ directory - a clone of the Node.js fork the runtime is built from -
joins the other local-only clones in .gitignore at the same time, so it stops
filling git status with 2.2G of untracked source.
Starred, Home, Templates and Archive have labels of four different lengths, and on a phone the count chip sat immediately after its label - so the four numbers landed at four different x positions and the column of counts read as ragged rather than as a column at all.
That packing was deliberate: the number beside the text rather than across a gap, on the grounds that a landscape phone's menu is wide enough for the gap to look like a mistake. It is reversed here, because the alignment is what a reader is actually using - the counts are compared with each other down the column, and four x positions is what stops that. The row keeps the same spread-apart layout at every width now, so every count sits at the end of its own row and they line up. The phone override is gone rather than re-tuned: there is no width at which the ragged version was wanted.
</details>and improves the following release tooling:
<details> <summary><a href="https://github.com/wekan/wekan/commit/8dc6bc7edd69fc44d0a084fe6d07c3799e1d28f2">A remote snap build that never starts now says why it did not</a>. Thanks to xet7.</summary>The riscv64 leg failed three times in sixteen minutes, and the whole of what it
printed was one line: Git operation failed with: Could not push 'HEAD' to
Launchpad. The build had not started - remote-build uploads the source to a
Launchpad git repository first, and it was the upload that failed - so there was
no Launchpad build log to print, which is what the job knew how to show.
snapcraft swallows git's own error, writes it to its own execution log, names that log's path in the output, and nothing reads it. So "rejected", "timeout", "auth" and "too big" all looked identical from the job log, which is why three runs narrowed nothing down. That log is printed now when an attempt fails.
The retry also clears snapcraft's local clone of the Launchpad repository before waiting. A retry that reuses a half-pushed one repeats the same failure, and three identical attempts sixteen minutes apart is what that looks like.
</details>Thanks to above GitHub users for their contributions and translators for their translations.
In short: a dependency release. Four updates arrive from dependabot, none of them in the shipped bundle: the two halves of typescript-eslint that let ESLint read and check TypeScript, the sinon test doubles the unit suites build their fakes from, and the docker/login-action step that signs in to the container registries when a release image is published. Nothing WeKan runs in a browser or on a server changes.
This release updates the following dependencies:
Thanks to above GitHub users for their contributions and translators for their translations.
In short: a large redesign of the pages you are always looking at. The
first header bar now says where you are, carries every control that used to
be scattered around the page, wraps instead of hiding what does not fit, and
holds a bookmarks menu - the star works on any page now, not only on a
board. All Boards gains a Home section for the board that opens after login,
an Archive in its left menu, a Table view, a heading naming the section you are
in, and an address for every section and workspace; the Admin Panel moves
under /admin with an address for every pane. The left menu those two pages
share folds away and is resized by dragging its inner edge, and the
workspaces in it are a real tree: drop one onto another to nest it, to any
depth, and fold a branch away with its caret. Public Boards becomes a
read-only page of its own, a swimlane, a list and a card can each be linked
to directly, and board roles are one capability table with a pane that shows
it. Below that: dependency updates, sixteen bug fixes -
the header bar's layout and where it starts, a filter that left a spinner
turning, a search that reached past your own boards, a left-menu caret that did
nothing when clicked - and the usual documentation and translation work.
This release adds the following new features:
The left menus - the one menu All Boards and the Admin Panel share.
<details> <summary><a href="https://github.com/wekan/wekan/commit/5df6f284212be9ec6070f8438dca78f0d1fa89d8">The left menu folds away, with the caret a list already has</a>. Thanks to xet7.</summary>At the top of both left menus - All Boards and the Admin Panel - there is now a caret that folds the menu away, and the same caret brings it back. It is the control a list has on a board: pointing down while the thing is open and right once it is folded, with the same two words in its tooltip, so there is nothing new to learn.
Folded, the menu is gone - no column, no strip, no narrow band of grey with a glyph in it - and the page beside it takes the whole width. A strip is still a column: it holds width, it keeps the page from starting at the window edge, and a caret alone in it is a target that has to be aimed at.
The way back is the pane title. Folded, the caret moves to the inline start of the heading beside it, and the caret and the title are one target: clicking anywhere on it brings the menu back. Nothing is lost by drawing nothing, because the way back is the largest thing on the page rather than the narrowest. The heading is drawn even for a pane that has no title of its own, so a folded menu can never be a menu you have lost.
One state for both pages: they draw one menu, and a reader who folds it away
on one of them has said what they want on the other. It is a Session value
first, so the fold is instant rather than waiting for a round trip, then
profile.leftMenuCollapsed on the user document so it survives a reload and
follows the reader to their other browser, and a cookie when nobody is signed in
Open is the default: a menu that remembered itself collapsed for somebody who has never collapsed one would be a page with no visible way to navigate.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/920d42b6db4c2df0d8302ee56b3d0d17fc40c3c3">Its width is dragged from its inner edge, the way the right sidebar's is</a>. Thanks to xet7.</summary>The menu's inner edge - the right one while reading left to right, the left one under a right-to-left language - now carries a grip, and dragging it changes the width. It is the same control the right board sidebar has on its own inner edge: the same strip, the same cursor, and the same sign flip on the drag so the widening direction mirrors under a right-to-left language rather than needing a second rule kept in step.
One number, on <html>. The width is a CSS custom property, and everything
that needs it reads that one: the Admin Panel's menu, the All Boards one - whose
grid track follows the menu - and the grip itself, which has to sit exactly on
the edge. An inline width on one element could not have done that, because the
menu is a different element in a different template on each page. A breakpoint
that wants a different default width overrides the variable rather than the
menu, or it would beat a dragged width at exactly that one screen size.
The grip is positioned against the row around the menu, not inside it: the menu is its own scroll area, and a handle within it would scroll away with the entries. Being positioned it is neither a flex item nor a grid item, so it adds no column and no gap - which is why the shared menu template can carry it and every Admin Panel pane gets it without naming it.
The width is remembered the same three ways the fold is: a Session value first,
then profile.leftMenuWidth on the user document, then a cookie when nobody is
signed in - the same cookie mechanism the fold uses rather than the localStorage
the right sidebar's width uses, so one reader's menu is not remembered in two
different places. It is saved once, when the drag ends; while dragging, the
width is written straight to the property, so the edge follows the pointer
without a database write per pixel. There is nothing to drag on a phone, where
the menu is full width above the content, or while it is folded, where there is
no edge.
Workspaces - the tree of folders for boards in the All Boards left menu.
<details> <summary><a href="https://github.com/wekan/wekan/commit/73a136bedd86fe3760ba707eca8612209a5e82ce">A workspace nests inside another one, and a caret folds the tree away</a>. Thanks to xet7.</summary>A workspace holds boards and, in the data, other workspaces - but a drag could only ever say one thing about them, and there was no way to fold a branch at all.
Where in a row a workspace is dropped is now what the drop means. The top quarter makes it that row's previous sibling, the bottom quarter its next sibling, and the middle half makes it that row's last child - a sub-workspace. The middle is the biggest target on purpose: reordering can also be reached by aiming at the neighbouring row's far edge, but nesting has only this one. Coming back up is a drop like any other - a child dragged onto a root row's edge is a root again - because nesting has to be undoable, or a workspace put one level too deep is stuck there.
The placeholder is a slot, not a line. While the pointer is over a row, an empty slot a row high opens above it, below it, or - for "into this one" - indented underneath it, which is exactly where the workspace will appear. A line between two rows is a target that has to be aimed at; a slot is a place to drop into. It opens as a pseudo-element of the row, so the rows below shift down and the row being aimed at stays where the pointer put it.
A workspace may not be dropped into itself or into its own descendant: the subtree would be cut off from the root, taking every workspace under it with it. That is refused while the workspace is still in the air, so the cursor says no rather than the drop landing and quietly doing nothing.
A caret folds a workspace's own workspaces away, at the start of the row - before the drag handle, so the carets of a tree line up whatever the drag-handles toggle says. It is the control a list has on a board and the one the left menu itself has, down to the two words in its tooltip, and it answers Enter and Space, because a tree that only opens with a mouse is a tree half the readers cannot open. A workspace with nothing under it keeps a spacer of the same width, so a row does not shift sideways the moment it gains its first child. Open is the default, and only the folded ones are stored - fifty workspaces with two folded is two keys - remembered in the same three layers as the rest of this menu: a Session value, the user's profile, and a cookie for a reader who is not signed in.
The depth is unlimited because nothing counts it: the menu draws itself again for a workspace's children, each level indenting by one caret's width with a logical property, so a right-to-left tree indents from the right by itself.
What a drag does to the tree is a pure module with its own tests - which third of a row the pointer is in, and the tree a move produces, guards included - so the rules are proved without a browser, and the page is pinned to calling them rather than working them out a second time. It is written up as Workspaces.
</details>All Boards - its sections, its controls, and what the page opens on.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a88dc8cbba676a7ef33ab076943f6dd5b57b9e05">Every section names itself at the top of its pane</a>. Thanks to xet7.</summary>The right pane began with board tiles and nothing said which list they were. The page is five lists of boards under one name - Starred, Remaining, Home, Templates, Archive, and a workspace - so the pane now opens with a heading naming the one you are looking at.
It is the Admin Panel's own paneTitle template with the same
.admin-pane-title class, so the two pages have one heading at one size and
colour rather than two written twice that drift apart. Only the space below it
is set here: the Admin Panel's own gap rule is scoped to its .main-body and
does not reach this page.
It is drawn once, above the view branch, since the board icons and the Table are two ways of showing the same section rather than two sections. Its words are the section's own title key - the same key the first header bar names the page with, and the same one the highlighted menu row carries - so all three say the same thing. A workspace shows its own name instead, untranslated: a workspace called "starred" is not the Starred section.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eea1f2ffed4af635c67659f176353de57b0d4040">Home, a section for the board that opens after login</a>. Thanks to xet7.</summary>One board can be Home: logging in opens it instead of the All Boards page. That has always existed, set from Multi-Selection's "Set as Home board (opened after login)", but there was nowhere that said which board it was - the setting was write-only, and the only way to find out what you had chosen was to log out.
Home is now a section of All Boards with a row in the left menu, a home icon and
the word Home, a count beside it like the other lists, and the address
/allboards/home. It sits under Starred and Remaining - the top row stays the
one the page opens on, since after login you are already in the Home board -
and the row is there whether or not a board is at it, because the place to drop
a board onto has to exist before there is anything in it.
Drop a board on the row to make it Home. The row is one more place in a column a board icon can already be dragged onto, so the gesture is the one already in the reader's hand; the alternative was three clicks through Multi-Selection. A drop replaces rather than toggling: Home holds one board, and a drop that sometimes set and sometimes cleared would depend on state the reader cannot see while dragging.
Home is a mark on a board, not a place boards are kept - like a star. The board stays in Remaining, or in its workspace, and appears in Home as well.
Taking a board off Home is the Android launcher's gesture. Pick the board up in Home and a Remove bar appears above the tiles - only while the board is actually in the air, because an affordance that shows up when the gesture is possible explains itself, and a trash can sitting permanently under somebody's boards is a button nobody dares press. Drag the board onto it and it turns red, let go and it asks, and the question says the board itself is not deleted. Every other target refuses the drop while the board is still in the air, so a board cannot leave Home by accident while you are filing it into a workspace.
The server accepts only a board the caller is a member of and that is not archived - a Home board that will not open would send that user to a board that refuses to draw at every login - and clears only the board that is actually theirs. Nothing automatic writes it; in particular Sandstorm's auto-open still persists nothing.
Documented in Home.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/50a1d4263e253b0771342e7469243b13eaf106d3">It opens on Starred, or on Remaining when nothing is starred</a>. Thanks to xet7.</summary>Starred was always the section All Boards landed on. On an account that has starred nothing that is an empty page with a full one behind it, which reads as WeKan having lost the boards rather than as a section nobody has filled in yet.
/ now opens Starred when the user has starred boards and Remaining when they
have none, and the left menu puts whichever one that is on top, so the
highlighted row is the first row. Only those two rows move — Templates and the
Archive keep their places.
The rule is one pure function pair in models/lib/allBoardsUrls.js, so the page
and its guard read the same one. The router no longer answers the question: it
runs before the user document has necessarily loaded, so it leaves / open and
the page decides — in an autorun, because on a cold load the user document lands
after the template is created and a single read would say "nothing is starred"
for everybody. It asks the user document's own starred list rather than the
query that counts the boards, whose answer depends on the subscription and would
draw Remaining and then jump to Starred mid-load. An address that does name a
section still wins, so this can never fight a row the reader has clicked.
The four menu rows became one row drawn once per section, since an order that depends on the user cannot be four copies of the same markup.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6d50d9d66">Boards in Archive moves to the left menu, and the Workspaces section gets its rules</a>. Thanks to xet7.</summary>Boards in Archive was a button in the first top header bar beside Sort, Search
and Multi-Selection. Those three act on the boards in front of you; Boards in
Archive is a place you go instead, so it is a row of the left menu now, under
Remaining, with a count beside it like the three lists above it — and it opens
as a SECTION of the All Boards page, drawn beside the menu rather than as a
full-width page that replaces it. Selecting something from a menu and then
losing the menu is a menu that throws itself away. Its address is
/allboards/archive, in the same shape as the other four sections.
The count is asked of the server rather than counted from what the page has: All Boards does not subscribe to archived boards at all, and the archive's own publication is paginated, so counting minimongo would answer 0 on a fresh load.
Boards can be dragged onto that row to archive them, from any of the four lists or from a workspace — the same drag the left menu already accepts for Remaining, and the alternative was three clicks through Multi-Selection. It asks before doing it, because a drop is easy to make by accident.
The Workspaces section has a rule above and below it. The left menu is three kinds of thing in one column — the three board lists, the workspaces tree, and the archive — and without them the tree ran into its neighbours as if it were more of the same list. They are a 2px dark grey line: a first attempt used the same near-white the menu's own edge uses and was too faint to separate anything.
The whole menu is styled like the Admin Panel's now — a panel with its own background, border and rounded corners, and a selected row filled with the per-user theme accent and white text — and it reaches the window's left and bottom edges the way that one does, instead of floating 14px in from an edge it is meant to look attached to. WeKan has one kind of left menu and it should look like one kind of left menu.
The click handler moved with the markup, because a Blaze event map only sees events inside its own template: one left behind in the header buttons would never fire and the row would silently do nothing, which is exactly what happened to this button once before. A guard now checks both halves of that.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a05c1ac08">Multi-Selection shows that it is on, and the Sort Boards popup gets a title</a>. Thanks to xet7.</summary>The Multi-Selection button in the first header bar looked identical whether or not a selection was running, so the one control that changes what clicking a board does gave no sign it had changed it — and the only way out was to find the row inside the sidebar. It is emphasised while it is on, says so in its label and its tooltip, and has an ✕ beside it to turn it off: the same pair the board's own Multi-Selection has.
The Sort Boards popup is titled "Sort Boards", from the key the app already has for that phrase. A title is what gives a pop-over its header, and the header is what carries the close button.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6051a4368">Its four controls stay in the header bar, and the hamburger that only led to them is gone</a>. Thanks to xet7.</summary>Sort, Search, Multi-Selection and Boards in Archive were rows of the right sidebar's home view, so reaching any of them meant opening a panel over the boards first. They are icons in the first top header bar now, left of the notification bell — exactly where a board's own controls are — each named by a tooltip.
That home view was the only thing the hamburger opened, so All Boards no longer offers one, nor the divider before it: a menu to reach what is already one click away is a step with nothing in it. The sidebar is still opened, by Search and by Multi-Selection, straight into their own view; and its home view is still what the back arrow of those views leads to. A board keeps its hamburger — what its sidebar holds, members and labels and activities and settings, is not in the bar and has nowhere else to be opened from.
Boards in Archive is drawn in both places now, and a Blaze event map only sees events inside its own template, so each copy has its own handler. A copy with markup and no map is a button that silently does nothing, which is what happened to that exact button once already; a guard now checks both.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/05e9cbc7c">One row of controls, in the header bar, and a Table view</a>. Thanks to xet7.</summary>The page carried two rows of controls: the second top header bar, holding only
the title, and a row of its own above the board icons — Multi-Selection with
its archive and duplicate actions, Sort and the search box — styled nothing
like the board header of the Swimlanes view. There is one row now, in the
header bar, with the same .board-header-btn buttons and the same Font Awesome
glyphs a board's header uses: Starred, Sort, Search, Multi-Selection and the
view menu. The actions ON a selection stay beside the boards they act on, and
appear only while something is selected.
Search is a field, not a button, and it does what the old right-pane search did: it filters as you type, across every one of your boards rather than the section the left menu has selected, and Escape clears it. That is a deliberate difference from the board header, whose Search is a button: on a board, Search opens a whole search view over cards; here it filters the list it sits above, and a filter belongs in the bar it filters.
The view menu names the current view — Lists or Table, never the words "Board
View" — the way the board header says "Swimlanes" or "Lists". Lists is the
default and is what an account that has never chosen sees. Table is the shared
table page, editable: Edit, Board title and Board description, ten rows a page,
the same boards the Lists view would draw. Edit opens the SAME
boardChangeTitlePopup the Swimlanes view opens rather than a copy of it, which
took one change to make true — its submit read Utils.getCurrentBoard(), and
on All Boards you are not looking at a board, so it now takes the board from
its own data context when it has one and falls back to the current board.
The view choice is remembered per browser, not on the user document: it is a
preference for one page and changes nothing anybody else can see. A board's
view IS on the profile, because it follows the user between devices; this
deliberately does not. The design is written down in
docs/Features/Page/All-Boards.md.
The first header bar - the strip always on screen: what it says and carries.
<details> <summary><a href="https://github.com/wekan/wekan/commit/1f2044219a0625a4d8d2de752dd4da2b4c5e5078">Starred: bookmarks to any page, not only to boards</a>. Thanks to xet7.</summary>The star group in the first header bar is a bookmarks menu now: the caret that opens the list, the count of what is in it, and the star that says whether the thing you are looking at is among them.
It held boards only. A board could be starred and reached from the dropdown; every other page - All Boards / Remaining, a workspace, Admin Panel / Settings / Version - could not, so the one control for "keep this where I can get at it" worked on one kind of destination and was simply absent on the rest, even though those pages have had their own addresses since the All Boards and Admin Panel URLs landed.
The star on any other page now stars that page. A bookmark is a relative URL
and the title from the browser tab: relative so it survives the site moving to
another host, and an absolute or protocol-relative URL is refused rather than
cleaned up, because what is stored goes into an href the reader clicks.
The browser tab says where you are - Product name - All Boards / Remaining
In All Boards / Starred the bookmarks are tiles beside the starred boards, with the white border the template-container tile carries, the theme's own colour behind them, and both the title and the URL - the title says where it goes, the address says what it is. Each tile has its own unstar button, since the star in the header bar stars the page you are on.
Dragging a tile past another reorders them, and that is the order of the header dropdown: the two are views of one array. The move matches both ends by URL rather than by index, because the two views are rendered separately and an index from one of them is a guess about the other. The list is capped at 50, oldest dropped, because it is a dropdown.
Documented in Starred.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f62258b5c">One star group, and every button whose icon does not say enough is named</a>. Thanks to xet7.</summary>The bar drew two stars in a row — one on the starred-boards dropdown and one for the board you are on — which read as a single control somehow drawn twice. The dropdown has no star now: it is a caret and the COUNT of starred boards, which is what it is about, and the board's own star sits immediately beside it. The two are wrapped in a rounded outline of their own, shaped like the phone/desktop toggle's but white where that one is black — the toggle is a white box on the bar, while these sit on the bar's own colour.
The count is shown even at zero, because it is now the button's only label; a bare caret says nothing about what it opens.
Clicking Filter or Search while the sidebar is already showing that view closes it. They only ever opened, so a second click did nothing visible and the only way back was the sidebar's own ✕, somewhere else on screen from the thing you just clicked. Filter has one exception — not while a filter is ON, because the sidebar is then the one place that says what is being hidden from the board, and closing it would leave a board showing a subset of its cards with nothing to say so. Search has no such case: its results are inside the panel.
The starred-boards popup has a title, "Starred Boards", and so has a header —
and the header is what carries the close button; without a title it rendered
with nothing to shut it but clicking away. The title reuses the key the app
already has for that phrase rather than a starredBoardsPopup-title of its own,
which would be a second copy of one phrase in all 147 language files, English in
every one at first.
Sort Cards, Filter, Search, Show Dependencies, the board's visibility (Private or Public), its watch level (Watching, Tracking or Muted), both Multi-Selection buttons and the two view menus carry their name beside the icon where the bar has room for it — a tooltip is the one place a name cannot be read without hovering. Below 1100px every one of those labels goes and they are icons again: a label is worth several icons' width, so on a narrow window keeping them costs more buttons off the first row than the names are worth. All of them together, not some, because half the buttons named and half not reads as a bar half finished — and which half you got would depend on which words happen to be short in your language. All Boards' four controls — Sort Boards, Search, Multi-Selection and Boards in Archive — are named the same way. Each label uses the same translation key as its own tooltip, so the two cannot say different things. Six view glyphs are six things to learn, a check-box outline says nothing about multi-selection, and a tooltip is the one place a name cannot be read without hovering. The bar wraps to a second row when it runs out of width, which is what makes the word affordable. Sort, Search and Boards in Archive stay icons — those glyphs are well known.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1164785d1">It names the All Boards list you are on, and the workspace inside it</a>. Thanks to xet7.</summary>The bar said "All Boards" on the Starred list, on Templates, on Remaining and
inside every workspace — it named the page and not the list you were looking
at. It shows the path now: All Boards / Starred,
All Boards / Workspaces / Engineering / Backend.
The section's words are the left menu's own key, so the title and the row highlighted beside it always say the same thing. A workspace's words are its NAME rather than the slug in the URL, and are printed as text rather than translated — a workspace name is what a person typed, and a workspace called "starred" is not the Starred section. A trail segment that names nothing stops the walk, so a stale link titles the part of the path that is still real instead of nothing at all.
The Admin Panel's Admin Panel / Settings / Version and this are one list of
segments rather than a helper each: the two pages do not have the same number
of them, and a workspace has as many as its tree is deep.
The bar named the panel and stopped there. The Admin Panel is four pages and each page is a stack of panes, so "Admin Panel" named the building and not the room - and all four of its routes answered the same three words, whichever one was open.
The title now names the same three things the address does. The page's words are the tab's own and the pane's words are the menu row's own, so the title, the tab that is marked active and the row that opened the pane cannot say different things about one place.
The button of the page you are ON also keeps the hover background, permanently and a shade darker. The tabs had marked themselves active all along, but the only rule that drew it was scoped to the second header bar - which those tabs no longer live in - so the state had been computed and invisible since they moved.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/28a5973c6b00d85a4c2a24e608e6dfd777a36df4">It starts where the left menus start</a>. Thanks to xet7.</summary>The All Boards and Admin Panel left menus indent a row by 4px of row margin plus 18px of link padding, so a row's icon lands 22px from the window edge. The first header bar sat flush against the edge, so its home icon started 10px in and every button under it was a little to the left of the menu below.
The bar now carries a 12px inline gutter: 12 + 4 + 6 = 22, the same x as the menu rows. It is on the BAR rather than on its first item because the bar wraps
The Admin Panel - where the settings live, and how they are addressed.
<details> <summary><a href="https://github.com/wekan/wekan/commit/03cb79bdd">It moves under /admin, and its address names the pane you are on</a>. Thanks to xet7.</summary>Every left-menu entry of the Admin Panel had a URL, but the four pages sat at
the TOP level - /settings, /people, /attachments - as if they were pages
of the app rather than of the Admin Panel, and /attachments is also the path
the file server serves attachments from, so the panel and the files were
claiming one address. They are under /admin now:
/admin/settings/version, /admin/people/login, /admin/problems/database,
/admin/attachments/backup.
The DEFAULT pane is named too. It used to be left implicit in a bare page URL -
/settings rather than /settings/version - so the address of "Settings" and
the address of "Settings showing Version" were one string. The address is meant
to say where you are, and the first pane is somewhere too. The bare
/admin/settings still resolves; it redirects to /admin/settings/version
rather than being a second name for it.
Every path the panel used to answer on redirects, and a bookmarked
/settings/global-webhooks keeps its pane rather than landing on the top of
the panel. The redirects are built from the same map the URLs are, so a page
cannot be given an address without also being given its redirect.
Board views - the board itself: its swimlanes, lists and cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/e755b60b3">A swimlane, a list and a card can each be linked, and the link lands on the thing it names</a>. Thanks to xet7.</summary>A card has had an address since there has been a card route. A swimlane and a
list had none, so "the Backlog list of this board" could only be sent as "open
this board and scroll down" — and List.absoluteUrl() answered with the URL of
whichever CARD the cache returned first for that list, so the one place that did
offer a list link showed a card's address, and an empty box for an empty list.
Both have their own now — /b/<board>/<slug>/swimlane/<id> and
/b/<board>/<slug>/list/<id>. Five segments against the card route's four,
which is what keeps the three apart: a card URL cannot match these and these
cannot match a card.
Following one brings the thing into view. The route cannot scroll — it runs before the board has rendered, and on a board that is already open it runs without re-creating anything — so it names what to reveal and the board body reveals it once the element exists, waiting for it rather than assuming it, because a large board renders in more than one pass. It gives up after a few seconds instead of spinning: a link to a list that was archived names an element that is never going to exist, and the board is still the right place to have landed. The revealed swimlane or list gets a brief outline, because a scroll that lands mid-board gives no sign of which of the things now on screen the link was about.
All three are copied the same way, from the first row of the hamburger menu, with the link icon and the name beside it. The card carried this as an icon in its title header named only by a tooltip — the one place a name cannot be read without hovering — and that button is gone, along with its handler and the "Copied" tooltip only it used.
The copy row sits above every permission check in those menus: copying an address is reading, not editing, and somebody who may only read the board can still tell a colleague which list they mean.
</details>Public Boards - the page a visitor sees without an account.
<details> <summary><a href="https://github.com/wekan/wekan/commit/54915db25">Its own read-only table page, not All Boards with a different query</a>. Thanks to xet7.</summary>/public rendered the All Boards page with its query swapped for { permission: 'public' }, which brought the whole of All Boards with it: the Starred /
Templates / Remaining menu counting the user's own boards beside a grid that
was not, the workspaces tree, the org and team filters, Multi-Selection with its
archive and duplicate actions, the sort popup, board dragging, and an "Add
board" tile that made a private board from a page about public ones.
Multi-Selection offered to archive boards the visitor has no rights to at all.
It is only the table now: board title and board description, ten rows a page, paged and counted on the server. No left menu, no create, no selection, no drag, and nothing on the page changes anything — a row's only action is to open its board. The rows carry their board's colour and background image, so a board is recognised here the way it is on All Boards.
A page costs six fields for ten boards, not ten board documents: the two
columns, the slug the link needs, and the two the row is coloured with.
members is deliberately absent — it is the largest field on a busy board and
this page shows no avatars. The selector is built on the server and takes
nothing from the client: public, not archived, a real board rather than a
template container, and not an internal ^Subtasks^ board.
Not carried over from the board tile: member avatars, the per-list card counts and the spent-time clock. What a visitor needs of a board they do not belong to is what it is called and what it is for; the rest is the inside of a board they have not opened, and each costs a query the page would otherwise not make — the clock answers by looking for cards, which this page does not publish, so it would read false for every board on every instance.
The design is docs/Features/Page/Public.md, which describes only what is different about this page and links back to the shared Table page design for everything else.
Two things it got wrong on the way, both reported by xet7: the page drew its
own "Public boards" heading
under a header bar that already said "Public", and a row's text was white on
white — the row was given a
board-colour class, but boardColors.css styles .board-list .board-color-X a
and nothing on this page matches that selector. The seventeen colour rules name
the public row too now, and a board with no colour set falls back to a readable
default instead of to the page's own background.
Board roles - what a member of a board may do.
<details> <summary><a href="https://github.com/wekan/wekan/commit/efa534fdd">One capability table, three roles fixed, and a Roles Status pane that shows it</a>. Thanks to xet7.</summary>What each role may do was spelled out THREE times — in the server allow helpers,
in the client's canModify* helpers, and in prose in the docs — and the three
had drifted apart. Every place they disagreed was a role that did not do what
its name says. models/lib/boardRoleCapabilities.js is that table now, in code,
and everything reads it: the allow rules, the UI helpers, the new Admin Panel
pane and the documentation.
"Comment only, assigned" had full write access. Nothing outside the card publications read its flag and it was not in the write rule, so the role could create and edit cards, lists and checklists — it was "Normal, assigned only" under another name. It is comment-only now, like the role it is named after.
"No comments" could not write anything. The write rule excluded it, so the role blocked editing as well as commenting — a second read-only role under a name that says otherwise, and one the UI still offered the edit buttons for. It blocks commenting only now.
The write rule did not exempt board admins. Every other helper ignores a
flag on an admin; that one read the raw flags, so an admin who also carried
isNoComments silently lost write access. Not reachable from the Web UI, which
writes all eight flags at once, but reachable over the REST API.
The UI helpers were part of the same drift — canModifyCard() did not exclude
isNoComments while the server did, and canModifyBoard() excluded neither
isNoComments nor isWorker — so each disagreement was a button offered to
somebody whose write the server then refused.
A fourth gap was found and is NOT fixed: a Worker cannot move a card, which is the one thing the role is for. Moving a card is a card update, so it goes through the write rule, which excludes Worker. The fix means letting a role write some fields of a card and not others, and validating that a member change only ever adds the caller — a field-level policy on the path every card update takes, which wants deciding on purpose. It is recorded under "Known gaps" with what it needs.
Roles Status, at Admin Panel / People / Roles below the Save button: a read-only table of what each role may do. It is the shared table page, with no markup of its own, no interactive rows and nothing editable, because a role's capabilities are a property of the code and not a setting. Every string is a translation key, the Yes/No of each cell included. The "Invite to board" column reads the pane's working copy, so the table follows the checkboxes above the Save button as they are ticked, before saving.
</details>and reorganises the following in the user interface:
All Boards - where its controls live, and what the page shows around them.
<details> <summary><a href="https://github.com/wekan/wekan/commit/f56254e71">The actions on a selection move into the header bar with everything else</a>. Thanks to xet7.</summary>The header bar took the controls of the page, but the actions ON a selection —
Move Board to Archive, Duplicate Board, and the "Selected:" star and home —
were left where they were, in a strip above the board icons, on the reasoning
that they are about those boards rather than about the page. That still left
two places to look for a button. They are in the header bar now, to the right
of the controls that are always there, as .board-header-btn like every
control beside them, and .boards-path-header holds the section title and
nothing else.
They appear only while something is selected: four buttons that would do nothing are worse than no buttons. Archive and duplicate carry their names; star and home stay icon-only under the "Selected:" label that already named them, because their names are sentences — "Set as Home board (opened after login)" — that belong in a tooltip, and spelling them out pushes the bar onto a second row. The label stays a label, with neither the button class nor any button behaviour.
Two things had to move with the buttons, because Blaze binds both to a single
template: their four click handlers, which an event map on boardList could no
longer see, and hasBoardsSelected, which decides whether they are drawn. The
stylesheet lost .path-right, .selected-action and .selected-actions,
which now select nothing, and the phone media query lost the four rules that
flattened a controls row this page no longer has.
Starred was the first button in the header bar, and Starred is a section:
the left menu already lists it beside Templates and Remaining, counts it, and
highlights it when it is the one shown. A second way to reach one section, one
click away from the first, is a control whose only job is to be kept in step
with the menu — so it is gone, and with it the header bar's own
js-select-menu handler and isSelectedMenu helper, which had no other caller
there. The left menu is part of boardList and has always had its own.
Multi-Selection moved to the right of the Lists/Table menu. The bar now reads left to right as what is shown — Sort, Search, Lists/Table — and then what is selected: Multi-Selection, followed by the archive, duplicate, star and home actions that appear with it.
The search box is 150px wide, half of the 300px it was drawn at when it had a
card of its own. Getting there turned up that it had no styling at all: its
rules said .boards-path-header .board-search, the bar it used to live in, so
from the moment the controls moved to the header bar they matched nothing and
the box rendered at the browser's default input size. They are
.all-boards-controls … now — and because a white box on a themed bar cannot
inherit that bar's light-on-dark colour without putting white text in a white
box, the input, the magnifier and the ✕ each set their own.
The "Selected: ★" button only ever added stars: it walked the selection and starred whatever was not starred yet. Once every selected board was starred it did nothing at all, so there was no way to undo from here what it had just done, and its tooltip read "Star the selected boards" whatever state the selection was in. It is a toggle now — none starred stars them all, all starred unstars them all, and a mixed selection stars the rest, leaving the starred alone. The mixed case deliberately does not flip each board independently: one click that starred some boards and un-starred others is not something a button may do. Only the boards that must change are called, because the server method flips one board and calling it for an already-starred one would un-star it.
The tooltip says which way the button goes right now, set-selected-starred or
the new set-selected-unstarred, and it reads the SAME function the click does
— two copies of "which way does this button go" would eventually be two
answers, and the tooltip would lie about what the click does. The rule itself
is models/lib/selectedStars.js: pure, so it is unit-tested rather than
reasoned about.
All four buttons on a selection are icon-only now, and they follow the "Selected:" label in the order star, home, archive, duplicate — the two that only mark a board first, the two that change what boards exist last. Archive and duplicate carried their names, which are sentences and pushed the bar onto a second row.
Checking that each button says what it is turned up one that did not: the ✕
that turns Multi-Selection off said "Clear filter", which is what the other ✕
in this bar does, in the search box. It is multi-selection-off. A guard now
requires every .board-header-btn in the bar to carry a title, and every
title to come out of a translation key rather than being literal English.
Both new keys are in all 147 language files as English placeholders, which is what the translation policy does with a string that is untranslated everywhere, so no language silently loses a tooltip.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fcfed1fd3">Search and Multi-Selection are the board header's own controls, opening a right sidebar</a>. Thanks to xet7.</summary>All Boards had a search field in its header bar, and a Multi-Selection
button whose actions were four icons beside it. A board has a search button
and a Multi-Selection button, and both open the right sidebar. Two pages, the
same two control names in the same place, behaving differently — and one of them
not the way the rest of WeKan does. They are the board's now, and they are the
same markup: headerBarControls.jade holds one headerSearchButton and one
headerMultiSelectionButton, included by the board header of the Swimlanes view
and by the All Boards bar. The two copies had already drifted — the ✕ that turns
Multi-Selection off said "Clear filter" on the board header, which is what a
different control does, and says multi-selection-off in both now.
Only the markup is shared. What a click does is not — a board searches and
selects cards, All Boards searches and selects boards — and it does not need to
be: a Blaze event map catches events from the templates rendered inside it, so
each bar's own map sees the clicks on its own copy. isActive is passed in,
because the two pages keep different selection objects.
All Boards has a right sidebar of its own. Not the board one: that is built
around a board's members, labels, activities and settings, and this page has no
board. It borrows the shell — the same .board-sidebar.sidebar classes, the
same ✕, the same title-and-back-arrow above a view — so the two look and behave
alike, and it has three views. home, what the hamburger opens, is the page's
menu: Search, Multi-Selection, and Boards in Archive, which had a handler in the
header bar and no way to reach it. search is the field that was in the bar,
still writing the page's own search term so the boards behind it narrow as you
type. multiselection is where the actions on a selection went — star, home,
archive, duplicate and a way out — each with its name beside it instead of being
a crowded icon.
The bar also gained the divider and the hamburger, in their own flex item and last in the source exactly as the board header has them, which is what keeps the hamburger in the top right on a phone while the other buttons wrap.
Two things this turned up. A .jade file is not picked up by being on disk — it
has to be imported from client/features/, and the shared controls threw "No
such template: headerSearchButton" on render until they were; a guard now
requires every .jade under client/components to be imported and every
+template it includes to exist, and it found a dangling +subtaskDeleteDialog
that has never had a template (unreachable, so it has never thrown, and it is
recorded with that reason rather than hidden). And deriving a template name from
a view name gave allBoardsMultiselectionSidebar for
allBoardsMultiSelectionSidebar — one letter, renders nothing, no error worth
the name — so the names are an explicit map the guard can check.
The designs are Search and Multi-Selection, one per shared control, each covering both pages.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/57dd62bad">The white bar above the board icons is gone</a>. Thanks to xet7.</summary>Once every control had moved to the header bar, what was left above "+ Add Board" was a white strip carrying one thing: a Font Awesome icon for whichever left-menu section is selected. Three things already say which section that is — the left menu highlights it, the header bar names the page, and the Starred control is emphasised while Starred is on — so the strip said nothing and cost a bar's worth of height on every screen. The board icons start at the top of the right column now.
It took more with it than the markup. currentMenuPath, sixty-four lines that
resolved a workspace path to an icon and a name, had no other caller. Every
.boards-path-header rule went — the seventeen that styled the bar and its
contents, and the phone media query that had already been reduced to holding
the title. So did the pulse keyframes, whose only user was the
multi-selection hint that lived in the bar. The pager's flex: 0 0 auto was
sharing a selector list with the bar, and is kept on its own: it is still the
fixed-height thing above the scrolling list.
/allboards/starred, /allboards/templates, /allboards/remaining,
/allboards/workspaces, and a workspace as deep as its tree goes:
/allboards/workspaces/engineering/front-end/design-system. The page was three
addresses and the workspaces tree had none at all — which workspace you had open
was a ReactiveVar, so it could not be linked or bookmarked either.
A workspace is addressed by the slugs of its names, not by its id: the id is
a random string and a URL should say where you are. The slugs come from
getSlug (limax), the same function that gives a board its slug, so a workspace
and a board turn a name into a URL the same way — including the scripts where a
naive slugifier returns an empty string. When a name slugifies to nothing anyway
— an emoji-only name — the node's id stands in, because a workspace with no
address could not be opened from a link.
The route resolves the section and splits the path; the PAGE resolves the
workspace, in an autorun, once its tree has loaded. The router cannot: the tree
is on the user document, which it has no way to read before the page has it —
and a one-shot read would always run before the tree arrived and never select
anything. / stays the home and still shows Starred; /templates and
/remaining redirect to the new form.
One thing this had to fix rather than add: the page filters boards by membership only on the All Boards routes, by route name, and a route missing from that list falls through to the public-boards branch — it would have shown public boards instead of your own.
</details>The Admin Panel - its own addresses.
<details> <summary><a href="https://github.com/wekan/wekan/commit/841479774">A URL for every left-menu entry</a>. Thanks to xet7.</summary>The panel was four addresses — /setting, /people, /admin-reports,
/attachments — each opening whichever pane its page happened to open first.
Which pane you were looking at was ReactiveVar state and nothing else, so a pane
could not be linked to a colleague, bookmarked, opened in a second tab or
reached with the back button, and /setting always landed on Version even if
you had just been in Global Webhooks.
Every entry has its own now: /settings/visibility,
/settings/global-webhooks, /people/roles, /admin-reports/cpu,
/attachments/s3. Lowercase, words separated by -, and the default pane
keeps the bare page URL — /settings, not /settings/version — so there is one
address for "the Settings page" rather than two that show the same thing. The
Settings path is plural: /setting was the odd one out beside /people,
/attachments and /admin-reports, and it still resolves, as a redirect.
The slug is not derived from the pane id. The ids are internal and read like
it — tableVisibilityMode-setting, layout-setting, report-cpu — while a URL
is something a person types and pastes into a chat, and a name derived from
another name is wrong the moment the two spellings differ. So it is an explicit
map, and the guard checks it against the real menus in BOTH directions: every
slug names a pane the page has, and every menu entry has a slug. Neither failure
shows up until somebody clicks that row. A slug that is not one falls back to
the page's default rather than rendering an empty panel, because a URL is typed.
/information and /translation redirected to /setting and handed their pane
over in a Session value the page consumed once. They redirect to the pane's
own address now.
The first header bar - what it stopped carrying.
<details> <summary><a href="https://github.com/wekan/wekan/commit/2f0445102">The 100% zoom control is gone</a>. Thanks to xet7.</summary>It scaled the board with a CSS transform, it did not work, and there is no plan to fix it — WeKan already has a font-size setting, which is what the control was reached for anyway. A control that does nothing is worse than no control.
What went with it: the pill and its number, the helper and three handlers, the
zoom utilities and their call sites, the profile.zoomLevel schema field and
its setter and server method, one translation string, and 57 stylesheet rules
spread over four files — which is how much of the stylesheet a broken feature
had accumulated. The card zoom is a different feature and is untouched.
Member Settings - the per-user panes.
<details> <summary><a href="https://github.com/wekan/wekan/commit/472ed566c8a86b46be41de33a3ad3902c14c5edb">Change Password's button is themed, like the Save button beside it</a>. Thanks to xet7.</summary>Change Password draws the useraccounts form, and its submit button carries that package's own classes rather than any of ours - so it fell back to the plain grey button, while the Save button one entry above it in the same menu was painted with the theme accent. Two buttons, one menu, two looks.
It is named in the same rules as every other primary button rather than given a copy of them, so the accent and the hover and active states keep one home, and it is scoped to a popup: the login page styles that form its own way and is not what this is about.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/95b0acd0b">Change Color shows as many swatch columns as the width allows</a>. Thanks to xet7.</summary>The swatch list is shared with the board-background picker, where it is a float-based two-column grid. Two columns is right for background thumbnails; for Select Color it meant Flat, Clear, Dark and Special each ran down the popup in a narrow pair, and most of them were below the fold however wide the browser was.
Auto-filling columns instead — the same answer the Change Language popup already uses. It takes as many columns as fit and collapses to one on a narrow window, so no media query is needed and a phone is unaffected. Both Change Color popups, Member Settings and Board Settings, get more width on desktop to spend on columns; below 800px every popup is a full-screen sheet and is left alone. The width is mirrored in the popup positioning code, which clamps a popup into the viewport by its width — computed for the default it placed a wide popup opened near the right edge with a third of itself off the screen.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/15157cde0">Change Settings: the button is Save, and it has a theme</a>. Thanks to xet7.</summary>It said "Apply" while every other settings form in WeKan says Save, and it was
pure black. Both came from one line: the submit carried no primary class, so
it fell to the base button rule — whose fallback is black — instead of the
primary rule beside it, which is the themed one. The Change Language form
directly above it in the same popup is the shape it now matches.
and updates the following dependencies:
npm-packages/meteor-node-stubs.
Update. Thanks to xet7.archiver and unzipper also move into
alphabetical order in package.json, which changes nothing that is
installed. Update. Thanks
to developers of dependencies and xet7.and fixes the following bugs:
The first header bar - how it lays itself out, and what sits under it.
<details> <summary><a href="https://github.com/wekan/wekan/commit/551a44d769652a26d86c1b0d9603f692dd370a7c">The Notifications panel starts below the bar, so the avatar is not over it</a>. Thanks to xet7.</summary>The panel is fixed at 48px from the top - a guess at the height of one header bar. The bar wraps to a second and a third row, and the user avatar is the item that wraps last: on a window where it did, the panel covered the row the avatar was on, and the avatar - which is inside a bar that paints above it - landed straight on the panel's own header, beside the ✕ that closes it.
It starts at the height the header MEASURES itself to be now, kept current by a
ResizeObserver because a bar re-wrapping is not a window resize. The panel's
own fixed header - the row the ✕ is in - follows the same number, and so does
the height it may take: the 100vh - 28px - 36px it had was the same guess
written as a subtraction in two pieces. That measurement is what everything else
laid out against the viewport already uses, so this is the panel joining them
rather than a new mechanism.
The house at the start of the bar sat further in on a wide window than on a narrow one, and on none of them on the line the left menu's rows below it start on.
Its inset is a SUM, and two of the terms were added at some widths only. The
header carried side padding of its own - 8px below 800px, 16px between 768 and
1024, none between 1024 and 1920, 8px above 1920: four widths, four insets. And
.allBoards is the SAME element as the house's own .home-icon, so its side
padding lands between that icon's margin and the link - and it was 15px on a
desktop against 6px on a phone.
Neither adds anything sideways now, and the link's own start padding is the same 6px in every rule, phone rules included: a bigger tap target on a small screen grows at the END, not at the inset. So the house is at 12px of the bar's own gutter plus 4px of the icon's margin plus 6px of the link's padding = 22px at every width - the same 22px the left menu's rows are indented by, so the two are one line down the page.
The existing guard added up the three terms it knew about, got 22, and passed while the house still moved. The new one pins the other side of it: no rule, at any width, may add a side inset of its own.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9a1463d7e">It fills each row before starting the next one</a>. Thanks to xet7.</summary>The bar wraps when its buttons do not fit, but everything after the drag-handles toggle was wrapped in one group — and a nested flex box is a single item to the bar, so the whole group moved to the second row together. The first row ended halfway across with nothing in the rest of it while the second row was crowded.
The group generates no box now, so its buttons are items of the bar itself and wrap one at a time: the second row takes only what did not fit on the first. The push that keeps them at the end of the bar moves to the group's first child, and on a row that wrapped there is no free space for it to absorb, so those items pack from the start — which is what fills the row rather than stranding it.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/49f4e77aa">It wraps to a second row instead of hiding the buttons that do not fit</a>. Thanks to xet7.</summary>The bar was one row with overflow: hidden, so a button that did not fit was
not drawn at all - and a button that is not drawn gives no sign that it exists.
It wraps now, and every height on it is a minimum, including the four phone
rules that pinned 40px or 48px: a fixed height cannot hold two rows, so it
would have cut off exactly what the wrap was for. A phone is where the buttons
run out of room first, which makes it the last place that should hide them.
Everything after the drag-handles toggle hugs the end of the bar from one
margin-inline-start: auto, a LOGICAL property, so a right-to-left language
mirrors it by itself rather than needing a second rule kept in step.
Both view menus lost their visible labels - the board's said "Swimlanes" and All Boards' said "Lists" - and name their view in a tooltip instead, where the other buttons of that bar already keep theirs. A divider after the notification bell separates what belongs to the page from what belongs to you.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/49f4e77aa">The right sidebar starts below it again, on every page that has no second bar</a>. Thanks to xet7.</summary>--wekan-header-height is what anything laid out against the viewport starts
below, and it measured #header - the SECOND header bar - from when every page
had one. Most pages have none now, their title being in the first bar and their
controls in a sidebar, so on those the variable was 0 and the All Boards
sidebar covered the bar above it.
It measures both bars, as the bottom of the lowest one rather than a sum of heights, so any margin between them counts and a bar that is absent contributes nothing without needing a special case. Each bar is watched by its own ResizeObserver, because the first one wrapping to a second row is a resize of that element and of nothing else.
Sidebar buttons are no longer drawn under the close button either: the ✕ is positioned absolutely, so it contributes no height and the row holding it collapsed to its padding.
</details>All Boards - the overview and its search.
<details> <summary><a href="https://github.com/wekan/wekan/commit/b50ccc63a83ac2185b3bdc2605adde9f955a685a">The selected workspace shows its board count again</a>. Thanks to xet7.</summary>The count was there, at the right of the ⋯ menu where it belongs, and it was invisible.
The theme accent fills the icon and the name of the selected row, not the whole row: the menu button and the count sit after it, on the panel's own light grey. A rule gave the count a light pill with white text "for contrast against the filled row" - but the count is not on the filled row, so it was white on light grey. The selected workspace was the one row whose board count could not be read, and it is the row you have just asked which boards are in.
The rule is gone rather than re-tinted, because there is no accent behind the count to contrast with: it keeps the same grey pill every other row has. The count and the menu button also hold their size now, so a long workspace name ellipses itself instead of squeezing them off the row.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c7bf2958a36f4ac120681ee435a564dd74d09517">A workspace row obeys the drag-handles toggle, and drags without one</a>. Thanks to xet7.</summary>The workspace rows in the left menu drew a ✥ handle whatever the Show desktop drag handles toggle in the first header bar said - and the handle was decoration: the whole ROW was draggable, so the drag started anywhere on it. A row that is draggable as a whole starts a drag on the way to a click, and a workspace row is a row you click, because that is how a workspace is opened.
The handle follows the toggle now, through the same helper the board tiles beside it use, so the two cannot drift apart. With handles on, the handle is drawn and is the only place a workspace drag may start. With handles off there is no handle and the workspace's icon and name is what reorders it.
draggable lives on the handle or on that icon-and-name anchor, never on the
row - the row also holds the ⋯ menu and the board count, and a drag started on
those is a drag of something else - and the dragstart handler stays on the row
because the event bubbles up from whichever child started it, so the reordering
itself is untouched. Clicking the anchor still opens the workspace: a click and
a drag are two gestures on one element, as they are on a board tile with handles
off.
Two things make that drag real rather than declared, and both are easy to leave
out. The anchor is opted out of the page-level dragscroll, which would
otherwise take the mousedown so the drag never begins - which is exactly what
"reordering does not work" looks like. And it carries user-select: none while
it is the drag source, because the name is TEXT: a press-and-move over
selectable text starts a selection, and the browser owns the gesture from
there. Everything that changes with the toggle follows the draggable attribute
itself rather than a second class, so there is one answer to "is this the drag
source right now" instead of two that can disagree.
The Lists/Table dropdown in the first header bar opened a bare list of two links, while the board's Swimlanes/Lists dropdown - which asks the same question about the same kind of page - opens with Board View above it and a close ✕ beside it. A popup with no title renders no header at all, so the two read as two different kinds of control.
It is titled now, with the BOARD's own translation. The convention is
<popupName>-title, which here would mean a second key saying the same two
words - and a new key starts as English in all 142 language files, so most
languages would have shown English for a phrase they have translated for years.
Member Settings / Archive went to /archive, and Boards in Archive is a
section of All Boards now: a row in its left menu, drawn beside it like
Starred and Remaining. That page is the thing the section replaced. Landing on
it meant the same list of boards with no menu beside it, no way across to
another section without going back first, and the menu row that says Archive was
not the row you had arrived at. A menu entry should land you on the same Archive
the menu itself offers.
All four entry points are the same line in a different menu - the board menu,
the member menu, the board sidebar and the All Boards sidebar - so all four go
to the section now, through the URL helper rather than a path spelled out in
four places that can drift apart. The member menu also closes itself behind the
click, like every other entry in it that navigates. /archive is still a route
and still renders, so a bookmark from before does not break.
The All Boards page chooses between the board icons and the Table view with
{{#if isAllBoardsView 'table'}}, and that helper was registered on
boardListHeaderBar and on allBoardsViewPopup but not on boardList — the
template that actually asks. A Blaze helper belongs to the template it is
registered on, so the page threw as soon as the router rendered it, right after
login, and All Boards did not come up at all.
Nothing noticed, because the guard read the jade and the JavaScript as two
separate files: it checked that the controls are in the header bar and that the
Table branch calls +tablePage, never that the template asking a question has
the helper that answers it. It now collects every helper this file registers,
and for each template in the jade every helper it uses that this file defines
must be registered on THAT template. Only helpers the file itself defines are
checked — a name it registers nowhere is a model helper on the data context,
like colorClass on a board, and a guard cannot tell one of those from a typo.
The other four templates in the file were clean.
The board scope listed the ways a user reaches a board — member, organization,
team, e-mail domain — and { permission: 'public' }. That last one is the odd
one out: it is not a relationship to the user at all, it is "anybody may open
this".
That belongs in the boards list, where a public board is meant to be discoverable. In a search it meant every public board on the instance was searched: on a public server a common word answered with strangers' cards, and following a hit dropped the user into a board they have no part in. Someone who wants to look inside a public board can still open it and search there.
The option defaults to including public boards, so every other caller — the
boards list, the lists and comments lookups — is unchanged. The search names its
scope once and passes it to all four board lookups plus the board: filter's
name resolution: one missed lookup and that branch still reaches the whole
instance, with nothing looking wrong.
The left menus - the one menu All Boards and the Admin Panel share.
<details> <summary><a href="https://github.com/wekan/wekan/commit/25265527648cffb1fe479c7a4c2a4f13b69f9041">The caret that folds the menu away does something when clicked</a>. Thanks to xet7.</summary>The caret at the top of the left menu - on All Boards and in the Admin Panel - rendered, pointed down, and did nothing at all when clicked.
Its template draws the caret; the click that folds the menu, and the helper that
says whether it is folded, live in a .js file beside it that nothing imported.
package.json sets meteor.mainModule, so the client is not eagerly
loaded: a file nobody imports is not in the bundle at all. The click handler was
never registered, so the caret was a dead control - and an unregistered Blaze
helper is undefined, so the menu never took the collapsed class either, which
is why even the caret itself never turned to point right.
One import fixes it. The new guard is what stops it happening a third time - it
had already happened to the Admin Panel reports' stylesheet: a test walks the
import graph from the client's entry point and pins that every file under
client/components which REGISTERS something with Blaze - a template's events,
helpers or lifecycle, a global helper, a BlazeComponent - is reachable from
it, and that every stylesheet and template beside them is too. A file that only
exports helpers is left alone: whoever uses it pulls it in.
Board views - filtering a board, and who sees which cards.
<details> <summary><a href="https://github.com/wekan/wekan/commit/7d9a2e303">A filter no longer leaves a spinner over an empty list, and a half-arrived card is not drawn blank</a>. Thanks to xet7.</summary>Two problems from one report with screenshots, both in how a list decides what to draw at its bottom edge.
"Once a filter is applied for a member, the 3 dots continue to animate." The screenshot shows a list with no cards under the filter and the load-more spinner still turning under it — and the scroll handler raising the window limit by ten every idle callback for as long as it stayed on screen.
The spinner asked "does this list hold more cards than the window I asked for?"
and answered it from a total built somewhere else than the cards being drawn.
The cards come from the filtered selector; the total came either from the list's
own card count or, in lazy card-loading mode, from a count document published
per list/swimlane — whose id was listId::swimlaneId and did not include
the filter. So changing the filter re-subscribed with a new selector under the
SAME document id, two publications wrote one document, and Meteor's merge box
serves whichever subscription it prefers — which during the changeover is the
older, pre-filter one. The list drew its empty filtered window while its count
still described the unfiltered list.
The count document now carries a short stable key for its selector, so each filter counts into its own document and a count can never describe another filter's cards. And the spinner no longer depends on that being right: there is nothing more to fetch unless the window came back FULL. If we asked for twenty cards and got three, those three are all there are, whatever any count says — which holds in both card-loading modes and for a stale count in either direction.
"Random blank cards are appearing on the board." White minicards with the handle icon and nothing else — no title, no members, no labels — mixed in among the real ones.
A minicard renders blank when its document is in minimongo without its fields:
getTitle() returns null for an undefined title, and every badge is
conditional on a field that is not there either. That is not a card with an
empty title — the schema declares title as optional with defaultValue: '',
so a card that went through it has the key, '' at worst. A document without
the key at all is a partial replication, and several publications ship cards
with a projection (openCardData publishes { _id: 1 } as the parent of its
children cursors, the search publications ship their own field lists), while
minimongo merges what every live publication says about an id.
Which of them produced these particular stubs is not settled — it needs the running board to catch — so this is a guard where the card is drawn rather than a fix at the source: a document that does not carry the field the minicard is built around is not drawn. It cannot hide a real card, and when the full document arrives the card appears, which is what the blank box was standing in for anyway.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1de699aca">Clicking outside the filter panel closes it</a>. Thanks to xet7.</summary>"If I use any filter, the modal that appears on screen sometimes doesn't disappear. Ideally, this should close the moment I click anything outside the modal."
It is the board sidebar showing its filter view, and nothing dismissed it but
the sidebar's own toggle or Escape. The reason is one argument: the document
click handler runs clickExecute(evt.target, 'multiselection'), and
sidebarView sits below multiselection in the escape hierarchy, so the loop
returns before ever reaching it. A click could not close the sidebar by design.
The handler lives in the filter template rather than raising that limit, because raising it would make every sidebar view close on any outside click — Archive, Settings and Card Settings are panels people work beside on purpose, and only the filter reads as a thing you open, use and are done with. Escape is untouched: it still returns the sidebar to its default view.
Three clicks deliberately do not close it. Inside the panel, obviously. A pop-over the panel opened — the label, member and due-date pickers render outside the sidebar, so without this, choosing a value in one would close the panel behind it. And the header button that opens the filter, which would otherwise toggle it shut in the same gesture that opened it. The handler is bound on the next tick so the opening click cannot reach the handler it just created, and it is removed by name when the panel goes, so it can never outlive it and close the sidebar under some later view.
The sidebar is hidden, not reset, so reopening it comes back to the filter you were using.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/550cd5c68">An assigned-only member sees only their own cards on a big board too</a>. Thanks to xet7.</summary>Three board-member flags mean the same thing — isReadAssignedOnly,
isNormalAssignedOnly, isCommentAssignedOnly — the member may only see the
cards they are assigned to. The board publication has always narrowed its card
cursor for such a member. boardCardsWindow, which is what ships the cards in
LAZY card-loading mode, did not.
So whether the restriction applied at all depended on the board's card-loading
mode. The same member saw only their own cards on a small board and every card
in the window on a big one (or on any board with CARDS_LOADING=lazy) — and
with the cards went their comments, attachments, checklists and checklist items,
because the window's children hang off the same selector. The restriction is
part of the window scope now, and of its count: an unrestricted count still told
the member how many cards the list really holds, and offered to scroll in cards
that would never arrive.
Two things this had to get right. The field projection: publish-composite hands
each child the document as the parent cursor published it, and that cursor
projected to { _id: 1 } — so board.members was undefined in every child and
the restriction would have been dead code. The parent publishes members now,
which also makes it reactive, and the board publication already ships them to
the same client.
And the merge. The board scope is spread into the client's selector at the top
level because FerretDB v1 (SQLite) does not push a top-level $and down to its
index — the wrapped form full-scanned the whole cards table on every poll and
cards never loaded on a big board. But a top-level spread can only be used when
the two selectors do not both speak for the same key, and the board Filter has
an assignee filter, so that collision is reachable from the UI: in the direction
the publication spreads them, the client's value would have won and the
restriction would have been silently dropped. The guard covered
boardId/archived only; it is mergeCardScope now, which merges when the
keys are disjoint and falls back to $and — where both hold — when they are
not. An assigned-only member filtering for someone else gets nothing rather than
everything, and an unrestricted member keeps the fast path.
Public Boards - what it lists.
<details> <summary><a href="https://github.com/wekan/wekan/commit/a4475e57e">The internal Subtasks boards are no longer listed</a>. Thanks to xet7.</summary>WeKan creates boards of its own to hold machinery — a subtasks board, for one.
Nobody chose to make one and nobody means to open one, so no list of boards
shows them; they are recognised by their title being wrapped in carets,
^Subtasks^.
The selector for that was typed out at each list — five copies of the same regular expression — and the sixth place forgot it: /public built its own query and listed every public subtasks board on the instance beside the real ones. It is one shared helper now, used by every list including /public. A function rather than a shared constant, because Mongo selectors get merged and mutated by their callers.
</details>and improves the following developer tooling:
<details> <summary><a href="https://github.com/wekan/wekan/commit/65a6a311e">build.sh installs Node on macOS with nvm, and installs the newest 24.x</a>. Thanks to xet7.</summary>"Install WeKan dependencies" ran brew install node@24, which gives whatever
24.x Homebrew currently has bottled rather than what nodejs.org has released,
and which is keg-only — so the branch also wrote PATH, LDFLAGS and
CPPFLAGS for it into ~/.zshrc by hand. It is nvm install 24 now: the
major alone, so it resolves to the newest 24.x every time it runs and never
needs bumping, and nvm puts it on PATH itself. npm comes with the Node it
installs, so brew install npm is gone too.
The nvm installer is fetched from a pinned release tag rather than master,
because the line pipes a downloaded script into a shell, and nvm.sh is
sourced before the first nvm call — nvm is a shell function, not a binary, so
without that every call is "command not found" even straight after a successful
install. An NVM_DIR the caller has already set is honoured, and a failed
install is reported instead of run into.
Two things the old branch did are now actively undone. npm config set prefix '~/.npm' cannot coexist with nvm: it overrides the per-version prefix, global
installs land outside the Node they were installed for, and nvm refuses to
switch versions while it is set — so the branch clears it, which a machine that
ran the Homebrew path needs. And that prefix directory was created with a
quoted tilde, mkdir "~/.npm", which makes a directory literally named ~ in
whatever directory build.sh was run from.
Linux is untouched and still installs Node with n, which the guard pins so
the split stays deliberate.
One browser test waited for header, #header to be visible on the page of the
user who had just been mentioned. The first header bar was rebuilt this release
and there is no <header> element and no #header id any more - the bar is
#header-quick-access, which two other specs already address it by. The
locator matched nothing, so the test asserted that a non-existent element was
visible, and failed in all three browsers.
The guard is what changed, not the app: the bar was deliberately rebuilt. It names the bar that is there now, and says so in the test for the next reader. It also asserts the BELL inside that bar, which is what the test is about - the count beside it arrives asynchronously, so asserting the count would be timing rather than behaviour, but a notification the user cannot see the bell for is not a notification.
</details>and documents the following:
<details> <summary><a href="https://github.com/wekan/wekan/commit/744df4154">What each board role may and may not do, as one table, read from the code</a>. Thanks to xet7.</summary>There was no comparison of the roles anywhere. Members.md listed three of them — Admin, Normal, Comment only — in one line each, and there are nine: board admin, normal, no comments, comment only, worker, read only, and an assigned-only variant of normal, comment-only and read-only. The API page shows how to set each flag without saying what any of them does.
Board
roles
is the table: for every role, which cards it sees, whether it may comment,
create or edit cards, move cards, edit lists and swimlanes, and change the
board's settings and members. It says where each column comes from, because the
answer is only two helpers in server/lib/utils.js plus isBoardAdmin() and,
for visibility, the assigned-only scope in the card publications. It is what the
SERVER allows, because the server is the authority and the UI can only hide
buttons.
Reading the code to write it turned up three roles that do not do what their name says. They are recorded as gaps rather than fixed, because each needs a decision about which side is wrong. "Comment only, assigned" has full write access — nothing outside the card publications ever reads that flag, so the role is in practice "Normal, but only sees my cards", which another role already means. "No comments" cannot write anything — the write helper excludes it, so the role blocks editing as well as commenting, while the schema calls it "not allowed to make comments" and the UI offers the edit affordances anyway. The write helper does not exempt board admins — every other helper ignores a flag on an admin; that one reads the raw flags, which the REST API can set individually. A fourth section lists the buttons the UI offers that the server then refuses.
A test keeps the page honest rather than trusting it — a permissions table that quietly goes stale is worse than none, because it is what an admin decides who to trust with. It parses the table and checks that every role the code can return has a row naming a flag it really reads, that the "create / edit" and "comment" columns match the flag lists in the two server helpers, that "which cards they see" matches the assigned-only scope, and that each gap it marks is still real and still explained — so fixing one has to update the page with it.
</details>and improves the translations:
<details> <summary><a href="https://github.com/wekan/wekan/commit/de1662146">The Roles Status strings are translated into 111 languages</a>. Thanks to xet7.</summary>The eleven strings the Roles Status table is built from were new, so they were
untranslated everywhere — on Transifex and in git — and every language showed
them in English. They are filled directly, per language, from that language's
own existing translations and its usual kanban vocabulary, with
fill-translations.mjs --apply, which writes only into keys that are still
English placeholders. A filled string can never overwrite a human translation
and is never pushed to Transifex, so it cannot masquerade as one there. The
thirty-one languages that have no translator at all keep the English source, as
they did before.
The grip on the left menu's inner edge got its tooltip this release, so its string was English in all 142 languages. It is translated directly - no external translation service, API or key - from each language's OWN existing strings: that language's "Drag to resize sidebar" sentence gives the pattern and the verb, and its own words for "left" and "menu" replace the sidebar, so the two controls read as the pair they are.
Applied with fill-translations.mjs, which writes only into keys that are still
English placeholders, so a human translation cannot be overwritten by it, and
filled strings are never pushed to Transifex and so cannot masquerade as human
ones there.
Thanks to above GitHub users for their contributions and translators for their translations.