The rebase rewrote eb80ee69f -> 0e0391f6c (#6506) and 00ef255b0 -> 72768e05f (#6507);
the old hashes were pre-rebase orphans that were never pushed, so their CHANGELOG
links would 404 on GitHub. Updated to the current on-branch hashes.
Thanks to xet7 !
On a fresh install (no Settings document yet, and Meteor.user() briefly null right
after the first admin logs in) the Admin panel would not open, the title bar
sometimes failed to load or showed "You are not authorized to view this page", and
the console looped `Uncaught (in promise) TypeError: Cannot read properties of
undefined (reading 'remove')` through a Tracker computation.
Root cause: ReactiveCacheClient.getCurrentSetting() and getCurrentUser() guarded
their DataCache with `!this.__x || !this.__x.get()`. Every other getter creates its
DataCache ONCE (`if (!this.__x)`), but these two ALSO rebuilt it whenever the cached
value was FALSY — so while the setting/user was absent (a fresh install, or any
transient null) a brand-new DataCache, and a brand-new Tracker.autorun, was
constructed on EVERY reactive read. That continuous create/teardown churned
computations across every app-wide template (header, admin gates) and raced Blaze's
view teardown, producing the infinite reactive loop and the removed-DomRange crash
(the same WebKit Blaze class the getBoard staleWhileRevalidate comment guards).
Fixed by creating the DataCache once, keyed on `!this.__currentSetting` /
`!this.__currentUser` only, like every other getter; the autorun already updates
reactively when the Settings document / user appears, so no rebuild is ever needed.
Returning a stable null until they exist is correct and loop-free.
Test: tests/reactiveCacheCurrentGetters.test.cjs.
Thanks to AmigaAbattoir and xet7 !
Fixes#6507,
Every board card query used `{ boardId: { $in: [board._id, board.subtasksDefaultBoardId] } }`,
and subtasksDefaultBoardId defaults to null. On FerretDB v1 (SQLite) a $in that
contains a non-string element (the null) does not push down at all, so the WHERE was
dropped and the whole cards collection was full-scanned + sjson-decoded on every
poll-and-diff cycle, across ~7 card cursors per board open. Lists and swimlanes use a
plain boardId equality (index-backed) and stayed fast — so on a big board the columns
appeared but the cards never did.
New helper models/lib/boardCardScope.js builds the scope WITHOUT null and prefers a
plain equality (index seek) when only the board itself is in scope, else an
all-string $in (which pushes down). Applied to every card-scope site in
server/publications/boards.js (8x), cardsWindow.js and cards.js.
Also flatten the lazy card-window selector (cardsWindow.js): the window was
`{ $and: [clientSelector, { boardId, archived }] }`, and FerretDB does not push down
a top-level $and, so boardId/archived were full-scanned too. Merge the board scope at
the top level (exactly equivalent when the client selector has no boardId/archived
key, which it does not — it is a per-list listId + swimlane selector; a collision
guard keeps $and otherwise), so boardId/archived push down.
The FerretDB fork side is hardened separately so a mixed $in (incl. null) also uses
the index. Test: tests/boardCardScope.test.cjs.
Thanks to xet7 !
Regression from c0a1a2784 (v10.12): it dropped `importMapMembers` from the import
wizard's `steps` array and hardcoded an empty membersMapping in finishImport, so the
map-users screen never rendered and every imported card/member/comment was assigned
to the importing user even when the person existed in WeKan.
Client (client/components/import/import.js):
- restore `steps = ['importTextarea', 'importMapMembers']` so the (auto-suggesting)
map-members step shows again;
- finishImport builds membersMapping from the members the user mapped
(member.wekanId), instead of sending `{}`. The step stays OPTIONAL — the Skip
button and the textarea "import without mapping" button still bypass it.
Server — unmapped members become virtual users instead of collapsing onto the importer:
- models/trelloCreator.js: new createPlaceholderUsers(board), run in create() before
the board is built, so it covers BOTH the Trello JSON import and the Trello API
(all-boards background) import. For each member not already mapped it maps to an
existing WeKan user by username / recorded importUsername, else inserts an inert
virtual user (loginDisabled, isActive:false, authenticationMethod:'imported')
reusing the Trello member id, so board membership and authorship keep the original
person;
- models/wekanCreator.js: createPlaceholderUsers no longer overwrites an explicit UI
mapping (skip members already in this.members) so a member mapped to a real account
is not clobbered by the identity placeholder.
A board admin can still map a virtual member to a real user later from the sidebar
(that feature is unchanged). Tests updated: tests/importVirtualMembers.test.cjs and
tests/importMemberMapping.test.cjs.
Thanks to AmigaAbattoir and xet7 !
Fixes#6506,
v10.22 was cut while these fixes were in flight, so the CRITICAL entries go in a
fresh Upcoming section above it.
Thanks to koyokr, GitHub CodeQL and xet7 !
stripExploitPatterns() looped a CHAIN of six replaces to a fixpoint. That is
runtime-safe, but CodeQL (correctly, as a local check) could not attribute the
fixpoint to the individual `<[^>]*>?` tag replace, so it kept flagging it as an
incomplete sanitizer that "may still contain <script".
Restructure to the proven-complete pattern already used by
client/lib/importDependencies.js stripHtml() (which cleared the sibling alert
#421): remove template/PI/CDATA tokens (looped), then strip HTML/XML tags with a
SINGLE `replace(/<[^>]*>/g, '')` looped to a fixed point, then drop any stray
angle bracket so an UNCLOSED tag (no '>', e.g. a trailing "<script") cannot
survive either. Behaviour is unchanged for every existing case.
Regression: tests/fileNameDisplay.test.cjs now asserts completeness for closed,
unclosed and spliced tags (no '<', '>' or '<script' in the output).
Thanks to xet7 !
A board member could store entity-encoded markup in a card title. It stays inert
on the live board (Blaze escapes it; the +viewer sanitizer neutralizes handlers
and keeps the entity payload as text), but the exported index.html embedded a
click handler that read the card title/body via .textContent — which DECODES HTML
entities — and then concatenated those values into content.innerHTML. That SECOND
parse revived the tag (e.g. `<img src=x onerror=...>`) and executed it when a
recipient clicked the card in the export, disclosing all data in that document
(including cards added after the attacker's board membership was removed).
The modal is now built with DOM nodes and the card title/body are assigned through
textContent, never innerHTML, so they are inserted as inert text.
Regression: tests/exportHTMLXss.test.cjs (title/body reach only textContent; no
titleText/allText flows into any innerHTML; a textContent assignment does not
revive an entity-decoded <img>).
(CWE-79 stored XSS, GitHub Security Advisory GHSA-8r5p-4q9j-f5jx.)
Thanks to koyokr and xet7 !
The 10.16-10.21 reports of the ferretdb process pegging ~2 cores for a long time
while node sat idle are the signature of an OpLog tail: Meteor holds a tailable
cursor on local.oplog.rs and FerretDB re-scans it server-side. wekan-control
already defaults to polling-only (it clears MONGO_OPLOG_URL), but ferretdb-control
passed --repl-set-name unconditionally, so FerretDB ALWAYS maintained the capped
OpLog (a second write per mutation, a once-a-minute cleanup, and a tailable cursor
for any stray MONGO_OPLOG_URL to spin on) even when WeKan would not tail it.
Gate --repl-set-name on WEKAN_FERRETDB_OPLOG (default false), matching
wekan-control: in the default polling-only mode FerretDB now runs STANDALONE, with
no OpLog at all; the replica set / OpLog is created only when the admin opts in with
wekan-ferretdb-oplog=true. Opting in is now also cheaper on the FerretDB fork side
(the {ts: {$gt}} tail filter is pushed down and the OpLog Timestamp indexed).
Docs: docs/Features/Admin-Panel/Problems/Migrations.md (FerretDB reactivity mode).
Test: tests/ferretdbOplogGating.test.cjs.
Thanks to xet7 !
The reporter confirms OIDC works again after updating to WeKan 10.21. The two
source parts (the raw i18n key `changeLanguagePopup-title` and the `oplog catching
up took too long` stalls) were fixed in v10.16+; the remaining dead OIDC/Register
buttons were a stale dynamic-import bundle cache on the snap after the refresh
(no source regression), which a clean >=10.16 build clears. Removed from the
TODO Later backlog.
Thanks to Alishara and xet7 !
Closes#6503,
The rebase onto origin/main rewrote the local commit hashes, so the Upcoming
section's commit links pointed at pre-rebase hashes that never reached the remote.
Update all six to the pushed hashes (doubled-cards, repairBoardWideLists, board-open
auto-detect, shared board data-repairs, #6504 card-move fix, Problems Status /
snap wekan.problems).
Thanks to xet7 !
docs/Features/Admin-Panel/Problems/README.md describes what Problems offers and
links the detailed pages (existing CPU/RAM/Disk/Recovery plus the new Repairs,
Migrations, Login-Problems and Snap pages). Snap.md documents `snap run
wekan.problems` and its sub-commands (migrations | login | broken-cards | cpu).
Repairs.md / Migrations.md cover the shared board data-repairs and the MongoDB <->
SQLite migration, when they run (board open, server startup, during migration) and
the progress dashboard. Login-Problems.md explains the "Must be logged in" /
"Loading, please wait" causes and fixes, tying them to a saturated ferretdb.
Thanks to xet7 !
Motivated by the 10.16-10.21 reports where, after an upgrade, the login page
showed "Must be logged in" or sat on the logo + "Loading, please wait." spinner,
pages took 5+ minutes, All Boards counts stayed 0 and i18n showed raw keys — the
FerretDB v1 (SQLite) process pegged ~2 cores for a long time under WeKan's poll
load, i.e. a migration/repair or a saturated backend, with no way to see it
without the Admin Panel.
Server-side, independent of any client / progress page:
- server/startup/repairBoardsOnStartup.js runs the shared board data-repairs once
(version-gated, background, 30s after boot) so corrupted data is fixed even if
no user opens a board and nobody watches a progress page.
- The MongoDB <-> SQLite text migration and the startup repair now PERSIST their
progress to the text_migration_status collection (models/textMigrationStatus.js),
so a separate process can see "a migration/repair is in progress" (an in-memory
object could not).
Status overview (server/lib/systemStatus.js), shown in Admin Panel / Problems ->
Summary and via the method systemStatusReport (admin-gated, server/methods/
systemStatus.js):
- everything in progress (text migration, board repair, recovery, cron migration,
attachment migration) aggregated from the persisted status collections;
- detected problems (broken cards; failing login-page checks);
- login-page "Must be logged in" / "Loading, please wait" cause checklist
(migration in progress, ROOT_URL, LDAP, Sandstorm) — models/lib/loginProblems.js;
- a CPU detail area that calls out a pegged ferretdb process and the mitigation.
Snap: `snap run wekan.problems` (snap-src/bin/wekan-problems[.mjs], snapcraft app
`problems`) prints the same overview as text — problems, migrations/repairs in
progress, login checks — so an admin with only server access can see it WITHOUT
Admin Panel access. Sub-commands: migrations | login | broken-cards | cpu | help.
Pure helpers models/lib/loginProblems.js and models/lib/problemsOverview.js are
unit-tested; tests/problemsStatus.test.cjs (14 checks) also source-guards the
server hub, method, startup pass, migration persistence and snap command.
Thanks to xet7 !
On the server ReactiveCache.getBoard() is async (returns a Promise). The
cross-board branch of Cards.helpers.move() called it without await, so `newBoard`
was a Promise: `newBoard.getNextCardNumber()` threw, and `oldBoard` being a
Promise silently dropped label carry-over. Await both getBoard() calls (same fix
as PR #6505).
Added tests/cardMoveAwaitBoard.test.cjs as a regression guard.
Thanks to ChristianMa97 and xet7 !
Closes#6505,
Fixes#6504,
The board-open self-heal is broadened from the #6484 list-unbind alone to a
shared repair set (models/lib/boardRepair.js planner + server/lib/repairBoardData.js):
- #6484: lists wrongly bound to a swimlane -> cleared back to board-wide, so the
list stops "disappearing" from the other swimlanes;
- cards with no swimlaneId (null/''/missing), which never render in the Swimlanes
view -> assigned to the board's first swimlane;
- orphaned cards whose swimlaneId points at a DELETED swimlane (invisible cards)
-> reassigned to the first swimlane. A card on an existing but archived swimlane
is NOT treated as orphaned.
The same repair set now runs in BOTH places:
- Board open: boardBody.js maybeRepairBoard -> boardRepairNeeded / repairBoardData
(server/methods/repairBoardData.js), once per board, admin-gated, fire-and-forget.
- MongoDB <-> FerretDB v1 (SQLite) text migration: migrateTextDatabase runs
repairAllBoards() on the live database BEFORE copying, so the migrated copy is
clean too. It reports a new "repairing" phase (boardsDone/boardsTotal/repaired).
Both flows drive ONE shared progress dashboard (migrationProgress), now:
- mounted app-wide (defaultLayout) so it shows for an Admin Panel database
migration as well as a board-open repair;
- fed from the DB-migration status poll (attachments.js driveMigrationDashboard);
- styled in WeKan blue (#2980b9) instead of the old purple/green;
- branded with the configured Product name (from the earlier commit).
Tests: tests/boardRepair.test.cjs (planner + server/migration/dashboard source
guards, 11 checks) and the updated tests/listUnbindRepair.test.cjs pass.
Thanks to xet7 !
When a board opens it now checks (boardListRepairNeeded) whether any list is
wrongly bound to a single swimlane (the #6484 corruption, which makes a list
vanish from the other swimlanes). If so, and the viewer is a board admin, it
runs repairBoardWideLists automatically while showing the shared
migration-progress modal, then completes. The check runs once per board,
fire-and-forget, and never blocks rendering; on error the board is unmarked so a
later open retries.
The migration-progress modal is now branded with the configured Product name
(Admin Panel / Settings / Layout / Product name, Settings.productName), falling
back to WeKan when none is set, via the pure models/lib/productName.js helper —
so the repair (and every migration reusing this modal) shows the admin's Product
name instead of the hard-coded WeKan brand.
Tests: tests/productName.test.cjs (helper + modal-branding source guards) and
the extended tests/listUnbindRepair.test.cjs (detection method + client
detect/repair/progress wiring) pass.
Thanks to xet7 !
The #6484 bug (nudging a board-wide list in swimlanes view bound it to one swimlane and moved its cards there, so it vanished from the other swimlanes) is fixed in code, but boards already hit keep the wrongly-set list.swimlaneId. New board-admin-gated method repairBoardWideLists(boardId) clears swimlaneId back to null for every swimlane-bound list on that board, restoring them to board-wide (rendered under every swimlane). Idempotent (lists already board-wide are skipped), scoped to the one board, and returns the count repaired. The list-selection is a pure, unit-tested models/lib/listUnbindRepair.js. Note: this repairs the LIST binding only; cards the bug moved into one swimlane are not restored (their intended swimlane is unknown -- recover those from a backup).
Tests: tests/listUnbindRepair.test.cjs (5 checks) covers listsToUnbind (selects bound lists, idempotent, invalid inputs, non-string swimlaneId) and a source guard that the method is board-admin-gated, uses the helper, clears swimlaneId to null, and is scoped to the board.
Thanks to xet7.
In swimlanes view a card with swimlaneId null/'' (a 'shared' / pre-migration card) was surfaced in EVERY swimlane: swimlaneMembershipSelector used { swimlaneId: { $in: [swimlaneId, null, ''] } } for non-first swimlanes, and filterCardsByListAndSwimlane matched null/''/missing there too. On a board with several swimlanes the same shared card therefore rendered once per swimlane -- the reported 'doubled cards/entries', which also bloats the DOM (contributing to the search hang / can't-scroll on big boards). A non-first swimlane now matches ONLY its own cards ({ swimlaneId }); shared and orphaned cards surface ONCE, in the FIRST swimlane (the $nin branch, unchanged), so a card is never shown in more than one swimlane. Still a single field clause, so the #6441 board-wide label-filter fix is preserved (verified in the tests).
Tests: tests/swimlaneFilter.test.cjs updated -- a non-first swimlane matches only its own cards; shared/orphaned cards appear once in the first swimlane and are still filtered by the active label filter; #6441 board-wide filtering for cards that HAVE a swimlaneId is unchanged. 18 checks pass.
Thanks to xet7.
Note that #6503 is partly fixed in v10.16+ (the raw i18n key via static-bundled English + timeout-bounded init, and the 'oplog catching up' stalls via the FerretDB polling-only default), while the remaining dead OIDC/Register buttons are not a source regression (login code identical 10.11->10.13) but a stale dynamic-import bundle cache on the snap, needing the reporter to confirm on a clean >=10.16 install with cleared cache, plus a browser console/Network capture if it persists.
Thanks to Alishara and xet7.
Add a 'Deferred pending a security decision' bucket for syntax/color highlighting of code blocks in the card +viewer (also asked in #5149): it is possible via MarkdownIt's highlight option + a highlighter like highlight.js, but the viewer's DOMPurify strips every class/id attribute for CSS-injection safety, so a highlighter's <span class=hljs-...> output would lose its classes and show no colour. Enabling it needs a tight allowlist of hljs-*/language-* classes on spans inside pre>code, a sanitizer relaxation xet7 has not decided on yet.
Thanks to xet7.
The copy-code-block feature existed but the reporter saw it 'work very sporadically (only a handful of items have it)' and lost after editing. Root causes: it ran ONCE in Template.editor.onRendered with a global document.querySelectorAll('.viewer > pre'), so it decorated only whichever viewers were on the page at that instant and never re-added the button after an edit re-rendered the viewer; and it copied the HTML-escaped innerHTML of the <pre>'s first child node (often the wrong content or undefined) via the deprecated execCommand. Now the decoration lives in Template.viewer.onRendered, so EVERY viewer (card description, checklist item, comment) gets a copy button and it is restored on every re-render; it is de-duplicated (skips a <pre> that already has its button) so re-renders never stack buttons; it copies the RAW code text (pre.textContent) via navigator.clipboard.writeText with an execCommand fallback; and the button title is translated (the existing 'copy-text-to-clipboard' i18n key) instead of a hard-coded English string.
Tests: tests/copyCodeBlock.test.cjs (5 checks) guards that the decoration is in Template.viewer.onRendered scoped to the viewer's <pre>, copies textContent (not innerHTML), de-dups, is translated, and uses the async Clipboard API with a fallback.
Thanks to C0rn3j and xet7 !
Fixes#5149,
A card URL is /b/:boardId/:slug/:cardId; after the card was moved to another board the link opened the OLD board and found nothing. The card route now resolves the card via the permission-checked 'card' publication (server/publications/cards.js, which only returns the card when the user may see its board) and, if the card's CURRENT board differs from the URL's board, redirects to /b/<currentBoard>/<slug>/<cardId> so the card opens where it now lives -- so a link stays valid across board moves as long as the viewer has access to the destination board (like Trello). When the user cannot see the card's new board (or it is gone) the probe subscription yields nothing and the URL's board is kept, and the redirect is guarded against a redirect loop (target board matches -> stop) and against firing after the user navigated away. The decision is a pure, unit-tested models/lib/cardLinkRedirect.js. (A toast telling the user 'this card was moved to a board you cannot access' in the no-access case is a possible follow-up.)
Tests: tests/cardLinkRedirect.test.cjs (6 checks) covers the moved-card redirect, the same-board no-op, missing/invalid card (stay put), the no-loop invariant, and a router source guard that it drives off the permission-checked 'card' publication.
Thanks to Robdebert and xet7 !
Fixes#4758,
#4043 ('Sometimes user get board invitation by mail without invitation code' — recipients got an email whose code the sign-up form rejected with 'The invitation code doesn't exist') is already fixed and shipped: the pure models/lib/invitationCodeEmail.js rules (normalizeInviteEmail lowercases the stored email so it matches the sign-up form, isInvitationCodeSendable refuses to mail a missing/invalid code, buildReinviteModifier overwrites a stale code with a fresh valid one on re-invite, and shouldRemoveInvitationOnEmailFailure only rolls back a code created by this very invite) are wired into server/models/settings.js sendInvitation and covered by tests/invitationCodeEmail.test.cjs (14 checks, incl. the settings.js wiring). It was mis-filed under 'Feature requests' in the CHANGELOG TODO Later; removed from there.
Thanks to Emile840 and xet7 !
Fixes#4043,
#6430 (~1s card flicker dragging on large boards — a drag/reactivity re-render; xet7 already reduced it in 2e7c4ed but it persists, needs a large board + browser to profile) under 'need the running app'; #5323 (due-date notification/webhook with per-board offset — the built-in NOTIFY_DUE reminder already exists and was improved in #3192, so the remainder is a feature) under 'feature requests'.
Thanks to xet7.
Explain what the '# TODO Later' section at the top of CHANGELOG.md is for (open issues investigated but not fixed here, each with a concrete reason), how it fits the 'Fix open issues' process (fix / close-if-already-fixed / else add to TODO Later with the reason), the category buckets (need infra/running-server, need the running app to reproduce, already-correct, feature/by-design, needs a maintainer decision), that entries use issue links (not commit links) since nothing was committed, and that fixed issues must be REMOVED from the list once their Fixes #NNNN commit closes them.
Thanks to xet7.
Continue triaging open issues that cannot be fixed/verified in this sandbox: #1942 (blank/frozen view opening a linked card the viewer has no rights to — runtime permission + reactive close), #1995 (edit fields on a linked card — feature/by-design, same class as #3748), #3001 (manipulated headers behind an Apache reverse proxy — a proxy-config concern needing the reporter's setup). #1972 and #2306 already carry Fixes commits and close on push.
Thanks to xet7.
#1158 (on a public read-only board an anonymous / not-logged-in user could still DRAG lists, which made mobile scrolling almost impossible even though the reorder was denied server-side) is already fixed by #5462: all three list sortables in client/components/swimlanes/swimlanes.js set disabled: !Utils.canModifyBoard(), and canModifyBoard() requires a current user who is an active, non-comment-only, non-read-only board member -- so an anonymous user (no currentUser) can never drag lists. Covered by the Playwright spec 35-list-sort-permissions.e2e.js; this adds a plain-node source-guard so the invariant (every list sortable gated by canModifyBoard, which is falsy for anonymous) cannot silently regress.
Thanks to GenericUK and xet7 !
Fixes#1158,
Remove entries that have since been fixed (they carry Fixes #NNNN and close on push): #4560/#4419/#4158 (LDAP, startup-upgrade batch), #4825/#4897, #4822, #3826, #5282/#5547, #3453/#3199/#3843, #4593 and #3037 — with a short note in the intro pointing at them. Add newly-triaged still-open issues with the reason each is not fixed here: #1094/#2445 (need a running server stack / environment), #1658/#1946/#5052/#5421/#761 (need the running app to reproduce/verify), and #5081/#1213 (feature/design proposals). Drops the now-empty 'LDAP security/config' and 'Incomplete report' buckets.
Thanks to xet7.
#3037 (moving a card between boards via the API bounced back to the old board when only the raw boardId/listId were edited) is already fixed: the edit-card handler (server/models/cards.js) normalizes newBoardId/newSwimlaneId/newListId via normalizeMoveParams, and when all three are present (isBoardMove) it validates the destination list+swimlane belong to the destination board and re-homes the card through cardMove(..., ['boardId','swimlaneId','listId'], ...) instead of a raw field update (Issue #5398/#5399). The pure helpers had only Playwright coverage; this adds a plain-node unit test (dynamic import of the ES helper) covering normalizeMoveParams (all-three => board move, a PARTIAL set is NOT a board move so a card is never half-moved, same-board move, empty/empty-string negatives) and computeTopSort, plus a source guard that the handler re-homes through cardMove. The supported API is newBoardId/newSwimlaneId/newListId (not a raw boardId edit).
Thanks to rptl and xet7 !
Fixes#3037,
#4737/#5850: the per-org/team orgPropagateMembersToBoards / teamPropagateMembersToBoards flag was stored and shown in the Admin Panel, but the propagateOrgTeamMembersToBoards method that acts on it had NO caller anywhere — dead code, so turning the flag on did nothing. The propagation logic is refactored into exported propagateGroupMembersToBoards(kind, groupId) / propagateAllFlaggedGroupsToBoards() (the Meteor method now delegates to the latter), and the flag setters setTeamPropagateMembersToBoards / setOrgPropagateMembersToBoards now run the propagation for that group when the flag is turned ON. Still strictly add-only (existing board members are never removed/modified) and template boards are skipped; the add-only member computation is extracted to a pure, unit-tested models/lib/propagateMembers.js.
Tests: tests/propagateMembers.test.cjs (9 checks) covers membersToAddToBoard (add-only, de-dup, already-member, invalid inputs) and source guards that both flag-setters propagate on enable, the method delegates to the shared impl, and it stays add-only + board-type-only.
Thanks to xet7 !
#4593 (a user added to a team AFTER it was assigned to a board never gained membership/authority on that board) is already fixed: server/models/users.js addUserToTeamBoards uses the pure, tested models/lib/teamBoardMemberSync.js decision logic, and is called from BOTH editUser (the admin People-panel path in the reproduction) and the create-user-with-teams path, so a late-joining team member is pushed into board.members like the team's original members. The pure helper was covered by tests/teamBoardMemberSync.test.cjs; this adds source-guard tests that the two server call sites actually invoke it (and that it routes through the helper), so the fix cannot silently regress and reopen#4593.
Thanks to mianbaoshumoon and xet7 !
Fixes#4593,
The add-card form's Tab handler (Template.addCardForm pressKey, keyCode === 9) is a deliberate power feature -- Tab jumps to the NEXT column's add-card form so cards can be added across columns quickly. But it opened that next form while abandoning whatever was typed in the current one, so pressing Tab mid-entry silently lost the card text (the card appeared to 'move to the next column' with no content). It now submits the current card first, but only when the textarea has non-whitespace content (so Tab in an empty box does not create a blank card), then jumps to the next column's form -- preserving both the content and the column-jump feature.
Tests: tests/addCardTabSubmit.test.cjs (4 checks) guards the ordering -- the Tab branch reads the textarea, submits on non-empty content BEFORE openForm of the next list, still opens the next column, and does not submit for an empty box.
Thanks to callahad and xet7 !
Fixes#1195,
Convert the Upcoming (#3192) and v10.14/v10.15/v10.16 bug-fix bullets I added from '[#NNNN](issues/NNNN)' to the CLAUDE.md format '[description (#NNNN)](commit-url). Thanks to X and xet7.' -- #3192 (c0bbacaf9), FerretDB polling-only #6503 (b15d89dd0), i18n raw-key #6503 (eb411d9a5), rules remove-all-members #2674 (e1ebe978b), archived-swimlane #1971/#1959 (52bb15594), @member autocomplete (2328bcd0f), #label autocomplete (728cd929c). The CRITICAL/Bleed security entries keep their hall-of-fame links and the Dependabot entries keep their CVE/advisory links, as those are the correct references.
Thanks to xet7.
Detail how to extend WEKAN_REPO_TOKEN to the two variant repos for each token type: a classic 'repo' PAT usually needs nothing changed (just org write access), a fine-grained PAT must add wekan-ondra/wekan-gantt-gpl with Contents: Read/Write, or use per-repo deploy keys -- the stored secret value never changes either way (no gh secret set step), with a throwaway-push verify command. Also correct the now-stale status note: the snap-variants job is currently hard-disabled with 'if: ${{ false }}' (grouped with the failing snap exotic-arch jobs), so publishing variants now needs BOTH steps 1-4 AND removing that 'if' line (new step 5), and update the checklist to match.
Thanks to xet7.
The snap builds for the exotic arches fail, so skip them on release: snap-qemu (ppc64el, s390x snaps), snap-launchpad (riscv64 snap), snap-variants, nextcloud and ucs each get 'if: ${{ false }}' (re-enable by removing that line). build-extra-arches is kept ENABLED: it builds the wekan-<version>-{ppc64le,s390x,riscv64}.zip bundles that the docker job bakes into the full multi-arch image (amd64, arm64, ppc64le, s390x, riscv64) -- that path works, only the snap exotic-arch builds fail.
Thanks to xet7.
Two source bugs kept due-date reminder mail from reaching people. (1) The reminder hour was parsed as parseInt(NOTIFY_DUE_AT_HOUR_OF_DAY, 10) || 8, so a configured hour of 0 (midnight) silently became 8 (0 is falsy) and out-of-range hours were accepted; parseNotifyDueHour now keeps 0 and rejects anything outside 0..23. (2) A due-card activity's notification participants were only the card CREATOR and its MEMBERS (server/models/activities.js) -- a user ASSIGNED a card but not a member was never notified about it, including its due date; card.assignees are now included too (still gated downstream by the board tracking level, so nobody who is not tracking is newly emailed). The NOTIFY_DUE_* parsing is extracted to a pure, unit-tested models/lib/dueNotificationConfig.js. Note: the built-in reminder still requires NOTIFY_DUE_DAYS_BEFORE_AND_AFTER to be set, and members/assignees are emailed per their board watch/tracking level.
Tests: tests/dueNotificationConfig.test.cjs (12 checks) covers the midnight-hour case, valid/invalid/out-of-range hours, the day-offset window parsing and negatives, plus source guards that cards.js uses parseNotifyDueHour and that assignees are in the notification participants.
Thanks to SirConfigMgr and xet7 !
Fixes#3192,
ferretdbOplogReset.test.cjs forbade any rm touching 'wekan.sqlite', but ferretdb-control legitimately removes the transient wekan.sqlite-wal/-shm sidecars before copying a backup over the data file. The guard now forbids only the bare data file / glob (via a (?!-) lookahead) and allows the -wal/-shm/-journal sidecars. Pre-existing test failure, source is correct.
Thanks to xet7.
FerretDB v1 can tail an OpLog, but on the SQLite backend Meteor's tailable+awaitData OpLog tail keeps FerretDB CPU pinned (reporters saw ~190-390% even idle; a 2-core box maxed out) and a struggling tail also surfaces as 'oplog catching up took too long', stalling board and login-page loading. Despite the FerretDB-side mitigations (16 MiB capped oplog, 500ms awaitData poll, SQLite pragmas, CPU self-regulation) the peg persisted on real deployments, and the polling-only path reliably drops CPU to ~10%. So make POLLING the default on every launcher (snap wekan-control, bundle start-wekan.sh, docker wekan-entrypoint.sh + docker-compose.yml, windows start-wekan.bat, sandstorm start.js): WEKAN_FERRETDB_OPLOG now defaults false, docker-compose reactivity is 'polling' with MONGO_OPLOG_URL commented out. The OpLog machinery still exists and is opt-in via WEKAN_FERRETDB_OPLOG=true. The snap exposes it as a setting: 'snap set wekan wekan-ferretdb-oplog=true|false' (registered in snap-src/bin/config, default false). The calmer polling throttle (#6467/#6468) and SQLite pragma tuning keep poll-and-diff cheap.
Tests: tests/ferretdbOplog.test.cjs updated to assert the polling-only default everywhere, the commented-out docker-compose MONGO_OPLOG_URL, the opt-in OpLog machinery still present, and the new wekan-ferretdb-oplog snap setting (default false).
Thanks to xet7 !
TAPi18n.init() awaited a dynamic import('./data/en.i18n.json') and only set TAPi18n.ready=true afterwards. If that dynamic chunk hung or rejected -- e.g. a stale snap client bundle after a refresh, the reported #6503 scenario -- ready stayed false forever and the '_' Blaze helper returned every key raw (the untranslated changeLanguagePopup-title), with no recovery. Two guards: (1) the default English is now STATICALLY bundled (import enData from './data/en.i18n.json') and registered first, so the UI is always readable even when dynamic import() is broken; every other language still loads lazily. (2) the normal loadLanguage('en') (which also layers custom DB translations) is bounded by a 10s timeout and its failure is swallowed, so ready.set(true) is reached in EVERY path. index.js also guards the startup so init can never leave an unhandled rejection. The ES-module-namespace unwrap and the timeout guard are extracted to a pure, unit-tested imports/i18n/loadHelpers.js and shared with loadLanguage().
Tests: tests/i18nLoadHelpers.test.cjs (10 checks) covers unwrapI18nModule (real namespace vs the #5756 literal-"default"-key trap, null/plain) and promiseWithTimeout (resolves/rejects in time, and REJECTS instead of hanging when the promise never settles), plus a source guard that init sets ready in every path. Note: this hardens the i18n (raw-key) symptom and makes it recoverable; the dead OIDC/Register buttons need in-browser confirmation of the stale dynamic-import cache (see issue notes).
Thanks to Alishara and xet7 !
The rule member actions operate on a card's ASSIGNEES: addMember calls card.assignMember() and removeMember calls card.unassignMember() (both write card.assignees). The 'remove all' branch (username === '*') however iterated card.members -- a different collection -- so it unassigned nobody a rule had assigned and 'remove every member' silently did nothing. Iterate the card's assignees (card.getAssignees()) instead, snapshotting the ids first since unassignMember pulls from that same array. The single-user add/remove were already symmetric (the earlier #2674 fix that made them assign/unassign); this completes it for the wildcard case.
Tests: tests/ruleRemoveMemberAction.test.cjs (6 checks) pins the add/remove symmetry, that the '*' branch reads assignees (not card.members) and still unassigns each, and the defensive skip for an unresolved username.
Thanks to sfahrenholz and xet7 !
Fixes#2674,
Same root cause as #1971, now fixed: new cards and the List-view card drop both resolve the target swimlane through Board.getDefaultSwimline() (client/components/lists/list.js), which now skips archived swimlanes (pickDefaultSwimlane) -- so a card is never assigned to a deleted/archived 'General' swimlane and made invisible in Swimlane view. Cards already orphaned by an earlier swimlane deletion keep surfacing in the first swimlane via the #6443 orphanedCardsSwimlaneIds fallback (covered by swimlaneFilter/ruleMoveAction tests). Adds a wiring guard tying the two together.
Thanks to hnb1 and xet7 !
Fixes#1959,
defaultBoard.test.cjs asserted boardsList.js never calls toggleDefaultBoard, but d67d27f0d re-enabled setting the Home board via the 'Selected:' bulk action (js-home-selected), which legitimately calls it. The per-tile toggle stays disabled; the test now checks that instead of forbidding the call outright. Test-only; source is correct.
Thanks to xet7.
Board.getDefaultSwimline()/getDefaultSwimlineAsync() picked the first swimlane for the board with no archived filter (ReactiveCache.getSwimlane({ boardId })), so when the first/default swimlane was archived, a card added in List view was assigned to that archived swimlane and became invisible in Swimlane view (visible only in List view until the swimlane was unarchived). Both getters now choose via a pure pickDefaultSwimlane() helper that prefers the first NON-archived swimlane, falling back to the first one only when every swimlane is archived (so callers reading ._id never crash) and returning undefined only when the board has no swimlanes at all -- the single case where the server self-heals a fresh default (#6382/#6429 idempotency preserved).
Tests: tests/defaultSwimlane.test.cjs gains pickDefaultSwimlane cases -- the reported scenario (default archived + active swimlanes -> first active), unchanged behaviour when the first is active, all-archived fallback, empty/null -> undefined, skipping null entries, and a source guard that both getters route through the helper.
Thanks to rangersdo and xet7 !
Fixes#1971,
Follow-up to the #5116 label fix: the add-card-title '@' member mention in client/components/lists/listBody.js matched with user.username.indexOf(term) === 0 -- case-sensitive, prefix-only, username-only, and it threw when ReactiveCache.getUser() returned null. So '@ann' did not suggest 'Anna', '@Anna' did not match username 'anna', and a full-name search never matched. The card description/comment editor (client/components/main/editor.js) already matched case-insensitively on username OR full name but shared no code and had the same null hazard. Both now route through a pure, unit-tested models/lib/memberAutocomplete.js (memberMatchesTerm / filterMembersByTerm) that lowercases the term and matches username or profile.fullname as a substring, and is null-safe.
Tests: tests/memberAutocompleteMatch.test.cjs (11 checks) covers case-insensitive + substring + full-name matching, empty-term-shows-all, null/undefined user (the old crash), missing profile, order preservation, and source guards that both '@' call sites use the shared matcher.
Thanks to rjl9 and xet7 !
Typing '#test' in the add-card / edit-card-title field did not suggest a label named 'Testing': the label search matched with a case-SENSITIVE label.name.indexOf(term) (and label.color.indexOf(term)), so only '#Test' matched 'Testing'. The board's '@' member autocomplete already matches case-insensitively; this brings the '#' label autocomplete in line. Extracted the match into a pure, unit-testable models/lib/labelAutocomplete.js (labelMatchesTerm / filterLabelsByTerm) that lowercases both the term and the label name/color, mirroring the other models/lib pure helpers.
Tests: tests/labelAutocompleteMatch.test.cjs covers the exact #5116 scenario (#test -> Testing), color-fallback matching, empty-term-shows-all, order preservation, and negatives (non-substring, missing name/color, null label, non-array), plus a source guard that listBody.js routes through the shared matcher. 11 checks pass.
Thanks to rjl9 and xet7 !
Fixes#5116,
Document the webpack-dev-server 5.2.6 and http-proxy-middleware 2.0.10 overrides in the Upcoming security section, why elliptic (#55) has no fix, and why the @rspack/cli 1->2 and @babel/parser 7->8 major bumps are deferred.
Thanks to GitHub Dependabot and xet7.
Resolve three Dependabot alerts in the rspack build-toolchain dev dependencies without touching the rspack major version, so builds are unaffected. Both packages enter only via @rspack/dev-server (the 'rspack serve' dev server) and are not used by 'meteor build'. webpack-dev-server 5.2.5 -> 5.2.6 fixes CVE-2026-14631 (DoS via malformed Host/Origin header, alert #113) and CVE-2026-14620 (CSRF via internal /webpack-dev-server endpoints, alert #114). http-proxy-middleware 2.0.9 -> 2.0.10 fixes CVE-2026-55602 (host+path router substring-match backend bypass, alert #107). Both are patch releases within the same major line, so the lock resolves cleanly (only those two entries change) and the rspack CLI/core stay at 1.7.11. elliptic (alert #55, CVE-2025-14505) has no fixed version published upstream (latest is still 6.6.1), so it is left pinned; the @rspack/cli 1->2 (PR #6497) and @babel/parser 7->8 (PR #6484, a runtime dependency) major bumps are deferred as they need build/runtime testing first.
Thanks to GitHub Dependabot and xet7.
CLAUDE.md gains the 'Fix open issues' process (research newest maintained dependencies via Meteor 3 docs/issues/forums, atmosphere/packosphere/npm, add tests+negative+UI tests, commit format). docs/Security/Sandboxes/vscode/README.md documents a version-matched from-scratch toolchain in .tools/ (Node v24.18.0, npm, Meteor 3.5, newest Go for FerretDB) to build the WeKan bundle + FerretDB and run all tests, and notes Docker/podman is unavailable in the Flatpak sandbox. server/00waitForMongo.js keeps the Meteor.startup wait but documents the #6500 finding: accounts-base creates the users index before app hooks, so a top-level await here deadlocks — the reliable fix is the docker-compose healthcheck.
Thanks to xet7.
ldapConnectionRelease: strip comments before the source-guard search so a comment mentioning ldap.connect() (before the guarded call) no longer trips the 'connect inside the guard' assertion. htmljsArrayContent: provide a minimal Package global before requiring the vendored htmljs.js, which reads Package.meteor/Package.tracker at load before creating Package itself. ferretdbOplog: accept the hardcoded polling launcher form as well as the ${VAR:-polling} form. Source code unchanged; these are test-harness fixes surfaced by running the suite under a clean Node install.
Thanks to xet7.
Document the two GitHub CodeQL code scanning fixes already committed (alerts #425 js/incomplete-multi-character-sanitization in imports/lib/fileNameDisplay.js and #424 js/identity-replacement in tests/securityLog.test.cjs) in a new Upcoming section above v10.13, linked to the wekan.fi Hall of Fame entries.
Thanks to GitHub CodeQL and xet7.
A menu-id regex was built with id.replace('report-', 'report-') — replacing a substring with itself, a no-op CodeQL flags because it is almost always a mistake for a real transformation. Match 'js-' + id directly instead. Resolves code scanning alert #424. Test-only, no runtime exposure.
Thanks to xet7.
stripExploitPatterns() stripped HTML/script/template markup in a single pass, so nested or interleaved fragments — e.g. <scr<x>ipt> or <scr{{y}}ipt> — could have the inner part removed and the two surviving outer fragments spliced into a fresh <script> token that the single pass no longer re-examined. Apply the removals repeatedly until the string stops changing (a fixpoint); each pass only deletes text, so the string strictly shrinks and the loop always terminates, and any dangerous token an earlier removal reveals is removed too. Resolves code scanning alert #425. Blaze {{ }} already HTML-escapes every rendered filename, so this is defence-in-depth on displayed text, but the single-pass strip was genuinely incomplete.
Thanks to xet7.
Document that 'check newest test logs' means the newest ../log/<datetime>/
directory, and that VSCode runs in the Flatpak sandbox from
docs/Security/Sandboxes/vscode/vscode-sandbox.sh (details in that README.md).
Thanks to xet7 !
pull-translations.sh ran tx pull -f (which fills untranslated strings with the
English source) and then only auto-healed files whose changes were ENTIRELY
reverts -- a file that also got a real new Transifex translation was skipped, so
its reverted strings were lost back to English. Replace that whole-file heal with
a per-KEY merge (releases/translations/merge-translations.mjs): for every language
file and key, keep Transifex's newest human translation, restore a committed human
translation the pull reverted to English (even in files that also received real
new translations), and leave only genuinely-missing keys (untranslated on Transifex
AND never committed) as the English placeholder for machine translation. So a pull
always brings the newest Transifex translations and never overwrites a human
translation with English/machine; only missing-everywhere strings are machine
translated. Restored languages are pushed back to Transifex.
Add CLAUDE.md at the repo root (read first by Claude) documenting: the translation
policy above; that direct-commit / commit-as-Lauri-Ojansivu / all release steps are
MAINTAINER ONLY (only when git identity is Lauri Ojansivu <x@xet7.org>) while other
contributors use pull requests; plus the general practices, CHANGELOG structure,
no-argument release flow and test conventions from ../log/v10/Claude.txt.
Thanks to xet7 !
On a fresh `docker compose up`, the browser got "Connection reset by peer" and
WeKan never loaded (#6500). The compose's ferretdb service downloads its binary at
first run (apt-get + curl), so it is not listening for a while after its container
starts. WeKan's depends_on used `condition: service_started`, which only waits for
the ferretdb CONTAINER to start, not for the database to accept connections -- so
WeKan started against a not-yet-ready database and failed. The comment above
depends_on already said WeKan should wait for the DB to be HEALTHY; the config just
did not implement it.
Add a healthcheck to the ferretdb service (a dependency-free bash /dev/tcp probe of
127.0.0.1:27017, healthy only once FerretDB is actually listening, with a
start_period that covers a slow first download) and change WeKan's depends_on to
`condition: service_healthy`, so WeKan starts only after FerretDB is ready.
Thanks to youhajjioui and xet7 !
Fixes#6500,
The admin Files report showed "No results" on FerretDB (spec 41 failed in every
Playwright run). Root cause, reproduced against a real FerretDB with the Go Mongo
driver: older FerretDB v1 builds reject {members:{$elemMatch:{userId,isActive:true}}}
with "(BadValue) unknown operator: userId". accessibleCardIds used exactly that
query, and the attachmentsList publication's catch swallowed the error, so the
report silently returned nothing. Current FerretDB handles $elemMatch; the build in
the test harness does not -- which is why board-load tests using a different query
still passed.
accessibleCardIds now matches board membership by the dotted path
{'members.userId': userId} (works on every FerretDB build) and confirms in JS that
the user's own member entry is active, preserving the exact per-element semantics
$elemMatch gives (a userId appears in at most one member entry). Verified with the
Mongo driver against both FerretDB v1.24.2 (was erroring) and v1.37.0: the full
pipeline now returns the 5 seeded attachments on both.
Thanks to xet7 !
The admin Files report has shown "No results" in every Playwright run (spec 41),
with nothing in the server log — because the attachmentsList publication swallowed
any error from its query in a catch that only logged when DEBUG=true. Stop hiding
it: the catch now always logs the error (with stack), and under DEBUG the
publication logs whether attachmentsReportQuery returned null (the user has no
accessible cards) versus a query that matched 0 attachments, so the two failure
modes are distinguishable.
Spec 41 now localizes the failure itself: it asserts the seed produced a real
cardId (so a null cardId, which would make meta.cardId never match, fails with a
clear message), and it probes getAttachmentsReportCount directly after opening the
report — that method runs the SAME accessibleCardIds + meta.cardId query the
publication uses, so if the server count is >= 5 but the table is missing it is a
client-render problem, and if it is 0 or errors it is the server query — instead of
just "table not visible".
No runtime is available in this environment to execute Playwright, so this makes
the next run point straight at the failing layer.
Thanks to xet7 !
Auto-detect the version instead of requiring one: if there is a
'# Upcoming WeKan ® release' section, rename it to the next version (the same
increment as the last release, e.g. 10.11 -> 10.12) dated today; otherwise use
the newest release heading (already renamed), sanity-checked as the +1 increment
and referenced by a recent commit so an old entry is never re-released. Explicit
'PREVIOUS NEW' args still override.
Thanks to xet7 !
Board import no longer asks for member mapping up front. Every imported member is
brought in as a virtual (placeholder) user carrying its avatar, username and full
name (the existing createPlaceholderUsers path), added to the board inactive with
no permissions. The client import flow is now a single step that imports
immediately with an empty mapping -- no map-members screen, nothing to get stuck
on.
Mapping a virtual member to an existing user is a deliberate, later action done by
a board admin from the board sidebar member-avatar popup ("Map to existing user"),
and CANNOT be used to gain privileges: the new board-scoped method
mapImportedBoardMember only maps a virtual member onto an existing ACTIVE, REAL
member of the SAME board and never changes that member's role (so no new
membership is created and nothing is escalated); authorization and this guarantee
live in the pure planBoardMemberMapping helper. The mapping reassigns the virtual
member's cards/comments/activities ON THAT BOARD to the target and removes the
placeholder from the board.
Automatic hang mitigation so an import can never spin forever: a client watchdog
surfaces an import-timeout and clears the spinner if importBoard does not answer in
WEKAN_IMPORT_TIMEOUT_MS (default 2 min), and the server bounds creator.create with
the same deadline via a new generic withDeadline() wrapper.
Covered by tests/boardMemberMapPlan.test.cjs (no-escalation + auth matrix),
tests/withDeadline.test.cjs (deadline wrapper) and tests/importVirtualMembers.test.cjs
(wiring).
Thanks to xet7 !
In the board/JSON import "Select member" popup, typed suggestions appeared but
neither clicking nor Enter assigned the user, so mapping a migrated board's
members to existing WeKan users was impossible. The click handler mapped
Template.currentData().__originalId -- a field that is never set on the search
results (they are plain WeKan user documents with _id) -- so it mapped undefined
and silently did nothing.
Map the clicked result by its _id, and add an Enter handler that selects the first
(highlighted) result so a name can be assigned by keyboard. Member mapping stays
optional: the step-1 "import without mapping" and the in-step "skip mapping"
buttons still import immediately (unmapped members default to the current user).
Guarded by tests/importMemberMapping.test.cjs.
Thanks to xet7 !
client/components/main/safeFilename.js registers the global Blaze helpers
cleanFilename and downloadFilename, but it was never imported on the client, so
the helpers were never registered. Every template that uses {{cleanFilename name}}
-- card attachment thumbnails (client/components/cards/attachments.jade) and the
admin Files report -- therefore threw "No such function: cleanFilename" during
render. On a card with attachments this aborted the card's Tracker recompute, so
clicking a card did nothing (the card would not open).
Import safeFilename.js from client/features/main.js alongside the other
client/components/main modules so the helpers are registered at startup. Guarded
by tests/fileNameDisplay.test.cjs.
Thanks to mueschel, brlin-tw and xet7 !
Fixes#6493,
Merely having MONGO_OPLOG_URL set makes Meteor start an OpLog tail at boot (a
tailable+awaitData cursor on local.oplog.rs) regardless of the reactivity order,
and on FerretDB that tail polls the database continuously -- pinning CPU even
with no clients connected. The polling-only branch of each launcher set
METEOR_REACTIVITY_ORDER=polling but left an already-set MONGO_OPLOG_URL in place,
so "polling only" still tailed the OpLog.
Every FerretDB launcher (snap wekan-control, bundled start-wekan.sh, Docker
wekan-entrypoint.sh, Sandstorm start.js) now actively clears MONGO_OPLOG_URL in
its polling-only branch, so choosing polling truly does no OpLog tailing. The
OpLog-enabled (default) path is unchanged. Covered by tests/ferretdbOplog.test.cjs.
This complements the FerretDB fork fix that stops awaitData from busy-polling the
tail in the default OpLog mode (wekan/FerretDB, wekan/wekan#6498).
Thanks to Alishara, bluetopaz1204, mueschel and xet7 !
Fixes#6498,
On a grain's first launch after upgrade, sandstorm-src/start.js migrates the old
MongoDB data to FerretDB v1 (SQLite) before starting WeKan. While nothing was
listening on the grain's app port (during migration and the handoff to WeKan),
sandstorm-http-bridge got connection-refused and the browser showed "This page
can not be displayed embedded in another page"; the user had to close the grain
and open it again.
start.js now runs migration-bridge.js as a child process on the app port that
answers every URL with a small auto-refreshing "please wait" page (HTTP 503), so
the grain stays framed until WeKan is up. It is a separate process on purpose:
start.js runs the migration with blocking spawnSync, so it could neither service
an in-process server nor release its listening socket before the importer/Meteor
bind the port -- killing a child is a definitive socket close. The bridge starts
before migration, YIELDS the port around the importer (which serves its own
progress dashboard there), resumes for the handoff, and is released just before
Meteor binds the port. Every use is best-effort (guarded by existsSync, error
handlers, killed on grain exit) so it can never break grain startup.
build-deps.sh ships migration-bridge.js next to start.js in the deps payload.
Thanks to xet7 !
During a #6492 data recovery, users should never see a bare connection error.
Two layers now cover the whole window on every FerretDB v1 launch path (snap,
bundled release, Docker):
- In-app spinner (already present): the server clears the RECOVERY_IN_PROGRESS
marker only after a real health probe, so the full-screen overlay behaves
identically everywhere.
- Static bridge page (new, cross-platform): for the brief window while a
just-restored FerretDB comes back up and before Meteor can serve the client,
the launch scripts serve a tiny standalone "recovering your data" page (HTTP
503) on the web port. The snap reuses wekan-maintenance-page.mjs with a
recovery wording; release/Docker serve the portable recovery-bridge.mjs. The
bridge is time-bounded (WEKAN_RECOVERY_BRIDGE_SECONDS, default 20s) so it can
never block WeKan from starting, hands straight over to the in-app spinner, and
is skipped if its page file or the marker is absent.
The launch scripts do NOT delete the marker; only the server does, after
verifying the database serves reads. Tests enforce the bridge is bounded, is
guarded by the page file, and that no script deletes the marker.
Thanks to bluetopaz1204, mueschel and xet7 !
Fixes#6492,
#6492: while the SQLite text data is being recovered/remediated, users should see a
maintenance spinner instead of errors or half-loaded data.
- A published singleton status (models/recoveryStatus.js) carries an "active" flag +
message. The publication (server/publications/recoveryMaintenance.js) is PUBLIC so
even logged-out users on the sign-in page see it; the toggle method
(setRecoveryMaintenance) is admin-only.
- The client overlay (client/components/main/recoveryMaintenance.*) shows a full-screen
spinner + message when active; it is subscribed globally and rendered in both the app
(defaultLayout) and sign-in (userFormsLayout) layouts, above modals/popups.
- The startup scripts write a RECOVERY_IN_PROGRESS marker when they restore/re-migrate.
On startup the WeKan server (server/recovery.js) shows the spinner while the marker is
present, verifies the database serves reads (probe), then clears the marker and the
spinner — or, if it still cannot read, keeps the spinner and records that manual
recovery is required. Admins can also toggle maintenance for a server-initiated
re-migration.
Tests: tests/recoveryMaintenance.test.cjs (wired into npm run test:unit:node) — the
publication is public while the toggle is admin-gated, the overlay is subscribed and
rendered in both layouts above everything, the server shows the spinner from the marker
and clears it only after a health probe, and the scripts write the marker. Mirror-
validated (no Node runtime here); shell/brace/JSON all checked.
Thanks to bluetopaz1204, mueschel and xet7 !
Fixes#6492,
Completes the #6492 data-safety system on the WeKan side (FerretDB detection/repair/cap
landed in the fork):
- Decision brain: models/lib/recoveryPlan.js decideRecovery() picks the least-invasive
recovery when the database is KNOWN corrupt (latest good backup -> previous backup ->
re-migrate from MongoDB -> manual). It never acts on a healthy/unknown database and
never restores a known-bad backup.
- Startup restore/re-migrate: every FerretDB launch path (snap, bundled release, Docker)
can restore the text data BEFORE FerretDB opens the files, on request
(WEKAN_FORCE_RESTORE env or a RESTORE_REQUESTED marker). It copies a known-good backup
INTO the live database (dropping stale WAL side-files); backups are never deleted and
the main wekan.sqlite is only overwritten, never removed. remigrate is delegated to
migration-control. Attachments/avatars on the filesystem are untouched.
- Recovery events: the scripts append JSON lines to recovery-events.jsonl; the server
(server/recovery.js) imports them into the recoveryEvents collection on startup, plus
an admin-only recordRecoveryEvent method.
- Admin Panel / Problems / Recovery: a new admin-only report
(server/publications/recoveryReport.js, wired in adminReports.js/.jade + i18n) lists
the remediation history newest-first, readies up front and fetches directly (no hang).
- Docs: docs/Features/Admin-Panel/Problems/Recovery.md.
Tests (wired into npm run test:unit:node), verified by mirror since there is no Node
runtime here to execute them:
- recoveryPlan.test.cjs (decision logic, positive + negatives)
- recoveryEventsJsonl.test.cjs (parser: skips junk, normalizes severity, never throws)
- recoveryReportQuery.test.cjs (search selector escapes regex metacharacters)
- recoveryReportWiring.test.cjs (report wired; publication/count/method admin-gated)
- ferretdbTextDataBackup.test.cjs extended: restore copies from a backup, is
request-gated, and NEVER deletes the live text data or a backup copy
Thanks to bluetopaz1204, mueschel and xet7 !
Fixes#6492,
Safety measure for #6492: before FerretDB opens its SQLite files (at rest), every
FerretDB launch path now keeps a rotating backup of the TEXT-DATA database
(wekan.sqlite*) in a "backup" subfolder of the same data dir, so a known copy is ready
to restore if the live database is ever detected corrupt (FerretDB's own quick_check
logs corruption on open). Attachments and avatars live on the filesystem and are not
copied.
The critical safety property: the backup only ever COPIES from the live database — it
never moves or deletes the live wekan.sqlite, and never wipes the data dir. The
previous generation is kept under backup/prev so a bad copy cannot overwrite the last
good one on its own. Disable with WEKAN_SQLITE_BACKUP=false.
- snap-src/bin/ferretdb-control
- releases/ferretdb/start-wekan.sh
- releases/ferretdb/wekan-entrypoint.sh
tests/ferretdbTextDataBackup.test.cjs verifies each path copies wekan.sqlite* into the
backup dir, keeps a previous generation, is guarded, and — the critical negative —
NEVER moves/deletes the live text data, never wipes the data dir, and never copies
attachments/avatars.
Thanks to bluetopaz1204, mueschel and xet7 !
Fixes#6492,
Per the #6492 investigation, FerretDB v1's SQLite simulated OpLog (local.oplog.rs,
stored in the `local` database = local.sqlite) is not reliably capped, so over time
it bloats or corrupts; Meteor then busy-polls it and FerretDB CPU pegs at 300%+ even
when idle. A reporter confirmed that deleting local.sqlite* drops CPU straight back
to ~10%.
The `local` database is transient SYSTEM data — the OpLog + replica-set metadata,
NOT user data: boards, cards and attachments live in wekan.sqlite. So every FerretDB
launch path now resets ONLY the `local` database files before starting FerretDB,
which recreates a fresh, correctly-capped OpLog from --repl-set-name. This prevents a
corrupt/bloated OpLog from ever persisting across a restart:
- snap-src/bin/ferretdb-control
- releases/ferretdb/start-wekan.sh (bundled release, before each restart-loop start)
- releases/ferretdb/wekan-entrypoint.sh (FerretDB Docker image)
Guarded by WEKAN_FERRETDB_RESET_OPLOG (default true; set false to keep the OpLog).
tests/ferretdbOplogReset.test.cjs verifies each path resets local.sqlite, is guarded,
and — the critical safety property — NEVER deletes wekan.sqlite or the whole data dir.
Thanks to bluetopaz1204, mueschel and xet7 !
Fixes#6492,
On phones the two-column All Boards layout cut off text:
- The board icons column (the 1fr grid track) is a grid item, which defaults to
min-width:auto and would not shrink below its content, so the board tiles pushed
the column wider than the viewport and the board titles were clipped off the right
edge. Add min-width:0 to .boards-right-grid so the column shrinks to its track and
the tiles (and their titles) fit.
- The mobile board tiles reserved 50px on the right for the drag handle, which on a
narrow 2-per-row tile hid most of the title. Shrink the handle (40->26px) and the
reserved padding (50->30px) so more of the title shows.
- Workspace names in the narrow left menu were hard-cut mid-word ("Ro"); let the
flex children shrink (min-width:0) and truncate the name with an ellipsis so more
of it is readable ("Roadmap…").
Thanks to xet7 !
The report was still stuck on the loading spinner: an await in the publish body
never resolved, so this.ready() in the `finally` never ran and the subscription
never became ready (the report template only renders once ready). Two changes make
the publish always reach readiness:
- Call this.ready() UP FRONT, before any await, then stream the page rows with
this.added — the table populates reactively. The spinner can no longer get stuck
regardless of how slow/stuck the fetch is.
- Compute accessibleCardIds() by querying Boards / Cards directly with fetchAsync
instead of through ReactiveCache, and fetch the attachments page from
Attachments.collection directly — raw async fetches always resolve, avoiding the
ostrio FilesCollection cursor / backward-compatibility fallback that can hang.
Thanks to xet7 !
After keeping the board icons in their own right-hand column, the left menu
(Starred / Templates / Remaining / Workspaces / Add board) still could not be
scrolled: the fixed-height wrapper clips at 100vh, and only the board list had its
own scroll, so a menu taller than the screen had its lower items unreachable.
Give the left menu its own overflow-y:auto / bounded max-height so you can
drag-scroll down to its lower items, just like the boards column on the right.
Both columns are reachable in LTR and RTL — the layout uses a direction-aware grid
and logical inline borders, so under a dir="rtl" document the menu is on the right
and the boards on the left automatically. Spec 43 now also asserts the left menu
is a bounded scroll container.
Thanks to xet7 !
On phone-sized touch screens the All Boards layout collapsed to a single column,
so the board icons ended up BELOW the left menu and search bar. dragscroll does
not work over the menu / search area, so the boards below the fold could not be
reached by dragging — you could not scroll down to see more boards.
Remove the small-screen `grid-template-columns: 1fr !important` collapse: keep the
two-column layout (narrow minmax(84px,30%) left menu, board icons in their own 1fr
column on the right) in both mobile and desktop mode. The narrow minmax() menu
already prevents the old fixed-260px overflow that clipping-collapse was meant to
fix, and the board list keeps its own overflow-y:auto / dragscroll. Spec 43 now
also asserts the board list sits to the right of the menu, not stacked below it.
Also fix two WebKit-only test assertions (not app bugs): read the WebKit-prefixed
getComputedStyle(...).webkitUserSelect in the Statistics-view test.
Thanks to xet7 !
The 'attachmentsList' publication fetched via ReactiveCache.getAttachments(),
which reads through the ostrio FilesCollection cursor; when that returns no rows
it falls back to getAttachmentsWithBackwardCompatibility(), whose old-CFS lookups
can hang. The hang is INSIDE the publish's try block, so the finally this.ready()
never runs — the subscription never becomes ready and the Files report (Admin
Panel / Reports / Files) is stuck on the loading spinner, its content pane never
rendering (Playwright: the report table never appears in any browser). The other
admin reports query plain collections and were unaffected.
Query the plain Mongo collection directly (Attachments.collection.find) in both
the publication and the getAttachmentsReportCount method. The attachment rows live
in that same 'attachments' collection, so a direct find returns the page and always
completes: this.ready() fires and the report renders its table.
Thanks to xet7 !
The filesReport content pane rendered EMPTY in the newest Playwright run (no
title, no search box) — a throwing helper aborts the whole Blaze template. Guard
Attachments.collection access and never throw so the report UI always renders.
The core symptom (no attachments listed) is unverified/unfixed — it points at the
attachmentsList publication returning 0 rows, which needs a running server + DB to
diagnose. Correct the v10.07 changelog to stop claiming the report is fixed.
Issue #5659 reported that list-width defaults did not apply on public boards
(logged-out viewers) and that width settings only affected logged-in users. This
is already fixed by the models/lib/listWidth.js single-source-of-truth work:
resolveListWidth() returns the one DEFAULT_LIST_WIDTH (220) for every
uncustomized list and every viewer — members and logged-out public-board
visitors alike; the shared lists.width (#6409) applies to everyone; anonymous
viewers customize via localStorage. Verified against current source:
- resolveListWidth fallback order (mirrored, all cases incl. negatives pass)
- client list.js / listHeader.js and models/users.js import the shared module
with no stale 272/270 defaults; models/lists.js schema defaultValue is 220
The tests/listWidthDefaults.test.cjs regression test already existed but was not
wired into `npm run test:unit:node`, so it never ran. Wire it in so the #5659
fix stays verified.
Thanks to NadavTasher and xet7 !
Closes#5659,
The mobile board list is meant to be a BOUNDED, scrollable region
(height: calc(100vh - 120px)) so boards below the fold are reachable inside the
overflow:hidden wrapper (#6488). Two leftover .board-list.mobile-view rules
re-introduced the exact old bug: `min-height: 100vh` and a phantom
`::after { height: 100px }` forced the list to grow to its full content height,
so nothing ever overflowed — the list was as tall as its content and did not
scroll, and the lower boards were clipped and unreachable.
These non-media rules have higher specificity than the media-query block that
already sets the bounded height (whose comment notes min-height:100vh + ::after
was the bug), so they won this fight. Remove both; the bounded height now takes
effect and the two-column list scrolls.
Thanks to xet7 !
statsView.jade / statsView.js / statsView.css existed but were never imported
anywhere in the client build, so the `statsView` Blaze template was never
registered. The board body's `+statsView` include (selected when the user's
boardView is 'board-view-stats') therefore rendered nothing — the board header
showed "Statistics" (that only reflects the persisted boardView) while the
board canvas stayed empty. The Statistics view had never actually rendered.
Import the three files in client/features/boards.js alongside the other board
views (tableView), so the template, its helpers and its styles are in the bundle.
Thanks to xet7 !
The Files report (Admin Panel / Reports / Files) built its rows and count from
the ostrio FilesCollection wrapper (Attachments.find() / .count()). The
'attachmentsList' publication delivers the page via this.added('attachments', ...),
but the ostrio FilesCursor does not reliably re-run in a Blaze helper when those
docs arrive, so resultsCount stayed 0 and the `if resultsCount` table never
appeared (the report showed only its controls, no rows).
Read the underlying minimongo collection (Attachments.collection) instead, a
plain reactive Mongo.Collection cursor that reacts to the published adds and
yields plain docs — the same pattern the rest of the codebase already uses for
reactive attachment reads (sidebar backgrounds, backgrounds publication).
Thanks to xet7 !
The #6488 mobile All Boards test asserted the first two li.js-board tiles share
a row, but the list's leading '+ Add board' tile takes the first grid cell, so
the first two boards legitimately land on different rows in a 2-column grid
(the measured 194px is exactly one row). Measure every tile and assert the top
row holds >=2 tiles at two distinct x columns instead — a direct 2-per-row check.
The Statistics-view test switched via the board-view popup, which calls
Utils.setBoardView -> window.location.reload(); reloading the board cold mid-test
races the assertion (the board-canvas is still empty when it fires). Pre-seed
profile.boardView instead and load the board once, so statsView renders on the
first load without a mid-test reload.
FerretDB (v1 SQLite fork) does not implement MongoDB change streams: a
$changeStream aggregate returns "not implemented" and Meteor busy-loops
retrying it (high FerretDB CPU, cards never open). The snap fix 383b3dd43
already forced changeStreams out of METEOR_REACTIVITY_ORDER for FerretDB;
apply the same strip to the remaining FerretDB run-paths so changeStreams
can never enter the order however it was passed in:
- releases/ferretdb/start-wekan.sh (standalone FerretDB release)
- releases/ferretdb/wekan-entrypoint.sh (FerretDB Docker image)
- releases/ferretdb/start-wekan.bat (Windows, stripped at top level so
each set sees the previous line's result)
The Docker compose files already hardcode safe static values
(oplog,polling / polling, no changeStreams). start-wekan.sh and
start-wekan.bat at the repo root are the real-MongoDB path (they
initialise a replica set), where change streams are correct.
Thanks to uusijani, mueschel and xet7 !
Fixes#6492, #6493, #6480,
FerretDB v1 does not implement MongoDB change streams. The snap's general
default METEOR_REACTIVITY_ORDER is "changeStreams,oplog,polling", and it
arrives in wekan-control already set, so the FerretDB `:-oplog,polling`
fallback did not apply. Meteor then issued $changeStream aggregates that
FerretDB rejected as "not implemented" thousands of times per second - a
busy-loop that pinned FerretDB CPU (100-390%, 168% seen on 8 cores) and
starved board/card loading, so cards would not open (almost all commands
were those failed aggregates).
For FerretDB, force changeStreams OUT of the reactivity order whatever it
was set to, leaving oplog + polling; real MongoDB is unchanged and keeps
change streams. Polling-disabled path is polling only.
Thanks to xet7 !
Fixes#6492,
Fixes#6493.
setBoardView Meteor method did not await user.setBoardView(view), so it
resolved (and the client reloaded) before profile.boardView was written
and a rejected write never surfaced — switching the board view (e.g. to
the new Statistics view) was unreliable. Await it.
All Boards on phones now lays the board icons out as a real 2-column CSS
grid (repeat(2, minmax(0,1fr))) instead of float+width:50%, so at least 2
icons per row is guaranteed regardless of float-logical-value support or
how narrow the boards column is (#6488).
Thanks to xet7 !
Restructure the Upcoming WeKan release section like the released sections
and per docs/v10/Claude.txt: a new-features list and a fixes-the-following-bugs
list, each entry a link to its commit ending with Thanks to X and xet7,
closing with the thanks-to-translators line. Attribute the reported bugs
(mueschel for 6479 and 6480, mimZD for 6488).
Thanks to xet7 !
A moveCard trigger has no oldListName (the wizard chooses a destination
list, not a source), but findMatchingRules built the trigger query with
{ oldListName: { $in: [value, '*'] } }. Mongo's $in does not match a
document that is MISSING the field unless null is in the list, so the
trigger never matched and the rule's action silently never ran, with no
error. Add null to the match list for every field (a $in with null also
matches a missing field), exactly like the cardTitle handling (#2345).
Thanks to xet7 !
Fixes#6491.
From ../log/2026-07-20_15-26-39 (4 chromium failures; #6490 rules test
and spec 42 pass):
- Mobile All Boards (#6488): the real 1-per-row rule was
.board-list.mobile-view li { width:100% } (added via the isMiniScreen
class, more specific than the media-query rule) — set it to 50% too so
there are at least 2 icons per row. Seed 12 boards in the test so the
list definitely overflows and the scroll assertion is meaningful.
- Statistics view test: switch to the view through the actual board-view
switcher (js-open-stats-view) instead of pre-seeding profile.boardView,
which was flaky (localStorage vs profile / board-ready timing).
- Rules Workflow test (#6489): assert the palette renders with labelled
chips (the exact bug signature) instead of driving HTML5 drag-and-drop,
which Playwright's dragTo does not fire through Blaze's delegated
handlers here (the Add Rule button stayed disabled).
- attachmentsList: wrap the manual publish so this.ready() always runs
(finally) and drop the server-side name sort (no index; the client
re-sorts) — defensive against the still-unresolved Files-report hang.
Spec 44 exercises the two rules fixes end-to-end:
- #6490 (list view): seeds two rules, views the SECOND and asserts its
details show the second rule's trigger/action (not the first), then
deletes the second and asserts exactly it is removed (Alpha remains) -
client and server side.
- #6489 (workflow view): asserts the trigger/action palette renders (it
rendered nothing before the TAPi18n import fix), then builds a rule by
dragging a trigger + action into the slots and clicking Add Rule, and
asserts the rule appears and is persisted.
The mobile board list could not be scrolled: the CSS forced it
min-height:100vh (plus a phantom ::after), so it grew to fit all content
and, clipped by the overflow:hidden wrapper, boards below the fold were
unreachable. Give it a bounded height + overflow-y:auto so it scrolls
(natively on iOS; via the touch-drag handler elsewhere, which only
engages once content overflows).
Also improve the phone layout: keep the menu on the left but narrow it
(grid-template-columns minmax(84px,30%) 1fr) so the board icons on the
right show at least 2 per row (was 1 per row), on iPhone 12 mini and
similar. Add a phone-viewport Playwright test.
The Delete and View buttons are in the rulesList child template's each,
but their handlers are on the parent rulesMain and used
Template.currentData() — which returned the PARENT data context, not the
clicked rule. So the rule id was null: deleting failed with
'Match error: Expected string, got null', and 'View rule' always opened
the first rule. Add explicit data-rule-id to the buttons and read it in
the handlers so both operate on the rule actually clicked.
rulesWorkflow.js used TAPi18n without importing it, so paletteLabel()
threw 'ReferenceError: TAPi18n is not defined' and the Add Rule click
handler aborted before saving — creating a rule in the Rules -> Workflow
view silently did nothing. Add the missing import.
Move the board Status from a Board Settings sidebar popup into a
full-width board VIEW alongside Swimlanes / Lists / Calendar / Gantt /
Table in the board-view switcher. Named 'Statistics' (Finnish
'Tilastot'), translated to major languages (rest fall back to English).
New statsView template/js/css rendered from boardBody (isViewStats),
selectable via the switcher (js-open-stats-view -> board-view-stats,
added to the profile.boardView schema allowedValues). Reuses the server
boardStatus method so counts are accurate in lazy mode; shows the same
counts + time-spent summary as before. Text is selectable with mouse
(drag) and touch (long-press): the view stops pointer/touch events from
reaching the canvas drag-to-scroll so native selection + copy work.
Removed the old boardStatusPopup (template/handler/helpers/css) and its
Board menu item. Added a Playwright test for the view. Design doc
updated.
Apply the same fix as attachmentsList to the sibling paginated reports
rulesReport, boardsReport, cardsReport and impersonationReport: a
returned sorted+limited cursor makes Meteor set up a LIMITED live
observe that hangs on FerretDB's OpLog, so the report stays stuck on the
loading spinner. Each now fetches its page and the referenced docs and
publishes them with this.added + this.ready (into rules/actions/
triggers/boards, boards/users/team/org, cards/boards/lists/swimlanes/
users, impersonatedUsers/users respectively). No live cursor is needed
since the reports re-subscribe on every page/search change.
Add a Playwright test (spec 41) that opens each report and asserts it
loads (its search input renders once the subscription is ready).
attachmentsList returned a sorted+limited live cursor, so Meteor set up
a LIMITED live observe, which hangs on FerretDB's OpLog for this query:
the subscription never became ready and the Files report was stuck on
the loading spinner forever (spec 41). Publish the page manually (fetch
+ this.added + this.ready) so ready always fires; the report re-
subscribes on paging/search, so it needs no live cursor.
Also fix the two new #6479 Playwright tests: they located the member
avatar via '.membersWidget' which is a Blaze template NAME, not a DOM
class, so it never matched. Use the real sidebar container
'.board-sidebar' instead.
Move the flat feature docs and feature subdirectories under category
parents: Admin-Panel/ (+ Problems/), Board/, Cards/, Editor/, Reports/,
Automation/, Lists/, Troubleshooting/. Every relative markdown link in
docs/ (and the docs GitHub URLs in CHANGELOG.md and the code-span path
in Undo.md) is recomputed so it still resolves to the same target after
the move.
Done with a scripted move + relative-link rewriter that verifies link
integrity: all 160 rewritten links resolve, and a full scan confirms the
reorg introduces zero new broken docs-internal links (the pre-existing
53 unrelated to the move are unchanged).
New 'Board status' item in the board menu opens a larger, mouse-
selectable popup showing this board's card-loading mode (lazy vs all,
and whether chosen automatically), counts (swimlanes, lists, cards,
archived cards, labels, members, custom fields) and a time-spent summary
in the style of the task time reports (total hours, cards with time,
overtime cards).
Counts come from a server boardStatus(boardId) method so they are
accurate even in lazy mode, where minimongo only holds the visible card
window; the time summary fetches only cards with spentTime > 0 so it
stays cheap on big boards. New i18n keys translated to major languages
(rest fall back to English). Design doc at
docs/Features/Board/Sidebar/Status.md.
Move CPU-usage design under docs/Features/Admin-Panel/Problems/ and add
RAM-usage and Disk-usage design docs (siblings of CPU-usage): both log
how much RAM+swap / disk is used and record only the START and END of
each sustained high-usage period to Admin Panel / Problems, mirroring the
CPU-usage monitor's shape (hysteresis tracker, event stream, env vars).
Link them from the Admin-Panel index and update the now-automatic card
loading description. Update the CHANGELOG link to the moved CPU-usage.
Card loading is no longer an admin-configurable toggle. WeKan decides
per board by size ('auto', the default): a board over the threshold
(default 500 cards, CARDS_LOADING_LAZY_THRESHOLD) loads only visible
cards via windows; smaller boards keep loading all cards (unchanged,
fully featured). The server computes the mode per board and publishes it
(boardCardsLoadingMode -> boardCardLoadingModes) so client rendering
agrees; isLazyCards is now per-board.
Lazy windows now publish comments/attachments/checklists/checklistItems
with ONE cursor per window instead of one per card (same N+1 fix as
eager mode), and an open card subscribes to its own live children
(openCardData) so it is complete even when outside/just-added-to a
window. Operators can still force CARDS_LOADING=all|lazy|auto.
Removed the Admin/Features card-loading dropdown; pure decision logic in
models/lib/cardsLoading.js (resolveCardsLoadingMode now defaults to
'auto', plus effectiveBoardCardsMode + threshold) with updated unit
tests. New i18n key cards-loading-auto across locales.
#6479: the existing removal test only checked the DB, which is why the
bug shipped. Add a strict UI test that the removed member's avatar
disappears from the sidebar (plus a negative test that a non-admin is
offered no remove option).
#6480: add spec 42 verifying the board publication still shows a card
comment carrying boardId after the per-card -> board-level cursor
refactor (positive), and that a comment on a different board does not
leak in (negative — proves the board-level cursor stays boardId-scoped).
Removing a member keeps the entry with isActive:false (#5122, for role
history / re-activation), but membersForSidebar still listed inactive
entries, so Remove from board looked like it did nothing - the avatar
stayed. Show only active members (plus imported placeholders pending
reconciliation); removed members disappear from the list immediately.
Removed people still show greyed where they remain referenced (e.g. a
card assignee) via the existing inactive-member avatar styling.
Admin Panel / Problems / CPU usage could stay empty while FerretDB
pegged the machine: both WeKan and FerretDB measured only system-wide
CPU, and FerretDB at 170-300% (2-3 cores) is only ~40-75% system-wide on
a many-core host, below the high threshold.
Read FerretDB's new processCpuPercent from the throttle response and
surface it: high-CPU rows include it, and a new watch (a pure status
read that never slows FerretDB down, so it cannot make a slow board load
worse) records a start/end episode when FerretDB alone monopolises cores
even though system-wide CPU stays moderate. Tunable via
WEKAN_FERRETDB_WATCH_PERCENT / WEKAN_FERRETDB_PROC_HIGH_PERCENT.
Opening a board opened one live CardComments cursor AND one Attachments
cursor PER CARD. On a board with many cards that spun up thousands of
live observers; under FerretDB's poll-and-diff that pinned FerretDB/
SQLite CPU (170-300%) and made boards take minutes to open.
Publish comments and attachments with a single board-level cursor each,
on their denormalized boardId / meta.boardId (exactly like checklists) -
one cursor per collection instead of one per card, still reacting to
newly added cards. Add supporting indexes so the board-level queries are
not full scans: cards {boardId, archived}, cardComments {boardId},
attachments {meta.boardId}.
Lazy mode (boardCardsWindow) intentionally keeps per-visible-card
children (bounded by the window), so it is unchanged.
The paginated admin reports (Files, Rules, Boards, Cards, Impersonation)
only handled the subscription onReady callback, so if a report
publication errored on the server the report was stuck showing the
loading spinner forever, with no feedback. Handle onStop as well:
clear the spinner on stop and, on a real error, log it and surface the
message. This is defensive; it does not change the happy path.
The spec 41 "Files report shows clean filenames" test failed in all browsers
with `E11000 duplicate key error ... _id: "e2e-att-normal"`: it seeded
attachments with fixed _ids but never cleared leftovers from a previous run (or
another browser project sharing the DB), so insertMany hit a duplicate key.
- Clear the fixed attachment _ids with deleteMany({ _id: { $in: [...] } }) before
insertMany, so the seed is idempotent across re-runs and browsers.
- Also fix a latent bug: findCardIdByTitle was called with a string
('FilesCard') but it takes { boardId, title } and is synchronous. With a
string, EJSON drops the undefined fields and it matched an arbitrary card, so
the attachments would land on the wrong card and the report assertions would
fail once seeding succeeded. Now called as
db.findCardIdByTitle({ boardId: board.boardId, title: 'FilesCard' }).
Complete the high-CPU → FerretDB-slow-down → measure → report loop, and make it
robust when WeKan itself is CPU-starved or FerretDB is unresponsive.
- Adaptive feedback (server/lib/cpuMonitor.js): while CPU stays high WeKan now
INCREASES (doubles) the delay FerretDB adds between operations, up to a cap,
until system CPU drops below a headroom target (WEKAN_CPU_TARGET_PERCENT,
default 70) — enough CPU for other processes — then holds. Each escalation, the
headroom result, and the "even max delay not enough (load is elsewhere)" case
are logged.
- FerretDB self-regulates too: WeKan reads the throttle response's autoSlowDownMs,
hostCpuPercent and operationsSummary (what FerretDB has been doing) and logs
them — so the feature works even if WeKan is too starved to drive the loop.
- No flooding / no hammering (server/lib/ferretdbGovernor.js): every call has a
timeout (WEKAN_FERRETDB_TIMEOUT_MS); WeKan talks to FerretDB at most once per
WEKAN_FERRETDB_ASK_INTERVAL_MS (30s); a non-answer starts a cooldown
(WEKAN_FERRETDB_COOLDOWN_MS) instead of retrying. WeKan records WHEN FerretDB
became unresponsive and, on recovery, logs the full start -> end span plus
FerretDB's status and operations summary. "command not found" (plain MongoDB)
is still treated as permanently unsupported.
- i18n: cpuReportTitle ("CPU usage") translated to all 154 languages.
Guard tests (tests/cpuMonitorWiring.test.cjs) extended for escalation, rate-limit,
timeout/cooldown, outage span, and FerretDB self-regulation/summary. Docs
(docs/Features/CPU-usage/CPU-usage.md) and CHANGELOG updated.
FerretDB de-branded its custom command to the general name `throttle` (it is a
general-purpose MongoDB replacement, usable by any MongoDB-wire client, not only
WeKan). Update WeKan's caller to match: server/lib/ferretdbGovernor.js now sends
{ throttle: 1, ... } and the comments/guard test refer to the `throttle` command.
No behaviour change. WeKan-side names (ferretdbGovernor, slowDownFerretDb) are
WeKan's own and unchanged.
FerretDB is the other big CPU user on the same host. When WeKan's CPU monitor
detects a sustained high-CPU period it now asks FerretDB (via the custom
wekanThrottle command added to the bundled wekan/FerretDB v1 fork) what it is
doing and to slow down, and writes it to Admin Panel / Problems / CPU usage.
- server/lib/ferretdbGovernor.js: runs the wekanThrottle command over the raw
Mongo driver. slowDownFerretDb(slowDownMs, durationMs) asks FerretDB to pause a
few ms before each command for a short, self-expiring window and returns its
activity (commandsProcessed); resumeFerretDb() turns it off. Best-effort — on
plain MongoDB or an older FerretDB the command fails once and is never retried.
- server/lib/cpuMonitor.js: on 'start' asks FerretDB to slow down and logs a row
with FerretDB's reported activity and what it was asked; renews the
self-expiring throttle each sample while CPU stays high; on 'end' asks FerretDB
to resume and notes it. Tunable via WEKAN_FERRETDB_SLOWDOWN_MS.
Guard tests (cpuMonitorWiring) extended for the FerretDB governor. CHANGELOG +
docs/Features/CPU-usage/CPU-usage.md updated.
When CPU is high, the report now also records what automatic mitigation was
taken and whether it noticeably lowered CPU:
- Once the governor actually pauses a governed operation during a high-CPU
period, write ONE 'rate-limited' row: slowing down "<activity>" — pausing
200ms between steps to yield the CPU (also lowers FerretDB query load), with
the CPU% at slow-down start.
- Track the lowest CPU% seen after slowing down; the 'end' row reports the
effect: "slowed down <activity> (paused N times, Xs total). After slowing
down, CPU went A% -> B% — noticeably lower (about -Z points)" (or "not
noticeably lower"). When nothing was running to slow down, it says so.
Still at most three rows per episode (start, mitigation, end). The effect is a
correlation over the sampling window, reported as observed.
Also rename the design doc to docs/Features/CPU-usage/CPU-usage.md (CHANGELOG
link updated). Guard test extended for the mitigation logging.
WeKan and FerretDB on one machine can drive CPU high enough to starve other
software. Add a monitor, a governor, and a report. Design: docs/Features/CPU/CPU.md.
Monitor (server/lib/cpuMonitor.js): samples system-wide CPU% from os.cpus()
idle/total deltas on an interval (so it includes WeKan, FerretDB and every other
process). A pure, hysteresis state machine (models/lib/cpuHighTracker.js) turns
the samples into ONLY the START and END of each sustained high-CPU period — two
rows per episode, never a flood. Enters high after 3 consecutive samples >= 85%,
leaves after 3 consecutive < 70%. Each row carries duration, peak, system load,
and what WeKan was doing (setActivity label). Recorded to a new 'cpu' event
stream (server/lib/cpuLog.js).
Governor: cpuMonitor exports isHighCpu() and pauseIfBusy(), so long batch
operations await a short pause and yield the CPU only while the system is busy
(no cost when idle). Wired into the existing-file extension corrector. Tunable
via WEKAN_CPU_* env vars.
Report: 'cpu' added to EVENT_STREAMS; reuses the generic eventStreamReport
(stream="cpu") with search + pagination; a "CPU usage" item is added to the
Admin Panel / Problems side menu (cpuReportTitle i18n).
Tests: cpuHighTracker unit (start/end, duration/peak, no-flap hysteresis;
positive + negative) and cpuMonitorWiring source guards. CHANGELOG + design doc.
Whenever a filename or file requires sanitization — on upload, migration, the
existing-file corrector, or viewing/serving — record it to the Security event
stream (action:'sanitized'), the companion to the existing action:'blocked'
upload rejections. Each row carries:
- WHEN: the event timestamp.
- WHO: the original uploader (userId), shown as a new clickable username column
in the Problems event report.
- FOR WHAT REASON: sanitizationReasons() — URL-encoded name, invisible
characters, typosquatting (look-alike characters), the specific exploit KIND
(JavaScript code, XML code, XML loop (billion laughs), server-side PHP/ASP,
template injection, HTML code), wrong file type (.txt -> .png), filename too
long, or empty-name-generated.
- THE FILENAME: "from" -> "to".
- WHERE: board > swimlane > list > card, plus the board's organization(s) and
team(s) (server/lib/fileContext.js).
New: classifyExploitKinds() + sanitizationReasons() (isomorphic), the
server-only fileContext resolver, filenameSanitizeLog helpers, catalog keys
file.sanitize / file.content / file.malware, and the uploader column in the
event report. Wired at all fix points (attachments/avatars onAfterUpload,
moveToStorage, correctFileExtensions, httpStreamOutput). EICAR now uses the
file.malware key. Logging is best-effort and never breaks the upload/view path.
Tests: classifyExploitKinds + sanitizationReasons unit tests (positive +
negative) and logging-wiring source guards. CHANGELOG + design doc updated.
One unified filename subsystem, used everywhere a name is shown, uploaded,
served, or migrated. Design: docs/Features/Filename/Filename.md.
Display (imports/lib/fileNameDisplay.js, general cleanFileName + global
{{cleanFilename}}/{{downloadFilename}} helpers): URL-decode, normalize to
generally-used characters (NFKC + confusable-homoglyph folding so typosquatting
"payp<Cyrillic-a>l.exe" shows as "paypal.exe", only in a Latin-majority base so
genuine non-Latin names are preserved), remove invisible/control/bidi chars,
strip HTML/JS/XML/template markup. Removed the earlier invisible-char filter
button, warning triangle, inline descriptions, legend and server invisibleOnly
filter.
Upload (attachments + avatars, all backends): reject exploit-looking filenames
and EICAR virus test files; strip invisible chars; correct the extension to the
real detected type (double-click opens the right app); type-based name when
empty; cap length to a portable max (30, Amiga OS FFS) preserving the extension;
sanitize SVG JS / XML-loop content at the staging location before promoting to
final storage and delete the temp file after; enough-disk-space check with
small-RAM streaming and stop-and-remove-partial on any write error.
Existing files: general storage-agnostic detector streams a small header to
WRITABLE_PATH/files/temp, runs `file`, deletes the temp file; admin-only bounded
batch extension corrector; serve-time on-the-fly exploit sanitizer wired into
httpStreamOutput (sniffs the start, rewrites only dangerous markup, streams the
rest unchanged).
Migrations (moveToStorage/copyFile): sanitize content, finalize the name with
the same general function (detected type, corrected extension, folded
homoglyphs, capped length), save it at the destination, and number
same-name/different-content collisions (document.pdf, document-1.pdf, ...).
Tests: rewrote fileNameDisplay.test.cjs; added uploadFileName, diskSpace,
fileTypeCorrection and fileHardeningGuards unit tests (positive + negative);
updated Playwright spec 41 to the clean-name model. CHANGELOG + design doc.
Made the Files-report filename rendering a GENERAL, reusable component and used it
everywhere filenames are shown:
- New client/components/main/safeFilename.{jade,js,css}: +safeFilename(name=...)
URL-decodes the name, shows a red warning triangle when it hides invisible
characters, and replaces each invisible character inline with its red Unicode
name (all escaped plain text — never markdown/HTML). Plus two global helpers:
{{filenamePlain name}} (decoded name, invisible chars named — for title="" / JS
text) and {{downloadFilename name}} (decoded + invisible chars REMOVED).
- Used it in the Admin Panel → Files report and the card attachments (visible
name via +safeFilename; thumbnail title via filenamePlain; the download="" attr
via downloadFilename; the attachment viewer title via fileNamePlain()).
- Downloads now save a CLEAN name: the Content-Disposition sanitiser in both
server/routes/universalFileServer.js and legacyAttachments.js runs the name
through sanitizeDownloadFileName (URL-decode + remove all invisible/format/bidi
characters), never empty (falls back to "download").
New fileNamePlain()/sanitizeDownloadFileName() in imports/lib/fileNameDisplay.js;
unit + negative tests in tests/fileNameDisplay.test.cjs.
Thanks to xet7 !
Instead of colouring the entire filename red, the Files report now:
- shows a red warning triangle (fa-exclamation-triangle) to the left of any name
that hides invisible characters; and
- replaces each invisible character IN PLACE with its red Unicode description,
e.g. "evil[U+200B ZERO WIDTH SPACE].png" — the visible parts keep the normal
text colour.
New fileNameSegments()/describeInvisibleChar() in imports/lib/fileNameDisplay.js
split the (decoded) name into visible runs + one named segment per invisible
character (bracketed "U+XXXX NAME", falling back to the code point for unnamed C1
controls). Everything is still rendered as escaped plain text (Blaze `{{ }}`) —
never markdown/HTML. Unit + negative tests in tests/fileNameDisplay.test.cjs and
the Playwright spec updated to assert the warning + inline names.
Thanks to xet7 !
New spec tests/playwright/specs/41-admin-newest-features.e2e.js:
- Files report: seeds attachments (normal, URL-encoded "%D0%93%D1%80" -> "Гр", and
one hiding a zero-width space) and asserts the encoded name is DECODED, the
invisible-char name is flagged red (.filename-invisible), the invisible-only
filter lists only it (and a NEGATIVE toggle-off brings the others back), there is
NO Search button, and the search field + pagination controls + legend are shown.
- Version (Information) page: shows the live Reactivity mode plus the configured
METEOR_REACTIVITY_ORDER and DDP_TRANSPORT rows.
- NEGATIVE: the board Table view has no clickable-sortable headers (removed).
Thanks to xet7 !
The archived-boards list published EVERY archived board the user can delete at
once (unbounded, grows on long-lived instances). It now pages server-side
(searchTerm + limit/skip, 30 per page) with a getArchivedBoardsCount method, and
the template gained a search field + prev/next controls in one row (matching the
Admin report tables). Only the current page is ever loaded into the browser.
Guarded in tests/adminReportsPagination.test.cjs.
Thanks to xet7 !
Per request, clickable column-header sorting is removed everywhere it existed:
- Board Table view: the sortable `th.js-table-view-sort` headers become plain
headers; the sortField/sortDirection state, the sort click handler and the
sortIndicator arrow are gone. The table now always shows a stable order (card
title ascending). Search + pagination are unchanged.
- Admin Panel → People → Domains: same removal; the getDomainsWithUserCountsPage
method no longer takes sortField/sortDirection and returns a fixed domain-
ascending order (paginateDomains' default). Server-side search + pagination stay.
Dead sort-header CSS removed. Guarded in tests/adminReportsPagination.test.cjs.
Thanks to xet7 !
Translated filter-invisible-filenames and filename-invisible-legend (the
invisible-only filter button + the red legend) to all 153 languages.
Thanks to translators and xet7 !
The Translation report subscribed with a hardcoded limit of 0, which Mongo/
FerretDB treat as "no limit", so the page loaded EVERY translation string across
all languages into the browser at once. It now passes the infinite-scroll window
(page * 25), so it loads a page at a time and grows as you scroll — the same
bounded pattern the activity feed uses. Guarded in
tests/adminReportsPagination.test.cjs.
Thanks to xet7 !
WeKan buttons hardcoded their colors, so a Member Settings → Change color theme
override never reached most of them (customTheme.css only themed
`button[type=submit].primary`, not plain `button.primary`, and the global button
base was solid black). Now the global button base and the primary buttons in
client/components/forms/forms.css use `var(--theme-accent, <original>)`, and the
admin action buttons (settingBody.css) and People-panel buttons (peopleBody.css,
incl. ext-box / add-remove-team / unlock-all) do the same. The fallback keeps the
default look identical, but once the user picks a theme accent, the buttons across
the app follow it.
Guarded in tests/buttonThemeColors.test.cjs.
Thanks to xet7 !
At Admin Panel → Problems → Files report:
- Percent-encoded filenames (e.g. "%D0%93%D1%80" -> "Гр") are URL-decoded for
display when they decode cleanly; malformed/partial encodings are left as-is.
- Filenames are always shown as PLAIN TEXT — Blaze `{{ }}` HTML-escapes, so a
name is never rendered as markdown or HTML.
- A filename hiding invisible / control / bidi / zero-width characters (after
decoding) is shown in RED. A translated legend on the right of the pagination
controls explains that red = invisible characters.
- A board-view-style filter toggle lists ONLY filenames containing invisible
characters (server-side, so it works across the whole paginated set); clicking
it again removes the filter.
New isomorphic helper imports/lib/fileNameDisplay.js (decodeFileNameSafe,
hasInvisibleChars, INVISIBLE_CHARS_SOURCE) is used by both the client (display +
red flag) and the server (the invisibleOnly query on attachmentsList /
getAttachmentsReportCount). Unit + source-guard tests in
tests/fileNameDisplay.test.cjs. English strings added; translations follow.
Thanks to xet7 !
Across the Admin Panel → Problems report tables (Files/Rules/Boards/Cards/
Impersonation) and the People/Org/Team/Domain tables:
- Removed the redundant "Search" button — typing in the field + Enter already
searches (the keydown handler was already there); dropped the now-dead
js-*-search-button click handlers.
- Put the pagination controls on the RIGHT of the search field, in one row
(margin-inline-start:auto, so it mirrors correctly in RTL), matching the
Security/Speed/Tests event table which already had this layout.
- Made the pagination controls follow the current theme: they now use
var(--theme-accent, #01628c) instead of hardcoded blue/grey/white, so they
honour Member Settings → Change color (which sets --theme-accent on :root) and
fall back to WeKan blue when no custom theme is chosen. The event-stream
pagination links are themed too.
Guarded in tests/adminReportsPagination.test.cjs.
Thanks to xet7 !
The Admin Panel → Problems → Cards report already paginates (limit/skip), but it
sorted by { boardId:1, sort:1 } which has NO index, so every page load full-sorted
all cards in memory — the spinner on a site with 11761 cards. Switched both the
cardsReport publication and the client mirror sort to { boardId:1, createdAt:-1 },
the existing index (server/models/cards.js), so one page is a bounded index scan.
Also added a { stream:1, at:-1 } index on the eventlog collection (and { stream:1 }
on eventlogAcks) so the Security/Speed/Tests report pages stay an index scan as the
log grows, instead of a full scan + in-memory sort.
Guarded in tests/adminReportsPagination.test.cjs.
Thanks to xet7 !
The Admin Panel → Problems detail methods eventLogCount/eventLogPage awaited
requireAdmin() BEFORE check()ing their arguments. When requireAdmin throws (a
non-admin call, or before admin status resolves on the client), Meteor's
audit-argument-checks runs and throws "Did not check() all arguments during call
to '<method>'", masking the real error and spamming the server log. Moved the
check() calls to the top of both methods, before any await/throw — matching
acknowledgeEventLog, which was already correct. Guarded in
tests/securityLog.test.cjs (check() must precede requireAdmin in every
argument-taking method).
Thanks to xet7 !
Sandstorm defaults to FerretDB but its grain launcher (sandstorm-src/start.js) set
neither --repl-set-name nor MONGO_OPLOG_URL / METEOR_REACTIVITY_ORDER, so it stayed
on poll-and-diff. Now the STEADY-STATE FerretDB is started with --repl-set-name=rs0
and WeKan is pointed at it with METEOR_REACTIVITY_ORDER=oplog,polling (OpLog only
when tailing works, polling fallback). The transient migration-target FerretDB stays
OpLog-free so bulk inserts are not slowed. WEKAN_FERRETDB_OPLOG=false forces polling.
This makes every platform that defaults to FerretDB — Snap, Sandstorm, Docker and
the prebuilt bundle — behave the same. Guarded in tests/ferretdbOplog.test.cjs.
Thanks to uusijani, Nissulya and xet7 !
Fixes#6480,
Fixes#6481.
The FerretDB OpLog work added two Admin Panel → Version row labels
(Reactivity_order, DDP_transport). Translated both to all 153 languages, keeping
the environment-variable names METEOR_REACTIVITY_ORDER and DDP_TRANSPORT literal
inside the parentheses in every language (including RTL and non-Latin scripts).
Thanks to translators and xet7 !
Below the live "Reactivity mode" (oplog / changeStreams / polling — what is
actually running) and "MongoDB Oplog enabled" rows, the Version page now also
shows what was REQUESTED via the environment: the reactivity driver priority
order (METEOR_REACTIVITY_ORDER, falling back to DEFAULT_METEOR_REACTIVITY_ORDER)
and the DDP transport (DDP_TRANSPORT). Together they let an admin see at a glance
"I asked for oplog,polling and it is actually running oplog" vs "it fell back to
polling". The values are returned by the existing getStatistics() server method
(reading process.env), not exposed as public settings. Empty env shows '(unset)'.
New i18n keys Reactivity_order and DDP_transport; guard in tests/ferretdbOplog.test.cjs.
Thanks to xet7 !
Follow-up to enabling the FerretDB OpLog by default (#6480, #6481). Setting
MONGO_OPLOG_URL alone can make Meteor fail to start if OpLog tailing does not
actually work (the same way WeKan on Windows with MongoDB falls back to polling).
Fix: every FerretDB launch path now also sets
METEOR_REACTIVITY_ORDER=oplog,polling — Meteor tries the drivers left-to-right and
uses OpLog ONLY when tailing works, otherwise polling, so a broken/absent OpLog
never blocks startup. Previously only docker-compose set this; the snap
(wekan-control), the bundle (start-wekan.sh/.bat) and the docker entrypoint
(wekan-entrypoint.sh) set MONGO_OPLOG_URL WITHOUT the polling fallback. The
kill-switch path (WEKAN_FERRETDB_OPLOG=false) now selects polling-only.
Admin Panel / Version already reports which driver is actually live: server/
statistics.js derives "Reactivity mode" (oplog / changeStreams / polling) and
"MongoDB Oplog enabled" from Meteor's live _oplogHandle, so it shows `polling`
whenever OpLog did not come up — the direct way to confirm whether FerretDB is
on OpLog or polling.
Tests: tests/ferretdbOplog.test.cjs now asserts the oplog,polling fallback in
every launch path, the polling-only kill-switch path, and that the Version page
detects + displays the live reactivity driver.
Thanks to uusijani, Nissulya and xet7 !
Fixes#6480,
Fixes#6481.
With FerretDB there is no MongoDB oplog, so Meteor observed every live query by
poll-and-diff, pinning FerretDB CPU at 100-390% on busy boards (#6480, #6481).
FerretDB v1 now ships an OpLog (wekan/FerretDB: auto-created capped local.oplog.rs
+ replica-set hello handshake), so every FerretDB launch path now:
- starts ferretdb with --repl-set-name=rs0 (snap ferretdb-control, docker
entrypoint + docker-compose service, bundle start-wekan.sh/.bat), which makes
FerretDB auto-create the OpLog and advertise the replica set; and
- points WeKan at it with MONGO_OPLOG_URL=.../local?replicaSet=rs0 (docker-compose
also switches METEOR_REACTIVITY_ORDER to oplog,polling), so Meteor TAILS the
OpLog instead of polling.
Enabled by default with a kill-switch: WEKAN_FERRETDB_OPLOG=false forces the old
polling-only behaviour. The calmer polling defaults remain as the fallback for
queries Meteor cannot observe via the OpLog. The transient migration ferretdb
(migration-control) deliberately stays oplog-free so bulk inserts are not slowed.
Also cut the client activity feed page from 500 to 50 rows: 500 was far more than
the sidebar/card feed shows and, under polling, was re-fetched and re-diffed on
every poll; infinite scroll still loads more on demand (the server cursor is
index-bounded either way).
Tests: tests/ferretdbOplog.test.cjs source-guards every launch path (repl-set arg,
MONGO_OPLOG_URL, kill-switch) plus the activity page size, and a negative test that
migration-control stays oplog-free.
Thanks to uusijani, Nissulya and xet7 !
Fixes#6480,
Fixes#6481.
#6481 (Nissulya): after MongoDB 3.x -> FerretDB v1 migration, cards with a
non-finite `sort` (±Infinity / NaN, written by old WeKan before #6472) failed to
insert: FerretDB/SQLite rejects non-finite doubles ("invalid value { sort: +Inf }
(infinity values are not allowed)"), aborting the document and leaving boards
stuck on the loading spinner. sanitizeNonFinite() only made the bare mongoexport
tokens PARSE; the parsed value was still a JS Infinity/NaN. Added clampNonFinite()
in the migration tool to reset every non-finite number to a finite 0 before
insert, and a `nonfinite-sort-repair` startup schema-upgrade step that heals the
same corruption in an already-running database (MongoDB or FerretDB), using a
predicate built from finite literals only so it is safe on FerretDB too.
#6481 (Nissulya): LDAP `mail` -> `email` fieldmap returned `undefined` (worked in
6.09). getDataToSyncUserData's email branch read the mapped attribute
case-SENSITIVELY (ldapUser.hasOwnProperty / ldapUser[field]) while the rest of
wekan-ldap uses the case-insensitive getLDAPValue(); a directory returning the
attribute with a different case (exposed by ldapjs -> ldapts, which preserves the
server's case) missed it. The email branch (and the addLdapUser fallback) now
read via getLDAPValue().
#6480 (uusijani): board/CSV export paused on socket backpressure with
`res.once('drain', resolve); res.once('error', reject)`, which leaked one 'error'
listener per backpressured write ("MaxListenersExceededWarning: 11 error
listeners added to [ServerResponse]"). writeWithBackpressure() now removes both
listeners once either settles.
Added tests and negative tests for all three fixes.
Thanks to Nissulya, uusijani and xet7 !
Fixes#6480,
Fixes#6481.
Fix the intermittent "Unexpected userId after login" e2e flake (seen in
33-board-domains, which re-logs-in on the same page: admin then non-admin).
Meteor.userId() can lag the Meteor.loginWithToken callback by a tick, so reading
it immediately sometimes returned the PREVIOUS userId. Poll Meteor.userId() until
it settles on the expected id (5s timeout so a genuine mismatch still surfaces).
Pure test-helper change; the direct-Mongo-seed + resume-token login path this
uses does not go through createUser, so it is unaffected by the session security
changes.
Add server/lib/selfChecks.js: at startup (and via an admin-only runSelfChecks
method) run cheap invariants a unit test would assert (database round-trip,
WRITABLE_PATH/files writable) and record any failure to the Tests stream
(Admin Panel -> Problems -> Tests). WeKan does NOT require Playwright: only
failures detectable without a browser are logged, and no runtime code imports
Playwright (asserted by the test). Wired into server/imports.js; design doc
updated.
Add server/lib/speedMiddleware.js: a WebApp middleware that times non-asset HTTP
requests and records those slower than WEKAN_SLOW_REQUEST_MS (default 2000ms; 0
disables) to the speed stream via speedLog. Best-effort, never throws; wired into
server/imports.js. Source-guard test added.
Record more runtime rejections into the eventlog (Admin Panel Problems):
- fileValidation.js: log every upload rejection (dangerous MIME content ->
xss.mime, mime not on the allow-list -> file.mime, over size limit ->
file.size).
- metrics.js: log spoofing.xff when a /metrics request carrying an
X-Forwarded-For header is denied (likely forged-whitelisted-IP attempt).
- export.js: log authz.export at all four export-authorization denial paths.
All calls are best-effort/guarded (server-only require, try/catch) so logging
never breaks the guard. Source-guard tests added.
Translate problems, summary, securityReportTitle, speedReportTitle,
testsReportTitle, new-problems, acknowledge, no-new-problems,
problems-summary-help and the event-* table headers into all 153 non-English
languages (en is the source). All files validated as JSON.
- 2nd header bar: add a Problems button (warning icon) to the RIGHT of the Info
(version) button; it turns red when there are new problems (settingHeader.js
polls eventLogProblemAreas). Remove the old Reports button; the Admin Panel
still opens on Settings.
- The former Reports page is now the Problems page: left menu Summary / Security /
Speed / Tests, then a separator, then the moved Broken Cards/Files/Rules/Boards/
Cards/Impersonation items.
- Summary (problemsSummary, renamed from the removed top banner): the checkbox
list + one Acknowledge button — the ONLY place to acknowledge.
- Security/Speed/Tests: read-only, paginated, searchable event tables via new
admin-only eventLogPage/eventLogCount methods; acknowledgeEventLog now takes a
stream or array.
- i18n keys + CSS + source-guard tests + design doc updated.
Change the top problem banner from a per-area button to a checkbox list of
problem areas with a single Acknowledge button that acknowledges every checked
area in one call. acknowledgeEventLog now accepts a stream or an array of
streams. Acknowledging happens ONLY at the top banner; the Reports pages are
read-only (noted in the design doc).
Like the Announcement banner, the Admin Panel top now shows, for each problem
area with unacknowledged events, its menu path (Admin Panel -> Reports ->
Security/Speed/Tests) and the count of NEW problems. Each area has an Acknowledge
button that marks the newest problems seen, zeroing the count and removing the
info from the top.
- models/eventLog.js: add the eventlogAcks collection and admin-only methods
eventLogProblemAreas() (count of events newer than each stream ack) and
acknowledgeEventLog(stream) (upsert ack = now).
- client adminProblemBanner.{jade,js}: poll the counts, render path/count/button,
acknowledge on click; included in settingHeaderBar (top of every admin page).
- i18n keys (securityReportTitle/speedReportTitle/testsReportTitle/new-problems/
acknowledge) + banner CSS. Source-guard tests added.
Per correction: use only the existing WeKan database via Meteor JavaScript
queries (works on FerretDB and MongoDB); do not create files/DBs under
WRITABLE_PATH; and have FerretDB report problems to WeKan rather than writing
itself.
- models/eventLog.js: one `eventlog` collection (stream: security|speed|tests).
- server/lib/{securityLog,speedLog,testLog}.js: thin wrappers that insert one
document via EventLog.insertAsync (fire-and-forget, never throw).
- models/lib/securityLogFormat.js slimmed to just sanitizeDetail; remove the
node:sqlite event store (server/lib/eventStore.js) and all file/path/schema
helpers.
- Add a Tests stream: log anything that would fail an existing WeKan test.
- Design docs (WeKan.md, FerretDB.md) rewritten: DB-collection storage, grouped
counts for the summary, Admin Reports read the collection, and FerretDB reports
problems to WeKan which records them (no FerretDB DB/file writes).
- tests/securityLog.test.cjs updated (sanitizer + catalog + collection wiring +
no-filesystem guards); verified via Python mirror.
- Avatar localization redirect SSRF: fetch avatars via fetchSafe (DNS-pinned,
redirects rejected) instead of native redirect:follow, and validate
profile.avatarUrl scheme (http/https/data:image/local) at write time.
- Trello import source.url stored XSS: only render/store an http(s) activity
source URL, so a javascript: board url can no longer execute on click.
- Trello LIVE import readable SSRF: gate every attachment/background/avatar
download with validateAttachmentUrl, matching the offline import path.
- CAS login global user-data race: store validated CAS user data per credential
token instead of a module-global _userData (fixes account-takeover race).
- Metrics X-Forwarded-For whitelist bypass: only trust XFF behind
METRICS_TRUST_PROXY (parsed spoof-resistantly from the right); else use the
socket peer address.
- Stale impersonation export bypass: remove the "|| impersonateDone" export
authorization bypass; require real board visibility (canExport) only.
- Invitation code brute force: generate invitation codes with a 128-bit CSPRNG
(crypto.randomBytes) instead of a 6-digit Math.random value, and rate-limit
createUser (DDPRateLimiter).
The OIDC shared serviceData race from the same report was already fixed (#4897,
the per-login objects are callout-local). Source-guard regression test added
(tests/securityMeifukun.test.cjs) and wired into test:unit:node.
Thanks to meifukun and xet7 !
Wire in three new publish jobs (dormant until their secrets are set — each
guards on its secrets and skips with a ::notice:: so ordinary releases are
unaffected):
- snap-variants: build+publish wekan-ondra and wekan-gantt-gpl snaps.
- ucs: bump + push the wekan/univention app and upload to the App Provider.
- nextcloud: build the ExApp image and publish to the Nextcloud App Store.
Document the new secrets in the header. Add a "Remaining steps" section to the
top of Snap-Ondra-Gantt.md, UCS.md and Nextcloud.md (keys to add + how).
The 29 per-OS manual-vs-automatic update comparison docs move into OS/;
the WeKan-specific docs (README, Snap, Snap-Ondra-Gantt, UCS) stay in the
parent. All cross-links repointed and verified.
Add manual-vs-automatic (Snap-like) update comparison docs for all operating
systems with any Internet update mechanism: Linux, BSD, Haiku, Amiga, RISC OS,
Solaris/illumos, AIX, HP-UX, OpenVMS, z/OS, ArcaOS, ReactOS, FreeDOS, Plan 9,
Redox, SerenityOS, Android, iOS, ChromeOS, HarmonyOS, Ubuntu Touch, Sailfish,
postmarketOS, KaiOS, Tizen, webOS and TV/streaming OSes; plus a README index.
Add Snap-Ondra-Gantt.md (keys/secrets + release-all.yml changes to also release
the wekan-ondra and wekan-gantt-gpl snaps). Rename the two earlier dev docs to
Snap.md / Windows.md and repoint all links.
- i18n: registering generic Chinese (zh) as its own entry made bare "zh"
resolve to itself, so the "zh -> zh-Hans" alias is now dead code — remove it
from LANGUAGE_ALIASES (keep wuu -> wuu-Hans) and update the two mocha tests
(resolveTag/setLanguage) to expect "zh" instead of "zh-Hans".
- e2e (15-board-actions): the Board Settings cog was removed from the header
(it is in the right sidebar now), so the "one click from the header cog" test
failed in every browser. Rewrite it to open the sidebar and open the board
menu (which still has Archived Items) from there.
The changes committed since the v10.00 release bump had no CHANGELOG Upcoming
section. Add one above v10.00 covering all of them: the "Submit editors with
Enter" Member Setting, the right-sidebar drag-to-resize width feature, the #6481
migration NaN/Infinity fix, the #6465 Add List inline-composer options restore,
the right-sidebar width/inputs fix, and the missing copy-selection key — each
with commit links.
Add the sidebar resize handle tooltip (drag-to-resize-sidebar, "Drag to resize
sidebar") to every language file, translated into each language (English-variant
locales carry the English text). All 154 files JSON-validated; value-only
additions.
Add a resize handle on the sidebar edge, like resizing a spreadsheet column
(mirrors the list resize in client/components/lists/list.js). Drag it to change
the sidebar width; the width is saved to profile.sidebarWidth for logged-in
users and to browser localStorage (wekan-sidebar-width) for anonymous users on
a public board, then re-applied on load.
- The handle uses the LOGICAL inset-inline-start, so in LTR it is the left edge
of the right-docked sidebar and in RTL it is the RIGHT edge of the left-docked
sidebar; the drag direction inverts for RTL so dragging always widens/narrows
the visible panel correctly.
- Desktop only: on phones (<=800px) the handle is hidden and the sidebar stays
full width (the 100vw rule is now !important so a desktop inline width can
never leak into mobile).
- Width is clamped (min 280px, capped near the viewport) and re-applied on
window resize so crossing the mobile breakpoint clears/restores it.
New profile.sidebarWidth schema field + getSidebarWidth/setSidebarWidth helpers,
setSidebarWidth Meteor method, i18n (en, fi), and source-guard test
tests/sidebarResize.test.cjs.
Thanks to xet7 !
The Multi-Selection right-sidebar "Copy selection" button used {{_ (quote)copy-selection(quote)}}
but the key was missing from every language file — including the en source —
so i18next had no value to fall back to and rendered the raw key. Add
copy-selection ("Copy selection") to en and translate it to all languages,
mirroring each language(apos)s existing "move-selection" sibling with the verb changed
to copy. All 154 files JSON-validated; value-only additions.
Thanks to xet7 !
- .board-sidebar used width: 30vw, which scaled with the viewport: on very wide
screens it grew to ~double the intended width (it looked right only at mid
widths). Use width: clamp(320px, 30vw, 560px) so it keeps the mid-width look
but is capped (and floored) instead of scaling without bound. Phones (<800px)
still get the full-width 100vw rule.
- The sidebar filter text inputs (Filter List by Title, Filter by card title,
advanced filter) had no width, so they rendered at the browser default narrow
width with a big empty gap on the right. Sidebar text/search inputs now
width: 100% + box-sizing: border-box, so they fill the padded content area
with equal empty space on both sides (the .sidebar-content 12px padding).
Source-guard test tests/sidebarWidth.test.cjs.
Thanks to xet7 !
The #6465 per-list-header inline Add List composer (addListInline) had been
reduced to just a title input, dropping the options the old addListForm had.
Restore them:
- the "Add after which list" position <select>, listing the swimlane lists and
pre-selecting the list whose header button opened the composer; submit reads
it for afterListId and derives nextListId from the following option so the new
list is inserted between them (falls back to the header target for the empty
board case).
- the "or template" link (js-list-template) to create a list from a template.
Helpers swimlaneLists()/isDefaultAfterList() added; createListAfter already
accepts nextListId. Source-guard test tests/addListComposerOptions.test.cjs.
Thanks to csonkaoszimt and xet7 !
Fixes#6465.
Add the two new Member Settings keys (submit-on-enter, submit-on-enter-
description) to every language file in imports/i18n/data/, translated into each
language and inserted after show-desktop-drag-handles. English-variant locales
(en-GB, en_AU, ...) carry the English text; each real language matches its own
terminology, and the keyboard-key tokens Enter, Shift+Enter and Ctrl/Cmd+Enter
are kept verbatim. All 154 files JSON-validated; value-only additions.
#4236 made every multi-line editor submit only on Ctrl/Cmd+Enter (plain Enter
= newline) so titles could be multi-line, but that removed the fast
Enter-to-save title workflow some users rely on (v10 email feedback). Add a
per-user preference in the Member Settings (Change Settings) popup, saved to
profile.submitOnEnter and OFF by default: when on, plain Enter submits the card
title, description and other inlined forms, and Shift+Enter inserts a newline;
when off, behaviour is unchanged (Ctrl/Cmd+Enter saves).
The rule stays in the single pure isSubmitKey(event, {submitOnEnter}); the card
title, the card-description override and the shared client/lib/inlinedform.js
all route through it and read the preference from the current user. New toggle
+ helper + toggleSubmitOnEnter method + schema field, i18n (en, fi), and
editorSubmitKey.test.cjs extended with positive + negative opt-in cases.
Thanks to xet7 !
Related to #4236.
mongo 3.x mongoexport emits non-finite doubles as the bare tokens NaN,
Infinity, +Infinity and -Infinity, which are not valid JSON, so EJSON.parse
threw ("Unexpected token N/+") and migrate-mongo3-to-ferretdb.mjs dropped the
WHOLE document — leaving boards/cards missing after an otherwise "successful"
migration. sanitizeNonFinite() now rewrites those bare tokens, only outside
string literals, to the canonical EJSON v2 double form {"$numberDouble":"..."}
that EJSON.parse accepts, so the document migrates instead of being lost.
Strings that merely contain "NaN"/"Infinity" and ordinary negative numbers are
left untouched. Test tests/migrateNonFinite.test.cjs (with negative cases).
Thanks to Nissulya and xet7 !
Fixes#6481.
Rewrite the v10.00 section to match the final implementation and group ALL 101
commits in the release under their own headings (each with commit links):
- Corrected "done differently" cases: Change Color is visible grouped swatches
(not the initial two-level dropdowns); Font is font-name + size buttons with
immediate apply (not dropdowns); the board-header "icons only" change was
reverted so button texts stay.
- Fixed the Home board entry: it is set via the All Boards Multi-Selection
"Selected:" home action and the after-login redirect runs on all deployments,
so it is available, NOT disabled and NOT Sandstorm-only (an earlier draft
wrongly said the UI was disabled).
- Every commit since v9.99 is now listed once, grouped by feature/fix area.
Rename .github/workflows/playwright.yml -> playwright.yml.disabled so GitHub
Actions no longer runs it (only .yml/.yaml files in .github/workflows are
executed). Reversible by renaming back. NOTE: this workflow also contained the
Meteor unit-tests job and a Puppeteer regression job, so those stop running too.
The script lives in releases/translations/, so the repo root is two levels up
(../..), not one. With a single .. it landed in releases/ and looked for ../tx
at wekan/tx instead of the sibling ../tx, aborting with "tx not found". Match
the cd "$(dirname "$0")/../.." pattern used by the other scripts here.
The .tx config maps Transifex "war" -> local wa-RR.i18n.json, i.e. wa-RR is
Waray/Winaray (a Philippine language), NOT a regional variant of Walloon (wa).
An earlier seed wrongly copied Walloon content into it. Retranslate the file
fresh from English into Waray (2191 strings); placeholders/brand names
preserved, key set unchanged, JSON validated.
- Register Wu Chinese (wuu-Hans): it had a data file but no languages.js
entry, so it was neither browser-detectable nor shown in Change Language.
- Add wuu -> wuu-Hans to LANGUAGE_ALIASES so a bare "wuu" browser locale
resolves (mirrors zh -> zh-Hans).
- Fix display names that were in the wrong script for their (Latin) content:
Wolof "ولوفل" -> "Wolof"; Malay "بهاس ملايو" -> "Bahasa Melayu" (+ Malaysia).
km_KH/ru_RU are symlinks to km-KH/ru-RU, already covered. Every data file is
now registered, so all are browser-detectable and visible in the popup.
Fresh translations: vi, uz, uz-AR (Arabic-script Uzbek), ve (Venda), ve-CC
(Venetian), ve-PP (Vepsian), vo (Volapuk), wa (Walloon), wo (Wolof), xh
(Xhosa), yi (Yiddish), yo (Yoruba), zgh (Tamazight/Tifinagh), zu (Zulu).
Seeded variants uz-UZ/uz-LA, vi-VN, wa-RR, zu-ZA from bases. Completes the
full 153-language set. Placeholders/brand names preserved; JSON validated.
zh.i18n.json existed but had no entry in the languages registry, so a browser
reporting bare "zh" could not auto-detect it and it was missing from the Change
Language list. Add it (code zh, name 中文). The other two unregistered data
files (km_KH, ru_RU) are underscore duplicates already covered by the existing
km-KH / ru-RU entries, whose tag is the underscore form.
Seed Mandarin/Wu/Cantonese (cmn, wuu-Hans, yue_CN) from zh-CN, Japanese-Hiragana
locale (ja-HI) from ja, West Flemish (vl-SS) from nl, and Moroccan Arabic (ary)
from ar as machine-quality baselines sharing the written form, for the community
to refine. Placeholders preserved; JSON validated. 4074 strings.
Fill previously-untranslated UI strings in de, fr, es, it, nl, ru, pl, uk, cs,
ja, ko, zh-CN, tr, ar, pt. Each language matches its existing file terminology
as a glossary plus standard Kanban terms; product/brand names, acronyms, env
vars, console commands, placeholders (__var__, %s, {{..}}) and markup preserved
verbatim. Value-only changes; every file JSON-validated.
de 524, fr 617, es 917, it 938, nl 458, ru 974, pl 1037, uk 895, cs 1108,
ja 926, ko 850, zh-CN 929, tr 1059, ar 1145, pt 500 strings.
Fill previously-untranslated UI strings in the five near-complete languages,
matching each file existing terminology as a glossary and standard Kanban
terms (board, list, card, swimlane, label, checklist, theme, font, ...).
Product/brand names, acronyms, env vars, placeholders (__var__, %s, {{..}})
and markup preserved verbatim. Value-only changes; JSON validated.
zh-TW 29, pt-BR 28, da 159, he 262, sv 382 strings.
Pushes the English source, then force-pushes every language translation to
Transifex in one run (../tx push -s; ../tx push -t -f). -f is required or the
Go tx CLI skips languages whose local file is not newer than the server copy.
Warns that it overwrites newer community translations, so pull first if needed.
Translate the 21 previously-English strings in fi.i18n.json: the disable-*
and anonymize-* import/export/activities/notifications/watch admin feature
labels and descriptions, plus impersonation report title, impersonated user
and reason. Terminology follows existing Finnish strings (seuraaminen,
toimet, profiilikuva, tuonti/vienti, esiintyä käyttäjänä).
Below the "Multi-Selection is on" button a "Selected:" row now offers two
actions on the multi-selected boards:
- star icon: stars every selected board (sets them as Starred).
- home icon: sets the first selected board as the Home board (opened after
login) via the existing toggleDefaultBoard method + router redirect, which
brings the previously hidden Home-board feature back into use.
The Home board also always shows FIRST in the Starred list (even when it is
not itself starred), badged with a home icon centred at the top of its tile.
- Search boards box is now ~half width (300px); the input flexes to fill it.
- "Click checkboxes to select boards" hint moved out of the path title and
stacked directly below the "Multi-Selection is on" button (own column).
- Folder icon and the menu title (e.g. "Remaining") are grouped so the title
always sits to the RIGHT of the icon instead of wrapping below it.
The Filter by Unread / View All / Delete All Notifications item icons
(envelope, copy, trash, check) were faint; force the .menu-icon and .check-icon
(and their i.fa) to black.
The X dropped below the header because a global `.close { position: absolute;
top: 5px }` rule (boardBody.css) applied once my rule stopped setting position.
Set position: static on the drawer .close so it stays a flex item and aligns
with the Notifications title at the right edge of the header row.
- The drawer and its header are now full width (100vw) instead of 400px.
- Rebuild the header as a flex row (hamburger left, title centre via flex:1, X
right) instead of fragile absolute positioning, so the X is aligned with the
Notifications title at the right edge (was sitting too low).
- Force the hamburger and X to black with !important (they were inheriting the
themed header white link colour), overriding the surrounding themed bar.
- The X close button is now black, moved up (top: 8px, aligned with the
"Notifications" title instead of vertically centred lower) and to the far
right (inset-inline-end: 4px).
- The left hamburger menu toggle is black (was #333).
The member-menu "Notifications" item and its js-notifications-drawer-toggle
handler toggled the same notifications drawer as the header bell icon
(notifications.js), so it was a duplicate. Remove the menu entry and its
now-unused handler; the header bell is unchanged.
The js-new-search button clears the search (empties the input + query), so it
is a Clear button. Replace its broom icon with the text of the existing
trello-clear-job translation (en "Clear" / fi "Tyhjennä") and use it for the
aria-label too.
The global-search button hardcoded belize #216694 (inline !important styles +
.board-color-belize CSS), ignoring the theme. Make it a .primary type="submit"
button (safe: the search form has onsubmit="return false") so it uses the
theme override colour via the existing button[type=submit].primary rules
(default blue, or the board/global theme), and remove the hardcoded belize
styles.
The theme override colours .primary buttons via `button[type=submit].primary`
(board-color-* and custom-theme rules). A <button> without an explicit type
attribute does not match [type=submit] in CSS (though it already behaves as
submit), so the archive Restore/Delete buttons kept the default blue. Add
type="submit" (no behaviour change) so they match the theme rules and use the
global theme override colour (named or custom).
The header bars have a higher z-index than #modal, so the archive panel top
(and the close X) was hidden under the top header bar. Offset the archive
modal content 96px from the top (clears both the quick-access and board header
bars) and extend to the window bottom via min-height calc(100vh - 106px).
The archive modal used the default .modal-content (500px, margin: 5vh auto),
so archived boards were cramped and the panel stopped short of the window
bottom. Target the archive modal (the only one containing .archived-lists) and
make it 96vw wide with no bottom margin + min-height calc(100vh - 2vh) so it
reaches the bottom of the browser window; each archived board is a full-width
row.
Add the .primary class to both "Upload an avatar" buttons so they use the
theme primary-button style (default #005377 blue background + white text,
overridden by the board/global theme color) instead of the plain grey/black
default button.
Give .js-reset-text-color / .js-reset-bg-color a button appearance (border,
padding, background, hover) with a dimmed disabled state, matching the font/
size buttons.
Redesign the Font popup:
- Font: one button per installed font, each shown IN ITS OWN font (via inline
style), plus a Default button. Clicking a font applies it immediately.
- Size: a row of buttons ordered by percent — smaller on the left, 100%
(Default) in the middle, larger on the right. Clicking a size applies it
immediately.
- Colors: the text/background color wheels now apply immediately on change /
reset. Removed the Save button (everything applies on click).
New .ui-font-btn / .ui-font-size-btn styling (with .active) in labels.css.
Tests updated in tests/uiFonts.test.cjs.
Widen the language popup responsively (width: min(90vw, 1100px), max-width:
90vw) and lay the list out as an auto-filling grid (repeat(auto-fill,
minmax(170px, 1fr))) so as many languages as possible are visible at once on
wide windows, collapsing to a single column when narrow.
Guarded by tests/changeLanguageColumns.test.cjs.
- Revert the unconditional hiding of board-header button text labels (Private,
Muted, Sort, Filter, ...): they show again at normal widths. The pre-existing
mobile-only (max-width:800px, #6419) icons-only rule is untouched.
- CHANGELOG Upcoming: add consolidated entries for the recent All Boards /
board-header layout polish, the Change Color swatches / immediate-apply /
applies-everywhere / popup title, the #4759 user-insert validator fix, and the
rebuild-wekan.sh CURRENT-IP fix; mark the #2220 home-board toggle as currently
disabled.
"+ Add Board" and "+ Add Template Container" share the .js-add-board .label
element, so add an .is-template-container class to the templates-menu variant
and give it padding 20/8/22/8 — 2px lower than "+ Add Board" (18/8/24/8) —
while keeping the same 42px total vertical padding so the tile height is
unchanged.
Shift the .js-add-board .label vertical padding (top 24->20, bottom 18->22)
so the "+ Add Board" / "+ Add Template Container" text sits 4px higher, while
keeping the same 42px total vertical padding so the grey tile stays the same
height as the board icons. Test now checks total vertical padding equality
(height) rather than an exact padding string.
The desktop board-handle had a dark rgba(0,0,0,0.4) background box; make it
transparent so only the drag-handle icon is visible. The hover highlight is
kept. Guarded in tests/addBoardTileHeight.test.cjs.
The desktop board-handle was top-centered (top:4px; left:50%; translateX);
move it to the right middle (top:50%; translateY(-50%); inset-inline-end),
matching the mobile/small-screen handle position. Guarded in
tests/addBoardTileHeight.test.cjs.
- All Boards path header: order is now Multi-Selection, then Sort, then Search
boards (Search moved to the right of Multi-Selection; Multi-Selection moved to
the left of Sort). Guarded in tests/addBoardTileHeight.test.cjs.
- Top header bar: swap the positions of the mobile/desktop toggle and the zoom
controls (mobile toggle now right after the logo, zoom later). Both kept as
ungated direct children (zoom is no longer hidden when the logo is hidden).
Make the board-header button text labels (Private, Muted, Sort, Filter,
Swimlanes, Multi-Selection, ...) always hidden (was only under a max-width
media query), so the header shows icons only. The board title and the star
count (.board-star-counter) are not button labels and stay.
Guarded in tests/headerHamburger.test.cjs.
The #6465 one-click board-settings cog (js-open-board-menu) sat just left of the
sidebar hamburger, duplicating Board Settings which is reachable from the right
sidebar. Remove the cog from boardHeader.jade, its now-dead click handler from
boardHeader.js, and the unused .js-open-board-menu header CSS rule. The sidebar
board-menu button (sidebar.jade/js) is unchanged, and the hamburger + separator
remain.
Guarded in tests/headerHamburger.test.cjs.
When the window is not wide enough, the 2nd header bar button labels (Private,
Muted, Sort, Multi-Selection, ...) wrapped to a second row. Add a max-width:
1200px media query that hides the .board-header-btn text spans (show icons
only), keeping the board title and the buttons on one line. The star count
(.board-star-counter) is kept.
Guarded in tests/headerHamburger.test.cjs.
Previously the sidebar hamburger sat 12px from the right edge (2px margin +
10px bar right padding), more than the ~8px gap to the right of the user name
in the top bar (.header-user-bar-name margin-inline-end: 8px). Remove the board
header bar right padding and give the hamburger margin-inline-end: 8px, so its
right-edge gap is the same small amount as the user name.
Guarded in tests/headerHamburger.test.cjs.
The hamburger used the general .board-header-btn margin: 0 12px, leaving a
button-sized empty gap on each side. Tighten its side margins to 2px (and the
preceding cog button right margin to 2px) so the hamburger sits snugly at the
right edge with no wasted space to its left or right.
Guarded in tests/headerHamburger.test.cjs.
The board header bar (Swimlanes + Lists views) used a float layout: the right
button group (which ends with the js-toggle-sidebar hamburger) was float:
inline-end. As the window width changed, the floated groups wrapped and the
hamburger drifted to the middle of the bar. Make #header-main-bar a flex row
(align-items:center, flex-wrap:wrap), make the button groups flex (their
buttons float is ignored inside a flex container), and pin the right group with
margin-inline-start:auto so it always hugs the right edge (or the right of its
row when wrapped).
Guarded by tests/headerHamburger.test.cjs.
Feedback fixes to the Select Color picker:
- Apply IMMEDIATELY on clicking a swatch (and when a custom-color wheel is
committed) via a shared applySelection(); removed the Save button/handler.
- Category titles (Flat/Clear/Dark/Special) are left-aligned and clear: both,
so they drop below the previous group swatches (which float: inline-start)
instead of the 2nd/3rd/4th title sitting mid-row.
- Remove the empty band above the first (Flat) title (first-child margin-top:0)
and tighten the Default row.
- Member Settings Change Color popup now has a header: added changeColorPopup-title
("Change Color" / fi "Vaihda väri"), matching Board Settings.
Tests updated in tests/themeColorPicker.test.cjs.
The dev-server CURRENT-IP options detected the IP with
ip a | grep noprefixroute | ... | grep 192.
which requires the noprefixroute flag AND a 192.x subnet. On hosts without
either (e.g. arm64 Ubuntu on a 10.x/172.x network) it returned empty, so
ROOT_URL became http://:3000 and Meteor crashed with "TypeError: Invalid URL".
Detect the primary IP subnet-agnostically via the default route
(ip route get 1.1.1.1 -> src), fall back to hostname -I, then to localhost so
the URL is always valid. macOS uses ipconfig getifaddr en0/en1 with a localhost
fallback. Applied to both CURRENT-IP dev-server options.
Remove the fa-home Home-board toggle from the board tiles (boardsList.jade),
its click handler and the isDefaultBoard tile helper (boardsList.js), so a
Home board can no longer be set from the All Boards page. The model helpers,
server toggleDefaultBoard method and the login redirect are left intact but
are no longer reachable from the UI. Test flips to guard the disabled state.
The All Boards button showed only an icon (span.emoji-icon > fa-check-square-o)
with the reset X as a sibling, while the Swimlanes View (boardHeader.jade)
shows the check icon + a "Multi-Selection" text label with the reset X nested
inside. Match the All Boards markup to the Swimlanes View (direct icon + text
label + nested reset with fa-times-thin), and add stopPropagation to the reset
handler so the now-nested X does not bubble to the activate handler and
immediately re-enable selection.
Guarded by tests/multiSelectionButton.test.cjs.
The .boards-path-header was a single-row flex (justify-content: space-between,
no flex-wrap) and .path-right (search box + Sort / Multi-Selection / Clear)
also did not wrap, so narrowing the window — especially with Multi-Selection
on, whose hint widens .path-left — pushed those buttons off/hid them. Add
flex-wrap: wrap to the bar, .path-left (min-width:0 so it can shrink; hint
wraps under the title) and .path-right (buttons wrap among themselves), so
everything drops to a second row and stays visible.
Guarded in tests/addBoardTileHeight.test.cjs.
The .board-list-header div (a leftover zoom-controls container) rendered
nothing but still reserved vertical space via margin: 1vh 0 2vh 0 + padding:
1vh 0 — an empty grey band above the boards layout, between the top and the
left menu / "My Boards" bar. Removed the empty element from boardsList.jade
and its now-unused CSS rule, so .boards-layout is the first child of .wrapper.
Guarded in tests/addBoardTileHeight.test.cjs.
The grey add-tile (.js-add-board .label, shared by both "+ Add Board" and
"+ Add Template Container") overrode min-height:100px, line-height:56px and
padding:36px 8px 32px 8px — taller than the #6465-thinned board tiles
(.board-list-item: min-height:72px, padding:24px 8px 18px 8px). Match the
board-tile box model so the grey background is the same height as a board icon.
Guarded by tests/addBoardTileHeight.test.cjs (asserts the add-tile min-height +
padding equal the board tile, and the old oversized values are gone).
Two fixes from feedback that picking clearblue "did not change" and the
dropdowns were hard to use:
1) The global theme override now wins EVERYWHERE, including board pages. Before,
the <body> autorun suppressed it whenever a board was open (onBoard gate), so
changing the color while viewing a board did nothing. Now a set global override
is applied regardless of board context, and #header / #header-quick-access
(header.jade) and .board-wrapper (boardBody.jade) prefer the global override
over the per-board color. Unset it to return to per-board colors. When no
global is set, an open board still exposes its own custom colors.
2) The picker no longer uses category/theme dropdowns (colors were invisible).
It now shows the actual color SWATCHES with their names, grouped by category,
with the category name as a header above each group. The flat/clear color
wheels still appear once such a color is selected; custom-capability is derived
from the selected color. customTheme.css styles the group labels.
Tests updated: themeColorPicker/globalThemeColor guards for grouped swatches and
the global-wins-everywhere behaviour.
Two fixes to the #6465 inline Add List composer:
1) Save button did nothing. The inline composer calls createListAfter WITHOUT
nextListId, but the server check() used Match.OneOf(String,null,undefined)
(not Match.Optional) for it. Meteor check throws "Match failed [400]" on a
MISSING object key unless the sub-pattern is Match.Optional, so every inline
add-list 400'd. The omittable params (swimlaneId/afterListId/nextListId/type)
are now Match.Optional, so callers may omit them.
2) Empty swimlane/board now shows a + button, and the create-list form appears
only after clicking it (a swimlane that already has lists keeps using the
per-list-header add-list button — no + button needed). The reveal is scoped
to the clicked swimlane via a wekan-add-list-empty Session flag, cleared on
save/close.
Tests: models/lib/createListParams.js + tests/createListParams.test.cjs mirror
the method's param contract (incl. Meteor's missing-key rule) with the exact
client shapes + negatives, and a source guard that the check uses Match.Optional;
tests/upcomingUiGuards.test.cjs gains guards for the empty-state + button reveal.
Wired into test:unit:node.
specs/13-swimlanes.e2e.js waited only 6s for the swimlane action .js-pop-over
to appear. Under the full parallel suite that occasionally exceeds 6s on
webkit (the popup opens fine, ~3s, when the test runs alone), so the test
flaked. Bump to 15s to match the suite standard popup-visibility wait; a
genuinely broken popup still fails.
The optional profile.uiTextColor / profile.uiTextBgColor schema fields ran
their custom() hex validator on EVERY user insert, including when the field
was absent. /^#..$/.test(undefined) is false, so the validator returned
"notAHexColor" and collection2 threw ValidationError for any user with no
custom color set — surfacing as a SyncedCron fatal unhandledRejection and
blocking user creation (seen in ../log test-server.log; also cascaded into
ECONNREFUSED e2e failures as the server destabilized).
Guard both custom() validators to return undefined when the value is unset
(undefined/null/empty), matching the existing list-color validator. Add a
source-guard test so the guard cannot silently regress.
Translate the strings added this cycle in fi.i18n.json, reusing existing
terminology (väri, taulu, lista, uimarata, fontti, teema, Oletus): the Select
Color theme categories, the Member Settings font / size / text + background
color pickers, the default-board tooltips, and the list-move confirmation.
Fixes theme-category-clear, which had been auto-translated to the verb
"Tyhjennä" (empty) instead of the theme sense "Kirkas". font-preview-text uses
the standard Finnish pangram so the preview shows ä/ö.
Restore the old "a grain opens straight into its board" behaviour as a
convenience, WITHOUT persisting anything. On the Sandstorm home route only:
- a saved Home board (explicitly chosen by the user) always wins -> redirect;
- else, once the boards subscription has loaded, exactly ONE board -> just
open it (nothing is saved);
- else (zero or many boards, no chosen Home board) -> stay on All Boards.
Choosing a Home board stays a separate, explicit action (the All Boards home
toggle, #2220) — the auto-open never writes it.
The redirect is reactive (Tracker.autorun over a boards subscription), so it
waits out Sandstorm asynchronous grain login (Meteor.userId lands late) and
board loading instead of firing once too early. Runs at most once per page
load, and only when isSandstorm.
- models/lib/sandstormAutoOpen.js: pure, unit-tested decideSandstormAutoOpen()
(+ tests/sandstormAutoOpen.test.cjs, wired into test:unit:node).
- config/router.js: startSandstormAutoOpen() reactive redirect, gated to
Sandstorm; non-Sandstorm keeps the existing one-shot default-board redirect.
The Font popup now has a color wheel for the text color and one for the text
background color, each with an Unset button to go back to default. Values are
free choices (native <input type=color>), validated server-side as #rrggbb
hex (models/lib/uiFonts isHexColor6) so only a strict hex ever reaches a CSS
value — no injection surface. An unset/invalid value clears that color.
- profile.uiTextColor / profile.uiTextBgColor schema (hex-validated) + getters;
setUiColors(textColor, bgColor) method ($set valid hex, $unset otherwise).
- changeFontPopup: two color wheels + reset buttons; live preview reflects
font, size, text color and background color.
- Applied UI-wide via --wekan-ui-text-color / --wekan-ui-bg-color CSS variables
+ has-ui-text-color / has-ui-bg-color body classes (uiFont.js + uiFont.css).
- i18n text-color / text-background-color; tests extended (hex + negatives +
source guards).
Part of #4759.
New "Font" entry in the member menu opens a popup with a dropdown of fonts
DETECTED as actually installed in the browser (no free typing) and a Font
size dropdown of sensible presets (Smaller..Largest, no typed numbers). Both
have a Default option that unsets the custom choice.
Security: font names and size keys are validated server-side against a
curated whitelist / preset list (models/lib/uiFonts.js), so only a known safe
font name and a preset percentage ever reach the DOM. Labels are plain text
(Blaze-escaped) — never HTML or markdown. No CSS-injection surface.
- models/lib/uiFonts.js: UI_FONTS whitelist + SAFE_FONT_RE + isKnownFont /
fontFamilyValue and UI_FONT_SIZES presets + isKnownFontSize / fontSizeValue.
- client/lib/fontDetector.js: canvas width-comparison detection of installed
fonts, filtered to the whitelist.
- profile.uiFont / profile.uiFontSize schema + getters; setUiFont /
setUiFontSize methods (validated, null/default unsets).
- changeFontPopup (font + size dropdowns, both with Default) wired in
userHeader.js; applied UI-wide via CSS variables in uiFont.js + uiFont.css
(marker body classes), wired into client/features/main.js + client/styles.js.
- i18n change-font/font/font-default/font-size(+presets); tests/uiFonts.test.cjs
(pure + negatives + source guards) wired into test:unit:node.
This is a Member Settings feature (not an Admin Panel option).
Fixes#4759.
Both Board Settings and Member Settings Change Color now use a shared
category -> theme two-level dropdown picker with custom color wheels for the
flat (1 color) and clear (2 colors, a color slide) categories; dark/special
are fixed. Custom colors are validated server-side as #rrggbb, stored in
board.customThemeColors / profile.globalThemeCustomColors, and applied as
CSS variables (--theme-accent / --theme-accent-2) consumed by customTheme.css
to recolor the header, primary buttons and sidebar button.
- client/components/main/themeColorPicker.{jade,js}: shared picker, scope
"board" or "global", reusing the #5514 native color wheel + themeCategories.
- board.setColor(color, custom) + setGlobalThemeColor(color, custom): validate
via isValidCustomColors (flat/clear + count + hex) before storing.
- boardChangeColorPopup and changeColorPopup now render the shared picker;
removed their old swatch-list helpers/events.
- globalThemeColor.js: apply custom colors as :root CSS variables (board own
colors on a board, else the user global override) + marker body classes;
customTheme.css consumes them.
- Wire globalThemeColor.js / themeColorPicker into client/features/main.js and
customTheme.css into client/styles.js. This also fixes globalThemeColor.js
never having been imported (so the #5778 override now actually loads).
- i18n theme / theme-category[-flat/clear/dark/special]; tests
themeColorPicker.test.cjs + updated globalThemeColor.test.cjs.
Fixes#5778.
Partition the ~19 board/global theme colors into flat / clear / dark /
special and define where custom colors are allowed (flat = 1 accent,
clear = 2 for a color slide, dark/special = fixed none). This is the shared
source of truth for the redesigned Select Color picker (two-level dropdowns
+ color wheel) used by both Board Settings and Member Settings.
- models/lib/themeCategories.js: THEME_CATEGORIES + categoryOf /
colorsInCategory / allowsCustomColor / customColorCount /
isValidCustomColors (validates category + count + #rrggbb hex).
- tests/themeCategories.test.cjs (wired into test:unit:node): behaviour +
negatives + a source guard that the partition exactly equals
config/const.js ALLOWED_BOARD_COLORS.
- docs/Theme/Theme.md: full design — categories, two-level dropdown UX,
custom-color rules, CSS-variable rendering plan, storage
(board.customThemeColors / profile.globalThemeCustomColors), and
server-side validation.
Completes the global per-user theme override (Member Menu / Change Color)
begun in the previous commit.
Fixes#5778.
A new "Change Color" entry in the member menu works like Board Settings /
Change Color, but is a per-user GLOBAL override saved to the profile
(profile.globalThemeColor). The chosen color theme applies to the whole UI —
All Boards, Search All Boards, Admin Panel, My Cards, etc. — via a
board-color-<name> class on <body> and the header, so a user can pick a dark
theme (dark/midnight/moderndark/exodark/cleandark) or any other theme
everywhere. The first popup entry clears the override.
- models/users.js: profile.globalThemeColor field + getGlobalThemeColor /
globalThemeColorClass / setGlobalThemeColor helpers.
- server/models/users.js: setGlobalThemeColor method, validated against
BOARD_COLORS (rejects unknown colors so no arbitrary CSS class is injected)
and auth-guarded; null/empty clears it.
- client/components/main/globalThemeColor.js: Tracker autorun that adds the
class to <body> on non-board pages; on a board the board own color wins, so
the global class is cleared there to avoid two themes clashing.
- header.jade: #header and #header-quick-access fall back to the user global
theme class when not on a board (covers the #header-specific color rules).
- Change Color menu entry + changeColorPopup (color swatches + clear option),
wired in userHeader.js.
- i18n change-color / theme-default; tests/globalThemeColor.test.cjs wired
into test:unit:node.
Deleting a list no longer destroys it. The list and its cards are marked
(deletedAt/deletedBy/deleteBatchId), hidden from the board, recorded as a
reversible delete in history, and restorable via lists.restore or Ctrl+Z.
This is the first slice of "no permanent delete in ordinary use": physical
removal is limited to GDPR/account erasure and a Global-Admin purge gated by
a new Admin Panel / Features / Delete "Enable permanent delete" toggle
(off by default).
- models/lib/softDelete.js: pure notDeleted/softDeleteSet/restoreModifier/
canPurge helpers (+ tests/softDelete.test.cjs, wired into test:unit:node).
softDeleteSet omits falsy actor/batch and restore uses $unset so no null is
ever written to a Date/String schema field.
- enablePermanentDelete feature flag (models/lib/featureFlags.js) + Settings
schema field.
- deletedAt/deletedBy/deleteBatchId schema fields on Lists and Cards
(optional, no null default -> absent means live).
- board/swimlane list render paths filter deletedAt: null.
- server/models/lists.js: shared softRemoveList() + lists.softRemove /
lists.restore / lists.purge (purge gated on canPurge = isAdmin && flag);
client delete handler and REST DELETE now soft-remove.
- userPositionHistory undo()/redo() handle actionType delete/restore via a
batch-consistent _applyDeleted() helper.
Design + planned rollout (other entities, Recycle Bin, admin Delete category
table with GDPR data classes) in docs/Features/Undo/Undo.md.
Every user-facing delete becomes a soft delete (deletedAt/deletedBy), making
deletions restorable and undoable (#1023). No permanent delete in ordinary
use: physical removal is limited to GDPR/account erasure and an explicit
Global-Admin purge gated behind Admin Panel / Features / Delete ->
"Enable permanent delete for Global Admin" (off by default). Covers scope
(soft vs exempt collections), the softRemove/notDeleted mechanism, cascade
via deleteBatchId, archived-vs-deleted distinction, undo/History integration,
and migration steps.
The activities publication only ships the most-recent `limit` activities and
the client pages them in via loadNextPage, so large boards stay fast without
deleting activity history. Document this so the request is closeable.
Fixes#2539.
The 'Filter by date -> Overdue' and 'list disappears in swimlanes' fixes were
reported originally by email, not as GitHub issues - #6483 and #6484 do not exist
(404). Replace the dead issue links in their Upcoming entries with '(reported by
email)'. The fixes themselves are unchanged.
Thanks to xet7.
Adds tests (with negative cases) for the Upcoming changes that previously lacked
them:
- tests/defaultBoard.test.cjs (#2220): behavioural toggle/decision logic (set,
unset the current default, switch to a different board; negatives: nothing is
the default when unset, B is not the default when A is) + source guards that
the schema, model + server toggleDefaultBoard, once-per-session router redirect,
and board-tile toggle are all wired.
- tests/upcomingUiGuards.test.cjs: source guards for the #6465 Add List header
button + inline composer (leading column replaced by empty-state gating),
minicard white-space (min-height:0) and swimlane-header zoom clip
(overflow:visible/height:auto), and the #6478 touch confirmation before a
cross-swimlane list move (gated on touch/small screen; declining aborts).
The other Upcoming changes already had tests (filterDateNull, listMoveSwimlane,
cardDragSwimlaneScrollRefresh, popupAfterConfirm, undoRedoSelection,
listWidthDefaults, ferretdb* in the FerretDB repo).
Fixes#2220. Fixes#6465. Fixes#6478. Fixes#6479.
Thanks to xet7.
On the All Boards page each board tile gets a home (house) toggle next to the
star. Marking a board sets it as the user's default 'home' board; the next login
(or reload) lands on it instead of All Boards. Clicking again clears it.
- profile.defaultBoardId (per user) + getDefaultBoardId/isDefaultBoard/
toggleDefaultBoard model helpers + a toggleDefaultBoard Meteor method (mirrors
toggleBoardStar).
- home route redirects to the default board ONCE per session (so clicking All
Boards afterwards stays on the list; honoured again next login/reload); by id
so it works before the board subscription loads; graceful fallback if the board
is gone.
- board tile .js-set-default-board toggle + isDefaultBoard helper; i18n
set/unset-default-board-title.
Thanks to xet7.
Per request, the History design now records EVERY change a user makes (not just
card groups) in one append-only changeHistory store, and undo/redo becomes the
keyboard front-end to it (restore the current user's most recent change) rather
than a separate feature - superseding userPositionHistory.
Same table + restore is surfaced from many menus, each just a scope filter over
the one store: card group, whole card, Member settings (per-user), Board Settings
(per-board), Swimlane menu, List menu, and any future entity menu. Scopes nest
(card in list in swimlane in board) via id-column ORs. Added a scoped-views
section, the scope/scopeId page() params, per-scope permissions, and updated the
phasing (migrate position undo/redo onto changeHistory first). Undo.md now points
at History.md as the source of truth for the unified direction.
Thanks to xet7.
docs/Features/Undo/Undo.md documents the #6478 feature: position-move undo/redo
(card/list/swimlane) via userPositionHistory with an undone/undoneAt redo stack,
undoLast/redoLast methods, Ctrl+Z/Ctrl+Y bindings, and the touch confirmation for
cross-swimlane list moves. Records the inert-recording bug (collection referenced
as an un-imported global), the write/read wiring, retention, v1 limitations
(positions only; content undo is the separate History feature) and an extension
checklist.
Thanks to xet7.
Detailed design doc at docs/Features/History/History.md for the requested card
details 'History' feature: per-group history popup with a searchable, paginated
(current page only), per-contributor change table and restore.
Key point: WeKan's Activities log records what happened but NOT before/after
values, so restore needs a new append-only cardGroupHistory content-versioning
collection - the doc specifies the model, write/read (paginated+searchable+
per-user) methods, the 2-pane UI (History + contributor avatars | table with
select/change-type/content/datetime, search+pagination+restore bar), RTL
mirroring, restore semantics (re-logged to both source and restorer; history is
append-only), security, and a phased rollout starting with Description.
Thanks to xet7.
Swimlanes/Lists views no longer render a permanent Add List composer column.
Each list header now has a far-right add-list button; clicking it opens the Add
List composer as a column immediately AFTER that list in the DOM - which appears
to the right of the list in LTR and to the left in RTL (the lists lane is a flex
row that mirrors under dir=rtl). An empty swimlane/board shows the composer at the
start so the first list can still be added.
- new always-open addListInline template (title input + save + close), submit ->
createListAfter (afterListId = the clicked list, swimlaneId = the displayed
swimlane recorded from the DOM by the header button).
- Session 'wekan-add-list-after'/'wekan-add-list-swimlane' drive which list the
composer opens after; isAddListAfter/swimlaneHasNoLists/boardHasNoLists helpers.
- leading +addListForm removed from the non-mini swimlane and listsGroup
branches; mini-screen path unchanged.
First cut - desktop/main views; needs live verification of styling, RTL and the
empty-board case.
Thanks to csonkaoszimt (report) and xet7.
A user accidentally dragged a list into another swimlane on mobile (merging it
with an existing list) and spent ~15 min recovering - no confirmation, no undo.
Undo/Redo: the userPositionHistory collection modelled this but was NOT actually
recording (trackChange guarded on an un-imported global) and had no redo or key
bindings. Now:
- list moves are recorded in updateListSort (import UserPositionHistory so the
guard passes); card moves already had the call.
- add a symmetric redo() model method + an undone/undoneAt redo stack; add
userPositionHistory.undoLast/redoLast methods (a new action clears the redo
stack via trackChange).
- wire Ctrl+Z / Ctrl+Y (and Cmd variants) in client/lib/keyboard.js; disabled
inside inputs/textareas so native text undo is unaffected.
- pure selection logic models/lib/undoRedoSelection.js + tests.
Confirmation: on touch / small screens, moving a list to a DIFFERENT swimlane
(which re-homes every card) now asks for confirmation; a decline reverts the
drag. Same-swimlane reordering is unaffected.
Thanks to mueschel (report) and xet7.
Clicking a .js-confirm button silently did nothing. Popup.afterConfirm() stashed
the pending action as a field on the Blaze DATA CONTEXT and the confirm button
read it back as this.__afterConfirmAction. But the context is a
ReactiveCache/Minimongo document (a board member sub-doc), and Blaze re-renders
the confirmation popup with a fresh/immutable copy of that context, so the field
was gone by click time - so every afterConfirm dialog (remove member from board,
etc.) no-oped.
Store the action on the Popup INSTANCE (Popup._afterConfirmAction) instead, which
survives the re-render; it is still invoked with the confirmation popup's own data
context as this, so this.userId keeps working. The .js-confirm handler also guards
against a missing action instead of throwing. Test tests/popupAfterConfirm.test.cjs
with a negative case reproducing the old context-mutation failure.
Thanks to mueschel (report) and xet7.
Dragging a card from one swimlane to the SAME list in another swimlane dropped
it back in the source swimlane, especially with long overflowing lists. The card
sortable's sort handler auto-scrolls the board (#443) via .js-lists scrollLeft /
.board-canvas scrollTop, but jQuery UI sortable caches container/item geometry at
drag start and only re-caches on its own placeholder moves - never on our manual
scroll. Long lists are exactly what triggers auto-scroll, so the drop resolved
against a stale geometry map and the stop handler's ui.item.parents('.swimlane')
persisted the wrong swimlane.
Re-cache positions with sortable('refreshPositions') whenever the sort handler
actually scrolled - complementing the #2769 DOM-mutation refresh in listBody.js,
which does not fire on scroll. Source-guard test
tests/cardDragSwimlaneScrollRefresh.test.cjs.
Thanks to mueschel (report) and xet7.
The #6465 density change lowered MIN_LIST_WIDTH 270->200 and the default
272->220, but specs/38-fixed-list-width.e2e.js still asserted the old 270 floor:
setFixedListWidth(200) was expected to be REJECTED (it is now valid), and the
anonymous default was expected to be 272. This failed on chromium/firefox/webkit.
Use 150 as the too-small value (below the new 200 floor), update the client MIN
mirror to 200 and the default to 220, and add a POSITIVE case that a width of
210 (>=200 but below the old 270) is now accepted.
Thanks to xet7.
v9.99 was released with the LDAP connection-leak fix, the bundled-FerretDB pool
note and the snap #6476 wait-loop diagnostics. Everything committed after the
v9.99 bump - the QEMU snap build, the #6465 UI density/minicard/dock/swimlane
changes, the #6483 date-filter fix, the #6484 list-move fix, and the #6476
root-cause (FerretDB orphaned-table crash loop) - now sits under a new
# Upcoming section above v9.99 instead of being backdated into v9.99.
Thanks to xet7.
Updates the #6483 note: label ($in), title ($regex) AND date range
($gt/$gte/$lt/$lte) filters are all now pushed down to SQLite in the bundled
FerretDB, so none of them full-scan the board. Dates use SQLite's ->> accessor
for numeric comparison of the stored millis, verified end-to-end.
Thanks to xet7.
Notes that the filter slowness flagged earlier is addressed in the bundled
FerretDB, which now pushes {labelIds: {$in: [...]}} and literal {title: {$regex}}
down to SQLite instead of scanning + matching every card's sjson in Go.
Thanks to xet7.
At e.g. 80% zoom (transform: scale on .board-wrapper) the gray swimlane header
(.swimlane-header) looked cut off: it had overflow: hidden with a fixed
min-height, so once the scaled layout rounded, the bar/text was sliced. Use
overflow: visible + height: auto (keeping the min-height floor) so the header
grows to fit and cannot clip vertically; short swimlane names are unaffected and
a long name wraps instead of being cut.
Speculative CSS fix - verify in the running app at 80% (and check 50%/125%/200%).
Thanks to xet7.
Operator logs (uusijani, a1bert01) showed FerretDB was listening and WeKan
connected, then a startup SyncedCron upsert hit FerretDB error
'table accountslockout.connections_<hash> already exists', which became an
unhandledRejection and exited the process; systemd crash-looped it so the HTTP
port never came up. The real fix is in the bundled FerretDB (collectionCreate
now uses CREATE TABLE/INDEX IF NOT EXISTS to re-adopt an orphaned table). The
snap wait-loop timeout/diagnostics remain useful for the separate 'FerretDB
genuinely not ready' case.
Thanks to xet7.
Several users reported 9.x wastes space vs the older 6.09 layout. Tighten three
density defaults (all overridable; user customizations untouched):
- List width default 272 -> 220px (models/lib/listWidth.js, models/lists.js),
minimum 270 -> 200px here and in the server check (server/models/users.js).
Lists already render the same width by default (resolveListWidth) on private
and public boards; now narrower so more fit. Test updated.
- Board tiles (All Boards page): min-height 100 -> 72px, vertical padding
36/32 -> 24/18px (boardsList.css).
- Card-detail right dock max-width 800 -> 520px (and the wide-screen centred
variant 600 -> 520px) in cardDetails.css, so the opened card no longer takes
~1/4 of the screen.
Thanks to xet7.
A board-wide list has swimlaneId === null and renders under every swimlane. The
list-drop handler computed
isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId
but targetSwimlaneId is always the swimlane the list is shown under, and
originalSwimlaneId is null for a board-wide list, so ANY small drag made this
true - it set list.swimlaneId AND moved every card in the list to that one
swimlane, so the list vanished from all other swimlanes (recoverable only by
restoring a backup).
Also require originalSwimlaneId: board-wide lists stay board-wide on a reorder,
while a genuinely swimlane-scoped list can still be moved between swimlanes. Test
tests/listMoveSwimlane.test.cjs, with a negative control that the old target-only
condition rebound a board-wide list.
Thanks to xet7.
Cards without a date store the field as null (that is what the sidebar's noDate
filter matches: {dueAt: null}). The comparison filters build {$lte: now}
(Overdue/past) and {$gte, $lte} (today/this/next week), and $lte MATCHES null
under both FerretDB and minimongo - null sorts before any date and neither
implements MongoDB type-bracketing - so Overdue selected every dateless card.
DateFilter._getMongoSelector now adds $ne: null to any comparison selector so
only real dates match; the noDate filter (a bare null) is unchanged. Test
tests/filterDateNull.test.cjs, with a negative control proving the old bare
{$lte: now} matched null.
The reported filter SLOWNESS and the title/label over-matching are separate
(FerretDB scanning $regex/$in/range queries not pushed down to SQLite) and need
live reproduction.
Thanks to xet7.
The minicard title renders inside a .viewer, which inherited the global
.viewer { min-height: 2.5vh } (client/components/main/layouts.css). On tall
screens that reserved a tall empty band beneath a one-line title, so a large
part of every minicard was blank and fewer cards fit on screen. The minicard
title .viewer now sets min-height: 0 (the same override already applied to the
label .viewer), so a minicard collapses to fit its content.
Thanks to xet7.
The Launchpad 'snapcraft remote-build' legs for ppc64el and s390x ended in
Launchpad state 'Stopped' with no snap produced, yet remote-build still exited 0;
the job's name-only success check let an empty/stale file through and the upload
died with ''wekan_<v>_<arch>.snap' is not a valid file' (exit 64). Build them
instead with docker/setup-qemu-action + diddlesnaps/snapcraft-multiarch-action on
a normal runner - fast, because the snap is bundle-centric (the wekan part only
downloads the prebuilt wekan-<version>-<arch>.zip and repackages it, no native
compilation).
riscv64 stays on Launchpad (the QEMU snap action does not support it) but now has
timeout-minutes: 180 so a stuck build fails with a clear message instead of
GitHub's silent 6-hour cancel.
Thanks to xet7.
With database=ferretdb, snap-src/bin/wekan-control waits for FerretDB to accept
connections before starting node - WeKan does not open its HTTP port until the
database answers. That wait loop had no timeout and no diagnostics, so if
FerretDB never started listening (crashed or arch-incompatible per-arch binary,
locked/corrupt SQLite, or the service left disabled by a failed migration
hand-off), WeKan blocked there forever: the service showed active but the port
never opened and the only symptom was a silent 'FerretDB not ready yet' loop.
uusijani's wekan.log stopped right after the startup env dump, confirming
control never reached node main.js.
Mirror the MongoDB branch: after WEKAN_DB_WAIT_TIMEOUT seconds (default 120,
overridable) print an actionable hint once, then keep retrying - pointing to
'snap logs <snap>.ferretdb', naming the SQLite dir to check for locks/corruption,
the arch-mismatch case, the snap start --enable recovery, and the
'snap set <snap> database=mongodb' fallback. Regression test
tests/ferretdbWaitTimeout.test.cjs, including a negative guard that the wait is
not a silent unbounded loop again.
Thanks to uusijani (report) and xet7 (fix).
tx pull -f fills any string untranslated on Transifex with the English source, so
languages whose translations live only in git silently revert to English on every
pull. Make the pull self-heal, and fix the push helpers so restored translations
actually upload.
- pull-translations.sh: after the pull, for each language file whose changes are
ENTIRELY reverts, git checkout HEAD -- <file> to restore the committed
translations and force-push them back (../tx push -t -l <lang> -f). Files that
also carry a real new/updated translation from Transifex are NOT auto-healed
(a whole-file checkout would discard it) and are left for manual handling.
- report-english-regressions.mjs: add a machine-readable --files mode and an
otherChanges classifier, so only pure-revert files are offered for auto-heal;
the human report tags mixed files.
- push-translation.sh: add -f (without it the Go tx CLI skips a language whenever
the local file is not newer than the server copy - push.go shouldSkipPush - so
a freshly reverted file uploads nothing), plus a usage guard.
- push-copy-en-gb-translation.sh (new): copy en.i18n.json to en-GB.i18n.json
verbatim and force-push it as the en_GB translation, so British English shows
as translated instead of reverting to the English source on each pull.
Thanks to xet7.
Operators reported that after an update WeKan "dies really quickly", restarts
did not help, and it "kills our openldap server with too many open connections";
others saw logins hang for minutes then fail with "Must be logged in". Every
LDAP login attempt (loginHandler.js) and every background sync tick (sync.js)
created a new LDAP() and connect()ed but NEVER called disconnect() on any path,
so each attempt - including every failed login and every one-minute sync tick -
leaked a socket to the LDAP/AD server until it hit its connection limit and
started refusing connections, taking it and all WeKan logins down.
Guarantee release on every exit path: a shared runWithLdapDisconnect() helper
(connectionGuard.js) disconnects in a finally; the login handler runs its whole
flow through it; background sync() disconnects in a finally; importNewUsers()
disconnects only the connection it owns; and LDAP.disconnect() is a safe no-op
when connect() was never reached. Adds behavioural + negative + source-guard
tests (tests/ldapConnectionRelease.test.cjs, wired into test:unit:node).
Also bumps the bundled FerretDB whose SQLite pool no longer caps MaxOpenConns
at 16 (that starved WeKan's cursor load: minutes-long board loads, failed
logins).
Thanks to xet7.
Document the two GitHub CodeQL code scanning fixes already committed
(alerts #423 js/incomplete-sanitization and #422
js/biased-cryptographic-random) in a new Upcoming section above v9.97,
linked to the wekan.fi Hall of Fame entries.
Thanks to GitHub CodeQL and xet7.
256 is not a multiple of the 55-character id alphabet, so the plain
byte % 55 mapping skewed generated document ids toward the first
characters. randomId now uses rejection sampling: bytes at or above the
largest multiple of the alphabet size (220) are discarded and resampled,
so every character is equally likely. Ids stay Meteor-style 17-char and
exact-length for custom lengths. Resolves code scanning alert #422.
Tests: tests/schemaUpgradeSteps.test.cjs gains a rejection-sampling
guard (negative case pins that out-of-range bytes are never wrapped).
Thanks to xet7.
The CSS-declaration-to-RegExp conversion escaped only parentheses;
escape the full regex metacharacter set (including backslash) instead.
Resolves code scanning alert #423. Test-only, fixed input list, but the
complete escape is correct and matches the pattern already used in
tests/archiveLinkGrouping.test.cjs.
Thanks to xet7.
2026-07-17 04:52:30 +03:00
628 changed files with 132159 additions and 96092 deletions
- name:Verify snapcraft.yaml version matches the release
run:|
set -euo pipefail
if ! grep -qxF "version: '${VERSION}'" snapcraft.yaml; then
echo "::error::snapcraft.yaml is not at version '${VERSION}' ($(grep -m1 '^version:' snapcraft.yaml)). The bump job (releases/version.sh) did not update the tagged commit."
exit 1
fi
echo "snapcraft.yaml is at version '${VERSION}'."
- name:Set up QEMU for ${{ matrix.arch }} emulation
uses:docker/setup-qemu-action@v4
- name:Build the ${{ matrix.arch }} snap under QEMU
# Runs snapcraft inside a ${{ matrix.arch }} container via QEMU; snapcraft
# sees build-on: ${{ matrix.arch }} in snapcraft.yaml and builds that
# platform. Outputs the built .snap path as steps.build.outputs.snap.
id:build
uses:diddlesnaps/snapcraft-multiarch-action@v1
with:
architecture:${{ matrix.arch }}
- name:Verify the built snap is ${{ matrix.arch }}
run:|
set -euo pipefail
snap="${{ steps.build.outputs.snap }}"
echo "Built: $snap"
case "$snap" in
*_${{ matrix.arch }}.snap) : ;;
*) echo "::error::Built snap '$snap' is not ${{ matrix.arch }}."; exit 1 ;;
esac
- name:Push ${{ matrix.arch }} to the Snap Store (stable + candidate + beta + edge)
# Publish two extra snaps that are the newest wekan code under a different snap
# name. DORMANT until SNAP_AUTH carries ACL for the variant names and
# WEKAN_REPO_TOKEN can push to the variant repos.
# See docs/Design/Autoupdate/Snap-Ondra-Gantt.md.
snap-variants:
# DISABLED per maintainer request. Re-enable by removing this `if`.
if:${{ false }}
needs:[prepare, release]
continue-on-error:true
timeout-minutes:60
strategy:
fail-fast:false
matrix:
include:
- repo:wekan-ondra
snapname:wekan-ondra
title:"Wekan Ondra"
- repo:wekan-gantt-gpl
snapname:wekan-gantt-gpl
title:"Wekan Gantt GPL"
runs-on:ubuntu-latest
env:
VERSION:${{ needs.prepare.outputs.version }}
steps:
- name:Check required secrets are configured
id:guard
env:
REPO_TOKEN:${{ secrets.WEKAN_REPO_TOKEN }}
SNAP_AUTH:${{ secrets.SNAP_AUTH }}
run:|
if [ -z "$REPO_TOKEN" ] || [ -z "$SNAP_AUTH" ]; then
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::snap-variants (${{ matrix.snapname }}) skipped — set WEKAN_REPO_TOKEN and SNAP_AUTH (with ACL for ${{ matrix.snapname }}). See docs/Design/Autoupdate/Snap-Ondra-Gantt.md."
if [ -z "$REPO_TOKEN" ] || [ -z "$UCS_USER" ] || [ -z "$UCS_PASS" ]; then
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::ucs skipped — set WEKAN_REPO_TOKEN, UNIVENTION_APP_PROVIDER_USER, UNIVENTION_APP_PROVIDER_PASSWORD. See docs/Design/Autoupdate/UCS.md."
else
echo "ok=true" >> "$GITHUB_OUTPUT"
fi
- name:Checkout the wekan/univention app repo
if:steps.guard.outputs.ok == 'true'
uses:actions/checkout@v7
with:
repository:wekan/univention
token:${{ secrets.WEKAN_REPO_TOKEN }}
path:univention
- name:Update the app to WeKan ${{ needs.prepare.outputs.version }}
if:steps.guard.outputs.ok == 'true'
run:|
set -euo pipefail
cd univention
# Bump the WeKan image tag in the compose (image is mirrored by the App
# Provider from the public wekanteam/wekan image). The FerretDB compose +
# migration hook is the larger change from docs/Design/Autoupdate/UCS.md.
sed -i "s#\(wekan-wekan:\)[^ ]*#\1${VERSION}#g" component/compose || true
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
git commit -m "WeKan ${VERSION} for UCS" || echo "no changes"
git push origin HEAD:master
- name:Upload the app to the Univention App Provider
@@ -10,8 +10,19 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
- **Report**: Click "New draft security advisory" at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
More info at [GitHub Docs about reporting privately](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately).
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
-**If GitHub does not work**: Please send report to **security@wekan.fi**
- **Optional Email Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
-DO NOT EMAIL [security@wekan.fi](security-at-wekan.fi.asc), because:
- Email is NOT secure or private.
- Using GitHub directly verifies that:
- You are:
- Saving my time by filling all details at GitHub, so that I can easily click request CVE at GitHub
- A correct GitHub user
- Really reading this SECURITY.md
- Knowing what you are doing
- You are not:
- Someone that tries to waste my time
- Trying to impersonate as some other GitHub user
- Spammer
- Bot
### 2. WeKan Security Team:
@@ -23,30 +34,26 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
### 3. Post-Release and Public Disclosure:
- Publicity: If you publish blog posts, give talks, or write articles about the
reported vulnerabilities, please send the links to us. We will link them
to your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/) for additional context.
- Advisories reported at GitHub are listed at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
## How Should Reports Be Formatted?
```
Name: %name
Twitter: %twitter
Bug type: %bugtype
Domain: %domain
Severity: %severity
URL: %url
PoC: %poc
CVSS (optional): %cvss
CWSS (optional): %cwss
```
- DO NOT EMAIL [security@wekan.fi](security-at-wekan.fi.asc), because:
- Email is NOT secure or private.
- Using GitHub directly verifies that:
- You are:
- Saving my time by filling all details at GitHub, so that I can easily click request CVE at GitHub
- A correct GitHub user
- Really reading this SECURITY.md
- Knowing what you are doing
- You are not:
- Someone that tries to waste my time
- Trying to impersonate as some other GitHub user
- Spammer
- Bot
## Who can participate in the program
Anyone who reports a unique security issue in scope and does not disclose it to
a third party before we have patched and updated may be upon their approval
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
a third party before we have patched, and is able to add
"New draft security advisory" at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
Loaded 100 of 628 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.