Commit Graph
279 Commits
Author SHA1 Message Date
Lauri Ojansivu 0470adcf3b snap wekan.problems: neutral check titles so a failing check does not read contradictorily.
The login-page checks used affirmative titles ("ROOT_URL is set", "A migration or
repair is in progress"), so a FAILING one printed e.g. "[error] ROOT_URL is set:
ROOT_URL is not set" — confusing. Use neutral subjects ("ROOT_URL", "Migration or
repair") so the "[severity] <title>: <detail>" line reads correctly in every state.

Thanks to xet7 !
2026-07-23 10:49:13 +03:00
Lauri Ojansivu fae1ccd176 FerretDB high CPU: run FerretDB standalone (no OpLog) by default; enable the replica set / OpLog only when wekan-ferretdb-oplog=true.
The 10.16-10.21 reports of the ferretdb process pegging ~2 cores for a long time
while node sat idle are the signature of an OpLog tail: Meteor holds a tailable
cursor on local.oplog.rs and FerretDB re-scans it server-side. wekan-control
already defaults to polling-only (it clears MONGO_OPLOG_URL), but ferretdb-control
passed --repl-set-name unconditionally, so FerretDB ALWAYS maintained the capped
OpLog (a second write per mutation, a once-a-minute cleanup, and a tailable cursor
for any stray MONGO_OPLOG_URL to spin on) even when WeKan would not tail it.

Gate --repl-set-name on WEKAN_FERRETDB_OPLOG (default false), matching
wekan-control: in the default polling-only mode FerretDB now runs STANDALONE, with
no OpLog at all; the replica set / OpLog is created only when the admin opts in with
wekan-ferretdb-oplog=true. Opting in is now also cheaper on the FerretDB fork side
(the {ts: {$gt}} tail filter is pushed down and the OpLog Timestamp indexed).

Docs: docs/Features/Admin-Panel/Problems/Migrations.md (FerretDB reactivity mode).
Test: tests/ferretdbOplogGating.test.cjs.

Thanks to xet7 !
2026-07-22 16:51:09 +03:00
Lauri Ojansivu 5a1c9a61e9 Admin Panel / Problems "Status" overview + snap run wekan.problems: show migrations/repairs in progress and login-page "Must be logged in" causes; run repairs server-side.
Motivated by the 10.16-10.21 reports where, after an upgrade, the login page
showed "Must be logged in" or sat on the logo + "Loading, please wait." spinner,
pages took 5+ minutes, All Boards counts stayed 0 and i18n showed raw keys — the
FerretDB v1 (SQLite) process pegged ~2 cores for a long time under WeKan's poll
load, i.e. a migration/repair or a saturated backend, with no way to see it
without the Admin Panel.

Server-side, independent of any client / progress page:
 - server/startup/repairBoardsOnStartup.js runs the shared board data-repairs once
   (version-gated, background, 30s after boot) so corrupted data is fixed even if
   no user opens a board and nobody watches a progress page.
 - The MongoDB <-> SQLite text migration and the startup repair now PERSIST their
   progress to the text_migration_status collection (models/textMigrationStatus.js),
   so a separate process can see "a migration/repair is in progress" (an in-memory
   object could not).

Status overview (server/lib/systemStatus.js), shown in Admin Panel / Problems ->
Summary and via the method systemStatusReport (admin-gated, server/methods/
systemStatus.js):
 - everything in progress (text migration, board repair, recovery, cron migration,
   attachment migration) aggregated from the persisted status collections;
 - detected problems (broken cards; failing login-page checks);
 - login-page "Must be logged in" / "Loading, please wait" cause checklist
   (migration in progress, ROOT_URL, LDAP, Sandstorm) — models/lib/loginProblems.js;
 - a CPU detail area that calls out a pegged ferretdb process and the mitigation.

Snap: `snap run wekan.problems` (snap-src/bin/wekan-problems[.mjs], snapcraft app
`problems`) prints the same overview as text — problems, migrations/repairs in
progress, login checks — so an admin with only server access can see it WITHOUT
Admin Panel access. Sub-commands: migrations | login | broken-cards | cpu | help.

Pure helpers models/lib/loginProblems.js and models/lib/problemsOverview.js are
unit-tested; tests/problemsStatus.test.cjs (14 checks) also source-guards the
server hub, method, startup pass, migration persistence and snap command.

Thanks to xet7 !
2026-07-22 16:22:39 +03:00
Lauri Ojansivu b15d89dd0c FerretDB: default to polling-only on all platforms; OpLog is opt-in (#6503).
FerretDB v1 can tail an OpLog, but on the SQLite backend Meteor's tailable+awaitData OpLog tail keeps FerretDB CPU pinned (reporters saw ~190-390% even idle; a 2-core box maxed out) and a struggling tail also surfaces as 'oplog catching up took too long', stalling board and login-page loading. Despite the FerretDB-side mitigations (16 MiB capped oplog, 500ms awaitData poll, SQLite pragmas, CPU self-regulation) the peg persisted on real deployments, and the polling-only path reliably drops CPU to ~10%. So make POLLING the default on every launcher (snap wekan-control, bundle start-wekan.sh, docker wekan-entrypoint.sh + docker-compose.yml, windows start-wekan.bat, sandstorm start.js): WEKAN_FERRETDB_OPLOG now defaults false, docker-compose reactivity is 'polling' with MONGO_OPLOG_URL commented out. The OpLog machinery still exists and is opt-in via WEKAN_FERRETDB_OPLOG=true. The snap exposes it as a setting: 'snap set wekan wekan-ferretdb-oplog=true|false' (registered in snap-src/bin/config, default false). The calmer polling throttle (#6467/#6468) and SQLite pragma tuning keep poll-and-diff cheap.

Tests: tests/ferretdbOplog.test.cjs updated to assert the polling-only default everywhere, the commented-out docker-compose MONGO_OPLOG_URL, the opt-in OpLog machinery still present, and the new wekan-ferretdb-oplog snap setting (default false).

Thanks to xet7 !
2026-07-22 10:35:52 +03:00
Lauri Ojansivu a4c57cf775 FerretDB polling mode: unset MONGO_OPLOG_URL so Meteor never tails the OpLog
Merely having MONGO_OPLOG_URL set makes Meteor start an OpLog tail at boot (a
tailable+awaitData cursor on local.oplog.rs) regardless of the reactivity order,
and on FerretDB that tail polls the database continuously -- pinning CPU even
with no clients connected. The polling-only branch of each launcher set
METEOR_REACTIVITY_ORDER=polling but left an already-set MONGO_OPLOG_URL in place,
so "polling only" still tailed the OpLog.

Every FerretDB launcher (snap wekan-control, bundled start-wekan.sh, Docker
wekan-entrypoint.sh, Sandstorm start.js) now actively clears MONGO_OPLOG_URL in
its polling-only branch, so choosing polling truly does no OpLog tailing. The
OpLog-enabled (default) path is unchanged. Covered by tests/ferretdbOplog.test.cjs.

This complements the FerretDB fork fix that stops awaitData from busy-polling the
tail in the default OpLog mode (wekan/FerretDB, wekan/wekan#6498).

Thanks to Alishara, bluetopaz1204, mueschel and xet7 !

Fixes #6498,
2026-07-21 10:56:41 +03:00
Lauri Ojansivu 8e4d57c21d Show a recovery maintenance page on all FerretDB v1 platforms
During a #6492 data recovery, users should never see a bare connection error.
Two layers now cover the whole window on every FerretDB v1 launch path (snap,
bundled release, Docker):

- In-app spinner (already present): the server clears the RECOVERY_IN_PROGRESS
  marker only after a real health probe, so the full-screen overlay behaves
  identically everywhere.
- Static bridge page (new, cross-platform): for the brief window while a
  just-restored FerretDB comes back up and before Meteor can serve the client,
  the launch scripts serve a tiny standalone "recovering your data" page (HTTP
  503) on the web port. The snap reuses wekan-maintenance-page.mjs with a
  recovery wording; release/Docker serve the portable recovery-bridge.mjs. The
  bridge is time-bounded (WEKAN_RECOVERY_BRIDGE_SECONDS, default 20s) so it can
  never block WeKan from starting, hands straight over to the in-app spinner, and
  is skipped if its page file or the marker is absent.

The launch scripts do NOT delete the marker; only the server does, after
verifying the database serves reads. Tests enforce the bridge is bounded, is
guarded by the page file, and that no script deletes the marker.

Thanks to bluetopaz1204, mueschel and xet7 !

Fixes #6492,
2026-07-21 10:06:41 +03:00
Lauri Ojansivu 9ea7f11d27 Show a full-page maintenance spinner while data recovery is in progress
#6492: while the SQLite text data is being recovered/remediated, users should see a
maintenance spinner instead of errors or half-loaded data.

- A published singleton status (models/recoveryStatus.js) carries an "active" flag +
  message. The publication (server/publications/recoveryMaintenance.js) is PUBLIC so
  even logged-out users on the sign-in page see it; the toggle method
  (setRecoveryMaintenance) is admin-only.
- The client overlay (client/components/main/recoveryMaintenance.*) shows a full-screen
  spinner + message when active; it is subscribed globally and rendered in both the app
  (defaultLayout) and sign-in (userFormsLayout) layouts, above modals/popups.
- The startup scripts write a RECOVERY_IN_PROGRESS marker when they restore/re-migrate.
  On startup the WeKan server (server/recovery.js) shows the spinner while the marker is
  present, verifies the database serves reads (probe), then clears the marker and the
  spinner — or, if it still cannot read, keeps the spinner and records that manual
  recovery is required. Admins can also toggle maintenance for a server-initiated
  re-migration.

Tests: tests/recoveryMaintenance.test.cjs (wired into npm run test:unit:node) — the
publication is public while the toggle is admin-gated, the overlay is subscribed and
rendered in both layouts above everything, the server shows the spinner from the marker
and clears it only after a health probe, and the scripts write the marker. Mirror-
validated (no Node runtime here); shell/brace/JSON all checked.

Thanks to bluetopaz1204, mueschel and xet7 !

Fixes #6492,
2026-07-21 10:06:41 +03:00
Lauri Ojansivu 63c77a27f4 Automatic recovery/remediation for the SQLite text data + Admin Panel Recovery report
Completes the #6492 data-safety system on the WeKan side (FerretDB detection/repair/cap
landed in the fork):

- Decision brain: models/lib/recoveryPlan.js decideRecovery() picks the least-invasive
  recovery when the database is KNOWN corrupt (latest good backup -> previous backup ->
  re-migrate from MongoDB -> manual). It never acts on a healthy/unknown database and
  never restores a known-bad backup.
- Startup restore/re-migrate: every FerretDB launch path (snap, bundled release, Docker)
  can restore the text data BEFORE FerretDB opens the files, on request
  (WEKAN_FORCE_RESTORE env or a RESTORE_REQUESTED marker). It copies a known-good backup
  INTO the live database (dropping stale WAL side-files); backups are never deleted and
  the main wekan.sqlite is only overwritten, never removed. remigrate is delegated to
  migration-control. Attachments/avatars on the filesystem are untouched.
- Recovery events: the scripts append JSON lines to recovery-events.jsonl; the server
  (server/recovery.js) imports them into the recoveryEvents collection on startup, plus
  an admin-only recordRecoveryEvent method.
- Admin Panel / Problems / Recovery: a new admin-only report
  (server/publications/recoveryReport.js, wired in adminReports.js/.jade + i18n) lists
  the remediation history newest-first, readies up front and fetches directly (no hang).
- Docs: docs/Features/Admin-Panel/Problems/Recovery.md.

Tests (wired into npm run test:unit:node), verified by mirror since there is no Node
runtime here to execute them:
  - recoveryPlan.test.cjs (decision logic, positive + negatives)
  - recoveryEventsJsonl.test.cjs (parser: skips junk, normalizes severity, never throws)
  - recoveryReportQuery.test.cjs (search selector escapes regex metacharacters)
  - recoveryReportWiring.test.cjs (report wired; publication/count/method admin-gated)
  - ferretdbTextDataBackup.test.cjs extended: restore copies from a backup, is
    request-gated, and NEVER deletes the live text data or a backup copy

Thanks to bluetopaz1204, mueschel and xet7 !

Fixes #6492,
2026-07-21 10:06:40 +03:00
Lauri Ojansivu 10b7487d9b Keep a rotating backup of the text-data database, ready to restore on corruption
Safety measure for #6492: before FerretDB opens its SQLite files (at rest), every
FerretDB launch path now keeps a rotating backup of the TEXT-DATA database
(wekan.sqlite*) in a "backup" subfolder of the same data dir, so a known copy is ready
to restore if the live database is ever detected corrupt (FerretDB's own quick_check
logs corruption on open). Attachments and avatars live on the filesystem and are not
copied.

The critical safety property: the backup only ever COPIES from the live database — it
never moves or deletes the live wekan.sqlite, and never wipes the data dir. The
previous generation is kept under backup/prev so a bad copy cannot overwrite the last
good one on its own. Disable with WEKAN_SQLITE_BACKUP=false.

- snap-src/bin/ferretdb-control
- releases/ferretdb/start-wekan.sh
- releases/ferretdb/wekan-entrypoint.sh

tests/ferretdbTextDataBackup.test.cjs verifies each path copies wekan.sqlite* into the
backup dir, keeps a previous generation, is guarded, and — the critical negative —
NEVER moves/deletes the live text data, never wipes the data dir, and never copies
attachments/avatars.

Thanks to bluetopaz1204, mueschel and xet7 !

Fixes #6492,
2026-07-21 10:06:40 +03:00
Lauri Ojansivu 54b1e9cd17 Reset FerretDB simulated OpLog on start to prevent bloat/corruption high CPU
Per the #6492 investigation, FerretDB v1's SQLite simulated OpLog (local.oplog.rs,
stored in the `local` database = local.sqlite) is not reliably capped, so over time
it bloats or corrupts; Meteor then busy-polls it and FerretDB CPU pegs at 300%+ even
when idle. A reporter confirmed that deleting local.sqlite* drops CPU straight back
to ~10%.

The `local` database is transient SYSTEM data — the OpLog + replica-set metadata,
NOT user data: boards, cards and attachments live in wekan.sqlite. So every FerretDB
launch path now resets ONLY the `local` database files before starting FerretDB,
which recreates a fresh, correctly-capped OpLog from --repl-set-name. This prevents a
corrupt/bloated OpLog from ever persisting across a restart:

- snap-src/bin/ferretdb-control
- releases/ferretdb/start-wekan.sh (bundled release, before each restart-loop start)
- releases/ferretdb/wekan-entrypoint.sh (FerretDB Docker image)

Guarded by WEKAN_FERRETDB_RESET_OPLOG (default true; set false to keep the OpLog).
tests/ferretdbOplogReset.test.cjs verifies each path resets local.sqlite, is guarded,
and — the critical safety property — NEVER deletes wekan.sqlite or the whole data dir.

Thanks to bluetopaz1204, mueschel and xet7 !

Fixes #6492,
2026-07-21 10:06:40 +03:00
Lauri Ojansivu 383b3dd432 Fix FerretDB high CPU: never send $changeStream to FerretDB (snap).
FerretDB v1 does not implement MongoDB change streams. The snap's general
default METEOR_REACTIVITY_ORDER is "changeStreams,oplog,polling", and it
arrives in wekan-control already set, so the FerretDB `:-oplog,polling`
fallback did not apply. Meteor then issued $changeStream aggregates that
FerretDB rejected as "not implemented" thousands of times per second - a
busy-loop that pinned FerretDB CPU (100-390%, 168% seen on 8 cores) and
starved board/card loading, so cards would not open (almost all commands
were those failed aggregates).

For FerretDB, force changeStreams OUT of the reactivity order whatever it
was set to, leaving oplog + polling; real MongoDB is unchanged and keeps
change streams. Polling-disabled path is polling only.

Thanks to xet7 !

Fixes #6492,
Fixes #6493.
2026-07-20 19:14:08 +03:00
Lauri Ojansivu fb5a98648a FerretDB: keep polling as the reactivity fallback so a broken OpLog never blocks startup.
Follow-up to enabling the FerretDB OpLog by default (#6480, #6481). Setting
MONGO_OPLOG_URL alone can make Meteor fail to start if OpLog tailing does not
actually work (the same way WeKan on Windows with MongoDB falls back to polling).
Fix: every FerretDB launch path now also sets
METEOR_REACTIVITY_ORDER=oplog,polling — Meteor tries the drivers left-to-right and
uses OpLog ONLY when tailing works, otherwise polling, so a broken/absent OpLog
never blocks startup. Previously only docker-compose set this; the snap
(wekan-control), the bundle (start-wekan.sh/.bat) and the docker entrypoint
(wekan-entrypoint.sh) set MONGO_OPLOG_URL WITHOUT the polling fallback. The
kill-switch path (WEKAN_FERRETDB_OPLOG=false) now selects polling-only.

Admin Panel / Version already reports which driver is actually live: server/
statistics.js derives "Reactivity mode" (oplog / changeStreams / polling) and
"MongoDB Oplog enabled" from Meteor's live _oplogHandle, so it shows `polling`
whenever OpLog did not come up — the direct way to confirm whether FerretDB is
on OpLog or polling.

Tests: tests/ferretdbOplog.test.cjs now asserts the oplog,polling fallback in
every launch path, the polling-only kill-switch path, and that the Version page
detects + displays the live reactivity driver.

Thanks to uusijani, Nissulya and xet7 !

Fixes #6480,
Fixes #6481.
2026-07-19 20:49:47 +03:00
Lauri Ojansivu 1039b2e328 FerretDB: enable OpLog tailing by default so Meteor stops poll-and-diff (fixes high CPU).
With FerretDB there is no MongoDB oplog, so Meteor observed every live query by
poll-and-diff, pinning FerretDB CPU at 100-390% on busy boards (#6480, #6481).
FerretDB v1 now ships an OpLog (wekan/FerretDB: auto-created capped local.oplog.rs
+ replica-set hello handshake), so every FerretDB launch path now:

- starts ferretdb with --repl-set-name=rs0 (snap ferretdb-control, docker
  entrypoint + docker-compose service, bundle start-wekan.sh/.bat), which makes
  FerretDB auto-create the OpLog and advertise the replica set; and
- points WeKan at it with MONGO_OPLOG_URL=.../local?replicaSet=rs0 (docker-compose
  also switches METEOR_REACTIVITY_ORDER to oplog,polling), so Meteor TAILS the
  OpLog instead of polling.

Enabled by default with a kill-switch: WEKAN_FERRETDB_OPLOG=false forces the old
polling-only behaviour. The calmer polling defaults remain as the fallback for
queries Meteor cannot observe via the OpLog. The transient migration ferretdb
(migration-control) deliberately stays oplog-free so bulk inserts are not slowed.

Also cut the client activity feed page from 500 to 50 rows: 500 was far more than
the sidebar/card feed shows and, under polling, was re-fetched and re-diffed on
every poll; infinite scroll still loads more on demand (the server cursor is
index-bounded either way).

Tests: tests/ferretdbOplog.test.cjs source-guards every launch path (repl-set arg,
MONGO_OPLOG_URL, kill-switch) plus the activity page size, and a negative test that
migration-control stays oplog-free.

Thanks to uusijani, Nissulya and xet7 !

Fixes #6480,
Fixes #6481.
2026-07-19 20:33:06 +03:00
Lauri Ojansivu 2f7b49d8de Fix FerretDB migration non-finite doubles, LDAP email mapping, and export listener leak.
#6481 (Nissulya): after MongoDB 3.x -> FerretDB v1 migration, cards with a
non-finite `sort` (±Infinity / NaN, written by old WeKan before #6472) failed to
insert: FerretDB/SQLite rejects non-finite doubles ("invalid value { sort: +Inf }
(infinity values are not allowed)"), aborting the document and leaving boards
stuck on the loading spinner. sanitizeNonFinite() only made the bare mongoexport
tokens PARSE; the parsed value was still a JS Infinity/NaN. Added clampNonFinite()
in the migration tool to reset every non-finite number to a finite 0 before
insert, and a `nonfinite-sort-repair` startup schema-upgrade step that heals the
same corruption in an already-running database (MongoDB or FerretDB), using a
predicate built from finite literals only so it is safe on FerretDB too.

#6481 (Nissulya): LDAP `mail` -> `email` fieldmap returned `undefined` (worked in
6.09). getDataToSyncUserData's email branch read the mapped attribute
case-SENSITIVELY (ldapUser.hasOwnProperty / ldapUser[field]) while the rest of
wekan-ldap uses the case-insensitive getLDAPValue(); a directory returning the
attribute with a different case (exposed by ldapjs -> ldapts, which preserves the
server's case) missed it. The email branch (and the addLdapUser fallback) now
read via getLDAPValue().

#6480 (uusijani): board/CSV export paused on socket backpressure with
`res.once('drain', resolve); res.once('error', reject)`, which leaked one 'error'
listener per backpressured write ("MaxListenersExceededWarning: 11 error
listeners added to [ServerResponse]"). writeWithBackpressure() now removes both
listeners once either settles.

Added tests and negative tests for all three fixes.

Thanks to Nissulya, uusijani and xet7 !

Fixes #6480,
Fixes #6481.
2026-07-19 18:36:11 +03:00
Lauri Ojansivu e0991d6602 Migration: keep cards/activities whose export has NaN/Infinity doubles.
mongo 3.x mongoexport emits non-finite doubles as the bare tokens NaN,
Infinity, +Infinity and -Infinity, which are not valid JSON, so EJSON.parse
threw ("Unexpected token N/+") and migrate-mongo3-to-ferretdb.mjs dropped the
WHOLE document — leaving boards/cards missing after an otherwise "successful"
migration. sanitizeNonFinite() now rewrites those bare tokens, only outside
string literals, to the canonical EJSON v2 double form {"$numberDouble":"..."}
that EJSON.parse accepts, so the document migrates instead of being lost.
Strings that merely contain "NaN"/"Infinity" and ordinary negative numbers are
left untouched. Test tests/migrateNonFinite.test.cjs (with negative cases).

Thanks to Nissulya and xet7 !

Fixes #6481.
2026-07-18 16:42:11 +03:00
Lauri Ojansivu 580276079e Fix #6476: snap FerretDB wait loop had no timeout, so WeKan hung with no web port.
With database=ferretdb, snap-src/bin/wekan-control waits for FerretDB to accept
connections before starting node - WeKan does not open its HTTP port until the
database answers. That wait loop had no timeout and no diagnostics, so if
FerretDB never started listening (crashed or arch-incompatible per-arch binary,
locked/corrupt SQLite, or the service left disabled by a failed migration
hand-off), WeKan blocked there forever: the service showed active but the port
never opened and the only symptom was a silent 'FerretDB not ready yet' loop.
uusijani's wekan.log stopped right after the startup env dump, confirming
control never reached node main.js.

Mirror the MongoDB branch: after WEKAN_DB_WAIT_TIMEOUT seconds (default 120,
overridable) print an actionable hint once, then keep retrying - pointing to
'snap logs <snap>.ferretdb', naming the SQLite dir to check for locks/corruption,
the arch-mismatch case, the snap start --enable recovery, and the
'snap set <snap> database=mongodb' fallback. Regression test
tests/ferretdbWaitTimeout.test.cjs, including a negative guard that the wait is
not a silent unbounded loop again.

Thanks to uusijani (report) and xet7 (fix).
2026-07-17 15:04:48 +03:00
Lauri Ojansivu 9ddbc777dd Startup schema upgrade, self-healing attachments, migration speed; fix
#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.
2026-07-17 03:07:59 +03:00
Lauri Ojansivu 65bf215cbe Fix #6473: attachments missing after the MongoDB -> FerretDB migration.
- 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.
2026-07-17 01:43:53 +03:00
Lauri Ojansivu 794346e396 cpu-exec qemu-user fallback everywhere; tests for all newest fixes.
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.
2026-07-16 17:40:24 +03:00
Lauri Ojansivu ed19445d30 Fix #6459 #6455 #6458 #6466 #6467 #6468 #6472 #6465 #5547 #5282.
- #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.
2026-07-16 17:08:31 +03:00
Lauri Ojansivu ab1c636e71 Snap: resume an interrupted MongoDB to FerretDB migration instead of redoing it
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.
2026-07-15 15:57:04 +03:00
Lauri Ojansivu 7d271413bf Snap: make database setting authoritative + wekan.database command + no-downtime on failure
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.
2026-07-15 08:58:36 +03:00
Lauri Ojansivu 73ad55f696 Snap migration: skip dotted collections (FerretDB rejects them) + clean partial SQLite
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.
2026-07-15 08:58:36 +03:00
Lauri Ojansivu 88e692ef43 Snap migration: fast-fail the mongod-7 readiness probe when the temp mongod has died
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.
2026-07-15 07:32:58 +03:00
Lauri Ojansivu b9a9bf1593 Snap: require a NON-EMPTY FerretDB SQLite before treating the migration as done
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.
2026-07-15 07:32:58 +03:00
Lauri Ojansivu 8db071f94c Snap: fix db-eval driver resolution (ESM ignores NODE_PATH) - the root cause
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.
2026-07-15 07:32:58 +03:00
Lauri Ojansivu 0ebf34aedc Snap: add wekan.migrate + wekan.maintenance commands and a maintenance page
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.
2026-07-15 07:32:58 +03:00
Lauri Ojansivu 3f42d4d3f0 Snap: decide FerretDB vs MongoDB from SQLite data, not the migration marker
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.
2026-07-15 07:32:57 +03:00
Lauri Ojansivu 2c895107a0 Snap migration: cite snapd seccomp allow-list in the disk-space guard comments
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.
2026-07-15 05:44:32 +03:00
Lauri Ojansivu 22a4f419ce Snap: harden MongoDB 6/7 -> FerretDB importer + never start an empty FerretDB
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.
2026-07-15 05:35:33 +03:00
Lauri Ojansivu 48e47e6687 Snap: RAM-aware WiredTiger cache so MongoDB starts on < 34 GB RAM boxes (#6454)
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.
2026-07-15 05:23:16 +03:00
Lauri Ojansivu 22804fdf38 Snap: make the MongoDB->FerretDB migration-done marker authoritative
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.
2026-07-15 04:58:44 +03:00
Lauri Ojansivu 87783b6f15 Snap: on startup, start the configured database instead of waiting forever
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.
2026-07-15 04:49:16 +03:00
Lauri Ojansivu 3cc9dccb3c Snap: start FerretDB after migration (internal snapctl set skips the configure hook)
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
2026-07-15 04:38:52 +03:00
Lauri Ojansivu c3e36d4a2e Snap mongo3->FerretDB: disk guard keys off free-space availability, not platform
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.
2026-07-15 03:14:08 +03:00
Lauri Ojansivu 2676e8d434 Snap mongo3->FerretDB: Snap/Sandstorm-aware disk-space guard
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.
2026-07-15 03:14:08 +03:00
Lauri Ojansivu 96428115f4 Snap mongo3->FerretDB: per-file progress, streaming, and disk-space guard
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.
2026-07-15 03:14:08 +03:00
Lauri Ojansivu c6a75de076 Snap mongo3->FerretDB: fix "EJSON.parse unavailable" (wrong anchor roots)
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.
2026-07-15 03:14:08 +03:00
Lauri Ojansivu faf7ff6efa Snap: migrate ANY MongoDB (3/7/other) to FerretDB SQLite, then shut down MongoDB (#6454)
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.
2026-07-14 18:09:03 +03:00
Lauri Ojansivu 3a4f3c896e Migration dashboard: use Admin Panel product name instead of WeKan
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).
2026-07-14 18:02:43 +03:00
Lauri Ojansivu 0514a2fa5d Snap: don't misdetect MongoDB 7 as MongoDB 3.x on upgrade (#6454)
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.
2026-07-14 17:42:44 +03:00
Lauri Ojansivu b8bdbcd2d7 Sandstorm/Snap migration: auto-open All Boards after migration; WeKan-themed dashboard
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.
2026-07-13 03:57:11 +03:00
Lauri Ojansivu 3476cd17c7 Sandstorm/Snap migration: extract Meteor-Files GridFS attachments + parse legacy v1 binary
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.
2026-07-13 03:28:13 +03:00
Lauri Ojansivu cab231ee3a Sandstorm/Snap migration: use the modern mongodb driver (OP_MSG) for FerretDB inserts
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.
2026-07-13 02:56:35 +03:00
Lauri Ojansivu beefe441d7 Sandstorm/Snap migration: resolve bson/mongodb from the modern server bundle, not the old base
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.
2026-07-13 02:36:09 +03:00
Lauri Ojansivu 0e0a8bbb33 Sandstorm/Snap migration: load mongodb/bson via createRequire; resolve EJSON robustly
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.
2026-07-13 02:20:23 +03:00
Lauri Ojansivu b0703befd7 Sandstorm/Snap migration: get EJSON from the mongodb driver, not a bare bson import
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.
2026-07-13 02:07:24 +03:00
Lauri Ojansivu 6142396ae9 Sandstorm/Snap migration: connect mongoexport to 127.0.0.1 (IPv6 localhost broke it)
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.
2026-07-13 01:49:21 +03:00
Lauri Ojansivu 3076f2317e Sandstorm/Snap migration: drop mongoexport --quiet (it hid the error) + live activity log
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.
2026-07-13 01:23:38 +03:00
Lauri Ojansivu 62df991532 Sandstorm/Snap migration: diagnose mongoexport failures + live progress dashboard
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.
2026-07-13 00:58:54 +03:00