Back to Wekan

WeKan ® 2026-08 releases

old-CHANGELOG/2026/08.md

11.411.4 MB
Original Source

WeKan ® 2026-08 releases

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-08Releases
023
031
0411
052
061
082
095
102
113
126
132
152
165
172
182
193
211
221
233
252
262
272
284
298
307
314

v11.39 2026-08-31 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.38 2026-08-31 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.37 2026-08-31 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v11.36 2026-08-31 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d2427bd8b">Disabled comparison and unset polling values no longer stop Snap startup</a>. Thanks to xet7.</summary>

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.

</details>

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

v11.35 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/830d1f31c">Snap services recover abandoned endpoint locks</a>. Thanks to xet7.</summary>

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.

</details>

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

v11.34 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v11.33 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/acb0f19c0">Website version tests follow the current release pins</a>. Thanks to xet7.</summary>

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.

v11.32 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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

v11.31 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ad0f1d80f">Compose leaves deployment endpoints to automatic startup</a>. Thanks to xet7.</summary>

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.

</details>

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.

v11.30 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v11.29 2026-08-30 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

This release updates the following dependencies:

  • @aws-sdk/client-s3 3.1117.0 → 3.1119.0 — the Amazon S3 client.
  • @aws-sdk/lib-storage 3.1114.0 → 3.1119.0 — managed multipart uploads to S3.
  • mongodb 7.5.0 → 7.6.0 — the Playwright test database driver.
  • pdfkit 0.19.1 → 0.20.1 — PDF generation.
  • puppeteer 25.8.0 → 25.9.0 — browser automation.

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.

</details>

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.

v11.28 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v11.27 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.26 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.25 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ab8c70613">Every FerretDB v1 Compose backend refreshes its cached binary</a>. Thanks to xet7.</summary>

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.

</details>

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

v11.24 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.23 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v11.22 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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

v11.21 2026-08-29 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d427583e">Legacy attachments apply security headers beyond SVG files</a>. Thanks to crypto-nidh and xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cacf4049c">Enable FerretDB's write-notified OpLog reactivity by default</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/350ec9000">Capture FerretDB connection failures in diagnostic logs</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3240ddd1b">Supervise diagnostic instrumentation and keep its measurements in files</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5b9b90f52">Run instrumented FerretDB alone for database restores</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cae03f7af">Install the matching DEBUGSPEED Chromium automatically</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/aedcb600b">Use Meteor's standard resume-login query and projection</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/33bcbe745">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 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.

</details>

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

v11.20 2026-08-28 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.19 2026-08-28 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8122c84a4">Tests retain an installed local Node across Dockerfile version bumps</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/89698bf46">Unsafe translation push scripts are removed</a>. Thanks to xet7.</summary>

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.

v11.18 2026-08-28 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.17 2026-08-28 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

v11.16 2026-08-27 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/404a79e00">Rebuilt 32-bit AppImages also protect older release bundles</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cfe73ab69">The full release publishes its AppImages automatically</a>. Thanks to xet7.</summary>

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.

v11.15 2026-08-27 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

</details>

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.

v11.14 2026-08-26 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.13 2026-08-26 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/FerretDB/commit/4fa9e3b8">Newly reported integer-conversion paths are closed</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/FerretDB/commit/89a34e2a">Follow-up CodeQL conversion and compatibility alerts are resolved</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/baa780331">The remaining IFTTT moved-from-list report gains exact regression coverage</a>. Thanks to TiibCD and xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7fb9ad1d7">LDAP encryption settings distinguish LDAPS, STARTTLS and unencrypted transport</a>. Thanks to robertdahlem and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b294ecfb0">REST login authenticates LDAP credentials through their registered handler</a>. Thanks to mariogalan, ViViDboarder, mgiacomoli, HaKePlan and xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e10fc10c5">Japanese add-card labels are confirmed to remain on one line</a>. Thanks to arifer612, skye0402 and xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b552af160">Legacy board imports default safely to private visibility</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bb267fddf">Anonymous verification links consume their token and verify the address</a>. Thanks to Sunildevb, jgrocha and xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5f16d1cbb">Long lists scroll at the bottom edge and accept the dropped card</a>. Thanks to lenxeon and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f8c7815b2">External MongoDB URLs are confirmed to survive Snap startup unchanged</a>. Thanks to cc4in and xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0c08ea8a1">A receiver holding its response cannot delay the originating database change</a>. Thanks to n8ores, RyanHecht and xet7.</summary>

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.

</details>

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.

v11.12 2026-08-25 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f80737e61">Automation conditions and actions gain 50 values</a>. Thanks to xet7.</summary>

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.

v11.11 2026-08-25 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5d33681e5">Free Pascal rewrite options and dependency equivalents are documented</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9d0dd9ffc">Completed locales remain included in translation-fill regression coverage</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/63bbf2460">Four wrongly seeded locales restore exact tokens in their own languages</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09444981d">Italian, Venda and Volapük complete the six-mismatch tier</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2de6cb556">Odia, Turkmen and Uyghur complete their marker-repair cleanup</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f3b17b34e">Four compact locale tiers restore every placeholder</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cdefe268d">Breton, Lithuanian and Yiddish complete their direct fills</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f08d8c479">Bhojpuri board and list actions gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/562e2e7ae">Bhojpuri tracking and uploads gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7a8bad2f">Bambara migration diagnostics gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4df8f5a05">Tibetan security controls and backup settings gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/045565cc4">Tibetan migration steps and resource monitoring gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7fc05e481">Buryat activity and board controls gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/629bda3b5">Buryat security and backup settings gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/76caec0bb">Buryat migration steps and monitoring gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e2d07af9f">Cherokee activity and board controls gain 50 values</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/acc164e40">Mongolian card controls no longer use Russian</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0aa734872">Mongolian checklists and subtasks no longer use Russian</a>. Thanks to xet7.</summary>

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.

