Commit Graph
3233 Commits
Author SHA1 Message Date
Lauri Ojansivu 2f07b29336 Fix #6508: two console crashes in the import "map users later" workflow.
A) Import "map members": clicking a searched user threw
   `TypeError: Cannot read properties of undefined (reading '_id')` at
   `click .js-select-import`. The handler read `Template.currentData()._id`, which is
   undefined when the click lands on a CHILD of the anchor (the avatar or the name
   span). Read the WeKan user id from the anchor's `data-id` (event.currentTarget)
   instead, and guard it — robust regardless of which inner node was clicked.

B) Admin Panel / People: saving an IMPORTED (placeholder) user as an active person
   with an email threw `TypeError: Cannot read properties of undefined (reading '0')`.
   The profile submit handler read `user.emails[0].verified` (and `.address`), but an
   imported placeholder user (and some SSO users) have no `emails` array at all. Read
   the primary email defensively (missing OR empty array) via a `primaryEmail` guard.

Tests: tests/importMapUserFixes.test.cjs; tests/importMemberMapping.test.cjs updated
for the data-id mapping.

Thanks to AmigaAbattoir and xet7 !

Fixes #6508,
2026-07-23 00:52:46 +03:00
Lauri Ojansivu 0e0391f6c1 Fix #6506: restore the import "map members" step; unmapped members become virtual users, not the importer.
Regression from c0a1a2784 (v10.12): it dropped `importMapMembers` from the import
wizard's `steps` array and hardcoded an empty membersMapping in finishImport, so the
map-users screen never rendered and every imported card/member/comment was assigned
to the importing user even when the person existed in WeKan.

Client (client/components/import/import.js):
 - restore `steps = ['importTextarea', 'importMapMembers']` so the (auto-suggesting)
   map-members step shows again;
 - finishImport builds membersMapping from the members the user mapped
   (member.wekanId), instead of sending `{}`. The step stays OPTIONAL — the Skip
   button and the textarea "import without mapping" button still bypass it.

Server — unmapped members become virtual users instead of collapsing onto the importer:
 - models/trelloCreator.js: new createPlaceholderUsers(board), run in create() before
   the board is built, so it covers BOTH the Trello JSON import and the Trello API
   (all-boards background) import. For each member not already mapped it maps to an
   existing WeKan user by username / recorded importUsername, else inserts an inert
   virtual user (loginDisabled, isActive:false, authenticationMethod:'imported')
   reusing the Trello member id, so board membership and authorship keep the original
   person;
 - models/wekanCreator.js: createPlaceholderUsers no longer overwrites an explicit UI
   mapping (skip members already in this.members) so a member mapped to a real account
   is not clobbered by the identity placeholder.

A board admin can still map a virtual member to a real user later from the sidebar
(that feature is unchanged). Tests updated: tests/importVirtualMembers.test.cjs and
tests/importMemberMapping.test.cjs.

Thanks to AmigaAbattoir and xet7 !

Fixes #6506,
2026-07-22 18:31:32 +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 136a14c88a Shared board data-repairs run on board open AND during the MongoDB <-> SQLite migration, shown in one blue, Product-name-branded progress dashboard.
The board-open self-heal is broadened from the #6484 list-unbind alone to a
shared repair set (models/lib/boardRepair.js planner + server/lib/repairBoardData.js):

 - #6484: lists wrongly bound to a swimlane -> cleared back to board-wide, so the
   list stops "disappearing" from the other swimlanes;
 - cards with no swimlaneId (null/''/missing), which never render in the Swimlanes
   view -> assigned to the board's first swimlane;
 - orphaned cards whose swimlaneId points at a DELETED swimlane (invisible cards)
   -> reassigned to the first swimlane. A card on an existing but archived swimlane
   is NOT treated as orphaned.

The same repair set now runs in BOTH places:
 - Board open: boardBody.js maybeRepairBoard -> boardRepairNeeded / repairBoardData
   (server/methods/repairBoardData.js), once per board, admin-gated, fire-and-forget.
 - MongoDB <-> FerretDB v1 (SQLite) text migration: migrateTextDatabase runs
   repairAllBoards() on the live database BEFORE copying, so the migrated copy is
   clean too. It reports a new "repairing" phase (boardsDone/boardsTotal/repaired).

