Every deletion path (admin removeUser method, self-service account
delete, DELETE /api/users/:userId) removed only the users document,
leaving dangling references everywhere: empty-avatar board members that
could not be removed, stale card members/assignees/watchers, orphaned
avatar files - reproducible for 8 years and reopened in 2025. A
server-only Users.after.remove collection hook now prunes
boards.members/watchers (object-shaped, $pull by userId),
cards.members/assignees/watchers and lists.watchers (string arrays), and
the user's Avatars files, on EVERY path that removes a user; a cleanup
failure can never break the deletion itself. Activities and comments are
deliberately preserved for history - their rendering is already
null-guarded (270eacb20, Playwright-verified).
Tests: tests/userDeletionCleanup.test.cjs (6, positive and negative,
end-to-end against a fake database).
Thanks to chotaire and xet7.
Drag start hid every list's "+ Add Card" composer link with
display: none, collapsing its row: lists shrank, auto-height swimlanes
shrank, and every swimlane below jumped up about one row (and back down
on drop), moving the drop target under the cursor mid-drag. The root
cause was proven by frame-diffing the issue's own GIF: the three largest
frame discontinuities are exactly drag start and drop, showing the
composer row vanish and the next swimlane header shift ~23px. The
composer now hides with visibility: hidden, preserving its layout box;
checked multi-selection cards keep display: none intentionally (the drag
payload previews the post-drop list).
Tests: tests/swimlaneDragJump.test.cjs (8, positive and negative,
including a guard that flags any return of the layout-collapsing rule).
Thanks to gerroon and xet7.
off-screen list.
The horizontal auto-scroll in the card sortable targeted .board-canvas,
which stopped overflowing horizontally when the swimlane layout made the
.js-lists lane the horizontal scroller - scrollLeftMax was always 0, the
guard compared 0 < 0 and never fired, and ui.helper[0].offsetLeft -= 15
assigned a read-only property. Edge-proximity auto-scroll now drives the
lane actually under the pointer (axis-agnostic, clamped, overshoot-safe
helpers in imports/lib/boardAutoScroll.js), with vertical scrolling kept
on the board canvas.
Tests: tests/boardAutoScroll.test.cjs (15, positive and negative,
including a reproduction that the old no-overflow target can never
scroll).
Closes#603: obsolete - the stale-drop-target cause was removed by
d887e553c (perfect-scrollbar removal, native list scrolling),
ed8471be9 (sortable auto-scroll re-enabled) and jQuery UI 1.14.2, whose
sortable resolves insertion neighbours with live offsets on every
mousemove, so a scrolled target list cannot desynchronize drop targets.
Thanks to anhenghuang, AlexanderS and xet7.
Sandstorm username races.
- #2345: the Card Title Filter is now SAVED by every board trigger
builder (a trigger doc missing the field could never satisfy the
matcher's $in - those rules fired for nothing), SHOWN in the rule
details, and ENFORCED for archive/restore triggers by resolving the
title from the card when the activity lacks it; legacy field-less
triggers keep matching; the filter popup prefills its current value.
- #2674: rules created via POST/PUT /api/boards/:boardId/rules never
fired - missing matching fields are now normalized to '*' wildcards
and trigger/action types validated; the addMember/removeMember rule
actions no longer crash on unresolvable usernames or member-less
cards and their writes are awaited. New docs/API/Rules.md (endpoints,
matching fields, wildcard semantics, the exact two-rule
add-on-move-to/remove-on-move-from workflow), linked from REST-API.md;
api.py help now lists the rule commands with the same example.
- #574: Sandstorm unique-username generation probes case-insensitively
(anchored, regex-escaped) and retries the next number when a
concurrent insert wins the unique-index race, instead of aborting the
login hook with E11000.
Tests: tests/rulesCardTitleFilter.test.cjs (12),
tests/rulesApiTriggerNormalize.test.cjs (19),
tests/sandstormUsername.test.cjs (11) - positive and negative cases.
Thanks to InfoSec812, sfahrenholz, mitar and xet7.
Inviting by email derives the new user's username from the email local
part; a collision with an existing user (cats@foo.com then
cats@facebook.com) made Accounts.createUser throw Meteor's raw
"403 Username already exists", shown to the inviter as just "403". The
username now probes cats, cats1, cats2, ... to the first free variant
(bounded), and exhaustion raises the translated error-username-taken.
Tests: tests/inviteeUsername.test.cjs (11, positive and negative,
including the literal reported scenario).
Closes#804: already fixed - every popup is viewport-clamped with
internal scrolling since ac368de06 (#5667 geometry rewrite: maxHeight
from live viewport space, bounded flex layout) plus the 04eb8a8fb
(#6221) global 90vh clamp; the member picker inherits the shell, so
neither standalone nor the Sandstorm iframe can grow past the viewport.
Thanks to lemoer, ocdtrekkie and xet7.
The due-date picker configures defaultTime 17:00 (received/start/end use
now()), but an inverted guard in datePickerRendered applied it only when
the card ALREADY had a date - the one case where it is unnecessary - so
for dateless cards the time input stayed empty and submit stored a
hard-coded 12:00. Empty pickers now pre-fill with the configured
default, submit falls back to it, and existing dates keep their own
time. The 2018 AM/PM display/parse mismatch was already fixed by the
native HTML date/time inputs (79b94824e); week start already honors the
user preference.
Tests: tests/datePickerDefaultTime.test.cjs (10, positive and negative).
Thanks to Vlasterx, saschafoerster, suncobran and xet7.
- #2769: refresh drag geometry when the DOM changes mid-drag - cards no
longer drop into the wrong swimlane while someone adds a card.
- #1992: board import restores all five card date fields (and creator)
with fallbacks; receivedAt/endAt were never imported at all.
- #2292: archiving a swimlane archives its cards (mirroring lists);
restore brings back exactly the cards archived with it.
- #2494: client noCache card lookup returned a Promise since the
Meteor 3 port - multi-select board moves wrote sort: NaN everywhere.
- #1853: subtask "View it" falls back to the subtask's boardId when the
deposit board is not loaded, instead of silently doing nothing.
- #1554: restore the reactive dependency lost in 7673c77c5 so cards
added after board render accept sidebar label/member drops.
- #2306: "filter to selection" and every bulk action are board-scoped;
foreign-board cards can no longer be silently archived.
- #1437: new POST /users/logout API endpoint revokes the presented
resume token, or all tokens with {"all": true}.
All with positive+negative regression tests (see tests/*.test.cjs).
Closes#1389: already fixed - the standalone pencil edit button
(0cc63b810) plus the wrapped viewer (bf2b3b40c) guarantee an edit target
even when a full-width link fills the description.
Thanks to hever, Cactusbone, Vermeille, Vanclief, Miffe, IcedQuinn,
ppouliot, GhassenRjab and xet7.
Since 8c3ce4bb7 (2021) the "close the previous inlined form" call was a
silent no-op: the inlinedForm escape action registers with
enabledOnClick: false and EscapeActions.clickExecute skips such actions,
so clicking another checklist item left both edit forms open - and the
submit handlers find the template's FIRST textarea, so the wrong form's
text was saved over the submitted item. Subtasks reproduced the full
bug; the checklists workaround corrupted the shared open-form tracker so
Escape fell through and closed the whole card details pane. A pure state
manager (client/lib/inlinedFormManager.js) now enforces the single-open
invariant: opening a form closes the tracked previous one (popups
untouched, preserving the 2021 intent); close/destroy only reset the
tracker for the form they own.
Tests: tests/inlinedFormSingleOpen.test.cjs (9, positive and negative,
including the exact reported A->B->C repro).
Thanks to Beebo89 and xet7.
Two defects: the tokenizer flipped into regex mode on every '/' even
inside quoted strings, so a quoted date like '06/04/2020' broke
tokenizing and the whole filter silently no-opped; and date custom
fields store Date objects while the comparison selectors used strings
and parseInt - equality and ranges could never match, != matched every
card. Dates now parse per the user's date format (day-first formats
respected) into half-open Date-range selectors; non-date fields keep the
legacy selectors unchanged.
Tests: tests/advancedFilterDate.test.cjs (14, positive and negative).
Closes#2521: already fixed since v3.88 (9819c9f80) - the checklist
completed/uncompleted activity text was missing due to an activityType
name mismatch between model and template; the current unified activity
template, i18n keys and helpers render it correctly.
Thanks to k1ng440, ott29 and xet7.
addSubtaskCard inserted every subtask card with the constant sort -1,
leaving the whole list tied: a drop between two equal sorts computes a
zero increment, the move modifier is empty and Card.move() silently
discards it - the card snapped back, and a multi-selection drop wrote
the SAME sort to every selected card, permanently destroying their
relative order (the data-loss risk in the issue triage). Subtask cards
now append with a unique sort (max+1), and the sortable stop handler
detects degenerate gaps (ties/inversions) and repairs the siblings'
sorts to a strictly increasing order before recomputing the drop index;
healthy lists take the unchanged fast path.
Tests: tests/subtaskCardReorder.test.cjs (14, positive and negative,
including the exact issue scenario and a guard that the old
{base:-1, increment:0} result can never be produced again).
Thanks to jayki and xet7.
- #3453: publish the FULL subtask ancestor chain, not only direct
parents - the cross-board full-path label vanished after refresh.
- #3748 (defect half): skip unresolvable custom-field definitions on
linked cards - phantom empty rows and a template TypeError on every
card details render. The mirroring itself is by design: linked cards
are pointers; label/custom-field ids are board-scoped; name-based
inheritance is the copy feature, not the link feature.
- #3199: group each archived card with its own Restore/Delete links and
add a clear separator below the group (RTL-safe, theme-neutral).
- #3843: comment-editor uploads and the attachments popup now share one
null-safe meta builder, and tests pin that comment attachments list in
the card's Attachments section.
Closes#3748: mirroring is by design (see above); the only actionable
defect is fixed in this commit.
Closes#3378: already fixed - notification_cleanup moved off SyncedCron
to a guarded Meteor.setInterval singleton, and quave:synced-cron 2.4.0
handles the E11000 history collision that percolate 1.5.x missed.
Closes#3276: already fixed by d3625db75 - the spent-time badge helpers
now read the live Cards document reactively, so the overtime color flips
immediately; verified behaviorally against the real component.
Closes#3843: behavior verified present and now regression-locked.
Thanks to MPeti1, peterbecich, fxkr, jghaanstra and xet7.
The maximized card pane had no position of its own, so it remained an
in-flow item inside the scrolled board canvas and rendered off-screen
after scrolling down; desktop-mode floating-window rules also overrode
every maximize geometry rule, and inline drag offsets survived
maximizing. The pane is now viewport-fixed with insets that win over
both (drag position restores on minimize), RTL-safe. Regression test
with a CSS cascade resolver that fails on the pre-fix stylesheet:
tests/maximizedCardPosition.test.cjs.
Closes#3566: mobile shows one list at a time by design (xet7's own
comments in the issue); Move Card popup is the supported flow, and the
2021-era touch drag glitches were since fixed with drag handles.
Closes#3252: already fixed - comment deletes are guarded in
client/components/activities/comments.js (tagged #3252), checklists in
checklists.js, attachments via #5282.
Closes#3542: no code bug - redirect_uri is the same expression on the
client and server (mismatches come from ROOT_URL/proxy configuration,
per the Bug:Environment-specific label), and the login-style half was
fixed via #5695.
Thanks to pravdomil, 1ubuntuuser, gastoncollaud and xet7.
- CHANGELOG.md: Upcoming entries for the LDAP group filter fix (#4036),
invitation code regeneration (#4043), CJK add-card footer wrapping
(#4023) and team-to-board member sync (#4593), whose code shipped in
the previous commit.
- server/models/users.js: do not delete the invitation code in
onCreateUser before the account insert is committed - a failed insert
(duplicate username/email) burned the code for every retry (#4043).
Consumption happens after a successful insert; re-invites regenerate
stale codes.
- tests/teamBoardMemberSync.test.cjs: the #4593 regression tests (13,
positive and negative) for the team board-member sync helpers.
Thanks to zeisss-mercedes, jkoenig134, yuki-snow1823, Sylvain2703,
szymonsztuka and xet7.
#5695#4897#4654#5174#4825#5659#4730#4419#4158#4560#4036#1959#1971.
- Startup schema upgrade (server/lib/schemaUpgradeSteps.js): on start,
check that data from EVERY old WeKan version is migrated to the newest
structure and migrate only what is missing - all platforms, MongoDB and
FerretDB. Version-gated (marker stores WeKan version + datetime of the
last re-check; one findOne per boot until a new release), non-blocking,
live dashboard at /schema-upgrade-status showing the product name, fast
on big databases (distinct() set-joins, updateMany batches). Steps:
archived-flag backfill, swimlane structure + visibility rescue
(#1959#1971: cards under deleted/archived/foreign swimlanes reassigned
to a visible swimlane; card-insert hook validates supplied swimlaneIds),
merge-per-swimlane-lists (v7.98/v8.0x list copies merged back into
shared lists: no duplicate columns in Lists view, everything visible in
Swimlanes view, cards keep their swimlane), embedded checklist items,
customFields boardIds, allows* backfills, members isActive, permission
lowercase, filesystem path healing.
- Migration speed (5-hour reports): insertMany batches in the text phase,
preloaded target-record maps in the file phases.
- Snap attachment-repair + FILES_ONLY importer mode (see prior commit
wiring) extended with CFS FileSystem-store coverage
(ATTACHMENTS_STORE_PATH era), missing-binary reporting, and
migrate-gridfs-to-fs.mjs bucket fixes.
- OAuth2/OIDC: OAUTH2_LOGIN_STYLE=redirect honored (#5695); concurrent
logins no longer contaminate each other's user data (#4897); account
merge no longer wipes avatars/templates (#4560).
- LDAP: background sync fixed (#4654), password login rejected after LDAP
migration (#4419), LDAP_ENCRYPTION=true actually encrypts (#4158),
group filter accepts multiple groups and admits admin-sync groups
(#4036).
- All Boards tiles: per-list card counts + member avatars restored
non-reactively (#5174#4825). Uniform default list widths (#5659).
Declined invitations no longer resurrect membership (#4730).
- Tests with negative cases for every fix (tests/*.test.cjs; suite green
except the pre-existing environment-only htmljs failure).
Closes#5600: OAUTH2_CA_CERT already works - the fetch migration
(49a272d6c) applies the CA to all OIDC calls via https.Agent.
Closes#3828: board members seeing all board cards is by design (no
per-card visibility model), and organizations/teams now exist.
Thanks to mueschel, ArturRuta, gerardo-junior, fabianrbz, Miuler,
butteredCat-2021, Griiimm, preciousamorc, farwayer, LeoLu-eng, hnb1,
rangersdo and xet7.
- Migration importers: resolve the CollectionFS GridFS id from
copies.<bucket>.key too (the REAL old-WeKan layout; only
original.gridFsFileId / gridFsFileId / copies.gridfs.key were tried,
none of which exist there) via a shared resolveCfsGridFsId() in
releases/migrate-mongodb-to-ferretdb.mjs,
snap-src/bin/migrate-mongo3-to-ferretdb.mjs and
snap-src/bin/migrate-gridfs-to-fs.mjs. The modern importer now drives
CollectionFS extraction from cfs_gridfs.<bucket>.files itself, so a
missing/empty cfs.<bucket>.filerecord no longer silently skips every
file; binaries without a filerecord are still extracted to disk, and
filerecords whose binary cannot be located are reported on the
dashboard instead of dropped.
- Meteor-Files phase: match versions.*.meta.gridFsFileId without a
storage flag (WeKan's own getFileStrategy rule), handle every version
(not just 'original'), and add a bucket-driven sweep of <bucket>.files
by metadata.fileId so binaries are recovered even when the record's
flags say nothing about GridFS. GridFS-flagged versions with no
locatable binary are reported.
- mongo3 importer: copy the CollectionFS top-level boardId/cardId/
listId/swimlaneId/userId into meta on the created records - an
attachment without meta.cardId shows on no card.
- Admin Panel > Attachments: the Filesystem Storage paths were computed
from process.env.WRITABLE_PATH IN THE BROWSER, so they always showed
"/data". New admin-only getAttachmentStoragePaths method returns the
real Snap-aware paths (shared models/lib/attachmentStoragePath.js),
also used for the settings document default (was /data/attachments,
actual Docker path is /data/files/attachments).
- Snap: `snap run wekan.database ferretdb` now says it does NOT migrate
data, says when nothing was switched, and points at
`snap run wekan.migrate` for re-running the full migration.
- Tests with negative cases: tests/migrationAttachmentExtraction.test.cjs
(behavioral resolveCfsGridFsId from all three importers + wiring
guards) and tests/attachmentStoragePath.test.cjs, wired into
test:unit:node.
- Companion fix in wekan/FerretDB: accept documents with literal dotted
field names like MongoDB 3.6+, so migrated documents containing them
are no longer silently dropped.
- CHANGELOG.md: new Upcoming section with these changes.
If attachments are already missing after an earlier migration, run
`snap run wekan.migrate` - the source MongoDB data was never modified,
and the fixed importers re-create the attachment records and files.
Thanks to mueschel and xet7.
The right-docked card details window exposed two real issues in the
Playwright run (03-cards-operations failing in all three browsers):
- Archiving or deleting a card left its id in the openCards session
list, so the floating details window kept rendering above the board —
and, docked to the right edge, exactly over the archives sidebar,
intercepting its Restore/Delete clicks. Both handlers now drop the
card from openCards and clear currentCard.
- An OPEN board sidebar now stacks above the card window (z-index 2002
vs 2001), so the sidebar is always usable while a card is open.
Also from the same run: the new one-click cog spec closed its popup with
a keyboard Escape that proved unreliable across engines — it now uses
the popup's own X button (.js-close-pop-over) like 19-accessibility
does. The chromium-only 33-board-domains failure is an unrelated
token-login race in the auth helper (userId mismatch), not caused by
these changes.
tests/uiDensity.test.cjs got guards for both follow-ups (openCards
cleanup in archive+delete, sidebar z-index above the card window).
Updated ChangeLog.
Thanks to xet7.
tests/ferretdbHasData.test.cjs runs the real snap-src/bin/ferretdb-has-data
against crafted directories. This guard gates two critical switches
(migration-control's finish_success and mongodb-control's #6458/#6466
failure handler), so its semantics must never drift. Positive: non-empty
.sqlite, no -wal sidecar required (clean shutdown checkpoints it away),
one good file among empties. Negative: 0-byte .sqlite from a failed
migration, empty/missing directories, -wal/-shm-only leftovers, and a
DIRECTORY named *.sqlite. All eight behaviors verified against the
actual script. Wired into test:unit:node; ChangeLog updated.
Thanks to xet7.
The 2026-07-16_18-33-28 test run failed one spec in all three browsers:
the #6465 one-click board-settings cog added a second .js-open-board-menu
element, so 03-cards-operations' bare class locator resolved to 2
elements and tripped Playwright's strict mode. The archives test now
scopes the click to the sidebar's instance (.board-sidebar
.js-open-board-menu), keeping its 'from inside the sidebar' intent.
Also added a spec for the new cog itself to 15-board-actions: visible in
#header-main-bar, popup not open before the click (negative), ONE click
opens the real boardMenu popup (.js-open-archives inside), Escape closes
it again.
Thanks to xet7.
tests/cpuExecWiring.test.cjs, with negative cases: every Linux bundle in
release-all.yml embeds cpu-exec + its own arch's static qemu-user (arm64
and the QEMU-built extra arches replace the inherited amd64 one, extra
arches tolerantly), the Windows/macOS bundles strip both Linux-only
files, qemu-user-static is installed in every bundle-building job and
both sandstorm jobs, the Sandstorm .spk ships cpu-exec + qemu via
build-deps.sh (tolerant when qemu is not installed), the Docker
entrypoint and the bundle launcher route ferretdb/node through cpu-exec
while keeping direct-exec fallbacks for bundles that lack it, and the
snap ships cpu-exec via the snap-src helpers part. Wired into
test:unit:node; complements tests/cpuExec.test.cjs (the script's own
behavior).
ChangeLog Upcoming: the regression-tests entry now lists the cpu-exec
wiring test, the subtasks default-board test (#6456) and the FerretDB
fork's integration-test fixes.
Thanks to xet7.
Fixes#6456.
The lazily-creating getters for the default subtasks helper board and
its landing list still used the sync Boards.insert / Swimlanes.insert /
Lists.insert / Boards.update APIs that Meteor 3 removed from the server,
so creating a subtask crashed with 'insert is not available on the
server. Please use insertAsync() instead' whenever the ^Board^ helper
board had to be created. The list creator additionally read
getDefaultSwimline()._id, which is a Promise on the Meteor 3 server.
- getDefaultSubtasksBoardId / getDefaultSubtasksListId are now PURE
getters (no creation), matching the #3868/#2256 rule that only the
server may create these.
- The server-side lazy creation moved into getDefaultSubtasksBoardAsync
and getDefaultSubtasksListAsync using insertAsync/updateAsync and
getDefaultSwimlineAsync — the exact path addSubtaskCard uses.
- The never-called date-settings twins had the same sync calls and no
server-only guard at all; they are pure getters now too.
- tests/subtasksDefaultBoard.test.cjs guards the async creators and that
no sync collection writes come back to models/boards.js, wired into
test:unit:node.
- Updated ChangeLog.
Thanks to sjohnen and xet7.
That commit's message listed all ten issues after a single Fix keyword,
but GitHub only auto-closes a reference that directly follows its own
closing keyword, so only the first issue was closed on push. Closing
the rest here; the fixes themselves are already on main.
Fixes#6455. Fixes#6458. Fixes#6466. Fixes#6467. Fixes#6468.
Fixes#6472. Fixes#6465. Fixes#5547. Fixes#5282.
Thanks to xet7.
Fix#6458 completely: the reporter's host runs inside a hypervisor that
masks AVX, and the old per-tool AVX wrappers (amd64-only, PATH-based)
were bypassed by absolute-path mongod invocations.
- New general snap-src/bin/cpu-exec: runs any binary directly when the
CPU has the features it declares (--features x86_64=avx,aarch64=atomics
or WEKAN_REQUIRED_CPU_FEATURES) and through same-arch qemu-user when
not; plain zero-overhead exec with no declared features. Lookup order:
WEKAN_QEMU_USER, qemu-<arch> next to the script (bundle/Docker),
$SNAP/qemu-<arch>, the snap's migratemongo/avx one, then system
qemu-user(-static). Clear error + direct run when no qemu exists, so
the real SIGILL surfaces instead of hiding.
- Snap: mongodb-control and migration-control run every mongod 7 through
cpu-exec, so MongoDB works without AVX (slower) and migration can READ
modern MongoDB data on such CPUs; completed-FerretDB switch and
migration fallbacks unchanged (switch_to_completed_ferretdb extracted).
- release-all.yml: every Linux bundle .zip ships cpu-exec + this-arch's
static qemu-user (amd64/arm64/ppc64le/s390x/riscv64; stripped from
win64/mac bundles), flowing into Docker and Snap automatically;
Sandstorm .spk gets both via build-deps.sh. Bundle launcher and Docker
entrypoint route node/ferretdb through cpu-exec.
- Docs: mongodb-avx-qemu.md and mongodb-raspi4-qemu.md document the
built-in mechanism.
Tests, with negative cases, for the newest features and fixes at
changelog: htmljs array-vs-attributes (#6459), description draft
normalize/reset (#6455), comment draft lifecycle (#5547), attachment
delete guard (#5282), rules move actions + import normalization (#6472),
snap mongod-failure/migration recovery (#6458#6466), FerretDB polling
defaults (#6467#6468), UI density guards for every #6465 fix, and a
behavioral cpu-exec test against fake cpuinfo + fake qemu. All wired
into test:unit:node. Go table tests for the FerretDB SQLite filter
pushdown are in the wekan/FerretDB repo.
Updated ChangeLog.
Thanks to xet7.
- #6459: card More menu rendered empty (no Delete): the vendored jade
compiler's htmljs patch made isConstructedObject(Array) false, so
'label {{_ ...}}:' lines got their content array mis-assigned as tag
attributes and Blaze threw 'foreign objects in attributes' killing the
whole cardMorePopup. Also fixed the list More link input (nonexistent
rootUrl helper -> absoluteUrl).
- #6455: saving a card description never removed a pre-existing unsaved-
edits record, and the draft/description comparison mismatched on Markdown
hard-breaks and empty descriptions, re-creating a phantom draft on every
save. Save now clears the draft; both sides normalized the same way.
- #5547: comment drafts: the draft-saving escape handler had been disarmed
since 2019 (flag with no setter), so a comment being written was lost
when the card closed. Drafts are now saved continuously (debounced),
flushed on teardown, cleared on submit; text no longer visually vanishes
on outside clicks.
- #5282: attachment delete: guard against the doc being already evicted
from Minimongo ('Removed nonexistent document'), like the existing
comment/checklist guards.
- #6472: rules move card to top/bottom: fall back to the card's current
list when listName does not resolve (was a swallowed TypeError = rule
silently did nothing); wizard stored listTitle which the engine never
reads (listName); empty destination wrote sort +-Infinity; rules JSON/CSV
import now uses the rules.createRule method instead of client inserts
rejected into minimongo limbo, and defaults missing trigger matching
fields to '*'; empty boardId no longer overrides the real board; the
server-side orphaned-cards fallback in List.cards()/cardsUnfiltered()
now actually resolves its async swimlane lookup.
- #6465: desktop density back to WeKan 6.09: base font 14px and headings
22/18/16px (vw clamps resolved to 18/24px on desktop), All Boards
wrapper padding 10px/15px, card details docks to the right edge instead
of floating on top of its own card (still draggable), 20px canvas
padding instead of ~48px white borders, removed the force-all-card-text-
to-title-size rule, admin org/team table Select/Unselect-all header
links are compact icons and header cells may wrap, zoom pill no longer
cut off, one-click board settings cog in the board header.
- #6458#6466: snap: mongodb-control now checks mongod's exit status and
does an AVX pre-flight (MongoDB 5+ needs AVX; without it mongod SIGILLs)
instead of pinging a dead port forever behind a 502: it switches to a
completed FerretDB migration, or (re)runs the migration (AVX-free) with
a 3-attempt guard. Migration importers no longer fail the whole run on
>=10 per-item errors ('Avatars Errors' etc.), and a failed 3.x-source
migration keeps the copied FerretDB data + checkpoint and resumes next
start instead of deleting it and 'falling back' to a mongod 7 that
cannot open 3.x data files.
- #6467#6468: with FerretDB, calm Meteor's no-oplog polling storm
(METEOR_POLLING_THROTTLE_MS=2000, METEOR_POLLING_INTERVAL_MS=30000
defaults in the snap and bundle launcher; overridable). The FerretDB
fork gets filter pushdown, a sane pool size and less locking in its own
repo.
- Updated ChangeLog.
Thanks to xet7.
Follow-up to PRs #6469 and #6470:
- The login-time org/team sync now clears the inherited method invocation
context (DDP._CurrentMethodInvocation.withValue) around a normal
Meteor.callAsync, instead of invoking the raw method handler with a
hand-made invocation object. This goes through Meteor's normal method
dispatch (check() argument audits, EJSON argument cloning) while still
being the server-to-server path (connection null) that the admin guard
in setUserOrgsTeamsFromLdap allows.
- Added the ddp package dependency to wekan-ldap for the DDP symbol.
- Fixed indentation in localizeAvatarFromBuffer and restored the comment
explaining the proceedAfterUpload argument.
- Removed a stray double blank line in the LDAP login handler.
- Updated ChangeLog about the merged LDAP fixes.
Thanks to xet7.
Added a build-sandstorm job that runs after the GitHub Release is created,
with the same steps as the standalone sandstorm.yml workflow (which still
exists for testing the .spk build on its own), attaching
wekan-<version>-sandstorm.spk to the Release. Non-blocking, so the
experimental Sandstorm build never fails or delays the release.
Thanks to xet7.
Both snap jobs (snap-native and snap-launchpad) pushed only to candidate, beta
and edge, and stable had to be released by hand afterwards. Publish to
stable,candidate,beta,edge instead. base: core24 allows stable and candidate.
Thanks to xet7.
A big migration can run for 5 hours, so being interrupted by a snap refresh,
`snap stop` or a reboot is normal - but it was treated as a failure, and every
interruption threw away all of the work.
Distinguish interrupted from failed. snapd's SIGTERM killed the importer, bash
resumed at the line after the importer call, read the signal exit code (143) as
"the migration failed", and fail_and_run_mongodb deleted the partial FerretDB
SQLite and set migrate=off. Now a SIGTERM/SIGINT trap (and an importer exit code
>= 128, since snapd signals every process in the service cgroup) keeps the
partial database and the checkpoint, leaves auto-migration on, and hands back to
mongodb-control so WeKan keeps serving from MongoDB until the next start resumes.
Both importers flush their checkpoint on the signal and exit 128+signo.
Checkpoint files, not just collections. Only fully-copied collections were
checkpointed; the file phase started over. Worse, the destination path was picked
with an "append _1, _2, ... while the file exists" loop, so every already-
extracted attachment was written a second time under a new name, orphaning the
first copy - a resumed migration silently needed twice the disk the up-front
space check budgeted for. Files are now checkpointed individually (recorded only
once the bytes are on disk AND the record points at them, and re-verified by size
on resume, so a half-written file is never mistaken for a finished one) and
skipped when resuming. The path is now deterministic: idHex is the record's _id,
so two records never collide and an existing file there can only be this record's
own partial extraction, which is meant to be overwritten.
Discard the checkpoint together with the SQLite it describes. The checkpoint
lives in $SNAP_COMMON, not in the SQLite dir that discard_partial_ferretdb wipes,
so it survived. The next migration then trusted it, skipped every "already
migrated" collection, copied only the rest into the now-empty database, and
reported success - leaving the snap serving a FerretDB missing most of its data.
The MongoDB 3 importer had no checkpoint at all and now resumes collections and
files the same way. The source MongoDB data is never touched on any of these
paths.
Thanks to markusst1982 and xet7.
Meteor's accounts-base "resume" login handler projects a single array element
with the positional operator:
Meteor.users.findOneAsync(
{ 'services.resume.loginTokens.hashedToken': hashedToken },
{ fields: { 'services.resume.loginTokens.$': 1 } })
FerretDB rejects that find with "Executor error during find command :: caused
by :: positional operator '.$' couldn't find a matching element in the array"
(code 51246).
Resume is how every already-logged-in browser re-authenticates on page load and
on every DDP reconnect, so the throw logged users out, the client retried the
connection, and the retry loop pinned the FerretDB CPU - the board looked broken
right after a migration that had just taken hours.
Replace the handler with one that projects the whole loginTokens array and picks
the matching token in JS. Upstream already does exactly this in its own $or
fallback query ("Cannot use ...loginTokens.$ positional operator with $or
query"), so the array is known to be small enough to fetch, and behaviour is
otherwise unchanged - including looking up the hashed token on its own first so
the unhashed token is not sent to the database unless it has to be.
Closes#6457
Thanks to markusst1982 and xet7.
9.93 is released, so move the two commits made after it into a fresh
"# Upcoming WeKan ® release" section above v9.93, organized like the previous releases:
Features: database setting authoritative + snap run wekan.database mongodb|ferretdb +
no-downtime handling of a failed migration.
Bugs: migration skips dotted (GridFS/CollectionFS/system) collections that FerretDB
rejects, and cleans up a partial SQLite from a failed migration.
No entry text changed; only relocated.
Problem: the control scripts force-switched to FerretDB whenever a *.sqlite file existed, so
`snap set wekan database=mongodb` did not stick and a failed migration caused downtime with no
way to keep WeKan running on MongoDB while fixing it.
- The `database` snap setting is now AUTHORITATIVE. Removed the "FerretDB SQLite present ->
force ferretdb / disable mongodb" overrides from wekan-control, ferretdb-control and
mongodb-control. wekan-control keeps only the safety guard (never start an EMPTY FerretDB
while MongoDB has data).
- New `snap run wekan.database mongodb|ferretdb` command (bin/wekan-database): switch which
database WeKan runs on. `mongodb` also pauses auto-migration (migrate=off) so it does not
switch back; `ferretdb` requires a non-empty SQLite first.
- Auto-migration can be paused: migration-pending now also returns "not pending" when
`snap get migrate` == off. A migration that FAILS now runs fail_and_run_mongodb: delete the
partial SQLite, set migrate=off, and exec mongodb-control so WeKan keeps working on MongoDB
(no downtime, no retry loop). fall_back_to_mongodb likewise sets migrate=off instead of the
done marker. `snap run wekan.migrate` re-enables migrate and retries.
- finish_success now restarts wekan.wekan so it reconnects to FerretDB after a successful
migration instead of waiting for the now-stopped MongoDB.
Registered the `database` app in snapcraft.yaml and snapcraft-core26.yaml (also added the
previously-missing migrate/maintenance apps to core26).
Thanks to xet7.
Two bugs surfaced when the MongoDB 7 -> FerretDB migration finally connected (after the
db-eval fix):
1. FerretDB v1 rejects collection names containing a dot:
invalid key: "attachments.chunks" (key must not contain '.' sign)
Both importers tried to copy GridFS internals (attachments.chunks/.files, avatars.*,
cfs_gridfs.*) and CollectionFS cfs.<bucket>.filerecord collections as TEXT, aborting the
migration. Now the text phase skips ANY dotted collection: none of WeKan's real data
collections contain a dot, and the dotted ones are GridFS internals (extracted in the file
phase), CollectionFS filerecords (now turned into bare attachments/avatars records in the
file phase via replaceOne upsert:true) or system.* collections.
2. The failed migration had already copied some text collections, leaving a NON-EMPTY but
incomplete files/db/wekan.sqlite. ferretdb-has-data then saw it as "migrated", so
mongodb-control disabled MongoDB and the snap tried to serve a half-migrated FerretDB. On
any failure migration-control now runs discard_partial_ferretdb: stop the temp target
FerretDB and delete the partial SQLite, so the snap cleanly keeps MongoDB and retries. The
finish_success switch is also gated on it (`rc -eq 0 && finish_success`).
Source MongoDB data is never modified or deleted.
Thanks to xet7.
9.92 is released, so move everything committed after it into a fresh
"# Upcoming WeKan ® release" section above v9.92, organized by topic like the
previous releases (features, then bugs):
Features: wekan.migrate command; maintenance mode + maintenance page.
Bugs: db-eval driver resolution (ESM ignores NODE_PATH); decide FerretDB vs
MongoDB from a non-empty SQLite database (not the marker); mongod-7
readiness probe fast-fail.
v9.92 is restored to what it shipped, including its "migration-success marker is
now authoritative" bug entry (which the Upcoming "presence of a FerretDB SQLite
database" entry supersedes). No entry text changed; only relocated.
ready_via_node waits up to 45s for the temporary source mongod to accept connections, to
allow a large MongoDB 6/7 database time to replay its journal (#6454). Add a liveness check:
if the mongod process (SRC_PIDFILE) has already exited - e.g. mongod 7 cannot open old
MongoDB 3.x data - stop waiting immediately and drop to the MongoDB 3.2 reader, instead of
pinging a dead process for the full timeout. Matters across many unattended 6.09 -> 9.x
upgrades. (With --fork, start_mongod usually already returns non-zero when mongod cannot open
the data; this also covers a mongod that started then died mid-probe.)
Thanks to xet7.
For unattended upgrades at scale (6.09/MongoDB 3.2 -> 9.x/FerretDB across many servers), the
"did the migration succeed?" test must not be fooled by a 0-byte stub file.
- New shared helper snap-src/bin/ferretdb-has-data: exit 0 only if a *.sqlite file bigger than
0 bytes exists in files/db. Used by mongodb-control, ferretdb-control and wekan-control in
place of the previous "*.sqlite exists" glob, so the DB decision is based on a real,
non-empty FerretDB database. (Deliberately does NOT require the -wal file, which SQLite may
checkpoint away when FerretDB is stopped - that would cause false negatives at startup.)
- migration-control finish_success now verifies the non-empty SQLite database exists (and logs
the WAL/shm sidecars) BEFORE switching the snap to FerretDB. If the importer returns success
but wrote no data, it does not switch or set the done marker - MongoDB data is kept and the
migration retries on the next start.
Thanks to xet7.
db-eval is the Node helper the snap uses instead of mongosh to check whether MongoDB/FerretDB
is up, elect the replica-set primary, and probe mongod-7 readiness during migration. It did
`import { MongoClient } from 'mongodb'` while the wrapper set NODE_PATH to the bundled driver
-- but Node's ESM loader IGNORES NODE_PATH (only the CommonJS loader honors it). So the import
resolved nothing and db-eval exited BEFORE opening any connection, making every
ping/primary/rs-* check fail.
This single bug explains the whole family of failures seen this week:
- WeKan looping "MongoDB not ready yet, retrying..." / "FerretDB not ready yet..." forever
even though the database was running and listening;
- replica-set initialisation failing;
- the MongoDB -> FerretDB migration falling back to MongoDB 3.2 because the mongod-7
readiness probe never connected (mongod 7 opened the data fine, but db-eval couldn't reach
it), and 3.2 cannot read WiredTiger-7 files -> no FerretDB SQLite produced.
Proof from the migration source mongod log: mongod 7 reached "Waiting for connections" with
ZERO "Connection accepted" before the readiness timeout killed it; the only connection in the
file was the native `mongo` shell to mongod 3.2.
Fix: resolve the driver with createRequire (CommonJS resolution honors NODE_PATH and the
bundle layout), anchored inside the modern bundle (npm-mongo v6 first). Applied the same fix
to the standalone migrate-schema-v843.mjs and migrate-gridfs-to-fs.mjs tools.
Thanks to xet7.
wekan.migrate (bin/wekan-force-migrate): force a fresh MongoDB -> FerretDB migration,
ignoring the database setting and the migration marker. Removes any partial FerretDB SQLite +
resume checkpoint, sets database=mongodb, and starts wekan.mongodb so migration-control
re-reads the existing MongoDB (3 or 7) data and migrates again. Source MongoDB data untouched.
wekan.maintenance on|off (bin/wekan-maintenance): toggles a $SNAP_COMMON/.wekan-maintenance
marker. While present, mongodb-control / ferretdb-control do NOT auto-migrate or auto-disable,
so `snap start wekan.mongodb` or `snap start wekan.ferretdb` stays up and you can reach the
data over the MongoDB wire protocol on port 27019 (one at a time - they share the port).
Maintenance page (bin/wekan-maintenance-page.mjs): in maintenance mode wekan-control execs
this instead of WeKan; it serves an "under maintenance" page (HTTP 503) on the web port for
ALL URLs. It shows the Admin Panel product name if set (not "WeKan"), read from
$SNAP_COMMON/.productname.txt, which is cached while a database is running by wekan-control
(new db-eval `product-name` command) and by both migration importers - so it is available even
when both databases are stopped.
Registered `migrate` and `maintenance` apps in snapcraft.yaml.
Thanks to xet7.
Regression fix. An earlier change made mongodb-control/ferretdb-control/wekan-control key
the "use FerretDB / disable MongoDB" decision off the .migration-to-ferretdb-done marker.
But migration-control ALSO writes that marker when the migration falls back (data
unreadable, tools missing) or when there is nothing to migrate - producing NO FerretDB
data. So on a server whose MongoDB 7 -> FerretDB migration failed:
- mongodb-control saw the marker -> "migration already finished; disabling mongodb
service" -> disabled MongoDB;
- FerretDB SQLite (files/db) was EMPTY;
- database=mongodb, so wekan-control waited on MongoDB -> "MongoDB not ready yet,
retrying in 5 seconds..." forever, and wekan.mongodb refused to stay up.
Now all three scripts decide from ACTUAL DATA ON DISK: MongoDB is disabled / FerretDB is
forced ONLY when a *.sqlite database exists in files/db. An empty files/db means the
migration did not succeed, so mongod starts normally and WeKan keeps working (and the
migration can be retried). The marker still prevents re-running a finished migration.
Thanks to xet7.
Upcoming section: document that statfs is in snapd's default seccomp allow-list, so the
MongoDB -> FerretDB migration's free-space guard is active on Snap; quotactl is not used and
its absence is covered by the ENOSPC/EDQUOT write-failure fallback.
Document that statfs (and statfs64/fstatfs/fstatfs64/statvfs/fstatvfs) are in snapd's
default seccomp allow-list (snapcore/snapd interfaces/seccomp/template.go), so free-space
checks WORK inside a snap (which also has no per-snap disk quota by default, so statfs shows
real usable space). quotactl is NOT in the allow-list, but it is never called; a snapd
storage-quota group is still caught by the ENOSPC/EDQUOT write-failure path. Only Sandstorm
grains genuinely lack statfs (FUSE unimplemented, quotactl blocked). No behaviour change.
migrate-mongodb-to-ferretdb.mjs (the modern MongoDB 6/7 driver importer that
migration-control runs, packaged from releases/):
- Resolve the mongodb driver by probing the WeKan bundle with createRequire anchors and
USE WHAT WORKS: prefer a v6+ driver that exposes GridFSBucket and speaks OP_MSG, since
an older bundled v2 driver's OP_QUERY is rejected by FerretDB ("Unsupported OP_QUERY
command: update"). A plain `import from 'mongodb'` could pick up that v2 driver. Fail
fast with a clear message if no usable driver is found.
- Bring it to parity with migrate-mongo3-to-ferretdb.mjs's data-safety and UX: measure
total attachment/avatar bytes up front and STOP before extracting if they will not fit;
if space runs out mid-run, stop + delete the partially migrated files + report how much
more disk is needed (a full disk can corrupt the still-running source MongoDB);
detect ENOSPC/EDQUOT where free space is not measurable (Sandstorm); show a live
per-file progress bar; and translate the dashboard into the browser's language.
Existing checkpoint/resume, schema upgrade, orphan fixing, compaction and index
creation are unchanged.
wekan-control:
- On startup, decide the database from what data actually exists on disk (FerretDB SQLite
at $SNAP_COMMON/files/db, MongoDB WiredTiger at $SNAP_COMMON), not the `database` setting
alone. If ferretdb is selected but its SQLite is empty while MongoDB still has data,
revert to database=mongodb instead of starting an EMPTY FerretDB that would look like all
data was lost, and tell the user to run the migration first.
Thanks to xet7.
Symptom: WeKan never started, looping "MongoDB not ready yet, retrying in 5 seconds..."
forever, even though mongod appeared to be running.
Cause: mongodb-control started mongod with a HARDCODED --wiredTigerCacheSizeGB 32. On any
server with less than ~34 GB RAM, mongod was OOM-killed seconds after startup. That
included the temporary mongod started to initialise the replica set: it died before
db-eval could reach it (log showed "child process started successfully" then 30 s of
failed pings), so the replica set was never initiated. The final mongod then ran with
--replSet rs0 but no config, no PRIMARY was elected, and wekan-control (which needs the
replica-set primary for Meteor change streams) waited forever.
Fixes in mongodb-control:
- WiredTiger cache is now RAM-aware: ~50% of (MemTotal - 1 GB), the same formula mongod
uses for its own default, capped at 32 GB, min 1 GB. Override with
MONGODB_WIREDTIGER_CACHE_GB. Applied to both the temp replica-set-check mongod and the
final mongod.
- Temporary-mongod readiness wait raised 30 s -> 90 s so a large database that needs
longer to replay its journal still gets its replica set initialised/reconfigured.
Fix in migration-control:
- The temporary source mongod started to READ the data during a MongoDB -> FerretDB
migration now uses a conservative RAM-aware cache (1/4 of (MemTotal - 1 GB), cap 8 GB,
override MIGRATION_MONGOD_CACHE_GB), leaving room for the target FerretDB and the Node
importer that run alongside it. Applied only to the mongod 7 (WiredTiger-only) read so
the flag can never clash with a legacy MMAPv1 MongoDB 3.x dbpath.
Thanks to xet7.
Symptom: on an already-migrated install, `sudo snap start --enable wekan.ferretdb`
started FerretDB and then it immediately stopped itself, logging:
"database is 'mongodb', not 'ferretdb'. Disabling ferretdb service."
Cause: every DB service keyed its behaviour off the `database` snap setting alone. If
that setting was never flipped to ferretdb (e.g. the migration's INTERNAL `snapctl set`
did not re-run the configure hook), ferretdb-control read `database=mongodb` and disabled
itself, so FerretDB could not be started by hand without first running
`snap set wekan database=ferretdb`.
Fix: the marker $SNAP_COMMON/.migration-to-ferretdb-done is now authoritative.
- ferretdb-control: if the marker exists, repair database=ferretdb and keep running
instead of self-disabling.
- mongodb-control: if the marker exists, repair database=ferretdb and disable mongodb,
BEFORE the migration-pending check so a finished migration is never re-attempted.
- wekan-control: already forces ferretdb on the marker (previous commit).
Immediate recovery on an already-migrated install running an older release:
sudo snap set wekan database=ferretdb
Thanks to xet7.
wekan-control (the wekan.wekan service) only WAITED for the database DATABASE points at
and never started a DB service, so if both wekan.ferretdb and wekan.mongodb were
stopped/disabled WeKan hung on "MongoDB not ready yet, retrying..." forever.
Now, on start, wekan-control:
1. If the migration-success marker $SNAP_COMMON/.migration-to-ferretdb-done exists,
forces database=ferretdb, then enables + starts wekan.ferretdb and stops + disables
wekan.mongodb. This self-heals the case where the migration's INTERNAL `snapctl set`
never re-ran the configure hook that flips the services.
2. Before waiting, enables + starts whichever DB service is configured
(snapctl start --enable = snap enable + snap start), so a merely-stopped DB is
brought back up instead of WeKan waiting forever.
Uses the snap instance name so parallel installs target their own services.
Thanks to xet7.
v9.91 was cut as a release, so move the Snap FerretDB service-flip fix out of it into a
fresh "# Upcoming WeKan ® release" section above v9.91. No entry text changed; only
relocated.
After a MongoDB -> FerretDB migration, WeKan hung on "MongoDB not ready yet, retrying..."
because no database was running: wekan.ferretdb stayed disabled and wekan.mongodb stayed
enabled-but-stopped.
Root cause: the wekan.ferretdb service disables itself while database=mongodb, and the
service flip that re-enables it lives in snap/hooks/configure. migration-control switches
the backend with an INTERNAL `snapctl set database=ferretdb`, but an internal snapctl set
does not re-run the configure hook, so the flip never happened.
Fix: finish_success now does the flip itself (mirroring the configure hook's ferretdb
branch): enable+start+restart wekan.ferretdb, then stop+disable wekan.mongodb, using the
snap instance name so parallel installs target their own services. FerretDB is started
before MongoDB is disabled so a DB is up first.
Immediate recovery on an already-migrated install:
sudo snap start --enable wekan.ferretdb && sudo snap stop --disable wekan.mongodb
v9.90 (2026-07-15) was already cut, but later work had been appended to it. Create a new
"# Upcoming WeKan ® release" section above v9.90 and move into it everything committed
after the v9.90 cut:
- new features: Snap/Sandstorm migration dashboard (per-file progress + disk guard)
- updates: i18n 10 more languages, native-name fixes, pull English-regression report
- tests: dev server also frees the MongoDB port
- bugs: MongoDB 3->FerretDB EJSON + stay-on-page; "Always show all code as plain text"
reactivity race; i18n browser-language -> file mapping fixes
v9.90 is restored to its cut contents (MimeBleed CRITICAL + the original features/updates/
tests/bugs). No entry text changed; only relocated.
Document the Upcoming i18n work: the ja_JP->ja clobber fix, case/underscore-insensitive
language matching, Chinese subtag fallback + zh alias, the cmn tag typo fix (bugs), and
the 10 new selectable languages, native-name corrections, and the pull-translations
English-regression report (updates).
Add languages.js entries for translation files that had none (so they were pulled from
Transifex but never selectable): ca-valencia (Català (Valencià)), en_ID/en_SG/en_TR
(English (Indonesia/Singapore/Turkey)), es_CO (Español (Colombia)), fr-FR (Français
(France)), pt_PT (Português (Portugal)), ru-UA (Русский (Украина)), tk_TM (Türkmençe
(Türkmenistan)), wuu-Hans (吴语(简体)). Skipped km_KH and ru_RU (duplicate the existing
km-KH / ru-RU entries) and zh (already aliased to zh-Hans).
Fix language names that used the English name instead of the native one:
ace "Acehnese" -> "Bahsa Acèh"
cy "Welsh" -> "Cymraeg"
cy-GB"Welsh (UK)" -> "Cymraeg (Y Deyrnas Unedig)"
af_ZA"Afrikaans (South Africa)" -> "Afrikaans (Suid-Afrika)"
Fix several browser-language -> translation-file mapping gaps:
- isLanguageSupported is now case-insensitive (requested) via a new TAPi18n.resolveTag()
that maps any input to the canonical languages.js tag case-insensitively AND
underscore<->hyphen-insensitively (so 'zh-hant'->'zh-Hant', 'JA-JP'->'ja-JP', browser
'af-ZA'->legacy key 'af_ZA'). loadLanguage/setLanguage/ensureLanguageLoaded resolve
through it, so the loaded file, the i18next code and the stored current tag all agree.
- Bare base codes with no plain-code file resolve via an alias map: 'zh' -> 'zh-Hans'
(Chinese has only script/region files), so plain 'zh' gives Simplified, not English.
- Browser detection (client/lib/i18n.js) now strips trailing subtags progressively
instead of jumping to the first segment: 'zh-Hans-CN' -> 'zh-Hans', 'zh-Hant-TW' ->
'zh-Hant' (Chrome sends these), rather than -> 'zh' -> English.
- languages.js: fix the 'cmn' entry whose tag was the typo 'cnm' (and code 'cn'), which
mismatched supportedLngs and broke selecting Mandarin; now tag/code = 'cmn'.
Adds i18n.test.js coverage for resolveTag, case-insensitive isLanguageSupported, the zh
alias, the underscore/hyphen match, and each Japanese variant loading its own file.
Two Transifex languages were writing to the same local file: .tx/config lang_map had
`ja_JP: ja`, which sent Transifex's ja_JP INTO imports/i18n/data/ja.i18n.json — the same
file the real Japanese (`ja`) uses. On `tx pull -a -f`, the emptier/untranslated ja_JP
overwrote ja.i18n.json, reverting Japanese strings to the English source. Map it to its
own file instead: `ja_JP: ja-JP` (-> ja-JP.i18n.json), matching languages.js which already
has three separate Japanese entries (ja -> ja.i18n.json, ja-JP -> ja-JP.i18n.json,
ja-Hira -> ja-HI.i18n.json). No other lang_map target collides. languages.js already loads
each to its own file, so no change needed there.
Also add releases/translations/report-english-regressions.mjs and call it from
pull-translations.sh: after pulling, it lists the language files where a previously
translated string reverted to exactly the English source (i.e. untranslated on Transifex),
so the regression is visible instead of being committed silently.
The MongoDB 3 -> FerretDB migration work had grown into one large bug bullet that
actually contained major features (live per-file progress bar, chunk-by-chunk streaming,
disk-space safety guard + rollback, Snap/Sandstorm/availability detection). Move those to
the "new features" section as their own bullet, and keep the EJSON-resolution fix +
stay-on-page hand-off as the bug entry (with a pointer to the feature bullet).
Previously the statfs free-space guards were gated on IS_SNAP. But a Snap container may
also be unable to report free space (statfs fails). Drive the behaviour off whether free
space is actually MEASURABLE instead:
- updateDiskFree: skip statfs only on Sandstorm (where it is known-unreliable — reports the
host disk, not the grain quota); everywhere else ATTEMPT statfs, and on failure leave
state.diskFree < 0 (unknown).
- ensureDiskForFile / the up-front total-size pre-check: run only when state.diskFree >= 0
(measurable); otherwise skip and rely on an ENOSPC/EDQUOT write failure.
- Dashboard already hides the free-space figure and switches to "free space required for
all files" whenever diskFree < 0, so a Snap-with-no-statfs degrades exactly like
Sandstorm: try, and stop + delete + report on a write failure.
IS_SNAP is now only used for the platform log line. CHANGELOG updated.
The statfs free-space checks only make sense on the Snap (a real filesystem whose size we
can read). A Sandstorm grain cannot see its free space or quota — its FUSE layer does not
implement statfs and quotactl is blocked (confirmed in the Sandstorm source), and statfs
on the writable /var reports the host disk, not the grain quota. So:
- Detect Snap via $SNAP (IS_SNAP) and Sandstorm via SANDSTORM / SANDSTORM_RAW_MONGO_PATH /
METEOR_SETTINGS.public.sandstorm (IS_SANDSTORM).
- Run the statfs up-front + mid-run free-space guards ONLY on Snap.
- On Sandstorm (and anything else), just try to migrate and detect "out of space" from an
actual ENOSPC/EDQUOT write failure (in the streaming writer and in the file-loop catches),
which triggers the same stop + delete-migrated-files + report rollback (onDiskAbort).
- Dashboard shows the free-space figure only when known (Snap); on Sandstorm it says free
space is unknown and phrases the requirement as "free space required for all files".
Sandstorm source used only for research (grain cannot query disk/quota); not committed.
CHANGELOG updated.
Add the missing Upcoming bug entry for the alwaysShowCodeAsText fix (the viewer read a
stale ReactiveVar and never re-rendered, so the toggle did nothing; now set inside the
mentions helper from the same reactive setting it already reads). The MongoDB 3 ->
FerretDB migration progress/disk-guard improvements were already recorded in the mongo3
migration entry.
Migration dashboard + migrator improvements for the MongoDB 3 -> FerretDB path:
- Live per-file progress: the file currently being extracted shows its name, size,
"file N of TOTAL", a percent progress bar, and whether it is an attachment or an
avatar. File-type words use WeKan's existing translations in the viewer's browser
language (Accept-Language), read best-effort from the bundle with an English fallback.
- RAM-safe streaming: each GridFS file is streamed chunk-by-chunk (sorted by n) straight
to disk via a spawned mongoexport + readline, with natural back-pressure, so a multi-GB
attachment never lands in memory. The old approach buffered the whole file through
mongoexport's 512 MB stdout maxBuffer and failed on big files.
- Disk-space guard (a full disk can corrupt the still-running source MongoDB): the
dashboard shows remaining free space; the migrator measures the total size of all
attachments+avatars up front and stops immediately if the volume cannot hold them; it
also stops mid-run if free space drops below a safety margin (default 1 GB,
MIGRATION_MIN_FREE_BYTES). On such a stop it deletes the partially-migrated files to
free the space back, reports how many files were migrated before stopping, and shows how
much more disk space is required to migrate all files.
Cannot run the snap here, so this is reviewed but not executed; needs a snap migration run
to verify. CHANGELOG updated.
The MongoDB 3 migrator resolves WeKan's modern bson (with EJSON) and mongodb v6 by
anchoring require() inside the Meteor bundle. It anchored the sub-paths relative to the
script, but in the snap the script runs from $SNAP/bin/ while the bundle is at
$SNAP/programs/server/... (one level up) — so every anchor resolved
$SNAP/bin/programs/server/... (nonexistent) and fell through to the ancient meteor-spk
base bson 1.x with no EJSON, giving "FATAL: EJSON.parse unavailable" and aborting the
migration.
Build candidate bundle roots from $SNAP (env) and the script's parent dirs, then try the
known modern-bundle sub-paths under each (npm-mongo's nested v6 driver first, so the
ancient v2 that FerretDB rejects with OP_QUERY is never chosen). Node's module resolution
walks up from the '_.cjs' anchor to programs/server/npm/node_modules/bson.
Also: on successful hand-off the dashboard now reloads the SAME page the user was on
(the board URL) instead of forcing All Boards, so a reload during migration keeps you in
place. The dashboard already serves the progress page for every URL. CHANGELOG updated.
The viewer (mentions -> markdown) applies alwaysShowCodeAsText via a ReactiveVar
(Markdown.alwaysShowCodeAsText) that only a separate startup Tracker.autorun kept in
sync with the setting. But the mentions helper re-renders whenever the settings doc
changes (it reads getCurrentSetting for renderLinksAsPlainText/stripLinks), and that
re-render often runs BEFORE the bridge autorun updated the ReactiveVar — so the inner
markdown helper read the stale flag and rendered links/markdown normally (clickable),
and since mentions does not depend on that ReactiveVar it never re-rendered again.
Net effect: toggling "Always show all code as plain text" did nothing (links stayed
clickable, code stayed rendered), even after reload (same race on load).
Fix: set Markdown.alwaysShowCodeAsText inside the mentions helper, from the same
reactive getCurrentSetting() it already reads, BEFORE rendering the inner markdown
via Blaze.toHTML. The markdown helper now always reads the up-to-date flag in the same
synchronous render, so the toggle takes effect immediately in every rich-text field
(card titles, descriptions, comments, checklists). The startup autorun stays for any
direct +markdown use outside a viewer.
kill_meteor_on_port freed the app port (3000) and rspack dev-server port (8080) but not
Meteor's bundled MongoDB on app-port+1 (3001). When the meteor parent is SIGKILLed its
mongo child is often orphaned and keeps holding 3001, so the next `meteor run` failed
with "Unexpected mongo exit code 48 ... port was closed, or was already taken". Also
free app-port+1 (which additionally clears a leftover standalone test mongod on :3001),
include it in the readiness guard/wait, and in the SIGKILL escalation. CHANGELOG noted.
Bring the Windows batch runner to parity with rebuild-wekan.sh: the :3000 server for
Node E2E + Playwright now runs from the precompiled .build\bundle instead of
`meteor run`, so no recompile. New :start_bundle_server subroutine (used by both the
parallel and sequential "Run ALL tests" flows):
- Builds .build\bundle once if missing (meteor build .build --directory).
- Resolves Meteor's bundled node via `meteor node -e process.execPath` and the sibling
dev_bundle mongod; runs the bundle with that node (native-module ABI match).
- Installs the bundle's programs\server npm deps once.
- Starts Meteor's mongod on :3001 (db "meteor", matching what the tests seed), or reuses
a MongoDB already listening there; stops it on exit only if this run started it.
- Starts `node .build\bundle\main.js` with MONGO_URL/ROOT_URL/PORT/WRITABLE_PATH set in
the parent scope (child inherits them) then unset so Mocha/E2E do not inherit MONGO_URL.
Mocha still uses `meteor test` (:3100, its own build) — it cannot run from a production
bundle. Intro text and cleanup updated; CHANGELOG notes the parity. Untested on Windows
here (no cmd available); needs one Windows verification run.
Group the Upcoming section under the standard headers: CRITICAL SECURITY ISSUE
(MimeBleed), new features (import/export privacy, always-show-code, render-links),
updates (webhook card description, Sandstorm docs), tests (yargs mocha crash, live
progress, .build/bundle reuse), bugs (Sandstorm migrated-grain Admin Panel, Version
storage engine). Also fold the bundle-server "meteor" vs "wekan" database fix into
the .build/bundle test entry.
The bundle-based :3000 server pointed at mongodb://127.0.0.1:3001/wekan, but the
Playwright and Node E2E tests seed their users/boards straight into
mongodb://127.0.0.1:3001/meteor (tests/playwright/helpers/db.js,
tests/e2e/list-regressions.js) — the DB name Meteor's built-in mongo used under the
old `meteor run`. Same mongod, different database: the app read an empty DB while the
tests seeded another, so every test needing seeded data failed (only the ~9 per browser
that need no seed passed). Point the bundle server at /meteor so app and tests share
one database again.
Complete the Upcoming test-infrastructure notes: the readiness poll that hung on
Meteor's :3000 proxy (progress froze at [0s]) is now fixed with a curl timeout and a
wall-clock-bounded wait, and "Run ALL tests" checks for .build/bundle specifically and
builds it once if missing. Also note the .sh (bundle) vs .bat (meteor run) split in the
server-start wait item.
Before running ALL tests, check for the .build/bundle directory specifically (the
:3000 test server runs it). If it is missing (never built, or a previous build left
.build without a bundle), build it with `meteor build .build --directory` first.
Previously it only checked the .build parent, so a missing/partial bundle was not
rebuilt here (only caught later by the main.js check).
"Run ALL tests" started the :3000 server with `meteor run`, which recompiles into
.meteor/local even after you already built .build/bundle. Node E2E + Playwright only
need a live WeKan over HTTP, so start that server from the precompiled bundle instead:
- `node .build/bundle/main.js` with Meteor's bundled node (matching native-module ABI),
MONGO_URL to Meteor's bundled mongod on :3001 (or reuse a MongoDB already listening
there). Boots in seconds, no recompile. One-time `programs/server` npm install runs
if missing. mongod is stopped on exit only if this run started it.
- The server boot log and the npm install now STREAM LIVE (scrolling: tail -n 0 -f /
tee) so you see exactly what is happening instead of a single frozen summary line.
- Sequential mode runs suites strictly one at a time, each streaming its own output;
the parallel-only combined refreshing table is now skipped in sequential mode.
Mocha still uses `meteor test` (its own .meteor/local-test build) — its in-process
server-side tests cannot run from a production bundle, so that one build is unavoidable
and copying the bundle would not help it. The script warns the bundle is run as-is, so
rebuild it after source changes. CHANGELOG updated.
In sequential mode only one suite runs at a time, so there is no reason to hide its
output in a log and show only a counter. Refactor the job runner: extract run_job_body
(the actual suite command) and, in sequential mode, pipe it through tee straight to the
console — you now see every test tick by one-by-one as the reporter prints it, then a
final PASS/FAIL + count line. Parallel mode still runs jobs in the background to logs
(the combined table reads them). tee is on the right of the pipe so the suite's exit
code is captured in STATDIR, not tee's.
The "Building WeKan" message during Run ALL tests suggested a fresh full build even
though the user had already built WeKan. Reword it: the tests start a live
`meteor run --port 3000`, and Meteor recompiles into .meteor/local on start only for
inputs that changed since the last build (sources / node_modules), then caches it.
No behavior change, message only.
The "Run ALL tests" server-readiness wait used `curl http://127.0.0.1:3000/sign-in`
with no timeout. Meteor binds the :3000 proxy early and accepts the TCP connection
while the app is still building, but does not send an HTTP response until the build
finishes. So curl blocked on that first connection for the whole build: the loop
never advanced and the new progress line froze at [0s] with only the log header,
even though the build was progressing in the log.
Give curl --connect-timeout 2 --max-time 4 so each poll returns quickly (not-ready
during the build, ready once /sign-in actually answers). The elapsed seconds and the
newest build step now refresh every few seconds as intended. Bound the wait by
wall-clock time (.sh: 1200s; .bat: ~240 polls) instead of a fixed count, since polls
now take a few seconds each. Also add the timeout to the Playwright-ALL precheck.
Audited both runner scripts and closed every place that used to sit silent for
minutes during test runs:
- Server first-build wait (Run ALL tests, both modes): .sh shows elapsed seconds +
newest build step from the server log in place; .bat shows elapsed seconds every
~10s and points at the live build log (echoing arbitrary build lines is unsafe on
cmd because they contain > < | &).
- Sequential per-job (option 2): .sh prints a live elapsed + pass/fail counter per
job and a final PASS/FAIL line; .bat now runs each job in its own minimized window
(reusing the proven parallel start-commands + .done-<key> flags) and polls a live
pass counter one job at a time, instead of redirecting silently to a log.
- Playwright ALL browsers single menu item (.sh option 10 / run_playwright_parallel):
streams each browser's list reporter live via tee while still saving the log;
PIPESTATUS[0] keeps per-browser pass/fail accurate through the pipe.
Single-suite items (Mocha, import, E2E, single Playwright) and the parallel combined
progress table already showed progress and are unchanged. CHANGELOG updated.
Record the Upcoming test-infrastructure change: "Run ALL tests" now shows elapsed
time + newest build step during the server build, and a live per-job pass/fail
counter in sequential mode, instead of sitting silent for minutes.
Running ALL tests (menu options 1/2) was silent for minutes in two places:
- While the :3000 WeKan server did its first rspack build, the readiness wait only
printed dots. Replace that with an in-place line showing elapsed seconds and the
newest build step from the server log (=> Compiled Rspack..., => Started MongoDB,
=> Started your app), so the build is visibly progressing.
- In sequential mode each job blocks until it finishes, so nothing showed while a
job built and ran. Add a live in-place counter per job (elapsed seconds + tests
passed/failed, from count_pass/count_fail on the job log) and a final PASS/FAIL
line when it completes.
The parallel-mode combined progress table is unchanged.
Update the "Test infrastructure" Upcoming entry with the confirmed root cause and
resolution: the second-stage boot crash was node_modules/yargs-parser/build/lib/index.js
using the guarded ternary `const require = createRequire ? … : undefined`, fixed by
generalizing the createRequire removal to a line-wise match (run before import.meta.url
expansion) and broadening the scan from node_modules/yargs to yargs and its ESM deps.
Also note the three server test files that were present but missing from the curated
server/lib/tests/index.js loader and are now registered. Verified: 450 passing, 0 failing
(411 before the three files were wired in).
The Mocha (server-side) test bundle crashed at boot with 0 tests run because
yargs and its ESM dependencies are dragged into the Meteor test server bundle,
where Meteor 3 wraps every module in a CommonJS function and their native-ESM
constructs (import.meta, `const __dirname`, `const require = createRequire(...)`)
are illegal and crash the bundle before any test runs.
Two gaps remained in scripts/patch-yargs-dirname.cjs:
- The `const require = createRequire(...)` removal (and its trigger) only matched
the direct-call form. yargs-parser/build/lib/index.js uses the guarded ternary
`const require = createRequire ? createRequire(import.meta.url) : undefined;`,
which slipped through and crashed at boot with "Identifier 'require' has already
been declared". Generalized the match to line-wise `const require = ...createRequire...;`.
- The scan only walked node_modules/yargs, so yargs-parser (the actual offender)
was never patched. Broadened the scan to yargs and its ESM dependency packages.
Also register the three server test files that were present but missing from the
curated server/lib/tests/index.js (they never ran): the MimeBleed file-validation
bypass regression, the import/export privacy settings, and the impersonation report
query. Server suite now boots and runs (411 passing).
CHANGELOG Upcoming documents the bug and the two-stage debugging journey.
The yargs postinstall patch expanded import.meta.url -> require('url').pathToFileURL(...)
BEFORE removing the const require = createRequire(...) call. The paren-naive removal
regex createRequire([^)]*) then stopped at the first ) of the injected require('url'),
replacing only part of the statement and leaving a dangling
/* Meteor: use wrapper require */.pathToFileURL(__filename).href));
in yargs' esm.mjs. That is invalid JS (a leading .pathToFileURL after a comment), so
Meteor's reify parser threw 'Unexpected token (19:33)' when the test server bundle pulled
yargs in -- meteor test reported 0 tests, even though every repo source file was fine.
Fix: remove the createRequire call BEFORE expanding import.meta.url (so the injected
parens can't break the match), and add a repair rule + trigger so re-running the script
collapses the dangling leftover in already-corrupted node_modules/yargs files back to the
marker. Fresh and already-broken files both end valid.
- models/fileValidation.js: a stray BOM (U+FEFF) inside the looksLikeDangerousMarkup
regex character class broke the server test build with a Babel 'Unexpected token'
syntax error, so Mocha ran 0 tests. JS \s already matches U+FEFF, so the explicit
BOM was redundant; use plain \s*.
- models/import.js: importBoard awaited assertImportEnabled() BEFORE the switch that
check()s the "board" argument. Meteor's audit-argument-checks tracks checks on the
async context, so awaiting first made the later check(board) not count, throwing
'Did not check() all arguments during call to importBoard' -- which broke the WeKan
JSON import (#21) and Excel import (#25) Playwright tests. Check "board" up front
(Match.OneOf(Object, Array)) before the await; the per-source switch still does its
specific check.
- server/publications/activities.js: move the disableActivities gate to AFTER the
check() calls so the argument-checks audit is satisfied when the toggle is on
(latent: it would have thrown 'did not check all arguments' with activities off).
Supersede the earlier 'keep MongoDB 7' approach: the WeKan snap now moves EVERY
existing MongoDB database onto FerretDB (SQLite) on first boot after an upgrade,
then shuts MongoDB down so only WeKan (Node.js) + FerretDB (SQLite) run. Text data
goes into SQLite; CollectionFS + Meteor-Files GridFS attachments/avatars are
extracted to files/attachments and files/avatars.
migration-control now routes by how the source can be READ, not by keep-vs-migrate:
- mongod 7 can open it (modern MongoDB 6/7, incl. a DB earlier migrated up from 3.x)
-> keep that mongod as source, run the modern driver importer
(migrate-mongodb-to-ferretdb.mjs) into a temp FerretDB SQLite target.
- else mongod 3.2 can open it (old MongoDB 3.x) -> run migrate-mongo3-to-ferretdb.mjs
(legacy mongoexport CLI).
- else (unreadable/corrupt, or tools missing) -> fall back to a normal MongoDB start
so the service is never left dead, and retry next boot.
On success: mark done + snapctl set database=ferretdb; mongodb-control then stops and
disables the MongoDB service. This also fixes the reported failure where a healthy
MongoDB 7 database was misdetected as MongoDB 3.x, the 3.x path failed, and — because
mongodb-control exec's this script — mongod never started and the service looped.
Probe readiness lengthened 20s -> 45s; the temp source mongod is tracked by pidfile
and force-stopped so it never leaves the dbpath locked. MongoDB data is never
modified or deleted; the snap only switches to FerretDB once migration succeeds.
Both migration dashboards (Snap MongoDB-3 importer and the Sandstorm/general
MongoDB importer) hard-coded 'WeKan'. If the migrated database has a product name
set in Admin Panel (settings.productName), the dashboard now shows that name and
does not mention WeKan; otherwise it defaults to WeKan.
- snap-src/bin/migrate-mongo3-to-ferretdb.mjs: read settings.productName via
mongoexport early in run() into state.product; title/h1/complete message use it.
The importer->app hand-off poll no longer keys off the product-dependent title
string; it checks a stable '<!--migration-dashboard-->' marker instead.
- releases/migrate-mongodb-to-ferretdb.mjs: read settings.productName via the driver
right after the source connects into state.product; the HTML __PRODUCT__ placeholder
is substituted in buildHtml(). Header clarified to FerretDB (SQLite).
The one-time MongoDB 3 -> FerretDB migration check probed whether mongod 7 could
open the existing data and, if that short probe failed for ANY reason (journal
recovery, stale mongod.lock, slow disk, large oplog), assumed the data was
MongoDB 3.x. A healthy MongoDB 7 database that failed the probe was then sent down
the 3.x path, where mongod 3.2 also could not open it, logging 'could not start
migratemongo mongod on the MongoDB 3.x data'. Because mongodb-control exec's the
migration script, mongod never started -- the MongoDB service failed to activate
and looped on every restart.
Fix: only run the 3.x migration when the data is POSITIVELY confirmed as MongoDB
3.x (mongod 3.2 can actually open it). In every other case -- MongoDB 6/7, data the
probe could not read, or missing tools -- fall back to a normal MongoDB 7 start in
the same service activation (set the marker + exec mongodb-control) instead of
exiting with nothing running. Also: lengthen the mongod-7 readiness probe 20s -> 45s
so large databases needing recovery are not misread, and track the temporary probe
mongod by pidfile + force-stop it so it never leaves the dbpath WiredTiger lock held.
The MongoDB data is never modified or deleted.
- client/lib/tests/renderLinksPlainText.tests.js: the 'render links as plain text'
toggle and XSS hardening. Config assertions (stripLinks adds <a> to FORBID_TAGS,
default does not, KEEP_CONTENT) plus real sanitizeHTML() behavior: <a> stripped to
its text when stripLinks, kept by default, and javascript: hrefs / event handlers /
<script> removed regardless.
- client/lib/tests/alwaysShowCodeAsText.tests.js: escapeHtmlSource (exposed on the
Markdown export) escapes <script>/comments/quotes/ampersand-first so code is shown
not executed; leaves plain text and empty strings unchanged.
- server/lib/tests/impersonationReportQuery.tests.js: the Impersonation report search
selector (extracted to models/lib/impersonationReportQuery.js so it is testable
without re-registering the publication) builds a case-insensitive $or across the
identity fields, escapes regex metacharacters, and is empty for a blank term.
The MimeBleed fix (fileValidationBypass.security.tests.js), SortBleed
(boards.security.tests.js) and import/export anonymization
(importExportSecurity.tests.js) already carry positive + negative tests.
WeKan's upload validation (models/fileValidation.js) detects a file's real MIME
type by running the Unix 'file' command. On minimal Docker/Alpine images without
'file', detectMimeFromFile() silently returned undefined and the code fell back to
the CLIENT-supplied fileObj.type. An authenticated board member (WITH_API=true)
could upload HTML+JS while setting fileType: image/png; the spoofed type is not on
the dangerous-MIME deny-list, so the dangerous-content scan was skipped and the file
was stored, yielding stored XSS served under the WeKan origin (CWE-434).
Fix: the client-supplied type can no longer gate the safety scan. When content-based
detection via 'file' is unavailable, fall back to a dependency-free JS content sniff
(looksLikeDangerousMarkup) that inspects the real bytes for HTML/SVG/XML/<script>
signatures and forces the dangerous-content scan regardless of the claimed MIME, so a
spoofed image/png that is really HTML+JS is rejected. The sniff matches only definitive
markup, so genuine binary uploads (real PNG/JPEG/PDF, incl. large ones) are unaffected.
Also log a one-time warning when 'file' is missing (previously silent).
Regression test server/lib/tests/fileValidationBypass.security.tests.js covers the
spoofed dangerous uploads and safe binaries.
CVSS:3.1 8.3 High (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N). Thanks to HNUfwj and xet7.
Disabling activities/notifications/watch can serve any law or policy, not only
GDPR. Remove GDPR mentions from the code comments and the en.i18n.json toggle
descriptions (and the Admin Panel docs), keeping the neutral #5820 reference.
A card description was recorded in the activity log (a-changedDescription, #5482)
but never added to the outgoing-webhook / notification params, so integrations got
an event with no description text.
The Activities.after.insert payload now sets params.description (the card's current
description, only when non-empty) for every card event, and forwards the activity's
oldValue/value alongside the existing timeValue/timeOldValue so a description change
carries the before/after text. New fields are additive and backward-compatible.
Impersonation events are already recorded in the impersonatedUsers collection
whenever an admin impersonates a user (e.g. to export a private board). Surface
them as a new admin report, newest-first, with the same server-side search +
pagination as the other reports.
- server/publications/impersonationReport.js: impersonationReport publication
(admin-only, sort createdAt -1, limit/skip, $or search over admin/user/board/
attachment ids and reason) + getImpersonationReportCount method. Also publishes
the referenced admin/impersonated user docs (same fields the People panel uses)
so usernames resolve and the Edit user popup has its data.
- adminReports.jade/.js: new Impersonation side-menu entry, sub-template (Date,
Admin, Impersonated user, Board, Reason), pagination + search wired through the
shared reportConfig. Clicking a username opens the same editUser popup as
Admin Panel / People.
- i18n: impersonationReportTitle, impersonation-admin, impersonation-user, reason.
- server/lib/tests/importExportSecurity.tests.js: positive and negative unit
tests for the anonymization helpers (map building, user-doc scrubbing, @mention
rewriting that ignores unknown handles and email addresses, requestedBy/assignedBy
identity replacement, and safe no-ops on empty input).
- docs/Features/Admin-Panel/Admin-Panel.md: new Features tab section documenting the
Performance, Security (rich-text hardening + import/export privacy) and
Notifications (GDPR) toggles.
New Notifications category with three GDPR toggles (all default off), enforced
server-side:
- Disable all activities: activity-feed entries are neither recorded
(Activities.before.insert returns false to cancel) nor shown (the activities
publication returns nothing, hiding existing and new entries).
- Disable all notifications: the Activities.after.insert dispatch hook returns
early, so no watch notifications are sent; activity recording is unaffected.
- Disable watch: the watch() method rejects any watch-level change, and the board
header watch button is hidden.
A synchronously-readable server cache (models/lib/featureFlags.js, kept current by
a Settings observer) lets the sync before.insert hook read the flags without
awaiting. Adds settings disableActivities, disableNotifications, disableWatch,
published in the public setting, plus global Blaze helpers activitiesDisabled /
watchDisabled for UI hiding.
Six new optional toggles (all default off) governing how boards and user data
cross the WeKan boundary, enforced server-side so they cannot be bypassed from
the client:
- Disable all import / Disable all export: master switches that turn off every
import/export feature. importBoard and cloneBoard call assertImportEnabled;
every Exporter build()/buildStream() call assertExportEnabled. The import and
export menu options are hidden in the UI via global importDisabled/exportDisabled
Blaze helpers.
- Disable import avatars / Disable export avatars: never carry avatars into/out of
WeKan. Import is gated at the single localizeAvatarFromBuffer choke point (covers
WeKan JSON import, LDAP, OIDC/OAuth2 login sync and board-open localization) plus
Trello inlineMemberAvatars; export skips the base64 avatar embed + avatarUrl in
both Exporter paths.
- Anonymize import users / Anonymize export users: replace username/fullname/initials
with counter placeholders (user1, user2, ...), drop avatars, and rewrite @username
mentions + requestedBy/assignedBy in card/comment text. The placeholder word "user"
is translated to the current user language. Export covers both build() and the
streaming buildStream() (which does an id-only pre-scan so mentions streamed before
the users array resolve to matching labels); import scrubs the incoming board in
WekanCreator before any placeholder user or card is created.
New shared server helper models/lib/importExportSecurity.js holds the settings
read, guards, anonymization map builder and text rewriter. Adds settings
disableAllImport, disableAllExport, disableImportAvatars, disableExportAvatars,
anonymizeImportUsers, anonymizeExportUsers, published in the public setting.
Translate the new render-links-as-plain-text-description, always-show-code-as-text
and always-show-code-as-text-description strings to Finnish, matching existing
terminology (tarkistuslistat, klikattava, 'Kun käytössä...', 'Oletuksena pois
käytöstä...').
New optional toggle under Admin Panel / Features / Security. When enabled,
rich text is never rendered as markdown or HTML -- the entire source is shown
as escaped plain text in every rich text field (board and card titles,
descriptions, comments, checklists, etc.), so hidden content is always
revealed: HTML comments (<!-- -->), the target URL inside a markdown link,
JavaScript and any other code. All code is always visible, not clickable, and
not running.
This extends the existing invisiblebleed protection (which already showed raw
source for description-less markdown links) to all content, and complements
the "Render links as plain text" toggle.
Implementation:
- New global setting alwaysShowCodeAsText (models/settings.js), published in
the public setting publication, default off.
- Admin Panel / Features / Security gains a second materialCheckBox toggle.
- The markdown helper has the raw text, so the decision lives there. Because
the wekan-markdown package cannot import app code, the setting is bridged via
a ReactiveVar on the exported Markdown object; the rich text viewer
(editor.js) keeps it in sync with the setting reactively, and the markdown
helper reads it and renders the fully HTML-escaped source in a <pre> when on.
Full escaping (not DOMPurify stripping) ensures tags like <script> are shown
as text rather than removed. The description-less-link branch now uses the
same full escaping so it too reveals all source.
New optional security feature under Admin Panel / Features / Security. When
enabled, all links -- both markdown links like [label](url) and raw HTML
<a href> tags -- are always shown as plain, non-clickable text in every rich
text field (board and card titles, descriptions, comments, checklists, etc.),
so a link can never be clicked and cannot present misleading anchor text.
This is a hardening option on top of the existing XSS sanitization (which
already strips javascript:/data: schemes, event-handler attributes and
dangerous tags); it addresses #6453, where a board title rendered as a
clickable link. Default off, so links stay clickable.
Implementation:
- New global setting renderLinksAsPlainText (models/settings.js), published in
the public setting publication.
- Admin Panel / Features gains a Security category with a materialCheckBox
toggle that saves the setting immediately.
- The shared DOMPurify sanitizer takes a { stripLinks } option that forbids the
<a> tag while keeping its inner text (KEEP_CONTENT), so links collapse to
plain text. The rich text viewer (mentions helper in editor.js) passes
stripLinks based on the setting, gated reactively so toggling re-renders.
On Sandstorm the grain owner (Sandstorm 'configure' permission) maps to a
WeKan admin, which gates the Admin Panel and its Attachments / Sandstorm
cleanup tools. A new grain worked because Users.after.insert derives the role
for the freshly inserted user, but a grain migrated from an older WeKan
already contains the user, so that insert hook never runs. Sandstorm
auto-login uses connection.setUserId() (bypassing accounts-base, so
Accounts.onLogin never fires), and the remaining observeChanges hook on
services.sandstorm only fires when that field actually changes on login. When
the migrated user's stored permissions already equal the grain's current
permissions, the login $set is a no-op with no oplog entry, so isAdmin was
never derived and the owner had no Admin Panel.
Reconcile at grain startup (a migrated grain reboots once migration
completes): grant WeKan admin to every Sandstorm user whose stored
permissions include 'configure'. Promote only, never revoke here (that is the
change hook's job on a real permission change), so a stale/empty stored
permission set can never lock the owner out of the Admin Panel.
The storage engine was read only from serverStatus, which requires the
cluster-level clusterMonitor role. A per-database WeKan user (readWrite on
the wekan db only, as in docs/Platforms/FOSS/Docker/Meteor3/) is not
authorized to run it, so it was rejected and the field stayed 'unknown'.
Fall back to a $collStats aggregation, which needs only the collStats action
that read/readWrite already grant, and read the real engine (wiredTiger /
inMemory) from storageStats. The MongoDB compatible version and Database
commit were already correct: they come from buildInfo, which needs no
special privileges.
The postinstall script (scripts/patch-yargs-dirname.cjs) added in 6b409c5da
to fix the Meteor/Mocha test boot crash makes npm mark the root package with
hasInstallScript: true. Sync the lockfile to match package.json.
A low-privilege (comment-only / read-only) board member could escalate to
board admin and take over a private board via the board 'sort' collection
allow rule. To support drag-to-reorder, a second Boards.allow({ update })
rule returned true for any board member whenever the update touched the
'sort' field. Meteor applies the WHOLE modifier once any allow rule approves
and no deny rule rejects, and does not scope the approving rule to the field
that satisfied it. Because canUpdateBoardSort only checked that 'sort' was
AMONG the modified fields, a restricted member could smuggle arbitrary board
mutations (members, permission, title) into the same $set as 'sort' in one
DDP Boards.update call, making themselves admin, flipping the private board
to public, and evicting the legitimate owner. The last-admin deny rule only
inspected $pull, so a wholesale $set of the members array bypassed it.
Fix: restrict canUpdateBoardSort so the sort-reorder rule approves an update
only when 'sort' is the sole modified field. Defense in depth: the last-admin
deny rule now also rejects a $set rewrite of the members array that would
drop the last active admin. Regression test covers the multi-field smuggling
case. The legitimate All Boards drag-reorder is unaffected: it persists order
per-user in profile.boardSortIndex, not in the board document.
CVSS:3.1 8.8 High (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H), CWE-863, CWE-269.
Thanks to 5ud0 / Tarmo Technologies and xet7.
- .github/workflows/meteor-spk.yml: assembles WeKan's modernized meteor-spk (the
packaging tool + its meteor-spk.deps: Node 24, FerretDB, Mongo 3.x CLIs, modern
sandstorm-http-bridge) via sandstorm-src/build-deps.sh — reusing sandstorm.yml's
setup — and zips it as meteor-spk-YYYY-MM-DD_HH_MM_SS.zip, the same kind of
self-contained bundle sandstorm-io/meteor-spk releases but with WeKan's deps baked in.
Uploaded as an artifact and to the newest GitHub Release. Needs no keyring (it does
not pack/sign a .spk).
- .github/workflows/sandstorm-store.yml: publishes an already-built .spk to the
Sandstorm App Market. Installs Sandstorm + assembles meteor-spk and restores
SANDSTORM_KEYRING like sandstorm.yml, downloads the newest .spk from the WeKan release
(built and attached by sandstorm.yml), renames it to wekan-<VERSION>.spk (VERSION from
the release tag, e.g. 9.88, overridable via inputs), and runs 'spk publish
wekan-<VERSION>.spk' exactly like releases/release-sandstorm.sh. Does not rebuild the
.spk.
Thanks to xet7.
The crash moved from yargs/lib/platform-shims/esm.mjs to yargs/build/lib/utils/
apply-extends.js (import.meta.resolve(config.extends)) — yargs' ESM build has more than
one file using import.meta. Rewrite the workaround to scan the whole node_modules/yargs
tree and rewrite every ESM-only construct (import.meta.url/.resolve/.dirname/.filename,
const require = createRequire(...), and yargs' own const __dirname) to the CommonJS
wrapper equivalents in one pass, so a new yargs file can't reintroduce the boot crash.
yargs is never executed at runtime here, so only valid CommonJS parsing matters.
Thanks to xet7.
Renaming only __dirname was not enough: yargs' esm.mjs also uses import.meta.url (invalid
in Meteor's CommonJS module wrapper -> 'Cannot use import.meta outside a module') and
redeclares require via createRequire(import.meta.url) (collides with the wrapper's
require). reify rewrites the file's import statements but not import.meta. Rewrite all
three to use the wrapper's __filename / __dirname / require, which yargs never actually
exercises here (it is only bundled, never called). Idempotent; also patches a file left
half-rewritten by the previous version of this script.
Thanks to xet7.
.meteorignore does not apply to node_modules under Meteor 3 / rspack (verified: the
rebuilt Mocha bundle still contained node_modules/yargs), so revert that no-op rule and
fix the actual crash instead.
Root cause (pre-existing, unrelated to app code): the whole yargs package gets pulled
into the server bundle as a transitive devDependency, and its ESM shim
node_modules/yargs/lib/platform-shims/esm.mjs declares a top-level
'const __dirname = fileURLToPath(import.meta.url)'. Meteor wraps that .mjs in a CommonJS
module function that already provides __dirname, so it collides —
'SyntaxError: Identifier __dirname has already been declared' — crashing the server
bundle at boot, which is why Mocha reported 0 tests.
Add scripts/patch-yargs-dirname.cjs, run from a package.json postinstall, which renames
yargs' local __dirname (and its .substring/.lastIndexOf uses) to __dirnameYargs so it can
no longer collide with the wrapper's. Idempotent and best-effort (never fails install);
yargs is not used at runtime here so the rename is inert for the app.
Thanks to xet7.
'meteor test' (Mocha) crashed at boot with 'Identifier __dirname has already been
declared'. Root cause is unrelated to app code: node_modules/yargs/lib/platform-shims/
esm.mjs declares a top-level 'const __dirname = fileURLToPath(import.meta.url)', which
collides with the __dirname that Meteor's CommonJS module wrapper already provides. yargs
gets pulled into the bundle only because i18next-sprintf-postprocessor (imported by
imports/i18n/tap.js) ships a stray rollup.config.js that does require('yargs'), and a
devDependency (mongodb) has since installed yargs 18 into node_modules so that require now
resolves. That build config is not part of the package's runtime entry
(index.js -> dist/commonjs), so exclude it from Meteor's bundle via .meteorignore.
Pre-existing (the same boot crash happened before this branch's test work) and independent
of the avatar/import-export feature.
Thanks to xet7.
Two build/load fixes surfaced by running the full test suite:
- server/imports.js: import the new server-only files so they actually load. In
mainModule mode a file not imported here never loads, so Accounts.onLogin avatar
localization (server/avatarLocalizationOnLogin.js) and the imported-user reconciliation
Meteor methods (server/importedUserReconciliation.js) would silently never register.
- Test __dirname collision: 'meteor test' crashed at boot with 'Identifier __dirname has
already been declared'. The client test index (client/lib/tests/index.js) loads
boardTriggersClass.tests.js, which referenced the bare __dirname global; under
meteor test that makes Meteor inject a colliding with its CommonJS
wrapper's, aborting the whole server bundle (the same failure already fixed once in
dependencies.openapi.tests.js — this was the next file to trip it). Replace __dirname
with a process.cwd()/PWD walk-up in boardTriggersClass.tests.js, and defensively in the
node-run tests/rtl.test.js, tests/stickers.test.js and tests/stickers.packs.test.js too.
Thanks to xet7.
Split the testable, security- and correctness-critical logic of the new avatars +
original-members feature into pure Node modules (module.exports, no Meteor) and cover them
with plain-Node tests/*.test.cjs (positive AND negative), matching the repo's existing
unit-test style. Wired into the test:unit:node script.
- models/lib/avatarUrlSafety.js (from server/lib/localizeAvatar.js): isExternalAvatarUrl,
isBlockedIp (SSRF ranges), decodeDataUri, checkUrlHostSync. Tests assert external vs
local/relative/non-http URLs, that private/loopback/link-local/CGNAT/metadata/multicast
IPv4+IPv6 (incl. IPv4-mapped) are blocked while genuine public addresses and near-miss
ranges are allowed, data: decoding, and that non-http(s) schemes, localhost and
metadata hosts are rejected.
- models/lib/importedUserReconciliationPlan.js (from server/importedUserReconciliation.js):
planReconciliation. Tests: matches merge, non-matches deactivate, never match another
placeholder or self or a username-less placeholder.
- packages/wekan-ldap/server/ldapPhoto.js (from sync.js): getLdapPhotoBuffer. Tests the
Buffer / array / {type:'Buffer'} / base64 shapes and jpegPhoto>thumbnailPhoto, and that
missing/empty/short values yield null (never throw).
- models/lib/importedBoardMemberPlan.js (from wekanCreator.js): planImportedBoardMember.
Tests: unmapped member kept by original id but inactive+non-admin, mapped role preserved,
importer and id-less members dropped.
The extractions are behaviour-preserving; the app modules now import these helpers.
Thanks to xet7.
Make it obvious which board members are backed by a real account and which are not,
both on card avatars and in the right sidebar member list.
- userAvatar (js/jade/css): the avatar gets an 'imported-member' class (dashed grey ring
+ amber '?' corner badge) for un-reconciled board-import placeholders
(authenticationMethod:'imported'), and an 'inactive-member' class (greyscale + dim +
grey badge) for members deactivated on the board (e.g. a person not in LDAP after
reconciliation). A placeholder is usually both; a real active account gets neither. The
tooltip explains the state.
- boards.js membersForSidebar(): active members first (as before) then the inactive /
imported ones appended, and sidebar.jade uses it, so the member list shows EVERYONE and
which are active vs not instead of hiding inactive members.
- i18n: imported-member-no-account, inactive-member.
Thanks to xet7.
server/importedUserReconciliation.js turns the inert import placeholders into the right
real members deliberately, after import — never auto-mapped during import.
- reconcileImportedUsers(): for each authenticationMethod:'imported' placeholder, find a
real account with the same username (not another placeholder) — the valid accounts are
whatever real WeKan accounts exist, typically provisioned by LDAP/OIDC at login — and
merge the placeholder into it; if none exists (e.g. the person is not in LDAP) leave the
placeholder and its board memberships inactive (isActive:false).
- mergeImportedUserInto(placeholderId, targetId): reassign every reference (board.members,
card.members, card.assignees, activities.userId, cardComments.userId) from the
placeholder to the target, reusing the target's membership if it already is a member,
then remove the placeholder.
- Admin-only Meteor methods reconcileImportedUsers and mergeImportedUser(placeholderId,
targetId) for the automatic sweep and for one-off deliberate mapping.
Because reconciliation matches against existing real accounts (which LDAP populates on
login) it needs no direct LDAP query and is provider-agnostic. Merges reassign by the
preserved original _id, so nothing is lost.
Thanks to xet7.
Importing a WeKan board no longer collapses unmapped members onto the importing user (a
wrong mapping attaches the wrong person and can leak board permissions). Instead each
original member carried in board.users becomes an inert placeholder user:
- createPlaceholderUsers() reuses the ORIGINAL _id, so every card/comment/activity
member reference resolves to the right person with no remapping, and populates
this.members as an identity map (id -> id) so _user() keeps the original member.
- Placeholders are created with Users.direct.insertAsync (bypassing the after.insert
hooks — Sandstorm identity sync and the disableRegistration/invitation-code gate that
would otherwise reject a non-account user), marked authenticationMethod:'imported',
loginDisabled:true, isActive:false, and carry NO password or email. Username collisions
suffix the placeholder's name so an existing account is never renamed/stolen.
- The member's avatar (embedded as base64 by the exporter) is restored into files/avatars
via the localizer, so the original picture shows and is exportable again (Sandstorm too).
- Board membership adds imported members inactive and non-admin, so they are visible but
hold no permissions until deliberately reconciled later; the importer stays sole admin.
Also guard the Sandstorm Users.after.insert hook to no-op on users without
services.sandstorm (placeholders), which otherwise threw reading preferredHandle.
Mapping/merging to real accounts and LDAP reconciliation are a later, deliberate step.
Thanks to xet7.
Board export already carried the original members' username, fullname and initials (the
field whitelist excludes passwords, emails and services). Now, for each member whose
avatar is a local WeKan file, embed the file itself as base64 (profile.avatarFile +
avatarFileName + avatarFileType) alongside the existing avatarUrl reference, in both the
in-memory build() and the streaming buildStream() paths.
Because this reads a local avatars file (no outbound fetch), it works inside a Sandstorm
grain too: a board that already has many stored avatars can be exported with every
member's picture intact, ready to re-import elsewhere. External, not-yet-localized
avatars are still exported as their URL only (Increment 1 localizes those into files
first, on board open / at login).
Thanks to xet7.
Second set of triggers so external avatars from identity providers become local WeKan
avatars (files/avatars), shown in WeKan and carried by board export/import.
- LDAP (packages/wekan-ldap/server/sync.js): syncUserData now reads the binary
jpegPhoto / thumbnailPhoto (tolerating Buffer / array / {type:'Buffer',data} / base64
shapes) and, unless the user already has a locally-stored avatar, stores it as a
self-contained data: URI in profile.avatarUrl. That displays immediately and needs no
coupling from this package to app code; the app then materialises it to a real file.
- OIDC (packages/wekan-oidc/oidc_server.js): capture the standard claim (with
an OAUTH2_AVATAR_MAP override) into serviceData and set profile.avatarUrl for new users.
- App (server/avatarLocalizationOnLogin.js): Accounts.onLogin localizes the logging-in
user's external avatar immediately (provider-agnostic), complementing the board-open
trigger. Fire-and-forget, idempotent, never blocks login. (Inside a Sandstorm grain
outbound fetch is sandboxed, so a Sandstorm profile picture is a best-effort no-op here
and needs client-assisted capture — a later step.)
Thanks to xet7.
First trigger for external-avatar localization: the single-board publication
(publishComposite('board')) now fires a best-effort, fire-and-forget
localizeBoardMemberAvatars(boardId). For every member whose profile.avatarUrl points
outside WeKan, it copies the image into WeKan's own files/avatars (via the SSRF-guarded
helper) and repoints avatarUrl at the local copy. Idempotent (already-local avatars are
skipped) and it never blocks or delays the publication.
This is the universal catch-all: no matter how a member's avatar became external
(OAuth2/OIDC picture URL, gravatar, a pasted URL), opening the board makes it a local,
exportable WeKan avatar. (Note: inside a Sandstorm grain outbound fetch is sandboxed, so
Sandstorm profile pictures need the provider-side capture added next; LDAP jpegPhoto is
binary and is imported at login, also next.)
Thanks to xet7.
First piece of making external avatars (Sandstorm profile pictures, LDAP jpegPhoto,
OAuth2/OIDC picture claims, gravatar, any URL/data-URI) into local WeKan avatar files
under WRITABLE_PATH/files/avatars, so they display without the identity provider and are
carried by board export/import.
server/lib/localizeAvatar.js:
- isExternalAvatarUrl(url): true unless already a local /cdn/storage/avatars/ (or legacy
/cfs/files/avatars/) URL.
- localizeAvatarFromBuffer(userId, buffer, {type,name}): write bytes into the Avatars
FilesCollection (onAfterUpload sets the user's local profile.avatarUrl).
- localizeAvatarFromUrl(userId, url): fetch (or decode a data: URI) then store.
- localizeUserAvatarIfExternal(user): idempotent no-op when already local.
Every network fetch is SSRF-guarded: http/https only; the host is resolved and any
private, loopback, link-local, CGNAT, multicast or cloud-metadata (169.254.169.254)
address is refused; 8s timeout; 5 MB cap; image content-type only. Not wired to any
trigger yet (board-open / export / login flows come next).
Thanks to xet7.
Now that the grain boots, logs in, lists/opens boards, uploads/downloads attachments,
and migrates an old MongoDB 3 grain end to end, drop the two temporary diagnostics:
- models/attachments.js: revert the Meteor-Files debug flag to false (it was enabled on
Sandstorm to trace the upload 'session expired [408]' issue and was spamming the grain
log with the full FilesCollection dump and every find()).
- server/ferretdbQueryDebug.js: remove the mongodb Collection.find wrapper that logged
FerretDB-rejected selectors (used to find the $elemMatch 'unknown operator: userId'
issue, since fixed in wekan/FerretDB).
Thanks to xet7.
Two UX requests once the migration itself works end to end:
- Auto-open All Boards. Previously the dashboard sat on the grain URL for 60s after
completion and the admin had to reload to reach WeKan. On success the importer now
exits after ~5s (long enough for the browser to fetch the done page), and the done
page runs a JS poller that fetches '/' until WeKan's app shell answers (tolerating the
brief importer->WeKan port hand-off gap) and then location.replace('/') opens All
Boards. The spinner keeps spinning through the hand-off so it is clear the grain is
still working; no meta-refresh on the done page (a refresh mid-hand-off would hit a
dead port). On failure it still stays up a minute with the errors.
- WeKan colours. Recolour the dashboard from the dark scheme to WeKan's blue/white/grey
(belize blue #2980b9 headings and spinner, white card on light-grey background, grey
borders, system font).
Thanks to xet7.
With text collections migrating, the 3 attachments still produced 0 extracted files and
errors like 'parse attachments.chunks: Unexpected Binary Extended JSON format
{"$binary":"iVBOR..."}'. Two causes, both fixed:
1. Legacy Extended JSON v1 binary. mongo 3.x mongoexport writes binary as
{"$binary":"<b64>","$type":"00"} (v1), but modern bson EJSON.parse only
accepts v2 {"$binary":{"base64":...,"subType":...}}. Rewrite v1 -> v2 per line
before parsing, so chunk data (and any binary in text collections) parses.
2. Meteor-Files native GridFS buckets were unhandled. The grain stores attachments in
attachments.files + attachments.chunks (FilesCollection record in the attachments
collection), not CollectionFS's cfs_gridfs.*. Reassemble those buckets to disk,
linking each GridFS file to its record via metadata.fileId/versionName, then repoint
the migrated record's versions.<v> at the file and DROP versions.<v>.meta.gridFsFileId
— otherwise WeKan's getFileStrategy keeps choosing the (now-empty) GridFS backend and
the image 404s. Also exclude <bucket>.files/.chunks (and tempstores) from the text
migration so binary chunks are no longer run through EJSON as documents.
Thanks to xet7.
Text collections exported and 'inserted N/N' was logged, but every document actually
failed with 'Unsupported OP_QUERY command: update' — nothing reached FerretDB. Cause:
requireAny('mongodb') resolved the ANCIENT meteor-spk base driver (v2.x, at the deps
root) because the modern driver is not directly under programs/server/npm/node_modules
— Meteor nests it at .../meteor/npm-mongo/node_modules/mongodb (v6.16). The 2.x driver
speaks legacy OP_QUERY, which FerretDB rejects; the 6.x driver speaks OP_MSG (the same
driver the WeKan app uses against FerretDB). Add the npm-mongo path as the first
resolver anchor, and log the resolved driver version.
(Separately, GridFS attachment/avatar extraction still finds 0 files for this grain —
its attachments appear to be in Meteor-Files buckets (attachments.files/.chunks) rather
than the CollectionFS cfs_gridfs.* the importer currently handles; to be addressed once
the modern driver confirms text migration and the real attachment collection counts.)
Thanks to xet7.
Link the mongodb default-import, mongoexport --quiet removal, diagnostic/dashboard, and
createRequire commits inline within their parent migration bullets.
Thanks to xet7.
The EJSON diagnostic was conclusive: require('bson') returned an object with no EJSON,
mongodb.EJSON was undefined, and mongodb/lib/bson* were MODULE_NOT_FOUND. Cause: the
importer sits at the deps root next to the OLD meteor-spk 0.6.0 base node_modules
(bson 1.x with no EJSON at all, plus an ancient mongodb that has MongoClient but no
EJSON re-export), kept there only for the niscu->3.0 stage — so a bare require resolved
those adjacent copies. WeKan's current bson/mongodb (with EJSON) live under
programs/server/npm/node_modules. Anchor createRequire inside that modern bundle first
(falling back to the deps root), and load both mongodb and bson through it. This also
switches the importer to the modern mongodb driver — the same one the WeKan app uses
against FerretDB.
Thanks to xet7.
The mongodb default import exposed MongoClient but NOT EJSON (mongodbPkg.EJSON was
undefined), so the previous 'take EJSON from mongodb' attempt still hit the FATAL
guard. Node 24's ESM default-interop for these CommonJS packages drops the
getter/re-export used for EJSON. Switch to createRequire (CJS require returns the real
module.exports with getters intact and honors NODE_PATH), and resolve EJSON by trying,
in order: mongodb.EJSON, mongodb/lib/bson.js, mongodb/lib/bson, bare bson's .EJSON,
and bare bson itself — falling back with a diagnostic list of what each attempt
returned so a remaining failure names the cause instead of just 'undefined'.
Thanks to xet7.
With mongoexport connecting, every collection then failed with 'parse <coll>: Cannot
read properties of undefined (reading parse)' — EJSON was undefined. The importer runs
with NODE_PATH=programs/server/node_modules, but the EJSON-bearing bson (7.3.0) lives
under programs/server/npm/node_modules, so a bare 'import bson' did not resolve to it
and EJSON came out undefined. The mongodb driver (which resolves fine — MongoClient
works) bundles bson and re-exports EJSON from lib/bson.js, so take EJSON from the same
default import and drop the separate bson dependency. Add an upfront guard that fails
loudly if EJSON.parse is ever missing instead of spamming one error per collection.
Thanks to xet7.
The real reason every mongoexport failed (revealed once --quiet was dropped):
'error connecting to db server: no reachable servers'. mongoexport is a Go tool
whose --host defaults to 'localhost', which resolves to ::1 (IPv6) first, but the
migration mongod listens only on --bind_ip 127.0.0.1 (IPv4) — so mongoexport could
never reach it, while the mongo shell (whose host defaults to 127.0.0.1) connected
fine and listed all 42 collections. Pass --host 127.0.0.1 explicitly.
Also fill the dashboard Activity panel from the first moment (mongoexport-ready line,
and one line per collection including empty ones) so progress is visible live instead
of the panel sitting on '(waiting…)' while only red connection errors accumulated.
Thanks to xet7.
The --version probe confirmed the bundled mongoexport runs fine (r3.2.22), yet every
per-collection export exited status=1 with empty stderr AND stdout. The empty stderr
was mongoexport's own --quiet flag suppressing its failure message. Drop --quiet so
the real reason (e.g. 'Failed: <reason>') prints to stderr; mongoexport's normal
progress lines also go to stderr, so stdout stays clean Extended JSON either way.
Also surface progress on the dashboard as the user asked: a rolling Activity log
(last ~15 lines: collections found, per-collection export/insert counts, GridFS file
extraction counts) rendered in a panel and mirrored to the grain log, so it is clear
what the migration is doing rather than only red errors appearing later.
Thanks to xet7.
Every per-collection mongoexport failed with 'failed: undefined' because the error
only logged (r.stderr || r.error): an empty stderr with a non-zero/killed status
(e.g. a SIGSEGV, where spawnSync reports status=null, signal='SIGSEGV', stderr='')
printed as 'undefined', hiding the real cause. Log a full describe(r) instead
(status, signal, error code, stderr AND stdout snippets), and probe
'mongoexport --version' / '--help' once at startup so the next run shows whether the
bundled mongoexport binary itself is broken (dies on --version too → an ABI/bundling
problem) or only the export invocation is. The mongo shell on the same libs works
(listCollections succeeds), so this narrows it precisely.
Also make the progress dashboard obviously live: a CSS spinner while running (✅/❌
when done), a server 'updated' timestamp that changes on every 2s auto-refresh, and
an error counter — the previous page refreshed but showed no sign of activity.
Thanks to xet7.
Same CommonJS/ESM interop issue as the bson import: with { EJSON } fixed, the
importer next threw 'Named export MongoClient not found. The requested module
mongodb is a CommonJS module'. The bundled mongodb driver is CommonJS under Node
24's ESM loader, so import the default and destructure MongoClient. These are the
only two third-party imports; the rest are node: builtins.
Thanks to xet7.
With the WiredTiger cache fixed, mongod 3.0 now starts and the migration importer
runs, but it crashed immediately: 'import { EJSON } from "bson"' threw
"SyntaxError: Named export 'EJSON' not found. The requested module 'bson' is a
CommonJS module". bson (as bundled in WeKan's server node_modules) is CommonJS, so
Node 24's ESM loader exposes no named exports on it. Import the default and
destructure EJSON from it, as Node's own error message advises. Shared by the Snap
migration too (same Node 24).
Thanks to xet7.
mongod 3.0.7 parses --wiredTigerCacheSizeGB as an integer number of GB, so the
0.25 from the previous commit failed at startup with 'Error parsing option
wiredTigerCacheSizeGB as int: Bad digit "." while parsing 0.25' and the migration
still crash-looped. Use 1 (GB). It is a cache cap, not a preallocation, so a short
migration of a normal-sized grain never actually uses that much.
Thanks to xet7.
Importing an existing WeKan grain crash-looped in the one-time MongoDB 3 -> FerretDB
migration: mongod 3.0 forked and its child aborted with exit code 14. Two bugs:
1. WiredTiger cache_size=0G. mongod 3.0 sizes the WiredTiger cache from detected
RAM (RAM/2 - 1GB), but inside a Sandstorm grain sandbox RAM detection returns 0,
so it computes cache_size=0G and WiredTiger refuses to open (minimum is 1MB),
logging 'Value too small for key cache_size' and 'Fatal Assertion 28561' to
/var/migration-mongod.log before aborting. Pass an explicit
--wiredTigerCacheSizeGB 0.25 (256MB) to both mongod invocations (the niscu ->
3.0 stage, which writes new WiredTiger data, and the 3.0 -> FerretDB stage) so
the cache size never depends on RAM detection. The data itself is fine (turtle
file: WiredTiger 2.5.3, i.e. written by mongod 3.0; the 'unclean shutdown'
notice is harmless, WiredTiger recovers from the last checkpoint).
2. Wrong source database. Sandstorm WeKan grains store their data in the
Meteor-default database 'meteor' (the niscu stage reads/writes 'meteor', and a
real grain's WiredTiger catalog shows meteor.* collections), but the importer
was told SRC_DB 'wekan', so even once mongod started it would have exported zero
collections. Only the FerretDB target database is 'wekan'.
Thanks to xet7.
Two grain-navigation regressions vs the last working Sandstorm build (v6.15):
1. Opening a grain showed 'Must be logged in' instead of the All Boards page.
On Sandstorm the platform authenticates the user asynchronously over DDP via
connection.setUserId(), which (unlike a password login) does not set
Meteor.loggingIn(), so Meteor.userId() is null for the first moments after the
grain opens. useraccounts' ensureSignedIn route trigger and renderBoardList()
both bounced that brief null window to the atSignIn route — a sign-in page that
does not exist inside a grain. config/router.js now uses a Sandstorm-aware
ensureSignedInUnlessSandstorm wrapper (a no-op on Sandstorm) and renderBoardList()
no longer redirects on Sandstorm; the All Boards list renders and reactively
fills in once the platform login lands.
2. The Sandstorm shell's outer grain URL did not update when switching boards. The
shell rewrites /grain/<id><path> when the app posts a { setPath } message
(sandstorm shell/imports/client/grain-client.js). The sync was a bare top-level
Tracker.autorun that could run before flow-router-extra's reactive path tracking
was initialised and then never re-run. Restore v6.15's event-driven
FlowRouter.triggers.enter (fresh entering path, no dependency on init order) and
additionally keep a watchPathChange() autorun wrapped in Meteor.startup as a
backup. setTitle now also re-runs on route change.
Thanks to xet7.
On Sandstorm the grain download route returned 403 for private boards, so
attachment thumbnails, minicard covers and the slideshow all showed broken
images even though the upload succeeded and the file was on disk. The download
handler (server/routes/universalFileServer.js) authorizes private-board files
via a Meteor login token (Authorization / X-Auth-Token / authToken query /
meteor_login_token cookie), but Sandstorm has none of these: authentication is
via connection.setUserId() and the sandstorm-http-bridge X-Sandstorm-* request
headers, so extractLoginToken() returned null and isAuthorizedForBoard() denied
every request. Sandstorm already gates grain access at the platform level, so any
request reaching WeKan is an authenticated grain user; trust it. Both
isAuthorizedForBoard() and isAuthorizedForAvatar() now short-circuit to allow
when Meteor.settings.public.sandstorm is set and the request carries the
bridge-injected X-Sandstorm-User-Id header. Gating on the setting means a spoofed
header cannot bypass auth on non-Sandstorm deployments.
Thanks to xet7.
Link the fix that switches attachment uploads to DDP transport on Sandstorm, since
the bridge strips Meteor-Files' custom x-* HTTP upload headers.
Thanks to xet7.
Adding a file to a card in the grain failed with HTTP 400 "Can't continue upload,
session expired [408]" and the file silently disappeared. The Meteor-Files HTTP
upload protocol signals the first chunk with an x-start header and tracks the
session with x-mtok / x-chunkid / x-fileid / x-eof, but Sandstorm's request-header
whitelist (web-session.capnp) does not include those, so the sandstorm-http-bridge
strips them: the server never sees x-start, treats every request as a chunk
continuation, cannot find the per-fileId session and returns 408. Use
transport: 'ddp' on Sandstorm (detected via Meteor.settings.public.sandstorm) so
uploads go over DDP method calls instead of custom HTTP headers; HTTP transport is
kept everywhere else (it avoids the Safari DDP-reconnect stall).
Thanks to xet7.
Attachment uploads in the grain fail with HTTP 400 "Can't continue upload, session
expired [408]": the upload start succeeds but the per-fileId session is gone by the
time a chunk arrives (both the in-memory _currentUploads entry and the
__pre_attachments pre-collection doc are missing), which points at the polling
observe (FerretDB has no oplog) firing a spurious removed event and aborting the
upload. The Attachments FilesCollection had debug: false, so nothing about the
upload path is logged. Enable Meteor-Files debug on Sandstorm only (gated on
Meteor.settings.public.sandstorm) so the grain debug log shows the start /
createStream / observer / continueUpload steps and pinpoints where the session is
lost. TEMPORARY: revert to debug: false once the upload is fixed.
Navigating between boards worked but the Sandstorm shell's grain URL never updated
(it stayed on the grain root), unlike standalone WeKan. The path was synced via a
global FlowRouter.triggers.enter callback, which does not fire reliably on client
navigation in this flow-router-extra / Meteor 3 setup. Sync it from a reactive
Tracker.autorun on FlowRouter.watchPathChange() instead (the same mechanism the
setTitle sync already uses), so every route change posts setPath to the shell and
the grain URL becomes /grain/<id><path>.
Thanks to xet7.
Opening a board in the grain fails with "MongoServerError: unknown operator: userId"
(BadValue) - FerretDB rejecting a query MongoDB accepts - but the driver stack trace
has no WeKan call site, so the offending selector is unknown. This wraps the raw
mongodb Collection.find() to log the collection name and exact selector whenever a
cursor iteration throws, to pinpoint the query. TEMPORARY: to be removed once the
query is rewritten FerretDB-compatibly.
Thanks to xet7.
The Sandstorm login was actually succeeding - Meteor.userId() gets set (confirmed:
Meteor.connection._userId held a valid id, Meteor.loggingIn() false) - but the grain
stayed on the sign-in page. WeKan's home route (renderBoardList) checks
Meteor.userId() once and, because the Sandstorm header login is asynchronous and
uses connection.setUserId() (bypassing accounts-base, so no Accounts.onLogin fires),
finds it still null on first render and redirects to 'atSignIn' - where the user is
stranded even after login completes moments later. Add a Sandstorm-only reactive
autorun that sends the user from 'atSignIn' back to 'home' as soon as
Meteor.userId() is set, so the grain lands on the All Boards page.
Thanks to xet7.
Link the fix that rewrites ROOT_URL to the grain's real URL so DDP, dynamic
imports and the Sandstorm auto-login stop failing cross-origin.
Thanks to xet7.
WeKan loaded but stayed on "Must be logged in", and the console showed
"Cross-Origin Request Blocked ... http://127.0.0.1:4000/__meteor__/dynamic-import/fetch".
The launcher sets a fixed ROOT_URL (http://127.0.0.1:4000, the internal bridge
target), but Sandstorm serves each grain at a per-session host (ui-<hash>.<host>)
via sandstorm-http-bridge, so the client sent its DDP connection and dynamic-import
fetches to 127.0.0.1:4000 - cross-origin and unreachable from the browser - and the
header-based login handshake (a DDP method call) never completed. Add a
WebApp.addRuntimeConfigHook that rewrites ROOT_URL to the grain's real base URL
(X-Sandstorm-Base-Path) per request, and re-asserts the SANDSTORM flag, so the
client talks to the same origin it loaded from. DDP, dynamic imports and the
Sandstorm auto-login now work.
Thanks to xet7.
Link the fix that sets process.env.SANDSTORM=1 in the launcher so the
Sandstorm header-based auto-login runs and pages stop showing "Must be logged in".
Thanks to xet7.
WeKan loads in the grain but every page showed "Must be logged in". The
wekan-accounts-sandstorm client only starts the automatic X-Sandstorm-* header
login when __meteor_runtime_config__.SANDSTORM is set, and the package only sets
that when process.env.SANDSTORM is present - which nothing did (start.js set
MONGO_URL/ROOT_URL/PORT/WRITABLE_PATH but not SANDSTORM, and the pkgdef env didn't
either). The launcher now sets process.env.SANDSTORM = '1' before loading the WeKan
bundle, so the package enables the runtime flag and the client auto-logs-in the
Sandstorm user from the X-Sandstorm-* headers. (App code detects Sandstorm via a
separate Meteor.settings.public.sandstorm flag from the pkgdef METEOR_SETTINGS,
which already worked.)
Thanks to xet7.
Link the change that stops auto-creating a board on a new grain and maps the
grain's Sandstorm permissions to the user's global WeKan role.
Thanks to xet7.
WeKan on Sandstorm is multi-board now, so a new grain/user no longer gets a
hard-coded 'sandstorm'/libreboard board auto-created - the user creates their own
boards from the All Boards page. The Users.after.insert hook no longer inserts that
board + Default swimlane. updateUserPermissions previously added the user as a
member of that single board (which would now crash, since the board no longer
exists); it now maps the grain's Sandstorm permissions to the user's GLOBAL WeKan
role instead - 'configure' (grain owner) becomes a WeKan admin, everyone else is a
regular user who can create and manage their own boards. Dropped the now-unused
Boards/Swimlanes imports.
Thanks to xet7.
WeKan on Sandstorm originally opened a single hard-coded board
(/b/sandstorm/libreboard) and redirected "/" to it. It now supports many boards and
that libreboard is no longer the right destination, so redirecting to it is wrong.
Remove the WebApp.handlers.get("/") redirect entirely: "/" now falls through to
WeKan's normal serving, whose client "home" route (config/router.js) renders the
All Boards list - the right landing page for a multi-board grain. This also removes
the server-side redirect that browsers rejected as a Corrupted Content Error
(FlowRouter.path does not resolve on the server + Content-Length mismatch).
Thanks to xet7.
Link the fix that builds the Sandstorm board redirect path directly and sends a
body with a matching Content-Length, so the grain's "/" stops failing with a
browser Corrupted Content Error.
Thanks to xet7.
The grain's "/" served a broken 301: Location was ".../:6080board" because
FlowRouter.path() does not resolve on the server in Meteor 3.x (it returned the
bare route name "board"), and the response advertised Content-Length: 90 while
sending an empty body, which the browser rejected as a "Corrupted Content Error"
(NS_ERROR_NET_CORRUPTED_CONTENT). Build the board path directly (/b/:id/:slug, the
'board' route) and send a real HTML body (meta-refresh + link) with a matching
Content-Length, so the redirect to the hard-coded Sandstorm board works. Together
with the Accept-Encoding strip (keeps Content-Length valid) and the modern bridge.
Thanks to xet7.
Link the fix that bundles a modern sandstorm-http-bridge (from the Sandstorm
install) so the grain stops serving Corrupted Content on WeKan's "/" redirect.
Thanks to xet7.
The grain boots, but the page failed with a browser "Corrupted Content Error"
(NS_ERROR_NET_CORRUPTED_CONTENT) on WeKan's "/" redirect (0 bytes, ~8.5s, then
failed). The meteor-spk 0.6.0 base bundles an ancient (~2016) sandstorm-http-bridge
that mangles responses - it always advertises Accept-Encoding: gzip to the app and
mishandles redirect/encoding. build-deps.sh now overwrites the bundled
/sandstorm-http-bridge with the modern one from a Sandstorm install
(/opt/sandstorm/latest/bin/sandstorm-http-bridge; override with SANDSTORM_HTTP_BRIDGE).
Sandstorm is amd64-only so one binary covers it. The sandstorm.yml workflow now
installs Sandstorm before assembling the deps so the bridge is available on the CI
runner. Complements the app-side Accept-Encoding strip.
Thanks to xet7.
Link the fix that strips Accept-Encoding in the Sandstorm grain so Meteor serves
uncompressed responses and the browser stops showing a Corrupted Content Error.
Thanks to xet7.
The grain now boots, but the page failed to load with a browser "Corrupted Content
Error" (NS_ERROR_NET_CORRUPTED_CONTENT). sandstorm-http-bridge advertises
"Accept-Encoding: gzip" to the app regardless of what the browser actually sent, so
Meteor served gzip/brotli-encoded responses the browser could not decode. Strip
Accept-Encoding in a WebApp.rawHandlers middleware (which runs before Meteor's
static/boilerplate serving) so responses go out uncompressed; bandwidth is a
non-issue behind the local bridge.
Thanks to xet7.
Link the fix that makes the services.sandstorm.id unique index use boolean
options so FerretDB accepts it and the Sandstorm grain boots past index creation.
Thanks to xet7.
wekan-accounts-sandstorm created the unique index on services.sandstorm.id with
{unique: 1, sparse: 1}. Real MongoDB accepts the truthy 1, but FerretDB (used by
the Sandstorm .spk) is strict and rejects it with "The field 'unique' has value
unique: 1, which is not convertible to bool", crashing the grain at boot during
index creation. Use real booleans (unique: true, sparse: true).
Thanks to xet7.
Link the fix that imports the Users/Boards/Swimlanes/Activities collections and
drops the removed-HTTP-package monkey patch, so the Sandstorm grain boots past
the capnp guard.
Thanks to xet7.
Once the capnp load was made non-fatal, the grain reached the rest of sandstorm.js
and hit two latent Meteor-2.x-isms the Meteor 3.x migration missed (this file only
runs on Sandstorm, so it was not exercised): (1) it referenced the Users, Boards,
Swimlanes and Activities collections as implicit globals, but those are now ES
module default exports, so `Users.after.insert` threw "Users is not defined" at
boot; and (2) it monkey-patched `HTTP.methods` from the meteor/http package, which
was removed in the Meteor 3.x migration, so `HTTP` was undefined. Import the
collections (and Accounts) explicitly, and remove the obsolete HTTP.methods patch
(WeKan's REST API no longer uses it - it uses WebApp/JsonRoutes with header auth).
Thanks to xet7.
Link the release-workflow change that builds amd64/arm64 natively on GitHub and
moves the exotic arches to a non-blocking per-arch Launchpad matrix, so the
release no longer hangs hours on riscv64.
Thanks to xet7.
The single snap job ran `snapcraft remote-build` for all 5 platforms on Launchpad
and blocked until the slowest resolved, so the release hung ~3.5h on riscv64's
Launchpad queue even though amd64+arm64 finished in minutes. Split it in two:
snap-native builds amd64 (ubuntu-24.04) and arm64 (ubuntu-24.04-arm) NATIVELY on
GitHub runners via snapcore/action-build; snap-launchpad builds s390x/ppc64el/
riscv64 on Launchpad in a NON-BLOCKING (continue-on-error), per-arch matrix
(remote-build --build-for <arch>) so a slow or failed exotic arch never delays the
release or the other arches. Each arch publishes to the Snap Store
(candidate,beta,edge) and attaches to the GitHub Release the moment it finishes.
All 5 arches still ship.
Thanks to xet7.
Link the fix that stops capnp.node's Node 24 ABI mismatch from crash-looping
the Sandstorm grain; the grain now boots and core WeKan works via the
sandstorm-http-bridge headers.
Thanks to xet7.
WeKan on Sandstorm bundles Node 24, but capnp.node (node-capnp) is built for an
older Node ABI (NODE_MODULE_VERSION 83 = Node 14), so `Npm.require('capnp')` in
sandstorm.js failed with ERR_DLOPEN_FAILED and crash-looped the whole grain on
boot. Load Cap'n Proto lazily in a try/catch and degrade gracefully: the grain
boots and core WeKan works, because login and user identity come from the
sandstorm-http-bridge X-Sandstorm-* HTTP headers (wekan-accounts-sandstorm) and
need no Cap'n Proto. Only the two capnp-only features are skipped when the addon
cannot load - the Powerbox identity-claim method and the Sandstorm activity
notifications - and they log a clear warning. These can be restored by rebuilding
node-capnp for Node 24, or reimplemented over the bridge's HTTP/JSON API (see the
Sandstorm CHANGELOG: use Cap'n Proto APIs without Cap'n Proto).
Thanks to xet7.
Link the fix that points FerretDB's --state-dir to writable /var so the
Sandstorm grain starts instead of crash-looping on a read-only /state.json.
Thanks to xet7.
FerretDB persists a state.json (version/UUID) via its state provider even
when telemetry is disabled, and its --state-dir defaults to "." — which in a
Sandstorm grain is "/", read-only. So FerretDB failed with "Failed to create
state provider: failed to persist state: open /state.json: read-only file
system", exited (code 1), and the grain crash-looped. sandstorm-src/start.js
now creates /var/ferretdb and passes --state-dir=/var/ferretdb (plus the
FERRETDB_STATE_DIR env) in startFerret(), covering both the migration and the
steady-state FerretDB launches.
Thanks to xet7.
List the glibc-loader grain crash-loop fix, the Sandstorm build-workflow
userns/ref fix, and the Meteor unit tests __dirname server-boot fix in the
Upcoming release section, each linking to its commit.
Thanks to xet7.
server/lib/tests/dependencies.openapi.tests.js is an ES module (top-level
import) that referenced the bare __dirname global. Under Node 24 / Meteor 3.5
the compiler injects "const __dirname = fileURLToPath(import.meta.url)" for
ESM files that use __dirname, which collides with the __dirname the CommonJS
module wrapper already provides, so the whole server bundle failed to boot
with "Identifier '__dirname' has already been declared" and the "Meteor unit
tests" CI job died before any test ran. Drop the __dirname seed
(process.env.PWD already reaches the repo root under `meteor test`, and the
suite skips gracefully otherwise).
Thanks to xet7.
Ubuntu 24.04 defaults kernel.apparmor_restrict_unprivileged_userns=1, which
blocks the unprivileged user namespaces the Sandstorm install and the spk
supervisor rely on, so sandstorm.yml now relaxes it on the runner. Also added
a "ref" workflow_dispatch input and check out that ref instead of a hardcoded
main, so a fix branch can be built and tested before it is merged to main.
Thanks to xet7.
The .spk bundled the host's new glibc libc.so.6 (Ubuntu 24.04, glibc 2.39)
but kept the old glibc 2.31 ld-linux from the meteor-spk 0.6.0 base, so node
failed at startup with "libc.so.6: undefined symbol: _dl_audit_symbind_alt,
version GLIBC_PRIVATE" (HTTP-BRIDGE exit 127) and the grain crash-looped.
sandstorm-src/build-deps.sh now also copies the host's ld-linux-x86-64.so.2
into the deps so the loader and libc are the same glibc, and adds a [verify]
gate that fails the build if the bundled libc and loader glibc versions
differ or the bundled node cannot run under them - turning a silent grain
crash-loop into a loud build failure.
Thanks to xet7.
Dependabot #6451 bumped lib-storage to ~3.1085.0, whose peer requires
client-s3 ^3.1085.0, but client-s3 was left at ^3.1071.0 — breaking
meteor npm install / npm ci with an ERESOLVE peer conflict.
- Drop the ~300 MB of modern MongoDB Database Tools (wekan/mongo-tools) from the
Sandstorm build: they are unused there (Sandstorm backs up/restores grains
itself; the MongoDB 3 -> FerretDB migration uses the legacy migratemongo CLIs +
the .mjs importer). They stay in the .zip bundle / Docker / Snap, which use them.
- Set PUPPETEER_SKIP_DOWNLOAD=1 for the Meteor build (puppeteer is a test-only
devDependency; its ~150 MB Chromium must never end up in the .spk).
- Pack step prints the .spk size and warns when it exceeds 100 MB.
meteor-spk pack crashed with 'open(/home/runner/.sandstorm-keyring): No such file
or directory' because the keyring restore step wrote the decoded SANDSTORM_KEYRING
secret to ~/.sandstorm/sandstorm-keyring, but spk reads the app private key from
~/.sandstorm-keyring (a file directly in $HOME). Write it to the correct path, and
fail early with an actionable message when the secret is missing (the .spk cannot
be signed without it) instead of the cryptic later crash.
The meteor-spk 0.6.0 base ships some runtime libs in meteor-spk.deps/lib as
DANGLING symlinks (e.g. libstdc++.so.6 -> a lib that no longer exists). When step
[3/7] refreshed them with the host's newer libs, 'cp -fL' refused with 'not writing
through dangling symlink' and failed the Sandstorm build. Add --remove-destination
so cp deletes the existing destination (dangling symlink included) before copying
the host's real library. Verified: plain 'cp -fL' reproduces the error; with
--remove-destination it succeeds and writes a real file.
In the swimlanes view, boardBody.js disabled the .js-swimlanes sortable — which
also carries moving a card from one swimlane to another — with !isBoardAdmin(),
though its own comment said it should be disabled only for non-members. So a normal
board member could move a card within a swimlane but not between swimlanes (and
could not reorder swimlanes). Gate it on !Utils.canModifyCard() instead (the client
write-access predicate: board member, not comment-only/worker/read-only), matching
the server's allowIsBoardMemberWithWriteAccess.
The install page (../w/wekan.fi/install/index.html) links to Node.js downloads for
each snap/bundle arch: the OFFICIAL nodejs.org build (amd64/arm64/s390x/ppc64le)
and the UNOFFICIAL unofficial-builds.nodejs.org build (riscv64, which nodejs.org
has no binary for). release-website.sh only updated the displayed Node.js version
number, and version.sh's local website block did not touch Node.js at all, so those
download URLs went stale (404) after a Node.js bump.
Both now rewrite the version in the nodejs.org/dist/vX.Y.Z and
unofficial-builds.nodejs.org/download/release/vX.Y.Z paths and the
node-vX.Y.Z-linux-<arch> filename to the current Dockerfile NODE_VERSION.
FerretDB/ and sandstorm/ were already ignored; add /mongo-tools/ and /mongosh/ so
that temporarily cloning those wekan repos under this repo for editing never risks
committing them (they are separate git repos).
core24 allows the candidate channel (core26 does not), so the automated
release-all.yml snap job publishes to candidate + beta + edge. The stable channel
is not published by the workflow — it is done manually later once proven. Updated
the workflow, snapcraft.yaml header, CHANGELOG and CPU-platforms.md.
The default snapcraft.yaml is base: core24 (grade: stable) so it CAN be published
to the Snap stable channel, but that is done manually once it is proven stable
enough (core26 cannot go to stable). Revert the automated release-all.yml snap job
to publish to edge + beta channels (was candidate + edge). Update the workflow
header, snapcraft.yaml header, CHANGELOG and CPU-platforms.md to match.
Bundle launchers (releases/ferretdb/start-wekan.sh/.bat): run the bundle's own
per-platform binaries — FerretDB (./ferretdb[.exe]) in the background and WeKan
(./main.js on the bundled ./node[.exe]) in the foreground — inside a restart loop,
so if WeKan exits the whole stack restarts. Ctrl-C / closing the window stops both.
Snap wekan part: the wekan-<arch>.zip bundle is now the single source of every
binary. Handle all snap arches (amd64, arm64, s390x, ppc64el->ppc64le, riscv64);
use the bundle's OWN arch-correct Node.js (fixes riscv64, which nodejs.org has no
build for) instead of re-downloading from nodejs.org; move the bundle's per-arch
MongoDB Database Tools into $SNAP/bin; keep the bundle's ferretdb. Drop the
separate mongotools part (the tools already ship in the bundle), so nothing is
downloaded twice.
Previously the per-arch MongoDB Database Tools were only in the Snap and the
Windows bundle. Now every build embeds them from the wekan/mongo-tools fork's
newest release (per-arch <tool>-<arch>):
- Linux .zip bundles (amd64/arm64/s390x/ppc64le/riscv64) download their arch's 8
tools; the win64 and macOS jobs drop the inherited Linux tools and add their own
(win64 .exe, mac-arm64). The Docker image inherits them from the Linux bundle.
- Sandstorm build-deps.sh adds the amd64 tools (separate from the legacy MongoDB
3.2 migratemongo CLIs used for the one-time migration).
All from wekan/mongo-tools, none from mongodb.com.
Databases / MongoDB clients:
- Remove the mongosh binary everywhere (snap + bundles). All scripted DB access
(readiness ping, replica-set initiate/status, v8.43 schema migration) now runs
through the new snap-src/bin/db-eval (bundled Node.js 24 + the mongodb driver).
Ports wekan-control, mongodb-control, migration-control, start-wekan.sh/.bat and
migrate-schema-v843.js -> .mjs off mongosh. The legacy MongoDB 3.2 mongo shell
(migratemongo, amd64) stays for migration-time reads only.
- MongoDB Database Tools now come from the wekan/mongo-tools fork (pure Go, every
arch) instead of mongodb.com, in snapcraft and the Windows bundle. version.sh /
test-download-urls.sh no longer probe mongodb.com for mongosh/tools. The MongoDB 7
server (mongod) is still fetched from MongoDB (amd64/arm64 only).
- FerretDB v1 downloaded per-arch (ferretdb-<arch>) from wekan/FerretDB, no
ferretdb.zip: release-all.yml bundle jobs, docker-compose.yml, sandstorm build.
Snap:
- Default snapcraft.yaml is now the core24 grade:stable build (core26 variant kept
as snapcraft-core26.yaml); release publishes to candidate + edge.
- Add s390x, ppc64el, riscv64 platforms as FerretDB-only (mongod not built there);
new installs default to FerretDB v1 (snap/hooks/install already guards existing
MongoDB data), DDP_TRANSPORT=sockjs. Fix Caddy ppc64el->ppc64le mapping.
Docs: add docs/Platforms/FOSS/Snap/CPU-platforms.md; update CHANGELOG and the
Sandstorm migration doc.
Add a "Build from source & start (up -d --build)" action to the Docker
menus in rebuild-wekan.sh and rebuild-wekan.bat. It builds the wekan-app
image from the local Dockerfile, tags it as the image the chosen compose
file references (ghcr.io/wekan/wekan:latest), and starts that freshly
built container instead of a possibly-stale prebuilt image pulled from
the registry - useful when a source change (e.g. the FerretDB
Version-page detection) is not in the pulled image yet.
Remove the obsolete top-level "version:" attribute from all docker
compose files, which Docker Compose v2 warns about and ignores:
docker-compose.yml, docker-compose-ferretdb-v2-postgresql.yml,
docker-compose-multitenancy.yml, .devcontainer/docker-compose.yml, and
docs/Databases/ToroDB-PostgreSQL/docker-compose.yml.
Thanks to xet7 !
Rename docker-compose-ferretdb-v1-sqlite.yml -> docker-compose.yml (the
new default) and the old MongoDB docker-compose.yml ->
docker-compose-mongodb-v7.yml, so "docker compose up -d" uses the light,
self-contained FerretDB v1 + embedded SQLite from
https://github.com/wekan/FerretDB by default.
Update rebuild-wekan.sh and rebuild-wekan.bat Docker menus to list
FerretDB v1 SQLite first (default) and point at the new filenames.
Update the compose files' own header comments (self-identification and
cross-references), note the new default at the top of docker-compose.yml,
and update the Docker docs (main Docker.md gets a compose-file/database
map; FerretDB2-PostgreSQL.md points at the v2 postgresql compose file).
Thanks to xet7 !
The Dev server options killed only the app port, but "meteor run" also
starts an rspack dev server on 8080 that can outlive the meteor parent.
A leftover one made restarting a dev server crash with
"Error: listen EADDRINUSE ... :8080". The kill now frees both the app
port and the rspack port 8080 (Meteor.devServerPort || 8080), matching
the rspack watcher by its devServerPort env.
Port detection now checks the listening socket directly (ss/lsof with a
bash /dev/tcp fallback that needs no external tools; netstat on .bat)
instead of an HTTP probe, so it also catches a server that is still
building and not yet serving. Fixes a hang where lsof did reverse-DNS
without -nP.
Add a new "Dev server -> Kill all dev servers" menu option that frees
every dev/test port the scripts use at once: dev app 3000 + Mongo 3001,
Mocha test server 3100 + Mongo 3101, Sandstorm standalone dev 4000 +
Mongo 4001, and rspack 8080. It kills meteor, the rspack watcher and
Meteor's bundled --replSet meteor Mongos, never a production/system
Mongo. Both scripts escalate to SIGKILL if a port does not free up.
Update CHANGELOG and Build-from-source.md docs.
Thanks to xet7 !
Every "Dev server" menu option now stops any Meteor dev server already
listening on its port before starting a fresh one, so re-running a dev
option no longer fails because the port is taken. .sh adds a reusable
kill_meteor_on_port helper called by all six dev options; .bat funnels
them through :runlog, which calls a new :kill_meteor_on_port (netstat +
taskkill). Both wait for the port to free up and escalate to SIGKILL.
Also update the build-from-source docs (README.md, Build-from-source.md,
Build-and-Create-Pull-Request.md, Emoji.md, and the two Sandstorm
developer docs) to the new two-level menu (Setup -> Install dependencies,
Setup -> Build WeKan, Dev server -> localhost:3000), fix a stale
dev-server port (localhost:4000 -> 3000), and fix the README anchor link
to the renamed heading. Add a CHANGELOG entry.
Thanks to xet7 !
Use the Finnish term jyva for Sandstorm grain in the sandstorm-migration
strings: Sandstorm-jyvassa, jyvan hakemistoihin, kayttavat jyvassa, jyvan
levytilan.
Translate the 12 sandstorm-migration-* strings in fi.i18n.json, matching the
existing WeKan Finnish terminology (siirto, Liitetiedostot, Profiilikuvat,
tiedostojärjestelmä, Poista, Tallennustila). The proper noun Sandstorm and the
literal paths/DB names are kept as-is.
Remove the en.i18n.json key sandstorm-migration whose value was the bare
proper noun 'Sandstorm' (the https://sandstorm.org project name), and hardcode
'Sandstorm' in the Admin Panel / Attachments tab label and panel title instead.
The other sandstorm-migration-* keys are real translatable strings and stay.
Replace the long flat menu with a short top-level menu (Setup / Dev server /
Tests / Docker / Tools / Quit); each opens a small submenu with 0) Back, so
only a few items show at once and labels are shorter (category gives context).
Docker becomes a two-stage submenu: pick backend, then Start (up -d) / Follow
logs (logs -f) / Stop (down) -- removing 12 near-identical flat entries.
All action handlers are unchanged; only the front-end dispatch is regrouped:
.sh keeps every case handler and reaches them via a choose() 'short|full' map
(dispatch wrapped in 'for _once in 1'); .bat routes category submenus to the
existing :labels. Verified: .sh bash -n clean + all 23 menu labels map to a
handler; .bat has no orphan gotos.
Expands the docker-compose menu so each backend (MongoDB docker-compose.yml,
FerretDB v1 SQLite, FerretDB v2 PostgreSQL, MongoDB Multitenancy) has three
grouped entries -- Start (up -d), Follow logs (logs -f) and Stop (down) --
instead of just Start. The .sh uses a generalized wekan_docker helper that
auto-detects 'docker compose' vs legacy 'docker-compose'.
New menu entries run 'docker compose -f <file> up -d' for each backend:
MongoDB (docker-compose.yml), FerretDB v1 SQLite, FerretDB v2 PostgreSQL,
and MongoDB Multitenancy. Each prints follow-logs + stop commands. The .sh
auto-detects 'docker compose' vs legacy 'docker-compose'.
The MONGODB_LOG_PATH/TOPOLOGY/SERVER_SELECTION/CONNECTION=debug vars were
temporary mongodb-driver diagnostics that got committed by accident; they
flood wekan-app logs with topology JSON. Remove them from the example compose.
Driver debug logs showed a 2nd, TLS-enabled Mongo monitor connection
retrying every ~0.5s against the plaintext FerretDB port; FerretDB logged
each rejection at WARN. Harmless (WeKan uses the real plaintext connection)
but noisy. Start FerretDB with --log-level=error everywhere it is launched
(Docker entrypoint, snap ferretdb-control, release start-wekan.sh, the
v1-sqlite compose example).
Also remove the dead, unused MongoDB Driver System (server/mongodb-driver-startup.js
and models/lib/{meteorMongoIntegration,mongodbConnectionManager,mongodbDriverManager}.js)
- an abandoned MongoDB 3.0-8.0 auto-detect/driver-select experiment whose
driver packages were never installed. WeKan uses the mongodb-7 driver via Meteor.
gitignore the local FerretDB/ reference clone.
The caddy part fetched Caddy from the Cloudsmith apt repo
(curl dl.cloudsmith.io | gpg --dearmor), which fails on Launchpad remote
builders (restricted network via a fetch proxy): gpg got no key
('no valid OpenPGP data found') and override-build failed with code 2 on
both arches. Download the official prebuilt vanilla Caddy static binary from
GitHub releases instead (per-arch, latest stable + pinned fallback). WeKan
uses only built-in Caddy directives, so no apt repo, gpg, or xcaddy needed.
Applied to both snapcraft.yaml and snapcraft-core24.yaml.
The wekan/FerretDB v1 fork's buildInfo reports a top-level ferretdbVersion
string (e.g. v1.24.2-60-gb5523566) + ferretdbFeatures, and its commit in
gitVersion -- not a ferretdb sub-document. Detection only checked
buildInfo.ferretdb, so the Version page showed Database type: MongoDB and
hid the FerretDB rows. Handle both shapes: top-level ferretdbVersion/
ferretdbFeatures and the ferretdb sub-document. Also confirm in the Sandstorm
doc that FerretDB writes <dir>/wekan.sqlite (+ -shm/-wal).
- start.js: chain niscu (2.x) -> MongoDB 3.0 -> FerretDB; keep niscud + old
bundled driver for the niscu->3.0 stage. Migration from old versions is
permanent (not dropped).
- build-deps.sh (new): assemble a modern meteor-spk.deps on top of upstream
meteor-spk 0.6.0 from dl.sandstorm.io (already has mongod 3.0 + niscud);
swap in Node 24, add FerretDB + Mongo 3.x CLIs + launcher/importer, keep
niscud. Extra binaries from GitHub releases. Nothing needed from projects.7z.
- sandstorm.yml: replace the dead releases.wekan.team/projects.7z download with
build-deps.sh.
- sandstorm-pkgdef.capnp: WRITABLE_PATH -> /var/files.
- docs: keep-niscu + build-source corrections; migration support is permanent.
CJS meteor-spk.deps launcher: on grain start, if old MongoDB 3.0 data exists and
migration is not yet done, start mongod 3.0 (wait for ready), start FerretDB v1
(SQLite), run migrate-mongo3-to-ferretdb (text -> FerretDB, GridFS attachments/
avatars -> /var/files), mark done (old data kept); then run WeKan (Node 24) on
FerretDB steady-state. Paths resolve from __dirname (grain mount root). Update
the design doc to point at the canonical file and mark the in-app panel done.
projects/ (unarchived meteor-spk fork binaries) and sandstorm/ (clone of
sandstorm-io/sandstorm) are large, machine-specific working copies used only
when building the Sandstorm .spk; never commit them.
When isSandstorm, show a new Sandstorm section in Admin Panel / Attachments:
one-time MongoDB 3 -> FerretDB migration status, disk space used by the raw
MongoDB 3 files / FerretDB SQLite / attachments / avatars in the grain, and a
guarded button to delete the redundant raw MongoDB files (only after a
confirmed-successful migration, behind a confirm) to free grain disk space.
- server/methods/sandstormMigration.js: admin-gated sandstormMigrationStatus
+ sandstormDeleteRawMongo (isSandstorm-gated, env-overridable grain paths).
- client Attachments: new Sandstorm tab (jade + js), shown only when isSandstorm.
- imports/i18n/data/en.i18n.json: sandstorm-* UI strings.
- migrate-mongo3-to-ferretdb.mjs: optional STATUS_FILE with the final migration
state, which the panel reads (shared by snap + the Sandstorm spk).
Existing Sandstorm docs live under docs/Platforms/FOSS/Sandstorm/; move the
new Meteor3/Migration.md there (was mistakenly at docs/Platforms/Sandstorm/),
fix its relative links (one level deeper) and the CHANGELOG path.
Add docs/Platforms/Sandstorm/Meteor3/Migration.md: how to build a modern
Sandstorm .spk (Node 24, FerretDB v1 SQLite) migrating an existing grain's
MongoDB 3.0 data on first launch, reusing the snap's migrate-mongo3-to-ferretdb
logic. Includes seccomp compatibility analysis, rewritten start.js, and a new
isSandstorm-only Admin Panel / Attachments / Sandstorm section design.
wekan.help backup/migration instructions hardcoded
/var/snap/wekan/common/files, which is wrong for a parallel snap install
(e.g. wekan_customer -> /var/snap/wekan_customer/common). Print
$SNAP_COMMON/files instead, which snapd sets per instance. Display-only;
all functional snap paths already derive from $SNAP_COMMON/$SNAP_DATA.
The .loadLanguage suite stubbed addResourceBundle on the shared
TAPi18n.i18n singleton and asserted the call count. loadLanguage()
reads this.i18n late and each test re-init()s the singleton, so
cross-test state could leave the stub on a different i18next instance
than the bundle was registered on, making the count read 0 and the run
intermittently report 'expected addResourceBundle to be called once'.
Assert on observable i18next state (hasResourceBundle/getResourceBundle
under the normalised toI18nCode) instead, and stop a leaked
Tracker.autorun in the .getLanguage reactive test.
registerCron() used the synchronous BackupSettings.findOne/upsert, which
Meteor 3 forbids on the server, so cron init threw and scheduled backups
never registered. Use findOneAsync/upsertAsync, make registerCron async,
and await it in all callers. Add tests/backupCron.test.cjs (positive +
negative + source guard) to the test:unit:node suite.
The Meteor/rspack build failed on server/methods/backup.js:
- archiver@8 is now pure ESM with no default export and no archiver('zip', …)
factory — it exports classes. Use `import { ZipArchive } from 'archiver'` and
`new ZipArchive({ zlib: { level: 6 } })` (the streaming API — directory/append/
finalize/pipe/on — is inherited from the Archiver core class).
- The S3 streaming upload requires @aws-sdk/lib-storage, which was not a
dependency (only @aws-sdk/client-s3 was), so rspack failed with
"Can't resolve '@aws-sdk/lib-storage'". Add it, pinned to ~3.1073.0 to match
the installed @aws-sdk/client-s3 (3.1073.0) and avoid the newer lib-storage's
peer requirement on a newer client-s3.
Refresh the Upcoming section: re-point every commit link to the current hash
(the earlier links referenced pre-rebase hashes that are not on GitHub), and add
bullets for the newest commits — Transifex duplicate-key fix, Finnish
translations, unit + negative tests, and the Alternative-Architectures doc.
Document which CPU architectures WeKan can run on and why, the architecture
matrix (Node 24 vs MongoDB vs FerretDB v1), why armhf/armv7l/i386/loong64 are
not supported (Node dropped 32-bit; QEMU/base-image gaps for loong64), the
JavaScript-engine alternatives (Deno/Bun cover fewer arches; QuickJS/JSC/
SpiderMonkey/JVM run on 32-bit but cannot run Meteor), and the server-rewrite
options (Go recommended, QuickJS niche, Tcl/Tk a poor fit for a web app), plus
hardware notes (ODroid-U3, Raspberry Pi OS 32-bit, ReactOS).
Extract the pure, security- and correctness-critical logic of the recent features
into Meteor-free models/lib/* modules (so production and tests share one
implementation) and add plain-Node tests with positive AND negative cases, wired
into test:unit:node:
- models/lib/mongoSelectorSafety.js hasWhere() — the windowed card publication
refuses a client selector carrying $where (server-side JS). Tests: safe
selectors pass; $where at top level / in $and / deep in arrays / in $elemMatch
is rejected. (was inline in server/publications/cardsWindow.js)
- models/lib/cardsLoading.js resolveCardsLoadingMode / parseCardsLoadingEnv /
windowCountId — CARDS_LOADING maps to 'lazy' only for exactly "lazy" (garbage
never enables the experimental mode); a falsy swimlane id collapses to one
count-doc id (listBody uses undefined, listHeader uses ''). (used by
server/cards-loading.js, server/models/settings.js, client/lib/lazyCards.js)
- models/lib/base64Chunk.js encodeAligned/encodeFinal — streaming an attachment
into the JSON export as base64 in any chunk sizes reconstructs exactly the
whole-file base64; aligned pieces never carry padding. (used by
Exporter.buildStream)
- models/lib/backupPaths.js filesRootFrom/scheduleText — WRITABLE_PATH -> files
root (no double /files suffix, Windows-safe) and the synced-cron schedule text
(daily/weekly/monthly + defaults, unknown frequency falls back to daily). (used
by server/methods/backup.js)
41 new assertions across 4 files, all passing; whole test:unit:node suite green.
en.i18n.json had two "database-migration" and two "database-migration-description"
keys, so the Transifex push failed with "Duplicate string key". The later
(block-2) copies — "Database Migration" / "Updating database structure..." — were
unused (only the Admin Panel / Attachments migration UI references these keys, and
it needs the MongoDB<->FerretDB wording in the earlier copy). Removed the unused
duplicates; JSON now parses with unique keys.
In lazy mode minimongo holds only the loaded window, so the list card-counter
badge and the WIP-limit checks (reachedWipLimit / exceededWipLimit, the add-card
gate, and setting the WIP limit to the current count) previously reflected only
the cards loaded so far. They now use lazyListCardCount(), which subscribes to
and reads the server-side count for the SAME selector list.cards(swimlaneId) uses
(listCardsSelector + the board Filter) — so the count is exactly the accurate
value list.cards(swimlaneId).length would report if every card were loaded. The
template's whole-list count uses (cards.length) are routed through a new
cardsCountWhole helper the same way. Default 'all' mode falls back to the loaded
length and is unchanged.
Adds the CARDS_LOADING=all|lazy knob everywhere WeKan is configured:
- docker-compose.yml and the two FerretDB compose files (v1-sqlite, v2-postgresql)
- releases/ferretdb/start-wekan.sh and start-wekan.bat (bundle launchers)
- releases/virtualbox/start-wekan.sh
- snap: added to snap-src/bin/config keys + triplet, so
"snap set wekan cards-loading=lazy" works, with an example in wekan-help.
All default to 'all' (unchanged behaviour).
Client side of CARDS_LOADING=lazy. In lazy mode each list's cardsWithLimit
subscribes to boardCardsWindow for just the cards it renders (growing with the
infinite-scroll limit) and to boardListCardCount for the total; showSpinner uses
that server count to decide whether more cards remain, since minimongo now holds
only the loaded window. Default 'all' mode is unchanged (helpers short-circuit on
isLazyCards()).
Adds Admin Panel / Features — a new admin section (route /admin-features, nav
entry, adminFeatures template) intended as the home for optional / performance /
future tier-gated capabilities. First control: "Card loading" (all | lazy), which
writes the cardsLoading Setting; it mirrors, and is overridden on startup by, the
CARDS_LOADING env var. Includes an experimental-note (counters/WIP limits and the
Calendar/Table/Gantt views reflect only loaded cards; reload boards after change)
and the i18n keys.
Adds an opt-in card-loading mode for very large boards. Default 'all' is
unchanged: the board publication still ships every card into minimongo.
When CARDS_LOADING=lazy (env) or Admin Panel / Features = lazy:
- The board publication no longer ships all cards, their comments/attachments,
or the board-level checklists/checklistItems (gated by a lazyCards() check).
- A new publish-composite 'boardCardsWindow' publishes only the cards of one
list/swimlane window (the client passes the exact selector it renders with,
which is ANDed with a server-forced { boardId, archived:false } scope and
refuses $where), plus each visible card's comments, attachments, checklists
and checklist items. As the user scrolls the client raises the limit and
re-subscribes, streaming more cards in on demand.
- A 'boardListCardCount' publication reactively publishes each window's total
card count into the client-only boardListCardCounts collection, so the client
knows whether more cards remain to load.
The mode is a Settings field (cardsLoading), seeded from the CARDS_LOADING env
var (authoritative on startup when set) and mirrored onto
Meteor.settings.public.cardsLoading for the publications to read; an
after.update hook keeps it in sync when an admin changes it.
The client HTML export cloned the live DOM and built the whole .zip as an
in-memory blob. Two problems on large boards: infinite scroll keeps only ~10
cards per list in the DOM, so most cards were missing from the export; and the
entire archive was buffered in browser RAM.
Before snapshotting, every list's cardlimit is now lifted so the whole board
renders (then we wait for Blaze to flush). The zip is written with JSZip's
generateInternalStream piped straight to the chosen file via the File System
Access API, chunk by chunk with backpressure, so the archive is never held
whole in memory; browsers without the API fall back to the previous blob
download.
The board export routes buffered the entire board in memory before sending:
JSON export built one object with every card, comment, activity, checklist and
base64 attachment; CSV called that same build(); Excel additionally loaded data
it never renders (activities/checklists/subtasks/rules) and did O(n^2)
result.cards.find() lookups. On big boards this peaked at gigabytes and could
exceed V8's max string length.
Now:
- Exporter.buildStream(res) writes the JSON document straight to the response a
document at a time from raw Mongo cursors, encoding each attachment file to
base64 in 3-byte-aligned chunks. Same _format/keys, so import round-trips.
- Exporter.buildCsvStream(res) keeps only the small lookup tables in memory and
streams cards from a cursor, writing one CSV/TSV row at a time.
- ExporterExcel uses the exceljs streaming WorkbookWriter, committing each row
to the response, drops the unused collections, and resolves parent/comment
card titles via an id->title map instead of scanning all cards.
Peak memory stays flat regardless of board size.
Admin Panel / Attachments / Backup no longer loads whole files or the whole
zip into memory. The backup zip is written with archiver, streaming each
attachment/avatar straight from disk (archive.directory) and each text
collection a document at a time from a Mongo cursor as NDJSON, piped directly
to the destination (filesystem or S3/Azure/GCS streaming upload) with no temp
file. Restore reads the zip with unzipper, piping each file entry to disk and
each NDJSON data entry line-by-line in 200-doc batches. A board with thousands
of cards or a 5 GB attachment now backs up/restores with flat memory.
Sandstorm packaging is not tested well enough yet, so it should not run as part of
a full release. Remove the build-sandstorm job from release-all.yml, and add a
separate, manually-triggered sandstorm.yml that ONLY builds the Sandstorm .spk and
uploads it as wekan-sandstorm-YYYY_MM_DD-HH_MM_SS.spk to the newest WeKan GitHub
Release (found via `gh release list`), plus a workflow artifact — so it can be
downloaded and tested for errors without affecting releases. Same experimental
caveats (unprivileged user namespaces, SANDSTORM_KEYRING signing secret).
Thanks to xet7.
Adds a Backup section to Admin Panel / Attachments:
- Checkboxes to select Attachments, Avatars and/or Data (all text-based
collections that are not attachments/avatars).
- Storage dropdown (filesystem, S3/MinIO, Azure, GCS).
- Backup now: the .zip is STREAMED DIRECTLY to the selected storage — no temp
file, no extra disk — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip containing
YYYY_MM_DD-HH_MM_SS/{attachments,avatars,data/<collection>.json}. Filesystem
pipes to the file; S3 uses @aws-sdk/lib-storage streaming Upload, Azure
uploadStream, GCS createWriteStream (cloud creds from the storage tabs).
- Scheduler (off/daily/weekly/monthly + time/day) saved and run via synced-cron.
- List backups: table with storage, datetime, path; select one and Restore with
"Add missing data only" or "Replace all data".
Server methods (admin-only): runBackup, restoreBackup, listBackups,
getBackupSchedule, saveBackupSchedule, backupStatus (polled for live progress).
The cloud upload paths and restore are not exercised end-to-end here; the whole
zip is assembled with jszip, so very large attachment sets use notable memory.
Thanks to xet7.
Adds a "Database migration" section to Admin Panel / Attachments with two buttons:
migrate text-based data (everything except attachments/avatars, which stay on the
filesystem) to FerretDB v1 (SQLite), or back to MongoDB.
WeKan is connected to one database at a time, so the new server methods open a
SECOND driver connection to the OTHER database (both MongoDB and FerretDB v1 speak
the MongoDB wire protocol) and copy the text collections from the live database
into it, upserting by _id (idempotent, re-runnable). The target URL is
WEKAN_FERRETDB_URL (default mongodb://127.0.0.1:27018/wekan) or WEKAN_MONGODB_URL
(default mongodb://127.0.0.1:27019/wekan); both must be running. Progress is polled
via migrateTextDatabaseStatus and shown live. After it finishes, point MONGO_URL at
the other database and restart (on Snap: snap set wekan database=ferretdb / =mongodb).
Admin-only. Attachments/avatars are skipped (they live on the filesystem).
Thanks to xet7.
Per the design: a MongoDB 7 database works with newest WeKan as-is and is NOT
migrated; only the old WeKan 6.09 / MongoDB 3.2 data is migrated to FerretDB v1.
- migration-control now: if mongod 7 can open the existing $SNAP_COMMON data it is
MongoDB 6/7 -> no migration (mark done, keep MongoDB); otherwise it is MongoDB
3.x -> migrate to FerretDB. The dump/restore-into-mongod-7 hop is removed.
- New importer migrate-mongo3-to-ferretdb.mjs reads MongoDB 3.2 with the legacy
mongo CLI (mongoexport — the modern Node driver can't talk to 3.2) and inserts
into a running FerretDB with the Node driver: text data streamed directly, and
CollectionFS/Meteor-Files GridFS attachments+avatars reassembled per-file from
exported chunks straight to files/attachments and files/avatars (no duplicate
copy, no mongodump/mongorestore, no intermediate MongoDB 7). Progress at ROOT_URL.
- FerretDB SQLite now lives at <files>/db, next to attachments/avatars (the
"files/<name>" layout), in ferretdb-control, migration-control, the offline
start-wekan.sh/.bat, the Docker entrypoint and docker-compose-ferretdb-v1-sqlite.
The amd64 6.09/MongoDB-3.2 path is not yet tested against real 6.09 data.
Thanks to xet7.
The bundled Node MongoDB driver cannot connect to a MongoDB 3.2 server, and no
single driver version spans 3.2 and MongoDB 7 / FerretDB. So instead of aliasing
an old (EOL) Node driver into package.json, use the migratemongo CLI tools for the
amd64 6.09/3.2 case: migration-control now dumps the 'wekan' database with the old
migratemongo mongodump (MongoDB 3.2, old libraries), then loads it into a fresh
temporary mongod 7 with the snap's modern mongorestore. The existing driver-based
migrator then reads that mongod 7 exactly like the arm64 MongoDB-7 case (both
GridFS types -> filesystem, text -> FerretDB v1 SQLite). The MongoDB-7 path
(arm64 + amd64-on-newest-WeKan) connects the driver directly, unchanged.
Also fix a real bug: the standalone migrator script in $SNAP/bin could not resolve
its `mongodb`/`bson` imports; set NODE_PATH to the WeKan bundle's node_modules.
Thanks to xet7.
Per docs/Backup/Backup.md and https://github.com/wekan/migratemongo, running the
old MongoDB tools/server in the snap needs LC_ALL=C, the mongo binaries on PATH,
and their libraries on LD_LIBRARY_PATH ($SNAP/lib/<arch>-linux-gnu). The 2016
MongoDB 3.2 binaries additionally need old libraries (libssl/libcrypto.so.1.0.0,
libpng12, libexpat) that modern bases lack.
- Add a migratemongo snapcraft part (both snapcraft.yaml and snapcraft-core24.yaml)
that stages https://github.com/wekan/migratemongo at $SNAP/migratemongo (bin/ =
MongoDB 3.2 binaries, lib/x86_64-linux-gnu/ = the old libraries, avx/ = QEMU
wrappers for non-AVX CPUs), amd64 only. This also fills in the
$SNAP/migratemongo/avx path that mongodb-control / mongodb-backup /
mongodb-restore already referenced but was never bundled.
- Replace the naive fresh-download mongodb-old part (a bare 3.2 mongod with no old
libs) with this.
- migration-control now reads the amd64 6.09 MongoDB 3.2 data with
$SNAP/migratemongo/bin/mongod, LD_LIBRARY_PATH=$SNAP/migratemongo/lib/..., and
the legacy $SNAP/migratemongo/bin/mongo shell (mongosh cannot talk to 3.2). The
MongoDB 7 path (arm64 + amd64-on-newest-WeKan) still uses the snap's mongod 7 +
mongosh.
Thanks to xet7.
On first boot after upgrading an old MongoDB-based WeKan snap, mongodb-control now
hands off to a new migration-control before starting mongod:
- Detects existing MongoDB data in $SNAP_COMMON and opens it with the right
bundled mongod: mongod 7 for MongoDB 7 data (all arches, including the arm64
snaps already on newest WeKan), or a bundled old mongod 3.2 (amd64 only, new
mongodb-old part) for WeKan 6.09 / MongoDB 3.2 data.
- Starts a temporary FerretDB v1 (SQLite) and runs migrate-mongodb-to-ferretdb.mjs,
which migrates text data to FerretDB and BOTH CollectionFS GridFS and
Meteor-Files GridFS attachments+avatars to the filesystem, shows a live progress
counter at ROOT_URL, checkpoints to $SNAP_COMMON (resumable), and compacts the
old MongoDB when done.
- On success switches the snap to database=ferretdb.
Idempotent (migration-pending helper + completion marker), resumable, never
deletes the source MongoDB data, only switches on success. The migrator is bundled
into the snap (/bin). Progress dashboard title fixed to 'MongoDB -> FerretDB'.
NOT yet tested end-to-end against a real 6.09 snap; in particular the 2016 mongod
3.2 binary may need extra runtime libraries on a modern base (amd64 6.09 path).
The arm64/MongoDB-7 path uses the already-bundled mongod 7.
Thanks to xet7.
- docker-compose-ferretdb.yml -> docker-compose-ferretdb-v2-postgresql.yml
(FerretDB 2 + PostgreSQL/DocumentDB, unchanged database).
- Add docker-compose-ferretdb-v1-sqlite.yml: FerretDB v1 with the embedded SQLite
backend, no PostgreSQL/MongoDB. Its ferretdb service fetches the v1 binary for
its architecture from the newest wekan/FerretDB release
(https://github.com/wekan/FerretDB/releases), caches it on the volume, and runs
it with --handler=sqlite.
Both compose files now carry the FULL wekan service from docker-compose.yml
(every documented environment variable and feature); the ONLY differences are
database-related: the ferretdb service replaces the mongodb service, MONGO_URL
points at FerretDB, MONGO_OPLOG_URL is dropped and METEOR_REACTIVITY_ORDER is
polling (FerretDB has no MongoDB change streams / replica-set oplog).
Thanks to xet7.
When upgrading from an old WeKan snap, $SNAP_COMMON/Caddyfile may still be in
Caddy v1 format, which Caddy 2 cannot parse (so caddy would fail to start).
caddy-control now runs migrate-caddyfile-v1-to-v2.sh before 'caddy run' when
caddy is enabled. It is safe: a no-op if the file already parses as Caddy 2;
otherwise it backs up the original, converts the common v1 directives (drop the
http:// scheme, 'proxy / TARGET' -> 'reverse_proxy TARGET', drop the v1
'websocket'/'transparent' presets that Caddy 2 does by default, 'gzip' ->
'encode gzip'), validates the result with 'caddy adapt', and only keeps it if
valid — falling back to the shipped Caddy 2 template otherwise, so caddy always
starts with valid config.
Thanks to xet7.
Two additions to the MongoDB -> FerretDB / GridFS -> filesystem migrator used by
the Snap and Sandstorm migrations:
- Resumable: progress is checkpointed to $WRITABLE_PATH/migration-progress.json
(atomic write) after every collection and file phase. On restart the migrator
loads the checkpoint and SKIPS collections it already finished, so an
interrupted migration (snap refresh, Sandstorm grain restart, power loss)
continues instead of starting over. Safe because every document is upserted.
- Compact after success: once the migration is complete (fresh run, or a later
boot that finds the completion marker), it runs 'compact' on each source
MongoDB collection so WiredTiger returns the now-duplicated disk space to the
OS. Runs once (tracked by a 'compacted' flag on the marker) and is best-effort.
The existing ROOT_URL progress dashboard and disk-space checks (statfsSync) are
kept; the dashboard is restored from the checkpoint on resume.
Thanks to xet7.
Mirrors releases/release-sandstorm.sh + releases/install-sandstorm.sh: installs
Meteor, meteor-spk 0.6.0 (from the WeKan projects.7z) and a dev Sandstorm, then
'meteor-spk pack' produces wekan-<version>.spk and attaches it to the GitHub
Release. Decoupled from the release job so it never blocks the core release.
EXPERIMENTAL in CI: Sandstorm needs unprivileged user namespaces (GitHub's Ubuntu
VMs allow them), and meteor-spk signs the .spk with the app private key from the
Sandstorm keyring -> provide it as the SANDSTORM_KEYRING secret. 'spk publish' to
the app market and the scp upload stay manual, as in release-sandstorm.sh.
Thanks to xet7.
Each wekan-<version>-<arch>.zip is now fully offline: its bundle/ directory
contains the WeKan server, a Node.js binary for that platform, a FerretDB v1
(SQLite) binary, and a start-wekan.sh (start-wekan.bat on Windows) that by
default runs WeKan on the bundled Node against the bundled FerretDB SQLite,
storing data and attachments/avatars on the filesystem under WRITABLE_PATH
(like docs/Platforms/Propietary/Windows/Offline.md). Override with WEKAN_DB /
MONGO_URL to use an external database.
- build-amd64/arm64/mac copy the runner's own Node 24 (matching arch); the
emulated ppc64le/s390x/riscv64 builds copy the target-arch Node they already
download; win64 bundles node.exe. Each overwrites the base bundle's node.
- start-wekan.sh supersedes start-wekan-ferretdb.sh (removed).
- Docker and snap have their own Node + entrypoint/wekan-control, so they strip
the redundant bundled node + launchers (keeping the ferretdb binary), saving
~80 MB per image/snap.
Thanks to xet7.
The main snapcraft.yaml uses base core26, which needs build-base devel + grade
devel until core26 is released, so it can only go to Snap Beta/Edge. This adds a
base core24 variant (a released base, grade stable) that builds the SAME newest
WeKan (Meteor 3.5, Node.js 24, FerretDB v1, MongoDB 7, Caddy 2), so the Stable
channel can be updated from the old 6.09 snap. Only base/build-base/grade differ
from snapcraft.yaml; migration wiring is added separately.
Thanks to xet7.
FerretDB is now built and released separately in the wekan/FerretDB repo (its
build.sh cross-compiles every platform into ferretdb.zip and attaches it to a
release there). The WeKan release workflow no longer sets up Go or compiles
FerretDB: build-ferretdb just downloads the newest ferretdb.zip from
https://github.com/wekan/FerretDB/releases/latest and republishes it as a
workflow artifact, and every WeKan build embeds its per-arch binary from that.
ferretdb.zip is no longer attached to the WeKan releases, since it already lives
on the wekan/FerretDB releases.
Thanks to xet7.
Under a sub-path deployment (ROOT_URL like https://host/wekan, typically behind a
reverse proxy that strips the prefix), language selection and other lazy-loaded
features failed with:
Error serving static file ENOENT ... /programs/server/build-chunks/build-chunks/<id>.js
rspack's client runtime builds each chunk URL as __webpack_public_path__ + chunkName,
and chunkName already includes the 'build-chunks/' prefix (u = e => 'build-chunks/'+e+...).
The default public path is '/', so at the site root chunks resolve correctly to
/build-chunks/<chunk>. But client/00-startup.js set the sub-path public path to
_rootPath + '/build-chunks/', so rspack appended a second 'build-chunks/', yielding
/wekan/build-chunks/build-chunks/<chunk> — which 404s. Set the public path to
_rootPath + '/' and let rspack add 'build-chunks/' itself.
Thanks to xet7.
Adds a build-ferretdb job that cross-compiles FerretDB v1 (wekan/FerretDB, pure-Go
SQLite backend, CGO off, no QEMU) for every platform Go + modernc.org/sqlite
support — not only the arches Node.js runs on — and packs them into ferretdb.zip:
ferretdb/<arch>/ferretdb-<arch> (Linux/macOS/BSD, executable)
ferretdb/<arch>/ferretdb-<arch>.exe (Windows only)
ferretdb/README.md (links to https://github.com/wekan/FerretDB)
Targets that do not compile on a given platform are skipped, so the zip contains
exactly what builds. ferretdb.zip is attached to the GitHub Release, and every
WeKan build (amd64, arm64, ppc64le, s390x, riscv64, win64, mac-arm64; and via the
bundle the docker image and snap) now extracts its FerretDB binary from that same
ferretdb.zip, so there is one source of truth. Windows/macOS bundles now include
FerretDB too (previously stripped), tolerant of an arch that FerretDB can't build.
Thanks to xet7.
getStatistics now reports, in Admin Panel / Version:
- Database type: MongoDB or FerretDB (detected from buildInfo.ferretdb).
- MongoDB-compatible version and the server git commit (buildInfo.gitVersion).
- FerretDB version and commit, when the server is FerretDB.
- Storage engine: WiredTiger for MongoDB, SQLite for FerretDB.
- Reactivity mode currently in use: changeStreams / oplog / polling (from
Meteor's oplog handle; FerretDB has no oplog so it shows polling).
Thanks to xet7.
Adds a 'database' snap setting (mongodb|ferretdb). A new ferretdb service runs
FerretDB v1 (SQLite) on the same port MongoDB would use; only one database runs
at a time. The configure hook flips the two services when the setting changes,
mongodb-control disables itself when ferretdb is selected, and wekan-control
drops MONGO_OPLOG_URL for FerretDB (which has no replica-set oplog, so WeKan
uses polling) and waits on a plain ping instead of rs.status().
The FerretDB binary ships inside the WeKan bundle the wekan part already
downloads, so no extra build is needed. New installs default to FerretDB on all
platforms via the install hook (fresh installs only; upgrades keep MongoDB, and
a pre-existing MongoDB data dir is detected and kept). Switch with
'snap set wekan database=mongodb'.
mongosh collects anonymized usage analytics by default and the snap invokes it
several times; disable it (writable HOME + disableTelemetry) so nothing phones
home. mongod itself has no phone-home telemetry (Free Monitoring is opt-in).
Thanks to xet7.
The extra-arch bundle build ran node:24-slim under QEMU, but the official
node:24 image publishes no linux/s390x manifest (only amd64/arm64/ppc64le),
so the s390x leg failed with 'no matching manifest for linux/s390x'. Build
the emulated native-module rebuild on an ubuntu:26.04 base (which publishes
every arch) and install the Node.js binary from nodejs.org instead; riscv64
uses unofficial-builds.nodejs.org (nodejs.org ships no riscv64).
MongoDB Community only ships amd64/arm64 server binaries, so on ppc64le,
s390x and riscv64 WeKan now bundles FerretDB v1 (the wekan/FerretDB fork with
its embedded pure-Go SQLite backend, MongoDB wire protocol). The FerretDB
binary is cross-compiled once for all five arches (CGO off, static, no QEMU)
and embedded in every .zip next to main.js, so amd64/arm64 users can opt in
too via WEKAN_DB=ferretdb. The Docker image now covers all five arches and
auto-starts FerretDB on the MongoDB-less ones. FerretDB telemetry is disabled
and locked (--telemetry=disable, plus DO_NOT_TRACK). armv7l is still excluded:
no Node.js 24 build exists for it anywhere.
Thanks to xet7.
The swimlanes CRUD e2e test (23-rest-api-more.e2e.js:209) regressed on all
browsers. Root cause: on the server ReactiveCache.getSwimlanes is async, so
board.swimlanes() returns a Promise in the REST handler. The #3624 change read
board.swimlanes().map(s => s.sort); .map on a Promise throws, the insert never
ran, the error was swallowed by the handler's catch (returned as 200 with no
_id), and the test read .title off a null swimlane.
The earlier "move the require to a top-level import" commit was not the real
cause (the same module.exports import pattern works server-side, e.g.
ruleDeletePermission in rulesButton.js). The actual fix is to await
board.swimlanes() before mapping. This also means API-created swimlanes now get
a real max(existing sort)+1 sort — previously the un-awaited `.length` yielded
undefined, so they were stored with no sort at all, which is the #3624 symptom.
Verified: the swimlane test passed in every kept local run through
2026-07-09_02-38-07 and failed starting 09-52-25 (right after the #3624 change),
confirming the regression window; the tests/swimlaneSort.test.cjs unit test
still passes.
Two e2e tests failed across Chromium/Firefox/WebKit after this release's changes:
- 23-rest-api-more.e2e.js:209 (swimlanes: create, get, edit, delete): the
#3624 change referenced models/lib/swimlaneSort via a function-scoped
require('/models/lib/swimlaneSort'); the new module was not reliably bundled/
resolved that way, so POST /api/boards/:id/swimlanes threw, was caught, and
returned an error object with no _id — the test then read title off a null
swimlane. Import nextSwimlaneSort at the module top level, matching every
other import in server/models/swimlanes.js, so it is statically bundled.
- 02-cards-open-view.e2e.js:93 (editing the card title does not break the card):
#4236 intentionally made plain Enter insert a newline in the card title
(consistent with the description), with Ctrl/Cmd+Enter saving. The CardPage
page object still saved with plain Enter, so the title never persisted and the
read timed out. Save with Control+Enter (sends ctrlKey on every platform,
which the save handler accepts).
Server-side Mocha, import regression, and Node E2E jobs already passed; this
makes the Playwright jobs green too.
In an RTL language (e.g. Arabic) the board root is dir="rtl". Both the minicard
title (a dir="auto" .viewer) and the add-card composer textarea are dir="auto"
but had no bidirectional isolation, so they shared the surrounding bidi context.
Typing a strong RTL character into one list's composer re-resolved that shared
context and visibly reflowed the displayed minicard titles of the other lists
(no underlying data change — it reverted on refresh).
Each minicard title and the composer textarea now use `unicode-bidi: isolate`,
making them self-contained bidirectional runs that cannot perturb one another.
The isolation is locked in (positive + negative) by tests/minicardBidiIsolation.test.cjs;
full browser RTL behavior is covered by tests/playwright/specs/18-rtl-layout.e2e.js.
Thanks to xet7.
List bodies used overflow-y:auto, so on platforms/browsers with overlay
scrollbars (macOS, iOS, Android, Firefox) the scrollbar auto-hid when idle and
there was no persistent affordance that a list is scrollable.
The list body now shows a scrollbar across all engines: overflow-y:scroll to
always reserve and show the track, ::-webkit-scrollbar width + thumb/track for
Chrome/Safari/Edge and mobile WebKit/Blink, scrollbar-width + scrollbar-color
for Firefox/Gecko (desktop and Android), and scrollbar-gutter:stable so content
does not shift. The cross-browser rules come from a Meteor-free builder
models/lib/scrollbarCss.js and are applied in client/components/lists/list.css;
both the builder contract and the applied CSS are unit tested (positive +
negative, including that no hide values sneak back in) in
tests/scrollbarCss.test.cjs.
Thanks to xet7.
When a card was copied — including generating a card from a template — each
subtask was inserted as a bare card document. The parent card's checklists were
copied, but the subtasks' checklists (and their items) were never copied, so
the new card's subtasks came out empty.
Card.copy() now copies each subtask's checklists (via Checklist.copy, which also
copies the checklist items) onto the newly inserted subtask. The subtask copy
document is built by a Meteor-free helper models/lib/subtaskCopy.js
(buildCopiedSubtaskFields — re-homes the subtask under the new parent and onto
the destination board; subtaskCopyChildKinds declares 'checklists' must be
reproduced), unit tested (positive + negative) in tests/subtaskCopy.test.cjs.
Thanks to ramses345 and xet7.
POST /api/boards/:boardId/swimlanes set the new swimlane's sort to
board.swimlanes().length (a count). When the existing swimlanes' sort values
were not a contiguous 0..n-1 run (after deletions/reordering, or swimlanes
created top-first with decreasing sorts), the count collided with or preceded
existing sorts and the new swimlane appeared FIRST instead of last.
The endpoint now computes the sort with nextSwimlaneSort(): max(existing sort)+1
for a reliable append (0 on an empty board), and honors an optional explicit
`sort` in the request body (the enhancement the reporter asked for). The pure
logic lives in models/lib/swimlaneSort.js, unit tested (positive + negative) in
tests/swimlaneSort.test.cjs.
Thanks to tamasberesoebb and xet7.
The activity feed rendered "edited comment undefined" / "deleted comment
undefined" for comment edits and deletions. The template passed the comment's
ID into the "%s" of activity-editComment / activity-deleteComment: on older
activities the id was absent (literally "undefined"), and a deleted comment's
CardComment document is already removed so it cannot be looked up live either.
The edit and delete activities now store the comment text (commentText), and a
new Activities.commentDisplayText() helper picks the stored text, else the live
comment text (edit case), else an empty string — so the feed never renders the
word "undefined". The template shows the (sanitized) comment text. The
text-selection logic is a Meteor-free helper models/lib/commentActivity.js,
unit tested (positive + negative) in tests/commentActivity.test.cjs.
Thanks to janchuelo and xet7.
Renaming a linked card (or linked-board card) wrote the new title only to the
linked target (via getRealId()/linkedId), leaving the linking card's OWN title
field stale. Filter-by-title queries each card document's own title field, so a
linked card silently dropped out of title filters after being renamed.
Card.setTitle() now also writes the linking card's own title (in addition to the
linked target), keeping it filterable while preserving the existing mirror
behavior. The set of documents to update is a Meteor-free helper
models/lib/linkedCardTitle.js (computeTitleUpdateTargets), unit tested
(positive + negative) in tests/linkedCardTitle.test.cjs.
Thanks to Ben0it-T and xet7.
Creating a board from a template copies the template board. The copy inherited
the template's subtasksDefaultBoardId / dateSettingsDefaultBoardId verbatim.
When those pointed at the template board itself, subtasks created on the NEW
board were dropped onto the TEMPLATE board, and the subtask card linked back
across boards to the parent.
Board.copy() now repoints any subtasks/date-settings default that referenced
the source board to the new copy, and clears the paired list id so it self-heals
to a list on the new board. Defaults that deliberately point at another (shared)
board are left untouched. The remap logic is a Meteor-free helper
models/lib/boardCopyDefaults.js, unit tested (positive + negative) in
tests/boardCopyDefaults.test.cjs.
Thanks to Xilef11 and xet7.
Security: with "Self Registration" and "Forgot Password" disabled in the Admin
Panel, the Register (.at-signup-link) and Forgot Password (.at-pwd-link) links
were hidden only by a one-shot jQuery .hide() in userFormsLayout.onRendered.
When a login failed (notably the LDAP "User not Found" path), useraccounts
re-rendered the form and recreated those link nodes without the inline
display:none, so the links reappeared and exposed registration / password-reset
flows that were meant to be off.
The disable state is now also reflected as a class on the stable <body>
ancestor (registration-disabled / forgotpwd-disabled) with matching CSS in
userForm.css. Because the class sits on <body>, the rule re-hides every copy of
the links that useraccounts recreates after a failed login, instead of relying
on a single inline style that the re-render dropped.
Thanks to Alsterdetektive1 and xet7.
#4055 reported the calendar/date-badge week number was one week too high
(Mon-Sat) and two too high (Sunday) for 2021-10-25..31 (ISO week 43). That was
the old moment-based calculation. The current native, DST-safe
dateUtils.getISOWeek() computes these correctly (verified across a DST timezone).
Adds tests/isoWeek.test.cjs pinning the exact reported dates so the off-by-one/
two cannot silently return.
Thanks to marcungeschikts and xet7.
The card title textarea submitted on plain Enter (only Shift+Enter made a new
line), while the card description textarea and the shared inlinedForm insert a
new line on plain Enter and only save on Ctrl/Cmd+Enter. So Enter could not add
a new line in the title, an inconsistent experience.
The title keydown handler now uses a shared, Meteor-free rule isSubmitKey()
(models/lib/editorSubmitKey.js): submit ONLY on Enter together with Ctrl or Cmd;
plain Enter and Shift+Enter fall through to the textarea default (a new line).
This matches the description field exactly. Unit tested (positive + negative) in
tests/editorSubmitKey.test.cjs.
Thanks to listenerri and xet7.
With LDAP_UTF8_NAMES_SLUGIFY enabled, sync.js slugified the username with
limax(text, { separator: '.' }) to transliterate UTF-8 names. limax replaces
every run of non-alphanumeric characters — including hyphens — with the
separator, so an LDAP username like "p.parta-partb" was turned into
"p.parta.partb". The generated Wekan username no longer matched and the user
could not log in.
slug() now slugifies each hyphen-separated segment on its own and rejoins
them with '-', so hyphens (and existing dots) survive while UTF-8 characters
in each segment are still transliterated. The hyphen-preserving logic is a
Meteor/limax-free helper packages/wekan-ldap/server/usernameSlug.js
(slugifyPreservingHyphens, with the transliterate function injected), unit
tested (positive + negative) in tests/ldapUsernameSlug.test.cjs.
Thanks to RowhamD and xet7.
In the board Calendar view the week-number column was numbered from Sunday
regardless of the user's "start day of week" setting: setting Monday moved
the grid (via firstDay) but the week numbers still referred to Sunday-start
weeks. FullCalendar's default weekNumberCalculation is locale-based, so it
ignored firstDay.
The calendar now passes weekNumberCalculation that computes the number with
weekNumberByFirstDay(date, firstDay) from the same firstDay used to lay out
the grid, so the number matches the displayed week. The Meteor-free week math
lives in models/lib/weekStart.js and is unit tested (positive + negative,
including a case where Monday-start and Sunday-start must differ) in
tests/weekStart.test.cjs.
Thanks to helioguardabaxo and xet7.
The "Due this week" date filter selected the wrong week (it showed next
week's cards) and ignored the user's configured start day of week.
Root cause: relativeWeek() derived its window from startOf(now(), 'week'),
but the native dateUtils.startOf() has no 'week' case, so it returned the
date unchanged. The window therefore started at "today + startDayOfWeek"
(next week) instead of the beginning of the current week, and never took the
configured start weekday into account.
Fix:
- New Meteor-free helper models/lib/weekStart.js computes startOfWeek(),
weekRange() and weekNumberByFirstDay() for an arbitrary start day of week
(0=Sunday..6=Saturday, default Monday). Unit tested (positive + negative)
in tests/weekStart.test.cjs, including a guard that "this week" must
contain today.
- client/lib/filter.js relativeWeek() now uses weekRange(now(), weekStartDay,
0|1) so "this week" and "next week" are the correct, non-overlapping
Monday..Sunday (or configured) windows.
- The this/next-week buttons now toggle per week: clicking the active one
clears it, clicking the other switches to it (the old code reset on either
state, so you could not switch directly from this week to next week).
Thanks to mimZD and xet7.
Switching directly from one board to another using the favorites bar reused
the same boardBody template instance (currentBoard went A -> B, never null),
so boardBody.onRendered never fired again and the one-shot
Utils.setBackgroundImage() call never re-ran. The previous board's inline
background:url(...) on .board-wrapper stayed on screen. Switching via the
"all boards" list worked only because it destroys and re-creates boardBody.
Two-part fix:
- boardBody.onRendered now calls Utils.setBackgroundImage() inside a reactive
this.autorun(), so it recomputes on every board switch (and background edit).
It performs no DB writes, so there is no autorun write loop.
- setBackgroundImage() now clears any stale inline background when the new
board has no image, instead of only ever setting one. The decision is a
Meteor-free helper models/lib/boardBackground.js (computeBoardBackground),
unit tested (positive + negative) in tests/boardBackground.test.cjs. Board
color keeps being applied reactively via the .board-wrapper colorClass.
Thanks to dasarne and xet7.
The rules move-card-to-top/bottom (on another board) and link-card-to-another-board
actions failed in two ways for boards the rule creator did not own:
- The action showed BLANK after creating the rule: the wizard inserted the
trigger/action/rule with three optimistic client Collection.insert() calls
that landed in minimongo limbo once the wizard subscription stopped (and the
client allow/deny rules rejected them outright for non-owner members). These
now create the rule through the server rules.createRule method, which keeps
the action's own destination boardId.
- Execution crashed with a generic Internal Server Error: the destination
swimlane fallback dereferenced ._id on a possibly-undefined swimlane titled
exactly Default, which does not exist when the destination board's default
swimlane was renamed, translated, or the board had no such swimlane.
Resolution now uses the board's real default swimlane and a Meteor-free
resolver (models/lib/ruleActionResolve.js) that returns empty string instead
of throwing. The same guard is applied to the createCard and linkCard actions.
Unit tested (positive + negative) in tests/ruleActionResolve.test.cjs.
Thanks to DarthKillian and xet7.
Translate the strings that were still in English (mostly keys added this
release: the Rules Workflow trigger/action palette, the relative-date
unit selector, the custom color-wheel label, the Activities hide toggle
and the changed-description activity), using the terminology already used
in the existing Finnish translations.
The Organization setting "Automatically add users with the domain name"
(org.orgAutoAddUsersWithDomainName) was never read at sign-up, so a user whose
email domain matched an organization was never added to it.
Add a pure helper models/lib/orgAutoAddByDomain.js (orgsToAutoAddForEmail) that
returns the ids of orgs whose configured domain exactly matches an email domain
(case-insensitive, exact — no subdomain, empty domain matches nothing), and wire
it into Accounts.onCreateUser so every non-admin sign-up path (password, LDAP,
invitation, new OIDC user) gains the matching org memberships using the same
{ orgId, orgDisplayName } shape used elsewhere, without duplicating existing
memberships. Add tests/orgAutoAddByDomain.test.cjs.
Replace the counterintuitive unlabeled material toggle switch on the card
details Activities panel with an eye / eye-slash icon toggle: open eye means
activities are shown, crossed eye means they are hidden. Clicking flips the
state, the icon reflects the current visibility, and the tooltip states the
action (Show activities / Hide activities). The same eye pattern from the
login/register password-visibility toggle is reused for consistency.
For consistency the board sidebar Activities toggle (same showActivities
state) is switched from the check / empty-square icon to the same eye /
eye-slash icon toggle.
The "Link to this card" popup Cards pull-down previously listed cards in
board sort order, making a card hard to find on boards with many cards.
Sort it alphabetically by title (case-insensitive, locale/numeric aware)
via a small pure helper sortCardsByTitle, with unit tests. Thanks to xet7.
The REST endpoint PUT /api/boards/:boardId/lists/:listId already accepted
title, color, starred and wipLimit, but the color was stored unvalidated. It
now validates the color with normalizeListColor (a named palette color OR a
custom #rrggbb hex) and rejects an unknown color with a clear 400 instead of
silently storing None or hanging. The pure field/validation logic is extracted
to a Meteor-free helper models/lib/listApiUpdate.js so it is unit testable
without a running server (tests/listApiUpdate.test.cjs).
api.py gains two commands mirroring editcard/editcardcolor:
editlist BOARDID LISTID NEWLISTTITLE and
editlistcolor BOARDID LISTID COLOR, with usage lines in the help block.
Thanks to C0rn3j and xet7.
Labels are embedded in the board document, not a separate collection, but the
Activities label() helper looked the label id up in the Cards collection and
always returned undefined. So the addedLabel/removedLabel webhook and
notification params never got a label value and the __label__ token in the
message text rendered as a bare, generic "label" with no name.
Resolve the label from the already-loaded board via getLabelById, and add a
pure labelDisplayName() helper (name -> color -> id fallback) so a nameless
label is shown by its color, matching the UI. Add a plain-Node unit test.
Card description changes did not create an activity, so no outgoing webhook
fired (unlike title/dueAt changes, whose Cards before.update hooks log an
activity). Log an a-changedDescription activity in the Cards before.update hook,
mirroring the title-change activity, so the existing Activities.after.insert
webhook hook fires. Fires on first-time set and later edits, not on no-op /
empty->empty saves. Pure descriptionChanged helper + tests, English i18n and
activity rendering.
The workflow trigger/action palette rendered hardcoded English labels
regardless of the UI language. Palette entries now carry an i18n labelKey
translated at render time via TAPi18n.__, reusing existing rule keys where
they fit and adding new r-w-* keys for workflow-only labels. Also routed
the slot clear tooltip through the existing r-remove key.
Deleting a rule ran three separate client-side Collection.remove() calls
(Rules + Triggers + Actions). Each is gated by a per-collection allow() rule
that resolves the board from that document own boardId via
Boards.findOneAsync(doc.boardId). When a trigger/action document has no
resolvable boardId (legacy rules created before boardId was denormalised, or
docs not published to the client), the board resolves to null and
allowIsBoardAdmin returns false, so Meteor rejects the mutation with 403
"Access denied" and the delete fails even for a legitimate board admin.
Route rule deletion through a new server Meteor method rules.deleteRule, which
authorizes ONCE (active board admin or site admin) and removes the rule, its
trigger and its action server-side (bypassing the brittle client allow/deny).
The permission decision is extracted into the Meteor-free helper
models/lib/ruleDeletePermission.js and covered by
tests/ruleDeletePermission.test.cjs. No permission is loosened: only board
admins and site admins may delete, matching the board-admin gate used to
create a rule.
Add a native color wheel (<input type="color">) alongside the existing fixed
named-color swatches in the color pickers for card labels, swimlanes, lists and
cards, so any #rrggbb color can be chosen. The schema color fields accept a
custom hex in addition to the named palette (via a validation that allows the
named values or a ^#[0-9a-fA-F]{6}$ hex), and normalizeListColor now passes hex
through, so existing named-color data keeps working. When a stored value is a
hex, elements apply an inline background-color instead of the card-details- /
swimlane- / list-header- / minicard- CSS class.
A new pure, Meteor-free helper models/lib/contrastColor.js computes a readable
text color from sRGB relative luminance (white on dark, black on light),
maps the named palette to hex, and validates/normalizes hex; it is applied as
an inline text color wherever a chosen color is a background behind text (label
chips, swimlane/list headers, minicard, card details header). Covered by
tests/contrastColor.test.cjs (20 assertions).
PUT /api/boards/:boardId/labels only wrote an HTTP response inside
`if (hasOwnProperty(req.body, "label"))` and read req.body.label.color/.name
unconditionally. Two consequences:
- A body without a `label` key fell through the `if` and NEVER sent a response,
so the request hung until the client timed out (the reported "timeout").
- A body where `label` was a bare string (e.g. { "label": "labelName" }, the
exact reported payload) produced name === undefined / color === undefined and
pushed a schema-invalid label via Boards.direct, returning 200 for garbage.
- The catch returned { code: 200, data: error }, masking real failures as
success (same anti-pattern already fixed for the board create/delete handlers).
The handler now returns a 404 for an unknown board, delegates input handling to
a new pure, Meteor-free helper models/lib/restLabel.js (buildBoardLabel), and
always sends a JSON response: 2xx for a well-formed create (documented contract
unchanged), 400 with a clear message for bad input, or the real error status in
the catch. buildBoardLabel accepts { label: { name, color } } (color defaulted
when absent, rejected when unknown) and, for convenience, a bare string taken as
the label name. Added tests/restLabel.test.cjs covering these cases.
Exporter.buildCsv resolved each card list/swimlane/owner/member/assignee/label/
customField by id and read a property directly on the result of Array.find(),
which is undefined when the referenced document was deleted. Large/old boards
often contain such dangling references, so the read threw "Cannot read property
title of undefined" and crashed the whole CSV export request. Extracted the
per-card row builder into a pure, null-safe helper models/lib/exporterCsvRow.js
(unit-testable without Meteor) that emits a blank cell for a missing reference
while keeping identical output for well-formed cards. Added
tests/exporterCsvRow.test.cjs.
The Calendar view queried cards with only a board+date selector via
Board.cardsInInterval()/cardsDueInBetween() and ignored the active board
Filter (member / assignee / due-date / label / custom-field), so it showed
every card in the interval regardless of the sidebar filter, unlike the
Board / Lists / Swimlanes views.
Extract the calendar selector building into a pure, Meteor-free helper
(models/lib/calendarFilter.js) that ANDs the Filter selector into the
date-interval selector via combineWithFilter, so the Filter is always applied
and can never collide with the interval selectors top-level $or. The calendar
events() callback now passes Filter._getMongoSelector() when Filter.isActive(),
and the onRendered autorun reads Filter.isActive() so events are refetched when
the filter changes. Add tests/calendarFilter.test.cjs.
Thanks to kerier and xet7.
Follow-up to #6439, which restored the drag preview but left the drop a
no-op: dragging a board on the All Boards page in "Custom (drag order)"
mode showed the dashed-border preview, but releasing it snapped the board
back without reordering.
The drop handler built the current on-screen order from
`el.classList[0]` for each `.js-board`. But the board list item is
`li.js-board(class="{{_id}} …")`, and Jade emits the literal `js-board`
class FIRST, so classList[0] is the string "js-board" for every board —
never the board _id. The ordered ids were therefore
['js-board','js-board',…]; computeReorderedSortIndex could not find the
dragged/target ids in that array, returned null, and nothing was written
to profile.boardSortIndex. The preview still worked because it is driven
by the dragover CSS class, independent of the id.
Read each board's _id from its Blaze data context (the same source
dragstart already uses via `this._id`) instead of the literal class, so
the real display order reaches the (already correct, unit-tested) reorder
helper and the drop persists. Add a regression guard in
tests/boardSortReorder.test.cjs asserting the wrong-class extraction
("js-board" repeated) yields no mapping while real ids reorder.
On old boards a swimlane can be deleted while its cards keep the now
dangling swimlaneId (an "orphaned" card). Cards with no swimlane at all
(null / '' / missing) already surface in every swimlane, but an orphaned
card matched no existing swimlane and so appeared in NO swimlane in
swimlane view, while still showing in list view (which applies no
swimlane scope) — the reported "no swimlane" symptom where some boards
show no cards in swimlane mode but work in list mode.
Mirror the existing orphaned-LIST fallback (Swimlanes.orphanedSwimlaneLists,
which surfaces orphaned lists in the first swimlane) for cards: when a
list's cards are fetched for the board's FIRST swimlane, build the
swimlane-membership clause as a single { swimlaneId: { $nin:
<otherSwimlaneIds> } } (everything not owned by another existing
swimlane: own id, null/'', missing, or orphaned). This keeps it a single
field clause with no second $or, so the #6441 board-wide label filter
still holds. Orphaned cards therefore appear once, in the first swimlane,
without a database migration.
Threaded through the pure selector helpers (swimlaneMembershipSelector /
listCardsSelector / filteredListCardsSelector) and the in-memory sibling
filterCardsByListAndSwimlane, a new List.orphanedCardsSwimlaneIds helper,
and the cards()/cardsUnfiltered()/allCards() model methods plus the
listBody cardsWithLimit render helper. Covered by new cases in
tests/swimlaneFilter.test.cjs (18 assertions pass; #6441 guards intact).
The .loadLanguage suite intermittently reported "1 failing" (the failure bouncing between the zh-CN and fr cases). Two causes: stubbing the shared TAPi18n.i18n let state leak between tests, and asserting via sinon-chai routed the matcher through chai-as-promised so it could evaluate after afterEach's sinon.restore(). Give each test a fresh i18next instance via TAPi18n.init() (mirrors the reliable .setLanguage suite) and assert with synchronous sinon.assert.* instead of sinon-chai.
Thanks to xet7.
Registering a new account showed a red "Internal server error" on the sign-up form even
though the account was created and could sign in — which typically happens when SMTP is not
configured. Root cause: useraccounts' `ATCreateUserServer` creates the account and then
calls `Accounts.sendVerificationEmail()` (because `sendVerificationEmail: true`); when SMTP
is missing/misconfigured that send throws **after** the user row is inserted, and with no
try/catch the exception leaves the createUser method as an opaque HTTP 500. The verification
email is best-effort at sign-up, so `Accounts.sendVerificationEmail` is now wrapped to log
and swallow a transport failure — registration completes and redirects to sign-in — while an
"already verified" error is re-thrown so the resend-verification flow still reports it. This
mirrors the [#5706](https://github.com/wekan/wekan/issues/5706) reset-password hardening.
Covered by `tests/verificationEmail.test.cjs`.
Thanks to Firas-Git and xet7.
Fixes#5672
A **linked card** (created by "Link card to this card") is only a placeholder on the board
that links it — its members are stored on the **real card** it points at (`linkedId`), which
lives on another board, and `Card.getMembers()` / `assignMember()` / `unassignMember()`
already read and write that real card. But the member **picker** listed the members of the
board you were *viewing* the linked card on, not the board the real card lives on. So on a
board that links a card from another board, the picker offered the wrong set of members and
toggling them did not behave as a consistent add/remove — "can not add members to the linked
card". The picker now resolves a linked card to its real card's board and offers *that*
board's active members (matching where the membership is actually stored), falling back to
the current board for normal cards or when the real card isn't loaded yet. Extracted the
target-card/target-board resolution into `models/lib/linkedCardMembers.js`, covered by
`tests/linkedCardMembers.test.cjs`.
Thanks to ITT5 and xet7 !
Fixes#5676
Searching a board for the value of a **number** or **currency** custom field (e.g. a
transaction number `2025001`, or a currency amount `123`) found nothing. Those field types
store their value as a JS **Number** (the inputs save `parseInt(...)` / `Number(...)`), but
`Board.searchCards()` only matched custom fields with `{ value: <regex> }` — and a MongoDB /
Minimongo regex only matches **string** values, so it silently skipped every numeric custom
field. The search now also adds an exact numeric-equality clause when the term is a plain
number (a comma is accepted as a decimal separator, matching the currency input), so numeric
custom fields match too; text/title/description matching is unchanged. Card search runs
against Minimongo on the client, which — like MongoDB — cannot regex a numeric field, so
equality is the correct cross-environment match. Covered by `tests/cardSearch.test.cjs`.
Thanks to MarcusDger and xet7.
Fixes#5680
The scheduled notification-tray cleanup logged `Exception in removed observeChanges
callback: Error: Removed nonexistent document …`. The crash itself came from the old
`cottz:publish-relations` package (since replaced by `reywood:publish-composite`), but the
cleanup that provoked it still fired one **un-awaited** `removeNotification()` per expired
notification — a separate `Users.update` `$pull` each time — so a user with K stale
notifications produced K writes to the Users collection, and every publication that
republishes user documents re-ran its observers K times in quick bursts (the churn that
surfaced the removed-document error), while any rejected write went unhandled. The cleanup
now scans only users that have notifications and prunes each user's stale notifications in a
single awaited `$pull … $in`. It also removes an activity's notifications only when *every*
entry for that activity is read and past its removal age (so a freshly re-created unread
notification sharing an activity id is not dropped), and guards missing/invalid `read`
timestamps. Covered by `tests/notificationCleanup.test.cjs`.
Thanks to xet7.
Fixes#5685
In the IFTTT-Rules "Move card to the board" and "Link card to the board" actions, the
board dropdown was empty for some users — the current board included — while colleagues
in the same company could pick boards normally. Root cause: the dropdown filtered the
(already access-scoped) client cache with `'members.userId': me`, i.e. it only kept boards
where the user has a *direct* member entry. A user who reaches a board through an
Organization, Team or email-domain share — but is not listed individually in
`board.members` — matched nothing, so the whole selector came up empty. The dropdown now
filters by the same visibility rule as `Boards.userBoards()` (public OR active member OR
active org OR active team OR active domain), so org/team/domain-shared boards appear too,
while archived boards, template containers, the user's templates board and internal helper
boards stay excluded.
Thanks to Augustin356 and xet7 !
Fixes#5698
A board admin adding another user could make the whole board silently vanish from a
user's board list, with no archive and "nothing out of order" in the logs. Root cause:
the `setBoardTeams` server method (used by the board Teams/Members management popups)
blindly overwrote the board's entire `members` array with a snapshot sent by the client.
When that client's board document was stale — e.g. a member had just been added via
`inviteUserToBoard` on the server and the change had not yet propagated to the client —
the overwrite dropped members, and in the worst case the board's own admin, so the board
no longer matched the board-list publication (which requires an active membership) and
disappeared. Because a wholesale `$set: { members }` never passes through
`foreachRemovedMember()`, no `removeBoardMember` activity was logged and no
card/watcher/star cleanup ran, which is why the logs looked normal. `setBoardTeams` now
reconciles against the authoritative server-side members instead of trusting the client
snapshot: it never drops an active admin, keeps every existing member the client still
lists, adds the members the client introduces, and only removes non-admin members the
client explicitly omitted (an intentional team-leave) — logging and cleaning up each such
removal so it is auditable rather than silent.
Thanks to DVNBLMHC and xet7 !
Fixes#5730
The Snap Store intermittently rejects `sudo snap install snapcraft --classic`
with "error: cannot install snapcraft: too many requests" (a 429-style rate
limit) that instantly failed the whole snap job before any build ran. Retry up
to 5 times with 30s backoff so one transient store throttle no longer fails the
release, matching the remote-build retry already in the same job.
The client board cache (imports/lib/dataCache.js) re-fetches its value inside a
reactive computation. When the board doc is briefly absent from minimongo (a
subscription stops and restarts, momentarily removing the doc), the empty value
was surfaced immediately, flashing the "Board not found" shell — and on WebKit
throwing a Blaze "Can't select in removed DomRange" error tearing down the card
view. It self-recovers when the doc is re-delivered, so it presents as a flicker,
reliably hit only on Firefox/WebKit timing (which is why it surfaced as
browser-specific Playwright failures in 14-voting-watchers and 24-feature-issues).
Fixed with an opt-in stale-while-revalidate mode on DataCache, enabled only for
getBoard: a transient miss over an already-cached board keeps the last value and
re-checks after a short delay, surfacing an empty result only if the board is
still gone then (a genuine deletion / access loss). First-ever loads and caches
that did not opt in are unchanged. Core decision extracted to the pure
shouldDeferCacheMiss helper with unit tests.
Also hardens the related Playwright specs: a retry in CardPage.openActionsMenu, a
longer visibility budget for the WebKit slow card-render in spec 16, and removes a
reload race in the move-list-right spec (moveListBy fires two un-awaited
updateListSort calls, so an immediate reload read a half-applied order).
The "Link to this card" target picker only excluded template cards, so an
existing linked card (or a card that links back to the current board) could be
chosen as a link target. That builds a chain of linkedId pointers, but the card
helpers (getTitle/getBoardTitle/getRealId) resolve linkedId only one hop, so
such a card renders as an empty/broken pointer and becomes inaccessible (the
reported freeze). As the reporter suggested, prevent the configuration: only a
real card — not a linked card/linked board, not one of the linking board's own
cards, and not a card that links back to one of them — may be a link target.
Enforced in the picker query and re-checked at creation time via the pure
isLinkableCardTarget guard, mirroring the #3328 parent/subtask cycle guard.
Two related problems with the default login authentication method.
- **Env var ignored:** the stored setting was only ever seeded as `password` and
`DEFAULT_AUTHENTICATION_METHOD` was never applied, so operators setting it (e.g. Kubernetes/Helm
`DEFAULT_AUTHENTICATION_METHOD: ldap`) saw no effect. Startup now applies the env var
authoritatively: it seeds the value on a fresh install and, on existing installs, keeps the
stored `defaultAuthenticationMethod` in sync with the env var on every boot (the operator's env
is the source of truth), so the method can be configured entirely by env without the Admin Panel.
The value is normalized (trimmed + lower-cased), so `DEFAULT_AUTHENTICATION_METHOD=LDAP` works.
- **Layout save hanging / not persisting:** the authentication-method `<select>` is populated by an
async `Meteor.call`, so clicking **Admin Panel > Layout > Save** before it loaded sent an empty
value for the **required** `defaultAuthenticationMethod` field, which silently failed validation —
the save looked stuck and nothing changed. The save now falls back to the currently stored method
when the select is empty, so a real value is never overwritten by `''`.
Both paths share one pure helper (`resolveDefaultAuthenticationMethod`) that never resolves to an
empty string.
Thanks to joe-speedboat and xet7 !
Fixes#5879
`Card.move()` wrote `boardId`/`swimlaneId`/`listId` into the update unconditionally, even for a
same-board drag to another list (the reported case). Keeping `boardId` in the `$set` on every drag
re-ran the `boardId`-gated `Cards.after.update` hook that re-syncs the card's checklists and
checklist items via `multi` updates, ran the cross-board consistency guard and the
`denyCrossBoardMove` deny-rule DB lookup, and invalidated more reactive dependents than
necessary — server work and reactivity churn that contributed to a ~1s card flicker on large
boards. `Card.move()` now writes **only the fields that actually change** (via the pure
`computeCardMoveModifier` helper) and skips the write entirely when a card is dropped back in the
same place, so a same-board move no longer touches boardId or the cross-board hooks. The
`moveCard`/`moveCardBoard` activity generators already re-check `doc.X !== oldX`, so trimming the
modifier does not drop any activity. Note: this reduces the drag-time work behind the flicker; the
residual reactive re-render cost on very large boards is a separate performance topic.
Thanks to mimZD and xet7 !
Fixes#6430
`Board.getDefaultSwimline()`/`getDefaultSwimlineAsync()` self-heal a missing default swimlane by
reading the board's swimlanes and inserting one if none exist. That check-then-insert is a race:
concurrent or repeated server calls for a swimlane-less board each saw zero swimlanes and each
inserted a new one, so some boards accumulated 30 000+ empty "Default" swimlanes and became
unloadable (the `key 'default (en)' returned an object instead of string` log is a harmless i18n
side effect — the title correctly falls back to the string `Default`). Follow-up to the
client-side [#6382](https://github.com/wekan/wekan/issues/6382) fix, which only stopped the
browser from auto-creating them. Fixed by making the server self-heal **idempotent**: the default
swimlane is now upserted with a deterministic `_id` (`<boardId>-default`), so the `_id` unique
index guarantees at most one default swimlane per board no matter how many times, or how
concurrently, the getters run. `archived`/`type` are set explicitly in the `$setOnInsert` because
their schema autoValue/defaultValue only fire on insert, not upsert. Note: this prevents new
duplicates; boards that already accumulated thousands still need a one-off cleanup.
Thanks to brlin-tw and xet7 !
Fixes#6429
In `.github/workflows/release-all.yml` the `charts` job now depends on the `docker` job
(`needs: docker`) instead of running in parallel right after `bump`. GitHub Actions runs a job
only when all of its `needs` jobs succeed, so the wekan/charts Helm chart is published only after
the multi-arch image is live on Docker Hub / Quay.io / GHCR (and is skipped if `docker` fails).
This prevents ArtifactHub from scanning a freshly published chart whose image tag does not exist
yet and emailing the maintainer about the missing Docker image.
Thanks to xet7 !
`/b/undefined/board/<cardId>` instead of the real board.** On the server
`ReactiveCache.getBoard()` is async and returns a Promise, but `Cards.board()` did not await it,
so the synchronous `Card.originRelativeUrl()`/`absoluteUrl()` interpolated a Promise —
`board._id` and `board.slug` were `undefined`, producing `/b/undefined/board/<cardId>` in
card activity notification emails (the client UI was unaffected because `this.board()` is
synchronous there). Fixed by making `Card.originRelativeUrl(board)`/`absoluteUrl(board)` accept an
already-resolved board and fall back to `this.boardId` (always available synchronously) when the
board is a Promise, and by passing the awaited board from `server/models/activities.js` so the
correct board id and slug are used.
Thanks to titver968 and xet7 !
Fixes#6427
— shell injection (RCE) via a malicious upload filename in the external antivirus scanner command path**
([GHSA-x3xm-pxrv-jg7p](https://github.com/wekan/wekan/security/advisories/GHSA-x3xm-pxrv-jg7p),
CWE-78 OS Command Injection). Same RCE class as
[AvatarBleed](https://wekan.fi/hall-of-fame/avatarbleed/) (CVE-2026-52891, GHSA-35j7-h385-2q9g)
and its follow-up regression (CVE-2026-53447 / GHSA-qfqv-42qw-vvwh area), but in a code path that
was **never** covered by those fixes.
In `models/fileValidation.js`, when an admin has configured an external scanner (antivirus)
command line with a `{file}` placeholder, the uploaded file path was interpolated into the command
and run through `asyncExec` (`promisify(exec)`), which spawns `/bin/sh -c` and interprets **all**
shell metacharacters:
```js
await asyncExec(externalCommandLine.replace("{file}", '"' + fileObj.path + '"'));
```
Wrapping the path in double quotes is not a shell boundary — inside double quotes the shell still
expands `$(...)`, backticks and `\`, so a filename such as `` a`id`.png `` or `$(touch /tmp/pwn).png`
escaped the argument and executed as the Wekan server process. Any **authenticated** user who can
upload an attachment could trigger it, on servers that have an external scanner configured. Unlike
the sibling MIME-detection path (`detectMimeFromFile`, which already uses `execFile` with no shell)
and unlike the AvatarBleed fix (which strips non-alphanumeric characters from the filename), this
scanner path had **zero** sanitization.
- **Fixed** by POSIX single-quote-escaping the interpolated file path via a new `shellQuote()`
helper (wrap in single quotes, escape embedded `'` as `'\''`). Inside single quotes the shell
interprets no metacharacters, so a malicious filename can no longer break out of the argument or
inject commands, while the admin's arbitrary command line and the exact on-disk path are both
preserved. CVSS:3.1 9.9 Critical (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H).
- Affected Wekan v9.06 and earlier through the current release; fixed at the upcoming WeKan release.
Reported by **DavidCarliez**.
Thanks to DavidCarliez and xet7 !
- imports/i18n/i18n.test.js crashed the whole run at load: chai 6.x plugins (sinon-chai,
chai-as-promised) are ESM-only, so use(require('sinon-chai')) handed chai.use() a module
namespace instead of the plugin function ("fn is not a function"). Now imports the default
export.
- That unmasked a second load crash - PositionHistory.helpers is not a function: the
meteor test entry (server/lib/tests/index.js) never ran the .helpers / .attachSchema shim
that server/main.js bootstraps, so the first model to call Collection.helpers({...}) threw.
The shim is now imported first in the test entry.
- With the suite finally running, 44 server tests failed because meteor test only loads what
the specs import (not the app's /server/imports). The specs now import the files that
register the methods / globals under test (cards.vote / cards.pokerVote, api.attachment.*,
cloneBoard, getBackgroundImageURL, applyListWidth, updateListSort, moveChecklist,
userPositionHistory.*, archiveBoard, sendSMTPTestEmail, and the Attachments global). Also
fixed genuine test bugs: the cards.vote / cards.pokerVote specs were written synchronously
against async methods; the header-login trust specs restored env vars with
process.env.X = undefined (which stores the string "undefined" and shadowed the trusted-IP
allowlist, making every trusted source read as untrusted); the DnsBleed decimal-loopback
matcher assumed the integer host survived URL normalisation; and the dependencies-OpenAPI
spec could not locate its source file from the built bundle. The two cards.archive /
cards.move specs tested Meteor methods that do not exist (archive / move are Minimongo
document helpers secured by Cards.allow / Cards.deny, already covered by the cards security
tests) and were removed. Server-side Mocha is now 409 passing, 0 failing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Chromium / Firefox / WebKit browser jobs run as separate processes against ONE
shared server + DB, so specs that used fixed identifiers or global cleanups raced
each other:
- 26-shared-templates.e2e.js seeded a fixed email domain (usera@acme-e2e.invalid) in
all three browsers, hitting E11000 duplicate key on the unique emails.address index.
Now uses a unique-per-run token for the org / team / domain and template titles.
- 38-impersonation.e2e.js cleaned up with a global
deleteMany({ reason: 'clickedImpersonate' }) that deleted another browser's in-flight
audit record mid-poll. Scoped the cleanup to the test's own adminId.
- 32-org-team-feature-toggles.e2e.js exercises setAllOrgsFeature / setAllTeamsFeature,
which do a global updateMany({}) across every org / team, so two browsers clobbered
each other's rows. These browser-agnostic server-method tests now run in a single
project (Chromium); Firefox / WebKit skip them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A git pull --rebase inserted upstream commit 17c62ccdf (Bump versions for
v9.72) under this session's work, rewriting its commit hashes. The CHANGELOG
still linked the pre-rebase hashes (37ac65e37, a4cce9a53), now orphaned and
not on main. Repoint to the rebased commits 219ee659e and 88f353dc1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On phones body.mobile-mode pins <body> to position:fixed + height:100vh (an
iOS board-view anti-bounce fix). The auth pages (userFormsLayout) have no
inner #content scroller, and .auth-layout/.auth-dialog were sized to the
viewport height, so the page was trapped at 100vh: the content below the
form — the sign-up/sign-in link, legal notice and language selector — could
not be scrolled into view, and the on-screen keyboard covered it.
userFormsLayout now tags <body> with .userform-layout while mounted, and CSS
lets those pages scroll as a normal document on mobile (position:static,
height:auto, min-height:100vh) with the auth layout/dialog growing to their
content. Scoped to .userform-layout so the board-view mobile lock is
unchanged. Removed on destroy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Page drag-to-scroll was wired into only boardsList (All Boards), myCards and
userFormsLayout (Login/Register). Every other defaultLayout page — Due Cards,
Global Search, Public, Bookmarks, Broken Cards, Settings, People, Admin
Reports, Attachments, Translation, Import, board Rules, Not Found — had no
page drag-scroll, and the per-template enable/disable calls fought each other
when navigating between two non-board pages (one template's onDestroyed
removed the class the next template's onRendered expected).
Replace them with a single route-aware autorun in defaultLayout (the
persistent shell that owns #content): non-board routes tag the real scroll
container (<body> + #content, so it works in both desktop and mobile mode),
and board-canvas routes (board/board-short/card) leave page tagging off so
their own .board-canvas.dragscroll handles dragging without double-scrolling
#content. Login/Register keep their own enable/disable (userFormsLayout has
no #content).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#6426: the first revert only cleaned boardsList.css. The same forced
"2x/3x bigger on mobile" rules remained in header.css (#header-quick-access
logo/quick-access bar) and boardBody.css (#content — which the All Boards
list and the board view render inside) plus the iPhone card-details body
text. All neutralized to normal size; the iPhone-12-mini header blocks that
had grown tall/wrapped to fit 3x text are back to a compact single row. The
#6419 mobile touch-target icon sizes in the card-details header are kept.
dragscroll: enablePageDragscroll/disablePageDragscroll tagged only <body>,
but on phones body.mobile-mode is position:fixed and the real scroll
container becomes #content, so Login/Register/All Boards could not drag-
scroll. Now tag whichever element actually scrolls — both <body> and
#content when present — so only the overflowing one scrolls. The touch
helper already prefers the nearer #content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
json.loads('') on an empty settings argument raised JSONDecodeError before the
request was sent (a bug api.py's own header flagged). Default empty settings to
{} and send it as valid JSON via json.dumps instead of str(dict) (which produced
invalid JSON with single quotes). Verified: both empty and object settings create
the custom field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate the 4 newly-added fixed-list-width keys in fi.i18n.json to Finnish,
matching existing terminology (Lista/listoille, leveys, 'vain sinua varten'
as in list-width-personal-note). The remaining strings identical to English
are technical terms, abbreviations and placeholder-only templates (poker
numbers, LDAP/OAuth2/CAS/OK, Gantt, S3/GridFS, operator abbrevs, __board__)
that are correctly the same in Finnish.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On iPhone 12 mini the right board sidebar showed as a narrow strip pinned to
the far right. The sidebar lives inside .board-wrapper, which is wider than
the viewport on a phone, so the absolutely-positioned sidebar
(inset-inline-end: 0) anchored to the right edge of the wide board instead of
the screen. Under the @media (max-width: 800px) breakpoint (which matches the
viewport in both mobile and desktop mode), pin the sidebar to the VIEWPORT
with position: fixed + 100vw so it is a full-width panel at the top of the
board. Also normalise the sidebar text sizes so the headings are not
oversized and small text not tiny, reading consistently with the board.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swimlane header buttons (collapse, swimlane menu, + new swimlane, drag
handle) hugged the top of the swimlane header on small screens (e.g. iPhone
12 mini). The menu was absolutely positioned at top:0 and each icon used a
top: calc(50% + Npx) offset that centered against the menu's own small
height, not the header. Stretch .swimlane-header-menu to the full header
height and flex-center its children vertically (align-items: center), and
drop the now-unnecessary per-icon top offsets, so the buttons sit in the
middle of the swimlane header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs every plain-Node (non-browser) test in one command: builds on
test:unit:node and adds the sticker, link-card-popup (#5715), Trello/Wekan
importer and oauth2 admin-group suites. All green.
Browser-driven suites (Meteor client mocha + Playwright e2e) are not
included here — they need a running app + a working browser, which an x86
Chromium cannot provide on an ARM host under qemu; run those on x86 CI.
rtl.test.js is intentionally excluded (2 pre-existing CSS lint failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The custom tap:i18n reimplementation registered each language's resource
bundle under the raw Wekan tag, but i18next (cleanCode: true) resolves and
looks translations up under a normalised code. For region/script-tagged and
legacy underscore tags (zh-CN, zh-Hans, ar-DZ, af_ZA, en_AU, ...) the stored
code and the lookup code disagreed, so the bundle was never found and the UI
silently fell back to English while simple tags (de, fr) kept working.
Normalise every i18next boundary (supportedLngs, addResourceBundle,
changeLanguage, t) through one helper (toI18nCode): underscores -> hyphens
and i18next's formatLanguageCode, so storage and lookup always agree.
Safely unwrap ES-module-namespace JSON imports without mistaking the data's
own "default" translation key for a module default export. Surface failed
language switches in the user header / login form instead of leaving the UI
silently English. Add positive and negative regression tests.
Closes#5756
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forgot Password -> "Email reset link" could return a raw HTTP 500 instead of
sending the email / showing success (#5706). When SMTP is unconfigured (no
MAIL_URL / MAIL_FROM, or a bad mail server), Meteor's Email.sendAsync throws and
the exception propagated unhandled out of the forgotPassword method as an opaque
500.
Harden the reset-password / verify-email / enroll-account email-template builders
so they never throw (guarded user name/language lookups, safe fallback if
translation throws), and wrap Accounts.sendResetPasswordEmail so a send failure
surfaces as a clean Meteor.Error('email-fail', ...) instead of a 500. Logic
extracted to server/lib/resetPasswordEmail.js with positive + negative unit tests
in tests/unit/resetPasswordEmail.test.js (wired into the test:unit:node script).
Note: this makes the failure graceful; receiving reset emails still requires SMTP
to be configured.
Closes#5706
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The link-card popup (linkCardPopup in client/components/lists/listBody.js)
used to auto-populate the List/Card sub-selects when a board was chosen,
leaving no way to clear them. Once the chosen board had cards, you could
only link to a card and never to the whole board.
The Swimlane/List/Card selects now default to a blank "(none)" option and
are not auto-selected on board change (this part was already merged from
seve12 / PR #5967). In addition, the popup's confirm ("link") button now
falls back to creating a board-level link when a board is selected but the
Card field is left blank, so a whole board can be linked with the single
bottom button regardless of whether the board has cards. The duplicate
board-link guard is preserved and a blank selection never creates a
phantom/undefined linked card.
Added tests/linkCardPopup.test.js (standalone Node, like
tests/cards.stickers.test.js) covering positive and negative cases.
Closes#5715
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In locales such as Persian/Farsi or Arabic, dates and times can be rendered
with non-Latin digits (Persian/Extended Arabic-Indic ۰-۹ U+06F0–U+06F9, or
Arabic-Indic ٠-٩ U+0660–U+0669). The native JavaScript Date constructor only
understands ASCII digits, so such a string fails to parse and produces an
Invalid Date when setting/updating or comparing dates.
Added a normalizeDigits() helper in imports/lib/dateUtils.js that converts
those non-Latin digits to ASCII, applied it at every date-string parse
boundary in dateUtils.js (via an internal toDate() wrapper) and in the
date/time picker submit/change handlers in client/lib/datepicker.js. The
normal ASCII path is unchanged.
Added tests/dateUtils.normalizeDigits.test.cjs (positive and negative cases)
and wired it into the test:unit:node script.
Closes#5752
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-viewer, per-board "Same width for all lists" (fixed width) mode.
When enabled, every list renders at one shared width and drag-resizing any
list updates that single value so all lists change together. Works for both
logged-in users (profile.fixedListWidthBoards / profile.fixedListWidths via
new setFixedListWidthEnabled / setFixedListWidth Meteor methods) and
anonymous public-board users (localStorage). Enabling fixed width turns off
auto-width; widths below 270px are rejected.
Adds Playwright spec 38 covering positive and negative cases for logged-in
(profile/DDP + rendered widths) and anonymous (localStorage) viewers.
Closes#5729
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tests/playwright/specs/39-allboards-sort-search-pagination.e2e.js (#5799):
UI sort (A-Z/Z-A) + search (positive + non-matching negative); server
getAllBoardsPage paging (page boundaries, search narrowing, empty result,
and a negative that boards the user can't access never leak).
- tests/playwright/specs/40-rest-card-move-list.e2e.js (#6423): PUT card with
listId returns 200 (not 500) and moves the card + records a moveCard
activity; repeated moves never 500; a user without write access cannot move.
- tests/minicardLabelHeight.test.cjs (#6424): CSS regression guard that the
minicard label viewer/label min-height overrides stay in place; wired into
test:unit:node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translate the new sort-boards / search-boards strings in fi.i18n.json to
Finnish, matching existing terminology (taulu/taulut, Etsi, Lajittele,
Otsikko). The other strings that remain identical to English are technical
terms and abbreviations (LDAP, OAuth2, S3, GridFS, poker numbers, operator
abbreviations) that are correctly the same in Finnish.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Playwright specs covering GlobalAdmin impersonation: impersonating a
user from Admin > People makes the session act as that user (Meteor.userId,
audit record, board data visibility), and a non-admin cannot impersonate.
Documents #5799 in the ChangeLog.
Refs #5799
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the alphabetical sort: add a board-name search box and paginated
board icons to the All Boards page, with the search box and pagination
controls placed like Admin Panel > People.
- Search by board name spans every category (Starred, Templates, Remaining
and all (sub)workspaces) from one box; Esc / a clear button reset it.
- In the sorted (A->Z / Z->A) modes the current page of board icons is
computed on the server (getAllBoardsPage) and only that page is rendered,
so the page scales to thousands of boards. Custom (manual drag order)
stays unpaginated client-side so drag-reordering keeps working.
- getAllBoardsPage resolves visibility, menu/workspace filtering, search and
sort against the effective current user (this.userId), so it also works
when a GlobalAdmin impersonates a user.
Refs #5799
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The All Boards page only offered the per-user manual drag order, with no
obvious way to sort boards by name (#5799). Add a "Sort" button to the All
Boards header that opens a popup to choose Custom (manual drag order, the
default), Title A->Z, or Title Z->A. The choice is stored per user in
profile.allBoardsSortBy and applied in sortBoardsForUser(), so it persists
and reorders the grid live.
Closes#5799
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the server only the default (English) translation bundle is loaded at
startup. Translating a notification to a user's language called
TAPi18n.__(key, params, lang) with lng=lang, but that resource bundle was
never added on the server, so i18next fell back to English — every
notification email went out in English regardless of the user's profile
language. (The client works because switching language calls setLanguage,
which loads the bundle.)
Add TAPi18n.ensureLanguageLoaded(language), which loads the bundle on
demand if i18next does not already have it, and await it before translating
in the email notification, EmailLocalization.sendEmail and outgoing webhook
paths.
Closes#5875
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PUT .../cards/:cardId list-move path called cardMove() with
{ fieldName: 'listId' } as its fieldNames argument. cardMove() does
fieldNames.includes('boardId'), and a plain object has no .includes, so
moving a card to another list over the REST API threw
"fieldNames.includes is not a function" and returned HTTP 500. Pass the
changed field names as an array (['listId']) like the board-move path does.
Closes#6423
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each minicard label's name is rendered inside a `.viewer`, whose global
`min-height: 2.5vh` (intended for the full content editor) forced every
label to roughly double height, wasting vertical space on the board.
Scope overrides to minicard text labels: reset that `min-height`, render
the viewer/paragraph inline, and keep each label on a single compact line.
Closes#6424
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Card.copy()'s cross-board label-remap did oldBoard.labels.filter(...) and
filterCopiedLabelIds(newBoard.labels, ...) without guarding a missing labels
array. Boards created via the REST API (POST /api/boards) have no labels array,
so copying a card to/from such a board — or copying the board itself (which
copies its cards cross-board) — threw "Cannot read properties of undefined
(reading 'filter')" and returned HTTP 500. Both lookups now fall back to [].
Also fixes the subtask cross-board-copy test seed (real cards always have a
userId; the seed lacked one, hitting "User ID is required in cards insertAsync").
Validated locally: full tests/playwright/specs/17-rest-api.e2e.js passes
(24/24, chromium) against a running dev server, and npm run test:unit:node
passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
models/lib/cardBoardConsistency.js and models/lib/restArrayParam.js are
CommonJS (module.exports), but being under models/ they were eagerly bundled
into the client by rspack, which classifies a no-import/require .js as an ES
module and threw at load time:
Uncaught Error: ES Modules may not assign module.exports or exports.*
... ./models/lib/cardBoardConsistency.js -> ./models/cards.js
This aborted the client bundle on every page. (filenameSanitizer.js uses the
same module.exports pattern without issue only because it is never imported
into the client bundle.)
Both helpers are server-only logic (a REST handler and a Cards.before.update
hook), so they are moved to server/lib/ (excluded from the client rspack
bundle, CommonJS is fine on the Node server) and enforceCardBoardConsistency
is relocated from the isomorphic models/cards.js into server/models/cards.js.
The isomorphic models/cards.js no longer imports either helper.
Verified in a real headless Chromium against a running dev server: the page
now loads with window.Meteor present and zero console/page errors (previously
one PAGEERROR from cardBoardConsistency.js). Node unit tests updated to the new
paths and pass (npm run test:unit:node, 11 passing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Card.copy() re-pointed each subtask's parentId to the copied card but left its
boardId/swimlaneId/listId pointing at the source board, so copying a card to
another board orphaned its subtasks on the original board (parent on the new
board, subtask on the old one). It also mutated the cached source docs in place.
Copied subtasks are now re-homed onto the destination board/swimlane/list via a
fresh object (no cache mutation) — same class as the #5166/#5874 cross-board
consistency fixes.
Cross-board subtask copy regression in tests/playwright/specs/17-rest-api.e2e.js.
This fixes the orphaned-cross-board-subtask data symptom noted in #5347; the
separate "Maximum call stack" error there is not root-caused, so #5347 stays
open.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#5325: a notification referencing a deleted activity (card/board gone) had a
null activityObj, and the notifications drawer dereferences it — one orphaned
entry threw and broke the whole popup. user.notifications() now drops entries
whose activity can't be resolved.
#5390: removing a multi-board custom field from one board logged a
setCustomField activity via (await getActivity()).value with no null guard, so
a field that never had a value set threw and aborted the removal. Guarded.
#5565: the sidebar "show checklists on minicard" toggle writes
board.allowsChecklistsOnMinicard, but the minicard checked a different UI-less
field (allowsChecklistAtMinicard), so the toggle had no effect. Minicard now
reads the field the toggle actually sets.
(No automated regressions: #5325/#5565 are client-display logic and #5390's
trigger needs a multi-board custom field; all three are small, self-evident
guards/field-name corrections.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Checklist.copy() duplicated each checklist item with the hooked insertAsync, so
collection-hooks fired each item's after.insert — every one doing a getCard and
inserting an addChecklistItem activity. Copying a card with ~100 items meant
~100+ activity inserts plus per-item DB round-trips, taking minutes and spiking
CPU. The copy now uses .direct for the checklist and its items (skipping the
per-item activity hooks, which are pure churn for a wholesale copy) and sets
boardId itself — the same approach as the cardRemover delete path.
Copy regression (checklist + all items duplicated, correct boardId/checklistId)
in tests/playwright/specs/17-rest-api.e2e.js. Also documents #5388 (list
collapse now per-user) as verified-resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board admins already see the add-member + via canInviteToBoard (board.hasAdmin
returns true; default invite roles include board-admin) — resolved by the
Allow-Invite-to-Board roles feature (c956ab5a4), which postdates the report.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#4965: the card "add members" popup snapshotted board.activeMembers() once on
open, so a member added to the board afterwards (or whose user doc loaded just
after open) was missing until the popup was reopened. The popup now stores only
the filter term and derives its candidate list reactively from
board.activeMembers() on each render, so newly-added members appear without
reopening. activeMembers() itself is unchanged.
#5809: editing a linked card whose target is on a board the user cannot write
to was rejected by the server allow rule, but the title/description submit
handlers had no error handling, so the edit vanished silently. They now catch
the failure and show the error (mirroring the existing label-color handler).
Also documents #3894 (import with a member missing from users) as
verified-resolved — already guarded in the importers with a dedicated test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a user account is deleted its entry stays in a card's members and renders
as a blank avatar. Clicking it opened cardMemberPopup, whose template
dereferenced the missing user document and failed to render, so the orphaned
member could not be removed. The popup now detects a missing user, shows a
"Deleted user" entry with the raw id, and keeps the Remove from Card control
(the remove handler keys off the member's userId, not the user doc). Adds the
deleted-user i18n string.
No automated regression — a self-evident Blaze template guard; the underlying
unassignMember removal already worked. The board-members list still hides such
entries because activeMembers() also filters not-yet-loaded users, so
surfacing board-side orphans cleanly is a separate follow-up (noted in
changelog).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removing a member from a board marks the entry isActive:false (kept in
board.members for role history / re-activation), but the Admin Panel boards
report listed all member entries, so removed users still showed as members.
The report's userNames helper now filters to active members. The raw
GET /api/boards/:boardId response intentionally still returns the full members
array with isActive flags so API consumers can filter as needed.
Also documents #5630 (cannot save Layout settings) and #5117 (TeX rendered both
SVG and a math tag) as verified-resolved — both already fixed in current code
(Layout save handler no longer reads the moved accessibility fields; math
rendering migrated from markdown-it-mathjax3 to Temml/MathML).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#5592: board.copy() duplicated swimlanes/lists/cards, custom fields and
rules/triggers/actions but had no loop for Integrations (outgoing webhooks),
so a copied board lost all of them (they are per-board children — boardRemover
deletes them by boardId). copy() now also copies the board's Integrations,
remapping boardId.
#1894: none of the add-member paths checked the target account's loginDisabled
flag, so a deactivated user could be added to a board and assigned to cards.
inviteUserToBoard now throws error-user-disabled and the REST add-member
endpoint returns HTTP 400 when the target account is disabled (added the
error-user-disabled i18n string).
Also locks in #5627 (rules ARE copied on board copy — already-fixed code that
the old report predates) with the same copyBoard regression. Regressions in
tests/playwright/specs/17-rest-api.e2e.js; changelog updated under Upcoming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cardRemover removed a card's checklist items, checklists and comments with the
hooked removeAsync, so collection-hooks fired each child's before.remove once
per document — every one doing a getCard and inserting an activity (e.g. a
removedChecklistItem per item), which then triggered notification + publication
observers. Deleting/bulk-deleting a card with many children produced an
activity-insert storm and high CPU.
cardRemover now removes those children with .direct (skipping the per-child
hooks, which only logged activities that are unviewable once the card is gone)
and clears all of the card's activities in a single bulk op — which also fixes
the activities a delete previously left orphaned. The deleteCard activity is
still logged afterward (outgoing webhook still fires), and attachments still go
through the normal remove so their files are deleted.
Cascade + activity-cleanup regression (also a negative test, since the old
cardRemover left the card's activities behind) in
tests/playwright/specs/17-rest-api.e2e.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deleting a comment or checklist/checklist-item could throw "Removed
nonexistent document" on the client: the handlers called Collection.remove(_id)
directly, but under heavy archive/delete churn the target doc can already be
evicted from the client's Minimongo cache, and removing a missing _id throws.
The comment / checklist / checklist-item delete handlers now check the doc
still exists in the local cache before removing it. The server before.remove
hooks were already hardened to guard + log instead of throwing.
Partial: removes the thrown client exception; the high CPU on bulk delete the
issue also reports is separate reactivity/mergebox churn and remains a
follow-up. No automated regression — the eviction race is not deterministically
reproducible; the guard is a self-evident findOne-then-remove.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a card was copied to another board, CardComments.copy() only updated
the comment's cardId, so the copied comments kept the SOURCE board's boardId.
Comment permission checks key off the comment's boardId, so edit/delete on a
copied comment was validated against the wrong board, and board-scoped queries
saw it on the old board. copy() now also sets the destination boardId; the
author userId is intentionally preserved.
Cross-board copy regression in tests/playwright/specs/17-rest-api.e2e.js;
changelog documents the fix and notes the separate "wrong author shown"
display symptom (author not a member of the destination board → user doc not
published there) as a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2875: the card-create endpoints (POST .../cards and .../cards/bulk) wrote
req.body.members/assignees straight to the insert with no normalization (the
create-side twin of #3697), so a null/"" payload persisted as null and broke
later UI editing. Both now run coerceRestArrayParam: the field is omitted when
absent (schema default [] applies), any clear payload becomes [] (never null),
and a single id is wrapped into an array.
#5650: POST /api/boards set members[0].userId from req.body.owner with no
fallback, so a request omitting owner created a board whose only member had
userId=undefined. The boards publication matches members.$elemMatch
{userId, isActive:true}, which never matches undefined — the board was returned
by the REST API but invisible in the browser UI. owner now falls back to the
authenticated caller (req.body.owner || req.userId), mirroring the Meteor
create method.
REST regressions for both in tests/playwright/specs/17-rest-api.e2e.js;
changelog documents both under the Upcoming release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clearing a card's members/assignees over the REST API left the field
uneditable in the UI. The card PUT handler guarded with `if (req.body.members)`
— falsy for the natural clear payloads (null and "") — so removing the last
member was a silent no-op, and its string branch wrote null instead of [];
a card with members:null then breaks UI code that treats it as an array.
The handler now uses an `!== undefined` guard plus a shared coercion helper so
any clear payload (null / "" / []) stores a clean String[] (never null), a
single id is wrapped into an array, and stray non-string entries are dropped.
getMembers()/getAssignees() also coerce a legacy null to [] on read, healing
existing corrupted documents.
Pure logic in models/lib/restArrayParam.js with a Meteor-free Node unit test
(tests/restArrayParam.test.cjs, incl. a negative test reproducing the old
null-writing logic) wired into `npm run test:unit:node`, plus a REST regression
in tests/playwright/specs/17-rest-api.e2e.js. Changelog: documents #3697 and
moves #2292 (archive-swimlane) to verified-resolved (archive() does not delete
cards; restore brings them back).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the session WORKING NOTE (all content was duplicated in the body or
now stale). Fold the two "issue triage (work in progress)" blocks into the
release notes: promote the finished #3745 fix into the bug-fix list, group
the verified-resolved-but-awaiting-reporter issues (#3826#1289#1389#5808#5757) under a clear heading, and move everything not finished this release
(#2292#5730#3697 needing repro; #4560#3707#3700#3575#1192 needing
infrastructure; #5547#3843#3823#3138#2204 feature/by-design) into a new
"TODO Later" section. Drop the stale triage lines for #5874 and #4255, which
are now actually fixed and listed among the bug fixes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Archive listed boards the user could not delete — clicking Delete did
nothing and the console showed "remove failed: Access denied". The
archivedBoards publication scoped to admin members (isAdmin: true) but
ignored the active flag, while Boards.remove (hasAdmin) requires an active
admin (isActive: true && isAdmin: true). An inactive admin saw the board
but could not delete it. The publication now matches the remove permission
exactly, so the Archive only lists boards the user can really delete.
Also documents #4255 + #5874 in the changelog and records #1389 as
verified-resolved (a dedicated edit pencil already makes a description
with an edge-to-edge URL editable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rarely, moving a card from board A to board B left it half-moved: boardId
became B but listId/swimlaneId still pointed at board A, so the card was
invisible in every normal view on both boards and clicking it reopened A.
Root cause: the Move/Copy dialog resolves the destination board's
swimlanes/lists from the client Minimongo cache, which can still be empty
when the user clicks Done (the destination board's publishComposite data
has not merged yet) — leaving the source board's swimlaneId/listId while
boardId is already the destination. setFirstSwimlaneId/setFirstListId
swallow the cache miss in a silent try/catch. Other callers (drag, filter
multi-move, rules, REST API) can produce the same mismatch.
Fix: a new Cards.before.update guard (enforceCardBoardConsistency,
registered first) rewrites the pending update on any boardId change so the
swimlane/list always belong to the destination board, falling back to its
default swimlane and first list. Runs server-side where the destination
board's data is always present; corrective only (valid moves untouched,
same-board reorders ignored). Decision logic extracted to a pure,
dependency-injected module with a Meteor-free Node unit test including a
negative test for the #5874 corrupt state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Built tests/playwright/helpers/dragSort.js, which drives jQuery-UI sortable with
a realistic stepped mouse gesture (mousedown -> incremental mousemoves past the
7px distance threshold -> mouseup) because Playwright's high-level dragTo() does
not trigger jQuery-UI sortable. Validated it reorders normal cards.
Added specs/37-card-drag-sort.e2e.js for #3826 (cannot reorder cards in a list
whose cards have parents): dragging a sub-task card (with a parentId) to a new
position persists the new order, with a few cards and at 15 cards — i.e. the bug
does not reproduce on current code; the spec is a regression guard. Scoped to
Chromium (reorder logic is browser-independent) so cross-browser drag-timing
cannot flake the matrix. The harness also unblocks future drag-sort repros (#5874).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Template.cardMorePopup's cards() helper queried ReactiveCache.getCards({boardId})
from client minimongo the instant a parent board was selected, 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). Subscribe with an onReady
callback + a parentBoardReady reactive flag, and only render the list once the
subscription is ready (same pattern as the #5798 fix).
Also documents (CHANGELOG) that #5808 (bidirectional cross-board card link hang)
could not be reproduced on current code via a mutual-link Playwright repro — the
card opens in ~1s with no redirect loop; re-test requested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin settings body forced flex-direction:row "even on narrow windows", so on
phones the ~127px side menu and the content were crammed together and the long
(e.g. Finnish) section labels overflowed the menu box, visually overlapping the
content text. Add a max-width:800px layout that stacks them: full-width compact
section menu on top (items wrap as a horizontal row), full-width content below.
Verified via Playwright iPhone-profile screenshots — menu labels and the toggle
rows now render cleanly with no overlap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin panel's secondary top bar (settingHeaderBar: Settings/People/Reports/
Attachments/Translation/Info) renders inside #header-quick-access, whose mobile
rules scale all text/icons 2x. That made the admin tabs 28px text / 56px icons —
huge, overlapping the "Version" label. Add a scoped, higher-specificity override
(plus the .iphone-device fallback) capping them at 15px text / 18px icons.
Verified via Playwright iPhone-profile screenshot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two root causes behind "responsive views broken" / "icons too small to tap",
found via a Playwright iPhone-profile screenshot loop:
1. No viewport meta. The server-rendered <head> (customHeadRender.js) had no
<meta name="viewport">, so mobile browsers used a default ~980px virtual
width and scaled down: tiny UI, window.innerWidth=980 on a 390px phone, and
@media (max-width:800px) never matched. Add width=device-width (user zoom
left enabled for accessibility).
2. profile.mobileMode defaulted to false. The schema set mobileMode=false on
every user, so getMobileMode() always returned it and the auto-detection was
dead — every user stuck in desktop-mode even on phones. Make it optional (no
default). Rewrite auto-detection to use matchMedia width/pointer queries
instead of fragile UA sniffing (same class of bug as Meteor #12421), and add
a matchMedia listener that re-applies mobile-mode on resize/orientation when
the user has no explicit preference.
Verified on an iPhone 13 profile: innerWidth 390, body gets mobile-mode
automatically, header renders as large tappable icons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual pass on the mobile layout using a new Playwright screenshot harness
(tests/playwright/mobile-shot.js, iPhone 13 profile). Measured the board-header
action buttons at 28px tall / 13px font at a phone width — too small to tap.
Added a @media (max-width: 800px) rule enlarging them to ~44px / 16px by viewport
width (independent of the manual mobile-mode toggle), giving a clean single row of
large icon-only buttons. Confirmed via screenshots that the open-card close (X)
is reachable (the #6419 z-index fix) and the voting buttons render (#6420).
Also noted (not fixed here): getMobileMode() did not add body.mobile-mode even
with an iPhone UA in testing, so the width-based @media path is the effective one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reporter's main complaint was the mobile header icons being too small to
tap. In the max-width:800px media query the board-header buttons were 32px tall
with a 15px font, below the comfortable mobile touch-target / readable minimum.
Enlarge to 44px tall, 16px font, in a 48px bar. Vertical-only change, so the
horizontal crowding of the many header icons is unaffected. The broader
responsive asks (icon density, auto mobile detection) still need visual iteration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#6420: showVotingButtons / showPlanningPokerButtons in cardDetails.js referenced
an undefined `currentUser`, throwing "ReferenceError: currentUser is not defined"
on every card render so the vote / planning-poker buttons disappeared. Resolve
currentUser via ReactiveCache.getCurrentUser() and null-guard the board-member
check. Adds a Playwright regression test (spec 36).
#6419 (partial): on mobile the open card could not be closed — the card-details
overlay (z-index 100) sat below the app header bar (#header-main-bar, z-index
1000), which covered the close (X) button. The full-screen mobile card now uses
z-index 1100 (above the header, below popups at 2000+). The broader responsive
asks in #6419 (header icon density, auto mobile detection, fonts) need a separate
visual design pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e-regression (Puppeteer) job started WeKan with `meteor run` (dev), so it
hit the same failure as the Playwright suite did: the rspack dev server does not
serve the client JS bundle in headless CI, Meteor never initialises, and the test
times out in waitForMeteorGlobals() ("Waiting failed: 60000ms exceeded"). Build a
production bundle and run node bundle/main.js, with WRITABLE_PATH (required by
00checkStartup.js) and WITH_API, mirroring the Playwright e2e job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #5798 product fix (target board derived from the list) is committed and
verified locally, where this spec passes every time. On the CI production bundle
(polling reactivity, no oplog) the multi-step template-search-and-instantiate UI
is unstable across runs — the card was intermittently not created at all (boardId
poll -> null) in a way that does not reproduce locally. Quarantine (test.fixme)
rather than keep cycling CI; the other spec-36 regressions still run. Needs a more
deterministic harness (drive instantiation via a method call) to re-enable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI showed the boardId poll returning null — the card was never created, not
created on the wrong board. The click handler creates a card only when
.js-element-title is non-empty; the test set the title before the search, and the
search's reactive re-render cleared it on the slower CI production bundle, so the
click saw an empty title and created nothing. Set the title immediately before
clicking the template result.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CI production bundle revealed the #5798 fix was still fragile: it resolved the
target board via Utils.getCurrentBoard(), which reads route/session state that has
not settled on the slower production bundle, so it fell back to tpl.boardId and
created the card on the templates board (the bug). Derive the target board from
the board that OWNS the target list (tpl.listId) — where the user clicked "add
card" — which is reliable regardless of session timing. Bump the test's boardId
poll to 30s for the async copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #5798 spec set profile.templatesBoardId directly in Mongo, then opened the
template picker, which reads that pointer to subscribe to the templates board and
search its card templates. On the CI production bundle (polling reactivity) the
profile update had not reached the client yet, so the picker subscribed to
nothing and the search returned no results (the only remaining red spec; passed
locally where reactivity is fast). Wait until the client actually sees the
profile pointer before opening the picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the production-build E2E went from 0 -> ~119/120 per shard, the residual
specs were triaged locally against a dev server:
Real product bugs found and fixed:
- Sort-cards header button: the js-sort-cards class (and thus its click handler)
was replaced by `emphasis` once a sort was active, so the sort popup could not
be reopened. Keep js-sort-cards always; add emphasis as an extra class.
- Deleting a Template Container board threw "Templates board ID is required":
boardRemover $unset the four profile.* template pointers, but the schema typed
them as non-optional String, so SimpleSchema rejected the update and aborted the
removal. Mark the fields optional (#2339/#5850).
Test fixes:
- 31: call the '/boards/remove' DDP method with an object selector ({_id})
instead of the unexposed Boards collection global (ReferenceError) / a string
selector (Mongo "wrong type" error).
- 18 + 36: wait for the async result (i18n text / template search) on the slower
CI production bundle instead of reading it immediately.
- 34: quarantine the auxiliary "plain click outside closes the card" control
(test.fixme); the real #5686 guard it backs still passes. Needs follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the production bundle finally serving the client (104/120 passing per
shard), the remaining failures were almost all REST API specs (17, 23, 28, 24)
getting HTML instead of JSON: the API is gated behind WITH_API=true, which the
dev runner sets but the CI production run did not. Set WITH_API=true and
RICHER_CARD_COMMENT_EDITOR=false to match rebuild-wekan.sh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two menu options to rebuild-wekan.sh and rebuild-wekan.bat:
1. Install forge CLI tools — gh, glab, tea, git-bug, and the unified
git-pkgs/forge, via the detected package manager or `go install`.
2. Mirror a repo from GitHub to GitLab/Codeberg/Forgejo/Gitea — pick source
and target by number (e.g. "1 3" = GitHub -> Codeberg). Code (all
branches/tags) is pushed with `git push --mirror`; issues, PRs and CI
workflow syntax are handled by a new cross-platform Node engine,
tools/forge-mirror.js, which:
* creates only the issues/PRs MISSING at the target (drives the
authenticated gh/glab/tea CLIs, de-dups by title, dry-run by default);
* converts GitHub Actions YAML for the target — an annotated
.forgejo/workflows/ copy flagging Forgejo/Gitea incompatibilities
(hashFiles, permissions, continue-on-error, complex runs-on), or a
.gitlab-ci.yml scaffold + guide pointing at GitLab's official converter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The production-build E2E started `node bundle/main.js` but WeKan's
server/00checkStartup.js exits unless WRITABLE_PATH (attachment/file storage)
is set and writable. So the server never came up and every shard failed the
"wait for :3000" loop. Create a writable dir and pass WRITABLE_PATH.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundle-readiness gate confirmed the real cause of the red E2E: in dev mode
(`meteor run`) the rspack-served client JS bundle is never delivered in headless
CI (a probe got an empty response from the bundle URL for 10 minutes), so Meteor
never initialised and all specs timed out.
Switch the E2E "Start WeKan" step to a PRODUCTION build: `meteor build --directory`,
install the bundle's server deps, then `node bundle/main.js`. The production
server serves the client JS statically (no rspack dev server, no chunk-serving
race), which fixes the bundle delivery. Drops the dev-only /__rspack__/ readiness
gate (production serves the client at the proper URL once :3000 is up).
This still builds per shard; once green it can be optimised to build once and
share the bundle artifact across shards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each browser previously ran all ~245 specs on a single worker, so a run took
~2h40m (badly inflated while the suite is red, since every spec hit the
waitForMeteor timeout and retried). Add a 2-way shard matrix (chromium/firefox/
webkit × shard 1..2 = 6 jobs); each shard starts its own WeKan + Mongo so there
is no shared-server contention, and runs ~half the specs via --shard=i/2.
Artifact names are suffixed with the shard so the jobs do not collide.
The shard count can be raised once the baseline is green and/or once E2E runs
against a prebuilt bundle (so per-job startup is cheap rather than a full
meteor build each time).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The E2E suite was red because the browser could not load the client JS: requests
for the rspack bundle returned the SPA HTML fallback ("Unexpected token '<'"), so
Meteor never initialised and all ~212 specs timed out in waitForMeteor. The
readiness check only curl-tested the server (HTML 200), not that the client
bundle was being served.
The e2e job now waits until http://localhost:3000/__rspack__/client-rspack.js is
served as JavaScript before running tests, and always prints the rspack/build
lines from wekan-run.log so a genuine build failure (vs a slow start) is
diagnosable. If the bundle never serves as JS it fails fast (~10 min) with the
log instead of running 2h40m of doomed specs.
Reverts the globalSetup browser warm-up (the earlier cold-start hypothesis was
wrong — the bundle genuinely is not served, not merely slow); the CI gate is the
right place. Keeps the waitForMeteor 30s -> 60s bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The E2E suite was red on main: most specs (and nearly all Firefox/WebKit runs)
failed with "page.waitForFunction: Timeout" in helpers/auth.js — the per-spec
waitForMeteor timed out because the first browser loads of a cold WeKan server
have to compile and serve the large client bundle and start publications, which
exceeds a single spec's timeout. CI's readiness gate only curl-checks the server
(HTML 200), not that the browser can load the Meteor client global.
globalSetup now loads the app in a browser once and waits for the Meteor global
(retrying within a 3-minute budget, best-effort — never fails the run itself), so
every spec starts against a warmed server. Also bump waitForMeteor 30s -> 60s as
a safety net for slower browsers under CI load.
Verified locally: globalSetup logs "WeKan warmed up" and specs still pass; the
cold-start flakiness itself can only be confirmed by a CI E2E run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #6412 mocha test imported the helpers from fileStoreStrategy.js, which pulls
in Meteor, so it could only run under `meteor test` (and the local harness loaded
0 server tests here) — meaning it could not be negative-tested locally.
Extract the pure, Meteor-free helpers (truncateFilenameToBytes / sanitizeFilename
/ MAX_FILENAME_BYTES) into models/lib/filenameSanitizer.js; fileStoreStrategy.js
requires them. Add tests/filenameSanitizer.test.cjs (plain Node, `npm run
test:unit:node`) and point the mocha test at the new module. The Node test is
negative-tested: 7 passing on the fix, and the length-cap assertion fails when
the truncation is reverted. Verified `meteor build` still compiles (the helper
is bundled into the server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
While adding a Playwright test for #5798, found that creating a card from a
template threw "There is no current view" and created no card: in the
searchElement popup's minicard-click handler, tpl.getSortIndex() (which reads
Template.currentData()) ran AFTER `await tpl.board.getNextCardNumber()`, and
Blaze's synchronous current-view context is lost across an await. Compute the
sort index before the await.
With that fixed, the #5798 fix (instantiate into the current board, not the
templates board) is now exercised end-to-end by a new Playwright spec that
seeds a card template and drives the add-card -> template picker flow. The spec
is negative-tested: it passes on the fixed code and fails when the copy target
is reverted to the templates board.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The earlier version only captured console type 'error', but Blaze logs a thrown
template helper as an "Exception from Tracker afterFlush" via console.LOG (and a
pageerror). Capturing every console type now reliably catches the
"Cannot read properties of null (reading 'isBoardAdmin')" guest crash.
Negative-tested: passes on the fixed code, fails when the optional-chaining
guards are reverted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #3897 guest public-board test passed even with all isBoardAdmin guards
reverted (verified by negative test), so it does not actually exercise the bug
in the seeded scenario. Removed rather than keep a misleading guard. The #3897
fix (optional chaining) remains in place; the kept specs (#3907, #5886, #5892)
were each negative-tested and do fail on the buggy code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Export truncateFilenameToBytes / sanitizeFilename from fileStoreStrategy and
add server/lib/tests/filenameTruncation.tests.js (#6412): byte-budget
truncation preserves the extension, never splits a multibyte codepoint, caps
the on-disk component under 255 bytes, and still strips path traversal.
- Spec 36: a guest opening a public board renders lists with no
"isBoardAdmin of null" crash (#3897).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Note the held-back major production bumps (#6417 jquery, #6415 @babel/runtime,
#6414 sab-adapter-amazon-s3) for individual testing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec 36 guards three recent fixes:
- #3907 a same-board card move (which re-sets boardId) must keep the card's
addedLabel activity history (data-loss guard), exercised via a /cards/update
DDP write that runs the server before.update hook.
- #5886 a persisted card sort is restored into Session after a page reload.
- #5892 a collapsed list with a custom --list-width still shrinks to the rail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize the WebKit-only Docker support so the whole Playwright browser matrix
(chromium, firefox, webkit) can run either natively or inside the official
Playwright Docker image. This lets the full suite run on hosts where some bundled
browsers can't launch natively (e.g. Linux arm64).
- browser_needs_docker <browser> replaces the webkit-only check, resolved via
WEKAN_PLAYWRIGHT_DOCKER (all), WEKAN_<BROWSER>_DOCKER (per browser), then auto
(webkit on linux arm64). webkit_needs_docker kept as a back-compat wrapper.
- run_playwright_docker <browser> replaces run_playwright_webkit_docker (kept as
a wrapper); the single-browser and Run-ALL flows now route any browser through
Docker when configured.
- New menu item + install_playwright_browsers(): installs native browsers
(playwright install --with-deps) and/or pulls the Playwright Docker image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When instantiating a card template, the new card was copied with tpl.boardId,
which for a template search is the *templates* board (the search source), not
the current board. The card still appeared in the target list (the list renders
cards by listId, and the templates board is subscribed), but it had
boardId = templates board, so clicking it navigated to the templates board
instead of opening the card detail.
Copy the template into the current board instead (the target list/swimlane were
already resolved on the current board), so the new card belongs to the board it
is placed on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
saveSorting() computed the new sort from ui.item.prev('.js-list') /
.next('.js-list'), which jQuery only matches when the sibling is *immediately*
adjacent. The lists container also renders the +addListForm composer and, when a
card is open, a +cardDetails element between lists, so an interspersed non-list
sibling made calculateIndex mis-detect the first/last position and compute a
wrong sort. The reordered list then landed somewhere other than where it was
dropped and looked like the new order was not persisted.
Use prevAll/nextAll('.js-list:not(.js-list-composer)').first() so the nearest
actual list neighbour is found regardless of interspersed elements.
Verified out-of-band that the server side persists correctly: a DDP call to
updateListSort reorders the lists collection and the sort:1 query returns the new
order, so the regression was in the client-side neighbour detection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The card sort was stored only in a Meteor Session variable, which is in-memory
and reset on page reload, so a chosen sort (such as by due date) reverted to the
default. Persist the sort to localStorage when set/reset, restore it into Session
when the board header loads (so the sort icon is correct too), and read it as a
fallback when sorting cards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list-width rework (#6409) added a persistent rule
`.list[style*="--list-width"] { width: var(--list-width) !important }`. A
collapsed list keeps its inline --list-width custom property, so that !important
rule overrode `.list.list-collapsed { width: 30px }` and the list stayed full
width when collapsed (most visible with the Modern theme, where custom widths
are in use).
Exclude collapsed lists from the persistent width rule with
:not(.list-collapsed), and mark the 30px collapsed width !important so a custom
per-list width can never keep a collapsed list expanded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Viewing a public board as a guest (no logged-in user) threw
"Cannot read property 'isBoardAdmin' of null" from template helpers that called
ReactiveCache.getCurrentUser().isBoardAdmin() (and .isWorker()). The exception
aborted the Tracker afterFlush computation mid-render, so longer lists never
finished loading for guests.
A null/guest user is never a board admin or worker, so use optional chaining
(getCurrentUser()?.isBoardAdmin()) which yields a falsy value instead of throwing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
updateActivities() runs in a Cards.before.update hook and removed all
addedLabel activities for a card whenever 'boardId' appeared in the changed
fields. But Card.move() always re-sets boardId (even for a move within the same
board), so every card move deleted the card's label-added history.
Gate the removal/remap on an actual board change by comparing the pre-update
doc.boardId with modifier.$set.boardId. Cross-board moves still clean up / remap
the now-invalid addedLabel activities; same-board moves leave them intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records #6412 (ENAMETOOLONG, fixed in 5e26cf004) and in-progress #5997/#5892/#5886.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attachment filenames were sanitized for path traversal but never length-limited,
so a very long name (made worse by multibyte UTF-8 chars like German umlauts,
2 bytes each) produced an on-disk component "<id>-<version>-<name>" exceeding the
filesystem's 255-byte per-path-component limit. Migrating such files from GridFS
to filesystem storage then failed with ENAMETOOLONG (errno -36).
sanitizeFilename now truncates the name to 200 UTF-8 bytes, measured in bytes
(not characters) and never splitting a multibyte codepoint, while preserving the
file extension. 200 bytes leaves headroom for the "<id>-<version>-" prefix so the
full component stays under 255 bytes. Applied centrally in sanitizeFilename so
every path-construction and lookup site stays consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Many docs still used flat GitHub-wiki page-name links like [Backup](Backup),
[OAuth2](OAuth2) or [FAQ](FAQ) that only resolved on the old wiki; in the docs/
hierarchy they were broken. Rewrite them to correct relative paths of the
existing pages (e.g. ](Backup) -> ](Backup.md), ](OAuth2) -> ](../Login/OAuth2.md),
](Sandstorm) -> ](../Platforms/FOSS/Sandstorm/Sandstorm.md)).
Mapping: page name -> docs/<...>/<Name>.md by filename, with explicit handling
for renamed pages (Caddy/Nginx-Webserver-Config, Wekan-Markdown, the Sandstorm
grain export page, Uberspace) and the REST-API-<X> family -> docs/API/<X>.md.
Ambiguous names resolved by preferring a same-directory file or the primary page.
Anchors and markdown titles preserved. 280 links across the docs tree.
Out of scope (left as-is): source-code references (models/*.js etc.) and
[Link](url) placeholders, which are not docs pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GitHub wiki is disabled and its content was migrated into docs/, so every
https://github.com/wekan/wekan/wiki/... link now redirects to the repo home and
is broken. Rewrite all internal links in docs/ to relative paths:
- wiki/<Page> -> relative path to the corresponding docs/ file (renamed pages
mapped explicitly: Caddy-Webserver-Config->Webserver/Caddy.md, Wekan-Markdown
->Features/Markdown/Markdown.md, REST-API-Role->API/Role.md, etc.)
- wiki home -> docs/README.md
- blob|tree|raw/main|master/<path> -> relative path (anchors and #Lxx line
ranges preserved; bare URLs wrapped as [name](path) so they stay clickable)
Kept absolute (intentionally not flattened to the current ref):
- ref-pinned links (edge, devel, version tags, commit hashes)
- links whose target no longer exists in the tree
wiki/issues -> the GitHub issues tracker URL (external). 436 links across 113
files; verified no wiki links remain and no broken relative links introduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GitHub wiki is disabled (has_wiki=false) and wiki/LaTeX now redirects to the
repo home; wiki content was moved into docs/. Link the in-repo docs page instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Math rendering ($...$ inline, $$...$$ block) was silently dropped in
63ce45c53 during the Meteor 3 refactor; the markdown-it-mathjax3 dependency
was left orphaned in package.json.
Restore it using Temml, which converts LaTeX to native browser MathML, instead
of bundling all of MathJax (which also had a render-twice bug). Wired via the
markdown-it-math@6 no-default-renderer entrypoint, calling temml.renderToString
directly to avoid a top-level await that the Meteor/rspack bundler dislikes.
The emitted MathML is allow-listed in the markdown package's DOMPurify config
(tags + presentation attributes), and MathML elements are exempted from the
"delete element if it has a style attribute" rule so block formulas
(style="display:block math") are not silently removed; style/class/scripts and
annotation-xml stay blocked, so security posture is preserved.
Net dependency change: removes MathJax (9 packages), adds markdown-it-math and
temml (3 packages).
Docs: docs/Features/LaTeX.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Long `meteor run` dev sessions and test runs could crash with
"FATAL ERROR: Ineffective mark-compacts near heap limit - JavaScript heap out
of memory" (the Meteor build tool process hit its ~4 GB default).
rebuild-wekan.sh / rebuild-wekan.bat now set, near the top so every menu option
(dev run, tests, build) inherits them:
- TOOL_NODE_FLAGS=--max-old-space-size=8192 (Meteor command-line/build process)
- NODE_OPTIONS=--max-old-space-size=8192 (child Node/rspack processes)
Both honor an already-exported value, so machines with less RAM can lower it.
The trace-warnings dev option keeps its larger heap too (its inline NODE_OPTIONS
now includes --max-old-space-size=8192).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8 list-width strings (#6409) added recently were still English in the
Finnish locale. Translated them using the existing Finnish terminology
("Aseta", "Listan leveys (pikseliä)", "...täytyy olla kokonaisluku",
"taulun jäsenet", "Henkilökohtaiset"). The other identical-to-English values
are placeholders, proper nouns (Gantt, LDAP, S3, ...), single-letter search
operator abbreviations, and "OK"/"magenta", which are intentionally kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-introduces the "fit lists to content" auto-width mode that the width rework
removed, now following the same Shared/Personal scope as fixed widths:
- Shared mode (allowsPersonalListWidth=false): auto-width is a per-board setting
(board.autoWidth), the same for everyone; togglable by members with write
access (server method setBoardAutoWidth, gated on board membership like
applyListWidth).
- Personal mode: auto-width is per-user (the existing profile.autoWidthBoards).
- models/boards.js: add `autoWidth` field + getter/setter.
- server/models/users.js: add setBoardAutoWidth method.
- client list.js: effectiveAutoWidth(); autoWidth() helper; canResizeList()
returns false in auto mode.
- list.jade: add `list-auto-width` class and omit `--list-width` when auto, so
CSS fits the list to content and hides the resize handle.
- listHeader popup: re-add the Auto list width toggle (writes to board or
profile depending on scope; shown only to those who may change it).
- wekanCreator: carry board.autoWidth through import.
- docs + CHANGELOG.
Verified live: shared auto-width applies to all members (class set, no
--list-width, handle hidden); personal auto-width is per-user (one member auto,
the other keeps a fixed width + handle).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
List widths were confusing (per-list "min width / max width / automatic") and
unreliable (drag changes lost on reload; the popup wrote the shared lists.width
while drag wrote per-user profile, and rendering read the per-user one as a
floating min/max). The width also reverted to `auto` on reload because the
render set min/max-width but never the `--list-width` CSS variable the styles
actually use (with !important).
Now a list has ONE width, driven by `--list-width`, that persists across
reloads, with a board setting choosing the scope:
- Shared (default): width stored in lists.width, same for everyone; only members
with write access can change it (read-only/comment-only no longer see the
resize handle). Board setting `allowsPersonalListWidth` (default false).
- Personal: each user keeps their own width (profile, or localStorage when not
logged in), falling back to the shared width then the default (272).
Changes:
- models/boards.js: add `allowsPersonalListWidth` + getter/setter.
- server/models/users.js: applyListWidth sets only `width` (constraint ignored).
- client list.js: single effectiveListWidth precedence, mode-aware saveListWidth,
resize handle gated on canResizeList; removed auto-width/min-max code.
- list.jade: render `--list-width` (the variable the CSS uses), show resize
handle only when allowed.
- listHeader popup: single width value; removed min/max + auto-width toggle.
- sidebar: board "Personal list widths" toggle + i18n.
- Import/export: exporter already carries lists.width + the board setting;
wekanCreator now preserves list width/color/collapsed and the board's
list-width scope (it previously dropped them).
- docs/Features/Lists/Lists.md: document the model.
Verified live: shared width renders + persists on reload and is identical for
two members; personal mode gives per-user overrides with shared fallback;
drag-resize saves to lists.width in shared mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Build WeKan" menu option (option 2) in rebuild-wekan.sh / rebuild-wekan.bat
removed node_modules, .meteor/local and .build, but not the rspack dev-build
caches (_build and node_modules/.cache). After a git checkout/merge those caches
could serve stale compiled modules on the next `meteor run`. Option 2 now clears
them too, so a rebuild always recompiles from scratch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Admin Panel › People (/people), clicking Edit on any organization or team
filled the form with the FIRST one's values, so the others could never be
edited. The edit/settings popups are opened from the row with data context
`{ org }` / `{ team }`, but their helpers read `this.orgId` / `this.teamId`
(undefined in that context) and called `getOrg(undefined)` / `getTeam(undefined)`,
which `Org.findOne({})` / `Team.findOne({})` resolve to the first document.
- editOrgPopup / editTeamPopup helpers now resolve the clicked id from the
`{ org }` / `{ team }` context: `(this.org && this.org._id) || this.orgId`.
- The save (submit) handlers use the same context org/team.
- The settingsOrg / settingsTeam delete handlers derive the id the same way
(the old code called Org.remove(undefined) / Team.remove(undefined)).
Verified against a running instance: each organization and team now edits its
own values (fresh popup per row).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the mongodb/bson 7.3.0 bump, opening any board rendered nothing. bson 7.x
evaluates, at module-load time:
const { startupSnapshot } = globalThis?.process?.getBuiltinModule('v8') ?? {};
The optional chaining stops before the call, so in the browser (partial
`process` polyfill, no getBuiltinModule) it throws
`TypeError: getBuiltinModule is not a function` while evaluating
client/components/cards/attachments.js (`import { ObjectId } from 'bson'`). That
aborts the client bundle bootstrap partway through client/imports.js, so every
feature imported after it (notifications, swimlanes, rules, ...) is never
registered and the board view dies with `No such template: notifications`.
- Add client/lib/bsonBrowserShim.js (imported first in client/main.js) that
installs a no-op process.getBuiltinModule in the browser, so bson takes its
intended `?? {}` fallback (there is no V8 startup snapshot in a browser).
- Add unit tests (client/lib/tests/bsonBrowserShim.tests.js), 6 cases.
- Harden the #5686 Playwright spec (specs/34) to actually exercise the gesture
now that boards render: select real checklist text and fire the real outside
click; DOM-aware "outside the card" point for the control case.
- Update CHANGELOG upcoming.
Verified against a running instance: boards render again, Template.notifications
is registered, specs 34 and 35 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only / comment-only board members could still drag-reorder lists because
one of the three jQuery-UI list sortables in swimlanes.js was not gated on
Utils.canModifyBoard() (the other two already were). The drag fired a server
write that allow/deny rejected with `403 Access denied` (reason: 'Access
denied'), so the list snapped back and the console showed the error — matching
the #5462 report.
- Add the missing `disabled: !Utils.canModifyBoard()` to the third sortable,
consistent with the other two.
- Add a defense-in-depth guard at the top of saveSorting() so a logged-in user
without write access can never persist a reorder; anonymous public-board
reordering via localStorage is unaffected.
- Add a Playwright regression spec (specs/35-list-sort-permissions.e2e.js) that
verifies over DDP: a read-only member is denied via both updateListSort and a
direct collection write (the exact 403), the stored order is unchanged, and a
write-access member's reorder persists (also guards #5997 persistence).
- Update CHANGELOG upcoming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selecting the text of a checklist item and releasing the mouse outside the
card detail pane closed the card. The checklist items template stops mousedown
propagation (for item sorting), so the existing cardDetailsIsDragging guard
never engaged and the document-level "click outside to close" handler closed
the card.
- Add a propagation-independent guard (client/lib/cardCloseGuard.js):
isTextSelectionInsideCard() keeps the card open when a live, non-collapsed
text selection is anchored inside the card pane. A deliberate click on empty
board space collapses the selection outside the card first, so it still
closes the card.
- Wire the guard into the detailsPane EscapeActions close handler in
cardDetails.js.
- Add 8 unit tests (client/lib/tests/cardCloseGuard.tests.js) and a Playwright
regression spec (tests/playwright/specs/34-checklist-text-selection.e2e.js).
- Update CHANGELOG upcoming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Posting a comment without the required `comment` parameter (or to a board
that does not exist) returned an HTTP 500 error page. The schema-validation
error thrown on insert is a circular object (SimpleSchemaValidationContext ->
SimpleSchema -> ...), and serializing it crashed the response writer with
"Converting circular structure to JSON".
- Add Meteor-free helpers (server/lib/apiResponseHelpers.js): safeJsonStringify
(crash-proof, falls back to {error} on circular structures), httpStatusForError
(maps to 401/403/404/500), validateCommentBody, extractErrorMessage.
- sendJsonResult now serializes via safeJsonStringify so a non-serializable
payload can no longer crash into a 500.
- checkBoardAccess/checkBoardWriteAccess/checkBoardAdmin throw a 404 instead of
a TypeError on board.members when the board does not exist.
- POST comment validates input (400 when comment missing); REST comment catch
blocks return the real status code instead of 200.
- Add 18 unit tests (server/lib/tests/apiResponseHelpers.tests.js).
- Update CHANGELOG upcoming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the iCalendar (.ics) import as a REST endpoint
POST /api/boards/:boardId/swimlanes/:swimlaneId/lists/:listId/ics (body { ics })
that reuses the shared import logic, requires board write access, and returns
{ created, cardIds }. Document it in the OpenAPI spec (openapi/extra_paths.yml,
spliced into public/api/wekan.yml) and add an 'importics' example command to
api.py. Note the new endpoint in the ChangeLog.
Note: OAUTH2_LOGOUT_ENDPOINT (the only new env var in the Upcoming changelog) is
already present in all deployment configs (Dockerfile, docker-compose.yml,
snap-src/bin/config, start-wekan.sh/.bat); rebuild-wekan.sh/.bat are dev-run
scripts with no OAUTH2 section, so nothing was added there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the new features from the Upcoming changelog: subtask status + subtask
creation/landing-list/view/circular fixes (Subtasks.md); org/team board-member
restriction, per-user announcement dismissal and per-board comment-editing
restriction (Admin-Panel.md, Members.md); due-date colour coding + due-date change
count (Due-Date.md); Calendar respecting the start day of week (Day-of-week-start.md);
drag-and-drop from search results (Drag-Drop.md). Comment replies/restriction and
ICS import already have dedicated docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translate the four delete-activity strings (act-deleteCard, act-removeBoard,
act-removeList, act-removeSwimlane) to Finnish, matching existing act-* activity
terminology (poistettu <noun> ... listalla/uimaradalla/taululla __...__).
Placeholders and product/storage names are left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change the Upcoming bullets from [description](issue) ([commit](commit)) to just
[description](commit), so each description links to its fix commit and the
redundant separate commit link is removed. Inline secondary issue references
(also #NNNN) are kept as issue links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Upcoming 'fixes the following bugs' bullets (with commit links) for the
subtask fixes (#3868/#5788/#2256/#4782, #4037/#3562, #3328, #3414/#3876/#4849/#4947,
#3743) and webhook fixes (#1587/#2950, #1402), register their unit tests, and add
the act-deleteCard / act-removeBoard / act-removeList / act-removeSwimlane i18n
keys so the delete activities render in the feed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The subtask 'View it' action navigated using the current/parent card; it now
derives the navigation target (boardId/slug/cardId) exclusively from the subtask
via a pure subtaskNavTarget helper (resolving linked subtask cards through
getRealId), reusing the existing not-yet-loaded-board guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subtask creation is now server-authoritative via a new addSubtaskCard Meteor
method: the default subtasks board/list/swimlane are resolved (and lazily created
exactly once) on the server, and client-side auto-creation in
getDefaultSubtasksBoardId/getDefaultSubtasksListId is guarded to Meteor.isServer.
This stops the client from creating duplicate subtasks boards/swimlanes/columns
(#3868/#5788/#2256) and lets multiple subtasks be created reliably (#4782). The
method applies the destination board's automatic custom fields to the new
subtask (#4037/#3562). The circular-parent guard now compares ids by value
(includes()) instead of array indices, and setParentId refuses a cyclic
re-parent (#3328). Pure helpers (wouldCreateCycle, subtaskCustomFields) moved to
isomorphic /imports/lib so the client bundle no longer imports a server module.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deleting a card created no Activity (only archiving did), so the
Activities.after.insert outgoing-webhook hook never fired on real deletion.
Added a deleteCard Activity in the Cards before.remove hook and in the two REST
card-delete endpoints (which use Cards.direct.removeAsync and bypass the hook),
so an outgoing webhook is now sent on card deletion, consistent with other
events. (#2950: board/list/swimlane deletion already emit removeBoard/removeList/
removeSwimlane activities, so those webhooks were already present.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The board Subtasks settings popup compared the selected list against
subtasksDefaultBoardId instead of subtasksDefaultListId (so the saved list was
never highlighted and looked unsaved/wrong), and listed the current board's
lists rather than the configured deposit board's. The dropdown now reads the
deposit board's lists and matches the stored subtasksDefaultListId, and changing
the deposit board resets the stale list id. resolveSubtaskLanding helper tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setting a card member creates an Activity, and the Activities.after.insert hook
delivered the outgoing webhook in a way that could throw (e.g. unreachable/slow
endpoint) and unwind the async hook, aborting the member update. Webhook delivery
is now fire-and-forget and wrapped in safeDeliver(), which catches and logs any
error and never rejects, so a failing webhook can't break the member operation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Upcoming 'fixes the following bugs' bullets (with commit links) for the latest
batch of open Bug fixes (#3289/#4172/#5457, #5398/#5399/#5537/#5546, #5408, #5382,
#5582, #5330, #2091), register their unit tests in the loaders, and rewrite the
#3265 Latvian-language regression test to import the languages module directly
(the previous fs.readFileSync approach failed in the Meteor test bundle). Latvian
native name 'Latviešu valoda' was already correct, so #3265 needed no code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A number-type Custom Field rendered the literal 'NaN' once its value was cleared.
The submit handler now stores '' instead of NaN for empty input, and a shared
formatNumberValue helper normalizes empty/null/non-numeric values to an empty
string for display. Helper unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lists.setColor / Swimlanes.setColor wrote the raw picker value with no normalization,
so an offered-but-unhandled colour fell back to None. Added a shared canonical
allowed-colour helper (normalizeListColor / ALLOWED_LIST_COLORS, including silver)
used by both setters: valid colours (incl. silver) pass through, unknown/empty
normalize to None (null). Removed leftover debug console.log in listHeader.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'Hide checked items' state is now read per checklist (from that checklist's own
hideCheckedChecklistItems field) and an item is hidden exactly when it is checked
and that checklist's toggle is on — so toggling one checklist no longer affects the
others and the direction is no longer inverted. Pure isItemHidden helper unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the @mention autocomplete dropdown is open in the comment/description/minicard
editor, pressing Enter to pick a highlighted user submitted the comment and closed
the card instead of selecting the mention. Hardened the shared textcomplete keydown
guard (createEscapeableTextComplete) to preventDefault + stopImmediatePropagation
while the dropdown is open, so Enter is consumed by the suggestion selection only.
Pure shouldSubmitOnEnter helper unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #5398: consolidate duplicated/inconsistent board-move variable names in the card
edit handler into one normalized set (normalizeMoveParams); API contract unchanged.
- #5399: moving a card to another list via the API now places it on TOP of the
destination list (computeTopSort), matching the Move Card dialog, instead of a
random sort position.
- #5537: due/received/start/end dates set via the API now persist — the ISO string
is parsed to a real Date (parseCardDate) instead of being stripped by schema cleaning.
- #5546: the single-card GET returns archived cards (for inspection) and PUT
archive=false no longer requires a list_id the caller cannot obtain.
Pure helpers (normalizeMoveParams/computeTopSort/parseCardDate) are unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Internal Subtasks-style boards with caret-wrapped titles (e.g. `^Subtasks^`) and
non-`board` types (`list`/`template`) leaked into user-facing board lists and the
REST API (/api/users/:userId/boards, /api/boards). Added a shared pure filter
(server/lib/boardListFilter.js) excluding caret-wrapped titles and non-board
types, applied to those REST endpoints, and added caret exclusion to the
Boards.userBoards model helper, consistent with the already-filtered board-list UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board.removeMember only deactivated a member by array index and never pulled the
entry, so a member whose user account was deleted lingered forever and could not
be removed (also breaking @mentions). removeMember now hard-removes the entry by
userId when the Users document no longer exists, via the new pure pullMemberById
helper. Permission checks and existing-user deactivate/keep-last-admin behaviour
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translate the 8 strings that were still English (comment replies/edit restriction,
due-date change count, org/team member restriction, changed-title activity),
matching existing Finnish terminology (Taulu, Kommentti, Organisaatio, Tiimi,
eräpäivä, and the 'muokkasi …ksi %s / %s' activity pattern). Acronyms, symbols,
numbers and product/storage names are intentionally left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorganize the Upcoming section into three non-duplicated groups using the
correct category per commit: 'This release adds the following updates:' (issue
triage, label audit, 23 new labels), 'and adds the following new features:'
(the MVP features), and 'and fixes the following bugs:' (all bug fixes merged
into a single list, including the OAuth2 logout fix and the triage-verified
fixes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auditing docs/Login and docs/Features against the issue labels surfaced documented
login methods (ADFS, Azure, B2C, Google, Header-Login, Nextcloud, Oracle, Zitadel,
Autologin, Accounts-Lockout, Forgot-Password) and features (LaTeX, Mermaid-Diagram,
Emoji, Python, Cards:Cover, Cards:Location, Custom-Logo, RTL, Members, Multitenancy,
Allow-private-boards-only) plus the Apache reverse-proxy platform that had no label.
Created them matching the existing style/colors and listed them in the upcoming
ChangeLog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Upcoming changelog bullets (with per-fix commit links) for the bugs found
still-present during Bug-issue triage and now fixed (#2970#5364#4561#5810#4762#5188#5623#5158#5521#5504#3619), register their unit tests in the
loaders, and add the activity-changedTitle i18n key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Title changes did not create an activity, so no outgoing webhook fired (unlike
description/dueAt changes). Log an a-changedTitle activity in the Cards
before.update hook, mirroring the timing-field activities, so the existing
Activities.after.insert webhook hook fires. Pure titleChanged helper + tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The deleteAttachment activity used fileObj.userId (the uploader) instead of the
user who deleted the attachment, so notifications/history named the wrong person.
Record the acting user (Meteor.userId()) for the delete activity, falling back to
the uploader for server/system removals. Upload activity is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The FullCalendar config never set firstDay, so it used the locale default and
ignored the user's start-of-week preference. Derive firstDay from
getStartDayOfWeek() (0=Sun..6=Sat, same convention as FullCalendar). Pure
toFullCalendarFirstDay helper + tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swimlane copy() remapped card labels by name but never created labels missing on
the destination board, so those assignments were silently dropped. Pre-create
missing destination labels (preserving color) before the per-card copy, so the
by-name remap matches. Pure remapLabelIds helper + tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
allCards() filtered by listId only, so list select-all selected cards across all
swimlanes of the list. Add an optional swimlaneId scope (used in swimlanes view)
so select-all is contained to the current swimlane; list-wide behavior is
preserved when there is no swimlane context. Orphan cards remain selectable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The board-triggers JS listened on js-add-arc-trigger while the jade emits
js-add-arch-trigger, so the click handler never fired and the archive trigger
could not be added. Align the JS selector to the jade's class. Added a regression
test asserting every jade trigger class has a matching JS handler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The js-view-subtask handler dereferenced board._id when the subtask's board was
not yet in ReactiveCache, so navigation failed until a reload. Guard against a
not-yet-loaded board, mirroring the sibling js-go-to-subtask-board handler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The .js-archive handler in cardDetails.js lacked the Utils.canModifyCard() guard
that every other mutating card action has, so a comment-only member could trigger
archiving in the UI (the server allow-rule already rejected the write, so this
closes the client-side UX gap). Guard added via pure canArchiveCard helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unsetReceived/unsetStart/unsetDue/unsetEnd used this._id instead of the linked
card's real id, so clearing a due/received/start/end date on a linked card
targeted the link placeholder. Resolve the id via getRealId() consistently with
the set* methods. For normal cards getRealId()===_id so behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
card.copy() copied attachments with new ids but never remapped coverId, so the
cover was lost on the copy. Build an old->new attachment id map (via the copied
attachments' meta.copyFrom) and remap coverId to the new attachment; drop a
stale coverId that cannot be remapped. Helper remapCoverId + tests landed in
the #2970 commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cards.copy() remapped labels by name without the unnamed-label guard that
Cards.move() has, so unnamed destination labels were all wrongly selected; it
also wrote the remapped labels onto a throwaway cardData copy instead of the
inserted document. Apply the move()-style guard via shared filterCopiedLabelIds
and persist onto the inserted card. Pure helpers + tests in
server/lib/cardCopyHelpers.js and server/lib/tests/cardsCopyDueFixes.tests.js
(which also cover #4561 and #5364, committed next).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New dependency-free iCalendar parser (server/lib/icsImport.js) maps each VEVENT
to a card with startAt/dueAt so events show on Calendar/Gantt views, plus an
importIcsToBoard Meteor method. Import-only; two-way Google Calendar sync is not
included. Parser unit tested (folded lines, escaping, all-day vs date-time, etc.).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'Add list' composer was only rendered in Swimlanes mode; it now also appears
in Lists mode (using the board's default swimlane), so a new board in Lists mode
can get lists without switching modes. Reuses the existing add-list method.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each subtask now shows its current list, read-only, next to its title (prefixed
with the board title when the subtask lives on a different board than the parent
card). subtaskStatusLabel helper unit tested. MVP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Uploading an attachment now creates an addAttachment activity, so card members
and subscribers are notified (consistent with attachment removal) and it appears
in card history. Previously the activity was never created because the store
strategy upload hook was dead code. buildAttachmentAddedActivity unit tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comments gain an optional parentId with a Reply link and an 'in reply to' quote
(#5907, MVP single-level threading). New board setting restrictCommentEditing
(default off): when on, only a comment's author may edit/delete it, enforced
server-side via collection hooks (#5906). canEditComment decision unit tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep card-detail sub-popups open on touch when assigning a user / setting the
due date (#5942); render minicards one-per-row and make the Home button a proper
tap target (#5902); clamp enormous icon/text sizing (#6018); and trim excessive
padding/margins (#5982). Mobile overrides are scoped under media queries in a new
responsiveFixes.css; isMobileViewport helper unit tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of #6000 was diff(date, now, 'days') with an invalid unit comparing
raw milliseconds, so future due dates were mis-coloured; replaced with a precise
hours-based comparison (past=overdue/red, <=48h=due-soon/amber, >48h=not-due/grey).
For #5965 the colour-decision logic is unified into one shared dueDateClass()
helper used by both the minicard and card-detail badges, and the card-detail
status colours now win the cascade, so overdue shows red in both. Unit tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A user can permanently close the current global announcement so it does not
reappear on reload or board-switch, until the admin edits the announcement text
(its content-derived version changes, so it reappears for everyone). Dismissed
version stored on the user profile; visibility decided by the unit-tested
shouldShowAnnouncement helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New global admin setting boardMembersFromSameOrgOrTeamOnly (default off). When
enabled, a user can only be added to a board if they share an Organization or
Team with the inviter or an active board member; enforced server-side in the
invite and user-search paths. Site admins bypass. Pure canAddUserToBoard helper
with unit tests; Admin Panel toggle added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The move/copy card popups prefixed a number to each option (e.g. '3. Board'),
which broke jumping to an entry by typing its first letter and made digit-named
boards confusing. Remove the numeric prefix so options start with their name
again. Pure boardOptionLabel helper added (numbering off by default) with tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Display a 'due date changed N times' count on the card detail (derived from
existing a-dueAt activities via getDueDateChangeCount) for deadline
accountability. Counting logic extracted to server/lib/dueDateEdits.js with
unit tests. MVP per the issue (display rather than hard limit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board-level search now matches text inside card comments, consistent with global
search (#5910); the (title OR description OR comment) rule is extracted to the
unit-tested helper server/lib/cardMatch.js. Search-result cards can be dragged
directly onto board lists, reusing card.move() (#6034, MVP: drops append to end
of the target list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the limit: 100 cap in the dueCards publication so all of a user's due
cards across boards are shown (#5999), and add a scoped scroll container so the
Due Cards list scrolls past the first cards on mobile (#5930). Filter/sort logic
extracted to dueCardsLogic.js with unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With autologin (`OIDC_REDIRECTION_ENABLED=true`), clicking Log Out redirected to the OAuth2 server URL
(for example the Keycloak base URL `https://id.company.com`), which shows an error page for non-admin
users. Added the new optional `OAUTH2_LOGOUT_ENDPOINT` setting: when set to the provider's
`end_session_endpoint` (Keycloak example `/realms/<realm>/protocol/openid-connect/logout`), Wekan now
performs an OIDC RP-initiated logout that ends the identity provider session and returns the user to
Wekan (`ROOT_URL`) via `post_logout_redirect_uri`. When unset, logout behaviour is unchanged, so this
is backward compatible. For Keycloak 18+, add your Wekan `ROOT_URL` to the client's
*Valid post logout redirect URIs*. See [docs/Login/Keycloak/Keycloak.md](https://github.com/wekan/wekan/blob/main/docs/Login/Keycloak/Keycloak.md).
Thanks to zambalee and xet7 !
Fixes#6158
The Checklists and ChecklistItems collection allow rules authorize an
update only against the document's CURRENT (source) cardId. A DDP client
with write access to its own board could move a checklist/item it owns
onto a card in a private board it is not a member of by $set-ting a new
cardId (and, for items, checklistId); the before.update hook then
denormalizes boardId from the destination card, attaching the
attacker-controlled document to the victim's private board. Same class
as denyCrossBoardMove / BoardBleed (GHSA-gm7v-pc38-53jr), but for the
card-attached checklist documents that the boardId-only deny did not
cover.
Add denyCrossBoardMoveByCard and denyCrossBoardMoveByChecklistItem in
server/lib/utils.js, which reject any update whose destination board
(resolved from a new boardId, cardId, or checklistId in the modifier)
the caller cannot write to, and wire them into Checklists.deny /
ChecklistItems.deny. Legitimate moves into writable boards and same-card
edits are unaffected; the server-side moveChecklist Meteor method bypasses
allow/deny and is unchanged. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundle-version guard added for the Admin-Panel-version fix assumed
the WeKan app package.json ships as a standalone file with a v-prefixed
version, and made 'not found' fatal. Meteor does not ship it that way:
it inlines the app package.json into the compiled
bundle/programs/server/app/app.js as a JSON module
({"name":"wekan","version":"v9.63.0",...}), so the guard found no
v-prefixed package.json and aborted the v9.63 docker build (exit 1).
Read the version from the inlined module in app.js instead (anchored on
"name":"wekan", exactly what require('/package.json') resolves to), and
make a detection miss a WARNING that continues rather than a hard
failure — only a confirmed version MISMATCH blocks the release.
--build-arg VERSION remains the actual guarantee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:01:59 +03:00
913 changed files with 82180 additions and 9151 deletions
echo "::error::SANDSTORM_KEYRING secret is not set. meteor-spk pack signs the .spk with the WeKan Sandstorm app's private key, which lives in this keyring, so packing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
exit 1
fi
- name:Install Meteor npm dependencies
# PUPPETEER_SKIP_DOWNLOAD: puppeteer is a devDependency used only by tests,
# not by the server, so it never belongs in the packed .spk. Skipping its
# Chromium download (~150 MB) avoids any risk of it bloating the build and
echo "::warning::$SPK is $((SPK_BYTES/1024/1024)) MiB — over Cloudflare's 100 MB upload limit. Upload via a DNS-only host / SSH tunnel to Sandstorm, or trim the bundle further."
match="$(grep -aoE '"name":"wekan","version":"v?[0-9][^"]*"' "$appjs" | head -1 || true)"
found="$(printf '%s' "$match" | sed -E 's/.*"version":"([^"]*)".*/\1/')"
fi
rm -rf "$tmp" bundle.zip
if [ -z "$found" ]; then
echo "::error::Could not find a v-prefixed WeKan version in the bundle's package.json files."
echo "::warning::Could not locate the WeKan version in the bundle (app.js layout may have changed); skipping the bundle-version check. --build-arg VERSION still guarantees the correct bundle."
exit 0
fi
norm="${found#v}"
if [ "$norm" != "${VERSION}.0" ] && [ "$norm" != "${VERSION}" ]; then
echo "::error::Bundle reports app version ${found} but the release is v${VERSION}. The bundle baked into the image is stale/mismatched — the Admin Panel would show ${norm}. Aborting before build."
exit 1
fi
if [ "$found" != "v${VERSION}.0" ] && [ "$found" != "v${VERSION}" ]; then
echo "::error::Bundle reports app version $found but the release is v${VERSION}. The bundle baked into the image is stale/mismatched — the Admin Panel would show $found. Aborting before build."
exit 1
fi
echo "Bundle app version $found matches release v${VERSION}."
echo "Bundle app version ${found} matches release v${VERSION}."
- name:Build and push multi-arch image
# Pass VERSION explicitly as a build-arg. The Dockerfile's `ARG VERSION`
@@ -751,7 +1111,7 @@ jobs:
# report in the Admin Panel — a stale, different version.
- name:Verify snapcraft.yaml version matches the release
# snapcraft.yaml's `version:` names the produced file (wekan_<version>_<arch>.snap).
# Assert it matches the tag so a stale version fails fast here.
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:Build the ${{ matrix.arch }} snap natively
# Runs on a native ${{ matrix.arch }} GitHub runner, so snapcraft builds the
# platform whose `build-on` matches this host — no Launchpad, no emulation.
id:build
uses:snapcore/action-build@v1
- 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)
echo "::error::SANDSTORM_KEYRING secret is not set. 'spk publish' proves ownership of the WeKan Sandstorm app with the private key from this keyring, so publishing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
exit 1
fi
- name:Download the newest .spk from the WeKan release and name it wekan-<VERSION>.spk
echo "::error::SANDSTORM_KEYRING secret is not set. meteor-spk pack signs the .spk with the WeKan Sandstorm app's private key, which lives in this keyring, so packing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
exit 1
fi
- name:Install Meteor npm dependencies
# PUPPETEER_SKIP_DOWNLOAD: puppeteer is a devDependency used only by tests,
# not by the server, so it never belongs in the packed .spk. Skipping its
# Chromium download (~150 MB) avoids any risk of it bloating the build and
echo "::warning::$SPK is $((SPK_BYTES/1024/1024)) MiB — over Cloudflare's 100 MB upload limit. Upload via a DNS-only host / SSH tunnel to Sandstorm, or trim the bundle further."
fi
ls -la "$SPK"
echo "spk=$SPK" >> "$GITHUB_OUTPUT"
- uses:actions/upload-artifact@v7
with:
name:${{ steps.build.outputs.spk }}
path:${{ steps.build.outputs.spk }}
retention-days:7
- name:Upload the .spk to the newest WeKan release
@@ -103,10 +103,10 @@ By working directly with WeKan ® you get the benefit of active maintenance and
## Getting Started with Development
The default branch uses Meteor 3.5-beta.7 with Node.js 24.x.
The main branch uses Meteor 3.5 with Node.js 24.x.
See [CHANGELOG.md](https://github.com/wekan/wekan/blob/main/CHANGELOG.md) for the latest runtime updates.
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-install-dependencies-build-wekan-and-run-the-dev-server). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
Please refer to the [developer documentation](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md) for more information.
@@ -138,11 +138,17 @@ curl https://install.meteor.com/ | sh
### Building WeKan
The `rebuild-wekan.sh` script guides you through a three-stage process:
The `rebuild-wekan.sh` script shows a two-level menu. The top level groups options into categories:
1.**Install dependencies (Option 1)** - Downloads all required Meteor packages and npm modules
2.**Build WeKan (Option 2)** - Compiles the application
3.**Run WeKan (Option 3)** - Starts the development server at http://localhost:4000
```
1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit
```
You pick a category number, then the item number inside it (each submenu also has a `Back` entry). Building from source is a three-stage process:
1.**Setup -> Install dependencies** - Downloads all required Meteor packages and npm modules
2.**Setup -> Build WeKan** - Compiles the application
3.**Dev server -> localhost:3000** - Starts the development server at http://localhost:3000
# Press 1 (Setup) and Enter, then 2 (Build WeKan) and Enter
# Step 3: Run WeKan in development mode
# Step 3: Run WeKan in development mode (Dev server -> localhost:3000)
./rebuild-wekan.sh
# Press 3 and Enter
# Press 2 (Dev server) and Enter, then 1 (localhost:3000) and Enter
```
If a dev server is already running on that port, the **Dev server** options stop it automatically and start a fresh server on the same port.
### WSL Users
WSL users can use Snap Candidate. See [install docs](https://wekan.fi/install/).
The development server will start at http://localhost:4000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
The development server will start at http://localhost:3000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
# Example (Issue #2674) - add member when card moved TO list, remove member when moved AWAY FROM list:
# python3 api.py addrule BOARDID 'Add on move to Doing''{"activityType":"moveCard","listName":"Doing"}''{"actionType":"addMember","username":"someuser"}'
# python3 api.py addrule BOARDID 'Remove on move from Doing''{"activityType":"moveCard","oldListName":"Doing"}''{"actionType":"removeMember","username":"someuser"}'
Admin API:
python3 api.py newuser USERNAME EMAIL PASSWORD
@@ -264,10 +277,9 @@ if arguments == 10:
boardid=sys.argv[3]
name=sys.argv[4]
type1=sys.argv[5]
settings=str(json.loads(sys.argv[6]))
# There is error: Settings must be object. So this does not work yet.
#settings = {'currencyCode': 'EUR'}
print(type(settings))
settings_arg=sys.argv[6]
# settings is optional; an empty arg -> {} (avoids json.loads('') crashing).
Loaded 100 of 913 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.