</details>

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.

v11.10 2026-08-23 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.09 2026-08-23 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.08 2026-08-23 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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:

  • @aws-sdk/client-s3 3.1113.0 → 3.1114.0 — the Amazon S3 client.
  • @aws-sdk/lib-storage 3.1109.0 → 3.1114.0 — managed multipart uploads to S3.
  • @google-cloud/storage 7.22.0 → 8.0.1 — Google Cloud Storage integration.
  • dompurify 3.4.13 → 3.4.14 — HTML sanitization in the browser.

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> <details> <summary><a href="https://github.com/wekan/wekan/commit/c51e6938d">Complete tests offer two-worker, one-by-one and at-once modes</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/09db353b6">Complete runs clean up older tests and databases before starting</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/69e9a8ba1">The complete test matrix cannot create an unbounded shell or Go compiler load</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/175328061">Test runtimes cannot consume the build tool's half-of-RAM heap allowance</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c34daf2bd">Build and release scripts detect and support Fedora hosts</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/87252792f">Installers support Alpine, Arch, RHEL and Oracle Linux</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ef8b1043e">Build and release scripts work when launched from macOS zsh</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7f7fc73e">Test runs use the Node and Meteor installations under .tools</a>. Thanks to xet7.</summary>

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.

</details>

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

v11.07 2026-08-22 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/394a89fbc">Move and Copy Card offer board-global lists in every swimlane</a>. Thanks to rmb82 and xet7.</summary>

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.

</details>

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.

v11.06 2026-08-21 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

v11.05 2026-08-19 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

v11.04 2026-08-19 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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:

  • Meteor 3.5.1 → 3.5.2-beta.0 — the framework WeKan is built on. Its matching 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.

v11.03 2026-08-19 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/518a4d2fec487fef91f66224a9be8fc5f94e7527">JFIF uploads use portable JPEG download names</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cc609f9feb494ebaec326e9c47107af909bbd29a">Edit actions use compact accessible pencil icons</a>. Thanks to xet7.</summary>

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> <details> <summary><a href="https://github.com/wekan/wekan/commit/a7879012c92a22e3c73c1ad580b48035a1c3bbba">Checkbox custom fields respond, stay removed, and leave clean exports</a>. Thanks to Heart1010 and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/679d50378d0d41dd782b92b8cd610128a7f7f438">Opened cards display their saved custom checkbox checkmark</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1d4ffd4080d95acae6b01a44906fddc67861aede">Cards and minicards control custom-field visibility independently</a>. Thanks to xet7.</summary>

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> <details> <summary><a href="https://github.com/wekan/wekan/commit/ad9300297f69031155762e174e6bb828f5a4dc75">Every custom field editor provides a copy-to-clipboard control</a>. Thanks to xet7.</summary>

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> <details> <summary><a href="https://github.com/wekan/wekan/commit/3b17a9f8597a7f5857b051f640c63cd1e3b99a4e">Currency and String Template custom fields no longer break minicards</a>. Thanks to xet7.</summary>

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> <details> <summary><a href="https://github.com/wekan/wekan/commit/49de60eb04616048803ba988cc97758091da65d2">Linked card content is editable from either board</a>. Thanks to hever and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5693c1965a09f59e4e76795c9aeccd3653fdb47f">Card locations detect plain latitude and longitude pairs</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fce4d79453c7625f10718da23b8fda2cbd65666e">Keyboard navigation uses one shared accessibility utility</a>. Thanks to xet7.</summary>

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:

  • @aws-sdk/client-s3 3.1109.0 → 3.1113.0 — the AWS S3 client used by S3-compatible attachment storage.
  • @rsdoctor/rspack-plugin 1.6.1 → 1.6.2 — the Rspack build-analysis plugin.
  • hotkeys-js 4.0.4 → 4.0.5 — keyboard shortcut handling.
  • papaparse 5.5.4 → 5.6.0 — CSV parsing and generation.
  • puppeteer 25.6.0 → 25.8.0 — browser automation for exports and tests.

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.

v11.02 2026-08-18 WeKan ® release

In short: release metadata advances WeKan to v11.02 and records the binary provenance carried by its platform bundles.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

v11.01 2026-08-18 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/134bd48de">Recovery audits permanent-delete changes and board purges</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4c0f3b941">Select All and Select None control the visible archived boards</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f1934fb3f">The Multi-Selection sidebar stays visible while selection is on</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cb5ef5e0">An empty Home action asks for exactly one board</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c1522ad2a">The CPU governor observes FerretDB before slowing it</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/65aec4501">Pagination runs in the database</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

v11.00 2026-08-17 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5b69b63fe">Desktop mode on a phone can scroll every page to its end</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

v10.99 2026-08-17 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ea8bd12a9">The export popup's checkboxes can be changed, and they say what they are</a>. Thanks to Heart1010 and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3b47e10ee">Loading the export routes no longer stops a production bundle at startup</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

v10.98 2026-08-16 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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:

  • the counter was global, so an attacker's failures were charged to the victim's account rather than to the attacker's address;
  • a correct password was refused while the lock held, and counted as a further failure on the way. The old code allowed an attempt only when there was no error AND no lock, so the owner typing the right password fell through to the same throw as the attacker.

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.

</details>

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:

  • #433, js/incomplete-sanitizationversion.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.
  • #432, 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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ebe985d45">A location in an Admin Panel table opens a map, through the card's own chooser</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/efa25f78d">Sandstorm packs the trimmed trees, instead of rebuilding them first</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7fa8ba12e">CHANGELOG.md keeps the current year, and older years move to old-CHANGELOG/</a>. Thanks to mimZD and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fa289e4a4">The route and the template say what the menu says</a>. Thanks to xet7.</summary>

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.

</details>

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.