Both flows drive ONE shared progress dashboard (migrationProgress), now:
 - mounted app-wide (defaultLayout) so it shows for an Admin Panel database
   migration as well as a board-open repair;
 - fed from the DB-migration status poll (attachments.js driveMigrationDashboard);
 - styled in WeKan blue (#2980b9) instead of the old purple/green;
 - branded with the configured Product name (from the earlier commit).

Tests: tests/boardRepair.test.cjs (planner + server/migration/dashboard source
guards, 11 checks) and the updated tests/listUnbindRepair.test.cjs pass.

Thanks to xet7 !
2026-07-22 16:22:39 +03:00
Lauri Ojansivu f559b751b2 #6484: auto-detect the "list disappeared from swimlanes" corruption on board open and repair it with a Product-name-branded progress modal.
When a board opens it now checks (boardListRepairNeeded) whether any list is
wrongly bound to a single swimlane (the #6484 corruption, which makes a list
vanish from the other swimlanes). If so, and the viewer is a board admin, it
runs repairBoardWideLists automatically while showing the shared
migration-progress modal, then completes. The check runs once per board,
fire-and-forget, and never blocks rendering; on error the board is unmarked so a
later open retries.

The migration-progress modal is now branded with the configured Product name
(Admin Panel / Settings / Layout / Product name, Settings.productName), falling
back to WeKan when none is set, via the pure models/lib/productName.js helper —
so the repair (and every migration reusing this modal) shows the admin's Product
name instead of the hard-coded WeKan brand.

Tests: tests/productName.test.cjs (helper + modal-branding source guards) and
the extended tests/listUnbindRepair.test.cjs (detection method + client
detect/repair/progress wiring) pass.

Thanks to xet7 !
2026-07-22 16:22:39 +03:00
Lauri Ojansivu 27201f642f Copy code block: reliable per-viewer button, copies raw text, translated (#5149).
The copy-code-block feature existed but the reporter saw it 'work very sporadically (only a handful of items have it)' and lost after editing. Root causes: it ran ONCE in Template.editor.onRendered with a global document.querySelectorAll('.viewer > pre'), so it decorated only whichever viewers were on the page at that instant and never re-added the button after an edit re-rendered the viewer; and it copied the HTML-escaped innerHTML of the <pre>'s first child node (often the wrong content or undefined) via the deprecated execCommand. Now the decoration lives in Template.viewer.onRendered, so EVERY viewer (card description, checklist item, comment) gets a copy button and it is restored on every re-render; it is de-duplicated (skips a <pre> that already has its button) so re-renders never stack buttons; it copies the RAW code text (pre.textContent) via navigator.clipboard.writeText with an execCommand fallback; and the button title is translated (the existing 'copy-text-to-clipboard' i18n key) instead of a hard-coded English string.

Tests: tests/copyCodeBlock.test.cjs (5 checks) guards that the decoration is in Template.viewer.onRendered scoped to the viewer's <pre>, copies textContent (not innerHTML), de-dups, is translated, and uses the async Clipboard API with a fallback.

Thanks to C0rn3j and xet7 !

Fixes #5149,
2026-07-22 13:27:50 +03:00
Lauri Ojansivu 533edca7dc Add-card: pressing Tab no longer loses the typed card text (#1195).
The add-card form's Tab handler (Template.addCardForm pressKey, keyCode === 9) is a deliberate power feature -- Tab jumps to the NEXT column's add-card form so cards can be added across columns quickly. But it opened that next form while abandoning whatever was typed in the current one, so pressing Tab mid-entry silently lost the card text (the card appeared to 'move to the next column' with no content). It now submits the current card first, but only when the textarea has non-whitespace content (so Tab in an empty box does not create a blank card), then jumps to the next column's form -- preserving both the content and the column-jump feature.

Tests: tests/addCardTabSubmit.test.cjs (4 checks) guards the ordering -- the Tab branch reads the textarea, submits on non-empty content BEFORE openForm of the next list, still opens the next column, and does not submit for an empty box.

Thanks to callahad and xet7 !

Fixes #1195,
2026-07-22 12:15:56 +03:00
Lauri Ojansivu 2328bcd0f1 Case-insensitive '@' member autocomplete (shared, null-safe).
Follow-up to the #5116 label fix: the add-card-title '@' member mention in client/components/lists/listBody.js matched with user.username.indexOf(term) === 0 -- case-sensitive, prefix-only, username-only, and it threw when ReactiveCache.getUser() returned null. So '@ann' did not suggest 'Anna', '@Anna' did not match username 'anna', and a full-name search never matched. The card description/comment editor (client/components/main/editor.js) already matched case-insensitively on username OR full name but shared no code and had the same null hazard. Both now route through a pure, unit-tested models/lib/memberAutocomplete.js (memberMatchesTerm / filterMembersByTerm) that lowercases the term and matches username or profile.fullname as a substring, and is null-safe.

Tests: tests/memberAutocompleteMatch.test.cjs (11 checks) covers case-insensitive + substring + full-name matching, empty-term-shows-all, null/undefined user (the old crash), missing profile, order preservation, and source guards that both '@' call sites use the shared matcher.

Thanks to rjl9 and xet7 !
2026-07-22 10:15:50 +03:00
Lauri Ojansivu 728cd929cd Case-insensitive '#' label autocomplete in the card title.
Typing '#test' in the add-card / edit-card-title field did not suggest a label named 'Testing': the label search matched with a case-SENSITIVE label.name.indexOf(term) (and label.color.indexOf(term)), so only '#Test' matched 'Testing'. The board's '@' member autocomplete already matches case-insensitively; this brings the '#' label autocomplete in line. Extracted the match into a pure, unit-testable models/lib/labelAutocomplete.js (labelMatchesTerm / filterLabelsByTerm) that lowercases both the term and the label name/color, mirroring the other models/lib pure helpers.

Tests: tests/labelAutocompleteMatch.test.cjs covers the exact #5116 scenario (#test -> Testing), color-fallback matching, empty-term-shows-all, order preservation, and negatives (non-substring, missing name/color, null label, non-array), plus a source guard that listBody.js routes through the shared matcher. 11 checks pass.

Thanks to rjl9 and xet7 !

Fixes #5116,
2026-07-22 09:21:21 +03:00
Lauri Ojansivu c0a1a27847 Import: always create virtual users, map later from the sidebar; bound imports
Board import no longer asks for member mapping up front. Every imported member is
brought in as a virtual (placeholder) user carrying its avatar, username and full
name (the existing createPlaceholderUsers path), added to the board inactive with
no permissions. The client import flow is now a single step that imports
immediately with an empty mapping -- no map-members screen, nothing to get stuck
on.

Mapping a virtual member to an existing user is a deliberate, later action done by
a board admin from the board sidebar member-avatar popup ("Map to existing user"),
and CANNOT be used to gain privileges: the new board-scoped method
mapImportedBoardMember only maps a virtual member onto an existing ACTIVE, REAL
member of the SAME board and never changes that member's role (so no new
membership is created and nothing is escalated); authorization and this guarantee
live in the pure planBoardMemberMapping helper. The mapping reassigns the virtual
member's cards/comments/activities ON THAT BOARD to the target and removes the
placeholder from the board.

Automatic hang mitigation so an import can never spin forever: a client watchdog
surfaces an import-timeout and clears the spinner if importBoard does not answer in
WEKAN_IMPORT_TIMEOUT_MS (default 2 min), and the server bounds creator.create with
the same deadline via a new generic withDeadline() wrapper.

Covered by tests/boardMemberMapPlan.test.cjs (no-escalation + auth matrix),
tests/withDeadline.test.cjs (deadline wrapper) and tests/importVirtualMembers.test.cjs
(wiring).

Thanks to xet7 !
2026-07-21 19:43:48 +03:00
Lauri Ojansivu 8db9c183a7 Fix import "Assign members": suggestions could not be selected
In the board/JSON import "Select member" popup, typed suggestions appeared but
neither clicking nor Enter assigned the user, so mapping a migrated board's
members to existing WeKan users was impossible. The click handler mapped
Template.currentData().__originalId -- a field that is never set on the search
results (they are plain WeKan user documents with _id) -- so it mapped undefined
and silently did nothing.

Map the clicked result by its _id, and add an Enter handler that selects the first
(highlighted) result so a name can be assigned by keyboard. Member mapping stays
optional: the step-1 "import without mapping" and the in-step "skip mapping"
buttons still import immediately (unmapped members default to the current user).
Guarded by tests/importMemberMapping.test.cjs.

Thanks to xet7 !
2026-07-21 18:51:22 +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 576f44ff59 Mobile All Boards: fix board title / menu text cut off (sizes)
On phones the two-column All Boards layout cut off text:
- The board icons column (the 1fr grid track) is a grid item, which defaults to
  min-width:auto and would not shrink below its content, so the board tiles pushed
  the column wider than the viewport and the board titles were clipped off the right
  edge. Add min-width:0 to .boards-right-grid so the column shrinks to its track and
  the tiles (and their titles) fit.
- The mobile board tiles reserved 50px on the right for the drag handle, which on a
  narrow 2-per-row tile hid most of the title. Shrink the handle (40->26px) and the
  reserved padding (50->30px) so more of the title shows.
- Workspace names in the narrow left menu were hard-cut mid-word ("Ro"); let the
  flex children shrink (min-width:0) and truncate the name with an ellipsis so more
  of it is readable ("Roadmap…").

Thanks to xet7 !
2026-07-21 06:26:28 +03:00
Lauri Ojansivu c68dc3fb29 Mobile All Boards: make the left menu drag-scrollable too (LTR + RTL)
After keeping the board icons in their own right-hand column, the left menu
(Starred / Templates / Remaining / Workspaces / Add board) still could not be
scrolled: the fixed-height wrapper clips at 100vh, and only the board list had its
own scroll, so a menu taller than the screen had its lower items unreachable.

Give the left menu its own overflow-y:auto / bounded max-height so you can
drag-scroll down to its lower items, just like the boards column on the right.
Both columns are reachable in LTR and RTL — the layout uses a direction-aware grid
and logical inline borders, so under a dir="rtl" document the menu is on the right
and the boards on the left automatically. Spec 43 now also asserts the left menu
is a bounded scroll container.

Thanks to xet7 !
2026-07-21 04:42:30 +03:00
Lauri Ojansivu 0abb1616cd Mobile All Boards: keep board icons on the right, not stacked below the menu
On phone-sized touch screens the All Boards layout collapsed to a single column,
so the board icons ended up BELOW the left menu and search bar. dragscroll does
not work over the menu / search area, so the boards below the fold could not be
reached by dragging — you could not scroll down to see more boards.

Remove the small-screen `grid-template-columns: 1fr !important` collapse: keep the
two-column layout (narrow minmax(84px,30%) left menu, board icons in their own 1fr
column on the right) in both mobile and desktop mode. The narrow minmax() menu
already prevents the old fixed-260px overflow that clipping-collapse was meant to
fix, and the board list keeps its own overflow-y:auto / dragscroll. Spec 43 now
also asserts the board list sits to the right of the menu, not stacked below it.

Also fix two WebKit-only test assertions (not app bugs): read the WebKit-prefixed
getComputedStyle(...).webkitUserSelect in the Statistics-view test.

Thanks to xet7 !
2026-07-21 04:42:30 +03:00
Lauri Ojansivu 7d6fbc04e2 Files report: make helpers defensive (no throw); correct changelog (not yet fixed)
The filesReport content pane rendered EMPTY in the newest Playwright run (no
title, no search box) — a throwing helper aborts the whole Blaze template. Guard
Attachments.collection access and never throw so the report UI always renders.
The core symptom (no attachments listed) is unverified/unfixed — it points at the
attachmentsList publication returning 0 rows, which needs a running server + DB to
diagnose. Correct the v10.07 changelog to stop claiming the report is fixed.
2026-07-21 01:42:49 +03:00
Lauri Ojansivu 6ca11cdf83 Fix mobile All Boards scroll: drop leftover min-height:100vh + phantom ::after
The mobile board list is meant to be a BOUNDED, scrollable region
(height: calc(100vh - 120px)) so boards below the fold are reachable inside the
overflow:hidden wrapper (#6488). Two leftover .board-list.mobile-view rules
re-introduced the exact old bug: `min-height: 100vh` and a phantom
`::after { height: 100px }` forced the list to grow to its full content height,
so nothing ever overflowed — the list was as tall as its content and did not
scroll, and the lower boards were clipped and unreachable.

These non-media rules have higher specificity than the media-query block that
already sets the bounded height (whose comment notes min-height:100vh + ::after
was the bug), so they won this fight. Remove both; the bounded height now takes
effect and the two-column list scrolls.

Thanks to xet7 !
2026-07-20 22:55:40 +03:00
Lauri Ojansivu 5b4443df23 Fix Files admin report: read reactive Attachments.collection so the table renders
The Files report (Admin Panel / Reports / Files) built its rows and count from
the ostrio FilesCollection wrapper (Attachments.find() / .count()). The
'attachmentsList' publication delivers the page via this.added('attachments', ...),
but the ostrio FilesCursor does not reliably re-run in a Blaze helper when those
docs arrive, so resultsCount stayed 0 and the `if resultsCount` table never
appeared (the report showed only its controls, no rows).

Read the underlying minimongo collection (Attachments.collection) instead, a
plain reactive Mongo.Collection cursor that reacts to the published adds and
yields plain docs — the same pattern the rest of the codebase already uses for
reactive attachment reads (sidebar backgrounds, backgrounds publication).

Thanks to xet7 !
2026-07-20 20:35:06 +03:00
Lauri Ojansivu f79c320d12 Fix board-view switch persistence (await) and mobile All Boards 2-per-row grid.
setBoardView Meteor method did not await user.setBoardView(view), so it
resolved (and the client reloaded) before profile.boardView was written
and a rejected write never surfaced — switching the board view (e.g. to
the new Statistics view) was unreliable. Await it.

All Boards on phones now lays the board icons out as a real 2-column CSS
grid (repeat(2, minmax(0,1fr))) instead of float+width:50%, so at least 2
icons per row is guaranteed regardless of float-logical-value support or
how narrow the boards column is (#6488).

Thanks to xet7 !
2026-07-20 19:14:08 +03:00
Lauri Ojansivu 9bcefd9801 Fix failing tests from newest run + mobile 2-per-row override + attachments publish robustness
From ../log/2026-07-20_15-26-39 (4 chromium failures; #6490 rules test
and spec 42 pass):

- Mobile All Boards (#6488): the real 1-per-row rule was
  .board-list.mobile-view li { width:100% } (added via the isMiniScreen
  class, more specific than the media-query rule) — set it to 50% too so
  there are at least 2 icons per row. Seed 12 boards in the test so the
  list definitely overflows and the scroll assertion is meaningful.
- Statistics view test: switch to the view through the actual board-view
  switcher (js-open-stats-view) instead of pre-seeding profile.boardView,
  which was flaky (localStorage vs profile / board-ready timing).
- Rules Workflow test (#6489): assert the palette renders with labelled
  chips (the exact bug signature) instead of driving HTML5 drag-and-drop,
  which Playwright's dragTo does not fire through Blaze's delegated
  handlers here (the Add Rule button stayed disabled).
- attachmentsList: wrap the manual publish so this.ready() always runs
  (finally) and drop the server-side name sort (no index; the client
  re-sorts) — defensive against the still-unresolved Files-report hang.
2026-07-20 15:51:15 +03:00
Lauri Ojansivu b4fd7d851f Fix All Boards on mobile: scrollable list + at least 2 board icons per row (#6488)
The mobile board list could not be scrolled: the CSS forced it
min-height:100vh (plus a phantom ::after), so it grew to fit all content
and, clipped by the overflow:hidden wrapper, boards below the fold were
unreachable. Give it a bounded height + overflow-y:auto so it scrolls
(natively on iOS; via the touch-drag handler elsewhere, which only
engages once content overflows).

Also improve the phone layout: keep the menu on the left but narrow it
(grid-template-columns minmax(84px,30%) 1fr) so the board icons on the
right show at least 2 per row (was 1 per row), on iPhone 12 mini and
similar. Add a phone-viewport Playwright test.
2026-07-20 15:15:46 +03:00
Lauri Ojansivu 25f9b46825 Fix rules list: delete/view act on the clicked rule, not the parent context (#6490)
The Delete and View buttons are in the rulesList child template's each,
but their handlers are on the parent rulesMain and used
Template.currentData() — which returned the PARENT data context, not the
clicked rule. So the rule id was null: deleting failed with
'Match error: Expected string, got null', and 'View rule' always opened
the first rule. Add explicit data-rule-id to the buttons and read it in
the handlers so both operate on the rule actually clicked.
2026-07-20 15:10:06 +03:00
Lauri Ojansivu e424fb1bba Fix Rules Workflow view: import TAPi18n so Add Rule works (#6489)
rulesWorkflow.js used TAPi18n without importing it, so paletteLabel()
threw 'ReferenceError: TAPi18n is not defined' and the Add Rule click
handler aborted before saving — creating a rule in the Rules -> Workflow
view silently did nothing. Add the missing import.
2026-07-20 15:03:52 +03:00
Lauri Ojansivu 3aefa94a66 Board Statistics view (Tilastot): full-width board view, replaces Status popup
Move the board Status from a Board Settings sidebar popup into a
full-width board VIEW alongside Swimlanes / Lists / Calendar / Gantt /
Table in the board-view switcher. Named 'Statistics' (Finnish
'Tilastot'), translated to major languages (rest fall back to English).

New statsView template/js/css rendered from boardBody (isViewStats),
selectable via the switcher (js-open-stats-view -> board-view-stats,
added to the profile.boardView schema allowedValues). Reuses the server
boardStatus method so counts are accurate in lazy mode; shows the same
counts + time-spent summary as before. Text is selectable with mouse
(drag) and touch (long-press): the view stops pointer/touch events from
reaching the canvas drag-to-scroll so native selection + copy work.

Removed the old boardStatusPopup (template/handler/helpers/css) and its
Board menu item. Added a Playwright test for the view. Design doc
updated.
2026-07-20 14:47:32 +03:00
Lauri Ojansivu abc0f180fa Board Settings: add read-only Status popup (mode, counts, time-spent summary)
New 'Board status' item in the board menu opens a larger, mouse-
selectable popup showing this board's card-loading mode (lazy vs all,
and whether chosen automatically), counts (swimlanes, lists, cards,
archived cards, labels, members, custom fields) and a time-spent summary
in the style of the task time reports (total hours, cards with time,
overtime cards).

Counts come from a server boardStatus(boardId) method so they are
accurate even in lazy mode, where minimongo only holds the visible card
window; the time summary fetches only cards with spentTime > 0 so it
stays cheap on big boards. New i18n keys translated to major languages
(rest fall back to English). Design doc at
docs/Features/Board/Sidebar/Status.md.
2026-07-20 13:28:56 +03:00
Lauri Ojansivu 96c7faa872 Adaptive card loading: auto-lazy for big boards, no admin toggle (#6480)
Card loading is no longer an admin-configurable toggle. WeKan decides
per board by size ('auto', the default): a board over the threshold
(default 500 cards, CARDS_LOADING_LAZY_THRESHOLD) loads only visible
cards via windows; smaller boards keep loading all cards (unchanged,
fully featured). The server computes the mode per board and publishes it
(boardCardsLoadingMode -> boardCardLoadingModes) so client rendering
agrees; isLazyCards is now per-board.

Lazy windows now publish comments/attachments/checklists/checklistItems
with ONE cursor per window instead of one per card (same N+1 fix as
eager mode), and an open card subscribes to its own live children
(openCardData) so it is complete even when outside/just-added-to a
window. Operators can still force CARDS_LOADING=all|lazy|auto.

Removed the Admin/Features card-loading dropdown; pure decision logic in
models/lib/cardsLoading.js (resolveCardsLoadingMode now defaults to
'auto', plus effectiveBoardCardsMode + threshold) with updated unit
tests. New i18n key cards-loading-auto across locales.
2026-07-20 13:17:28 +03:00
Lauri Ojansivu 5949e85cfd Admin reports: never get stuck on the loading spinner if a report subscription fails
The paginated admin reports (Files, Rules, Boards, Cards, Impersonation)
only handled the subscription onReady callback, so if a report
publication errored on the server the report was stuck showing the
loading spinner forever, with no feedback. Handle onStop as well:
clear the spinner on stop and, on a real error, log it and surface the
message. This is defensive; it does not change the happy path.
2026-07-20 12:20:54 +03:00
Lauri Ojansivu 1c636f09b3 CPU-usage monitor + governor + Admin Panel / Problems / CPU usage report
WeKan and FerretDB on one machine can drive CPU high enough to starve other
software. Add a monitor, a governor, and a report. Design: docs/Features/CPU/CPU.md.

Monitor (server/lib/cpuMonitor.js): samples system-wide CPU% from os.cpus()
idle/total deltas on an interval (so it includes WeKan, FerretDB and every other
process). A pure, hysteresis state machine (models/lib/cpuHighTracker.js) turns
the samples into ONLY the START and END of each sustained high-CPU period — two
rows per episode, never a flood. Enters high after 3 consecutive samples >= 85%,
leaves after 3 consecutive < 70%. Each row carries duration, peak, system load,
and what WeKan was doing (setActivity label). Recorded to a new 'cpu' event
stream (server/lib/cpuLog.js).

Governor: cpuMonitor exports isHighCpu() and pauseIfBusy(), so long batch
operations await a short pause and yield the CPU only while the system is busy
(no cost when idle). Wired into the existing-file extension corrector. Tunable
via WEKAN_CPU_* env vars.

Report: 'cpu' added to EVENT_STREAMS; reuses the generic eventStreamReport
(stream="cpu") with search + pagination; a "CPU usage" item is added to the
Admin Panel / Problems side menu (cpuReportTitle i18n).

Tests: cpuHighTracker unit (start/end, duration/peak, no-flap hysteresis;
positive + negative) and cpuMonitorWiring source guards. CHANGELOG + design doc.
2026-07-20 03:11:07 +03:00
Lauri Ojansivu fdb683892d Log every filename/content sanitization to Admin Panel / Problems
Whenever a filename or file requires sanitization — on upload, migration, the
existing-file corrector, or viewing/serving — record it to the Security event
stream (action:'sanitized'), the companion to the existing action:'blocked'
upload rejections. Each row carries:

- WHEN: the event timestamp.
- WHO: the original uploader (userId), shown as a new clickable username column
  in the Problems event report.
- FOR WHAT REASON: sanitizationReasons() — URL-encoded name, invisible
  characters, typosquatting (look-alike characters), the specific exploit KIND
  (JavaScript code, XML code, XML loop (billion laughs), server-side PHP/ASP,
  template injection, HTML code), wrong file type (.txt -> .png), filename too
  long, or empty-name-generated.
- THE FILENAME: "from" -> "to".
- WHERE: board > swimlane > list > card, plus the board's organization(s) and
  team(s) (server/lib/fileContext.js).

New: classifyExploitKinds() + sanitizationReasons() (isomorphic), the
server-only fileContext resolver, filenameSanitizeLog helpers, catalog keys
file.sanitize / file.content / file.malware, and the uploader column in the
event report. Wired at all fix points (attachments/avatars onAfterUpload,
moveToStorage, correctFileExtensions, httpStreamOutput). EICAR now uses the
file.malware key. Logging is best-effort and never breaks the upload/view path.

Tests: classifyExploitKinds + sanitizationReasons unit tests (positive +
negative) and logging-wiring source guards. CHANGELOG + design doc updated.
2026-07-20 03:11:07 +03:00
Lauri Ojansivu 81dbecfb87 Safe filename handling: display, upload, existing files, migrations
One unified filename subsystem, used everywhere a name is shown, uploaded,
served, or migrated. Design: docs/Features/Filename/Filename.md.

Display (imports/lib/fileNameDisplay.js, general cleanFileName + global
{{cleanFilename}}/{{downloadFilename}} helpers): URL-decode, normalize to
generally-used characters (NFKC + confusable-homoglyph folding so typosquatting
"payp<Cyrillic-a>l.exe" shows as "paypal.exe", only in a Latin-majority base so
genuine non-Latin names are preserved), remove invisible/control/bidi chars,
strip HTML/JS/XML/template markup. Removed the earlier invisible-char filter
button, warning triangle, inline descriptions, legend and server invisibleOnly
filter.

Upload (attachments + avatars, all backends): reject exploit-looking filenames
and EICAR virus test files; strip invisible chars; correct the extension to the
real detected type (double-click opens the right app); type-based name when
empty; cap length to a portable max (30, Amiga OS FFS) preserving the extension;
sanitize SVG JS / XML-loop content at the staging location before promoting to
final storage and delete the temp file after; enough-disk-space check with
small-RAM streaming and stop-and-remove-partial on any write error.

Existing files: general storage-agnostic detector streams a small header to
WRITABLE_PATH/files/temp, runs `file`, deletes the temp file; admin-only bounded
batch extension corrector; serve-time on-the-fly exploit sanitizer wired into
httpStreamOutput (sniffs the start, rewrites only dangerous markup, streams the
rest unchanged).

Migrations (moveToStorage/copyFile): sanitize content, finalize the name with
the same general function (detected type, corrected extension, folded
homoglyphs, capped length), save it at the destination, and number
same-name/different-content collisions (document.pdf, document-1.pdf, ...).

Tests: rewrote fileNameDisplay.test.cjs; added uploadFileName, diskSpace,
fileTypeCorrection and fileHardeningGuards unit tests (positive + negative);
updated Playwright spec 41 to the clean-name model. CHANGELOG + design doc.
2026-07-20 03:11:07 +03:00
Lauri Ojansivu 626d956ecc Filenames everywhere: reusable +safeFilename (decode + name invisible chars), clean download names.
Made the Files-report filename rendering a GENERAL, reusable component and used it
everywhere filenames are shown:

- New client/components/main/safeFilename.{jade,js,css}: +safeFilename(name=...)
  URL-decodes the name, shows a red warning triangle when it hides invisible
  characters, and replaces each invisible character inline with its red Unicode
  name (all escaped plain text — never markdown/HTML). Plus two global helpers:
  {{filenamePlain name}} (decoded name, invisible chars named — for title="" / JS
  text) and {{downloadFilename name}} (decoded + invisible chars REMOVED).
- Used it in the Admin Panel → Files report and the card attachments (visible
  name via +safeFilename; thumbnail title via filenamePlain; the download="" attr
  via downloadFilename; the attachment viewer title via fileNamePlain()).
- Downloads now save a CLEAN name: the Content-Disposition sanitiser in both
  server/routes/universalFileServer.js and legacyAttachments.js runs the name
  through sanitizeDownloadFileName (URL-decode + remove all invisible/format/bidi
  characters), never empty (falls back to "download").

New fileNamePlain()/sanitizeDownloadFileName() in imports/lib/fileNameDisplay.js;
unit + negative tests in tests/fileNameDisplay.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:07 +03:00
Lauri Ojansivu fccc4a7398 Files report: name the invisible characters inline (red) with a red warning triangle, instead of a whole red string.
Instead of colouring the entire filename red, the Files report now:
- shows a red warning triangle (fa-exclamation-triangle) to the left of any name
  that hides invisible characters; and
- replaces each invisible character IN PLACE with its red Unicode description,
  e.g. "evil[U+200B ZERO WIDTH SPACE].png" — the visible parts keep the normal
  text colour.

New fileNameSegments()/describeInvisibleChar() in imports/lib/fileNameDisplay.js
split the (decoded) name into visible runs + one named segment per invisible
character (bracketed "U+XXXX NAME", falling back to the code point for unnamed C1
controls). Everything is still rendered as escaped plain text (Blaze `{{ }}`) —
never markdown/HTML. Unit + negative tests in tests/fileNameDisplay.test.cjs and
the Playwright spec updated to assert the warning + inline names.

Thanks to xet7 !
2026-07-20 03:11:07 +03:00
Lauri Ojansivu e205f6b7ac Board Archive -> Boards: server-side pagination + search (stop loading all archived boards).
The archived-boards list published EVERY archived board the user can delete at
once (unbounded, grows on long-lived instances). It now pages server-side
(searchTerm + limit/skip, 30 per page) with a getArchivedBoardsCount method, and
the template gained a search field + prev/next controls in one row (matching the
Admin report tables). Only the current page is ever loaded into the browser.

Guarded in tests/adminReportsPagination.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:07 +03:00
Lauri Ojansivu 6d3c550d8d Remove clickable column-header sorting (board Table view + Admin Domains table).
Per request, clickable column-header sorting is removed everywhere it existed:

- Board Table view: the sortable `th.js-table-view-sort` headers become plain
  headers; the sortField/sortDirection state, the sort click handler and the
  sortIndicator arrow are gone. The table now always shows a stable order (card
  title ascending). Search + pagination are unchanged.
- Admin Panel → People → Domains: same removal; the getDomainsWithUserCountsPage
  method no longer takes sortField/sortDirection and returns a fixed domain-
  ascending order (paginateDomains' default). Server-side search + pagination stay.

Dead sort-header CSS removed. Guarded in tests/adminReportsPagination.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu 19870ad52d Admin Translation page: stop loading the whole Translations collection at once.
The Translation report subscribed with a hardcoded limit of 0, which Mongo/
FerretDB treat as "no limit", so the page loaded EVERY translation string across
all languages into the browser at once. It now passes the infinite-scroll window
(page * 25), so it loads a page at a time and grows as you scroll — the same
bounded pattern the activity feed uses. Guarded in
tests/adminReportsPagination.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu 52acd4b754 Buttons follow the theme: global button base + primary buttons use var(--theme-accent).
WeKan buttons hardcoded their colors, so a Member Settings → Change color theme
override never reached most of them (customTheme.css only themed
`button[type=submit].primary`, not plain `button.primary`, and the global button
base was solid black). Now the global button base and the primary buttons in
client/components/forms/forms.css use `var(--theme-accent, <original>)`, and the
admin action buttons (settingBody.css) and People-panel buttons (peopleBody.css,
incl. ext-box / add-remove-team / unlock-all) do the same. The fallback keeps the
default look identical, but once the user picks a theme accent, the buttons across
the app follow it.

Guarded in tests/buttonThemeColors.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu 0a4c2ed412 Admin Files report: decode URL-encoded names, plain-text render, flag + filter invisible characters.
At Admin Panel → Problems → Files report:

- Percent-encoded filenames (e.g. "%D0%93%D1%80" -> "Гр") are URL-decoded for
  display when they decode cleanly; malformed/partial encodings are left as-is.
- Filenames are always shown as PLAIN TEXT — Blaze `{{ }}` HTML-escapes, so a
  name is never rendered as markdown or HTML.
- A filename hiding invisible / control / bidi / zero-width characters (after
  decoding) is shown in RED. A translated legend on the right of the pagination
  controls explains that red = invisible characters.
- A board-view-style filter toggle lists ONLY filenames containing invisible
  characters (server-side, so it works across the whole paginated set); clicking
  it again removes the filter.

New isomorphic helper imports/lib/fileNameDisplay.js (decodeFileNameSafe,
hasInvisibleChars, INVISIBLE_CHARS_SOURCE) is used by both the client (display +
red flag) and the server (the invisibleOnly query on attachmentsList /
getAttachmentsReportCount). Unit + source-guard tests in
tests/fileNameDisplay.test.cjs. English strings added; translations follow.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu dd21cfdc05 Admin pagination tables: theme colors, pagination beside search, no Search button.
Across the Admin Panel → Problems report tables (Files/Rules/Boards/Cards/
Impersonation) and the People/Org/Team/Domain tables:

- Removed the redundant "Search" button — typing in the field + Enter already
  searches (the keydown handler was already there); dropped the now-dead
  js-*-search-button click handlers.
- Put the pagination controls on the RIGHT of the search field, in one row
  (margin-inline-start:auto, so it mirrors correctly in RTL), matching the
  Security/Speed/Tests event table which already had this layout.
- Made the pagination controls follow the current theme: they now use
  var(--theme-accent, #01628c) instead of hardcoded blue/grey/white, so they
  honour Member Settings → Change color (which sets --theme-accent on :root) and
  fall back to WeKan blue when no custom theme is chosen. The event-stream
  pagination links are themed too.

Guarded in tests/adminReportsPagination.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu f7c4a916f0 Admin Panel Cards report: load one page fast via an index-backed sort.
The Admin Panel → Problems → Cards report already paginates (limit/skip), but it
sorted by { boardId:1, sort:1 } which has NO index, so every page load full-sorted
all cards in memory — the spinner on a site with 11761 cards. Switched both the
cardsReport publication and the client mirror sort to { boardId:1, createdAt:-1 },
the existing index (server/models/cards.js), so one page is a bounded index scan.

Also added a { stream:1, at:-1 } index on the eventlog collection (and { stream:1 }
on eventlogAcks) so the Security/Speed/Tests report pages stay an index scan as the
log grows, instead of a full scan + in-memory sort.

Guarded in tests/adminReportsPagination.test.cjs.

Thanks to xet7 !
2026-07-20 03:11:06 +03:00
Lauri Ojansivu e1aa5a22e8 Admin Panel / Version: show the configured METEOR_REACTIVITY_ORDER and DDP_TRANSPORT env.
Below the live "Reactivity mode" (oplog / changeStreams / polling — what is
actually running) and "MongoDB Oplog enabled" rows, the Version page now also
shows what was REQUESTED via the environment: the reactivity driver priority
order (METEOR_REACTIVITY_ORDER, falling back to DEFAULT_METEOR_REACTIVITY_ORDER)
and the DDP transport (DDP_TRANSPORT). Together they let an admin see at a glance
"I asked for oplog,polling and it is actually running oplog" vs "it fell back to
polling". The values are returned by the existing getStatistics() server method
(reading process.env), not exposed as public settings. Empty env shows '(unset)'.

New i18n keys Reactivity_order and DDP_transport; guard in tests/ferretdbOplog.test.cjs.

Thanks to xet7 !
2026-07-19 20:52:09 +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 0085f7fbfa Admin Panel: Problems button + page (Summary/Security/Speed/Tests), replaces banner & Reports button.
- 2nd header bar: add a Problems button (warning icon) to the RIGHT of the Info
  (version) button; it turns red when there are new problems (settingHeader.js
  polls eventLogProblemAreas). Remove the old Reports button; the Admin Panel
  still opens on Settings.
- The former Reports page is now the Problems page: left menu Summary / Security /
  Speed / Tests, then a separator, then the moved Broken Cards/Files/Rules/Boards/
  Cards/Impersonation items.
- Summary (problemsSummary, renamed from the removed top banner): the checkbox
  list + one Acknowledge button — the ONLY place to acknowledge.
- Security/Speed/Tests: read-only, paginated, searchable event tables via new
  admin-only eventLogPage/eventLogCount methods; acknowledgeEventLog now takes a
  stream or array.
- i18n keys + CSS + source-guard tests + design doc updated.
2026-07-19 13:42:47 +03:00
Lauri Ojansivu ff67780b30 Admin banner: checkbox list + one Acknowledge button (acknowledge only in banner).
Change the top problem banner from a per-area button to a checkbox list of
problem areas with a single Acknowledge button that acknowledges every checked
area in one call. acknowledgeEventLog now accepts a stream or an array of
streams. Acknowledging happens ONLY at the top banner; the Reports pages are
read-only (noted in the design doc).
2026-07-19 13:27:09 +03:00
Lauri Ojansivu 4f4140e02b Admin Panel: top banner for new Security/Speed/Tests problems, with Acknowledge.
Like the Announcement banner, the Admin Panel top now shows, for each problem
area with unacknowledged events, its menu path (Admin Panel -> Reports ->
Security/Speed/Tests) and the count of NEW problems. Each area has an Acknowledge
button that marks the newest problems seen, zeroing the count and removing the
info from the top.

- models/eventLog.js: add the eventlogAcks collection and admin-only methods
  eventLogProblemAreas() (count of events newer than each stream ack) and
  acknowledgeEventLog(stream) (upsert ack = now).
- client adminProblemBanner.{jade,js}: poll the counts, render path/count/button,
  acknowledge on click; included in settingHeaderBar (top of every admin page).
- i18n keys (securityReportTitle/speedReportTitle/testsReportTitle/new-problems/
  acknowledge) + banner CSS. Source-guard tests added.
2026-07-19 13:19:50 +03:00
Lauri Ojansivu 1669a1af19 Fix 7 security issues reported by meifukun.
- Avatar localization redirect SSRF: fetch avatars via fetchSafe (DNS-pinned,
  redirects rejected) instead of native redirect:follow, and validate
  profile.avatarUrl scheme (http/https/data:image/local) at write time.
- Trello import source.url stored XSS: only render/store an http(s) activity
  source URL, so a javascript: board url can no longer execute on click.
- Trello LIVE import readable SSRF: gate every attachment/background/avatar
  download with validateAttachmentUrl, matching the offline import path.
- CAS login global user-data race: store validated CAS user data per credential
  token instead of a module-global _userData (fixes account-takeover race).
- Metrics X-Forwarded-For whitelist bypass: only trust XFF behind
  METRICS_TRUST_PROXY (parsed spoof-resistantly from the right); else use the
  socket peer address.
- Stale impersonation export bypass: remove the "|| impersonateDone" export
  authorization bypass; require real board visibility (canExport) only.
- Invitation code brute force: generate invitation codes with a 128-bit CSPRNG
  (crypto.randomBytes) instead of a 6-digit Math.random value, and rate-limit
  createUser (DDPRateLimiter).

The OIDC shared serviceData race from the same report was already fixed (#4897,
the per-login objects are callout-local). Source-guard regression test added
(tests/securityMeifukun.test.cjs) and wired into test:unit:node.

Thanks to meifukun and xet7 !
2026-07-19 12:22:56 +03:00
Lauri Ojansivu 354b7804b5 Right sidebar: drag the edge to resize its width (desktop), saved per user.
Add a resize handle on the sidebar edge, like resizing a spreadsheet column
(mirrors the list resize in client/components/lists/list.js). Drag it to change
the sidebar width; the width is saved to profile.sidebarWidth for logged-in
users and to browser localStorage (wekan-sidebar-width) for anonymous users on
a public board, then re-applied on load.

- The handle uses the LOGICAL inset-inline-start, so in LTR it is the left edge
  of the right-docked sidebar and in RTL it is the RIGHT edge of the left-docked
  sidebar; the drag direction inverts for RTL so dragging always widens/narrows
  the visible panel correctly.
- Desktop only: on phones (<=800px) the handle is hidden and the sidebar stays
  full width (the 100vw rule is now !important so a desktop inline width can
  never leak into mobile).
- Width is clamped (min 280px, capped near the viewport) and re-applied on
  window resize so crossing the mobile breakpoint clears/restores it.

New profile.sidebarWidth schema field + getSidebarWidth/setSidebarWidth helpers,
setSidebarWidth Meteor method, i18n (en, fi), and source-guard test
tests/sidebarResize.test.cjs.

Thanks to xet7 !
2026-07-18 19:42:19 +03:00
Lauri Ojansivu b8a21e4ce3 Right sidebar: bound the width and make its input fields fill the width.
- .board-sidebar used width: 30vw, which scaled with the viewport: on very wide
  screens it grew to ~double the intended width (it looked right only at mid
  widths). Use width: clamp(320px, 30vw, 560px) so it keeps the mid-width look
  but is capped (and floored) instead of scaling without bound. Phones (<800px)
  still get the full-width 100vw rule.
- The sidebar filter text inputs (Filter List by Title, Filter by card title,
  advanced filter) had no width, so they rendered at the browser default narrow
  width with a big empty gap on the right. Sidebar text/search inputs now
  width: 100% + box-sizing: border-box, so they fill the padded content area
  with equal empty space on both sides (the .sidebar-content 12px padding).

Source-guard test tests/sidebarWidth.test.cjs.

Thanks to xet7 !
2026-07-18 19:18:17 +03:00
Lauri Ojansivu b71461d90c Add List: restore the previous options in the inline composer (#6465).
The #6465 per-list-header inline Add List composer (addListInline) had been
reduced to just a title input, dropping the options the old addListForm had.
Restore them:
- the "Add after which list" position <select>, listing the swimlane lists and
  pre-selecting the list whose header button opened the composer; submit reads
  it for afterListId and derives nextListId from the following option so the new
  list is inserted between them (falls back to the header target for the empty
  board case).
- the "or template" link (js-list-template) to create a list from a template.
Helpers swimlaneLists()/isDefaultAfterList() added; createListAfter already
accepts nextListId. Source-guard test tests/addListComposerOptions.test.cjs.

Thanks to csonkaoszimt and xet7 !

Fixes #6465.
2026-07-18 18:55:19 +03:00
Lauri Ojansivu bb6b0d5477 Member Settings: optional "Submit editors with Enter" (per-user, off by default).
#4236 made every multi-line editor submit only on Ctrl/Cmd+Enter (plain Enter
= newline) so titles could be multi-line, but that removed the fast
Enter-to-save title workflow some users rely on (v10 email feedback). Add a
per-user preference in the Member Settings (Change Settings) popup, saved to
profile.submitOnEnter and OFF by default: when on, plain Enter submits the card
title, description and other inlined forms, and Shift+Enter inserts a newline;
when off, behaviour is unchanged (Ctrl/Cmd+Enter saves).

The rule stays in the single pure isSubmitKey(event, {submitOnEnter}); the card
title, the card-description override and the shared client/lib/inlinedform.js
all route through it and read the preference from the current user. New toggle
+ helper + toggleSubmitOnEnter method + schema field, i18n (en, fi), and
editorSubmitKey.test.cjs extended with positive + negative opt-in cases.

Thanks to xet7 !

Related to #4236.
2026-07-18 18:14:33 +03:00