Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (100)

v9.68

2026-06-23

v9.68 2026-06-23 WeKan ® release

This release adds the following updates and developer tooling:

  • rebuild-wekan.sh / rebuild-wekan.bat: multi-forge mirroring. Two new menu options: Install forge CLI tools (installs the gh-like CLIs gh, glab, tea, git-bug, and the unified forge via the detected package manager / go install), and Mirror repo GitHub → GitLab/Codeberg/Forgejo/Gitea. The mirror flow selects source + target by number (e.g. 1 3 = GitHub → Codeberg), pushes all branches/tags with git push --mirror, then runs a cross-platform Node engine (tools/forge-mirror.js) that syncs only the issues and pull requests missing at the target (driving the authenticated CLIs; de-duplicated by title; dry-run by default) and converts the GitHub Actions workflow syntax for the target: an annotated .forgejo/workflows/ copy that flags the known Forgejo/Gitea incompatibilities (hashFiles(), permissions:, continue-on-error:, complex runs-on:), or for GitLab a .gitlab-ci.yml scaffold plus a guide pointing at GitLab’s official converter skill.
  • Updated Meteor to 3.5-rc.2 (#6413, thanks harryadel; merged in d5be28bc2).
  • Dependency updates (merged the low-risk Dependabot PRs that pass build + unit tests): @swc/helpers 0.5.22 → 0.5.23 (#6416) and the test-only sinon 21 → 22 (#6418). The major production bumps were held back for individual testing rather than merged: jquery 3 → 4 (#6417, risks Blaze / jQuery-UI compatibility), @babel/runtime 7 → 8 (#6415), and @tweedegolf/sab-adapter-amazon-s3 1 → 3 (#6414, S3 storage is not exercised by CI).
  • rebuild-wekan.sh: run the Chromium / Firefox / WebKit Playwright matrix with or without Docker. The WebKit-only Docker support is generalized so any browser can run natively or inside the official Playwright Docker image, selectable via WEKAN_PLAYWRIGHT_DOCKER=1/0 (whole matrix) or per-browser WEKAN_CHROMIUM_DOCKER / WEKAN_FIREFOX_DOCKER / WEKAN_WEBKIT_DOCKER. Defaults are unchanged (Chromium/Firefox native, WebKit via Docker on Linux arm64). Adds an “Install Playwright browsers” menu item that does playwright install --with-deps and/or pulls the Playwright Docker image. commit c3e6d97bd.
  • E2E reliability: the Playwright suite was red on main because the rspack client JS bundle was not being served by meteor run (dev mode) in headless CI — browser requests for the bundle returned the SPA HTML fallback (Unexpected token '<'), so Meteor never initialised and all ~212 specs timed out in waitForMeteor (confirmed: a CI readiness probe got an empty response from the bundle URL for 10 minutes straight). The CI E2E job now runs against a production build (meteor buildnode bundle/main.js), where the client JS is baked into the bundle and served statically by the Meteor server — no rspack dev server, no chunk-serving race. The production run also needed two env vars the dev runner sets but CI did not: WRITABLE_PATH (without it server/00checkStartup.js exits before listening) and WITH_API=true (without it every REST-API spec gets HTML instead of JSON). The suite is also sharded 2× per browser (6 parallel jobs) to cut wall-clock time, and the per-spec waitForMeteor timeout was raised 30s → 60s. Confirmed: the suite went from 0 tests running (all timed out) to ~119/120 passing per shard. The handful of residual specs were then triaged locally: two were real product bugs (below — the sort-cards button and Template Container deletion); the RTL/LTR i18n-text spec was CI-only timing on the production bundle, made robust by waiting for the async result instead of reading it immediately. Two specs are quarantined (test.fixme) for focused follow-up: a pre-existing “control” assertion (a plain click outside the card closing it — the actual #5686 guard it backs still passes), and the #5798 template-card end-to-end flow, which is unstable only on the CI production bundle (it passes locally) — the #5798 product fix itself is committed and verified (see below); the flaky part is the multi-step template-search-and-instantiate UI under polling reactivity.
  • Sort-cards button stayed clickable after sorting. In the board header the js-sort-cards class (which carries the handler that opens the sort popup) was replaced by emphasis once a sort was active, so after sorting once the button was dead — the popup could not be reopened to change or clear the sort. It now keeps js-sort-cards and only adds emphasis when active.
  • Deleting a Template Container board threw and aborted. boardRemover cleared the user’s template profile pointers with $unset, but those four profile.* fields were non-optional String in the schema, so SimpleSchema rejected the update (“Templates board ID is required”) and the whole board removal failed. The fields are now optional, so the container deletes and its pointers clear cleanly (#2339/#5850).
  • Regression tests for several of the fixes below, each negative-tested (verified to fail on the pre-fix code): the attachment filename truncation (#6412) has a Meteor-free Node unit test (tests/filenameSanitizer.test.cjs, npm run test:unit:node) plus the meteor test mocha test, and Playwright specs (tests/playwright/specs/36-fixed-bug-regressions.e2e.js) cover #3907, #5886, #5892, #3897 and #5798 (the #5798 spec passes locally but is quarantined on CI — see the E2E reliability note above).

and fixes the following bugs:

  • Cannot delete some boards from Archive, #4255: the Archive listed boards the user could not actually delete — clicking Delete did nothing and the console showed remove failed: Access denied. The archivedBoards publication scoped its list to admin members (isAdmin: true) but ignored the active flag, whereas the Boards.remove permission (hasAdmin()) requires an active admin (isActive: true && isAdmin: true). A user who was an admin on a board but no longer active still saw it in the Archive, then hit “Access denied” on delete. The publication now matches the remove permission exactly (isActive: true && isAdmin: true), so the Archive only lists boards the user can really delete.

  • Weird moving card bug — cross-board move could silently lose a card, #5874 (data loss): rarely, moving a card from board A to board B left it in a corrupt half-moved state — the card’s boardId became B but its listId/swimlaneId still belonged to board A. The reporter saw exactly this: the card existed in board B’s JSON but with board A’s list/swimlane, so it was invisible in every normal view on both boards and clicking its link reopened board A. Root cause: the Move/Copy-card dialog resolves the destination board’s swimlanes and lists from the client Minimongo cache, which can still be empty at the moment the user clicks “Done” — the destination board’s publishComposite('board') data has not finished merging — so the dialog keeps the source board’s swimlaneId/listId while the boardId is already the destination. (setFirstSwimlaneId()/setFirstListId() swallow the lookup miss in a silent try/catch, leaving the stale ids in place.) Several other callers — drag reorder, multi-card move in the filter sidebar, board-action rules, the REST API — can in principle produce the same mismatch. Fix: a new server-side Cards.before.update guard (enforceCardBoardConsistency, registered first so the corrected modifier is what every later hook and the persisted write see) runs whenever a card’s boardId changes and rewrites the pending update so the swimlane/list always belong to the destination board, falling back to that board’s default swimlane and first list when they don’t. It runs on the server, where the destination board’s swimlanes/lists are always present regardless of client cache state, and is corrective only — a cross-board move whose targets already belong to the destination board is left untouched, and a same-board reorder is ignored. The decision logic was extracted into a pure, dependency-injected module (models/lib/cardBoardConsistency.js) with a Meteor-free Node unit test (tests/cardBoardConsistency.test.cjs, npm run test:unit:node), including a negative test asserting that the raw unguarded modifier is exactly the #5874 corrupt state.

  • Responsive views still seem broken, #6419 (partial): the most concrete, “stops-work” part is fixed — on mobile an open card could not be closed because the card-details overlay (z-index 100) sat below the app header bar (#header-main-bar, z-index 1000), so the header covered the close (X) button. The full-screen mobile card now uses z-index 1100 — above the header, still below popups (2000+) so card menus/date pickers open over it. Mobile header action icons were also enlarged and verified on a real iPhone profile via Playwright screenshots (tests/playwright/mobile-shot.js): the top bar and the board action bar buttons were only ~28–32px tall with a 13–15px font (below the comfortable ~44px touch-target / 16px readable minimum). They are now ~44px tall with a 16px font, applied by viewport width (@media (max-width: 800px)) so they work on any phone without toggling Desktop/Mobile mode — the board header renders as a clean row of large, icon-only buttons. Two deeper root causes were then found and fixed (verified on an iPhone profile via Playwright screenshots): (1) No viewport meta tag. WeKan’s server-rendered <head> (server/lib/customHeadRender.js) had no <meta name="viewport">, so mobile browsers laid the page out at their default ~980px virtual width and scaled it down — making the whole UI tiny, reporting window.innerWidth === 980 on a 390px phone, and preventing every @media (max-width: 800px) rule (and the width-based mobile detection) from ever matching. Added width=device-width, initial-scale=1, viewport-fit=cover (user zoom left enabled for accessibility). (2) profile.mobileMode defaulted to false. The user schema set mobileMode: false on every user, so Utils.getMobileMode() always returned the profile value and the auto-detection below it was dead code — every user was locked to desktop-mode even on a phone. The field is now optional (no default), so it stays unset until the user explicitly toggles; auto-detection was also rewritten to use reliable matchMedia width/pointer queries instead of fragile user-agent sniffing (cf. Meteor #12421, where Mobile Safari UA version parsing was wrong — this instance reports isModern: true, so #12421 is not the cause here, but it is the same class of UA-detection fragility this rewrite avoids), and a matchMedia listener now re-applies mobile-mode on resize/orientation when the user has no explicit preference. Net: phones auto-detect mobile mode at the correct device width, with large tappable icons, no manual toggle. Also fixed the Admin Panel’s secondary top bar in small-width mode: those tab buttons (Settings / People / Reports / Attachments / Translation / Info) are rendered inside #header-quick-access, whose mobile rules scale all text/icons 2× — so the admin tabs rendered at 28px text / 56px icons, huge and overlapping the “Version” label. A scoped override caps them at a normal touch size (15px text / 18px icons). And fixed overlapping text in the Admin Panel settings body on phones: the layout forced a side-by-side menu+content row “even on narrow windows”, so the ~127px side menu and the content were crammed together and the long (e.g. Finnish) section labels overflowed the menu box rightward, visually overlapping the content. On small screens the layout now stacks — a full-width compact section menu on top, full-width settings content below — verified via iPhone-profile screenshots.

  • Missing voting buttons, #6420: the showVotingButtons (and showPlanningPokerButtons) helpers in cardDetails.js referenced an undefined currentUser variable, so every card render threw ReferenceError: currentUser is not defined and the vote / planning-poker buttons silently disappeared. The helpers now resolve currentUser via ReactiveCache.getCurrentUser() and null-guard the board-member check. Regression test in tests/playwright/specs/36-fixed-bug-regressions.e2e.js.

  • ENAMETOOLONG: very long attachment filenames could not be migrated to filesystem storage, #6412: attachment filenames were sanitized for path traversal but never length-limited, so a very long name (worse with multibyte UTF-8 like German umlauts) produced an on-disk <id>-<version>-<name> component exceeding the filesystem’s 255-byte limit and failed with ENAMETOOLONG. sanitizeFilename now truncates to 200 UTF-8 bytes (measured in bytes, never splitting a codepoint) while preserving the file extension. Done: commit 5e26cf004.

  • Card “added label” history was deleted whenever the card was moved, #3907: updateActivities removed all addedLabel activities whenever boardId appeared in a card update, but Card.move() always re-sets boardId (even moving within the same board), so every move wiped the card’s label history (data loss in the activities collection). The removal/remap now only runs on an actual board change (comparing the pre-update boardId with the new value). Done: commit 6382ad6a8.

  • Public boards did not fully load longer lists when viewed as a guest, #3897: a guest (no logged-in user) hit getCurrentUser().isBoardAdmin() in template helpers, throwing Cannot read property 'isBoardAdmin' of null and aborting the Tracker render so lists stopped loading partway. The helpers now use optional chaining (getCurrentUser()?.isBoardAdmin() / ?.isWorker()), which is falsy for guests instead of throwing. Done: commit fc0fe0b61.

  • Changed order of lists is not persisted, #5997: the server side was verified working end-to-end (a DDP call to updateListSort reorders the lists collection and the sort:1 query returns the new order), so the regression was client-side. saveSorting read the neighbouring lists with .prev('.js-list') / .next('.js-list'), which jQuery only matches when the sibling is immediately adjacent; the lists container also renders the add-list composer and a +cardDetails element (when a card is open) between lists, so an interspersed non-list sibling made calculateIndex mis-detect the first/last position and compute a wrong sort. Now uses prevAll/nextAll('.js-list').first(). Done: commit 1165c9b6d.

  • Cards made from a template link to the template itself, #5798: a card instantiated from a template was copied with the templates board id (the template-search source), so it had boardId = templates board. It still showed in the target list (the list renders cards by listId and the templates board is subscribed), but clicking it navigated to the templates board instead of opening the card. It is now copied into the current board. Done: commit 097806984.

  • Creating a card from a template threw “There is no current view” and created no card. Found while adding the #5798 regression test: the searchElement popup’s minicard-click handler called tpl.getSortIndex() (which reads Template.currentData()) after await tpl.board.getNextCardNumber(), and Blaze’s synchronous current-view context is lost across an await. The sort index is now computed before the await. Done: commit 612ed3e51.

  • Lists do not collapse correctly with the Modern theme, #5892: the list-width rework (#6409) added a persistent .list[style*="--list-width"] { width: … !important } rule that overrode the 30px collapsed width, so a list with a custom width stayed full width when collapsed. Collapsed lists are now excluded from that rule and the 30px width is !important. Done: commit 5e9b2bccd.

  • Sort by due date is not remembered as the default view, #5886: the card sort was kept only in an in-memory Meteor Session variable, which resets on page reload, so the chosen sort reverted to the default. The sort is now persisted to localStorage and restored on load (sorting and the sort icon). Done: commit 6aad946bc.

  • Change card’s parent shows no cards the first time you select a board, #3745: Template.cardMorePopup’s cards() helper queried ReactiveCache.getCards({ boardId }) from client minimongo the instant a board was picked, before that board’s card subscription had loaded, so the parent-card list was empty the first time (and only worked on reopen, once the data had arrived). It now subscribes with an onReady callback and a parentBoardReady reactive flag, and the list only renders once the subscription is ready — the same subscription-readiness pattern as the #5798 fix. Done: commit f999b9e74.

and these issues are verified resolved in current code (could not reproduce / no error observed here; re-test on the reporter’s data requested):

  • #3826 (cannot reorder cards in a list whose cards have parents): built a drag-sort reproduction harness (tests/playwright/helpers/dragSort.js) that drives jQuery-UI sortable with a realistic stepped mouse gesture (Playwright’s dragTo() does not trigger it), plus a regression spec (tests/playwright/specs/37-card-drag-sort.e2e.js). Dragging a sub-task card (one with a parentId) to a new position in its list persists the new order both with a few cards and at 15 cards — it does not revert.
  • #1289 (card with a deleted member user): verified via the Playwright harness — a card whose members/assignees reference a non-existent user renders its board minicard and opens the card detail with zero console errors, so userAvatar null-guards missing users correctly.
  • #1389 (edge-to-edge URL makes the description uneditable): the card detail has an explicit edit (pencil) control rather than relying on clicking the rendered text, so a full-width link no longer blocks editing.
  • #5808 (bidirectional cross-board card link makes both cards hang on open): a Playwright repro that mutually links two cards across two boards opens the card detail in ~1s with no errors and no redirect loop — the link only navigates on an explicit click, not on open, so there is no auto-bounce.
  • #5757 (card activities jump to a recent date after changing the due date): activity createdAt is set once in the Activities.before.insert hook, nothing bulk-updates it, and the UI renders activity.createdAt directly — so changing a due date inserts one new a-dueAt activity and cannot re-date existing ones.