<details> <summary><a href="https://github.com/wekan/wekan/commit/ccb2d818a">build.sh and build.bat option 2 build the RELEASE bundle, minus the .zip</a>. Thanks to xet7.</summary>

"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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e9963e8b8">Directory-Structure.md describes the tree WeKan has now</a>. Thanks to TNick and xet7.</summary>

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 allimports/, 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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9fbfb26d2">Admin Panel docs are filed the way the Admin Panel menu is</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.97 2026-08-16 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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:

  • The tree is outside the bundler's graph. Meteor 3.5 compiles through rspack — the server output is minified, and 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.
  • It is a full 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.

</details>

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.

</details>

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

v10.96 2026-08-16 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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:

  • the only 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;
  • and that 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:

  • @aws-sdk/client-s3 3.1108.0 → 3.1109.0 — the S3 client WeKan stores attachments through when S3 storage is configured.
  • @aws-sdk/lib-storage 3.1104.0 → 3.1109.0 — the multipart upload helper beside it, which is what actually streams a large attachment to S3.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/14e8ca4a1">The Sandstorm .spk gets under 1 GiB, by dropping what a grain cannot run</a>. Thanks to xet7.</summary>

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:

  • uWebSockets.js ships twenty prebuilt binaries — Linux/macOS/Windows times x64/arm64 times four Node ABIs, 121M — and its loader is one line: 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.
  • Source maps — 4766 files, 188M, over a fifth of the bundle. They exist for a debugger attached to the process. A packed app has none, and a missing .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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cdbd33b59">The Sandstorm .spk ships no uWebSockets.js at all, because a grain talks sockjs</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c1e0f0297">Source maps go from every platform, not only the Sandstorm .spk</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.95 2026-08-16 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v10.94 2026-08-16 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

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.

</details>

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

v10.93 2026-08-15 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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:

  • The probe forced 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.
  • Nothing said why. The first probe's reason is printed now, the three-second poll stays quiet, the last one reports again if the window expires - and the reason is put on the page, because whoever is waiting is looking at a browser, not at 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.

</details>

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

v10.92 2026-08-15 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cc4139699ef8d6b6e51efb45b9a06f66e4340b4">HashBleed: the admin user endpoints answered with password and session-token hashes</a>. Thanks to ybsun0215 and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cc4139699ef8d6b6e51efb45b9a06f66e4340b4">GuestBleed: an outsider named on a card could read a private board's cards</a>. Thanks to ybsun0215 and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cc4139699ef8d6b6e51efb45b9a06f66e4340b4">StaleBleed: a removed board member kept seeing the board's id and title</a>. Thanks to ybsun0215 and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/9cc4139699ef8d6b6e51efb45b9a06f66e4340b4">AuthorBleed: six paths let the caller choose whose name went into the board's history</a>. Thanks to ybsun0215 and xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6157094ab8e633e65427c7cd046537af77956bff">The npm packages Meteor's own packages bundle are raised inside the built bundle</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6157094ab8e633e65427c7cd046537af77956bff">Guard suites for both, and the Debian findings that no upgrade can fix</a>. Thanks to xet7.</summary>

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:

  • The 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.
  • The nine build/<tool> binaries and build/ferretdb are other repositories' builds - wekan/mongo-tools-patches and wekan/FerretDB - and are fixed there.
</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0745dec4aec88703e0d19ddea0f6b8d59c747e9">One import page, for every source, with the same checkboxes the exports use</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bada25de4d95b0e5b473ec3176dccffcc5a999a7">Imported cards bring their attachments, from a .json and from a .zip</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fe77b7461e6737a528e953b1080e91f206812b45">A large .zip is uploaded and streamed instead of unpacked in the browser</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5d68d94bb7d3d765215f4d3725dd05800d55bf2d">The import page asks with the app's own checkboxes, in two columns</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/05ddc83cb786f7ca3baede3e80bf925a72e0e696">Clean Light's top bar is one shade of dark, icons included</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c56b556538045e0ead870ad09ede2de334a167db">The text colour under Member Settings / Font reaches all of the text</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b77a1663d80cb3ee5d876c491da8508ca5f403bc">Member Settings / Font / Text background color is removed</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d716de7ccbae184cc3cdda4a54c82e8e90461ab">The board Export menu is grouped under subheadings instead of one flat list</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b0ca22609905c45c35090d097f46d339fb4a748a">JSON and .zip export at every menu, streamed, with the attachments as files</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bd009cf601b81072308f7e1f56e73c88a89aa7cd">A board, a swimlane and a list export to PDF and Excel in the card export's own layout</a>. Thanks to xet7.</summary>

#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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bd009cf601b81072308f7e1f56e73c88a89aa7cd">One selection popup, and one list of what an export can contain</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0204ae014d1b312355be89bba09717ffeb28d47">Hide minicard label text became the Labels text row, and now works when logged in</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8d0a457e6893756c4cddf58ec776e728005e01c8">Custom Fields and Edit custom fields are one group in the card's menu</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6a6fc252929cb98b8bd6116dcd45d1cf68837d35">Change Background Image holds both ways of setting one, and Unset sits beside Save</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/48cf269870c087f36137f1b4960712c0df1b6f39">Members and Labels fold in the board sidebar, by their headings</a>. Thanks to xet7.</summary>

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 same control an opened card's sections use, and the same one Activities got below them. Folding Members hides the tabs and the avatars in them; folding Labels hides the labels and the + that creates one. Enter and Space do what a click does.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7d0396ebec899e0ade36d78cf6a19ad8f6fba4ff">The board's controls in the header bar fold into one caret</a>. Thanks to xet7.</summary>

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

  • Private, Muted, Sort Cards, Filter, Search, Show dependencies and Multi-Selection - and the bar is the logo, the board's name and the caret.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f0204ae014d1b312355be89bba09717ffeb28d47">The board sidebar's Activities has a caret instead of an eye</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ac02941663a1a9917896f790a6ab83b8ba9cd15a">v10.92 could not start bash: the release notes outgrew an environment variable</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/804004e9e88cf4d21c4f34e7201f532a7ae59cc8">Comparing the two databases reads the one that is already running</a>. Thanks to waltermhl and xet7.</summary>

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".

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f525b391a649e7b9cb2c683a3d3f622ec9a385be">A label on a minicard opens the labels, and not the card as well</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/67b91c850433ed04569734ff749c759ac557ac81">A custom field made from a card is actually created, and its pencil is beside its name</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ac201d4b5988bf314b4efcb0619ccdc36ab64595">Upload background image did nothing, and every attachment upload now shares one config</a>. Thanks to xet7.</summary>

"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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ac201d4b5988bf314b4efcb0619ccdc36ab64595">An avatar with no name to draw broke the render around it</a>. Thanks to xet7.</summary>

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:

  • the card button row (+cardButtons) - a slice used it as a boundary and swallowed it;
  • a custom field's name: +cardCustomField renders the value and nothing else, so a card with three custom fields showed three values with nothing to say what they were;
  • the attachment count beside the Attachments heading, which the board setting for it still governs;
  • and one thing GAINED that should not have been: a second, bare + beside the End date's add button.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c747d51bafd7f7ac09c618be8ea9e00aa37b9762">A group's caret is on its first field's title, so the titles read as one row</a>. Thanks to xet7.</summary>

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:

  • Labels - Labels, Stickers, Location
  • Date Format - the format, Received, Start, Due, End
  • Members - Members, Assignee, Creator, Requested By, Assigned By
  • Dependencies
  • Sort - Sort, List, Spent time

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c4d1801022c1c2ce021c2bb759208b820d128c9d">A section is one full-width row with one rule, and is named once</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bada25de4d95b0e5b473ec3176dccffcc5a999a7">Editing a card title keeps the X that closes it</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/698e980e27522b53f60ad6fce347727cb11eb017">Exported dates are in the reader's own time zone and date format, not the server's</a>. Thanks to Heart1010 and xet7.</summary>

"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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/698e980e27522b53f60ad6fce347727cb11eb017">A description's markdown is drawn as bold and italic instead of being stripped</a>. Thanks to Heart1010 and xet7.</summary>

"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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/82db0800ef6cda8adae06ac3e4f34d3517309a37">Move/Copy selection and Move/Copy swimlane are one dialog each, not two</a>. Thanks to xet7.</summary>

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.

</details>

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:

  • where a card goes - board, swimlane, list, above or below which card, and Done - drawn four times, by Move card, Copy card, Copy checklist to many cards and Convert checklist item to card;
  • where a list goes - drawn twice, by Copy list and Move list;
  • the Create Board form - drawn four times, by the one on All Boards and three popups, one of which creates a TEMPLATE board and says so with a Session flag rather than with different markup.

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/7a3cd208c70562c2496751f7e830f5a33db95ab9">Hindi was Gujarati and Tamil was Telugu, in 2,841 strings between them</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fb969b27459a6286c46418d053566c1f0beb9bdf">The scan said zero and meant zero of the tags it happened to name</a>. Thanks to xet7.</summary>

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 조직.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ae3686d67ca7bde36338a6907744a2696a6204de">Latin in a non-Latin file, and English that stopped looking untranslated</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b65518dca99550cbd27b9932170879ecd36ec185">Georgian was Turkish, Algerian Arabic was French, Mongolian was English</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/482065d8fb67dc91547a6f3da4f1aefe39904051">Thai: 683 values that were Vietnamese</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8df7376600b378c5d58842683f347c3d74d64e1e">Greek: 924 values that were Italian, and the Latin-only backlog is empty</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/330b6421908495a818f431d08abf4c3e0421148d">Board status and Board roles, in the 47 files that still showed English</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8e62dd157aae8cf0b0f403a2d125c8b30864d7ef">Twenty everyday words, and the 202 that were already right</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cd13f56c685ae615edb9036140d698964d4360b7">The newest languages go past the words on the board, into the menus</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0d3bb3ee75cc07b2787353d17ba157c910c4e59d">Ten more languages, and Latin among them</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/240f8f2d4a1a62f17c90fe0f2b952e937df3d2ec">Uyghur, Belarusian, Catalan, Estonian, Malay, Turkmen, Xhosa and Igbo, and the sort letters</a>. Thanks to xet7.</summary>

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

  • a wrong string is readable, obviously improvable and an invitation to the person who speaks the language, which an English placeholder never is. When that person sends a correction through Transifex it REPLACES the filled one, because the merge always prefers a human translation.

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.

</details>

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

v10.91 2026-08-13 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/55552a720">The pages shown when there is no database use the product name and the dashboard's colours</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

v10.90 2026-08-13 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details>

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.

</details>

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a70e69767">A Launchpad build that outlives its job says so, instead of just CANCELLED</a>. Thanks to xet7.</summary>

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.

v10.89 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/60b4ceda3">A snap ends up on FerretDB, whatever it was running before</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bb76e7bf1">There is no database setting on the snap any more, and nothing to type</a>. Thanks to xet7.</summary>

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:

  • set to 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;
  • set to ferretdb with no FerretDB present, it would have served an empty site, so the guard against that had to exist anyway;
  • and every script had its own copy of "which database is this, then".

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/18b881262">A path that stops resolving when the step changes directory, and the last bare downloads</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bdacff80e">A release script the job cannot see, and an hour of build thrown away at the push</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.88 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e771f8c66">A download that 503s is retried for a quarter of an hour, and a 404 still fails at once</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.87 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details>

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

v10.86 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

This release updates the following dependencies:

  • aldeed:collection2 4.2.1 → 4.2.2 — the Meteor package that validates every write against a collection's schema. It is the one that formats the error a list with no title used to crash inside.
  • rspack 1.2.0 → 1.2.1 — the Meteor build plugin that bundles the client. Build-time only.
  • @aws-sdk/client-s3 3.1105.0 → 3.1108.0, with the eighteen @aws-sdk/* credential, signing and presigner packages it pulls in — the optional S3 attachment storage. Unused unless S3 is configured.
  • @smithy/core 3.31.1 → 3.32.0 and its HTTP handler, IMDS credential and signature-v4 packages — the transport layer under those AWS clients.
  • @google-cloud/storage 7.21.0 → 7.22.0 — the other optional attachment backend.
  • bson 7.3.1 → 7.3.2 — the BSON codec the MongoDB driver speaks to MongoDB and to FerretDB with.
  • ws 8.21.2 → 8.21.3 — the WebSocket implementation Meteor's DDP connection runs over.
  • nanoid 3.3.17 → 3.3.18 — id generation inside the build toolchain.
  • terser 5.49.2 → 5.50.0, browserslist 4.28.7 → 4.28.8, electron-to-chromium 1.5.402 → 1.5.405, baseline-browser-mapping 2.11.12 → 2.11.13, update-browserslist-db 1.3.0 → 1.3.1 — the minifier and the browser-support tables it targets. Build-time only.
  • eslint 10.8.0 → 10.8.1 and the @typescript-eslint 8.66.0 → 8.67.0 family — linting, development only.
  • puppeteer 25.5.0 → 25.6.0 (with puppeteer-core and @puppeteer/browsers 3.1.0 → 3.2.0) — the headless browser some tests drive. It ships in no WeKan bundle.
  • memfs 4.68.0 → 4.68.1 and the eight @jsonjoy.com/fs-* 4.68.0 → 4.68.1 packages it is built from — the in-memory filesystem the build uses.
  • modern-tar 0.7.7 → 0.8.4, markdown-it-math 6.0.0 → 6.0.1, bare-url 2.4.7 → 2.5.2, @babel/helper-validator-identifier 7.29.7 → 8.0.4, @types/node 26.1.2 → 26.2.0 — transitive updates that came with the rest.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b99fa701">A snap already running on FerretDB is not stopped by the old MongoDB files beside it</a>. Thanks to mueschel and xet7.</summary>

"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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c3811c528">A MongoDB this snap has no reader for stops and says so, on every CPU</a>. Thanks to Philippe-Bentegeac, JDeepix and xet7.</summary>

"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".

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/40ff8151e">Two copies of one database are reconciled automatically: the newer is served, the older merged into history</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/440062906">Copying the old snap common directory back is what destroys the restore</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f2e58945f">An archived card is still named in its own history</a>. Thanks to rptl and xet7.</summary>

"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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f7b0a4d1d">A board's type now follows Member Settings / Font size</a>. Thanks to CCmesch and xet7.</summary>

"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%.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/32b61f186">Unchecking "Show on minicard" on a checklist now hides it</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/eb1416889">The PDF export writes umlauts instead of question marks, and no longer prints markdown</a>. Thanks to Heart1010 and xet7.</summary>

"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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1d76e0d43">The two variant images are published on all three registries by the release itself</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2ad02cccc">A version bump cannot rewrite the database image, and old releases are not backfilled onto FerretDB</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/de8a29385">A chart package whose container image was deleted cannot come back into the index</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.85 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details>

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

v10.84 2026-08-12 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details>

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.

</details>

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

v10.83 2026-08-11 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.49.07c74941ff043f26aa4411ef5065d6b2d0766e369fc2a4458364c2f5571c12762
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.49.0092132531555a39eac12566240a5f1ed02f62148b2dca0540a74c68e5957f6b5
armhfNode.jswekan/node-patchesv24.19.0b55350f3071b765a98ed66fdc410657ff168a937935057077fd7ab33cb30b9aa
armhfFerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
armv6Node.jswekan/node-patchesv24.19.0128ded0cda638c1f144eadb23ad249889515df017d298fd49c8faf3db110f0f1
armv6FerretDBwekan/FerretDBv1.49.07c27b2c15448709a24eace9b3c951c62fbe33413f7d20d56cb5520f4436efe2d
armv7Node.jswekan/node-patchesv24.19.08dbe0a9aa8550ad5275c5538ebf868eb2037f0c4d9cccbe319f63b7e5854cd45
armv7FerretDBwekan/FerretDBv1.49.0144404fb9793dc8e039874812f4e2cb3e6d8b1df0ffdbe50254e7790a342a2f4
i386Node.jswekan/node-patchesv24.19.03b0b3bbfe27daf583b3a0f432efacc508407a012cdd9e8847250e7c015565bac
i386FerretDBwekan/FerretDBv1.49.01f70cb1687411b2a0fa9ac3b5bfc8c4ed9ce25ec2ddfa17e6fd3efb38136a39c
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.49.0576364db59dfce3ba564b9a3e484496eb57f95d76d2007b9f83241acdbd2f4fa
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.49.037d70cd90aad6d3867b6686507ff1888f1edf6791818c31d014d130e8f39fc14
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.49.07c61d4853d5163ad8761449d693fd458ebbb8611a2351c718014876242c5b1fb
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.49.0bd4912da70f5e6c1475ab989668c76b4ab7db4ee06c44357693df64f5e1d0e0b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.49.0no checksum published
win-arm64Node.jsnodejs.orgv24.19.08502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f
win-arm64FerretDBwekan/FerretDBv1.49.0792166623e774b0af2aced31ed3ae39f545ca5268dc4c2b8d1a329228ff52cbc
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.49.0f42c50aa84095a9616b00f27a584c66b7bf79e3b109450c62a5f146ba3c85478

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/484eae736">Honour the boardId parameter the client has been sending all along</a>. Thanks to markusst1982 and xet7.</summary>

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.

</details>

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

v10.82 2026-08-11 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.48.02737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.48.05ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.48.006ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.48.09b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.48.04e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.48.00400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.48.0d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.48.06c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.48.0ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2

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.

</details>

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.

</details>

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.

</details>

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.

</details>

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

  • the unsupported-architecture exit. The other two removed the whole staged directory and exited 0, which leaves 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.

v10.81 2026-08-11 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.48.02737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.48.05ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.48.006ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.48.09b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.48.04e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.48.00400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.48.0d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.48.06c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.48.0ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2

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.

</details>

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.

</details>

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.
</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1a2ec0633">Rebuild the Helm index.yaml from the chart packages it actually serves</a>. Thanks to xet7.</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.

v10.80 2026-08-10 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.48.02737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.48.05ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.48.006ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.48.09b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.48.04e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.48.00400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.48.0d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.48.06c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.48.0ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2

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.

</details>

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.

</details>

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.

</details>

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

v10.79 2026-08-10 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.48.02737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.48.05ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.48.006ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.48.09b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.48.04e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.48.00400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.48.0d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.48.06c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.48.0ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f342d54d1">The Docker image gains it as a candidate platform, gated on a base that publishes linux/arm/v6</a>. Thanks to xet7.</summary>

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.

</details>

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.

<details> <summary><a href="https://github.com/wekan/wekan/commit/664b8fde6">The next base declares all six architectures too, armhf included</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

</details>

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.

<details> <summary><a href="https://github.com/wekan/wekan/commit/35dd10b89">They say who maintains these repositories and who commits, before any identity check</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d07794811">What is in .tools/, and the changelog format of every repository under it</a>. Thanks to xet7.</summary>

.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

  • a 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.
</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bf1f7aea4">TSC is xet7's too, under another organisation</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.78 2026-08-09 WeKan ® release

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".

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.48.02737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.48.05ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.48.006ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.48.09b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.48.04e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.48.00400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.48.0d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.48.06c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.48.0ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/6dce9dda7">Three good snaps were reported as FAILED by the step that saves their logs</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/04fb6e338">Flatpak: attach the bundles to the release, and only the bundles</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.77 2026-08-09 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.

</details>

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.

</details>

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

v10.76 2026-08-09 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.
</details>

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.

v10.75 2026-08-09 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.

</details>

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

v10.74 2026-08-09 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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

  • a document written before this fix, a restored backup, a bad migration - and it sits at the one place export turns a stored path into bytes, for attachments and avatars alike. The download path had always checked containment this way; its private copy of the function is gone, so download and export ask the same question. The streaming attachment export, which read a stored path with nothing but an 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.
</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/35daaaa2795e83da9a23172d6279a9ba63ef1b0f">ParentBleed: one card id bridged a private board into another board's feed</a>. Thanks to Alpastx and xet7.</summary>

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.

</details>

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

  • it is what the account was called when it tried, so a rename does not rewrite history. The address uses the same spoofing-safe rule as the login throttle, or an attacker could write a colleague's address into the security log by sending a header.

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:

  • @aws-sdk/lib-storage 3.1085.0 → 3.1104.0 — the S3 multipart uploader the optional S3 attachment storage uses.
  • markdown-it 15.0.0 — the markdown renderer behind card descriptions and comments. A major version; its breaking changes are in plugin APIs WeKan does not use.
  • @playwright/test 1.62.0 → 1.62.1 — the browser test runner, in tests/playwright only; it ships in no WeKan bundle.
  • actions/checkout 4 → 7, actions/download-artifact 4 → 8, actions/upload-artifact 4 → 7 — the GitHub Actions steps every release workflow starts and ends with. Build-time only.

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8c2565e99">releases/: delete two superseded scripts, and two exemptions that outlived them</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.73 2026-08-08 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.

</details>

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

v10.72 2026-08-08 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

This release updates the following dependencies:

  • Meteor 3.5.1-beta.0 → 3.5.1 — the framework WeKan is built on, now on the final release instead of the prerelease it was tracking. The four packages that were on -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.
  • @meteorjs/rspack 2.1.0-beta.0 → 2.1.0 — the bundler Meteor builds the client with, the counterpart of the Meteor release above and off its prerelease for the same reason. Fifteen packages leave the lockfile with it: 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.
  • jquery 3.7.1 → 4.0.0 — the DOM library the whole client is written against, a major version. It drops Internet Explorer and the long-deprecated helpers $.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.
  • @babel/parser 7.29.7 → 8.0.4 — the parser Babel reads JavaScript source with, also a major version. Nothing in WeKan's source imports it; it is declared in package.json so the build resolves a single version of it. Update. Thanks to dependabot and xet7.
  • An npm dependency refresh — 926 → 903 packages in the lockfile, moving the AWS S3 client 3.1095.0 → 3.1105.0, 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.

</details>

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.

</details>

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 &nbsp; 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.

</details>

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.

</details>

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.

</details>

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.

</details>

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.

</details>

and has the following developer-tooling fixes:

What the snap is built from - the parts in snapcraft.yaml.

<details> <summary><a href="https://github.com/wekan/wekan/commit/3c7926662c5e14dde16465d6a5380746cf298708">The Caddy part stops asking the GitHub API which version to download</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3c7926662c5e14dde16465d6a5380746cf298708">The mongodb part asks for the package names Ubuntu 24.04 really publishes</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/3c7926662c5e14dde16465d6a5380746cf298708">The mongodb part makes bin a real directory whatever it was before</a>. Thanks to xet7.</summary>

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.

</details>

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".

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/96daaac3dbf988b72d3af2bc184333c3a4de49ee">A retry loop that outlived its own test, and an ignore entry that explains itself</a>. Thanks to xet7.</summary>

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.

<details> <summary><a href="https://github.com/wekan/wekan/commit/40fff29d08ead7306911e112e97d381eb2f296c5">Companion repos live in .tools/, and the build scripts clone them there</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6b0af16ebae40005f19f28a8bf4dac2c00b0081">EVERYTHING runs the floating-promises guard too, and Windows gets one log per browser</a>. Thanks to xet7.</summary>

./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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b55cbda20dcb1faa82d9c080472017f1e275c0e6">Tests -> EVERYTHING runs again instead of quitting the script</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f138b3124f7514909b0314ab3a6db367eb806056">The "Playwright ALL browsers" option on Windows writes logs again</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/8ebeb85832c62aeba72d774cdb4df7d5c6105f43">The snap-launchpad job stops blaming LP_CREDENTIALS for every Stopped Launchpad build</a>. Thanks to xet7.</summary>

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

  • Launchpad cancelling it mid-run, usually an out-of-memory in the Meteor 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.
</details>

and updates the documentation and translations:

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

v10.71 2026-08-06 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c09e538fe">The release scripts run on the bash 3.2 that macOS ships</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.70 2026-08-05 WeKan ® release

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.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9eff391b97427f4ed31ff07753a83d1f571f511">A platform no source has a Node.js for is not built, instead of failing the release</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b9eff391b97427f4ed31ff07753a83d1f571f511">The 32-bit Windows import library moves with the runtime it belongs to</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.69 2026-08-05 WeKan ® release

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/0aa74076a20b191f3055932d1cc795115ea966f2">The Docker image builds for whatever bundles landed, so one failed exotic arch never skips it</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/d0934867661a0186d2a21f1b6e0e551fa5af5c51">The native embeds authenticate the fork lookup, tolerate a missing checksum, and cross-build win32</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/b46acc47c5cae3b4e0ee543436cf994532408d80">Every extra-arch bundle is best-effort, so an emulated CPU that crashes cannot fail the release</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/59e54e384aa70a44d0825cb4ade7d9acc0918be3">win32 and mac-x64 resolve the fork version, and win32 cross-builds ia32 addons against the fork's node.lib</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.68 2026-08-04 WeKan ® release

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e9171ff7d16456a38b6066a242b261b969abd4ba">The extra-arch bundles and the Docker image take Node.js only from the fork</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/bff53e93adfb7cc596fb87f51d1769e795afe208">New win32 and mac-x64 .zip bundles, so every fork platform has a bundle</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.67 2026-08-04 WeKan ® release

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.

</details>

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

v10.66 2026-08-04 WeKan ® release

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.

</details>

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

v10.65 2026-08-04 WeKan ® release

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

  • which is already set to MIT - is corrected to point at the real cause.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/a2d47df8c2322b357199d27abf0f139a2cd8f5a6">The Docker image drops linux/386, which its ubuntu:26.04 base cannot provide</a>. Thanks to xet7.</summary>

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.

</details>

The Launchpad snap builds - the ppc64el, s390x, riscv64 and armhf snaps built on Launchpad with snapcraft remote-build.

<details> <summary><a href="https://github.com/wekan/wekan/commit/50e2e14d99bb27455cf69f169bc43ed48cff96d9">The Launchpad snap builds flatten history first, so the push to git.launchpad.net stops timing out</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/646c5db79d450bfd5ff403e4d0e4fae443671794">A Launchpad snap failure no longer blames the licence when it is already set</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.64 2026-08-04 WeKan ® release

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.

</details>

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.)

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/ea4a7a5eb8fe912d6dbedb4b00b16918da075159">The release job checks out before downloading the bundles, so the checkout stops deleting them</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/55463f685a09f75b07a127ec901b3d564353d46b">i386 and armhf are skipped when no Node.js exists for them, instead of failing the run</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

v10.63 2026-08-04 WeKan ® release

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.

</details>

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

v10.62 2026-08-04 WeKan ® release

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.

</details>

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

v10.61 2026-08-04 WeKan ® release

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.

</details>

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.

</details>

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.

</details>

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

v10.60 2026-08-04 WeKan ® release

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.

</details>

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

v10.59 2026-08-04 WeKan ® release

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c3742f2ed7cfeb3dc709997ca9403d3fae5f7188">A login for a user that does not exist now takes as long as one that does</a>. Thanks to NinjaGPT and xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c3742f2ed7cfeb3dc709997ca9403d3fae5f7188">The REST login endpoint stops naming missing users and throttles password guessing</a>. Thanks to NinjaGPT and xet7.</summary>

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.

</details>

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.

</details>

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

v10.58 2026-08-04 WeKan ® release

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.

</details>

and updates the following dependencies:

  • fast-uri 3.1.4 → 3.1.5 — the URI parser Fastify's JSON schema validation uses.
  • postcss 8.5.22 → 8.5.25 — the CSS transformer the stylesheet build runs on.
  • socket.io-parser 4.2.6 → 4.2.7 — encodes and decodes the Socket.IO protocol.
  • brace-expansion 5.0.8 → 5.0.9 — the {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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/92198b0671ee27545d62c9537aaccb50cabe49f7">i386 and loong64 asked for a base image that is not built for their CPU</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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

  • so the further a CPU is off the beaten path, the further behind its newest build tends to be. Asking for the exact newest version fails for precisely the architectures this job exists to serve.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1a263e9e21f60f2e1eaa7d1295ede4c21b6f465a">It comes from the most verifiable source that has it, and is checked against its checksum</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/581475a0d56b69ad94611ff7086f966f22c4b2ee">The heap limit for builds is worked out from the machine, not fixed at 8 GB</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4b77eb655f0a1b4c28a3f66bd98ffc3667d1b52e">A test run that fails in the build now leaves the build log behind</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cf178e5948f91041af2a02ad2503d6ce37cae105">build.sh exited 0 while printing that the build had failed</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/c6e3e2ef154822ef47559fd4358cbe63db9bff9c">_build is the handoff to Meteor, not leftovers - excluding it broke the build</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/062f9127a8e23bfceb007fdbd2885f3d17dff9b8">A cloned repository has to be in both .gitignore and .meteorignore</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/dcdda5f0f8c34aeb4ca5c58f925386e7d507cb40">The filter that selects what to build stopped every workflow loading</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/18c6dcc68fb3023feaa9382c596ba4df72aa947d">The other way a workflow fails before it starts is guarded too</a>. Thanks to xet7.</summary>

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.

</details>

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

v10.57 2026-08-03 WeKan ® release

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:

  1. the attacker puts http://<public-host>/attachment.txt on a Trello card
  2. validateAttachmentUrl() resolves it, sees a public IP, allows it
  3. that host answers 302 Location: http://127.0.0.1:18080/secret
  4. fetch() follows, and the loopback body is stored as the imported attachment, readable back through WeKan

That 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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/4c183b4d5942f09ab30d0dcdad4d6e4df889023d">IPv6 addresses are classified by their bytes, not by how they are spelled</a>. Thanks to tonghuaroot and xet7.</summary>

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.254
  • 64:ff9b::c0a8:101 — NAT64 (RFC 6052) → 192.168.1.1
  • 2001:0:… — Teredo (RFC 4380), the IPv4 stored as the complement of the low 32 bits
  • 0:0:0:0:0:ffff:7f00:1 — IPv4-mapped, merely spelled out → 127.0.0.1

On 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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f628a698890dcbbd965ddee7b3b164523a3acf4a">The links follow the move, including the ones inside the pages that moved</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/f628a698890dcbbd965ddee7b3b164523a3acf4a">Doc links written from the repository root resolve from the page holding them</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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

v10.56 2026-08-02 WeKan ® release

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/852ad1714b58589fd132c4f63ef32fc47eef709d">The board counts line up in one column</a>. Thanks to xet7.</summary>

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.

v10.55 2026-08-02 WeKan ® release

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:

  • @typescript-eslint/eslint-plugin 8.63.0 → 8.65.0 — the TypeScript rules ESLint checks the source against. A development dependency: it runs in the linter, never in the bundle. (#6564, Update). Thanks to dependabot.
  • @typescript-eslint/parser 8.63.0 → 8.65.0 — the other half of the same pair: what lets ESLint read TypeScript at all, so it moves with the plugin above. (#6562, Update). Thanks to dependabot.
  • sinon 22.0.0 → 22.1.0 — the spies, stubs and fake timers the unit tests build their doubles from. Also a development dependency. (#6563, Update). Thanks to dependabot.
  • docker/login-action 4.5.1 → 4.6.0 — the GitHub Actions step that signs in to the container registries before a release image is pushed. Pinned by commit hash rather than by tag, which is why the change is a hash and not a version number. (#6561, Update). Thanks to dependabot.

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

v10.54 2026-08-02 WeKan ® release

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

  • a public board has this menu too - through the same cookie helpers the public list and swimlane collapse states already use.

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.

</details>

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.

</details>

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

  • instead of only the product name. Ten open tabs of one WeKan were ten identical tabs, and a bookmark of any of them was named after the whole app rather than after the page. The path is the one the header bar already computes for the page title's tooltip, published rather than worked out twice.

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/03cb79bdd">It says where you are: Admin Panel / Settings / Version</a>. Thanks to xet7.</summary>

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

  • padding applies to every row it wraps onto, while a margin on the first item would indent the first row and leave the second starting at the edge. The number is a variable so it has one home, and the phone overrides use it too rather than putting the bar back against the edge at the widths where the alignment matters most.
</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/fa6bcd1f4">No Starred button in the bar, Multi-Selection after the view menu, a narrower search</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/2537a52ec">The Selected star is a toggle now, and every button in the bar names itself</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/5cc399b47">A URL for every left-menu entry here too, workspaces included</a>. Thanks to xet7.</summary>

/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.

</details>

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.

</details>

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.

</details>

and updates the following dependencies:

  • aldeed:collection2 4.2.0 → 4.2.1 — cleans and validates every write against a collection's SimpleSchema, so it is on the path of every insert and update WeKan makes. Update. Thanks to xet7.
  • Meteor 3.5 → 3.5.1-beta.0 — the framework WeKan is built on, and with it the build system, the MongoDB driver and the accounts packages that ship as part of the release. Update. Thanks to Meteor developers and xet7.
  • meteor-node-stubs fork — the Node core-module shims the browser bundle is built against, forked into npm-packages/meteor-node-stubs. Update. Thanks to xet7.
  • @meteorjs/rspack 2.0.1 → 2.1.0-beta.0 — the rspack bundler Meteor builds the client with, the counterpart of the 3.5.1-beta.0 release above. It moved to rimraf 6, whose glob 13 no longer ships a command line of its own, so twenty-seven packages leave the lockfile with it (975 → 948) without anything being dropped from WeKan itself. 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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/93042642f4fe3ae91d0b54e988a97d7ddcebdd5d">The All Boards house starts at the same X at every window width</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/89d12c6fe689aa970cafda2429ff480ed7038a92">Its view menu opens a titled popup, like the board's own</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/cdb611340120d0c9f56e890a85fa2f1c3da0e4f8">Archive opens the section of this page, not the full-width page it replaced</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/1049e3e8f">It no longer throws No such function: isAllBoardsView as it renders</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/96289e118">Search All Boards searches all of your boards, not Public Boards anymore</a>. Thanks to xet7.</summary>

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.

</details>

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.

</details>

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.

</details>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/e8d9c06a7">The notifications spec looks for the header bar that exists</a>. Thanks to xet7.</summary>

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.

</details> <details> <summary><a href="https://github.com/wekan/wekan/commit/720d451d64bdae4beb39d73fe5180846d2096a70">The left menu's drag-to-resize tooltip is translated into every language</a>. Thanks to xet7.</summary>

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.

</details>

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