Compare commits

...
1796 Commits
Author SHA1 Message Date
Lauri Ojansivu 58ad81e301 continuousBackup integration suite: stop each engine before its directory is removed.
Three tests registered the hook that removes their temp directory before the hook
that stops their engine. node:test runs after-hooks in the order they were
registered, not last-in first-out, so the directory was gone when stop() sealed
into it, and the ENOENT (files/index.ndjson) surfaced after the test as
"generated asynchronous activity after the test ended" - which failed the whole
suite although all 8 tests passed. One hook now stops the engine first and then
removes the directory. Five runs in a row, and the suite runner, pass.

Thanks to xet7 !
2026-11-04 19:20:58 +02:00
Lauri Ojansivu 2912f4b080 Close #6870: attachments are already off site-wide in Admin Panel / Attachments / Transfer limits.
How: Admin Panel / Attachments / Transfer limits, set "Attachments upload" and
"API upload" to Blocked. That is the site-wide "Disable attachments" the issue
asks for, as two settings instead of one checkbox.

Comment for the issue:

  A site-wide switch for this already exists, so a new "Disable attachments"
  setting is not needed. Open Admin Panel / Attachments / Transfer limits and set
  "Attachments upload" and "API upload" to Blocked. While they are on, the server
  refuses an upload on every path - the upload pipeline, the api.attachment.upload
  and api.attachment.copy methods, and the /api/attachment upload,
  upload-background and copy routes - on every board, including boards created
  later, and no board's allowsAttachments flag has to be touched. Download, move
  and delete are not affected, so files uploaded earlier stay reachable. What was
  missing is that a card still showed the "+" button and accepted a drop for an
  upload that would then be refused; since d8b3579117 the card asks the server
  once a minute and hides both. There is no second setting, so nothing can
  disagree with what the server enforces. Two settings instead of one checkbox
  is the one difference from the request.

Thanks to Raymond-Voogt and xet7 !

Fixes #6870,
2026-11-04 19:17:53 +02:00
Lauri Ojansivu 97d1eca09d CHANGELOG: In short for the upcoming release. 2026-11-04 19:12:14 +02:00
Lauri Ojansivu 95b75177b8 CHANGELOG: continuous backup runs on one server at a time, with a lease. 2026-11-04 19:12:06 +02:00
Lauri Ojansivu 880ec2d707 Continuous backup runs on one server at a time, with a lease.
With several WeKan servers on one database, every server applied the continuous
backup setting, so each of them tailed the oplog and wrote its own archives into
the same target directory (#6861, item 3).

The leader holds a lease: one continuousBackupLease document with its id and an
expiry. Taking it is one atomic operation (an insert while there is none, else
an update that matches only an unheld, own or expired lease), the leader renews
it every 15 seconds and it lapses after 45, so another server takes over within
a minute when the leader stops. A server that loses the lease stops its stream.
Startup, saving the settings and a change saved on another server all converge
through that loop, and a restore or a fetch from the cloud takes the lease and
waits one renewal so it is the only writer. One server is always its own leader.

Checked against the real driver on FerretDB (SQLite): 24 clients at once give
exactly one leader; renewal, expiry, loss of the old leader and release behave.
tests/continuousBackupLease.test.cjs pins the same and the wiring in
server/continuousBackup.js. docs/Backup/Continuous-Backup.md no longer says
single-server only.

Thanks to Raymond-Voogt and xet7 !

Refs #6861, not closed: the remaining items of the issue were fixed in 4e28465964.
2026-11-04 19:11:32 +02:00
Lauri Ojansivu dbc7cccea3 CHANGELOG: a card no longer offers the attachment plus when uploads are blocked for the whole site. 2026-11-04 19:07:11 +02:00
Lauri Ojansivu d8b3579117 A card no longer offers the attachment "+" when uploads are blocked for the whole site.
#6870 asks for a site-wide switch that turns attachments off, because the
per-board allowsAttachments flag has to be set on every board. That switch is
there already: Admin Panel / Attachments / Transfer limits, "Attachments upload"
and "API upload" set to Blocked, and the server refuses on every path - the
upload pipeline, api.attachment.upload and .copy, and the /api/attachment
upload, upload-background and copy routes. What was missing is that the card
still showed the "+" button and accepted a drop for an upload that would be
refused.

attachmentUploadPolicy tells a signed-in client the one boolean (nothing about
the limits), asked at most once a minute, and the "+" and the drop/paste upload
follow it. No second setting is added, so there is nothing that could disagree
with what the server enforces. tests/attachmentUploadPolicy.test.cjs pins the
client, the method, and the server enforcement it relies on, and that no
"disableAttachments" setting exists.

Thanks to Raymond-Voogt and xet7 !

Refs #6870, not closed: the reporter asked for a single Features checkbox, and the
two Blocked limits are two settings.
2026-11-04 19:07:09 +02:00
Lauri Ojansivu c7e53e9f38 CHANGELOG: an integration update for a missing or wrong-board integration returns 404. 2026-11-04 19:04:14 +02:00
Lauri Ojansivu 6012521ccc An integration update for a missing or wrong-board integration returns 404.
PUT /api/boards/:boardId/integrations/:intId answered 200 with the requested id
when no integration matched that id and board (PR #6871). The two activities
routes (POST and DELETE .../activities) had the same fault: they updated
nothing and answered 200 with `data: null`. All three now look the integration
up on that board after board-admin authorization and answer the JSON 404 the GET
route already uses (#6862), before validating or writing anything.

tests/restIntegrationUpdate.test.cjs and the Playwright spec are the PR's.
tests/restIntegrationWriteRoutes404.test.cjs adds the activities routes and
pins, for the whole file, that every handler that updates an integration looks
it up first and only after authorization.

Thanks to AhmedLukman and xet7 !
2026-11-04 19:04:12 +02:00
Lauri Ojansivu 0195a5b3d4 Merge remote-tracking branch 'origin/main' 2026-11-04 15:02:36 +02:00
Lauri Ojansivu b12e761afb CHANGELOG: missing-board exports return 404 and an over-long export token 400. 2026-11-04 14:50:49 +02:00
Lauri Ojansivu 0c0d666ffa Missing-board exports return 404, and an over-long export token returns 400.
The board Excel export answered a missing board with HTTP 200 and the text
"Board not found" (PR #6869). The two PDF export routes had the same fault, and
the Excel and PDF routes answered an over-long auth token with 200 too, while
export.js already used 404 and 400. They now choose their status the same way.

tests/boardExcelNotFound.test.cjs and the Playwright spec are the PR's.
tests/exportErrorStatuses.test.cjs scans every models/export*.js route for a
text answer without a status, so the fault cannot return anywhere there.

Thanks to AhmedLukman and xet7 !
2026-11-04 14:50:33 +02:00
Lauri Ojansivu fc2ad0a48b CHANGELOG: nodemailer, body-parser, qs, uuid and postcss-selector-parser raised in the bundle. 2026-11-04 14:45:57 +02:00
Lauri Ojansivu dad03959d9 Raise nodemailer, body-parser, qs, uuid and postcss-selector-parser in the bundle.
A Grype scan of the image found advisories in packages that Meteor's own
packages carry in programs/server/npm/node_modules/meteor/<package>, which
package.json cannot reach.

nodemailer 8.0.11 -> 10.3.1: meteor/email only calls createTransport, the
'stream' plugin hook, nodemailer/lib/well-known and nodemailer/lib/mail-composer,
and all four work in 10.3.1 (checked with the bundle's own package specifiers,
through its exports map). body-parser 1.20.6/1.20.8 -> 1.20.9 and 2.3.0 ->
2.4.0: a minimum may now be "name@MAJOR", so each major is raised inside
itself. qs -> 6.16.1, postcss-selector-parser 6.1.4 -> 6.1.5.

uuid 8.3.2 and 9.0.1 -> 11.1.1: 11 moved its entry point, which is why it was
refused before. The bumper now writes the recorded dist/index.js back as an
ESM re-export of dist/esm/index.js, which require() loads and which leaves
uuid's own ESM and CJS entry points as they are.

glob 14 and minimatch 11 are already in package.json. Run against a copy of
the published image's bundle, the bumper raises all of these and every raised
copy loads.

Thanks to the Grype scan reporter and xet7 !
2026-11-04 14:45:04 +02:00
github-actions[bot] ac960e7d81 Bump versions for v12.94 2026-11-04 01:07:15 +00:00
Lauri Ojansivu 3213d7fa1d Prepare v12.94 release 2026-11-04 03:06:07 +02:00
Lauri Ojansivu 4757676abf CHANGELOG: on a phone in desktop mode the opened card starts below the header bar. 2026-11-04 03:04:05 +02:00
Lauri Ojansivu efaf94242e On a phone in desktop mode the opened card starts below the header bar.
The header wraps to several rows on a phone, far taller than the 8px the
floating card docks at, so the card's top row sat among the header's rows and
was only seen by dragging. Below 801px a card without an inline top, popup
cards included, starts at --wekan-header-height + 8px; a dragged place is an
inline top and is never overridden.

Thanks to the iPhone 12 mini reporter and xet7 !
2026-11-04 03:03:59 +02:00
Lauri Ojansivu d87baa0244 CHANGELOG: the opened card covers the header bar on an iPad. 2026-11-04 02:56:44 +02:00
Lauri Ojansivu 006d24502c The opened card covers the header bar on an iPad: the header drops to the content's level.
On iPad Chrome and Safari (both WebKit) the opened card's top row - the collapse
caret, the card number, the menu, maximize and X - was behind the header bar,
although the card has z-index 2001 and the header 1000. iOS WebKit makes every
scrolling element (#content, the board wrapper) a stacking context of its own, so
the card's z-index is compared only inside it while the header bar, a flex item of
body and a stacking context too, is painted over all of it. While a card is open
the header bar (#header-quick-access) is now at z-index 0, the level of the
content, which comes after it in the page and so paints over it; with no card open
it keeps 1000.

A browser spec makes #content and the board wrapper stacking contexts with
isolation: isolate, which is the same trap, and shows the card over the header open
and collapsed, its close button reachable, in Chromium and WebKit; without the rule
it fails with the card's top at 8 behind a header 88 high, as in the screenshot.

Thanks to xet7 !
2026-11-04 02:56:33 +02:00
Lauri Ojansivu 8f4efbca57 Merge remote-tracking branch 'origin/main' 2026-11-04 01:35:33 +02:00
Lauri Ojansivu dde09dead5 CHANGELOG: where the time goes when a WeKan board is opened. 2026-11-04 01:21:48 +02:00
Lauri Ojansivu 3621413d6f Speed test: where the time goes when a WeKan board is opened, on FerretDB and on MongoDB.
app/board-open.cjs seeds a board of 200, 2000 or 5000 cards, opens it in Chromium and
records the wall clock to the first card and until the DDP WebSocket is quiet, what
crossed it, the Meteor server's CPU by who spends it (Node inspector) and FerretDB's
CPU (pprof). Two runs on each database: the Meteor server's CPU is the same on both
(0.1-0.5 s) and the DDP server is 1% of it; a 5000-card board takes FerretDB (SQLite)
400-600 ms longer to open, the data sent to the browser being the same ~210 KB, a
small board 70-100 ms. FerretDB uses about 4 s of CPU for the 5000-card open: SQLite
39%, filtering in Go 34%, find/getMore/aggregate 0.7-0.8 s each. The README says what
was measured and what was not: the development build, one machine, the browser's own
time not separated, and the aggregate's source not identified.

Thanks to Raymond-Voogt and xet7 !
2026-11-04 01:21:16 +02:00
github-actions[bot] 3c3fc07b3c Bump versions for v12.93 2026-11-03 23:04:07 +00:00
Lauri Ojansivu c1e8976b28 Prepare v12.93 release 2026-11-04 01:02:51 +02:00
Lauri Ojansivu dcce935310 CHANGELOG: the opened card and popups move under a finger. 2026-11-04 01:00:21 +02:00
Lauri Ojansivu 1b05673200 The opened card and every popup move under a finger: Pointer Events and touch-action.
On an iPad the opened card did not move when dragged by a finger from the collapse
caret, the space beside it, the card number, or the drag handle, while the page
behind it scrolled. The card was moved by mousedown / mousemove / mouseup, and a
finger sends none of them while it moves; the drag areas also lacked
touch-action: none, so the browser took the gesture for a scroll. The card window
is now moved by Pointer Events: a press on the header row or the handle moves it, a
press on a link or button in the row (the caret, close, the menu) waits for a real
drag so a tap still does its job and a drag does not also collapse or close, the
title still edits, and with drag handles on the handle alone moves the window. The
card number is above the title's edit zone so it drags too, the window is kept
within reach, and touch-action: none is on the card header, its handle and every
popup's title bar and handle. Blaze hands over a jQuery event without isPrimary, so
the native event is read.

A browser spec drags with a real touch (DevTools protocol, Pointer Events) from the
caret, the number and the space beside the title, with handles on and off, a tap
that still collapses, a mouse that still drags and a popup; without the fix the
finger tests fail.

Thanks to xet7 !
2026-11-04 01:00:19 +02:00
Lauri Ojansivu dafb63299f CHANGELOG: a popup card covers the header bars. 2026-11-04 00:42:59 +02:00
Lauri Ojansivu 84b0480aa8 An opened card opened as a popup covers the header bars, in every browser.
A card opened as a popup (a search result, a table row, a Roadmap/Gantt bar) got
the generic wide-layout rule top: 97px and z-index 30, because the desktop-mode
rules that dock an opened card at the top (top: 8px, z-index 2001) skip a popup.
So it opened BELOW the header, and collapsed it was a bar floating over the
swimlane, while the same file's popup wrapper rule says it must cover the header
bars. The popup card now docks at top: 8px with z-index 2001 in the wide layout;
mobile mode keeps its own full-window rule. A browser spec shows the card over
the header, open and collapsed, in Chromium and WebKit, and fails without the fix
(top 97 against a header 88 high).

Thanks to xet7 !
2026-11-04 00:42:57 +02:00
Lauri Ojansivu f6fc97e77a The full screen button says Minimize while the page is in full screen.
The same button leaves full screen; its title and name were still Maximize, which
does not say so. The existing Minimize translation is used, so no language file
changes.
2026-11-04 00:29:00 +02:00
Lauri Ojansivu d7105e531d CHANGELOG: a full screen button in the header bar. 2026-11-04 00:27:30 +02:00
Lauri Ojansivu f25957dcf3 A full screen button in the header bar, to the right of the logo.
It puts the whole page in full screen with the Fullscreen API (the unprefixed one
and the webkit-prefixed one) and takes it out again; the icon follows the
browser's own events, so Esc is understood. The label is the existing Maximize
translation, so no language file changes. It is drawn only where the browser
offers the API and the page may use it: not on iPhone, not in an iframe without
allowfullscreen. Unit tests drive the pure part with fake documents, including a
refusal; a browser spec checks the position, the label, entering, leaving, Esc,
and that nothing is drawn where the API is not enabled, in Chromium and WebKit.

Thanks to xet7 !
2026-11-04 00:27:28 +02:00
Lauri Ojansivu 984ea93aa8 CHANGELOG: Dependabot alerts 180 and 183-187, and the SQLite membership test fix. 2026-11-04 00:13:58 +02:00
Lauri Ojansivu 2441563008 Speed test: SQLite after the membership test fix (68.3 ms, 4.1x), the Due Cards profile and what was tried.
Thanks to Raymond-Voogt and xet7 !
2026-11-04 00:13:34 +02:00
Lauri Ojansivu f3950b9adc Dependabot alerts 180 and 183-187: minimatch in the other lockfiles, and the Go modules of the embedded benchmark.
minimatch before 11.0.1 (GHSA-g3hv-75wg-m249): npm-packages/exceljs still had
10.2.6 through readdir-glob 3, and the vendored jade lockfile under
meteor-jade-loader had three copies through glob 13 and test-exclude; both get
an override to minimatch 11, as the root lockfile has. A test now reads every
package-lock.json of the repository, not only the reported ones.

The benchmark module of embedded engines pulled klauspost/compress 1.18.0,
golang.org/x/net 0.23.0 and golang/glog 1.0.0 (GHSA-259r-337f-4rfw,
GHSA-5cv4-jp36-h3mw, GHSA-qxp5-gwg8-xv66, GHSA-vvgc-356p-c3xw,
GHSA-6wxm-mpqj-6jpf). They are raised to 1.18.7, 0.62.0 and 1.2.4, and
govulncheck then found more in x/net, x/text and OpenTelemetry, which are raised
too (x/net 0.62.0, x/text 0.43.0, otel 1.42.0): it now reports none. A test
holds every go.mod of the repository at or above these floors.

Thanks to dependabot and xet7 !
2026-11-04 00:12:45 +02:00
Lauri Ojansivu 5e07b8db6c Docs: the speed test, version history and embedded engines READMEs link directories, not README.md files. 2026-11-03 23:59:03 +02:00
Lauri Ojansivu eeeb0dbec9 CHANGELOG: Go databases compared with FerretDB's SQLite. 2026-11-03 23:47:33 +02:00
Lauri Ojansivu 96d56ecc6c Go databases that could be linked into FerretDB, against its SQLite: none is faster where it counts.
A benchmark of its own (docs/Databases/FerretDB/1/SpeedTest/embedded, a Go module,
no cgo) loads the speed test's cards into SQLite set up as FerretDB sets it and
into Pebble, Badger, bbolt, BuntDB, Nutsdb and Clover, and asks each the six
questions WeKan asks of cards, at 15,377 and 153,768 cards. A board's cards take
the same time on every engine that is not slower, because decoding the JSON
dominates; the key-value stores win single reads and writes by microseconds that
a FerretDB call hides; Pebble is 12x slower on fetching a board, Clover 90-190x
slower in total, and bbolt with its default fsync 100-160x slower on writes. The
README accounts for every entry of the awesome-go section.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:47:13 +02:00
Lauri Ojansivu aadfd9b6b3 CHANGELOG: Dolt in the speed test and the version history experiment. 2026-11-03 23:19:00 +02:00
Lauri Ojansivu 1c44876beb Speed test: Dolt, and what a database with version history would give WeKan's undo and redo.
Dolt runs under the existing MySQL handler with no new handler and gives the same
answers, but is by far the slowest: 20,470 ms in total, every card query a
2.8 s whole-table scan. The VersionHistory scripts drive WeKan-shaped changes
through FerretDB onto Dolt (a commit per write, undo and redo with DOLT_REVERT,
AS OF, +0.4 ms a write, a conflict when the same document is restored again) and
through the REST API of TerminusDB (author and message per commit, field-level
restore). ClickHouse and StarRocks were tried with the MySQL handler and need a
handler of their own; the README says where each fails.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:18:51 +02:00
Lauri Ojansivu bd86b518ea CHANGELOG and tests: the small-scale speed test, pgrust, TiDB and the databases that cannot run FerretDB. 2026-11-03 22:47:16 +02:00
Lauri Ojansivu f576a0cca9 Speed test: TiDB added, and the README says what was tried: pgrust, TiDB, Noria, YugabyteDB, CockroachDB.
TiDB (MySQL protocol, Apache-2.0) runs FerretDB after its metadata table's
generated columns became VIRTUAL, and gives the same answers as MongoDB; it is
a speed test row and opt-in for the conformance run (WEKAN_CONFORMANCE_TIDB=1).
Noria has no JSON type, triggers or generated columns, which the MySQL handler
needs; YugabyteDB refuses a CREATE INDEX on a jsonb expression, which every
FerretDB v1 PostgreSQL index is; CockroachDB is not open source. The README
also records that analyzing a PostgreSQL table after its indexes are created
took the PostgreSQL row from 454 to 145 ms, and that the same on MySQL and
MariaDB did nothing.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:45:56 +02:00
Lauri Ojansivu 46c0ad43e8 Speed test: small-scale results ordered fastest to slowest, with times slower, and pgrust.
Results.md now opens with an At a glance table (rank, total, how many times
slower than the fastest, the same for the slowest user), every table is ordered
by its own numbers, the header says the real data size and the real operating
system, and the load table is ordered by the whole load time. pgrust
(PostgreSQL in Rust, malisper/pgrust:v0.4-beta) is one more row through the
same PostgreSQL handler; in the conformance run it is opt-in with
WEKAN_CONFORMANCE_PGRUST=1. The run is 1/20 of a production WeKan so it takes
minutes.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:36:56 +02:00
Lauri Ojansivu 499772e4c2 Speed test README: sorting in PostgreSQL was tried and was not faster.
The ten newest of 30,116 users took 52 ms with an ORDER BY in the database and
38-49 ms decoded and sorted in Go, so the change was not kept; the README says
what was measured so it is not repeated.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:53:31 +02:00
Lauri Ojansivu 90fe133363 Merge remote-tracking branch 'origin/main' 2026-11-03 21:37:36 +02:00
Lauri Ojansivu b812d36871 CHANGELOG: the speed test README. 2026-11-03 21:24:06 +02:00
Lauri Ojansivu d05a42a5af Speed test README: the FerretDB v1.100-v1.105 and WeKan improvements since the last run.
SQLite sorts and stores JSONB, PostgreSQL uses a GIN index for membership and
pushes down $or, $elemMatch and $and, MySQL and MariaDB read through their
indexes with multikey side tables, and WeKan stopped scanning whole collections
every hour or at every start. Results.md still shows v1.99.0, which the README
now says, and the open items are updated to match.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:24:03 +02:00
github-actions[bot] 42507de1d0 Bump versions for v12.92 2026-11-03 18:51:08 +00:00
Lauri Ojansivu 131b0cc509 Prepare v12.92 release 2026-11-03 20:48:47 +02:00
Lauri Ojansivu 93ae5ac6ee Merge remote-tracking branch 'origin/main' 2026-11-03 20:46:48 +02:00
Lauri Ojansivu 34bb91d1f5 CHANGELOG: a missing REST checklist answers a JSON 404. 2026-11-03 20:43:21 +02:00
Lauri Ojansivu d231df56fc REST API: a missing checklist, or one under another card, answers a JSON 404, not an empty 500.
GET /api/boards/:boardId/cards/:cardId/checklists/:checklistId answered 500 with
no body when the checklist did not exist or belonged to another card. It now
answers 404 with the same error text the checklist update and delete routes
use; board access and the parent-card check are unchanged. Route tests drive
the registered handler (found, missing, other card, other board, denied, lookup
failure) and a browser spec reads the checklist over REST and after a reload.

Thanks to AhmedLukman and xet7 !
2026-11-03 20:43:06 +02:00
github-actions[bot] e422ab691e Bump versions for v12.91 2026-11-03 18:01:08 +00:00
Lauri Ojansivu 3f712a0445 Prepare v12.91 release 2026-11-03 19:59:16 +02:00
Lauri Ojansivu d99dadadee Merge remote-tracking branch 'origin/main' 2026-11-03 19:42:04 +02:00
Lauri Ojansivu a58b7bee2c CHANGELOG: code scanning alert 600, a test replacing getLabel with itself. 2026-11-03 19:17:12 +02:00
Lauri Ojansivu 62da564a1e Code scanning #600: a test no longer replaces getLabel with itself, and the guard knows anchored patterns.
tests/boardLabelLookup.test.cjs sliced the getLabel method out of models/boards.js
and then ran .replace(/^getLabel/, 'getLabel') on it, which does nothing: the
slice already starts at getLabel. tests/noIdentityReplacement.test.cjs skipped
every regular expression with an anchor, so it could not see it; a leading ^ or
a trailing $ over plain characters is now compared like any other.

Thanks to GitHub CodeQL and xet7 !
2026-11-03 19:16:44 +02:00
github-actions[bot] 2ada2e2d61 Bump versions for v12.90 2026-11-03 15:58:18 +00:00
Lauri Ojansivu 5de1acf496 Prepare v12.90 release 2026-11-03 17:57:04 +02:00
Lauri Ojansivu 3008bb5606 Merge remote-tracking branch 'origin/main' 2026-11-03 17:55:41 +02:00
Lauri Ojansivu 4f7521ae3c Tigre: the login page background label uses the Tigre word for color, not the Tigrinya one.
Thanks to Raymond-Voogt and xet7 !
2026-11-03 17:31:40 +02:00
Lauri Ojansivu 1c63049913 CHANGELOG: login page background color and a readable support page. 2026-11-03 17:20:07 +02:00
Lauri Ojansivu 6b27764452 Admin Panel: a login page background colour, and a readable layout for the login text and the support page.
Lay-out has a new Login page background color (loginBackgroundColor). Only a
hex colour is accepted, so the value in the CSS custom property can never carry
CSS; the text under the login logo turns white on a dark colour and black on a
light one. The colour is on body only while the sign-in layout shows. The
support page and the text under the login logo get a centred column with sized,
bold headings and space between paragraphs. The label is translated into every
language file.

Thanks to Raymond-Voogt and xet7 !

Fixes #6866,
2026-11-03 17:19:22 +02:00
Lauri Ojansivu ae49937b44 Tests: the Playwright specs expect the accessibility-audit colours and drop below the last checklist row.
The belize and nephritis buttons, the limegreen header, the red-string and
typed-relation lines are now the darker shades that keep their contrast, and
the checklist drag released on the sortable's own bottom edge.
2026-11-03 16:48:44 +02:00
Lauri Ojansivu 7f27ea1f26 CHANGELOG: Show focus outline, and Escape closes the add-list composer. 2026-11-03 16:04:58 +02:00
Lauri Ojansivu 144777ae6b Member Settings: a per-member switch to hide the focus outline.
Change Settings has Show focus outline, on by default: the blue ring round the
focused control is an accessibility feature (WCAG 2.4.7), and it also shows
after a click on a minicard. A member who turns it off - profile.hideFocusOutline,
a toggle method that takes the signed-in user only - gets a class on body that
hides the outline and its halo for them and nobody else. The label is translated
into every language file (best effort; the low-confidence ones are listed in the
changelog).

Thanks to mimZD and xet7 !

Refs #6755,
2026-11-03 16:04:44 +02:00
Lauri Ojansivu f4b5025a7b Escape closes the inline add-list composer.
The composer opened after a list (or in an empty swimlane) stays open after a
list is created, so several can be added, and only its X closed it. Escape now
closes it at the level of an inlined form, with the same reset as the X, and not
on a click elsewhere.

Thanks to mimZD and xet7 !

Fixes #6865,
2026-11-03 15:39:24 +02:00
github-actions[bot] cdcf8ec39d Bump versions for v12.89 2026-11-03 13:35:18 +00:00
Lauri Ojansivu 739f9ed7a0 Prepare v12.89 release 2026-11-03 15:33:40 +02:00
Lauri Ojansivu f06d082e34 Merge remote-tracking branch 'origin/main' 2026-11-03 15:33:13 +02:00
Lauri Ojansivu 4036b16774 CHANGELOG: shortcuts run once on a non-Latin keyboard. 2026-11-03 15:30:41 +02:00
Lauri Ojansivu 0110e84ce5 Keyboard shortcuts run once on a non-Latin layout.
On a Farsi (or Cyrillic) keyboard the d shortcut opened the Due date popup and
closed it at a glance. keyboard.js re-dispatched every key typed on another
script as its Latin key, a leftover from Mousetrap; hotkeys-js resolves the key
from event.code itself, so the shortcut ran twice and the repeat clicked the
same control again, closing it. The re-dispatch is removed, and a test drives
real key events for Latin, Farsi and Cyrillic at the library.

Thanks to mimZD and xet7 !

Fixes #6840,
2026-11-03 15:30:39 +02:00
github-actions[bot] 00a50ae139 Bump versions for v12.88 2026-11-03 12:58:20 +00:00
Lauri Ojansivu c67a67bc9a Prepare v12.88 release 2026-11-03 14:57:20 +02:00
Lauri Ojansivu 0b8a7face3 CHANGELOG: every theme and chosen color is readable; padded REST labels. 2026-11-03 12:37:32 +02:00
Lauri Ojansivu 8b2114cd0f Themes: every board theme and every color a member chooses is readable.
Eight themes (nephritis, pumpkin, moderatepink, strongcyan, limegreen, relax,
corteza, modern) had white text on an accent at 2.1 to 4.3:1: their accents and
hover, selected and emphasis shades are darkened until it reads at 4.5:1. The
filter sidebar's headings, the clear themes' quiet text and cleanlight's
add-dependency link are darker. A color a member chooses is made legible: a
custom theme accent or slide end is darkened under the header's white text, the
text color is moved to read on the theme's surfaces and no longer reaches the
header, labels and primary buttons, a dependency line is drawn at 3:1, Kanri's
tag text is the better of dark and light, and a custom label, card, list or
swimlane color already derives its text. All 25 themes show no colour contrast
failure on the board, the sidebars, Card Settings, the card and its popups.

Thanks to Samunosuke and xet7 !
2026-11-03 12:37:10 +02:00
Lauri Ojansivu 0426c7741a Board labels: a padded name finds the stored label, in every way a label is added.
A label is looked up by name and color before one is added, and the lookup
compared the name as sent while the schema stores it trimmed: a padded ' Bug '
missed the stored 'Bug', and each repeat created another label with the same
name and color. The REST builder now trims, as the pull request did, and so
does the lookup itself, which the card's label form, the importers and the list
sync share. A legacy label stored with padding is still found.

Thanks to AhmedLukman and xet7 !
2026-11-03 12:37:10 +02:00
Lauri Ojansivu c2a28a9233 CHANGELOG: the dark board themes are readable again. 2026-11-03 11:42:09 +02:00
Lauri Ojansivu a6b10e3592 Dark themes: the quiet greys, Card Settings, popups and sidebars are readable again.
The accessibility audit made the quiet text greys dark for every theme, and
old rules never followed the dark themes: Card Settings drew white text on
light grey blocks, popup items, the sidebar's shortcuts row, quiet buttons and
sidebar labels were black on dark, and an !important list header beat each
theme's own. The quiet text is a variable with a light value on the dark
surfaces; Card Settings' sections, popup items and the other rules follow the
theme. axe-core finds no colour contrast failure in cleandark, moderndark, dark
and exodark on the board, sidebar, Card Settings, an opened card and the custom
fields popup.

Thanks to Samunosuke and xet7 !

Fixes #6863,
2026-11-03 11:41:58 +02:00
github-actions[bot] 1ac4d1b1f9 Bump versions for v12.87 2026-11-02 22:34:10 +00:00
Lauri Ojansivu 222492f313 Prepare v12.87 release 2026-11-03 00:33:10 +02:00
Lauri Ojansivu 4f95b10f0d Merge remote-tracking branch 'origin/main' 2026-11-03 00:26:43 +02:00
Lauri Ojansivu 86a18665f0 CHANGELOG: REST single-item GETs answer 404 when nothing is found. 2026-11-03 00:22:26 +02:00
Lauri Ojansivu 1ded34dfdd REST API: a single-item GET that finds nothing answers 404, not 200 with an empty body.
The scoped card GET (/api/boards/:boardId/lists/:listId/cards/:cardId) sent
200 with no body for a missing, wrong-list, wrong-board or archived card. The
list, swimlane, custom field, comment and integration GETs did the same, and
the user GET threw a TypeError for an unknown name. Each now answers 404 after
the board check, so a stranger still learns nothing about which ids exist.

Thanks to AhmedLukman and xet7 !
2026-11-03 00:22:24 +02:00
github-actions[bot] db791e1d40 Bump versions for v12.86 2026-11-02 17:03:48 +00:00
Lauri Ojansivu e8e22dc25e Prepare v12.86 release 2026-11-02 19:02:52 +02:00
Lauri Ojansivu 08d20930de Merge remote-tracking branch 'origin/main' 2026-11-02 18:56:24 +02:00
Lauri Ojansivu 10f244de77 CHANGELOG: daily jobs run once per installation. 2026-11-02 18:49:22 +02:00
Lauri Ojansivu 4e28465964 Daily jobs run once per installation: notification cleanup and the file integrity audit.
The notification cleanup ran at every start and daily on every server, reading
and rewriting the same users 24 times at a rollout; a notification added by
another server between a read and its write was lost. A run now claims a
document per job and day in dailyJobRuns, and the timer looks hourly for an
unclaimed day. The file integrity audit read 'lastRun', checked it, then wrote
it, so two servers could both scan; one atomic update now claims the run.

Continuous backup has no leader: that is documented as single-server, and the
lease it needs is not built, so #6861 item 3 stays open and this commit does
not close it.

Thanks to Raymond-Voogt and xet7 !
2026-11-02 18:49:19 +02:00
github-actions[bot] 2936a682ce Bump versions for v12.85 2026-11-02 15:27:08 +00:00
Lauri Ojansivu ab80aaf9a3 Prepare v12.85 release 2026-11-02 17:26:05 +02:00
Lauri Ojansivu 0fb943b6fb Merge remote-tracking branch 'origin/main' 2026-11-02 17:25:22 +02:00
Lauri Ojansivu 6d8b0c15ec CHANGELOG: a cleared connection pool is a lost connection; the board key backfill uses the key index. 2026-11-02 17:18:17 +02:00
Lauri Ojansivu abc23a7eda Database problems: a cleared connection pool is a lost connection; the board key backfill uses the key index.
collection2 wraps a failed write as 'ValidationError: Failed validation <driver
text>', and the driver's 'Connection pool for <host> was cleared' names a
replica set member that MONGO_URL does not list, so the schema rule matched.
The text is now a lost connection. The board key backfill looked for boards
without a key with an $or including an exists-false branch, a scan of every
board at every start of every server; one $in on the key index finds the same
boards.

Thanks to Raymond-Voogt and xet7 !

Fixes #6860,
2026-11-02 17:17:58 +02:00
Lauri Ojansivu d9aa210012 Accessibility: the results of the fixes in Audit.md, page by page. 2026-11-02 17:17:07 +02:00
Lauri Ojansivu 2e8e1e2ce2 CHANGELOG: the accessibility audit findings are fixed in the default theme. 2026-11-02 16:08:47 +02:00
Lauri Ojansivu c8bcc4817d Accessibility: fix the findings of the axe-core audit in the default theme.
The default theme's blue is #2573a7, so the header's white and #f2f2f2 text
passes WCAG AA; label colours, quiet greys and grey tiles have text that
passes too. The Starred menu, the user menu and initials avatars carry their
visible text in their accessible name. The card's Pomodoro field and the
comment and description editors have a name. A card section heading is an h3
holding a real button with aria-expanded, and its menu and layout toggle sit
beside it. The sign-in form is the main landmark with a level-one title, every
page has a level-one heading, and the shortcuts list is reachable by keyboard.

After the fixes axe-core finds no violation on 22 pages. Spec 45 runs the scan
and tests/accessibilityAuditFixes.test.cjs pins each fault by its shape.

Thanks to xet7 !
2026-11-02 16:08:32 +02:00
Lauri Ojansivu 2dd25c733e Accessibility: the result of an automated axe-core audit of seven pages.
Thanks to xet7 !
2026-11-02 15:03:54 +02:00
github-actions[bot] cb6c6533c5 Bump versions for v12.84 2026-11-02 12:45:02 +00:00
Lauri Ojansivu 292b804474 Prepare v12.84 release 2026-11-02 14:43:46 +02:00
Lauri Ojansivu 623fccf06e Merge remote-tracking branch 'origin/main' 2026-11-02 14:39:48 +02:00
Lauri Ojansivu cfd7ad2e5f Catch two unhandled rejections found by the wider promise lint.
The instance-editing hook looked up the board of an edit without a catch,
and the service worker registration had none.

Thanks to xet7 !
2026-11-02 14:37:12 +02:00
Lauri Ojansivu 762e5a9432 CHANGELOG: npm run lint:promises. 2026-11-02 14:30:59 +02:00
Lauri Ojansivu f1d2a497bc Lint for promises that are not awaited, handled or marked void.
npm run lint:promises runs typescript-eslint's no-floating-promises and
no-misused-promises over models/lib, server/lib and imports/lib, with type
information read from tsconfig.lint.json (nothing is type-checked). The
eleven findings were promises that never reject (their own catch) and now
say so with void, and three checks that used a promise as a boolean.

Thanks to xet7 !
2026-11-02 14:30:49 +02:00
Lauri Ojansivu 2f9c972d46 CHANGELOG: the code-quality findings. 2026-11-02 14:15:17 +02:00
Lauri Ojansivu 582a22c291 Fix GitHub code scanning code-quality findings.
Real faults: a card's original-position description formatted a promise
because it did not await the history; the attachment swipe distances and
two drag-scroll locals were undeclared globals; a file that cannot be read
during a GridFS upload left the upload open; api.py called exit() instead of
sys.exit(); a rules test redefined a property and a security-log test kept
an unreachable branch. Also 31 missing semicolons, unused imports in the
card details, exporters and attachment API, unused Python imports, and
comments on empty except clauses. The list-sync scanner test cut its source
at the last Meteor.startup, which the new index startup of #6855 had moved.

Thanks to GitHub CodeQL and xet7 !
2026-11-02 14:15:02 +02:00
github-actions[bot] d200234535 Bump versions for v12.83 2026-11-02 11:40:29 +00:00
Lauri Ojansivu 8548bae527 Prepare v12.83 release 2026-11-02 13:38:58 +02:00
Lauri Ojansivu d6b2d55c36 Merge remote-tracking branch 'origin/main' 2026-11-02 13:35:16 +02:00
Lauri Ojansivu f7e3533b13 CHANGELOG: LDAP account binding. 2026-11-02 13:32:03 +02:00
Lauri Ojansivu e91c14cd9a LDAP: a reused login name no longer opens the previous owner's account.
An account found by username or e-mail that records another directory
entry's identifier (same attribute, other value) is refused at login and
skipped by the import. An existing account now records its identifier and
its attribute, so LDAP_UNIQUE_IDENTIFIER_FIELD takes effect for it.
LDAP_EMAIL_MATCH_REQUIRE looks users up by username instead of _id, which
never matched. The background sync skips an account whose username matches
several entries instead of binding it to the first.

Thanks to Raymond-Voogt and xet7 !

Fixes #6859,
2026-11-02 13:31:40 +02:00
github-actions[bot] e7439e1d45 Bump versions for v12.82 2026-11-02 11:00:45 +00:00
Lauri Ojansivu 4a34b91ecf Prepare v12.82 release 2026-11-02 12:59:36 +02:00
Lauri Ojansivu d9984d2069 Merge remote-tracking branch 'origin/main' 2026-11-02 12:54:11 +02:00
Lauri Ojansivu e982282954 CHANGELOG: card numbers on a move, a restore and a set number. 2026-11-02 12:42:43 +02:00
Lauri Ojansivu e68938698c Card numbers: a card moved to another board, restored or given a number cannot take one another card has.
#6815 checked the number of every new card, but a card also enters a board's
numbering by an update: a move from the browser, which computed the
destination number from the cards it had loaded (often none, so 1), a restore
from the archive, and a number set by the REST API or an import. A before-update
hook now does what the insert hook does: a taken number is replaced by a fresh
one from the board's counter, a free one is kept and the counter raised past it.

Thanks to Raymond-Voogt and xet7 !

Fixes #6858,
2026-11-02 12:42:33 +02:00
github-actions[bot] 55299a5172 Bump versions for v12.81 2026-11-02 10:22:59 +00:00
Lauri Ojansivu 8bbc6fcda1 Prepare v12.81 release 2026-11-02 12:20:06 +02:00
Lauri Ojansivu 71463fb445 Merge remote-tracking branch 'origin/main' 2026-11-02 12:17:05 +02:00
Lauri Ojansivu b60a1e5a85 CHANGELOG: the database health probe reports a finding once. 2026-11-02 12:15:29 +02:00
Lauri Ojansivu 114e9ecd5a Database health: one server probes the database per period, so a finding is reported once.
Every server read the same MongoDB log and counters and recorded each
finding itself: one collection scan became up to 24 identical rows in Admin
Panel / Problems on 24 replicas. A probe now claims the period in a shared
document, and the state that compares one probe with the next lives there.
A claim that cannot be made at all does not stop the probe.

Thanks to Raymond-Voogt and xet7 !

Fixes #6857,
2026-11-02 12:15:09 +02:00
github-actions[bot] c6739fe452 Bump versions for v12.80 2026-11-02 10:01:31 +00:00
Lauri Ojansivu efd45c651e Prepare v12.80 release 2026-11-02 12:00:25 +02:00
Lauri Ojansivu fdc2c57b8f Merge remote-tracking branch 'origin/main' 2026-11-02 11:52:22 +02:00
Lauri Ojansivu 2c75b95908 CHANGELOG: minimatch 11.0.4 for GHSA-g3hv-75wg-m249. 2026-11-02 11:42:44 +02:00
Lauri Ojansivu a3630d96ce Update minimatch to 11.0.4, GHSA-g3hv-75wg-m249 (Dependabot alerts #180 and #182).
minimatch before 11.0.1 grows its glob parts without bound while it
preprocesses a pattern, then compares them pairwise. archiver (readdir-glob),
@typescript-eslint/typescript-estree and @babel/cli's glob used 10.2.6; they
now use the root minimatch 11.0.4.

Thanks to dependabot and xet7 !
2026-11-02 11:41:52 +02:00
github-actions[bot] db2aafe16c Bump versions for v12.79 2026-11-02 09:35:57 +00:00
Lauri Ojansivu c4b90d08a0 Prepare v12.79 release 2026-11-02 11:34:58 +02:00
Lauri Ojansivu cb5278ddb3 CHANGELOG: a collection-scan row says which query it is. 2026-11-02 11:31:54 +02:00
Lauri Ojansivu bc79de8c57 Database health: a collection scan row names the command, the operators per field and the queryHash.
A row said only which fields a slow query filtered on, which is not enough
to find the query in the code or to run explain(). It now shows the command
(for a slow getMore, the query it continues, instead of 'filter on nothing'),
the operators used on each field - never values - and MongoDB's queryHash.

Thanks to Raymond-Voogt and xet7 !

Fixes #6856,
2026-11-02 11:31:47 +02:00
Lauri Ojansivu 81714cd695 CHANGELOG: the schema re-check and due-date reminders run once. 2026-11-02 11:28:59 +02:00
Lauri Ojansivu d42369e96a Full collection scans run once, not at every start of every server; due-date reminders are sent once.
The startup schema re-check claims a run in _wekan_migration so one server
re-checks while the others skip (a claim older than 2 hours is taken over),
and a run that leaves work unresolved is retried once a day instead of at
every start. The daily due-date reminder scan is claimed per day in
dueReminderRuns, so 24 replicas no longer send 24 reminders per card.
A partial index on lists serves the 15-minute list sync scan, and a sparse
index on boards.dueReminderDays serves the due-date lookup.

Thanks to Raymond-Voogt and xet7 !

Fixes #6855,
2026-11-02 11:28:44 +02:00
Lauri Ojansivu efff590f74 CHANGELOG: the announcement shows on Home and All Boards. 2026-11-02 11:26:12 +02:00
Lauri Ojansivu 64b5762020 Announcement: shown on Home and every All Boards list, not only on a board.
The banner waited for membership of the current board, and Home, Remaining,
Starred, Templates and the other All Boards pages have none. It now shows to
every signed-in user there, and to the members of the open board on a board.
Signed-out visitors still do not see it.

Thanks to titver968 and xet7 !

Fixes #6854,
2026-11-02 11:25:57 +02:00
Lauri Ojansivu c29a59eb95 CHANGELOG: any year by number in the date picker. 2026-11-02 11:17:04 +02:00
Lauri Ojansivu dcaa316a01 Date picker popup: the year is a number field, any year by typing or stepping.
A list of years had to end somewhere. The year is now a whole number
(0 is 1 BC) found by jumping through the calendar in use, so thousands of
years back or ahead are one entry away, up to the range a date holds.

Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 11:17:02 +02:00
Lauri Ojansivu 7721c5f655 CHANGELOG: year, month and day drop-downs in the date picker. 2026-11-02 11:10:08 +02:00
Lauri Ojansivu 492895200b Date picker popup: year, month and day drop-downs under the calendar.
The heading above the calendar shows the month and year only, not the
YYYY-MM-DD of the selected day. Under the calendar, year, month and day
drop-downs pick any past, present or future date, in every calendar
system.

Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 11:09:34 +02:00
Lauri Ojansivu 1b346dc465 CHANGELOG: the date picker heading names the year. 2026-11-02 10:50:22 +02:00
Lauri Ojansivu 45c291b406 Date picker popup: the heading names the year as well as the month.
Thanks to brlin-tw and xet7 !

Fixes #6853,
2026-11-02 10:50:13 +02:00
Lauri Ojansivu c3af2a08a9 CHANGELOG: the macOS sandbox keychain and Claude Code token. 2026-11-02 10:16:03 +02:00
Lauri Ojansivu 21c94ce098 Fix macOS Sandbox for VSCodium.
Thanks to xet7 !
2026-11-02 10:14:47 +02:00
Lauri Ojansivu 2d8a28c2f6 Merge remote-tracking branch 'origin/main' 2026-11-02 09:44:15 +02:00
Lauri Ojansivu c58b2896d1 Fix macOS Sandbox for VSCodium.
Thanks to xet7 !
2026-11-02 09:43:32 +02:00
github-actions[bot] 6b98272272 Bump versions for v12.78 2026-11-02 07:14:42 +00:00
Lauri Ojansivu ee22fb1a3b Prepare v12.78 release 2026-11-02 09:13:43 +02:00
Lauri Ojansivu 154c9a6e52 Updated Changelog. 2026-11-02 09:11:57 +02:00
Lauri Ojansivu 121f37be91 Tests: drawing refit, Due Cards board scope, dependency import counts, and SAML's one Issuer.
Changes since October whose commits lacked a unit, negative or browser
test:
- 2929b61c85: a new drawing refits when its window changes, and is left
  alone once drawn on, edited, or zoomed or resized by hand.
- c64b3a3b1d: Due Cards, in "me" and "all users", shows an assigned-only
  member only their assigned cards; the owner sees them all.
- 8a6aeba7d1: the All Boards dependency import says how many lines were
  added, already there and unmatched.
- 5c50f38e25: a signed SAML Response with two Issuers is refused, which
  node-saml 5.1.0 accepted; the same Response with one Issuer logs in.
The browser specs pass in Chromium.

Thanks to xet7 !
2026-11-02 00:54:59 +02:00
Lauri Ojansivu 79c40407d1 Tests: Admin Panel pane titles, audit log alignment, GCS help, Data health "Never", card reports.
October Admin Panel changes whose commits had no test of their own:
- 85af731c1b and 2017f2871f: the Password length and Audit log panes
  name themselves in the title bar, the audit log has its own URL, and
  its cells align to the start - right in Hebrew - with no left/right
  rule left in its CSS.
- e6f7d226ca: no help line of a storage field is shown twice.
- 77956c98ed: Data health's "Never" is its own key, also when an
  administrator translates the List Sync one differently.
- 69f3358154: the card reports stay instance-wide and admin-only.
tests/playwright/specs/admin-pane-titles-and-help.e2e.js passes in
Chromium.

Thanks to xet7 !
2026-11-02 00:31:24 +02:00
Lauri Ojansivu 148d18d456 Tests: OpenSSL 4 decrypt refusal, the Scrum rollover lease, and the backup catch-up scan.
October changes whose commits had no node test of their own:
- ffe49dd2fd: decryptFile() refuses both the OpenSSL 4 and the OpenSSL 3
  wording of a failed authentication as "changed or the key is wrong",
  on any OpenSSL; an unrelated error (ENOSPC) is passed through.
- 0218cd823f: the rollover and batch-job leases are claimed before every
  write, another server's live lease is refused, and each lease field is
  written in one place in the whole tree.
- 8cbe2605fe: on macOS, a stopped engine and one with watching off run no
  catch-up scan.

Thanks to xet7 !
2026-11-02 00:30:57 +02:00
Lauri Ojansivu 15877dd1bc Source server: a local package fork that a prebuilt package implies is loaded first.
ecmascript implies the local dynamic-import fork, so compiling ddp-server,
listed before it in .meteor/versions, stopped the source-loaded server
with "isopack dynamic-import not yet loaded". The loader now loads the
local package asked for and retries. The source server still stops later,
on "Cannot find module 'meteor/accounts-base'"; the bundle server is what
the browser tests ran against.

Thanks to xet7 !
2026-11-02 00:24:55 +02:00
Lauri Ojansivu 3ccb465c3f Tests: the #6755 browser test focuses the minicard link itself.
Chromium does not leave the focus on the minicard after its menu popup
is closed with Escape - the menu's opener is a link without href, which
cannot take the focus back - so the test asserted a state the browser
had not reached. It now focuses the minicard's link, the state of the
report, and checks that d still opens the Due date editor from there.
Passes in Chromium and WebKit.

Thanks to mimZD and xet7 !
2026-11-02 00:24:55 +02:00
Lauri Ojansivu 71d9776b61 CHANGELOG: the Rules page has a Back to the board.
Thanks to mimZD and xet7 !
2026-11-02 00:07:50 +02:00
Lauri Ojansivu 34d9817110 Rules page: Back to the board.
The Rules page lost its Back with its own sidebar; only the trigger, action
and rule details pages kept one, to the rules list, so the way back to the
board was the Board View menu. The rules list and its Workflow, Bots,
Blocks and History views now have a Back link to the board at the top. The
three sub-pages keep their own Back and show no second one.

Thanks to mimZD and xet7 !

Fixes #6852.
2026-11-02 00:07:42 +02:00
Lauri Ojansivu aa939d7bec Tests: CrossBoardExportBleed and UnassignedAttachBleed are in the security regression inventory.
Both are published on the Hall of Fame, so the inventory counts 143 and
names each one's unit and browser regression tests. CrossBoardExportBleed
was published without being added, which failed the whole node-suite run.

Thanks to xet7 !
2026-11-02 00:05:44 +02:00
Lauri Ojansivu 194e5b0a51 CHANGELOG: UnassignedAttachBleed, and shortcuts after a click on a minicard or a closed popup.
Thanks to Javier García Herrera, mimZD and xet7 !
2026-11-02 00:05:10 +02:00
Lauri Ojansivu 532a96da11 Security: UnassignedAttachBleed - attachment API writes obey assigned-only card scope.
An assigned-only member whose card assignment was removed kept the card and
attachment ids and, through POST /api/attachment/upload and
DELETE /api/attachment/delete/:id, still added a file to that card and
soft-deleted its existing one: both asked only for board write access. The
DDP methods api.attachment.upload/delete, and copy and move onto a target
card over REST and DDP, had the same shape.

mayWriteCardAttachment (server/lib/assignedOnlyAttachments.js) applies the
same card rule the ordinary card Delete already used to all eight handlers.
A refusal is recorded in Admin Panel / Problems as UnassignedAttachBleed
(authz.assigned-attachment-write, medium: never disables the account).
tests/unassignedAttachBleed.test.cjs reproduces the reported sequence and
fails if any attachment API write lacks the card check;
tests/playwright/specs/unassigned-attach-bleed.e2e.js replays it over REST.

GHSA-w26g-rc43-q6cm.

Thanks to Javier García Herrera and xet7 !
2026-11-02 00:04:03 +02:00
Lauri Ojansivu ba0de93f79 Keyboard shortcuts keep working after a click on a minicard or a closed popup.
The shortcut filter refused every key while a link or button had the focus.
The minicard is itself a link, so a click on its date badge or its menu left
that link focused, and closing a popup returns focus to the button that
opened it: hovering a card and pressing d then did nothing until a reload.

A focused control now keeps only the keys it uses itself - Enter for any of
them, Space for a button, summary or checkbox, End/Home/arrows for a tab -
so Space still activates a focused button (audit A01) and every other key
is a shortcut again.

Thanks to mimZD and xet7 !

Fixes #6755.
2026-11-02 00:03:54 +02:00
Lauri Ojansivu 9a15491985 CHANGELOG: sign-in errors kept through a redraw, and the Firefox full-run test fixes.
Thanks to xet7 !
2026-11-01 20:35:06 +02:00
Lauri Ojansivu c8ed3e0fd0 Tests: the bulk export popup is opened until it stays, in all five Haitian Creole tests.
Each test navigates to All Boards and at once opens Export all boards.
All Boards is still drawing just after the route changes, and in
EVERYTHING's full Firefox run the popup opened then was closed again: no
header title to read (the first test; it passed in the run before).

All five tests open the popup again until its title is there. The spec in
Firefox, three times beside the running suite: 15 of 15.

Thanks to xet7 !
2026-11-01 18:56:50 +02:00
Lauri Ojansivu 4a9c3f21d2 Tests: the drawing surface's width is waited for, not read once after its viewBox.
The test waited until the drawing's viewBox had grown past the old 800,
then read the surface's box once. The element's layout follows the
viewBox a frame later, and in EVERYTHING's full Firefox run it was still
800 px against more than 1088 expected (it passed in the run before).

It polls the surface's width as well. The spec in Firefox, three times
beside the running suite: 9 of 9.

Thanks to xet7 !
2026-11-01 18:23:13 +02:00
Lauri Ojansivu 3a2fef4dc7 Sign-in: login errors are rendered from template state, and the SAML error test clicks a finished form.
saml-popup-error's first test failed in EVERYTHING's full Firefox run and
2 of 3 times alone: #login-error-message stayed empty after the SAML
button was clicked with a stub that fails at once.

The test clicked #at-saml (force) as soon as Meteor connected, while the
sign-in form was still drawing: the click landed on a button being
replaced, or - in another attempt - below the window ("Element is
outside of the viewport"), and no handler ran. It now waits for the form,
scrolls the button into view and clicks until the error shows. With one
worker both tests pass 6 of 6 in Firefox against a rebuilt bundle.

Found on the way: #login-error-message sits inside `if isLoading`, and
showLoginError wrote into the element - when that block is drawn again
the element is new and empty, so an error shown just before was lost. The
template now renders the message from a ReactiveVar (loginErrorText) as
escaped text, so a redraw keeps it and it can still never be HTML.
oauthProviders.test.cjs pins the same region, the translated codes and
text-only output in that form.

The other sign-in specs (44 files) pass in Firefox with it, apart from
runs where two copies of a settings-changing spec ran at once.

Thanks to xet7 !
2026-11-01 17:49:21 +02:00
Lauri Ojansivu f241681e80 Tests: background notification recovery checks its own mail, not the first one the sink caught.
The SMTP sink captures every mail the shared test server sends while it
listens. The test waited until user2 had exactly one mail, then read
sink.messages[0] - in EVERYTHING's full Firefox run another worker's
comment notification ("Please look", to another user) had arrived first,
so "Frozen recovery body 0" was looked for in the wrong mail.

It reads the mail addressed to user2. The whole spec in Firefox, twice:
24 passed, 2 conditionally skipped.

Thanks to xet7 !
2026-11-01 17:32:09 +02:00
Lauri Ojansivu c71c875649 Tests: the custom field settings popup is opened until its Create button shows.
The String Template help test opened a card and at once its custom field
settings popup. The opened card was still drawing, and in Firefox under
the whole suite the popup sometimes closed again before Create could be
clicked: ve-CC and rup failed in EVERYTHING's full Firefox run, 42
others passed.

The test opens the popup again until its Create button is there. ve-CC,
rup and en, twice each in Firefox beside the running suite: 6 of 6.

Thanks to xet7 !
2026-11-01 17:06:02 +02:00
Lauri Ojansivu 0da86171e9 CHANGELOG: TODO Later - the whole EVERYTHING run has passed in the browsers, conformance and FerretDB.
Thanks to xet7 !
2026-11-01 14:15:06 +02:00
Lauri Ojansivu b80e21fd48 CHANGELOG: Android bundles start again.
Thanks to xet7 !
2026-11-01 14:11:20 +02:00
Lauri Ojansivu 24a7270b7f Android bundles start again: npm 12 runs argon2's and bcrypt's install scripts.
All four Android bundle jobs failed: WeKan crash-looped on

  No native build was found for platform=android arch=x64 runtime=node
  abi=147 ... loaded from: .../meteor/accounts-password/node_modules/argon2

until the 240 s smoke test gave up. Meteor's npm rebuild had printed
"install scripts blocked because they are not covered by allowScripts",
though prepare-bundle-npm.mjs writes argon2 and bcrypt into every
rebuild directory's allowScripts.

npm 12 applies allowScripts to a project's DECLARED dependencies only.
The package.json that prepare-bundle-npm writes there declared none, so
argon2 and bcrypt were "extraneous" and their scripts stayed blocked -
reproduced with npm 12.2.1 on this release's bundle, and with the two
declared the same rebuild runs both install scripts. Linux, macOS and
Windows never noticed: prebuilds load there, and the Windows jobs compile
the two themselves. Android has no prebuild, and check-addon-arch passed
the Linux ones it cannot load.

prepare-bundle-npm.mjs now declares each reviewed installer a rebuild
directory has installed, at its installed version, and leaves a declared
one as it is. The Android build runs it itself first - the Linux bundle
it rebuilds may predate this - and loads argon2 and bcrypt with Android's
Node.js right after the install, so a missing addon stops the build with
its name instead of a smoke-test timeout.

prepareBundleNpm.test.cjs and androidBundles.test.cjs pin both; they fail
on the code before. Not run on Android here: the build needs Termux on
an Android-CPU runner.

Thanks to xet7 !
2026-11-01 14:11:10 +02:00
Lauri Ojansivu 73ea323da1 CHANGELOG: CrossBoardExportBleed, GHSA-8885-xrq4-j47h.
Thanks to ccsimplyspolit and xet7 !
2026-11-01 14:01:07 +02:00
Lauri Ojansivu be6727607c CrossBoardExportBleed: a checklist scope stays on its board, and every export obeys Disable all export and Anonymize export users.
GHSA-8885-xrq4-j47h, reported by ccsimplyspolit, both findings confirmed
in the current code:

1. GET /api/boards/<board>/export?checklistId=<id> (and exportZip)
   selected the scoped checklist and its items by id alone. A checklist
   of another board - a private one too - was streamed with every item
   and its creator, whose profile the anonymization map did not cover.
   Both selectors are now bound to the exported board's cards, and a
   checklist of another board is recorded in Admin Panel / Problems
   (authz.export-checklist-scope).

2. The streaming CSV/TSV export asked neither Disable all export nor
   Anonymize export users. Neither did, it turned out, Excel (board,
   card, all boards), PDF (board and card), the chart exports, the
   calendar feed or the external formats: only the WeKan JSON, ZIP and
   Trello exports did, though the setting promises that "the server
   rejects any export request" and names CSV and Excel.
   - refuseDisabledExport() answers 403 before anything is written, and
     all 13 HTTP export routes call it first; assertExportEnabled() still
     guards the builders. Both record the attempt (authz.export-disabled,
     never blocking the account: a page opened before the switch still
     shows the menus).
   - anonymizeExportIdentities() anonymizes what one exporter writes -
     users, @mentions, requested-by / assigned-by, comments - with the map
     build() uses, board members included. The CSV/TSV stream, Excel,
     PDF, chart exports (forExport: the chart views keep names), the
     calendar feed and every external format call it.

tests/crossBoardExportBleed.test.cjs drives the gate and the anonymizer
and scans the tree: no scoped checklist selected by id alone, no export
route without the gate, no exporter naming people without anonymizing.
All 7 fail on the code before. The new spec
security-cross-board-export.e2e.js replays the reporter's requests and
checks every export route while export is disabled and the CSV and
Kanboard exports while anonymizing; it passes with export-access and
scrum-export-anonymization against a rebuilt bundle.

Thanks to ccsimplyspolit and xet7 !
2026-11-01 14:00:19 +02:00
Lauri Ojansivu 9c5fa760c3 CHANGELOG: glob 14.0.3, checked: no WeKan code loads it, no telemetry, tarballs match the lockfile.
Thanks to dependabot and xet7 !
2026-11-01 13:41:40 +02:00
Lauri Ojansivu 27040c6f2f Merge pull request #6851 from wekan/dependabot/npm_and_yarn/npm_and_yarn-0e36e05768
Bump glob from 13.0.6 to 14.0.3 in the npm_and_yarn group across 1 directory
2026-11-01 13:36:11 +02:00
Lauri Ojansivu c15e1ab4b2 Tests: the Card Settings splitter is dragged once the popup holds still.
The splitter test measured the splitter straight after opening Card
Settings, while the popup was still placing itself. In WebKit the press
landed where the splitter had been and dragged nothing: the pane stayed
526 px against more than 606 expected (the first attempt in EVERYTHING's
WebKit stage; the retry passed).

It now measures once two reads of the splitter and the panes 150 ms apart
agree. WebKit 5 of 5, Chromium 3 of 3.

Thanks to xet7 !
2026-11-01 11:58:06 +02:00
Lauri Ojansivu b676cdc169 CHANGELOG: the WebKit date-range refusal and the language and push fixes of the browser tests.
Thanks to xet7 !
2026-11-01 11:25:20 +02:00
Lauri Ojansivu 4ad13db5ef Filters: the date-range forms are checked by WeKan, not refused by WebKit after Clear.
After Clear emptied a refused range, WebKit kept a half-edited date
segment in the From field and fired `invalid` on it at the next submit -
though its validity read valid - so the form never submitted and the new
range never applied, with no message (card-date-range.e2e.js in WebKit,
also on retry: 0 submit events, `invalid` on .js-card-date-from). The old
Clear handler emptied the fields the same way, so this was there before.

Both range forms are novalidate now. The submit handler already decides
whether a range is valid (rangeRefused), marks the end date invalid with
the reason and says it in the form, so nothing is lost.

filterDateRangeRefusal.test.cjs pins novalidate on both forms. Against a
rebuilt bundle card-date-range and card-movement-filter pass twice each
in WebKit, Firefox and Chromium.

Thanks to xet7 !
2026-11-01 11:24:59 +02:00
Lauri Ojansivu efe0e9287c Tests: a language switch is waited for, and WebKit does not freeze on asking for a push subscription.
Two causes behind most of EVERYTHING's WebKit failures so far, both in the
test pages rather than in WeKan:

- 94 specs switch language with setLanguage and then click at once. The
  client loads that language afterwards and draws everything again, which
  closes a popup opened before that: the board view popup of the flow and
  report help specs (Latin monte-carlo in WebKit; Corsican and Scottish
  Gaelic in earlier Firefox runs) had no entry left to click. The
  fixture's context now wraps Meteor.callAsync in each page so that a
  setLanguage call resolves once <html lang> - which client/lib/i18n.js
  sets in the same flush that redraws - names the language. It waits at
  most 10 s and then resolves anyway, so it can only make a spec patient,
  never fail one. A page of a context a spec makes itself uses
  untilLanguageShown().
- Playwright's WebKit freezes the page's main thread on
  PushManager.getSubscription(): after it not even a timer runs. Member
  Notification Settings asks it on opening, to show whether this device
  gets push (client/lib/webPush.js), so every WebKit spec that opened it
  hung in its next click - notification-activity-mute and the member
  scope of notification-delivery-settings, in every language. In WebKit
  the page now answers as a device with no subscription does. Chromium,
  Firefox and Safari answer the real call; no spec subscribes.

WebKit: those notification specs 10 of 10, and the Latin, Corsican and
Product Backlog specs 13 of 13; Chromium and Firefox 11 of 11 each with
the same subset. playwrightPageInit.test.cjs runs both page scripts on a
fake page: setLanguage waits for the language and keeps its result,
other methods are not held, a page whose Meteor arrives later is
wrapped, the wait is capped; WebKit's getSubscription answers null and
Chromium's and Firefox's are left alone; the fixture installs both.

Thanks to xet7 !
2026-11-01 11:21:45 +02:00
Lauri Ojansivu cfcead6a1a CHANGELOG: the Firefox fixes - date range refusal, history Retry, whiteboard line width - and the browser test fixes.
Thanks to xet7 !
2026-11-01 09:23:34 +02:00
Lauri Ojansivu 40f60582b7 Whiteboard: a line is drawn at its own width, not the whiteboard's 800.
Inside `each strokes` the template drew stroke-width=width, and Spacebars
looks a name up among the template's helpers before the line's own
fields: the width helper is the drawing's width, so every line was drawn
800 wide - one black shape over the whole whiteboard, in every browser.
The lines are mapped with strokeWidth now.

whiteboard.test.cjs: every name the line's path uses is a field of the
line and none is a helper of the template, which would win - it fails on
the code before. whiteboard.e2e.js checks each drawn path is the pen's
width (4) in both browsers.

The same spec drew the second person in another context of the same
browser. In Firefox two contexts share the pointer capture the first pen
takes: one line zig-zagged between both paths and the other page drew
nothing, 4 of 4. The second person now has a browser of their own, as
two people do; both pens are still down at once. Firefox 6 of 6 and
Chromium 4 of 4 against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu 96845f6f4b History recovery: Retry is enabled only once the board lets this person write on it.
After a reload the notice for an unanswered keyboard undo or redo shows
as soon as its board is open - before the board and its members have
arrived. undoRedoLast does nothing while Utils.canModifyBoard() is false,
so a Retry clicked in that moment was dropped without a word and the
notice stayed (history-recovery-notice.e2e.js, Firefox).

Retry is disabled until the retry can act; Forget needs no right and
stays usable. For someone who may only read the board Retry stays
disabled, as it could never do anything for them.

historyRecoveryNotice.test.cjs pins the binding and the guard it mirrors,
and that Forget is not gated - it fails on the code before. The spec
waits for Retry to be enabled before it clicks; the spec passes in
Firefox against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu c5f5ad2338 Filters: a refused date range says why in the form, so Clear still works in Firefox.
The filter sidebar's two date-range forms (card dates, card movement)
reported a reversed range with reportValidity(). Firefox opens that
bubble just under the end date - over the Apply and Clear buttons - and
while the field has focus it takes every click on them: no mousedown and
no click reached the page, twice in a row, so a refused range could not
be cleared at all (card-date-range.e2e.js, 4 of 4 in Firefox; with the
bubble suppressed it passed).

The end date is still marked invalid with the reason (setCustomValidity,
for :invalid and screen readers), and the reason is shown in the form as
a role="alert" line under the buttons. Typing in the range or Clear takes
it back.

filterDateRangeRefusal.test.cjs: both forms report through rangeRefused,
show the line and take it back on input and Clear, and nothing in the
filter sidebar calls reportValidity() - it fails on the code before. The
card-date-range and card-movement-filter specs check the line appears
with the reason, is gone after Clear or an edit, and is absent for an
accepted range; both pass in Firefox against a rebuilt bundle.

Thanks to xet7 !
2026-11-01 09:22:05 +02:00
Lauri Ojansivu 0c8c254dc5 Tests: the viewport matrix cleans up only what it seeded.
viewport-matrix is Chromium-only (device emulation), so in Firefox and
WebKit every test of a viewport is skipped and its beforeAll never runs -
but its afterAll still does, and read board.boardId of a board that was
never seeded: each of the twelve viewports failed in EVERYTHING's Firefox
stage, on the last test of the group, in 0 ms.

afterAll returns when nothing was seeded. Firefox: 132 skipped, none
failed. Chromium: phone-portrait 11 of 11, and its board is cleaned up.

Thanks to xet7 !
2026-11-01 09:09:14 +02:00
Lauri Ojansivu 0a1f8584ee Tests: the board view popup is opened until it stays open for the Product Backlog.
The test switches language and then opens the board, and when the language
arrives the board header draws again and closes a popup opened before
that. In Firefox the click on the board view toggle often came first: no
popup at all, and no Product Backlog entry to open - 2 of 6 runs alone,
the st variant in EVERYTHING's Firefox stage.

The test opens the popup again until its Product Backlog entry is there.
9 of 9 in Firefox (st, en, haw, three times each).

Thanks to xet7 !
2026-11-01 09:01:09 +02:00
Lauri Ojansivu 2042fd5102 Tests: the Blocks drag test grabs the block's body, not its edge.
In Firefox a Khmer label overflows its Blocks block and widens the box the
test measures, so the first point of the block it found, scanning from the
left, was the block's outline - and a press there dragged nothing: km and
km-KH failed every time (4 of 4), every other language passed.

The grab point is now one with the block's own body on both sides of it.
The whole spec in Firefox: 104 of 104, Khmer, Georgian, Fijian,
Kinyarwanda (the long labels the search was written for) and Arabic
right-to-left among them.

Thanks to xet7 !
2026-11-01 08:23:41 +02:00
Lauri Ojansivu 0403e27c62 Tests: the notification delivery sections are opened until they stay open.
The member-scope test switches language just before it opens Notification
Settings, and when the language arrives the popup draws again with every
section closed. Under a whole-suite run in Firefox that came after the
click on the section's summary, so the text checks passed and the select
the test then used was hidden: vi and zh_SG failed, 101 others passed,
and vi passed 3 of 3 alone.

expand() opens the section when it is closed and waits until its content
shows, again if a redraw closes it. vi, zh_SG, en and fi twice each, and
the board-scope, webhook-form and tray tests: all pass.

Thanks to xet7 !
2026-11-01 08:04:55 +02:00
Lauri Ojansivu d9e48a909d Tests: the list-sync specs' mock servers are reachable from WeKan when the browsers run in Docker on macOS.
The list-sync specs answer as Jira or GitLab from a server in the test
process, and WeKan's server fetches from it. With the browsers in Docker
on macOS that server was on the container's loopback, which Docker
Desktop keeps from the host: "fetch failed", and every test of
list-sync-estimate, -planning, -source and -time-estimates failed in
EVERYTHING's Firefox stage.

build.sh already forwarded the test SMTP port from the host into the
container through docker exec; the same forwarder carries any TCP. On
macOS it now also forwards a small pool, 39101-39104 by default
(WEKAN_TEST_REVERSE_PORTS), and names it to the specs, which listen
through helpers/reachable-server.cjs: the first free port of the pool,
or any port when there is none, as before. Linux forwards nothing.

30 of 30 of those specs pass in Firefox in Docker now.
playwrightReachableServer.test.cjs: the pool is read strictly, a port in
use is skipped, no pool listens anywhere, and when every forwarded port
is taken it fails clearly instead of listening where WeKan cannot reach.
playwrightDockerHost.test.cjs: macOS forwards the SMTP port and the pool
(or the one configured), names it to the container and removes its ready
files; Linux forwards nothing.

Thanks to xet7 !
2026-11-01 07:50:40 +02:00
Lauri Ojansivu 0cea141702 Tests: the duplicate relation picker is opened once the card has opened.
The test clicked Add dependency straight after the minicard. Under a
whole-suite run in Firefox that click landed while the card was still
opening, and the picker it opened was closed again when the card's route
and details settled - no popup, and the wait for its type select ran out
(the ve-PP variant; 59 of 60 passed, and it passed 3 of 3 alone).

The test waits for the opened card, then opens the picker again until its
type select shows. 6 of 6 (en, ve-PP, tlh, twice each) beside the suite.

Thanks to xet7 !
2026-11-01 07:21:43 +02:00
Lauri Ojansivu 9188843369 Tests: the checklist drag tests drag once the opened card's rows hold still.
The opened card draws the sections above its checklist for a moment after
it opens, and they push the rows down: in Firefox they moved 50 px between
the test measuring them and releasing the mouse. With handles off the
release landed on item 1's own title and opened its editor, so the titles
read 2,3 (EVERYTHING's Firefox stage); with handles on the enabled drag
dropped item 1 between 2 and 3. 2 of 6 in a row failed that way.

drag() waits until two reads of the rows' positions 150 ms apart agree,
then measures and drags. 10 of 10, both variants five times, while the
whole suite ran beside it.

Thanks to xet7 !
2026-11-01 06:57:18 +02:00
Lauri Ojansivu dbf07aadf9 Tests: the activity recovery report is searched again until no worker lease covers it.
The report is a snapshot taken when the search runs, and the live
one-second recovery worker leases each pending intent while it looks at
it. A search made during that lease shows the seeded orphan as "Delivery
reservation active" - correct - and the page keeps showing it, so the
wait for "Original activity is missing" ran out in EVERYTHING's Firefox
stage (the en variant).

The test searches again until a snapshot falls between leases, up to 30 s,
and then asserts the same status and the disabled Retry. 10 of 10 in
Firefox, every test twice, while the whole suite ran beside it.

Thanks to xet7 !
2026-11-01 06:40:42 +02:00
Lauri Ojansivu e001c4f9c2 CHANGELOG: the Docker test proxy redials an unanswered connect.
Thanks to xet7 !
2026-11-01 06:23:55 +02:00
Lauri Ojansivu ffe207e191 Tests: the Docker test proxy dials the host again when a connect is never answered.
Even on the bridge network, EVERYTHING's Firefox stage lost a test now and
then to a page whose document never came (RuleBleed's cross-board test,
Red Strings, shared templates). Inside the container the cause was in
plain sight: a connect to host.docker.internal:3000 in SYN_SENT after nine
retransmits, while the next connection went straight through. Through the
container's proxy 9 of 1000 requests went unanswered; from the Mac itself
none did.

The proxy no longer waits on such a connect. It reads nothing from the
browser until a connection to the host is up (pauseOnConnect), drops a
dial that has not connected in 1.5 s - a normal one takes milliseconds -
and dials again, five times, so no byte is lost or sent twice. If none
connects the browser's connection is closed rather than left open.

playwrightDockerHostProxy.test.cjs: two unanswered dials, then one that
connects - the request arrives once and is answered, and the abandoned
dials are closed; when every dial is unanswered the client is closed after
exactly the attempts, in well under the old 30-60 s wait.

Thanks to xet7 !
2026-11-01 06:23:34 +02:00
Lauri Ojansivu b2198cb61b CHANGELOG: the Firefox stage's Docker network, host paths and phone invitation test.
Thanks to xet7 !
2026-11-01 06:10:30 +02:00
Lauri Ojansivu a6bec88266 Tests: the phone invitation tile is measured in one layout with its buttons.
43-mobile-allboards read the invitation tile's box, then each control's
box, one round trip at a time. Firefox draws the row above the board list
later than Chromium, and in between it moved the whole list down 29.4 px:
the Decline button (1102-1162) was compared with where the tile had been
(853-1139) and failed, 2 of 2, in EVERYTHING's Firefox stage. The tile was
never clipped - settled, it runs 882-1168 around all three controls.

The tile and its message and two buttons are measured in one layout now,
polled until they hold, and a missing or zero-height control fails too.
Firefox: 3 of 3. With the original #6488 bug put back (the tile forced to
4rem) it still fails, naming the message and both buttons.

Thanks to xet7 !
2026-11-01 06:10:03 +02:00
Lauri Ojansivu 8a1f7a14a9 Tests: the Playwright container sees the repo and the files root at their own host paths.
A spec and the server share files by path. admin-continuous-backup gives
the server a key file it wrote under .tools/tmp, and reads the stream.json
the server wrote to its target - and with the repository mounted at /repo
and the files root at /wekan-files, every such path named a file only one
side had: "The encryption key file cannot be read", and no stream.json, in
both of its tests in EVERYTHING's Firefox stage (the only browser there that
runs in Docker on this Mac, and on Linux arm64 every browser does).

Both are mounted at the path they have on the host now, the specs are told
that path in WEKAN_FILES_PATH, and the in-container script finds the proxy
and the Playwright binaries from its working directory. Both tests pass in
the container now.

playwrightDockerHost.test.cjs drives run_playwright_docker on macOS and
Linux: the repo and the files root are mounted at their own paths,
WEKAN_FILES_PATH and HOME follow, and the function names no container-only
path. It fails on the code before.

Thanks to xet7 !
2026-11-01 06:08:05 +02:00
Lauri Ojansivu 7f948bf438 Tests: the macOS Playwright container uses Docker's bridge network, not the host network.
Three Firefox tests of one EVERYTHING run waited the full 60 s in
waitForMeteor: their page's own document never arrived. Measured from
containers during that run, on Docker Desktop for macOS:

- host network, through the WeKan proxy:          3 of 600 requests lost
- host network, straight to host.docker.internal: 8 of 600 connects never
  completed
- bridge network, through the same proxy:          0 of 1000
- from the Mac itself:                             0 of 300

So it is Docker Desktop's host network, not WeKan and not the proxy. On
macOS nothing needs that network - WeKan and MongoDB are reached through
host.docker.internal and SMTP through docker exec - so the container runs
on the bridge network, where the proxy still serves localhost:3000 and the
browser keeps ROOT_URL's origin. Linux keeps the host network, which is
the real one there.

playwrightDockerHost.test.cjs drives run_playwright_docker with uname
stubbed: Darwin gets bridge and the proxy, Linux gets host and none, and
the function names no network of its own. It fails on the code before.

Thanks to xet7 !
2026-11-01 06:06:04 +02:00
Lauri Ojansivu 6d0ab6cf11 CHANGELOG: continuous backup, single-file builds and the EVERYTHING test fixes.
The seven commits since the Dependabot entries: two bug fixes (continuous
backup watching before its first scan; single-file Mach-O signing and
start/backup waiting for WeKan) and five test-run fixes, grouped by topic.
The short changelog guard is a group of its own now, so the developer-
tooling subsection is grouped throughout. CHANGELOG.md regenerated.

Thanks to xet7 !
2026-11-01 05:47:52 +02:00
Lauri Ojansivu 89b64966c6 Tests: the password minimum specs wait until the server has the policy they wrote.
password-min-length-signup-login.e2e.js writes the password policy straight
into the settings document. The server learns it from its settings observer,
which polls on the standalone test MongoDB EVERYTHING runs: for up to ten
seconds it still answered the old policy. A sign-up form loaded meanwhile kept
the old minimum (6 instead of 10), and an administrator's Set password was
refused under the minimum the test before had left enforced - two failures in
EVERYTHING's Chromium stage that never showed on a dev server, whose MongoDB
is a replica set.

untilServerHas() asks the server (getPasswordPolicy) until it has the policy
the test wrote; the sign-up form is loaded again after it. 6 of 6 with two
workers, each test twice.

Thanks to xet7 !
2026-11-01 05:01:18 +02:00
Lauri Ojansivu d5e3325e87 Tests: Card Settings section order waits for the published board before reading the card.
"with every field allowed, the card draws exactly the Card pane's
sections" writes the board's Scrum visibility straight to the database and
reloads. The board publication's cursor has limit: 1, which Meteor's
change-stream driver does not follow, so it polls: a reload could still be
served the board as it was for up to ten seconds, and the card drew no
Scrum section - failing every time in EVERYTHING's Chromium stage. Live in
the browser the update arrived after 9.8 s; in the app itself a write
through a method re-polls at once, so only this direct write waited. The
test waits for the published board (up to 15 s) before it reads the card.

Thanks to xet7 !
2026-11-01 03:27:46 +02:00
Lauri Ojansivu 897376ba54 Tests: the Board Settings button theme test checks the settings that still have action buttons.
button-theme-foreground.e2e.js opened Board Settings / Swimlane, List and
Card and asked each for its column-actions buttons. Card Settings has had
none since 3e5bc58105 ("Card Settings: a checkbox per section, ...":
card actions live in the card's own section hamburgers), so every theme
failed on Card, in EVERYTHING's Chromium stage. Swimlane and List are
checked, in all four themes, and the test says why Card is not.

Thanks to xet7 !
2026-11-01 03:17:38 +02:00
Lauri Ojansivu 16103f9ad2 Tests: the whole-suite run keeps its test database in .tools/, not beside the checkout.
build.sh and build.bat started the test mongod on :3001 with its dbpath
in the checkout's parent directory (../mongodb-test-3001). That is the
one thing in a test run that was written outside the repository, against
the rule that everything WeKan's scripts write stays in .tools/ - and a
sandboxed editor, which may write only the repository, could not create
it, so EVERYTHING's browser stages had no database to run against. It is
.tools/mongodb-test-<port> now, which git and Meteor already ignore.

toolsDirOnly.test.cjs pins the new path in both scripts and fails on a
test database beside the checkout.

Thanks to xet7 !
2026-11-01 02:51:41 +02:00
Lauri Ojansivu 8cbe2605fe Continuous backup: files are watched before the first scan, and a macOS start catches up once.
The file engine scanned its sources first and started watching them
after, so a change made between the two - after the scan had passed a
file, before the watcher began - waited for the next scan, five minutes
by default. And macOS reports file events through FSEvents, which
delivers nothing for a moment after a watcher is created: an EVERYTHING
run on a loaded Mac lost a log line appended right after the start
(tests/integration/continuousBackup.test.cjs, "the appended log line was
streamed by watching", 4 !== 5).

The watchers start first now; a change seen by the scan and a watcher
too is recorded once, as they share one queue. On macOS one catch-up
scan runs two seconds after the start. Not elsewhere: inotify is
watching when fs.watch returns, and a second full scan of a large store
would double the start-up cost.

A new test, on macOS and Linux, with a watcher that never reports:
watching starts before the first scan; on macOS the catch-up scan
records a change made right after the start; on Linux no second scan
runs. It fails on the code before this change.

Thanks to xet7 !
2026-11-01 02:42:03 +02:00
Lauri Ojansivu f5eb4ecf61 Mac smoke test: a process group's live members come from libproc, not ps.
When macOS refuses to signal a process group (EPERM, which it returns for
a group of zombies), the smoke test checked with `ps` whether any member
was still alive. A Mac that will not let the build run `ps` - "Operation
not permitted", as here, where EVERYTHING's node suites run - failed the
check itself, and with it a correct app: macAppPackaging.test.cjs, every
EVERYTHING run on this machine.

On macOS it asks the kernel through libproc now (proc_listpids for the
group, proc_pidinfo for each member: a zombie's info cannot be read, a
live one's status is not SZOMB), with no process started. Elsewhere it
uses ps exactly as before.

tests/mac-smoke-app.py: the ps cases run as the non-macOS path; on macOS
an EPERM asks libproc and never ps; and a real group - a live child, then
a zombie leader alone - is told apart. macAppPackaging.test.cjs passes,
three runs in a row.

Thanks to xet7 !
2026-11-01 02:32:31 +02:00
Lauri Ojansivu 8d4f4caa2f Single file: a macOS build is signed whatever its target is called, and start/backup return once WeKan runs.
EVERYTHING's node suites run under Meteor's Node.js, which has single
executable applications, so singleFile.test.cjs built and ran a real
single executable - and it failed twice:

- The builder ad-hoc signed only a target named mac*/darwin*. Built from
  this Mac's Node.js under another label (the test builds 'amd64'), the
  file is still a Mach-O binary, the injected archive had invalidated its
  signature, and macOS killed it (SIGKILL) before it printed anything:
  status null. It signs every Mach-O output now, read from the file's
  magic number.
- `start --background` and `backup` returned as soon as they had spawned
  WeKan, before it wrote its pid file - which it does only after reading
  its settings and extracting FerretDB. A `stop` right after them said
  "WeKan was not running" and left it running. Both wait now
  (status.waitUntilStarted) until the pid file names the process they
  started, give up at once if it ended, and say so if it did not start.

singleFile.test.cjs: 24 of 24 under Meteor's Node.js, the real single
executable included; a new test pins waitUntilStarted, and that both
commands use it.

Thanks to xet7 !
2026-11-01 02:23:36 +02:00
dependabot[bot] 9a74138a84 Bump glob
Bumps the npm_and_yarn group with 1 update in the / directory: [glob](https://github.com/isaacs/node-glob).


Updates `glob` from 13.0.6 to 14.0.3
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v13.0.6...v14.0.3)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 14.0.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 23:51:54 +00:00
Lauri Ojansivu fa7e447536 CHANGELOG: ten Dependabot updates, checked; node-saml 5.2.0 where logins run; @smithy/core 3.38.1.
Thanks to dependabot and xet7 !
2026-10-31 23:06:40 +02:00
Lauri Ojansivu 05cc2460ea Tests: the short changelog guard holds between releases too.
changelogShort.test.cjs asserted an Upcoming section always, and one
Upcoming in CHANGELOG.md - but right after a release there is none until
the next real entry (CLAUDE.md, Making a release), so it failed after
every release, v12.77 included. With no Upcoming it checks that the
newest release is first and that nothing links an Upcoming that does
not exist; with one, what it checked before.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu 5c50f38e25 SAML logins are verified by @node-saml/node-saml 5.2.0, as package.json says.
Dependabot's 5.1.0 -> 5.2.0 bump (#6850) changed the app's package.json,
but a SAML login is verified by the wekan-accounts-saml package's own
copy, pinned by its Npm.depends to 5.1.0 - so nothing that runs changed.
5.2.0 is a hardening release a login relies on: it rejects a message
with more than one Issuer, checks idpIssuer on the Response and its
assertion, rejects a parsed query whose RelayState is not the signed
one, and checks a POST LogoutResponse's status and issuer.

The package pins 5.2.0 now. samlLogin.test.cjs required '5.1.0' in one
place and '^5.1.0' in the other; it requires the package and package.json
to name the SAME version, and at least 5.2.0, so a bump of one alone
fails.

A login roundtrip with the package's own copy (node-saml 5.2.0,
xml-crypto 6.4.0) and WeKan's options: a signed Response and a signed
Assertion log in; a NameID changed after signing, a signature by another
key, an unsigned response, and an answer to a request WeKan never made
are refused. saml-popup-error.e2e.js completes a real signed login in
the browser.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu 338afc15f8 @smithy/core 3.38.1: the AWS SDK's logger redaction fix, and the telemetry gate passes again.
The @aws-sdk/client-s3 3.1159.0 bump (#6844) resolved @smithy/core to
3.37.0, which npm marks deprecated: "a bug in custom logger data
redaction" (aws/aws-sdk-js-v3#8393). With a custom logger passed to an
AWS client, sensitive fields nested in a list or map reached the logger
unredacted. @smithy/core 3.24.0 to 3.38.0 have it, so 3.36.2 before the
bump did too; 3.38.1 is the fix.

WeKan's only AWS client is the S3 backup (server/methods/backup.js), and
it passes no logger, so nothing was logged. Updated all the same, in the
lockfile only and within the ranges the SDK already allows - one package,
nothing else moved. Its tarball matches the lockfile's integrity hash,
and compared with 3.37.0 it adds no network host, telemetry, process
spawning or install script. A multipart upload to a local S3 stand-in
still arrives signed AWS4-HMAC-SHA256, whole.

releases/check-telemetry.py --source failed on it: the deprecation text
links github.com, a URL origin new to package-lock.json. It passes now.

Thanks to xet7 !
2026-10-31 23:06:08 +02:00
Lauri Ojansivu dd2082b0ea Merge pull request #6846 from wekan/dependabot/npm_and_yarn/isomorphic-git-1.44.8
Bump isomorphic-git from 1.44.3 to 1.44.8
2026-10-31 22:39:27 +02:00
Lauri Ojansivu 4a86e9c19e Merge pull request #6850 from wekan/dependabot/npm_and_yarn/node-saml/node-saml-5.2.0
Bump @node-saml/node-saml from 5.1.0 to 5.2.0
2026-10-31 22:38:24 +02:00
dependabot[bot] 568cd9b6ec Bump isomorphic-git from 1.44.3 to 1.44.8
Bumps [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) from 1.44.3 to 1.44.8.
- [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases)
- [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.44.3...v1.44.8)

---
updated-dependencies:
- dependency-name: isomorphic-git
  dependency-version: 1.44.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:37:49 +00:00
Lauri Ojansivu fea468471d Merge pull request #6849 from wekan/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.72.1
Bump @typescript-eslint/eslint-plugin from 8.72.0 to 8.72.1
2026-10-31 22:37:36 +02:00
Lauri Ojansivu 83199b9af7 Merge pull request #6848 from wekan/dependabot/npm_and_yarn/rspack/dev-server-2.2.2
Bump @rspack/dev-server from 2.2.1 to 2.2.2
2026-10-31 22:37:10 +02:00
Lauri Ojansivu 3839ee698d Merge pull request #6845 from wekan/dependabot/npm_and_yarn/mailparser-3.12.4
Bump mailparser from 3.12.2 to 3.12.4
2026-10-31 22:36:48 +02:00
dependabot[bot] a60b996c92 Bump @node-saml/node-saml from 5.1.0 to 5.2.0
Bumps [@node-saml/node-saml](https://github.com/node-saml/node-saml) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/node-saml/node-saml/releases)
- [Changelog](https://github.com/node-saml/node-saml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-saml/node-saml/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: "@node-saml/node-saml"
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:36:39 +00:00
Lauri Ojansivu 1230164c16 Merge pull request #6847 from wekan/dependabot/npm_and_yarn/i18next-26.5.11
Bump i18next from 26.5.4 to 26.5.11
2026-10-31 22:36:29 +02:00
dependabot[bot] 8008f410fe Bump @typescript-eslint/eslint-plugin from 8.72.0 to 8.72.1
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.72.0 to 8.72.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.72.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.72.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:36:27 +00:00
Lauri Ojansivu bd90782da2 Merge pull request #6844 from wekan/dependabot/npm_and_yarn/aws-sdk/client-s3-3.1159.0
Bump @aws-sdk/client-s3 from 3.1155.0 to 3.1159.0
2026-10-31 22:35:45 +02:00
dependabot[bot] 5eadc45563 Bump @rspack/dev-server from 2.2.1 to 2.2.2
Bumps [@rspack/dev-server](https://github.com/rstackjs/rspack-dev-server) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/rstackjs/rspack-dev-server/releases)
- [Commits](https://github.com/rstackjs/rspack-dev-server/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: "@rspack/dev-server"
  dependency-version: 2.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:35:25 +00:00
Lauri Ojansivu 3785a13f6a Merge pull request #6842 from wekan/dependabot/npm_and_yarn/imapflow-2.9.1
Bump imapflow from 2.8.0 to 2.9.1
2026-10-31 22:35:18 +02:00
Lauri Ojansivu 97cabdeca4 Merge pull request #6843 from wekan/dependabot/github_actions/helm/chart-releaser-action-1.8.0
Bump helm/chart-releaser-action from 1.7.0 to 1.8.0
2026-10-31 22:34:58 +02:00
Lauri Ojansivu 2802326888 Merge pull request #6841 from wekan/dependabot/github_actions/docker/login-action-4.7.0
Bump docker/login-action from 4.6.0 to 4.7.0
2026-10-31 22:34:17 +02:00
dependabot[bot] 1f46af0639 Bump i18next from 26.5.4 to 26.5.11
Bumps [i18next](https://github.com/i18next/i18next) from 26.5.4 to 26.5.11.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.5.4...v26.5.11)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.5.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:51 +00:00
dependabot[bot] 1bcb1da92f Bump mailparser from 3.12.2 to 3.12.4
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.12.2 to 3.12.4.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.12.2...v3.12.4)

---
updated-dependencies:
- dependency-name: mailparser
  dependency-version: 3.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:31 +00:00
dependabot[bot] 067efac7f8 Bump @aws-sdk/client-s3 from 3.1155.0 to 3.1159.0
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.1155.0 to 3.1159.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1159.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1159.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:33:21 +00:00
dependabot[bot] 2bdd16060e Bump imapflow from 2.8.0 to 2.9.1
Bumps [imapflow](https://github.com/postalsys/imapflow) from 2.8.0 to 2.9.1.
- [Release notes](https://github.com/postalsys/imapflow/releases)
- [Changelog](https://github.com/postalsys/imapflow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/postalsys/imapflow/compare/v2.8.0...v2.9.1)

---
updated-dependencies:
- dependency-name: imapflow
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:54 +00:00
dependabot[bot] 433bc62211 Bump helm/chart-releaser-action from 1.7.0 to 1.8.0
Bumps [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/helm/chart-releaser-action/releases)
- [Commits](https://github.com/helm/chart-releaser-action/compare/cae68fefc6b5f367a0275617c9f83181ba54714f...df287104677278f869ca5480bd260358397cc3f9)

---
updated-dependencies:
- dependency-name: helm/chart-releaser-action
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:54 +00:00
dependabot[bot] 29f9c62a64 Bump docker/login-action from 4.6.0 to 4.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:32:36 +00:00
github-actions[bot] 21766cf38d Bump versions for v12.77 2026-10-31 19:47:43 +00:00
Lauri Ojansivu 101cdb7579 Prepare v12.77 release 2026-10-31 21:46:40 +02:00
Lauri Ojansivu 54cf951d9c Merge remote-tracking branch 'origin/main' 2026-10-31 21:46:38 +02:00
Lauri Ojansivu 57a43ee8e1 CHANGELOG: the date-popup guards follow movable popups and the date picker's empty cells.
Thanks to xet7 !
2026-10-31 21:44:13 +02:00
Lauri Ojansivu ff2c0ed7f6 Tests: the date-popup guards follow movable popups and the date picker's empty cells.
Two node guards still described v12.76's behaviour before it changed on
purpose. calendarPopupResize asserted that a popup without a date editor
does not drag; every popup drags by its title bar now (#6839), and with
drag handles on only by its handle - both pinned instead, its mock no
longer answering every selector as if a drag handle were there.
calendarTodayMarker picked a month's days with filter(Boolean), which an
empty cell passes now that it is an object with its own _id (#6838); it
skips the cells marked empty.

Thanks to xet7 !
2026-10-31 21:44:11 +02:00
github-actions[bot] 63d022fe5f Bump versions for v12.76 2026-10-31 19:44:06 +00:00
Lauri Ojansivu e9cf825b22 Prepare v12.76 release 2026-10-31 21:43:03 +02:00
Lauri Ojansivu 4ce8514e36 CHANGELOG: open issues #6834-#6840 - Today in the date picker, the card title and movable popups, Change Settings, the d shortcut, minicard dates.
#6834 is fixed in part and #6837 needs a decision: both in TODO Later.

Thanks to mimZD and xet7 !
2026-10-31 21:32:28 +02:00
Lauri Ojansivu 429fdc5319 Tests: a card-button rule is a button on the minicard (#6836 was already there).
"Show rule button on minicard" is Board Settings / Card, Card buttons,
Show on Minicard (allowsCardButtonsOnMinicard), in WeKan since v12.16:
the board's card-button rules are drawn under each minicard, and a click
runs the rule on that card without opening it. Off by default, which is
why it was not seen. It had no test.

minicard-rule-button-6836.e2e.js (Chromium and WebKit): with it on, a
card-button rule's button is on the minicard and a click archives the
card without opening it; with it off, no minicard has one.

Thanks to mimZD and xet7 !

Fixes #6836,
2026-10-31 21:31:36 +02:00
Lauri Ojansivu ffd5cbd2ad Change Settings: icons in a column on the first line, compact wrapped lines, rows divided.
Member Settings / Change Settings used the shared popup list as it is:
a flex row whose every line is 33px high with its icon centred. Its
settings are sentences, and most wrap - in Persian, in German - so a
setting became two tall lines with its icon floating between them and
the next setting right under it, with nothing to tell where one ended.
Each row is now an icon column, the words and the check mark at the
end; the icon sits on the first line, a wrapped line is an ordinary
line high, a rule divides the rows, and the Card settings checkbox has
the same space beside its words as an icon has.

Scoped to the popup's own list (change-settings-list): a popup keeps
the screens before it - the member menu - in the same .pop-over, so its
data-popup would have restyled those too.

change-settings-popup-6835.e2e.js, in Persian and English, Chromium and
WebKit: each row's icon is on its first line, a line is under 28px high
and a row only as tall as its lines, rows are divided, and the member
menu beside it is untouched.

Thanks to mimZD and xet7 !

Fixes #6835,
2026-10-31 21:29:31 +02:00
Lauri Ojansivu cf4b48df09 Minicard: a due date uses the card's width instead of three lines beside an empty strip.
The dates row of a minicard reserved 84px at its trailing edge - a third
of a 250px card - for nothing: the avatars float after it, and clear:
both starts it below the menu. A due date with its "1 day left" was
broken over three lines beside that empty strip, in a Persian board of
the report. It reserves the drag handle's room now, 24px, and the same
badge takes two lines at the default list width.

The board header bar wrapping to a second row, and the list header icons
under the card count, in the same report, did not reproduce on the
current code at the same window width, in Persian, with the card count
on; they are in CHANGELOG TODO Later until the reporter can say which
WeKan and which browser zoom showed them.

minicard-dates-width-6834.e2e.js (Chromium and WebKit, English and
Persian): the dates row reserves at most 24px, and the due date badge is
at most two lines high.

Thanks to mimZD and xet7 !
2026-10-31 21:26:15 +02:00
Lauri Ojansivu 73c21dfe3e Tests: typing d in the card title types at the end of the title in WebKit too.
WebKit puts the caret at the start of a field on focus and Chromium at
the end, so the d typed into the title landed at its start in WebKit and
the check for a title ending in dd failed there - while what the test is
for, that d typed in a field opens no Due date editor, held in both.
The test puts the caret at the end itself now.

Thanks to xet7 !
2026-10-31 21:23:04 +02:00
Lauri Ojansivu 22a12dc7f8 Opened card: the whole title edits; the window and every popup move by their bar or handle.
The opened card's title was split like a minicard's: the leading half
opened the editor and the trailing half dragged the window, so clicking
the right of the title, or beside a short last line, did not edit. Now
all of the title edits. With drag handles off, the window is moved from
the empty space of its header row above the title - between the collapse
caret and the menu; with drag handles on, only by the drag handle. A
reader who may not edit has no edit zone and still drags by the title.

Every popup now moves the same way - by its title bar with drag handles
off, by the handle before its title alone with them on - where only the
Drawing, Board View and Board Card settings popups and the date popups
moved before. A dragged popup is kept inside the window as it grows. The
drag pins a popup where it is before moving it: one placed from the
right edge (the member menu) jumped across the window, and one with a
margin by that margin, as the drag began.

card-title-edit-and-move-6839.e2e.js, in Chromium and WebKit: a two-line
title edits from its right, middle and left; the empty header row moves
the window by exactly the drag with handles off and not at all with them
on, when the handle does; and the card menu, Labels and the member menu
move by their title bar, or with handles on by the handle alone.
titleClickToEdit, popupMoveMaximize and popupBounds pin it.

Thanks to mimZD and xet7 !

Fixes #6839,
2026-10-31 21:23:04 +02:00
Lauri Ojansivu b1403ca7af The d shortcut opens the Due date editor of a card that is still being opened.
On a slower server a card's details render a few hundred milliseconds
after the click that opens it. `d` pressed in that moment found no Due
date control yet and did nothing - only the hovered-minicard path (#6755)
waited for it. Both paths now wait for THAT card's control, stop if
another card was opened meanwhile, click it once, and give up after 3
seconds.

The card details also closed any popup of their card when they were
destroyed (#6686), which a re-render of the card still open does too: a
popup could open and shut again at once. They close it now only when
the card is no longer the open one - closed, or another card opened.

The Playwright test emulates a 300 ms connection in Chromium, clicks
each card open and presses d at once: the editor opens and is still
open 1.5 s later, and closing the card still closes it. It fails on the
code before this change. dueDateKeyboardShortcut and
labelsPopupStaleCardContext pin both halves.

Thanks to mimZD and xet7 !

Fixes #6840,
2026-10-31 21:06:53 +02:00
Lauri Ojansivu bc21c901f4 Date picker: a Today button, and a month navigated back to is drawn whole.
The board Calendar view already has Today (and Calendar mode too), but
the date picker of every date popup - Due, Start, End, Received and the
rest - had only « ‹ › », so a user who paged to another month had to
page back. Today, between the month and ›, shows today's month and
focuses today; it changes nothing until a day is clicked and saved.

Testing it found a picker bug that was already there: going back to a
month drew it wrong - October came back as "29, empty, 30, empty", with
no 31st. Blaze matches list items by _id, and the weeks and days had
none; the empty cells, all alike, were moved around. Every week and
cell, empty ones too, now has an _id that names its month.

calendarPickerControls.test.cjs checks the ids, that Today shows today's
month and focuses today, and that it selects nothing; the Playwright test
pages two months ahead, presses Today, and checks every day of the month
in order, today focused, and the card's due date unchanged.

Thanks to mimZD and xet7 !

Fixes #6838,
2026-10-31 21:06:53 +02:00
Lauri Ojansivu 88fef5dc08 CHANGELOG: the Password length strings in every language, and its title bar.
Thanks to xet7 !
2026-10-31 15:18:23 +02:00
Lauri Ojansivu 3f9379ac86 Translations: the Password length page and Change your password, in 234 languages.
The 18 new strings of Admin Panel / Problems / Password length and of the
"Change your password" dialog, written in the language and script each
locale tag names, with the terms each file already uses for password,
account, save and sign in, and __min__ kept exactly. Only those 18 keys
changed in each file, written with fill-translations.mjs --apply, which
fills English placeholders only.

Low confidence, for a speaker to correct on Transifex: ace, ak, an, ary,
ast-ES, ay, ba, bho, bi, bm, bo, br, bua, chr, co, csb, cv, cy, dz, ee,
ff, fj, fo, fur, ga, gd, gn, gv, ha, haw, hsb, ig, iu, jv, kl, kok, ks,
kw, lg, lld, mg, mi, mt, nah, nap, nd, nso, ny, om, pap, qu, rm, rn,
rup, rw, sah, sc, scn, sd, se, si, sm, sn, so, ss, st, szl, te-IN, tg,
ti, tig, tk_TM, tlh, tn, to, tpi, ts, tt, uz-AR, ve, ve-CC, ve-PP, vo,
wa, wa-RR, wal, wo, wuu-Hans, xh, yi, zgh.

Thanks to xet7 !
2026-10-31 15:18:13 +02:00
Lauri Ojansivu 85af731c1b Admin Panel / Problems / Password length names itself in the title bar.
The title bar keeps its own copy of the menu labels, and the new pane
had none: tests/adminUrls.test.cjs failed on problems/password-length.

Thanks to xet7 !
2026-10-31 15:18:11 +02:00
Lauri Ojansivu bb32a50550 CHANGELOG: sign-up works again, Admin Panel / Problems / Password length, and a changed password signs every session out.
Thanks to xet7 !
2026-10-31 14:56:22 +02:00
Lauri Ojansivu c99aa7f845 Admin Panel / Problems / Password length, and a changed password signs every session out.
PASSWORD_MIN_LENGTH (default 8) is now the length below which a sign-in
is REPORTED, in the new Admin Panel / Problems / Password length, and
PASSWORD_MIN_LENGTH_ENFORCE (off by default) is what makes it a rule: a
shorter new password is refused, and an account that signs in with an
older shorter one sees only "Change your password" until it has a long
enough one. Signing in is never refused, enforced or not, so nobody is
locked out by a minimum set after their password. LDAP, OAuth2, OIDC,
SAML, CAS and Sandstorm accounts are neither reported nor asked: their
password is their provider's. An administrator's Set password no longer
applies WeKan's policy to such an account.

Meteor sends a password hashed, so only the browser knows its length: it
notes it on the sign-in form and, after a sign-in that worked, says "it
was short" - never the password or its length.

The page has both settings and a table, one row per account: username,
first and last short-password login, the latest IPv4 and IPv6 address
with the country's flag and city when a CDN says where it is, the count,
and when it was fixed, with the Too short (default) / Fixed / All filter.
Fixed is a changed password not seen short since; a short one seen
again reopens the row.

Changing one's own password - Member Settings, the forced change, a
reset link - now signs EVERY session of the account out, the one that
changed it too, and only the new password signs in again. The browser
goes to the sign-in page as soon as it is signed out, since the server
closes the connection before the answer can arrive.

Documented in docs/Features/Admin-Panel/Problems/Password-length.md, the
settings on every platform. The new strings are in every locale in
English for now. Tests: passwordPolicy, passwordChangeRequired and
shortPasswordLogins (node); password-min-length-signup-login,
password-length-report and password-change-other-sessions (Playwright,
Chromium and WebKit), which change each setting on the page and check
what it does, and that sign-up with no mail server registers and signs
the user in.

Thanks to xet7 !
2026-10-31 14:54:22 +02:00
Lauri Ojansivu 508484ef1e Sign-up, and every method and publication, pass audit-argument-checks again.
Registering answered "Internal server error" - and created the account
anyway, so the next try said it already existed - and the
starredPublicBoards and translation publications failed for everybody:
"Did not check() all arguments". audit-argument-checks asks,
synchronously, the moment a handler returns, whether check() saw every
argument. The REQUIRE_2FA wrapper of every method and publication, and
the Audit log's wrapper of the administrators' methods, were async and
awaited before they called the handler - even with REQUIRE_2FA off - so
the handler's own check() came after the audit had already failed the
call, and the handler went on running.

A wrapper now calls the handler at once when it needs no lookup
(REQUIRE_2FA off, nobody signed in, a cached answer). When it must await
first, it hands the audit over (server/lib/argumentChecks.js): the
arguments count as seen for the outer audit, and the handler runs under
an audit of its own that it must satisfy exactly as unwrapped.

tests/wrappedHandlerArgumentChecks.test.cjs runs both wrappers under a
model of the check package's audit: a checking handler passes, off and
on, a refusal stays a refusal, a handler that does not check still fails
on every path, and no wrapper in server/ awaits before it has checked.

Thanks to xet7 !
2026-10-31 14:13:40 +02:00
Lauri Ojansivu 548196c9a9 CHANGELOG: Meteor in .tools/.meteor no longer springboards forever.
Thanks to xet7 !
2026-10-31 13:23:36 +02:00
Lauri Ojansivu 9a118c83b2 Meteor in .tools/.meteor no longer springboards forever.
A build from the menu printed uname -p and sysctl hw.cpu64bit_capable
over and over and never reached its first package. With
METEOR_WAREHOUSE_DIR set and no SANDBOX, Meteor takes the running tool
to be wherever the warehouse's "meteor" link points - 3.5.4, the release
it was installed with - so with .meteor/release at 3.6-rc.1 every start
springboarded to 3.6.0-rc.1, found itself the wrong tool again and
springboarded again: 28 times in 20 seconds of meteor --version. The
earlier 1.1 GB build log was the same loop, its module trace printed
anew on every exec.

tools-env.sh and tools-env.bat now set SANDBOX=true beside the
warehouse, as Meteor's own self-test does; that one check is all it
changes. The Nextcloud test scripts and the dev-source runner, which set
the warehouse themselves, do the same. meteor --version now springboards
once, from the linked 3.5.4 to 3.6.0-rc.1.

tests/meteorSpringboardLoop.test.cjs checks both env scripts, that
nothing sets the warehouse without SANDBOX, and runs the installed
Meteor to count its springboards.

Thanks to xet7 !
2026-10-31 13:23:24 +02:00
Lauri Ojansivu d442bd8a7e CHANGELOG: builds trace module resolution only with WEKAN_BUILD_TRACE_MODULES=1.
Thanks to xet7 !
2026-10-31 13:02:19 +02:00
Lauri Ojansivu 5a13e927bc Builds trace module resolution only with WEKAN_BUILD_TRACE_MODULES=1.
Turning the NODE_DEBUG=module trace off for EVERYTHING alone was not
enough: a dev build from the build.sh menu wrote 1.1 GB, three million
lines, in its first three minutes, before it was interrupted. The trace
is now opt-in in build.sh and build.bat alike, and the diagnostics line
says whether it is on and how to turn it on. The other build diagnostics
(resolver commands, npm verbose, METEOR_PROFILE) are unchanged.

Thanks to xet7 !
2026-10-31 13:01:57 +02:00
Lauri Ojansivu c78af24a36 CHANGELOG: EVERYTHING builds without the per-require module trace.
Thanks to xet7 !
2026-10-31 12:46:14 +02:00
Lauri Ojansivu 6551062492 EVERYTHING builds without the per-require module trace.
The build's NODE_DEBUG=module trace wrote over 12 GB in the first half
hour of an EVERYTHING run on macOS - twice, through tee - and slowed the
build it traced, so the run would fill the disk before reaching the tests.
A build from the menu still traces by default; WEKAN_BUILD_TRACE_MODULES=0
turns it off, and EVERYTHING sets that unless the caller asked for 1. The
other build diagnostics (resolver commands, npm verbose, METEOR_PROFILE)
are unchanged.

Thanks to xet7 !
2026-10-31 04:15:01 +02:00
Lauri Ojansivu a0ed5de6f1 CHANGELOG: the breached-password check's lookup digest (CodeQL #599).
Thanks to GitHub CodeQL and xet7 !
2026-10-31 03:52:47 +02:00
Lauri Ojansivu f406d46fe7 Breached-password check: one Web Crypto lookup digest for the browser and the server.
CodeQL #599 (js/insufficient-password-hash, CWE-916) flagged the SHA-1
the server computed for PASSWORD_CHECK_PWNED. That digest is not password
storage - accounts-password keeps passwords with bcrypt - and it is never
stored or logged: Have I Been Pwned's range API is keyed by SHA-1, its
first 5 hex digits are the question and the rest is compared in memory.
A slow hash cannot be used, because the service could not match it.

The browser and the server now compute it with one implementation,
breachLookupDigest in models/lib/passwordPolicy.js, which says what it is
and is not; the server no longer hashes a password with a hash of its
own, and a digest that cannot be computed lets the password through.
Tests pin that the digest is SHA-1 for ASCII and UTF-8, that it reaches
only the lookup and the comparison, and that nothing in WeKan hashes a
password-named value with a fast hash. Unreleased hardening, so no Hall
of Fame entry: docs/Security/Password-Breach-Check-Digest-2026-10-31.md.

Thanks to GitHub CodeQL and xet7 !
2026-10-31 03:52:32 +02:00
Lauri Ojansivu 4b0aa715eb Merge remote-tracking branch 'origin/main' 2026-10-31 03:36:43 +02:00
Lauri Ojansivu f5af33ac38 CHANGELOG: Audit log documentation.
Thanks to xet7 !
2026-10-31 00:44:17 +02:00
Lauri Ojansivu f165a24192 docs: Admin Panel / People / Audit log.
Thanks to xet7 !
2026-10-31 00:44:14 +02:00
Lauri Ojansivu e6ea524541 CHANGELOG: password policy, compulsory 2FA, idle sign-out, audit log, HSTS, cross-site guard and S3 encryption.
Thanks to xet7 !
2026-10-31 00:33:31 +02:00
Lauri Ojansivu de23dece66 Translations: the password policy, two-factor, audit log and S3 encryption strings in 234 languages.
The 16 strings the security features added were English placeholders in
every locale; each is now in the language and script its locale tag
names, with the terms the file already uses for password, account,
attachment, backup and two-factor authentication. __min__ is kept
character for character. Where the right word is spelled like the
English ("Administrator"), the column header says "Admin" or "Who"
instead, because a value equal to English is not a translation.

fy and fy-NL: the existing two-factor strings were Dutch in a Frisian
file, and are Frisian now; tests/frisianTwoFactorLanguage.test.cjs.
tig: Tigre spells storage መክዘን, not Tigrinya's መኽዘን.

Low confidence, for a speaker to correct on Transifex: ace, ak, an, ary,
as, ast-ES, ay, ba, bho, bi, bm, bo, br, bua, chr, ckb, co, csb, cv, dz,
ee, ff, fj, fy, gn, gv, haw, hsb, ht, ig, iu, kl, kok, ks, kw, la, lg,
lld, mg, mi, nah, nd, nso, ny, om, pap, ps, qu, rm, rn, rup, rw, sah, sc,
scn, sd, se, si, sm, sn, so, ss, st, szl, tig, tlh, tn, to, tpi, ts,
uz-AR, ve, ve-CC, ve-PP, vo, wa, wa-RR, wal, wo, xh, yi, zgh.

Thanks to xet7 !
2026-10-31 00:32:43 +02:00
Lauri Ojansivu 2017f2871f Audit log: its own Admin Panel URL and title, and right-to-left table alignment.
/admin/people/audit-log opens the pane and names it in the title bar like
every other People pane, and the table aligns to the start of the line,
so it reads right to left in Arabic, Hebrew, Persian and the rest.

Thanks to xet7 !
2026-10-31 00:32:33 +02:00
Lauri Ojansivu 302b47db6f docs: Security settings, every hardening option in one page.
Thanks to xet7 !
2026-10-31 00:15:13 +02:00
Lauri Ojansivu 482c2c29f4 Attachments, avatars and backups in S3 can be encrypted at rest.
Admin Panel / Attachments / S3 / Server-side encryption: SSE-S3 (AES256)
or SSE-KMS (aws:kms, with a KMS key or the account's default). S3
encrypts each object as it is written and decrypts it for WeKan on read,
so reads, range requests and moves are unchanged. Every attachment and
avatar upload, every renamed object and every backup written to S3 now
carries the choice; a value that is not one of them, or a malformed key,
adds nothing.

On the filesystem, encryption at rest stays with the disk or volume
(LUKS, an encrypted cloud volume): exports, PDF, thumbnails, virus scans,
backups and WebDAV all read the stored files directly, and encrypting
them inside WeKan would change every one of those and convert every file
already stored.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:12:09 +02:00
Lauri Ojansivu 4d5b832499 Admin Panel / People / Audit log: what site administrators did.
Nothing recorded who set somebody's password, made somebody an admin,
changed a login setting or restored a backup. Every call of those
administrative methods by a site administrator is now kept, one document
each, for a year: when, who, which action, the outcome - a refused call
too - its arguments and the address. A password or a secret never
reaches the trail: values under a credential-named key, the arguments
that are a password by position, and a password inside a URL are
replaced by [redacted]. An administrator editing cards is not in it,
the trail is shown to site administrators only, and a trail that cannot
be written never changes what the method did. It and the REQUIRE_2FA
wrapper keep each other's marks, so neither wraps the other twice.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:09:20 +02:00
Lauri Ojansivu 41173f0f6b Sign a browser session out after it has been idle for LOGOUT_IDLE_MINUTES.
LOGOUT_WITH_TIMER ends a login at a deadline however busy its user is;
nothing ended a session somebody walked away from. LOGOUT_IDLE_MINUTES
(Admin Panel / People / Login, every platform; empty or 0 is off) does:
the browser reports a key press, click, touch or scroll at most once a
minute, the server keeps the time per login token in idleSessions, and
once a minute every token idle past the limit is removed, which signs its
connections out. A tab that reconnects and resumes its login by itself -
a laptop waking up - is not activity and does not extend the session.
REST API tokens have no session record and are never removed. The time
is kept beside the user rather than on the token, because updating one
element of the token array needs the positional $ operator, which not
every database WeKan runs on supports.

Thanks to xet7 !
2026-10-31 00:08:20 +02:00
Lauri Ojansivu d7abe9910a Password policy for new passwords, and compulsory two-factor authentication.
New passwords: PASSWORD_MIN_LENGTH (default 8) and PASSWORD_CHECK_PWNED
(off by default), in Admin Panel / People / Login and on every platform.
The browser sends a password to Meteor hashed, so the server checks every
password that reaches it as text - Admin Panel Set password and Add user,
POST /users/register - and the sign-up, Change Password, reset and
enrolment forms check in the browser. Signing in is never checked, so an
existing shorter password keeps working until it is changed. A breached
password is found with Have I Been Pwned's k-anonymity range API: only 5
hex digits of the SHA-1 leave WeKan, and an unreachable service never
refuses a password.

REQUIRE_2FA: off (default), admins or all. It covers accounts that sign in
with a WeKan password; LDAP, OAuth2, SAML, CAS and Sandstorm accounts sign
in through an identity provider and are not asked. Such an account can
still sign in, sees only the 2FA set-up until it has 2FA, and until then
the server refuses every method and publication but signing in and
enrolling, HTTP routes do not accept its login and the REST API gives it
no token. Once required, 2FA cannot be switched off.

The new interface strings are pending translation.

Thanks to xet7 !
2026-10-31 00:07:58 +02:00
Lauri Ojansivu 48a2e97ea3 A state-changing request that logs in only with the cookie must come from WeKan itself.
POST /import-trello and other HTTP routes accept the login cookie as the
login, so a page on another site could make a signed-in browser send one.
The cookie is SameSite=Strict, which keeps it off such a request in
today's browsers; this is the second line, for older browsers, same-site
sibling hosts and routes added later. A POST, PUT, PATCH or DELETE whose
only login is the cookie must carry an Origin, or Referer, naming
ROOT_URL's host or the Host / X-Forwarded-Host it was sent to, which on
Sandstorm is the grain's own host. Token-authenticated API calls, Meteor's
cookie routes and DDP are not affected. A refusal is recorded in Admin
Panel / Problems as csrf.cross-site-cookie and never blocks the account
whose cookie it was.

Thanks to xet7 !
2026-10-30 23:52:09 +02:00
Lauri Ojansivu 4993242c5f Strict-Transport-Security on every response of an HTTPS site.
WeKan sent no HSTS header, so a link typed as plain HTTP or a downgrade on
a hostile network could strip TLS from the next visit unless the reverse
proxy added it. It is now sent whenever ROOT_URL is https: 180 days, this
host only. HSTS_MAX_AGE, HSTS_INCLUDE_SUBDOMAINS and HSTS_PRELOAD (Admin
Panel / People / Login, every platform) change it; 0 stops it, and preload
is sent only with includeSubDomains and at least a year. Every response
also carries X-Content-Type-Options: nosniff and
Referrer-Policy: strict-origin-when-cross-origin.

Thanks to xet7 !
2026-10-30 23:50:02 +02:00
Lauri Ojansivu 12eef31162 CHANGELOG: snap backup and restore instructions work on AlmaLinux, Rocky Linux and Fedora.
Thanks to Group-Deed and xet7 !
2026-10-30 23:14:01 +02:00
Lauri Ojansivu 0b5c855820 Snap backup and restore instructions work on AlmaLinux, Rocky Linux and Fedora.
snapd keeps the snaps in /var/lib/snapd/snap on Fedora, RHEL, AlmaLinux,
Rocky Linux, CentOS Stream and Arch Linux, which have no /snap, so the
instructions that exported /snap/wekan/current/bin and its lib directory
did not find mongodump and mongorestore there. They now pick the directory
with one S= line and the CPU's library directory with $(uname -m), and the
scripts that can be run from a host shell find the snap in either place.

Thanks to Group-Deed and xet7 !
2026-10-30 23:13:50 +02:00
Lauri Ojansivu 0c53ef6960 CHANGELOG: boards from before the templates feature are shown on All Boards again.
Thanks to xet7 !
2026-10-30 22:56:38 +02:00
Lauri Ojansivu 2ab0afdc93 Old boards without a type are shown on All Boards again.
Boards, swimlanes, lists and cards from before the templates feature have
no `type`: the schemas' defaultValue fills it on insert only. All Boards
publishes `type: 'board'` and the card queries ask for 'cardType-card', so
a Sandstorm grain migrated from MongoDB 3 opened onto an empty All Boards
page although its board was intact in the database. The new
type-defaults-backfill startup step sets the schema default on every
document whose type is missing, null or empty, and never touches an
explicit type.

Thanks to xet7 !
2026-10-30 22:56:24 +02:00
github-actions[bot] f2a1d7b9ba Bump versions for v12.75 2026-10-30 20:26:45 +00:00
Lauri Ojansivu cd1f1b8f05 Prepare v12.75 release 2026-10-30 22:25:30 +02:00
Lauri Ojansivu b941f36e4c Merge remote-tracking branch 'origin/main' 2026-10-30 22:25:01 +02:00
Lauri Ojansivu 487a528806 CHANGELOG: a changed password signs the account out in every other browser.
Thanks to xet7 !
2026-10-30 21:39:35 +02:00
Lauri Ojansivu dc9a7bb1ac Tests: a changed password signs the account out in every other browser.
A report described "failure to invalidate session on password change": the
same account signed in on two browsers, the password changed in one, and
the other's session "updated" rather than ended. WeKan does not do that.
Member Settings / Change Password is Meteor's changePassword, which removes
every login token but the changing session's; the admin's Set password is
setPasswordAsync, which removes them all, as resetPassword does; and a
session whose token is gone is closed, while WeKan's HTTP routes accept only
a token still in the list.

password-change-other-sessions.e2e.js runs the report in two browser
contexts: both signed in with the password, the password changed through
the form in one, and the other is signed out within seconds, stays signed
out after a reload, and only the new password signs in. It passes three
times in a row in Chromium and in WebKit. passwordChangeSessions.test.cjs
pins those paths and fails on a password change with logout: false, the
LDAP sign-in's password sync being the one stated exception.

Thanks to xet7 !
2026-10-30 21:39:20 +02:00
github-actions[bot] 696a1a2c8a Bump versions for v12.74 2026-10-30 08:15:31 +00:00
Lauri Ojansivu ef1e683cdd Prepare v12.74 release 2026-10-30 10:14:18 +02:00
Lauri Ojansivu 55163d0ce6 Merge remote-tracking branch 'origin/main' 2026-10-30 10:14:15 +02:00
Lauri Ojansivu b974ba426d CHANGELOG: Caddy and FerretDB run with no MPTCP listener.
Thanks to xet7 !
2026-10-30 04:31:14 +02:00
Lauri Ojansivu e9751b65d4 Caddy and FerretDB run with no MPTCP listener (CVE-2026-98432).
CVE-2026-98432 is a Linux kernel use-after-free in IPv4 TCP connection
handling, shown to give remote code execution through an MPTCP listener
(golang-announce, 2026-10). Since Go 1.24 every Linux listen socket of a
Go program is MPTCP by default, and WeKan ships two Go servers: Caddy, the
snap's web server on ports 80 and 443, and FerretDB, the default database.

Every place WeKan starts one sets GODEBUG=multipathtcp=0: the snap's
environment (every app, Caddy and FerretDB among them), the Docker
entrypoint and the bundle launcher - which keep an operator's own
multipathtcp setting - every docker-compose FerretDB service (the commented
FerretDB 2 example too), and the FerretDB the Sandstorm grain starts. Caddy
is an upstream binary, so no rebuild is needed: Go reads GODEBUG at start.
wekan/FerretDB builds with `godebug multipathtcp=0` as well. The real fix
is a patched host kernel.

Checked on Linux: a Go server's default listener reports MPTCP true;
GODEBUG=multipathtcp=0, or the go.mod setting, makes it false.
tests/goMptcpOff.test.cjs pins every launcher, runs the launcher line, and
fails when something turns MPTCP back on.

Thanks to xet7 !
2026-10-30 04:30:39 +02:00
github-actions[bot] 5abc2809b9 Bump versions for v12.73 2026-10-30 02:18:18 +00:00
Lauri Ojansivu 03f3a197cf Prepare v12.73 release 2026-10-30 04:17:07 +02:00
Lauri Ojansivu 6ed3f5cfd0 CHANGELOG: v12.73 was not released, so its section is Upcoming again.
"Prepare v12.73 release" renamed Upcoming, but its push was refused and
the release workflow was never dispatched: there is no v12.73 tag and no
v12.73 GitHub Release. As CLAUDE.md says for a release that published
nothing, the heading goes back to "# Upcoming WeKan ® release", so the next
release-all.sh takes v12.73 again. It also gains the entry for the fix that
keeps this from happening again.

Thanks to xet7 !
2026-10-30 04:16:04 +02:00
Lauri Ojansivu e372016826 Release All pulls before releasing, and pulls and retries when its push is refused.
v12.73 was prepared on a main that origin had moved past: the release
bot's v12.72 version bump had landed meanwhile. release-all.sh renamed
Upcoming and committed "Prepare v12.73 release", its bare git push was
refused, and set -e stopped it before gh workflow run - so no release ran,
and no tag was made, while the CHANGELOG already said v12.73.

release-all.sh now runs build.sh's git pull right after its checks, before
it chooses the version or renames anything, and checks the Upcoming section
again after it; and it pushes with build.sh's git push, which repairs moved
CHANGELOG links and pulls and pushes once more when origin moved meanwhile.
If the push still does not finish, it says that the workflow was NOT
dispatched, and the command to dispatch it.

releaseUpcomingPreflight pins the order - the pull before the version and
the rename, the push before the dispatch - and fails on a bare git push;
releaseTelemetry and the version-selection fixture follow.

Thanks to xet7 !
2026-10-30 04:14:48 +02:00
Lauri Ojansivu fd699b67e9 Merge remote-tracking branch 'origin/main' 2026-10-30 04:07:50 +02:00
Lauri Ojansivu 827641193f Prepare v12.73 release 2026-10-30 04:07:36 +02:00
Lauri Ojansivu fb7bd9c6de CHANGELOG: CodeQL alert #598, the Sandstorm host test.
Thanks to xet7 !
2026-10-30 04:02:02 +02:00
Lauri Ojansivu fcd995095d Tests: the Sandstorm host check names no host but sandstorm.org.
CodeQL alert #598 (incomplete URL substring sanitization, CWE-20) flagged
tests/sandstormOrgHosts.test.cjs, which skipped files that did not include
the old Sandstorm host name as a substring. The file no longer names that
host at all. It finds every Sandstorm host on a top-level domain other than
org by its shape, written as a URL - a scheme or // before it, or a port or
path after it - so property paths such as services.sandstorm.id, file names
such as sandstorm.js and the domains reserved for tests are not hosts. The
demo and the store workflow's older-market publish step stay allowed by
their subdomain and place.

A new self-test checks what it finds and what it does not; run against the
docs and workflows from before the sandstorm.org switch it finds 13 old
hosts, the installer address of release-all.yml among them.

Thanks to xet7 !
2026-10-30 04:01:36 +02:00
Lauri Ojansivu b7b5ec3711 CHANGELOG: the short CHANGELOG.md starts with the upcoming release.
Thanks to xet7 !
2026-10-30 03:56:32 +02:00
Lauri Ojansivu 1af642069c The short CHANGELOG.md starts with the upcoming release's In short.
CHANGELOG.md showed only the five newest releases, so what was about to be
released was only in CHANGELOG-big.md. changelog-short.mjs now puts the
unreleased # Upcoming WeKan ® release first - its In short and a link to
its full section - and the five newest releases below it; right after a
release, when there is no Upcoming yet, none is invented.

CLAUDE.md and AGENTS.md say what "update wekan changelog" means: find the
commits not in the changelog yet, add an Upcoming section when a release
was cut meanwhile, write the entries and the In short into
CHANGELOG-big.md, run changelog-short.mjs, run the changelog tests, and
commit both files.

tests/changelogShort.test.cjs: Upcoming first, whole, above the releases,
its link resolves, and no Upcoming appears after a release.

Thanks to xet7 !
2026-10-30 03:56:11 +02:00
Lauri Ojansivu 0343e6614b CHANGELOG: the short CHANGELOG.md, written from CHANGELOG-big.md.
Thanks to xet7 !
2026-10-30 03:30:23 +02:00
Lauri Ojansivu f1c0db247a CHANGELOG.md is the short changelog, written from CHANGELOG-big.md.
CHANGELOG-big.md is the changelog as it was: # Status, # Upcoming and every
release in full, with its <details> entries. Everything that writes or edits
the changelog uses it - release-all.sh, release-notes.sh and the release
workflow, the commit-link repair in build.sh and fix-changelog-hashes.sh,
the archive, the wrapper, the version scripts - and the release notes'
"More details at ChangeLog" link goes to its section. remote-release.py,
shared with FerretDB and the patch repositories, takes the file from its
config, and only WeKan's names CHANGELOG-big.md.

CHANGELOG.md is new: no Status and no Upcoming, only the In short paragraph
of each of the five newest releases and a link to its full section, in plain
Markdown - no <details>, <summary>, </summary> or </details>, and HTML links
written as Markdown links. releases/changelog-short.mjs writes it;
release-all.sh runs it right after naming the release, so it is in the
release commit, and changelog.sh does too. The Sandstorm App Market keeps
embedding CHANGELOG.md, now the short one.

old-CHANGELOG/'s headers say they moved out of CHANGELOG-big.md. References
to Upcoming or TODO Later in code comments and docs name CHANGELOG-big.md.
CLAUDE.md and AGENTS.md describe both files: CHANGELOG-big.md is written,
CHANGELOG.md is generated and never edited by hand.

tests/changelogShort.test.cjs: CHANGELOG.md is exactly what the generator
writes, the five newest releases with their whole In short, links that
resolve, and none of the four tags or an HTML link; the changelog tests
read CHANGELOG-big.md.

Thanks to xet7 !
2026-10-30 03:29:48 +02:00
Lauri Ojansivu 61e838df46 Rename CHANGELOG.md to CHANGELOG-big.md.
Only the rename, in a commit of its own, so that git log --follow and blame
keep the changelog's history; the next commit adds the new short
CHANGELOG.md and points the release tooling at CHANGELOG-big.md.

Thanks to xet7 !
2026-10-30 03:29:34 +02:00
github-actions[bot] 057cbc26d9 Bump versions for v12.72 2026-10-30 00:37:33 +00:00
Lauri Ojansivu 22e8b9bc86 Prepare v12.72 release 2026-10-30 02:36:31 +02:00
Lauri Ojansivu 602b47f06a CHANGELOG: new Sandstorm app market screenshots.
Thanks to xet7 !
2026-10-30 02:35:31 +02:00
Lauri Ojansivu a086323a1f Updated Sandstorm WeKan screenshots.
Thanks to xet7 !
2026-10-30 02:33:12 +02:00
Lauri Ojansivu 22140739ab CHANGELOG: card links never launch an OS handler, the Apache guide and Sandstorm publishing.
Thanks to ocdtrekkie and xet7 !
2026-10-30 01:26:40 +02:00
Lauri Ojansivu 55b71ce19e Tests: the Sandstorm store workflow may publish to apps.sandstorm.io too.
#6833 publishes WeKan to both Sandstorm app markets, apps.sandstorm.org
and the older apps.sandstorm.io, whose API is alpha-api.sandstorm.io.
sandstormOrgHosts.test.cjs refused every sandstorm.io host; it allows
that publish step in sandstorm-store.yml only, and checks that both
markets are published to.

Thanks to ocdtrekkie and xet7 !
2026-10-30 01:25:31 +02:00
Lauri Ojansivu 1a97c8bd86 Apache guide: no request captures in proxy targets, and httpd 2.4.60 or newer.
Checked against the 2024 Confusion Attacks on Apache httpd
(blog.orange.tw, CVE-2024-38472 to -38477 and CVE-2024-39573, fixed in
2.4.60). The guide's examples were reverse proxies only, with no
RewriteRule, but each passed the WebSocket with ProxyPassMatch and $1 - a
request capture in a proxy target, the shape those attacks abuse - and in
the sub-URL example that rule looked for ^/sockjs while the requests come
to /wekan/sockjs, so its WebSocket never connected.

- Both examples pass the WebSocket with "ProxyPass ... upgrade=websocket"
  on their fixed prefix (2.4.47+); no ProxyPassMatch remains.
- The guide asks for httpd 2.4.60 or newer, drops NameVirtualHost (a no-op
  in 2.4) and the path in the sub-URL ServerName, which is not valid.
- A "What not to add" section: no RewriteRule or ProxyPassMatch with a
  request capture in the target, no <Files>/<Location> ACL in front of
  WeKan, AllowEncodedSlashes Off and MergeSlashes On, <Directory /> denied.

Verified with Apache 2.4.67: both virtual hosts parse, and in front of a
WebSocket server both pass HTTP and the WebSocket, at / and at /wekan;
the old sub-URL example fails its WebSocket ("Unexpected server response:
200"). tests/apacheReverseProxy.test.cjs pins it and fails on the old guide.

WeKan's own redirects stay on its origin (CAS checks the callback's origin
against ROOT_URL; the file and avatar redirects build their own paths), so
a backend-chosen Location cannot be steered elsewhere.

Thanks to xet7 !
2026-10-30 01:24:48 +02:00
Lauri Ojansivu b4e5f90afb Card links never launch an OS handler with a code-execution history.
Custom URL schemes in card text link only when Admin Panel -> Features ->
URL lists them, and never javascript:, data: or another scheme that runs
code in the page. A listed operating-system handler was linked, though, and
some have a public code-execution history: ms-msdt (Follina,
CVE-2022-30190), search-ms and search, ms-officecmd, ms-appinstaller,
ms-cxh and ms-cxh-full, and telnet - the last step of the 2026 Edge full
chain, where Windows ran telnet.exe from a user-writable PATH directory.
An administrator listing one would let any board member plant a link that
runs something on whoever clicks it.

- They are never linked, listed or not, by the app's parser and by the
  markdown package's copy of the rule (the two lists must be identical),
  and a click on one does nothing should such a link reach the page.
- A card title or description, comment, checklist or checklist item that
  ADDS a link to one is shown in Admin Panel -> Problems (HandlerLink,
  CWE-939), with who wrote it; the text is saved as written and the writer
  is told nothing. The word in prose, or a link the text already had, is
  not recorded.
- docs/Features/Admin-Panel/Settings/Visibility.md lists both kinds of
  never-linked scheme, and says what a listed file: does on Windows (an SMB
  link outside the network sends the clicker's NTLM hash).

Tests: tests/urlSchemeAllowlist.test.cjs (positive and negative, through
the real DOMPurify, both sanitizer passes and the detector; fails on the
old code), and custom-url-schemes.e2e.js, which passes in Chromium and
WebKit: listed handlers do not link, a description saved with one through
the editor is recorded for its writer, and a kept link or a comment that
only mentions the word is not. Firefox does not start in this
environment.

Thanks to xet7 !
2026-10-30 01:19:16 +02:00
Lauri Ojansivu 2c1fc39f61 Fix Sandstorm store spk publish syntax.
Thanks to ocdtrekkie and xet7 !
2026-10-30 01:14:55 +02:00
Lauri Ojansivu ee82c21718 Merge pull request #6833 from ocdtrekkie/patch-1
Sandstorm: Publish to both app markets
2026-10-30 01:08:19 +02:00
Jacob Weisz 10851cd3f4 Sandstorm: Publish to both app markets 2026-10-29 18:04:45 -05:00
github-actions[bot] a937ed2180 Bump versions for v12.71 2026-10-29 22:39:44 +00:00
Lauri Ojansivu 22be2f2dc0 Prepare v12.71 release 2026-10-30 00:38:34 +02:00
Lauri Ojansivu 833c0bc021 CHANGELOG: two entries made after v12.70 go to Upcoming, not into v12.70.
The v12.70 release renamed Upcoming to v12.70 at 23:59, and the entries for
the Hall of Fame guards (43c4d41877) and the Sandstorm install address
(ca2dc5fae5), committed after it, were added to its Tests and Releases
groups - inside a released section, for commits that release does not
contain, and with no Upcoming section left for release-all.sh to take the
next version from. They move to a new Upcoming section; the v12.70 section is
again exactly what v12.70 shipped. changelogEntriesBelongToTheirRelease
passes.

Thanks to xet7 !
2026-10-30 00:36:01 +02:00
Lauri Ojansivu 1251c1c8b3 Merge remote-tracking branch 'origin/main' 2026-10-30 00:33:58 +02:00
Lauri Ojansivu 5b428faea6 CHANGELOG: the Sandstorm build installs from install.sandstorm.org.
Thanks to xet7 !
2026-10-30 00:28:08 +02:00
Lauri Ojansivu ca2dc5fae5 Sandstorm: build from install.sandstorm.org, and link sandstorm.org everywhere.
v12.69's build-sandstorm in release-all.yml stopped because the
certificate of install.sandstorm.io had expired. It fetches the installer
from install.sandstorm.org, as sandstorm.yml and sandstorm-store.yml
already did, and so does releases/install-sandstorm.sh. That installer
downloads Sandstorm from dl.sandstorm.org and configures a development
server at local.sandstorm.org, so the docs follow: docs, apps, dl, install
and local.sandstorm.io links and addresses become sandstorm.org in the docs,
SECURITY.md, code comments and the accounts-sandstorm README, each URL
checked to answer there. demo.sandstorm.io stays, as it has no sandstorm.org
name; released CHANGELOG sections are records and stay as they are. The
release audit's baseline allows the two code comments' docs.sandstorm.org.

tests/sandstormOrgHosts.test.cjs: every Sandstorm build installs from
install.sandstorm.org, and no tracked file names a sandstorm.io host
outside those kept on purpose; it fails on the old release-all.yml.

Thanks to xet7 !
2026-10-30 00:27:54 +02:00
Lauri Ojansivu b9f6eff3d9 CHANGELOG: the Hall of Fame guards read linked vulnerability names.
Thanks to xet7 !
2026-10-30 00:20:59 +02:00
Lauri Ojansivu 43c4d41877 Tests: the Hall of Fame guards read a linked vulnerability name too.
hallOfFameProblemsCoverage and securityRegressionCoverage collected the
names from <td valign="top"><b>Name</b></td>, so a name that was a link to
its page was never seen. Now that every name is one, the match takes the
link; the four it had missed - ImportPlacementBleed, SamlSubjectBleed,
StampBleed and UploadBleed - are listed as guarded by the suites that name
them, and the inventory count goes from 137 to 141.

Thanks to xet7 !
2026-10-30 00:20:47 +02:00
github-actions[bot] 609c6d3d64 Bump versions for v12.70 2026-10-29 22:00:25 +00:00
Lauri Ojansivu 8097935f5d Prepare v12.70 release 2026-10-29 23:59:10 +02:00
Lauri Ojansivu 4f7222f541 CHANGELOG: AppImages, Launchpad snap source, certificate errors and three test fixes.
Thanks to xet7 !
2026-10-29 23:50:04 +02:00
Lauri Ojansivu 6bbb4d39bc Downloads: an unverifiable TLS certificate is named as that, not as an outage.
v12.69's build-sandstorm stopped because install.sandstorm.io's certificate
expired that day (Let's Encrypt, notAfter 2026-10-29 19:02 GMT), and
fetch.sh reported "The server is having an outage" for curl's exit 60. It
now says the certificate could not be verified and that only that server's
owner can fix it. dl.sandstorm.io, where Sandstorm itself is downloaded
from, is valid.

Thanks to xet7 !
2026-10-29 23:49:36 +02:00
Lauri Ojansivu a1f2c006b4 Snap on Launchpad: fetch.sh is uploaded with the tools-env.sh it sources.
releases/prepare-launchpad-source.sh uploads only the files the snap recipe
reads, and listed releases/fetch.sh without releases/tools-env.sh, which
fetch.sh sources for its TMPDIR. In v12.69's armhf build the caddy part's
fetch.sh had no TMPDIR, mktemp made nothing, and curl got an empty -o seven
times ("option -o: is badly used here"), with pauses of up to 480s, before
it was reported as a server outage.

- tools-env.sh is uploaded, and it makes .tools/tmp in a tree that has only
  snapcraft.yaml and releases/, as the Launchpad source does.
- fetch.sh stops at once when it has no temporary file, and does not retry
  curl's exit 2 (it could not start) or call it an outage.
- tests: every releases/ file a listed script sources or runs must be
  listed; tools-env.sh and fetch.sh work in such a tree with no .git and no
  TMPDIR; both fail on the old list. fetch.sh's two new refusals are tested.

s390x failed earlier, on Launchpad: even a new build answered "Could not
find repository at path" three times.

Thanks to xet7 !
2026-10-29 23:48:30 +02:00
Lauri Ojansivu dfdadaa8ca AppImages: FerretDB and mongo-tools are readable by every user again.
releases/fetch.sh downloaded into a mktemp file and moved it into place, so
every download kept mktemp's 0600, and the bundled ferretdb and mongo-tools
were 0711 after chmod +x. A user other than the owner gets "Permission
denied" from such a file, and since today's appimagetool checks for it, all
six v12.69 AppImages (i686, x86_64, aarch64, armhf and both musl ones)
failed: "has mode 0711, but it must be readable for all users".

A download now gets the mode its umask gives a new file, and the AppDir is
made a+rX before it is packed. tests/releaseDownloads.test.cjs fails on the
old fetch.sh with 711 instead of 755 and checks that a stricter umask is
still followed; tests/appImageRuntime.test.cjs pins both.

Thanks to xet7 !
2026-10-29 23:44:34 +02:00
Lauri Ojansivu 856a32968b Tests: board drag settings open Card's Draggable table in its section hamburger.
Since Board Settings / Card became the card's Card Settings (52de57c844),
the Card Draggable table is in the Card section's hamburger, not in the
popup, so board-drag-settings.e2e.js failed in every language waiting for a
table that is no longer there. It opens Card the way the cog menu does and
then that hamburger, as board-settings-columns.e2e.js already does.

Thanks to xet7 !
2026-10-29 23:42:36 +02:00
Lauri Ojansivu e4b83d2274 Tests: the keyboard card move waits for the reset sorts to reach the client.
F02 F06 reset three cards to sort 0 in the database and waited only for the
minicards to show in id order. gamma 0, alpha .125, beta .25 already is id
order one time in six, so the wait passed while the client still had the old
sorts, the move was computed from them, and the card landed last instead of
second (1 in 3 runs here). The test waits for the client's own copy of all
three cards to read sort 0; 8 repeats pass.

Thanks to xet7 !
2026-10-29 23:28:31 +02:00
Lauri Ojansivu d06b468629 Tests: ReplyBleed's mocha test counts the attempt in every ReplyBleed row.
ReplyBleed is one Problems row per source, and the webhook's source is the
client address, so the bounce and board-address tests leave rows of their
own in the test database, which persists between runs. The test's findOne
picked one of those (source "ses"), whose count the spoofed reply never
moves, and failed although the attempt was recorded under 127.0.0.1. It sums
the count over all ReplyBleed rows, the way emailBounces.tests.js already
does.

Thanks to xet7 !
2026-10-29 23:07:04 +02:00
Lauri Ojansivu 379058bc9d CHANGELOG: OpenSSL 4 backup errors, Bun assets on Python 3.9, snap build logs, macOS test fixes.
Thanks to xet7 !
2026-10-29 22:52:42 +02:00
Lauri Ojansivu 5534c8830c Tests: the macOS suites run inside the sandboxed editor too.
mac-smoke-app.py checked that a killed descendant was gone with ps, which a
sandboxed macOS app may not run; it waits for os.kill(pid, 0) to find the
process gone and uses ps only to tell a zombie from a live one. macosSandbox
skips its live sandbox-exec check only when even an (allow default) profile
cannot be applied, which is the case inside the sandbox it is testing.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu 48a8937ff0 Snap on Launchpad: this attempt's build log is found by name, not only by mtime.
Whether a Launchpad build log belonged to the current attempt was decided by
[ "$log" -nt "$attempt_marker" ] alone. bash 3.2 compares whole seconds, so a
log written in the marker's second was not "newer": it was neither printed
nor checked for an infrastructure failure. this_attempts_log() counts a log
whose name did not exist when the attempt started, or one rewritten after the
marker; an earlier attempt's untouched log still does not count. Used by
infra_failure() and by the log printing, and tested for all three cases.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu 2dcccf0d4a Bun release assets: runs on Python 3.9, which macOS ships as python3.
hashlib.file_digest is Python 3.11+ and Path.write_text(newline=) is 3.10+,
so releases/bun/assets.py failed with AttributeError/TypeError on macOS's
python3 3.9. It hashes in chunks and writes the LF-only .sha256sum through
open(newline='\n'), and the test still pins LF on every platform.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu bf266fb4da DB conformance: a live run is still recognised where ps is not allowed.
In a sandboxed macOS app ps fails with "operation not permitted", so every
live conformance run looked dead and a second run would remove its
containers. command_line_of() falls back to pgrep -lf, which still works
there. The test pins the fallback.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
Lauri Ojansivu ffe49dd2fd Continuous backup: a changed encrypted file is reported as changed, also with OpenSSL 4.
A Node.js linked to OpenSSL 4 reports an AES-GCM tag mismatch as
ERR_OSSL_BAD_DECRYPT ("bad decrypt") instead of OpenSSL 3's "Unsupported
state or unable to authenticate data", so decryptFile() let the raw OpenSSL
error through instead of "An encrypted backup file has changed or the key is
wrong". Both are recognised now; tests/continuousBackupEncryption.test.cjs
passes with OpenSSL 4.1.0 and 3.5.9.

Thanks to xet7 !
2026-10-29 22:51:29 +02:00
github-actions[bot] ee16bdce6b Bump versions for v12.69 2026-10-29 20:03:10 +00:00
Lauri Ojansivu 44bae91f8e Prepare v12.69 release 2026-10-29 22:02:00 +02:00
Lauri Ojansivu ae13eab23e SECURITY.md, CLAUDE.md, AGENTS.md: a report without a GHSA goes from 3 to 5 stars when its GHSA arrives.
A researcher who sent a report to security@wekan.fi, or some other way, and
is later able to add it as a GitHub security advisory: the maintainer
requests the CVE through that GHSA, and the Hall of Fame row's 3 stars
become 5. SECURITY.md says so to researchers. CLAUDE.md and AGENTS.md tell
an AI updating or auditing the Hall of Fame to check each such 3-star row
against the advisories, match by the report's subject and reporter, and
upgrade the index row, its own page, the CVE cell, the Process cell and the
star audit together.

Thanks to xet7 !
2026-10-29 21:52:15 +02:00
Lauri Ojansivu f5f6ad4d4d CHANGELOG: ImportPlacementBleed, and Release all missing builds after a failed rerun.
Thanks to Zachariah Mithani (AploSec) and xet7 !
2026-10-29 20:27:59 +02:00
Lauri Ojansivu 0d6d481fcb ImportPlacementBleed: an import into a board is placed only beside that board's own list, swimlane or card.
The scoped importer, reached by POST /api/import/zip and the importScoped
method, authorized the import on boardId alone. The listId and swimlaneId
beside it were written onto the new cards as given, and a cardId was looked
up with no board filter, so a writer of board A could create cards on A
whose list and swimlane were board B's, and could probe whether a card id
existed on a board they cannot see.

Before anything is written, the list, swimlane and card are now looked up
together with the board's id; an id of another board is refused (400 over
REST, error-import-placement from the method), with the same answer for a
foreign card and a missing one. Cards are placed only by what was found on
the board. A refusal is recorded in Admin Panel / Problems as
ImportPlacementBleed (authz.import-placement), without blocking the account.

Tests: the decision with the reporter's ids, the check before every write,
a negative scan for a target id written or looked up without the board, and
a browser test of the reporter's attack by both routes, which fails on the
previous code.

Thanks to Zachariah Mithani (AploSec) and xet7 !
2026-10-29 20:27:14 +02:00
Lauri Ojansivu 99d9958e39 Release all missing: the missing assets are built even when the rerun of failed jobs fails again.
The planner needed recover, so one job failing again in the rerun of the
full release skipped the planner and every build after it, and assets that
could be filled were not. For v12.68 the rerun failed on four Launchpad
snaps (503 / timeout) and on the OpenBSD bundle, whose start-wekan.sh fault
is in the tagged code and so in every rerun of it. The planner now runs
unless the run is cancelled, and each job after it checks the planner's own
result, since an implicit success() also counts recover. recover stays red
and now names the jobs that failed again.

Thanks to xet7 !
2026-10-29 20:15:32 +02:00
Lauri Ojansivu daae36b313 CHANGELOG: Invite via Email adds the invited new user, not the first user of the database.
Thanks to momosari and xet7 !
2026-10-29 17:36:01 +02:00
Lauri Ojansivu 8556459f97 Invite via Email adds the invited new user, not the first user of the database.
Meteor 3's server Accounts.createUser is createUserAsync. inviteUserToBoard
called it without await, so the new user's id was a Promise; findOneAsync
read the Promise as the empty selector {} and returned the FIRST user,
usually the site administrator. That user was added to the board and got
the invitee's language and invitedBoards, while the invitee was created but
not added. It was not FerretDB: any database did the same.

createUser and sendEnrollmentEmail are awaited (an enrollment email that
fails is logged, as the invitation email is), and the user found must be
the one just created. Every server ReactiveCache getter now refuses a
Promise as its id or selector, so a missing await fails loudly instead of
picking the first document. Tests: the guard and the reporter's case in
memory, a scan that no server file calls an async Accounts API without
await, and a browser test that inviting a new address adds exactly that new
account - it fails on the previous code.

Thanks to momosari and xet7 !

Fixes #6831.
2026-10-29 17:35:21 +02:00
Lauri Ojansivu 7f9e8ea7a3 CHANGELOG: History on FerretDB, date paths as strings, migration reports refused documents.
Thanks to vijlautens and xet7 !
2026-10-29 16:58:42 +02:00
Lauri Ojansivu 01968fc637 History on FerretDB: date paths are stored as strings, and the migration reports refused documents.
A History row of a date-valued custom field held datePaths as an array of
path arrays, [["3","value"]]. FerretDB v1 does not store an array inside an
array, so on FerretDB the change was not recorded, and migrateTextDatabase
to FerretDB dropped those rows while it swallowed the per-document error and
still reported success.

Each path is now a JSON string, '["3","value"]'. Rows already written in
the array form on MongoDB still restore, and the migration to FerretDB
rewrites them to strings. The migration counts every document the target
refuses and ends as failed with the count per collection and the first
error, instead of success: true.

Thanks to vijlautens and xet7 !

Fixes #6832.
2026-10-29 16:58:21 +02:00
Lauri Ojansivu 04c2598cf4 Merge remote-tracking branch 'origin/main' 2026-10-29 10:55:23 +02:00
Lauri Ojansivu 77ded27994 CHANGELOG: OpenBSD bundle starts again.
Thanks to xet7 !
2026-10-29 08:25:49 +02:00
Lauri Ojansivu ec6cf66bff OpenBSD bundle starts again: start-wekan.sh takes only a number from sysctl.
OpenBSD's sysctl prints nothing and exits 0 for a name it does not have, so
`sysctl -n hw.memsize || sysctl -n hw.physmem` stopped at the empty
hw.memsize and $(( / 1048576 )) ended start-wekan.sh before WeKan answered.
Both start-wekan.sh copies now try hw.memsize, hw.physmem64 (NetBSD, whose
hw.physmem is 32-bit) and hw.physmem in turn and take the first numeric one.
The test runs the lines against fake OpenBSD, NetBSD and macOS sysctls, and
shows the old chain failing on the OpenBSD one.

Thanks to xet7 !
2026-10-29 08:25:38 +02:00
github-actions[bot] 8561f49ed9 Bump versions for v12.68 2026-10-29 05:16:53 +00:00
Lauri Ojansivu 749e9b76a7 Prepare v12.68 release 2026-10-29 07:15:43 +02:00
Lauri Ojansivu ad43de3c01 CHANGELOG: Card Settings, two panes, the card and the minicard follow its order, menus inline.
Thanks to xet7 !
2026-10-29 02:03:11 +02:00
Lauri Ojansivu 2bd5d1180a Card Settings: two panes, the card and the minicard follow their order, menus inline.
The opened card and the minicard now draw Card Settings' sections, in
Card Settings' order, each section's fields in the order of its rows:
- Section order is per side: cardSettingsSectionOrder for the card,
  the new minicardSettingsSectionOrder for the minicard. A board that
  stores none: the card starts with Labels, Date, Members, Card, ...
  (the card's historical order as far as the grouping allows), the
  minicard with what it drew first before.
- CARD_LAYOUT's and MINICARD_LAYOUT's reorderable sections are Card
  Settings' sections, so dragging a section or a row in Card Settings
  moves it on the card or the minicard. Stored field orders map: a
  field keeps its place relative to the others of its new section.
- On the card every section has the shared heading (its icon, title,
  caret and hamburger) - Voting and Planning Poker too, which had a
  heading of their own. The minicard stays compact: no headings.

Card Settings shows both sides at once: the minicard's pane first (on
the left), the card's second, each with a caret that folds it, each
scrolling on its own, stacked when the popup is too narrow for two.
The Show radio is gone. A splitter between the panes changes their
widths by dragging, like spreadsheet columns (clamped, kept per user
as a fraction, reset by a double-click, moved by the arrow keys). The
popup is wider again for two panes and its corner grip resizes it,
the panes following.

A hamburger's content opens under its line inside the pane, not in a
popup: the hamburger shows it is open (aria-expanded, highlighted, the
panel attached to it); its button, the panel's X or Escape closes it.
A custom field's edit form and the + create form open inline the same
way and close only their panel when saved. The popup layer added in
9c9742e09d is removed, as nothing uses it now.

Docs: Card-Settings.md, Card-Fields.md and Board Settings / Card.md.
New tests cardSectionsOnCard, minicardSections, cardSettingsSplit,
cardSettingsInlinePanels and card-settings-sections-order.e2e.js; the
guards of the old layouts and orders say why they changed.

Thanks to xet7 !
2026-10-29 02:02:54 +02:00
Lauri Ojansivu eddfd515ea CHANGELOG: Card Settings, hamburger menus open on top of it, next to their button.
Thanks to xet7 !
2026-10-28 23:23:18 +02:00
Lauri Ojansivu 9c9742e09d Card Settings: hamburger menus open on top of it, next to their button.
A hamburger of Card Settings opened its menu in Card Settings' place:
the popup slid to the menu and Card Settings disappeared until its back
arrow. Its menus, and the forms it opens (a custom field's edit form,
the + under Custom Fields, Subtask settings), now open as a popup
LAYER: on top of Card Settings, next to the button that opened them,
with Card Settings visible under it and scrolled where it was.

Popup.open(name, { layer: true }), from inside a popup, pushes the
layer on the stack as before, so Popup.back() - the layer's X, Escape,
a form's Save - closes the layer alone. The popup template draws the
popup under it with the layer on top, outside the scrolling content;
depth counts the sliding popups only, so a confirmation opened from a
layer still slides. One layer at a time: another replaces it, its own
button closes it, a press elsewhere in the popup closes it.

A layer opens toward the middle of the popup: a hamburger at the end of
a line gets its right edge, the + on the left its left edge, so the
create form no longer ran out of the popup's left side where its Save
could not be clicked; below the button, or above when there is more
room, its height and width capped to the room
(models/lib/popupLayerPosition.js). A menu's checkboxes start at its
edge rather than .check-div's form indent.

tests/popupLayer.test.cjs runs the placement and pins the Popup wiring
and that every Card Settings menu opens as a layer;
card-add-fields.e2e.js checks Card Settings stays visible under the
menu, the menu sits by its button, toggles, closes on a press elsewhere
and by its X alone, and that the + form opens inside Card Settings.

Thanks to xet7 !
2026-10-28 23:23:08 +02:00
Lauri Ojansivu 0250e54a07 CHANGELOG: Card Settings, a checkbox per section, every field always shown, only in Board Settings.
Thanks to xet7 !
2026-10-28 22:44:02 +02:00
Lauri Ojansivu 3e5bc58105 Card Settings: a checkbox per section, always every field, only in Board Settings.
Card Settings is now only Board Settings / Card. The opened card has no
Card Settings button any more: it simply shows every field its board
shows on cards, empty or not.

- On this card, Show empty and Show empty fields are removed, with the
  hamburgers' Select all / Select none rows, their handlers, helpers,
  board methods (setCardFieldAlwaysShown, setShowEmptyCardFields), CSS
  and the three translation keys. The card no longer reads its own
  shownFields / hiddenFields, nor the board's cardFieldsAlwaysShown /
  showEmptyCardFields; the schema keeps them so old documents validate.
- Every section has a checkbox before its title: whether the whole
  section is shown on the chosen side, whatever its rows are ticked.
  Stored per side in cardSettingsHiddenOnCard / cardSettingsHiddenOnMinicard
  (setCardSettingsSectionShown, board admins). The opened card's view
  of the board and a new minicard view read false for every row of a
  hidden section, Scrum fields included; Card Settings itself reads the
  raw board, so the rows keep their ticks.
- A section with one row on the chosen side is one line: one checkbox,
  the name, one hamburger.
- A hamburger is drawn only when it holds something.
- Custom Fields: its items are the board's custom fields, in their sort
  order, reordered by drag and drop within the section, with a + under
  them that creates one. Show on Minicard sets a field's showOnCard,
  Show on Card its alwaysOnCard; its hamburger edits the field. The
  section's checkbox is the board's Custom Fields flag of that side.
- Card actions (Filter: Subtasks, Edit voting, Card kind, ...) live in
  the card's own section hamburgers; Card Settings keeps only board
  actions.

Docs: docs/Features/Cards/Card-Settings.md replaces
Card-Settings-On-Card.md; Card-Fields.md and Board Settings / Card.md
rewritten. New tests cardSettingsSectionShown and
cardSettingsCustomFields; the guards of the removed behaviour say why
they changed; card-add-fields.e2e.js covers section hiding per side, an
empty card showing every field, empty hamburgers, the one-row line, the
Custom Fields items and +, and that the opened card has no Card
Settings.

Thanks to xet7 !
2026-10-28 22:43:29 +02:00
Lauri Ojansivu 039935dd0b CHANGELOG: Card Settings, a space between a checkbox and its label.
Thanks to xet7 !
2026-10-28 20:46:50 +02:00
Lauri Ojansivu 5adfc1b9db Card Settings: a space between a checkbox and its label.
On the rows of Card Settings - on the opened card and in Board Settings /
Card - and in their hamburgers' popups, a checkbox touched its label:
the rows' grid had lost its column gap with the old pane's CSS, and a
hamburger's option put the box and the text side by side with none.
Both now have a space's width (0.3em) between them.

A ticked box still touched the label, because a tick is drawn rotated
and reaches about 3px past its box. Left to right it is drawn 3px
further left, so a ticked box keeps the same space and the label does
not move when it is ticked; right to left the tick leans away from the
label and stays as it was.

tests/cardSettingsOnCard.test.cjs pins the gaps and the tick rule, and
card-add-fields.e2e.js measures at least 3px for ticked and unticked
boxes on the rows and in Card's hamburger, with every label in the same
place.

Thanks to xet7 !
2026-10-28 20:46:42 +02:00
Lauri Ojansivu f269b1ef16 CHANGELOG: Board Settings / Card shows the same checklists as the opened card, by the same templates.
Thanks to xet7 !
2026-10-28 20:23:42 +02:00
Lauri Ojansivu 52de57c844 Board Settings / Card is the opened card's Card Settings, same templates.
Board Settings / Card had its own layout: an Admin Panel left menu of
sections and a pane with two lists, column headings and per-column Select
all. It now renders the same template as the opened card's Card Settings
(cardSettingsInline), with no card: Show on Minicard / Show on Card, the
sections as checklists, their rows as the items, and the same section and
row hamburgers. The popup's header is the title and the close button, so
the inline's own title and X are not drawn there, and a row's hamburger
has no On this card. The popup is 600px wide, about an opened card,
instead of 1200px.

The popup no longer registers the Card Settings handlers itself: the
template inside it does, and registering them on both would handle every
click twice. The left menu, the pane, the column Select all, the
in-place action forms and the personal-only mode are removed with their
helpers and CSS. A non-admin sees what a card shows them: the board's
rows disabled, their own rows (Labels text, List title) usable.

Dragging, on the card and in Board Settings / Card:
- With drag handles on, a row or a section is dragged only by its
  handle; the rest of the line scrolls the page. With them off, by its
  icon and name.
- A row dropped on another section goes back where it was and nothing
  is saved: the sections are unrelated kinds of fields. jQuery UI fires
  a delayed "out" just before "stop", so where it was dropped is read
  in beforeStop.

Found while testing both open at once:
- Each Card Settings has its own Show radio group name; one name made
  the card's and the popup's radios one group.
- A section hamburger's Select all acts on the Card Settings it was
  opened from.
- Rows are keyed, so switching Show moves each row with its side instead
  of relabelling rows in place a flush before they reorder, when a
  click could land on the neighbouring field.

Docs: Card-Settings-On-Card.md, Card-Fields.md and Board Settings /
Card.md. The node guards that pinned the old layout now pin the shared
template, with negatives that the old layout and the double handler
registration are gone; card-add-fields.e2e.js compares the two views
section by section and row by row on both sides, and tests the drag
rules, and the specs that drove the old pane drive the new one.

Thanks to xet7 !
2026-10-28 20:23:05 +02:00
Lauri Ojansivu 81e6988491 Southern Sotho: "dikarolo", not "masimo", for data fields.
Three Southern Sotho strings called a data field "masimo", which means
farm fields: Show empty fields in Card Settings, the five fields of a
cron schedule, and the webhook's optional fields. They now use
"dikarolo", the word the file already uses for card and custom fields.
sesotho-popup-titles.e2e.js, which checks that Card Settings names data
fields as such, now reads Card's "Show empty fields" in Board Settings /
Card and caught the first. The cron and webhook wording is low
confidence; a Southern Sotho speaker's correction on Transifex replaces
it.

Thanks to xet7 !
2026-10-28 20:22:56 +02:00
Lauri Ojansivu 8b4987d5bd CHANGELOG: the Data health entry no longer repeats its group's name.
Thanks to xet7 !
2026-10-28 18:50:35 +02:00
Lauri Ojansivu e12106ce7a CHANGELOG: Meteor 3.6-rc.1, with WeKan's six Meteor package forks rebased onto it.
Thanks to xet7 !
2026-10-28 18:50:00 +02:00
Lauri Ojansivu 120f484a30 Meteor 3.6-rc.1, with WeKan's six Meteor package forks rebased onto it.
meteor update --release 3.6-rc.1 failed with "No version of
shell-server satisfies all constraints" (and the same for
dynamic-import, allow-deny, ddp-server, facebook-oauth and
google-oauth): those six are WeKan's forks in packages/, made because
Meteor 3.6-rc.0 lacked the 3.5.3 security fixes, and pinned to rc.0's
versions.

Meteor 3.6-rc.1 carries the security fixes of 3.5.3 and 3.5.4, but not
what is WeKan's own in the forks: the Admin Panel / Problems reports of
refused attempts, and the crypto-RNG keys of dynamic-import and meteor
shell (rc.1 still makes them with Math.random()). So the forks are kept
and rebased: each is rc.1's package with WeKan's changes over rc.0's
package merged three-way onto it. The 3.5.3 hunks merged away, as rc.1
has them; what remains over rc.1 is only the hooks and the keys. The
versions are rc.1's with a _1 wrap number.

@meteorjs/rspack moves to 3.0.0-rc.2 with the release. The Dockerfile's
METEOR_RELEASE and the snap description follow. The experimental Bun
build's Meteor patch applies unchanged to 3.6-rc.1; prepare-meteor.sh
now checks for rc.1's tag commit and applies meteor-3.6-rc.1.patch, and
the rc.0 copy is removed.

tests/meteor353SecurityFixes.test.cjs and meteorForkRandomKeys.test.cjs
are moved to rc.1 with their assertions kept;
meteor-353-security.e2e.js passes on rc.1 in Chromium and Firefox.

Thanks to xet7 !
2026-10-28 18:49:59 +02:00
Lauri Ojansivu 08ff3327c4 Card fields guard: the Scrum section filter asks the board first.
tests/cardShownFields.test.cjs still pinned the Scrum filter of the
opened card's sections as it was before #6830, which now requires the
board's Scrum visibility before asking the card.

Thanks to jullbo and xet7 !
2026-10-28 18:49:18 +02:00
Lauri Ojansivu d74dbbe409 CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.

Thanks to xet7 !
2026-10-28 18:43:49 +02:00
Lauri Ojansivu 2fd0c77164 Merge remote-tracking branch 'origin/main' 2026-10-28 18:43:48 +02:00
Lauri Ojansivu 6e18251e5f CHANGELOG: Show empty fields shows no Scrum section the board has turned off (#6830).
Thanks to jullbo and xet7 !
2026-10-28 18:28:57 +02:00
Lauri Ojansivu 0a224b3cea Show empty fields shows no Scrum section the board has turned off.
With Board Settings / Card's "Show empty fields" on, every opened card
showed the "Scrum settings" heading and its sections although Board
Settings / Scrum had none of its fields turned on. The Scrum fields are
the card's only sections with no board flag in the template, and
orderedSectionsOf asked only whether the card shows the field, which
Show empty fields answers yes for every field. A Scrum section now
needs the board's Scrum visibility (boardAllows) first, as Members,
Vote and Poker already do.

Thanks to jullbo and xet7 !

Fixes #6830.
2026-10-28 18:28:31 +02:00
Lauri Ojansivu a834211ab6 CHANGELOG: Card Settings on the opened card, its sections as checklists.
Thanks to xet7 !
2026-10-28 14:39:48 +02:00
Lauri Ojansivu c0ec28e82f Card Settings on the opened card, its sections as checklists.
The opened card's "+ Add" button is now "Card Settings". Clicking it no
longer opens the Board Settings / Card popup: the button and everything
below it on the card are replaced by Card Settings itself, until the X at
the end of its title puts the card back. Top to bottom:

- Card Settings, with its X (close) at the right of the title.
- "Show" and two radio buttons, Show on Minicard and Show on Card, which
  choose what the checkboxes below are.
- The sections (Card, Date, Time, ... Comments) drawn like checklists: the
  section is the checklist's title, with a count of the ticked rows and a
  hamburger; its rows are the checklist items. An item's checkbox is the
  board-wide Show on Card or Show on Minicard of that field.
- A row's hamburger holds On this card and Show empty; a section's holds
  Select all / Select none of each column, the section's card actions and,
  for Card, Show empty fields, Ask for a reason and the Draggable table.

Sections and rows reorder by drag and drop like checklists and their items,
and by the Up and Down keys. A section's order is the board's new
cardSettingsSectionOrder (normalised by sectionOrder() in
models/lib/cardSettingsSections.js); a row's is the card's or minicard's
field order, as in the popup. Sections collapse like checklists, kept per
user and board. The rows, checkboxes and handlers are the popup's, shared
rather than copied, and Board Settings / Card still opens the popup.

Design: docs/Features/Cards/Card-Settings-On-Card.md.

New strings: "Show" (card-settings-show) and the two hamburgers' titles,
"Actions". Every language takes its existing translation of the same
words; "Show" was written directly, with low confidence, for an, ary, bi,
csb, cy, cy-GB, fur, hsb, hy, jv, ka, kk, km, km-KH, km_KH, kok, lb, lld,
mai, mn, oc, or_IN, pa, sc, sn, so, szl, te-IN, tg, tk_TM, tl, tpi and tt.

Tests: tests/cardSettingsOnCard.test.cjs (the order rules, the layout, the
shared handlers, the translations) and
tests/playwright/specs/card-add-fields.e2e.js rewritten for the card's Card
Settings, the popup's own tests now opening it from Board Settings. The
guards that pinned the card template's indentation and the old opener say
why they changed.

Thanks to xet7 !
2026-10-28 14:39:29 +02:00
Lauri Ojansivu 6dcb9fa6e2 CHANGELOG: Data health cleans up custom fields whose boards were all deleted (#6829).
Thanks to Raymond-Voogt and xet7 !
2026-10-28 13:51:17 +02:00
Lauri Ojansivu 9dc3fc3f31 Data health: custom fields whose boards were all deleted get their own check.
customfield-dead-board counted every custom field that names a deleted
board, but its Repair only removes dead boards from a field that is also on
a live board; a field whose boards were ALL deleted was skipped on purpose,
since removing them would leave it on no board. So those fields stayed in
the count with a Repair button that could never change it: one instance
showed 510 and "Repaired: 0" after every click.

- New check customfield-boards-deleted: fields whose boards no longer exist
  (through boardIds or the legacy boardId). No existing board shows them.
  Repair deletes the definitions after the confirming second click, like
  the other repairs that delete data. A value a card on a live board still
  holds is left alone and stays listed under card-dead-field-values.
- customfield-dead-board counts only fields that still have a live board,
  so its count goes to zero when its Repair has run.

docs/Features/Admin-Panel/Problems/Data-Health.md lists the new check.
Test: tests/dataHealth6818.test.cjs.

Thanks to Raymond-Voogt and xet7 !

Fixes #6829.
2026-10-28 13:50:49 +02:00
github-actions[bot] e0eeae82d6 Bump versions for v12.67 2026-10-28 03:56:20 +00:00
Lauri Ojansivu b349576a15 Prepare v12.67 release 2026-10-28 05:55:24 +02:00
Lauri Ojansivu d519091909 Merge remote-tracking branch 'origin/main' 2026-10-28 05:13:33 +02:00
Lauri Ojansivu 55d64e6493 CHANGELOG: WeKan vs Trello vs Jira dates.
Thanks to xet7 !
2026-10-28 05:10:26 +02:00
Lauri Ojansivu cbdff47edb Docs: WeKan vs Trello vs Jira says when WeKan added the features it listed as missing.
The page listed Trello's Email Inbox, Complete Card Checkbox, Workspaces,
Butler scheduled and repeating tasks and Card Repeater as not yet in WeKan,
and Sync Lists and Map Card as WeKan ideas not yet implemented. Each now has
the WeKan release that added it: Workspaces v8.17 (2025-11-06), the complete
checkbox and card location v9.42 (2026-06-13), Scheduled Rules and recurring
cards v9.46 (2026-06-15), list sync v11.69 (2026-09-11), the Map view v12.10
(2026-09-29) and Email to card v12.40 (2026-10-14). The links to the 2018 and
2021 releases point to old-CHANGELOG/, where those sections are now.

Thanks to xet7 !
2026-10-28 05:10:21 +02:00
Lauri Ojansivu b36092896b CHANGELOG: the Add card form closes after a card (#6827), and the Bun guard's mongo:9.0.4.
Thanks to brlin-tw and xet7 !
2026-10-28 04:47:19 +02:00
Lauri Ojansivu 70de255333 Add card: the form closes after the card is added, unless Member Settings keeps it open.
A list's Add card form stayed open after a card was added, for the next
card, which the reporter found unexpected. It now closes once the card - or
every card of a multi-line paste - is added. Member Settings has a new
switch, "Keep the Add card form open after adding a card", off by default,
for who types several cards in a row. The close happens after the last
title, not at submit, because addCard still reads the form between the
titles of a paste; a failed add keeps the form and its text.

The two new strings are translated into every locale. Low confidence: ace,
ak, am, an, ary, as, ast-ES, ay, ba, bho, bi, bm, bo, br, bua, chr, ckb, co,
csb, cv, dz, ee, ff, fj, fo, fur, gn, gv, ha, haw, hsb, ig, iu, kl, kok, ks,
kw, lb, lg, lld, mai, mg, nah, nap, nd, nso, ny, om, or_IN, pap, ps, qu, rn,
rup, rw, sah, sc, scn, sd, se, sm, sn, so, ss, st, szl, ti, tig, tlh, tn,
to, tpi, ts, uz-AR, ve, ve-CC, ve-PP, vo, wa, wa-RR, wal, wo, wuu-Hans, zgh.

tests/keepAddCardFormOpen6827.test.cjs pins the switch, its default and
where the form is closed, with negative cases for a paste and a failure.
tests/playwright/specs/keep-add-card-form-open-6827.e2e.js drives it: closed
after one card and after a paste, kept open with an empty title once the
switch is on. multiline-titles opens the form again for its second card, and
assist-due-from-title keeps it open to check the next card's form has no
leftover hint.

Thanks to brlin-tw and xet7 !

Fixes #6827,
2026-10-28 04:46:52 +02:00
Lauri Ojansivu bce686ce46 Tests: the Bun smoke-test guard pins mongo:9.0.4, not the mongo:8 that stopped starting.
tests/bunSmokeMongo.test.cjs required the workflow's docker run to say
mongo:8, the floating tag that refused to start on the runner's 6.19+ kernel
in v12.66. It now requires mongo:9.0.4, and fails if mongo:8 comes back.

Thanks to xet7 !
2026-10-28 04:42:42 +02:00
Lauri Ojansivu facfc125ee CHANGELOG: the v12.66 release build is whole again.
Thanks to xet7 !
2026-10-28 04:31:25 +02:00
Lauri Ojansivu 1167cbd3b0 Bun bundles: the smoke-test MongoDB starts on a 6.19+ kernel, and Chromium is found.
linux-x64: mongo:8 refused to start on the runner's kernel ("Linux kernel
versions 6.19 and newer has a known incompatibility"). The smoke test pins
mongo:9.0.4, which recognises Ubuntu kernels that have the fix
(docs/Databases/Supported.md).

linux-arm64: Chromium was installed into ~/.cache/ms-playwright, while
build-target.sh loads tools-env.sh, which points Playwright at
.tools/ms-playwright, so the browser smoke test found no browser. The install
step uses the same path.

Thanks to xet7 !
2026-10-28 04:30:46 +02:00
Lauri Ojansivu d24598ce25 Snap variants: a copy of the source tree without .git gets its own .tools/tmp.
tools-env.sh made .tools/tmp only in a git checkout or where .tools already
existed, so that scripts mounted alone into a container (/releases) do not
make a /.tools there. The wekan-ondra and wekan-gantt-gpl snaps build from a
copy of the tree without .git (/root/project), so tools-env.sh found no
TMPDIR, and every fetch.sh in snapcraft.yaml stopped at "TMPDIR: unbound
variable" - reported as the Caddy download failing (v12.66). A tree with
package.json beside releases/tools-env.sh is a source tree too.

tests/toolsDirOnly.test.cjs sources it under set -euo pipefail with no
TMPDIR in such a copy, and, as the negative case, in releases/ alone, where
no .tools/ is made and no TMPDIR is invented.

Thanks to xet7 !
2026-10-28 04:30:46 +02:00
Lauri Ojansivu f68b130f15 Sandstorm: meteor-spk is assembled where the .spk is packed from, in .tools.
sandstorm-src/build-deps.sh still built meteor-spk.deps in
~/projects/meteor-spk, and the workflows put that on PATH, while
sandstorm-slim-spk.sh had moved to .tools/projects/meteor-spk with
aeff4900df: "no meteor-spk.deps at .tools/projects/..." and the v12.66 .spk
did not pack. build-deps.sh sources tools-env.sh and defaults to
WEKAN_METEOR_SPK_DIR, and the Sandstorm, Sandstorm store and meteor-spk
workflows use $GITHUB_WORKSPACE/.tools/projects/meteor-spk.

tests/toolsDirOnly.test.cjs scans sandstorm-src/ too, and fails on a workflow
that names ~/projects.

Thanks to xet7 !
2026-10-28 04:30:46 +02:00
Lauri Ojansivu 290e525b46 start-wekan.sh starts again where there is no /proc: FreeBSD, OpenBSD and macOS.
The memory budget read _memory_mb=$(awk ... /proc/meminfo) with no fallback
since 1f9ada5599. awk fails where there is no /proc, and under set -eu an
assignment from a failed substitution ends the script without a word, so the
FreeBSD and OpenBSD bundles and the Mac app never started WeKan (v12.66).
It is `|| true` again, and sysctl then gives the memory.

tests/macosShellPortability.test.cjs runs the memory lines under set -eu
without /proc and requires a number, with the unguarded line as the negative
case, and fails on any set -e script with a /proc read in $(...) that has no
fallback.

Thanks to xet7 !
2026-10-28 04:30:46 +02:00
Lauri Ojansivu 427d212627 Release: the emulated-arch builds mount .tools/tmp as the container's /tmp.
install-node-for-arch.sh refuses to run without TMPDIR, because the
container's own /tmp is a small part of its writable layer.
repack-bundle-for-arch.sh mounts this checkout's .tools/tmp as /tmp and sets
TMPDIR; the workflow's own docker run did not, so armhf, armv6, armv7, i386,
ppc64le, riscv64 and s390x all stopped at "TMPDIR must be the mounted
.tools/tmp" in v12.66, and win32, which builds from the i386 bundle's
Node.js, had nothing to build with.

Thanks to xet7 !
2026-10-28 04:30:46 +02:00
github-actions[bot] c1fe35bf6c Bump versions for v12.66 2026-10-28 01:02:24 +00:00
Lauri Ojansivu 0dee1da686 Prepare v12.66 release 2026-10-28 03:01:09 +02:00
Lauri Ojansivu 8ff34b423b CHANGELOG: the Drawing popup's width, File status's list of findings, and three Firefox specs.
Thanks to xet7 !
2026-10-28 02:57:47 +02:00
Lauri Ojansivu 0c40503a2b Tests: three Firefox specs wait for what they test, and drag inside the window.
- assist-search-phrases: the feature is turned off in the database, and a
  search sent before the browser had the published setting still ran the
  phrase. The search is repeated until the browser has it; a phrase that is
  never turned off still fails.
- board-settings-columns: a non-admin member's count of 0 passed in Chromium
  before the pane had rendered, and in Firefox counted Show on Card's
  Select all / Select none, which share the class and are a member's like the
  checkboxes under them. It waits for the pane and counts only the board-admin
  column actions.
- card-drawing-canvas: a maximized drawing fills the window, so its corner
  handle is at x 1236 of 1280 and a drag 120px further was not delivered by
  Firefox. The drawing is zoomed out to 80% first, which leaves room.

Thanks to xet7 !
2026-10-28 02:57:29 +02:00
Lauri Ojansivu 35ba67ee69 File status: one kind of finding no longer fills the whole list and hides the others.
The report lists at most 500 findings and counts the rest. They were listed
first come, first served, so on the test server 14358 baseline-move-candidate
rows of the integrity baseline took all 500, and the extension-mismatch and
mime-mismatch a type check is run to find were only numbers in the totals -
the Firefox stage's file-status-audit.e2e.js failed on it, and an
administrator would not have seen which file it was.

When the list is full, a kind with fewer rows listed now takes the place of
the last row of the kind with the most, so every kind found is listed and the
totals still count all of them. fileStatusAudit.test.cjs: 8 rows are 8
kinds with 40 orphan files crowding them, every finding is listed or counted
as omitted, and with room for all nothing is dropped.

Thanks to xet7 !
2026-10-28 02:57:29 +02:00
Lauri Ojansivu 58751aa339 Drawing: the popup is as wide as its drawing, so opened from the card's menu nothing is cut off.
The previous fix assumed the Drawing popup grew to its drawing's width and
ran off the window. Measured in the rebuilt app, it did not grow at all:
every popup is min(380px, 55vw) wide, the drawing's min(820px, 90vw) overflowed
inside it and the popup clipped it - the larger pen sizes and Save were cut
off by the popup, not by the window. Its box never changed, so the
ResizeObserver that keeps a movable popup inside had nothing to react to.

popup.css gives the Drawing popup min(860px, calc(100vw - 20px)), not
!important so that maximized `width: auto !important` still wins. Opened from
the card's menu at the right of a 1280px window it is now 860px wide at
x 408, 12px from the edge.

popup-drawing-window.e2e.js waits for the drawing and a box that stays put
before measuring: the Drawing popup is the card menu's popup element renamed,
which for a moment still had the 960px menu's place, and a drag pressed at
that place selected the page's text and closed the popup. The first drag
goes toward the side that has room.

Thanks to xet7 !
2026-10-28 02:57:21 +02:00
Lauri Ojansivu 2844fa07f7 CHANGELOG: autofocus, the drawing's size and place, the storage settings' English, the Helm chart reference, and four specs.
Thanks to xet7 !
2026-10-28 00:32:41 +02:00
Lauri Ojansivu bb614743fd Drawing: maximized it takes the window, and opened at the right it is not cut off.
Two faults the Chromium stage found, measured in the browser:

- A maximized new drawing was sized to its stage (1212 x 563 in a
  1280 x 720 window) but drawn 784px wide. popup.css still had the rule of
  the older design that kept the drawing at 800 x 500's shape -
  width min(100%, (100vh - 230px) * 1.6), aspect-ratio 8 / 5 - which
  overrode the surface's own size. d5d027a6f2 replaced that design; the
  rule is removed. (2929b61c85's refit was right, and is kept.)
- Opened from the card's menu at the right of the window, the Drawing
  popup grows to min(820px, 90vw) in the same popup the narrow menu was
  placed for, and its right part - the larger pen sizes and Save - was
  cut off by the window until it was dragged. A movable popup that grows
  is now kept inside the window by the rule a drag keeps it there, 12px
  from each edge (client/lib/popupBounds.js); not while maximized.

tests/popupBounds.test.cjs drives the bounds and fails on the 8 x 5 rule
coming back; popupMoveMaximize and popup-drawing-window.e2e.js pin the
drawing taking the window when maximized instead of its old shape.

Thanks to xet7 !
2026-10-28 00:31:58 +02:00
Lauri Ojansivu 9549388dd0 Tests: the smaller font preset is measured on the Card Settings heading text, not its line.
c8a15b514f moved the headings' smaller font from the heading line to its
text (.card-settings-pane-head > span, sidebar.css): on the line, the
em-sized columns shrank and each heading drifted off its cells. The
clean-task-boards spec still measured the line, which inherits the
body's 11.2px, and failed in Chromium; it measures the text, 9.6px. All
four clean-board specs pass.

Thanks to xet7 !
2026-10-28 00:28:35 +02:00
Lauri Ojansivu 9c0f408a00 Autofocus no longer takes the keyboard from a field being typed in, nor replaces its text.
client/lib/autofocus.js focused an [autofocus] element whenever a re-render
added one: the LAST [autofocus] anywhere in the document, selecting its
text, 50 ms later - and decided before those 50 ms whether it was already
focused. On the import page, typing the attachment token after pasting a
Jira export moved the focus to the pasted JSON, all of it selected, and the
token replaced it: the import then said "Your text is not valid JSON"
(import-attachment-auth.e2e.js in Chromium). A focus() then select() on the
textarea, recorded in the browser and found at autofocus.js in the running
bundle, showed it.

Now only an [autofocus] element that was itself just added is a candidate,
and whether to focus it is decided when the focus happens: never while
the user is typing in another field (an input, textarea, select or
contenteditable). Opening a popup or a form still focuses its field -
focus is then on the button that opened it, or on a composer field the
re-render removed. client/lib/autofocusDecision.js is the decision;
tests/autofocusDecision.test.cjs drives it with stand-in elements.

Thanks to xet7 !
2026-10-28 00:27:24 +02:00
Lauri Ojansivu 3f72fcd7f9 Tests: the Move and Copy Card helpers wait for the destination picker before reading it.
CardPage._selectBoard asked whether the popup had a boards select with
count(), which answers at once. Asked while the popup was still rendering,
it said 0 - which the helper reads as "a worker, who has no boards
select" - so the move went to the named list of the SAME board, and
#6793's Move Card spec waited in vain for the card's new boardId. A trace
showed it: one select, and that for the list. The helper now waits for
the lists select, which every picker draws, before asking about the
boards one. The #6793 specs and 03-cards-operations (29 tests) pass in
Chromium.

Thanks to xet7 !
2026-10-28 00:21:29 +02:00
Lauri Ojansivu 3e998651ea Admin Panel / Attachments: a storage setting's English text is not shown twice.
Each storage setting (S3, Azure, Google Cloud, WebDAV ...) shows its
description and menu path in English and in the user's language, so the
product's own terms stay recognizable. The English line was typed into
the template a second time above the {{_ key}} line: in English, and in
every language without that translation yet, the same sentence stood
twice in a row - which is also why Playwright's strict getByText found
two and storage-settings-translations.e2e.js failed in Chromium.

The English now comes from en.i18n.json (englishText; all 26 typed copies
were identical to it, so no text changes), and the translated line is
shown only when it differs (translatedDiffers), reactively with the
language. tests/attachmentSettingsBilingual.test.cjs fails on a typed
English twin coming back and on a translation shown unconditionally.

Thanks to xet7 !
2026-10-28 00:15:32 +02:00
Lauri Ojansivu cbf9d2f843 Tests: the UploadBleed and StampBleed specs do the legitimate case first, and check the block.
Both failed in Chromium on their negative case: an upload onto one's own
card answered 403 "Please sign in to upload.", and a checklist without a
foreign stamp "Access denied". The guards were right. The attempt came
first, and a signed-in account that attempts a vulnerability is blocked
(server/lib/blockOnSecurityEvent.js): loginDisabled, and the page's
session ended - so the legitimate call that followed ran signed out.
Printing Meteor.userId() around the first call showed it.

Each spec now does the legitimate case first and the attempt last, and
checks that the attempt blocked the account (services.securityBlock names
UploadBleed or StampBleed). The stamped checklist comes before the
stamped upload: its refusal names no account, so the user is still
signed in for the upload. Both pass in Chromium.

Thanks to xet7 !
2026-10-28 00:11:35 +02:00
Lauri Ojansivu 9994e82bae Helm chart: values.yaml's settings reference is generated from docker-compose.yml.
The chart's commented reference of every WeKan setting was copied from
docker-compose.yml by hand once and had fallen 74 settings behind, which
tests/chartsFerretdb.test.cjs reported as soon as .tools/charts was there.
releases/charts-values-reference.mjs regenerates the block from the wekan
service's environment, and release-charts.sh runs it on every chart
release. A comment after a value goes on the line above, not into the
value.

The chart change itself, and the 512 MiB request / 2 GiB limit #6606 had
asked of it, are committed in wekan/charts. All 19 chart tests pass.
tests/chartsValuesReference.test.cjs runs the generator on a temporary
values.yaml: every compose setting written, nothing outside the block
touched, no comment inside a value, and a second run changes nothing.

Thanks to xet7 !
2026-10-28 00:06:39 +02:00
Lauri Ojansivu e13a5a59ad CHANGELOG: .tools/ only, the macOS and Windows editor sandboxes, and the mirror source.
Thanks to xet7 !
2026-10-28 00:04:07 +02:00
Lauri Ojansivu 6ac70c4cdd Windows sandbox: VSCodium in Windows Sandbox, seeing only Downloads\repos\wekan.
The Windows counterpart of the Linux Flatpak sandbox and the macOS
sandbox-exec one. Windows Sandbox is a disposable Hyper-V virtual machine
that comes with Windows 10 and 11 Pro, Enterprise and Education; it sees
nothing of the host but the folders mapped into it.

- docs/Security/Sandboxes/Windows/sandbox.ps1 (on the host; sandbox.bat
  starts it) offers to turn the Windows Sandbox feature on, copies
  user.name and user.email into the checkout's .git\config, and writes
  .tools\sandbox\wekan-vscodium.wsb: the checkout as the only mapped
  folder, read-write at the same path under the sandbox's user; network
  on; microphone, camera and printers off; ProtectedClient; half of the
  host's memory, 4 to 16 GiB.
- sandbox-logon.ps1 runs inside at logon: TEMP, TMP and TMPDIR in
  .tools\tmp, git's configuration in .tools\home, PortableGit and the
  VSCodium zip downloaded once into .tools - each verified (VSCodium's
  .sha256, the SHA-256 in Git for Windows' release notes) before it is
  unpacked - and VSCodium opened on the checkout, its settings and
  extensions in .tools\vscodium.
- README.md is the design: what is refused and allowed, how, and the
  trade-offs.

Not run on Windows yet: written and reviewed on macOS, where PowerShell is
not available. tests/windowsSandbox.test.cjs reads the scripts and pins
the single mapping, the .tools locations, verification before unpacking,
and that native commands cannot stop Windows PowerShell 5.1.

Thanks to xet7 !
2026-10-28 00:03:42 +02:00
Lauri Ojansivu 42ed50413f macOS sandbox: VSCodium under sandbox-exec, reaching only ~/Documents/repos/wekan.
The macOS counterpart of the Linux Flatpak sandbox
(docs/Security/Sandboxes/vscodium). docs/Security/Sandboxes/macOS/sandbox.sh
installs VSCodium with Homebrew when it is missing, writes a Seatbelt
profile for this checkout's path to .tools/sandbox/vscodium-macos.sb, a
Finder launcher beside it, and starts VSCodium under sandbox-exec:

- every file under /Users and /Volumes is refused - the home directory,
  ~/.ssh, ~/.gitconfig, other users and /Users/Shared, other disks - except
  the checkout, and the login session's ssh-agent socket is refused;
- HOME is .tools/home, TMPDIR .tools/tmp, VSCodium's settings and
  extensions are in .tools/vscodium, and env -i drops SSH_AUTH_SOCK and an
  editor's ELECTRON_RUN_AS_NODE;
- --no-sandbox, because Chromium's own helper sandbox cannot start inside
  sandbox-exec ("Operation not permitted"); every helper is inside this
  one instead;
- user.name and user.email are copied into the checkout's .git/config,
  since ~/.gitconfig cannot be read inside.

Verified on macOS 27 with --check (home, ~/.ssh, ~/.gitconfig and
/Users/Shared refused; the checkout, git and the network allowed) and by
starting VSCodium in it: the window, its helpers and the extension host
ran. README.md is the design: what is protected, how, and the trade-offs.
tests/macosSandbox.test.cjs pins the rules and, on macOS, runs a profile
against a shell.

Thanks to xet7 !
2026-10-28 00:00:20 +02:00
Lauri Ojansivu 8af1e8aec8 mirror.sh: the WeKan checkout is the source, not a second clone in .tools/wekan-github.
Syncing wekan/wekan cloned the whole GitHub repository - 365,000 objects,
500 MB - into .tools/wekan-github, although it is this checkout
(~/repos/wekan on Linux, ~/Documents/repos/wekan on macOS,
Downloads\repos\wekan on Windows). Only the destination mirrors
(wekan-gitlab, wekan-codeberg ...) need checkouts of their own.

The source's branches and tags are fetched into this checkout instead,
under the private namespace refs/mirror-source/<source>/, which already
holds nearly every object: 1.7 seconds for 1,278 refs, measured. That
namespace is not a branch; the working tree and local main are untouched,
so unpushed local commits never reach a mirror. The fetched refs are
deleted again in a finally, after the push and also when the fetch, the
push or the merge-preserving retry fails.

tests/activeForgeMirror.test.cjs and tests/mirrorGitDirectories.test.cjs
run syncGit against a stand-in checkout (WEKAN_MIRROR_SOURCE_CHECKOUT for
the CLI), assert that GitHub's wekan is not cloned, that only the
destination is, and that the source refs are cleared - also after a failed
fetch.

Thanks to xet7 !
2026-10-27 23:59:26 +02:00
Lauri Ojansivu e79ec61bcc Windows: build.bat and the releases\ batch files keep every file in .tools\ too.
releases\tools-env.bat is the counterpart of releases/tools-env.sh: TEMP,
TMP and TMPDIR in .tools\tmp, npm's cache and global installs, Go's caches
and the Playwright browsers in .tools\, METEOR_WAREHOUSE_DIR at
.tools\.meteor once Meteor is there, and those tools first on PATH, so no
user or system PATH setting has to change. build.bat and every
releases\ batch file call it; the bundle launchers (start-wekan.bat) do
not, as they run a finished bundle where there is no .tools\.

build.bat's install option no longer installs a system-wide Node.js with
Chocolatey: it unpacks the Dockerfile's version from nodejs.org's zip
into .tools\node-v<version>-win-<arch>, verified against SHASUMS256.txt
first, and Meteor goes into .tools\.meteor (LOCALAPPDATA is .tools for the
installer only). build-bundle-win64.bat works in .tools\tmp\win64-bundle-
<version> and says where the zip is. tools-env.sh also keeps Go's caches
and the Playwright browsers in .tools/, as tools-env.bat does.

tests/toolsDirOnly.test.cjs pins tools-env.bat, its callers and the .tools
install; tests/buildScriptParity.test.cjs reads the Playwright directory
and Node.js version from their new places.

Thanks to xet7 !
2026-10-27 23:53:23 +02:00
Lauri Ojansivu aeff4900df Every file the scripts write stays in .tools/: temporary files, downloads, Meteor, Node.js, npm.
build.sh and the releases/ scripts wrote all over the machine: temporary
files to /tmp and macOS's /var/folders, downloads and release zips to
~/Downloads or ~/Lataukset, Meteor to ~/.meteor, Node.js to /usr/local
(sudo n) or ~/.nvm, the npm cache to ~/.npm, Volta to ~/.volta, the
Sandstorm toolchain to ~/projects, and PATH lines into ~/.zshrc and
~/.bashrc. A sandboxed editor (Flatpak on Linux, sandbox-exec on macOS)
can reach only the checkout, and a container's own /tmp is a few hundred
megabytes. Everything is in .tools/ now, which git and Meteor ignore:

- releases/tools-env.sh, sourced by build.sh and every script that writes
  files, sets TMPDIR/TMP/TEMP to .tools/tmp (also downloads and release
  zips), NVM_DIR, npm_config_cache, VOLTA_HOME and the meteor-spk
  directory to .tools/, METEOR_WAREHOUSE_DIR to .tools/.meteor once Meteor
  is installed there, and puts those tools first on PATH, so no shell rc
  file is edited. HOME is not moved: git needs ~/.gitconfig and ~/.ssh.
  A TMPDIR already inside a .tools/ is kept, so a test's own directory
  there stays its own; without a writable one it stops rather than fall
  back to /tmp. It never fails a caller running set -euo pipefail.
- macOS's mktemp ignores TMPDIR when given no template, so every mktemp
  names one: mktemp "$TMPDIR/name.XXXXXX". Fixed /tmp files are in
  $TMPDIR.
- build.sh installs Node.js on Linux into .tools/n (N_PREFIX, no sudo),
  nvm on macOS into .tools/nvm (PROFILE=/dev/null: no rc file edited), and
  Meteor with HOME=.tools for the installer only, into .tools/.meteor.
  build.sh is replaced, not edited in place, while it may be running.
- Containers get this checkout's .tools/tmp as their /tmp: every
  db-conformance database (MariaDB's temporary tables were verified to land
  there, and MongoDB's socket) and the repack container, which empty it
  themselves before they are removed. install-node-for-arch.sh requires
  the mounted TMPDIR.
- tests/run-node-suites.cjs gives every suite TMPDIR in .tools/tmp, and the
  tests that copied a script into a fixture copy tools-env.sh too; tests
  that pinned ~/.meteor, the rc file edits, sudo n or /tmp/manifest.json
  pin the .tools/ places now and say why.

tests/toolsDirOnly.test.cjs runs tools-env.sh against a TMPDIR already
set elsewhere and under set -euo pipefail, and fails on any script that
makes a temporary file without naming .tools/tmp, writes to the home
directory, or edits a shell rc file, or on a container whose /tmp is its
own.

Thanks to xet7 !
2026-10-27 23:49:55 +02:00
Lauri Ojansivu d1b96f65c3 CHANGELOG: the checkout on every system, and companion repositories only in .tools/.
Thanks to xet7 !
2026-10-27 23:15:41 +02:00
Lauri Ojansivu 96ad680cd4 Release scripts find the checkout on every system, not only at Linux's ~/repos/wekan.
The checkout is at ~/repos/wekan on Linux, ~/Documents/repos/wekan on macOS
and Downloads\repos\wekan on Windows (CLAUDE.md). release.sh,
release-bundle.sh, release-snap.sh, release-sandstorm.sh, release-cleanup.sh,
install-sandstorm.sh, node-update.sh, docker-push-gantt.sh, up.sh and
rel.sh hard-coded Linux's ~/repos/wekan (rel.sh also /home/wekan/repos), so
on a Mac they cd'd into nothing - or into a stale copy - and edited or
packed the wrong tree. Each finds the checkout from its own location now
(REPO_DIR, as mirror.sh already did), which is right on every system; the
parent directory is $(dirname "$REPO_DIR").

DOWNLOAD_DIR defaulted to ~/Lataukset, the Finnish Linux name of Downloads.
macOS keeps ~/Downloads in every language, so it is Lataukset where that
exists and Downloads otherwise.

The VirtualBox appliance scripts keep /home/wekan: that image is a Linux VM
with its own fixed layout. tests/repoLocationPerOs.test.cjs pins the three
locations in CLAUDE.md and AGENTS.md and fails on any script that
hard-codes one system's checkout path or assumes Lataukset.

Thanks to xet7 !
2026-10-27 23:15:29 +02:00
Lauri Ojansivu 9927adcb44 Companion repositories are read from .tools/ only, never from ../w/.
wekan.fi, charts and docker-cleanup-volumes used to be checked out in ../w/
beside the repository, and several places kept that as a fallback after
they moved to .tools/ (CLAUDE.md). With no .tools/wekan.fi, the Hall of Fame
tests read a stale ../w/wekan.fi and failed on markup the site no longer
has.

- tests/hallOfFameProblemsCoverage.test.cjs and
  tests/fixedVulnerabilityClasses.test.cjs read only .tools/wekan.fi (and
  skip, saying so, when it is not there).
- release-charts.sh, backfill-charts.sh, reindex-charts.py and version.sh
  look for the charts checkout only in .tools/charts.
- docker-build.sh clones and runs docker-cleanup-volumes in .tools/.

tests/noLegacySiblingRepos.test.cjs fails when a script, test or document
under releases/, tests/, docs/ or tools/, or at the root, looks in ../w/
again; tests/reindexCharts.test.cjs now requires ../w/charts to be gone.

Thanks to xet7 !
2026-10-27 23:13:02 +02:00
Lauri Ojansivu 6675f0e5d3 CHANGELOG: the shell scripts and tests on macOS, and two Card Settings specs.
Thanks to xet7 !
2026-10-27 23:07:27 +02:00
Lauri Ojansivu 43e09631c4 Single file: a Node.js without single executable applications is named; tests run on an arm64 Mac.
A Node.js can be compiled without single executable applications - the
Homebrew one on macOS is - and `node --build-sea` then only says "Single
executable application is disabled" and exits 9, which the build
reported as a bare failed command. build.mjs asks first (seaSupported) and
stops with what to do: build with a Node.js from nodejs.org, given with
--host-node.

tests/singleFile.test.cjs skips the end-to-end build on such a Node.js
and says why, tests seaSupported against stand-ins of both kinds, and
expects the 32-bit refusal to name the host's own arch - it expected
"x64", so it failed on an arm64 Mac.

Thanks to xet7 !
2026-10-27 23:06:49 +02:00
Lauri Ojansivu 1f9ada5599 Shell scripts on macOS: no /proc, so command lines and memory are asked another way.
macOS and the BSDs have no /proc.

- releases/db-conformance.sh decided whether a container's run was still
  going from /proc/<pid>/cmdline. On a Mac every live run looked dead, so
  a second conformance run removed the first one's containers and
  networks. The command line comes from ps -o command= where there is no
  /proc (tests/dbConformanceStaleContainers.test.cjs passes on macOS now).
- start-wekan.sh and releases/ferretdb/start-wekan.sh sized the Node.js
  and FerretDB heaps from /proc/meminfo, so on a Mac they assumed 2 GiB
  whatever the machine had. They ask sysctl hw.memsize (macOS) or
  hw.physmem (BSD) there, as build.sh already did.
- releases/debug-speed-watch.sh shows the load average from sysctl
  vm.loadavg where there is no /proc/loadavg.

tests/macosShellPortability.test.cjs fails on a /proc memory, load or
command-line read without such a fallback (the Docker entrypoint, which
runs only on Linux, excepted). releases/nextcloud-pack.sh is listed in
tests/buildScriptParity.test.cjs as run by nextcloud.yml.

Thanks to xet7 !
2026-10-27 23:06:49 +02:00
Lauri Ojansivu 0ed8fb20f2 Tests: strip-node.sh is exercised on a Mac, and the ELF cases skip there.
tests/stripNode.test.cjs built its ELF fixture with `gcc`, which on macOS
is clang and writes a Mach-O, so the section reader found nothing and the
test failed with "the fixture has symbols and debug data". The ELF cases
now skip unless the compiler really wrote an ELF, and a new case runs
releases/strip-node.sh on a real Mach-O where there is a Mac: the local
symbol is gone, the exported one stays, the binary is re-signed ad hoc,
passes codesign --verify --strict and still runs.

Thanks to xet7 !
2026-10-27 23:02:53 +02:00
Lauri Ojansivu 283e51eac0 Shell scripts: the root and releases/ scripts run on macOS too.
Started from zsh on macOS, a script gets bash 3.2 as /bin/bash and BSD
tools. Every .sh at the repository root and under releases/ was checked
with bash 3.2 and for GNU-only tool options; what failed:

- test-menu.sh: declare -A (bash 4) is a case function, runner_for, and
  an empty match list no longer trips set -u in bash 3.2.
- releases/ferretdb-latest-tag.sh: with no GitHub token, "${auth[@]}"
  was an unbound variable under set -u in bash 3.2; it uses
  ${auth[@]+...} as ferretdb-asset.sh already did.
- find-replace.sh: never worked anywhere ('$1' in single quotes searched
  for the text $1) and used GNU grep -Z and sed -i. It replaces literal
  text with perl now, skipping the directories find.sh skips.
- sha256sum (missing on older macOS) falls back to shasum -a 256 in
  release-x2.sh, repack-bundle-for-arch.sh, install-node-for-arch.sh and
  sandstorm-capnp-schemas.sh; -c works with both.
- sandstorm-capnp-schemas.sh: nproc falls back to sysctl hw.ncpu, and
  sed -i takes a .bak suffix (GNU and BSD alike) that is then removed.
- sandstorm-slim-spk.sh: ln -sfT is ln -sfn, stat -c%s is wc -c.
- virtualbox/ipaddress.sh: ifconfig where there is no ip.

tests/macosShellPortability.test.cjs parses every bash script with
bash 3.2 when /bin/bash is 3.x, and fails on any of these shapes coming
back: associative arrays or mapfile, flock without a fallback, tar
--transform, a GNU-only sed -i, sha256sum / nproc / ip without a
fallback, ln -T or stat -c.

Thanks to xet7 !
2026-10-27 23:02:14 +02:00
Lauri Ojansivu fa1bb293f9 UCS migration and Nextcloud packing work on macOS: no flock, no GNU tar.
The UCS App Center preinst hook locked with flock(1), which is util-linux
and missing on macOS and the BSDs, so tests/ucsMigration.test.cjs could not
run the hook there ("flock: command not found"). Without flock it now takes
the same lock with perl's flock() on the inherited fd 9: a flock(2) lock
belongs to the open file, so it is held until the hook closes fd 9, exactly
as flock -n 9 holds it. Verified: a second locker is refused and the lock
frees when the first one closes. perl is on UCS (Debian) and macOS.

The Nextcloud ExApp archive was packed with GNU tar's --transform, which
bsdtar (macOS, the BSDs) does not have. releases/nextcloud-pack.sh copies
the source under a staging <app-id>/ and packs that directory, with the
same exclusions, using only options both tars share; COPYFILE_DISABLE=1
keeps macOS tar from adding ._* AppleDouble files. The workflow and
tests/nextcloudRelease.test.py both use it, and
tests/ucsNextcloudWorkflows.test.cjs fails if --transform comes back.

Thanks to xet7 !
2026-10-27 22:57:57 +02:00
Lauri Ojansivu d8d7b01334 Tests: the Card Settings both-sides and Scrum title specs follow the current Card Settings.
card-settings-both-sides read row[side].needsCard for every row, but six
rows have no minicard side by design (suggested members, checklist from
description, attachment previews and details, Drawing, action items), so
it threw "Cannot read properties of undefined". It expects what
rowsForSide lists now: rows that have the side, less the card-only List
title.

The Scrum settings title spec read the pane title right after clicking
Scrum. Card Settings opens on Card now, so it read "Card" and failed on
the "Card button" label. It waits for the pane title to be Scrum's menu
name first.

Both failed in the Chromium stage of the 2026-10-27 EVERYTHING run.

Thanks to xet7 !
2026-10-27 22:25:33 +02:00
Lauri Ojansivu b1370fbc5e CHANGELOG / TODO Later: the six maintainer-decision items are done.
API JSON uploads and the site logo stored again, the validate-and-move
method moves again, REST card placement checked, WIP limits admin-only,
AuthBleed attempts recorded and Data health's "Never" with a key of its
own - each removed from TODO Later now that its fix is in Upcoming. The
browser runs and WebKit stay there.

Thanks to xet7 !
2026-10-27 21:46:31 +02:00
Lauri Ojansivu a40c4495d4 Lists: a WIP limit is set by a board admin, on every path.
The applyWipLimit, enableWipLimit and enableSoftLimit methods have always
required a board admin ("You must be a board admin to modify WIP
limits."), but a member with write access could still set wipLimit with a
plain list update - the Lists allow rule grants write members every field
- or with REST PUT /api/boards/:boardId/lists/:listId. A WIP limit is a
board-wide workflow rule, so the methods' rule now holds everywhere:

- a Lists deny rule refuses an update touching wipLimit unless the caller
  is admin of the list's board. Every WeKan client path goes through the
  methods, so it refuses nothing in ordinary use;
- the REST PUT asks for a board admin (checkBoardAdmin) when the body
  carries a wipLimit; write access still edits title, color and starred.

tests/wipLimitAdminOnly.test.cjs runs the real deny rule, pins the REST
check before the write, and fails on any list method that changes a WIP
limit without a board admin check first.

This was in CHANGELOG TODO Later as needing a maintainer decision;
decided: board admins only.

Thanks to xet7 !
2026-10-27 21:45:43 +02:00
Lauri Ojansivu 02bb0814c3 Tests: linkedWritePolicy and webhookBleed read the code as it is now.
linkedWritePolicy sliced POST cards' linked-card branch up to the getCard
that #6823 removed, so the slice ran to the end of the file and found
checkBoardAccess elsewhere; it ends at the branch's author-spoof note now,
and fails if that is missing. webhookBleed (GHSA-7w2h-g83c-jqrp) pinned
copyBoard's signed-out refusal as one line; it records an AuthBleed
attempt first now, so the guard accepts the block and still requires the
refusal before the board is read.

Thanks to xet7 !
2026-10-27 21:42:47 +02:00
Lauri Ojansivu 77956c98ed Admin Panel / Problems: Data health's "Never" has a key of its own.
The Data health status line ("Last Run: Never") reused List Sync's
list-sync-last-synced-never, so a translator who words that one as "never
synced" would make Data health read wrong. It is data-health-never-run
now, added after data-health-repair with add-pending-keys.mjs
--all-locales (the completeness suites require every locale to carry
English's keys in order) and listed in pending-transifex.json.

Every locale already translates the identical English "Never" under
list-sync-last-synced-never; fill-translations.mjs --apply filled the new
key from that in 234 locales, writing only the English placeholders. A
real Transifex translation still replaces it on the next pull.

This was in CHANGELOG TODO Later as needing a maintainer decision.

Thanks to xet7 !
2026-10-27 21:42:47 +02:00
Lauri Ojansivu 314c153c9b Admin Panel / Problems: AuthBleed attempts are recorded.
copyBoard, copyCard, copySwimlane and createCardWithDueDate - four of the
AuthBleed methods - refused with plain not-authorized errors, so an
attempt left no trace in Admin Panel / Problems. AuthBleed was on the
PENDING list of tests/hallOfFameProblemsCoverage.test.cjs.

A new catalog key, authz.copy (AuthBleed, high, CWE-862), is recorded
where no legitimate client can be:

- a signed-out caller of any of the four - no WeKan client copies while
  signed out, and with no userId nobody can be blocked;
- any refusal of createCardWithDueDate, which no WeKan client calls.

The membership refusals of copyBoard, copyCard and copySwimlane are
deliberately NOT recorded: a member who is not admin of a shared or
default board template, or a stale Copy Card / Copy Swimlane dialog,
reaches them in ordinary use, and a high-severity record naming the
account disables that login (server/lib/blockOnSecurityEvent.js). Each
record is wrapped so logging cannot break the guard.

tests/authBleedCopyMethods.test.cjs runs the real method bodies and pins
both: what is recorded, and that the ordinary-use refusals are not.
AuthBleed leaves the PENDING list.

This was in CHANGELOG TODO Later as needing a maintainer decision.

Thanks to xet7 !
2026-10-27 21:29:17 +02:00
Lauri Ojansivu e7505a4fef Attachments: the API's JSON uploads, copies and the site logo are stored again.
The JSON branches of REST /api/attachment/upload, upload-background and
copy, their Meteor-method twins in server/attachmentApi.js, and Admin
Panel's site logo upload created the file with Attachments.insertAsync.
Meteor-Files (ostrio:files 3) has insertAsync only on the client; on the
server it is not a function, so every one of them failed - a REST 500
"Attachments.insertAsync is not a function". Only the multipart branches,
which use addAttachmentFromStream, worked.

All seven store with writeAsync(buffer, { fileName, type, userId, meta },
true) now, as email-to-card and the importers already do. Its
after-upload hook validates the file - type, size and the external
scanner, which these paths skipped - and moves it to the default storage,
or to meta.copyStorage for a copy, so the second deferred moveToStorage
each branch made is removed. The file records who stored it.

tests/attachmentApiServerWrite.test.cjs pins the seven calls and fails on
any server-side Attachments.insertAsync or Avatars.insertAsync.

This was in CHANGELOG TODO Later as needing a maintainer decision; the
site logo upload was found by the new test's tree-wide check.

Thanks to xet7 !
2026-10-27 21:26:30 +02:00
Lauri Ojansivu d3e80af1ac Attachments: validateAttachmentAndMoveToStorage moves the file again.
After validating, the method moved the file from Meteor.defer by calling
the moveAttachmentToStorage method. A deferred call has no method
invocation and so no userId, and that method's first check is "You must be
logged in.": every move was refused, the error went nowhere, and the file
stayed where it was.

Every check moveAttachmentToStorage makes - a site admin, the board
visible, the destination on the allowlist - is made by this method before
the defer, so the deferred step now calls moveToStorage itself, as the
upload path already does, and logs a failure instead of dropping it.

tests/moveStorageBleed.test.cjs pins it; its existing guard still requires
the admin check and the allowlist before every caller-named move, and a
new one fails on any Meteor.defer or Meteor.setTimeout in server/ or
models/ that calls a Meteor method.

This was in CHANGELOG TODO Later as needing a maintainer decision.

Thanks to xet7 !
2026-10-27 21:23:54 +02:00
Lauri Ojansivu c355926767 REST: a new card's list and swimlane must belong to the board in the URL.
POST /api/boards/:boardId/lists/:listId/cards and its bulk sibling wrote the
URL's listId and the body's swimlaneId onto the new card without checking
either against the board. A list or swimlane id of ANOTHER board gave a
card that pointed at a list and a swimlane its own board does not have -
the #6572 shape, a card that shows on neither board - and the list
permission check was evaluated against the wrong board's rules. The PUT
route has refused this since #6572; the create routes now do the same with
a 400 that names the field, and the bulk route reports it per entry. An id
that is not a string (such as { $ne: null }) is refused rather than looked
up. The linked-card branch was already checked by createLinkedCardFor.

tests/restCardPlacement.test.cjs pins both routes and fails on any REST
handler in server/ that inserts a card without the check.

This was in CHANGELOG TODO Later as needing a maintainer decision; the PUT
route's #6572 rule already decided it.

Thanks to xet7 !
2026-10-27 21:22:49 +02:00
Lauri Ojansivu 6abcc8fa1f CHANGELOG: Problems reports collection scans and read rate, literal board search, one "created" activity, and less database load.
The six fixes from Raymond-Voogt's reports #6821-#6826. CHANGELOG.md went
over its 450,000-byte budget with them, so v12.40 is moved to
old-CHANGELOG/2026/10.md by releases/changelog-archive.mjs.

Thanks to Raymond-Voogt and xet7 !
2026-10-27 21:20:58 +02:00
Lauri Ojansivu b87858baaf Problems / Database: report queries without an index and a high read rate.
The database health probe (server/lib/databaseHealth.js) saw restarts, slow
storage and a full disk, but not what made one instance slow: a 10 GB
collection scanned three times per search, and an hourly job reading all
1.1 million cards (#6824, #6825). Both were visible only in MongoDB's own
log, as "Slow query" lines with COLLSCAN, and in serverStatus: 1.6 billion
documents read in two hours.

Two more checks, every 5 minutes with the others:

- db.collection-scan: the "Slow query" lines MongoDB keeps (getLog
  'global'), newer than the last probe, where a query read a whole
  collection of at least 10,000 documents. Grouped by collection and the
  field names filtered on - never values - with count, documents read and
  time. Small tables read whole by design (settings, roles) and internal
  namespaces are left out. Needs the getLog privilege; without it, and on
  FerretDB, the check finds nothing and is skipped.
- db.scan-rate: serverStatus.metrics.queryExecutor.scannedObjects between
  two probes above 50,000 documents per second. A database restart, which
  resets the counter, is not counted.

Both are rows in Admin Panel / Problems / Database with what to do;
docs/Databases/MongoDB/Storage-Requirements.md lists them.

tests/databaseHealthScans.test.cjs drives the log parsing and the rate,
including that a filter's values never reach the row. From the reporter's
patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6826.
2026-10-27 21:19:51 +02:00
Lauri Ojansivu d481bffdfe History audit and position-history cleanup: once per installation.
Both were Meteor.setInterval jobs, so every WeKan process behind one
database ran them. On a 24-pod installation (54,234 History rows, 148,660
board memberships, 1,760 position-history entries):

- the change history integrity audit read the whole History 24 times an
  hour, and
- UserPositionHistory.cleanup ran 24 times a day, each run one query per
  board membership of every user, each fetching up to 1000 whole
  documents, to trim nothing - outside the CPU governor.

Both are SyncedCron jobs now, the way the other periodic jobs already
are: synced-cron's { intendedAt, name } lock runs each scheduled time on
one pod only, and its running-job check (indexed in #6769) skips a run
while the previous one is still going. No new collection is needed for
it. The audit stays hourly; the cleanup runs at 4:15 am.

The cleanup asks the database which user/board pairs hold more than 1000
non-checkpoint entries (aggregate) and trims only those: the 1000th
newest entry's date with one indexed query, everything older in one
remove. The result is the same as before - the newest 1000 per user and
board are kept, checkpoints never - also for pairs whose user has since
left the board. It waits for a quiet CPU and pauses between pairs.

models/lib/positionHistoryCleanup.js holds the plan with the database
calls passed in; tests/positionHistoryCleanup.test.cjs runs it on an
in-memory history against the old algorithm's result, counts the queries,
and fails if anything in server/ or models/ drives either job from a
per-process timer again. From the reporter's patch, with the claim moved
onto the existing SyncedCron.

Thanks to Raymond-Voogt and xet7 !

Fixes #6821.
2026-10-27 21:18:08 +02:00
Lauri Ojansivu dad16609c9 Board search: match the typed text, not a regular expression.
The sidebar card search, the link-card popup and the list, swimlane and
board pickers (Board.searchCards / searchLists / searchSwimlanes /
searchBoards, models/lib/cardSearch.js) and the import member search
(UserSearchIndex) compiled the typed term AS a regular expression:

- "C++", "(2026)", "[x" or a case number like "PL0900-2026(1)" threw
  "Invalid regular expression" and the search showed nothing;
- "." matched any character and "*" or "?" changed what was searched for.

Every server-side search (global search, publications, boardTextSearch)
already escapes the term and matches it literally. These now do the same,
still case-insensitive, through literalSearchRegex() in cardSearch.js,
which the user search reuses rather than carrying its own escape. The
Advanced Filter's /regex/ syntax is a separate path and is unchanged.

tests/noUnescapedDynamicRegExp.test.cjs no longer needs its reviewed
exceptions for models/boards.js, models/lib/cardSearch.js and
models/users.js; tests/searchLiteralTerm.test.cjs drives the matching and
fails on any model, client or imports file that compiles a raw term into
a RegExp. From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6822.
2026-10-27 21:15:51 +02:00
Lauri Ojansivu 5f3b211b37 New issue and REST linked card: one "created" activity, not two.
A card inserted through the Cards collection gets its "created" activity
from Cards.after.insert. Two paths inserted through the collection AND
called cardCreation() themselves:

- Board View / Version Control, New issue (vcsCreateIssue), and
- POST /api/boards/:boardId/lists/:listId/cards with linkedId
  (createLinkedCardFor).

Each such card got two "created" activities, and every watcher two
notifications and two emails. The other explicit callers - REST create,
bulk create, email view, email to card - insert with Cards.direct, where
no hook runs, and are right to record it themselves.

Both extra calls are removed. Outside a method (a REST route, a server
job) the hook has no user, so the REST linked card's activity would have
lost its author: the hook now falls back to the card's own userId, which
createLinkedCardFor sets to the session user. Activities never exclude
their actor from notifications, so the fallback only names an author
where there was none, such as a recurring card's next copy.

tests/noDoubleCreateActivity.test.cjs walks server/ and models/ and fails
on any explicit cardCreation that follows a hooked insert. The
GHSA-6jr3-42jf-vhm5 guard in restApiIdorBatch now pins the linked card's
attribution through createLinkedCardFor and the hook. From the reporter's
patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6823.
2026-10-27 21:14:23 +02:00
Lauri Ojansivu 2be842351b Search sessions: indexes, and stale sessions of every user are removed.
Every global search and card report (server/publications/cards.js) upserts
its search session by { userId, sessionId }, reads it back by the same two
fields, and removed the user's sessions older than a day by
{ userId, modifiedAt }. The sessiondata collection had only its _id index,
so each search scanned every stored session three times.

And only the searching user's OWN old sessions were removed: a user who
never searched again kept theirs forever. Each session holds the pickled
selector of the search, which lists every board the user can see - large
for an administrator. One instance had 4,751 sessions taking 10 GB, the
biggest collection in its database.

- Indexes { userId: 1, sessionId: 1 } and { modifiedAt: 1 }.
- Sessions older than a day are removed for everybody on each search, as
  one indexed delete. Paging (next/previous) works within the day as before.

Test: tests/searchSessionIndexes.test.cjs. From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6824.
2026-10-27 21:12:45 +02:00
Lauri Ojansivu e9440d503e Recurring cards: an index for the hourly job.
server/cardRecurrenceSchedule.js runs every hour and asks for the cards
with a recurrenceInterval ($exists: true, not null or 'none'). There was no
index on the field, so the query read every card in the database to find
the few that recur. One instance logged it as a slow query: COLLSCAN,
1,108,017 documents examined, 1,155 ms.

A partial index on recurrenceInterval holds only the cards that have one,
the same way activities indexes commentId and attachmentId. The job's
$exists: true lets MongoDB use it; tests/recurrenceIndex.test.cjs pins both.
From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6825.
2026-10-27 21:12:45 +02:00
Lauri Ojansivu 66b4ad8dcb CHANGELOG / TODO Later: the unverified browser runs, WebKit, and five behaviours that need a maintainer decision.
Thanks to xet7 !
2026-10-27 21:01:51 +02:00
Lauri Ojansivu aae0490b71 CHANGELOG: FrameBleed follow-up - a 304 kept a file's own CSP - and the Scrum, Sesotho and drag reorder specs.
Thanks to xet7 !
2026-10-27 20:59:24 +02:00
Lauri Ojansivu 4a656baea3 Tests: the Scrum, Sesotho and drag reorder specs read one list per side, and open the SMTP fields first.
Card Settings draws each row once per list, and its minicard list head is
empty; the Date section's card list starts with the fixed Mark as complete
row; the SMTP host field is shown only while email settings are enabled and
SMTP is chosen.

Thanks to xet7 !
2026-10-27 20:57:38 +02:00
Lauri Ojansivu e8a1106782 FrameBleed follow-up: a 304 Not Modified no longer replaces a stored file's own Content-Security-Policy.
Files are served private, no-cache, so every later view of an attachment,
avatar or thumbnail revalidates and gets a 304, and a browser updates the
stored response's headers with the 304's. The app-wide frame-ancestors 'self'
that FrameBleed's restore puts on every response therefore replaced the
file's own policy - an SVG's default-src 'none' and sandbox - from the second
view on. The SVG body is still sanitized on every serve, so this lost a layer
of defence rather than opening script execution. The app-wide framing headers
are now left off a 304, so the stored 200 keeps the policy its route chose; a
header a route set on the 304 itself is kept. Not recorded in Admin Panel /
Problems: it fires on ordinary revalidation, not on an attempt.

Thanks to xet7 !
2026-10-27 20:57:38 +02:00
Lauri Ojansivu a599e066b3 CHANGELOG: Apple Glass Pastel muted text, and the S3 settings spec.
Thanks to xet7 !
2026-10-27 20:46:59 +02:00
Lauri Ojansivu 9679aaf636 Apple Glass Pastel: muted text is darker, and the contrast spec waits for the theme.
The theme's quiet text, #6b7280, was 4.51:1 on #f7f7f7 and 4.44:1 on the
tinted card header, at or under the 4.5:1 it needs; #4b5563 is about 7:1.
The opened card and sidebar contrast tests measured right after a reload,
before the board's colour had reached the page, and so measured WeKan's stock
colours; they now wait for the theme class as the header test does.

Thanks to xet7 !
2026-10-27 20:32:47 +02:00
Lauri Ojansivu 13d3a74b3f Tests: the S3 settings spec expects both of its translated Save buttons.
The S3 pane has had two Save buttons since #6465, one under the Enabled and
Read switches and one after the connection fields; the spec matched one.

Thanks to xet7 !
2026-10-27 20:30:13 +02:00
Lauri Ojansivu 06e27909df CHANGELOG: #6820, and the oldest release moved to old-CHANGELOG/2026/10.md to stay under the size budget.
Thanks to mimZD and xet7 !
2026-10-27 20:26:11 +02:00
Lauri Ojansivu 66ee2d12f3 Card Settings: a ticked Draggable checkbox is a checkbox, not a dot.
forms.css gives every input the 9px 14px padding of a text field. The
Draggable table brought its native checkboxes back without removing it, so a
16px box left the browser about 2px to draw in and a ticked one was a small
blue dot beside an unticked square. The same was true of the WIP limit groups
popup and of task-list checkboxes in a description. All three have no padding
now, and a test fails on any stylesheet that brings back a native checkbox
with it.

Thanks to mimZD and xet7 !

Fixes #6820.
2026-10-27 20:25:48 +02:00
Lauri Ojansivu 9e47a188ce CHANGELOG: the browser test run's fixes - opened card, editors, visibility, drawing, and the specs.
Thanks to xet7 !
2026-10-27 20:14:39 +02:00
Lauri Ojansivu a8f65adff2 Tests: browser specs follow deliberate changes, and their fake SMTP server waits for its port.
LinkedWriteBleed over REST is refused and recorded but no longer disables the
account (#6798); notification intents are seeded recent, past the expiry of
#6768; quoted custom field values; the Login and Windows SSO forms each have
a Save; the Bots view under Rules; the popup stub has onCreated and helpers;
the board view panel fills the window first. An invite mail goes to a running
SMTP sink, a sink waits for a busy port, and the allowed AssignedBleed read
retries on a socket reset only. The first checklist is added with the title
line's +.

Thanks to xet7 !
2026-10-27 20:14:03 +02:00
Lauri Ojansivu 2929b61c85 Drawing: a new drawing fills the window it is shown in, also once maximized.
A new drawing was fitted to the popup before it was maximized and kept that
size. It is refitted whenever its room changes, until it is zoomed or resized
by hand.

Thanks to xet7 !
2026-10-27 20:14:03 +02:00
Lauri Ojansivu 69a8bae0c6 Editors: text typed right after a form opens is no longer selected and replaced.
50 ms after an inlined form opens it focuses the field and selects what it
holds, so the first key replaces a title being edited. Text typed or pasted in
those 50 ms was selected too, and the next key - Enter for a new line - left
only a line break. The selection is skipped when the field already changed.

Thanks to xet7 !
2026-10-27 20:14:03 +02:00
Lauri Ojansivu 978af29b33 Tests: the language specs read the option values, tabs, addresses and texts the app has.
The Gregorian option's value is gregorian; the date-changed triggers are on
the card triggers tab; the shared templates and security panes have their own
addresses; Shortcuts opens as a modal; the roles pane has two descriptions;
saved rule descriptions are lowercased; the feature list is FEATURE_KEYS long;
a plain member is not shown the Rules editor at all (#2217), and the server
still refuses its rule writes.

Thanks to xet7 !
2026-10-27 20:07:01 +02:00
Lauri Ojansivu e6f7d226ca Admin Panel / Attachments: the Google Cloud Storage key help is shown once.
A leftover copy of gcs-credentials-description, from before the English and
translated pairs, showed the same sentence twice under the key field.

Thanks to xet7 !
2026-10-27 20:07:01 +02:00
Lauri Ojansivu 437da1d9c7 Board visibility: the Private and Public descriptions are shown again.
The helper handed TAPi18n.__ over unbound; __ reads this, so it threw and both
descriptions of the visibility popup were an empty line in every language
since the custom descriptions of Admin Panel / Settings / Visibility. It is
passed bound, and a test fails on any TAPi18n.__ passed as a bare value.

Thanks to xet7 !
2026-10-27 20:07:01 +02:00
Lauri Ojansivu a4197f228f Tests: the Card Settings specs open the section a row is in, and pick the list by its side.
The popup opens on its Card section and draws only the open section's rows,
each once per list (Show on Card, Show on Minicard). The specs looked for rows
of other sections and matched a row twice. The linked timer spec reloads after
its database writes, so the board's spent time has reached the page.

Thanks to xet7 !
2026-10-27 20:05:44 +02:00
Lauri Ojansivu f852cb7032 Add card: the due date hint follows the board's switch while the form is open.
The hint was recomputed only on typing, so turning Due date from title off in
Board Settings / Card left a hint already shown.

Thanks to xet7 !
2026-10-27 20:05:44 +02:00
Lauri Ojansivu 8f9103c336 Opened card: it stays visible after Maximize and Minimize, or Collapse.
The class that keeps the window unpainted until it is placed beside its
minicard was a static class of the template, removed by hand once placed.
Blaze writes a tag's static classes again whenever its class attribute
changes, so maximizing and minimizing, or collapsing, brought it back and left
the card open but invisible. It is now a reactive class, off for good once the
window is placed.

Thanks to xet7 !
2026-10-27 20:05:44 +02:00
Lauri Ojansivu 6241be9053 Tests: the browser specs add a first checklist with the title line's +, where #6796 put it.
Since #6796 a card with no checklist has only the + of the Checklists title
line (a.add-checklist-top); the bottom a.add-checklist appears once there is
a checklist. The specs looked for the bottom one only, so every spec that adds
a card's first checklist failed, and 12-checklists skipped its checks silently.

Thanks to xet7 !
2026-10-27 19:45:29 +02:00
Lauri Ojansivu c438682700 CHANGELOG: documentation of Show empty fields, the [label] switch and Data health.
Thanks to xet7 !
2026-10-27 19:42:29 +02:00
Lauri Ojansivu 30e960e129 Docs: Show empty fields of Card Settings, the [label] title switch of Member Settings, and Data health repairs.
Thanks to xet7 !
2026-10-27 19:42:20 +02:00
Lauri Ojansivu 9bea13f152 CHANGELOG: #6818 Data health, and the shared-template spec fix.
Thanks to Raymond-Voogt and xet7 !
2026-10-27 17:08:29 +02:00
Lauri Ojansivu d9cf523f9e Admin Panel / Problems: Data health, a nightly scan of broken references, with repairs.
The scan counts what points at nothing: cards on a missing or another board's
list or swimlane, ghost and duplicate board members, boards with no active
admin, custom field values and boards that no longer exist, checklist items
without a checklist, and subtasks or linked cards whose card is gone. It runs
nightly when the CPU is low, claimed so that only one pod scans or repairs.

A repair that only moves data reuses the upgrade steps; one that deletes asks
twice, and the server refuses it without that confirmation. Every repair edits
with $pull or a guarded update, so a concurrent edit is not overwritten, and a
member dedupe keeps the admin entry. Legacy scalar boardId and string boardIds
of custom fields are understood.

Translations done with low confidence: ay, csb, lld, kok, wa-RR, ve-CC, bi, tpi.

Thanks to Raymond-Voogt and xet7 !

Fixes #6818.
2026-10-27 17:08:15 +02:00
Lauri Ojansivu c1c9d3fb03 CHANGELOG: #6808 follow-up, linked card fields, search fixes, browser spec seed and Tigre/Tsonga wording.
Thanks to xet7 !
2026-10-27 16:44:06 +02:00
Lauri Ojansivu 00cf8f0958 Tests: the shared-template language specs read the description beside the scope list, where it is.
The Corsican and Southern Sotho specs looked for .templates-description and
.shared-templates-hint inside ul#templates-setting, which holds only the three
scope checkboxes; both paragraphs are its siblings in .templates-settings.

Thanks to xet7 !
2026-10-27 16:44:06 +02:00
Lauri Ojansivu 125e1659f8 Search: attachments are found again, by has:attachment, by their filename, and has:-attachment / has:-checklist work.
Found by the Haitian attachment-search browser test. Every attachment
lookup of the global search read the attachment's card from a top-level
cardId, which attachments do not have - they name it in meta.cardId - so
has:attachment, a free-text match on a filename and attachment:<name>
found no card at all; tests/databaseQueryBounds.test.cjs had pinned that
projection. The negated predicates (has:-attachment, has:-checklist)
were ignored and returned the cards WITH one. Soft-deleted attachments
do not count for has:attachment.

Thanks to xet7 !
2026-10-27 16:39:00 +02:00
Lauri Ojansivu 6c2edd6752 Board search: one result highlighted at a time.
Stepping to the next result with Enter highlighted it while the last
one stayed lit for the rest of its three seconds, so two cards were
highlighted at once and it was not clear which was current - found by
the #2171 browser test. Revealing a result removes the highlight from
any other.

Thanks to xet7 !
2026-10-27 16:36:01 +02:00
Lauri Ojansivu 8788458a87 Tests: the browser specs' boards show every field, as the specs were written for.
The specs predate an opened card showing only the fields it has data
in: a checklist spec found no Add checklist on an empty card (and five
others skipped their assertions the same way). The seeded board turns
Card Settings / Card's Show empty fields on - what an opened card did
before - and the specs of the empty-field rules (card-add-fields) turn
it off.

Thanks to xet7 !
2026-10-27 16:35:24 +02:00
Lauri Ojansivu 1f5a7b8220 A linked card shows the fields its source card has.
An opened card shows the fields it has data in, but a linked card keeps
its data on its source card - members, labels, dates, custom fields,
checklists - and the decision read the linked card's own, empty
document: a linked card hid every field of its source. Found by the
#1946/#6613 browser test, whose source member's avatar was missing.

shownFieldsOf() takes the data, and the counts of checklists, subtasks,
text notes, attachments and comments, from the source card, and the
card's own choices (On this card) from the card opened. The opened
card, its sections and Card Settings' On this card all decide through
it. tests/linkedCardShownFields.test.cjs runs it.

Thanks to xet7 !
2026-10-27 16:34:50 +02:00
Lauri Ojansivu 036ac4f56b #6808 follow-up: a card deleted elsewhere also leaves the card's address.
Found by the #6808 browser test: when an opened card really is deleted,
its window is destroyed with its data - usually before the server has
answered whether the card is gone - and the confirmation was dropped
for a destroyed window, so the window closed but the address stayed
the card's. The question is asked outside the window now
(confirmGoneCard, waiting for a settled connection and login), and the
window's end asks it too when the card is missing; the close
(closeGoneCard) clears the window, the popup and the address. A card
that only left for a reconnect still answers available and stays.

Thanks to brlin-tw and xet7 !
2026-10-27 16:32:52 +02:00
Lauri Ojansivu c7145402af Translations: Tigre card noun in the Retention help line of #6819.
The Retention help text came with Tigrinya's bare ካርድ for card; Tigre
says ወረቀት ካርድ, which tests/tigreAllBoardCardContexts.test.cjs holds.

Thanks to xet7 !
2026-10-27 16:30:59 +02:00
Lauri Ojansivu f615d06e47 CHANGELOG: StampBleed, UploadBleed, the regression audit, #6817 and #6819.
Thanks to mimZD, Raymond-Voogt and xet7 !
2026-10-27 16:28:52 +02:00
Lauri Ojansivu ebcd02be6f Hall of Fame regression audit: every fixed vulnerability still fixed, siblings of six of them closed.
Every Hall of Fame vulnerability without a test naming it (24 names, 30
advisories) was checked against the current source. No fix had been
reverted. Looking for the same mistake elsewhere found siblings:

- FloppyBleed: the fs-path-heal upgrade step joined an attachment's
  stored name into candidate paths as-is; a record a client inserted
  named ../../../etc/passwd made the next upgrade copy that file into
  storage - an arbitrary file read. Only the name's last component is
  used now; the bulk move limits a legacy name's extension the same way.
- FilePathBleed (GHSA-g6vm-7757-pr88): the insert guard looked only
  inside versions; with versions left out ostrio:files falls back to a
  top-level path, which removing the record deletes and validation
  reads. A client's top-level path is refused for attachments and
  avatars, recorded by the existing canaries.
- MoveStorageBleed: validateAttachment, which deletes a file failing the
  current upload rules, was open to anybody who could see the board, a
  public one included. Only who may change the file, or a site admin,
  validates now; a refusal is recorded (authz.attachment-validate).
- HookUrlBleed / IntegrationBleed: the REST reads of a board's
  integrations gave the webhook URL - whose path is its secret - to
  every member reading the board; non-admins get what the publication
  gives them.
- AttachmentMigrationBleed: four migration methods answered any
  logged-in user about any board, deleteDuplicateEmptyLists.getStatus
  with a private board's list titles; they require a visible board and
  record refusals (authz.migration-board).
- LegacyAttachBleed (GHSA-xcxp-hx9w-q5q9) and MimeStorageBleed
  (GHSA-rv5g-5p4g-3v89): the legacy route judged a name dangerous only
  for .svg, the response policy missed x-javascript, ecmascript, xsl and
  +xml types, and the sniffers missed <tag/onx= handlers.

OIDCBleed refusals are now recorded in Admin Panel / Problems too. New
suites name the vulnerabilities they guard, existing ones name the
advisories they already covered, and tests/securityRegressionCoverage
counts 128 of 137 published vulnerabilities with a named test (was 125).

Thanks to xet7 !
2026-10-27 16:28:12 +02:00
Lauri Ojansivu 2799eb900f Admin Panel / Attachments / Retention: readable labels, and the default shown.
The Retention page (#6789) showed only the variable names
(ATTACHMENTS_RETENTION_DAYS, ...), the raw choices uploaded and
card-inactive, and Default () with nothing between the brackets
whenever the variable was set but empty - which is what a Helm chart or
a docker-compose file listing the variable does.

Catalog fields (models/lib/authConfigCatalog.js) can name a labelKey, a
helpKey and choiceLabelKeys: the form shows the translated label with
the variable name beside it, a help line, and the choices by name.
Fields without them look as before. An empty variable is unset for the
value shown, as the retention code itself reads it, so the default is
shown again; yes/no defaults are translated. The five retention fields
use them; seven new strings in every language (low confidence for the
rarest and constructed ones). From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6819.
2026-10-27 16:24:58 +02:00
Lauri Ojansivu ccc67ddf68 Show on Card and an empty field: already handled by Show empty fields, Show empty and On this card.
#6817: on v12.65 the reporter ticked Due in Board Settings / Card's
Show on Card and an opened card without a due date showed no Due field.
Show on Card says what a card can show; since v12.65 an opened card
shows a field when it has data. Showing an empty field is done since
#6806 (Upcoming) by Card Settings / Card's Show empty fields, the Show
empty column, or + Add / On this card. The test pins the reporter's
board through each, and that Show on Card off still wins.

Thanks to mimZD and xet7 !

Fixes #6817.
2026-10-27 16:14:34 +02:00
Lauri Ojansivu 51111e1958 StampBleed and UploadBleed: documents on one's own card stamped with another board, and unauthorized attachment uploads.
StampBleed (found reviewing the patch for #6813): checklists, checklist
items and text notes carry a denormalized boardId, by which the board
publication publishes them, and items a checklistId, by which a
checklist draws them. The insert allow rules check write access on the
document's card only, and the server's insert hooks kept a boardId the
client sent. So a member of any board could insert, on their own card,
a document stamped with a private board's id - published to that
board's members - and an item with that board's checklist id, drawn in
it. 4bd6a7f088 made the server take boardId from the card; now the
forged insert is refused outright and recorded, for those three and for
a direct attachment insert with a foreign meta.boardId. WeKan's own
client never sends a mismatch.

UploadBleed: Meteor-Files' upload pipeline (_FilesCollectionStart_
attachments / Write and its HTTP twin) inserts the stored attachment
SERVER-side, so the allow rules were never asked, and the attachments'
onBeforeUpload only cleaned the file name: any DDP client, signed in or
not, could put a file on any card of any board - published to its
members with an addAttachment activity and its notifications - and the
admin's uploads-blocked switch was skipped too. The upload is
authorized on every chunk now: signed in, allowed to edit the card (or
write the board, for a background or map image), uploads not blocked,
and a card attachment's board, list and swimlane are its card's.

Both are recorded in Admin Panel / Problems (authz.card-child-stamp,
authz.attachment-upload), once per upload. tests/stampBleed.test.cjs and
tests/uploadBleed.test.cjs run the real decisions, and fail on a card
child with no insert board check or a FilesCollection without a server
upload check; upload-stamp-bleed.e2e.js attacks the running server.

Thanks to xet7 !
2026-10-27 15:38:58 +02:00
Lauri Ojansivu eade07c914 CHANGELOG: #6809, #6810, #6811, #6812, #6813, #6814, #6815 and #6816.
Thanks to brlin-tw, jullbo, Raymond-Voogt and xet7 !
2026-10-27 15:23:55 +02:00
Lauri Ojansivu 595e26cfa8 Translations: Tigre and Tsonga wording of the [label] switch of Member Settings.
The new texts of f89c8f8030 took Tigrinya's word for name (ስም) in Tigre,
which has its own (ስሜት), and repeated Tsonga's nhloko-mhaka (title)
three times in one sentence - the shape the guards against generated
filler catch. tests/tigreAllEstablishedNounContexts.test.cjs and
tests/wrongLanguageCorrections20261014.test.cjs pass again.

Thanks to xet7 !
2026-10-27 15:23:55 +02:00
Lauri Ojansivu 32031c64c4 Upgrade: cards moved to another board before #5874 are shown again.
#5874 fixed new cross-board moves (server/lib/cardBoardConsistency.js),
but a card moved before that fix can still point at a list of the board
it LEFT. Every view of its own board selects cards by that board's
lists, so the card is shown nowhere, and swimlane-structure's
dangling-listId rescue does not see it because the list exists. One
production database (1.1 million cards) had 706 such cards on 383
boards, 391 not archived.

A startup upgrade step, card-list-other-board, gives each such card its
own board's first unarchived list (and that list's swimlane when it is
bound to one), or a Rescued Data list when the board has none. Cards are
streamed with two fields, one updateMany per (list, board) pair,
idempotent. From the reporter's patch, with one change: the step runs
BEFORE swimlane-structure, so a rescued card whose new list is not bound
to a swimlane - it kept the swimlane of the board it left - is put on a
swimlane of its board in the same pass, not hidden in the Swimlanes view
until the next release re-runs the checks.

Thanks to Raymond-Voogt and xet7 !

Fixes #6814.
2026-10-27 15:09:38 +02:00
Lauri Ojansivu f2908f34e7 Card numbers stay unique: the server checks the number a browser sends.
#5813 / #4743 made server-side card numbers atomic with a per-board
counter, but a card added in the browser - the add-card form, a card
from a template, a link, a checklist item turned into a card - still
brings the number the browser computed: the highest among the cards it
has, plus one. With lazy card loading the browser has only the cards in
view, so it reissued numbers other cards already carried, and the
counter never saw them, so the server reissued them as well. One
production database had 842 numbers used by two or more open cards.

Cards.before.insert on the server: a number another card on the board
already has is replaced from the counter, after raising the counter to
the board's highest number; a free number the counter did not issue
raises the counter to it. A board with no counter yet is seeded from its
highest number first, as getNextCardNumber's lazy seed does - the
reporter's patch created it from the browser's number alone, which can
be below numbers outside that browser's view, and the seed would then
never run. 0 / missing stays no number. Existing duplicates are not
renumbered: a number may be written down elsewhere.

Thanks to Raymond-Voogt and xet7 !

Fixes #6815.
2026-10-27 15:08:21 +02:00
Lauri Ojansivu 742996dcce Deleting a checklist deletes its items.
Every way to delete a checklist - the card's checklist menu, DELETE
/api/boards/:boardId/cards/:cardId/checklists/:checklistId, the
removeChecklist rule action and the durable rule plans - removed only the
checklist document; Checklists.before.remove writes the removeChecklist
activity and nothing else. Every item stayed in the database with a
checklistId pointing nowhere: invisible on the card and never cleaned
up. One production database had 2335 of them from 690 deleted
checklists.

Checklists.after.remove now removes the checklist's items with
ChecklistItems.direct.removeAsync, as deleting a card already removes
its children: no activity per item, the checklist's own removeChecklist
activity records the deletion. Items orphaned before this are left for
the administrator to decide. From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6816.
2026-10-27 15:06:32 +02:00
Lauri Ojansivu f89c8f8030 Add card: [Text] at the start of a title becomes a label only when turned on in Member Settings.
Since #3986 a new card's title starting with [Text] followed by more
text lost the prefix and got the label Text, created on the board when
it had none - for everybody, without a hint. Titles pasted from ticket
systems, mails and monitoring tools ([Follower] #123456 - ...,
[EXTERNAL] ...) were changed and filled the board with unwanted labels.

It is a Member Settings switch now, Turn [label] at the start of a new
card title into a label (profile.titleBracketLabel), off by default: a
title is kept as typed. Who wants #3986's behaviour turns it on. Both
texts are translated in every language; low confidence for the
constructed and rarest ones (tlh, vo and some 35 others).

Thanks to jullbo and xet7 !

Fixes #6810.
2026-10-27 15:04:31 +02:00
Lauri Ojansivu 6f5aab875b Date popups: Save without picking a day saves today instead of Invalid date.
A date popup of a card without that date opens the calendar on today,
marks today (#6751) and names it in the calendar's heading - so today
is what the form shows. But the form's date field was empty, and Save
without clicking a day answered Invalid date.

The form starts from today now (draftDate in
imports/lib/datePickerTime.js): the date field, the calendar's selected
day and its heading agree, and Save saves today at the popup's default
time (17:00 for a due date, #1502). A card's own date is kept as before,
and an emptied date field is still refused.

Thanks to brlin-tw and xet7 !

Fixes #6809.
2026-10-27 14:56:21 +02:00
Lauri Ojansivu 4bd6a7f088 Dragging a checklist item on one's own board is no longer reported as a cross-board attack.
Dragging a checklist item within its card sets cardId and checklistId
to where the item already is. The ChecklistBleed deny rule
(GHSA-gv8h-5p3p-6hx7) checked only that the destination board was
writable, so for a comment-only member or a Worker - whose own board is
one they cannot write to - an ordinary drag tripped the high-severity
canary checklist-item.cross-board-move and named them in Admin Panel /
Problems / Security as if they had attacked another board.

The board the document is already on is no destination now. That board
is the one of its SOURCE CARD, which the allow rule decides on - not
the document's own denormalized boardId, as the reporter's patch read
it: the insert hooks kept a boardId the client sent, so an item on the
attacker's card could carry a victim board's id and would have passed
as staying while moved onto a victim card. Those hooks (checklists,
checklist items, and now text notes on the server) set boardId from the
card always. A move to another unwritable board, and an unknown
destination, are still denied and still reported.

Thanks to Raymond-Voogt and xet7 !

Fixes #6813.
2026-10-27 14:52:06 +02:00
Lauri Ojansivu 2dfdf77c2b Tests: the Card Settings drag and drop spec turns drag handles on and off for its user by user.id.
The fixture's user has an id, not an _id: setHandles(user._id, ...) of
8224852de1 matched no user, so the handle half of the spec never had
drag handles on.

Thanks to xet7 !
2026-10-27 14:52:06 +02:00
Lauri Ojansivu f6fd560817 Upgrade: boards with a Show on card custom field keep it on their minicards.
Custom fields on minicards became a Board Settings / Card switch,
allowsCustomFieldsOnMinicard (1d4ffd4080), opt-in, with false written
into every board. Before it, a custom field marked Show on card was
always drawn on the minicard, so after the upgrade those fields
disappeared from every minicard until a board admin found the switch -
reported as custom fields being visible only after opening the card.

A startup upgrade step, custom-fields-minicard-on, turns the switch on
for each board that has at least one custom field with showOnCard: that
board's own earlier choice. Boards without one keep the opt-in default.
Once ever, like cover-on-card-on: it records itself in _wekan_migration,
so a board turned off afterwards stays off on later upgrades. The test
db's distinct() returns array elements one by one, as MongoDB does.
From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6812.
2026-10-27 14:48:19 +02:00
Lauri Ojansivu a24da2a039 Minicard: only the custom fields that have a value, as before.
Until the minicard's sections were rewritten (1d4ffd4080), a minicard
drew a custom field marked Show on card only when it had a value - the
2019 check of e2a374f0aa. The rewrite dropped it, so every field a card
carries was drawn, also the ones added empty by Automatically on card /
Always on card: boards with several such fields got a stack of empty
rows on every minicard.

hasCustomFieldValue() in models/lib/customFieldsWD.js: null, undefined,
an empty or blank string and an empty multi-select are empty; 0 and an
explicitly unticked checkbox are values (the old check hid 0). The
minicard draws a field only when it has one; the opened card is
unchanged. From the reporter's patch.

Thanks to Raymond-Voogt and xet7 !

Fixes #6811.
2026-10-27 14:47:25 +02:00
Lauri Ojansivu d05e4db865 CHANGELOG: Card Settings drag and drop lists, and #6808.
Thanks to brlin-tw and xet7 !
2026-10-27 10:05:27 +02:00
Lauri Ojansivu 7ed3ca7c5c A card opened from its link stays open when the connection drops and comes back while the page loads.
Behind a reverse proxy the DDP connection can drop and come back while a
card's link is still loading (a WebSocket upgrade the proxy refuses,
SockJS falling back). Every subscription re-runs on the way back, one
answers before the resumed login with nothing, and the card leaves
Minimongo for a moment. #3114's close-on-removal took that moment for a
deletion: it closed the card and went to the board's address.

The window now waits while the subscription, the connection or the login
is settling, and when the card looks gone with everything settled, asks
the server (cards.openCardAvailable, the 'card' publication's own rule,
now one function in server/lib/openCardAccess.js) and closes only on its
no, if Minimongo still agrees. A card really deleted, archived or moved
elsewhere still closes. Not reproduced here: no proxy in this
environment; the browser test drops and restores the connection.

Thanks to brlin-tw and xet7 !

Fixes #6808.
2026-10-27 10:05:01 +02:00
Lauri Ojansivu 8224852de1 Card Settings: a Show on Card and a Show on Minicard list per section, reordered by drag and drop again.
On 2026-10-25 each row became one line with both sides' cells and order
arrows. But the lines could follow only one side's order, and the
number beside an arrow was the field's place among ALL of that side's
fields: an arrow often changed the number and nothing visibly moved,
the minicard's arrows never moved a line, and an arrow that did nothing
was not disabled. Drag and drop, which the arrows had replaced, had been
there since 2026-10-02.

Each section has the two lists again, each in its own order and
reordered by drag and drop: Show on Card - with On this card and Show
empty beside each row - and Show on Minicard. A row is dragged by its
handle (drag handles on) or its icon or name; Up and Down on a focused
one move it one row in the list. A dropped row goes right after the row
now above it in the side's whole order, through placeKey, so a pinned
header stays first and a section moves as one. The arrows and their
numbers are gone.

Thanks to xet7 !
2026-10-27 10:05:01 +02:00
Lauri Ojansivu 8d61402f5b CHANGELOG: Ask for a reason and the select buttons in Card Settings.
Thanks to xet7 !
2026-10-27 09:17:10 +02:00
Lauri Ojansivu 37b3c8f599 Card Settings: Ask for a reason is the same checkbox as Show empty fields, and Select all / Select none are Draggable's buttons.
Ask for a reason when moving cards was a Font Awesome square a few
pixels off Show empty fields above it, its classes toggled by hand. It
is the same animated checkbox now, in the same place, read reactively
from the board and set by a board admin. Select all and Select none
under the columns' headings were links; they are the Draggable table's
buttons - white text on the theme's colour - in its container.

Thanks to xet7 !
2026-10-27 09:17:00 +02:00
Lauri Ojansivu 9b65aed902 Merge remote-tracking branch 'origin/main' 2026-10-27 08:30:03 +02:00
Lauri Ojansivu 37e9253c93 Merge pull request #6807 from ocdtrekkie/sandstorm-org
Switch Sandstorm build to new fork
2026-10-27 08:23:40 +02:00
Jacob Weisz 3f718859bc Switch Sandstorm build to new fork 2026-10-27 01:09:09 -05:00
Lauri Ojansivu 831b2bd8b5 CHANGELOG: Card Settings Select all and Select none, aligned columns.
Thanks to xet7 !
2026-10-26 16:40:33 +02:00
Lauri Ojansivu c8a15b514f Card Settings: Select all and Select none per column, headings aligned with their cells, labels whole.
Under each column heading of every pane - On this card, Show on
Minicard, Show on Card, Show empty - Select all and Select none set
that column for the open section's rows at once, each the way its own
checkbox would: On this card in one write of the card, Show empty in
one write of the board's list, the Scrum rows in one Scrum write (one
by one, each would carry the same revision), the others through their
own checkbox's handler. Who may change the column gets the buttons; a
personal row (Labels text, List title) is left as it is.

The headings drifted left of their cells, further each column, and the
labels were cut: the heading line had the smaller font, and the columns
are in em, so its columns were narrower than the rows'. The font is on
the headings now, every heading and cell starts at its column's start,
and the popup is up to 1200px wide instead of 900px, so a row's whole
label fits where the window has the room.

Thanks to xet7 !
2026-10-26 16:40:14 +02:00
Lauri Ojansivu c687fe5577 CHANGELOG: Card Settings opens on Card, Show empty fields, Show empty.
Thanks to jullbo and xet7 !
2026-10-26 16:23:01 +02:00
Lauri Ojansivu 1aa9df6075 Card Settings: opens on Card, Show empty fields for the board, and the Show empty column.
Card Settings opens on its topmost entry, Card - also from + Add,
which opened on the first section of fields. Under the Card pane's
title, Show empty fields (the board's showEmptyCardFields, off by
default): ticked, every card of the board shows all its fields, the
empty ones too, and the Show empty columns are hidden - by CSS only, so
their ticks stay saved and come back as they were when it is unticked.
On this card still hides a field from one card, and Show on Card off
still hides it from all.

The Always show column is called Show empty; it is stored as before, in
cardFieldsAlwaysShown. Both headings are translated in every language.

Thanks to jullbo and xet7 !

Fixes #6806.
2026-10-26 16:22:29 +02:00
Lauri Ojansivu cca9ce36cf CHANGELOG: Always show in Board Settings / Card.
Thanks to jullbo and xet7 !
2026-10-26 15:33:30 +02:00
Lauri Ojansivu b54525abbe Board Settings / Card: Always show, for every field - the description, attachments and comments by default.
Show when empty became Always show and covers every per-card field,
the description, attachments and comments included: those were always
shown by a fixed list, and are now the default ticks of the board's
cardFieldsAlwaysShown. A board that never set it shows them as before;
unticked, they are shown when the card has a description, an
attachment or a comment, like any other field. The board's list is
exactly what is ticked once set, an empty list too. On this card still
hides an always-shown field from one card.

The first name, cardFieldsShownEmpty, was never pushed, so there is
nothing to migrate. tests/cardFieldsAlwaysShown6806.test.cjs pins the
default, unticking the four, the storage and the column; the browser
test unticks Comments.

Thanks to jullbo and xet7 !

Fixes #6806.
2026-10-26 15:33:12 +02:00
Lauri Ojansivu 5056790c05 CHANGELOG: Show when empty in Board Settings / Card, and the heading colour guard.
Thanks to jullbo and xet7 !
2026-10-26 15:06:52 +02:00
Lauri Ojansivu 80b936223f Board Settings / Card: Show when empty, the fields every card of the board shows even while empty.
Since an opened card shows only the fields it has data in, a board where
nearly every card gets labels, a due date and a checklist needed + Add,
the section and On this card for each new card before a field could be
filled in. The new Show when empty column of Board Settings / Card (and
of + Add, the same popup) is the board's list, cardFieldsShownEmpty: a
ticked field counts like data and is on every card from the start. On
this card still hides it from one card, Show on Card off still hides it
from all, and the four always-shown fields have no such checkbox.

It is a board admin's setting, stored normalised by
setCardFieldShownEmpty, and translated in every language.
tests/cardFieldsShownEmpty6806.test.cjs pins the rule, its storage, the
column, and fails on any caller of isShown or cardFieldsAfter that does
not pass the board; card-add-fields.e2e.js ticks it in the browser.

Thanks to jullbo and xet7 !

Fixes #6806.
2026-10-26 15:05:10 +02:00
Lauri Ojansivu 21b2da9ad2 Tests: the title heading colour guard reads the heading block itself.
It read from the LAST comment in minicard.css and cardDetails.css to the
end of the file, so it passed only while the heading rules' neighbours
happened to set no colour - and failed once the Card kind check mark,
appended at the end of cardDetails.css with its own colour, came after
them. It reads the heading block, from its comment to the blank line
after it, and still fails on a colour there.

Thanks to xet7 !
2026-10-26 15:05:09 +02:00
github-actions[bot] 43c8606548 Bump versions for v12.65 2026-10-25 16:13:18 +00:00
Lauri Ojansivu e5ed5d16dc Prepare v12.65 release 2026-10-25 18:12:06 +02:00
Lauri Ojansivu 213af1c1bd CHANGELOG: Custom Fields forms in the pane, Card kind shows the selected one, #6805.
Thanks to mimZD and xet7 !
2026-10-25 18:02:36 +02:00
Lauri Ojansivu ca53019d96 Card Settings / Scrum settings and Card kind popup: the selected card kind is shown.
A check mark at the end of its line, bold, and aria-checked in a radiogroup.
Before, the selected kind only had an is-active class that nothing styled.

Thanks to xet7 !
2026-10-25 18:02:14 +02:00
Lauri Ojansivu 37024aa9f9 Right-to-left: the comment form's avatar is inside the card.
The comment form keeps 38px free for the writer's avatar, which is drawn
at the reading start (inset-inline-start: -38px) - but the room was a
physical LEFT margin, so on a right-to-left page (Persian in the report)
the avatar was drawn on the right, outside the card. The room is
margin-inline now, and the other margins of the comments' layout are
logical too, so all of it mirrors with the page.
tests/commentFormRtl6805.test.cjs pins the avatar to the room kept for it
and fails on any left/right-only margin or padding in comments.css.

Thanks to mimZD and xet7 !

Fixes #6805.
2026-10-25 17:59:52 +02:00
Lauri Ojansivu f33987931f Card Settings / Custom Fields: Create Field and Link fields to another card are forms in the pane, not popups.
Under Edit custom fields, the Custom Fields pane has the two forms its
buttons opened: Create Field (also from Board Settings, for a board
admin) and Link fields to another card. In the pane the list's own two
buttons are not repeated; a new field saved there empties the form for
the next one, and Card Settings stays open. Opened as their own popup -
the card's Custom Fields hamburger - they step back when saved, as
before (client/lib/popupBack.js, now for a form drawn by more than one
popup, and telling whether it stepped back).

Thanks to xet7 !
2026-10-25 17:59:52 +02:00
Lauri Ojansivu 80d0b2c47e CHANGELOG: Card Settings pane columns line up.
Thanks to xet7 !
2026-10-25 17:34:54 +02:00
Lauri Ojansivu ede0d68b27 Card Settings: the pane's columns line up, and an inline form's buttons no longer push the rows aside.
Each line of a section pane is its own grid, and its side columns were
sized by their content, so no two lines - nor the heading line - agreed.
They have fixed widths now (On this card 7em, Show on Minicard and Show
on Card 9em each), a longer heading wraps in its column. The inline forms
float their Save left and Delete right (Change spent time, ...); the form
keeps those floats inside it (flow-root), where they had pushed the
heading and the first row aside.

Thanks to xet7 !
2026-10-25 17:34:52 +02:00
Lauri Ojansivu a433d20189 CHANGELOG: Card Settings sections and + Add as one popup, card actions in their sections, the card menu's order.
Thanks to xet7 !
2026-10-25 17:31:02 +02:00
Lauri Ojansivu c69f3fe25a Card Settings: Card is the first entry, Subtasks and Planning Poker are sections of their own, and each section's settings are forms in its pane.
- The first entry is Card, with the card icon: Ask for a reason when
  moving cards, the Draggable table, and the card's own rows that were
  the last entry (Card number, Collapse, Parent card, ...).
- Subtasks is its own section under Checklists, Planning Poker its own
  under Voting.
- Each section's settings are the form itself under the pane's title,
  not a link to a popup: Subtask settings, Edit custom fields, Change
  spent time, Card kind, Edit voting, Edit Planning Poker. Saved in the
  pane Card Settings stays open; the section hamburgers of the opened card
  still open the same forms as their own popup, which step back when saved
  (client/lib/popupBack.js).

Thanks to xet7 !
2026-10-25 17:30:37 +02:00
Lauri Ojansivu 65621f44cc Card actions menu in the maintainer's order; Card Settings: Time under Date, Edit voting and Edit Planning Poker in the Voting pane, Select all centred under Draggable.
- The opened card's actions menu is one list, in this order: Move Card,
  Copy Card, Save as Template, Copy Template to Many Cards, Card
  recurrence, Move to Top, Move to Bottom, Watch, Star | Set color,
  Drawing | Copy link to clipboard, Link to board, QR code, History,
  Export, Import, Move Card to Archive, More. Each item keeps the
  permission it had; the two copies of the menu for the two kinds of
  member are one list (cardMenuWrites). tests/cardActionsOrder.test.cjs.
- Card Settings: Time is under Date in the left menu.
- Card Settings / Voting: Edit voting and Edit Planning Poker are the forms
  themselves, under the pane's title; saved there, Card Settings stays
  open, saved in their own popup (the section hamburgers) they step back.
- Select all / Unselect all are centred under their Draggable heading.

Thanks to xet7 !
2026-10-25 17:13:36 +02:00
Lauri Ojansivu 4f20f24201 Card actions in their sections: Filter: Subtasks, Edit voting, Edit Planning Poker, Change spent time and Card kind in the opened card's section hamburgers and in Card Settings' panes.
- The five leave the card's actions menu. On the opened card each is in
  the hamburger of its section: Subtasks (Filter: Subtasks, and the
  subtask settings), Voting, Planning Poker, Spent time, and a new Scrum
  settings heading over the Scrum fields the card shows (Card kind).
  Voting, Planning Poker and Spent time show their heading when ticked
  "On this card" before there is anything in them, so the hamburger can
  fill them in.
- Card Settings: everything in a section's hamburger is also at the top
  of that section's pane, under its title - the card's actions when it
  is opened from a card, Subtask settings also from Board Settings.
  tests/cardSectionActions.test.cjs fails for a hamburger that is not.
- Filter: Subtasks is one function, client/lib/filterSubtasks.js.
- Card Settings: no select-all rows for Show on Minicard and Show on Card
  in its first entry, where there is nothing under them; the popup is
  high enough for every entry of its menu.

Docs: docs/Features/Cards/Card-Fields.md.

Thanks to xet7 !
2026-10-25 16:55:03 +02:00
Lauri Ojansivu 20e2273c07 Board Settings / Card and the opened card's "+ Add" are one popup: sections in the Admin Panel's left menu, each row with Show on Minicard, Show on Card and On this card.
- Board Settings / Card is the Admin Panel's left menu and pane. The menu
  lists the sections (models/lib/cardSettingsSections.js): Card Settings
  first (Ask for a reason when moving cards, the select-all of each side,
  Draggable), then Date, Members, Labels, Description, Checklists,
  Attachments, Time, Custom Fields, Voting, Scrum settings, Comments and
  Card. Every row of the table is in exactly one section.
- The pane shows the open section's rows, one line each, in the reading
  order On this card, Show on Minicard, Show on Card, the field: each side
  cell the board-wide checkbox, the field's position on that side and its
  up and down arrows (the Up and Down keys on a focused arrow too; the
  maintainer's choice, replacing the drag and drop of the two long lists).
  Every checkbox is the same animated one, and only the one just clicked
  animates.
- The opened card's "+ Add" opens this same popup for the card, with the
  "On this card" column; the separate Add popup is gone. A reader who may
  not change the board sees its checkboxes disabled.
- A row's name leaves out what its section says: under Scrum settings the
  rows are Sprint, Release, ..., not "Scrum settings: Sprint".
- The popup moves by its title bar, maximizes, is 900px wide (popupOffset
  and popup.css agree) and high enough for its menu.
- A maximized card's comments, at the top of the right column, have no
  separator line above them.
- New string "On this card" in every language. Low confidence: ace, ak, ay,
  bi, bm, bua, chr, csb, dz, ff, fj, gn, gv, iu, kl, ks, kw, lld, nah, nap,
  nso, om, pap, rn, rup, rw, sc, scn, se, sm, ss, st, tig, tlh, to, tpi, ts,
  ve, ve-PP, vo, wal, wo, wa-RR, zgh, nd, xh, zu, ja-HI.

Docs: docs/Features/Cards/Card-Fields.md and
docs/Features/Board-Sidebar/Board-Settings/Card.md. The Playwright specs of
the old two-list layout are rewritten for the sections and arrows.

Thanks to xet7 !
2026-10-25 16:12:19 +02:00
Lauri Ojansivu cef5ddc506 CHANGELOG: cover on by default, new drawing as cover, + Add popup, maximized card comments.
Thanks to xet7 !
2026-10-25 15:13:55 +02:00
Lauri Ojansivu 5d80b07bed Opened card: when maximized, the comments section is in the right column.
The comments are their own template, cardCommentsSection, in the left
column of the opened card and, on a maximized card, at the top of the
right one beside the fields; the right column is as wide as the left.

Thanks to xet7 !
2026-10-25 15:13:44 +02:00
Lauri Ojansivu c4f528159b Opened card: cover on by default, a new drawing is the cover, one line under + Add, no empty Similar cards or Custom Fields; the + Add popup moves, maximizes and fits its menu and labels.
- The cover image on the opened card is on by default. The schema default
  is true, and the once-ever upgrade step cover-on-card-on turns it on for
  the boards created while it was opt-in; a board turned off later stays
  off, also on an install that had no board yet.
- A new drawing becomes the card's cover; saving an edit leaves the cover.
- One separator line under "+ Add": the Sort group's gate read the board
  itself, not the card's view, and drew its rule over fields the card does
  not show.
- Similar cards: asked when the card opens, and the section is there only
  when there are some, with how many.
- Custom Fields: only on a card that has a custom field value or ticked
  Custom Fields in "+ Add" - which is now how a card with none gets the
  section and its menu.
- The "+ Add" popup moves by its title bar (and by the drag handle, shown
  only while drag handles are on, for every movable popup), has maximize /
  minimize beside its close button, is wide enough for the pane's labels
  and high enough for every menu entry, and a pane opened shows its ticks
  without animating them - only the clicked one animates.

Thanks to xet7 !
2026-10-25 15:11:03 +02:00
Lauri Ojansivu 0782bda994 CHANGELOG: Opened card shows only the fields it has data in, with + Add for the rest; archive one release to old-CHANGELOG.
Thanks to xet7 !
2026-10-25 14:00:30 +02:00
Lauri Ojansivu 6fe0a7ef94 Opened card shows only the fields it has data in, and "+ Add" under the cover adds the rest, in the Admin Panel's left menu and pane.
An opened card shows only the fields it has data in, plus its
description, attachments and comments, empty or not, and its cover when
it has one. "+ Add", under the cover, opens a popup with the Admin
Panel's own left menu and pane title: the sections (Date, Members,
Labels, Description, Checklists, Attachments, Time, Custom Fields,
Voting, Scrum settings, Comments, Card) on the left, the open section's
fields on the right - the items of Board Settings / Card - each with a
checkbox. Ticking an empty field shows it on this card; unticking a field
hides it from this card, also when it has data. The card keeps its
choices in shownFields and hiddenFields, which accept only field keys.

Board Settings / Card still decides what a card CAN show: a field turned
off there is on no card and not offered. The opened card and its section
templates get their own currentBoard, the card's view of the board with
the flags of the fields it does not show reading false, so the existing
template checks apply without being edited; the minicard is unchanged.

The design is docs/Features/Cards/Card-Fields.md. Seeded e2e cards show
every field, as the specs written before expect.

Thanks to xet7 !
2026-10-25 14:00:05 +02:00
Lauri Ojansivu 40dbb829c9 CHANGELOG: Drawing custom colors kept with the account when signed in.
Thanks to xet7 !
2026-10-25 12:10:05 +02:00
Lauri Ojansivu 91c12ebcce Drawing: custom colors are kept with the user's account when signed in, and in the browser on a public board without signing in.
The custom colors added to the Drawing popup's palette are stored in the
user's profile (profile.drawingColors, through the new setDrawingColors
method), so they follow the user to every browser. The method takes only
#rrggbb codes, each once, at most 16, and refuses a caller who is not
signed in; the schema holds only #rrggbb. Someone drawing on a public board
without signing in keeps them in this browser, as before.

Thanks to xet7 !
2026-10-25 12:09:52 +02:00
Lauri Ojansivu c486518f03 CHANGELOG: Drawing on the whole window with zoom and any color; Romanian, Tamazight and Urdu in their own languages.
Thanks to xet7 !
2026-10-25 08:56:46 +02:00
Lauri Ojansivu d5d027a6f2 Drawing: the whole window to draw on, growing from its corner, with zoom, any color and Save above it; Drawing in Add Card's Search / Template / Drawing / More options.
The drawing area takes all the room the popup has: a new drawing is as
wide and high as that room, instead of a fixed 800 x 500 in the middle.
A handle at its bottom-right corner (bottom-left right to left) makes it
larger, or smaller without cutting off a line, and the magnifier buttons
zoom it, scrolling when it no longer fits. A drawing keeps its size when
saved; older ones are 800 x 500. Save is above the drawing, at the end of
the tools.

Colors: clicking a color opens a color wheel, a brightness slider and the
HTML color code; custom colors are added to the palette, kept in this
browser; the eyedropper picks from the screen where the browser can
(EyeDropper API) and from the drawing elsewhere. Any #rrggbb is a color,
nothing else, so a color never becomes anything else in the SVG.

Add Card: Drawing is in the row Search / Template / Drawing / More
options, for one card, and opens the new card's drawing on the whole
window, saved as an SVG attachment.

New strings in every language. Low confidence: tlh, vo, la, chr, iu, kl,
nah, ay, qu, gn, zgh, tig, wal, om, bo, dz, bua, sah, cv, csb, szl, hsb,
rup, lld, fur, nap, scn, sc, co, ve-CC, ve-PP, wa, gv, kw, rm, ak, bm, ee,
ff, lg, rn, rw, ss, ts, ve, nso, tn, st, ny, sn, nd, wo, fj, sm, to, bi,
tpi, haw, pap, ks, uz-AR, ace, wa-RR, ary, yi, wuu-Hans.

Thanks to xet7 !
2026-10-25 08:56:20 +02:00
Lauri Ojansivu d3dead539b Translations: Romanian, Tamazight and Urdu in their own languages.
Romanian (ro 158 values, ro-RO 179): about 160 values each were Italian
("Sposta in cima", "rosso", "ti ha inviato un invito"), with some Spanish
("disparador", "carril") and French ("Connexion", "Erreur"); also cedilla
ş ţ replaced with comma-below ș ț.

Standard Moroccan Tamazight (zgh, 344 values): about 100 values were
Arabic and about 230 Kabyle or French in Latin letters; all now in
Tifinagh. Values already in Tifinagh, including reviewed ones with a
Latin product name, are kept as they were.

Urdu (ur, 2389 values): most of the file was English spelled letter by
letter in Urdu script ("منتقل کو توپ" for Move to Top - توپ is a cannon,
"سیزے" for Size) or English word order word for word; rewritten in
natural Urdu, with the stray "اردو:" prefixes removed.

Search keywords in these four locales are one word of letters, as the
query parser reads them; the underscore, space and zero-width non-joiner
forms could not be typed as operators.

Every placeholder matches English. tests/wrongLanguageCorrections20261025
pins each language, the placeholders and the search keywords; the guards
that pinned the old Arabic and Kabyle values say why they changed.

Low confidence: much of zgh (technical and statistics terms), Urdu
technical and Blockly terms, Romanian search keywords (culoar, termen,
verificare, câmppersonalizat).

Thanks to xet7 !
2026-10-25 08:27:47 +02:00
Lauri Ojansivu da4a0cf013 CHANGELOG: The Drawing lasso: select lines to move, resize, group and ungroup, and change their layer.
Thanks to mimZD and xet7 !
2026-10-25 07:40:48 +02:00
Lauri Ojansivu 1c26e1bcd0 Drawing: a lasso that selects lines to move, resize, group and ungroup, and to bring to front or back or one layer up or down.
A loop drawn with the Lasso selects every line wholly inside it, and a
grouped line brings its whole group. The selection moves together from
inside its box, stopping at the edge; resizes from the handle at its corner,
line widths to the nearest allowed one; groups and ungroups, the group kept
with the saved drawing; and goes to the front, the back, or one layer up or
down. Undo now steps back through every change, a move or a resize being one
step.

New strings in every language. Low confidence: ace, ak, ay, bm, bo, dz, br,
bua, chr, csb, cv, ee, ff, fj, fur, gn, gv, haw, hsb, ig, iu, kl, ks, kw, lg,
lld, nah, nap, nd, ss, ts, ve, ve-CC, ve-PP, qu, rm, rup, sah, sc, scn, se,
szl, tig, tlh, vo, wa, wa-RR, wal, wo, yi, zgh, uz-AR, ja-HI, om, sm, to, tpi,
bi, mi.

Thanks to xet7 !
2026-10-25 07:40:16 +02:00
Lauri Ojansivu 0390468e07 CHANGELOG: An SVG of any size is sanitized whole, streaming.
Thanks to xet7 !
2026-10-25 07:24:13 +02:00
Lauri Ojansivu e748559838 SVG of any size is sanitized whole, streaming: on upload, on validation and on every serve.
A streaming tokenizer (models/lib/svgSanitizer.js) replaces the regular
expressions that read an SVG whole: it removes script, foreignObject,
iframe/object/embed, SMIL set/animate, event handlers, javascript:/vbscript:
URIs after decoding character references, dangerous CSS, DOCTYPE/ENTITY XML
loops and processing instructions, wherever they are and however the file is
split into chunks, with bounded memory. UTF-16 is decoded and checked.

Upload sanitizing streams to a temporary file and renames it; validation
checks the whole SVG instead of rejecting any over its 1 MB scan; serving
pipes through the sanitizer with no 8 MB fallback to an unsanitized
download.

Thanks to xet7 !
2026-10-25 07:23:31 +02:00
Lauri Ojansivu 9e9e1e271f CHANGELOG: Drawing with a pen from Add Card and Add attachment, saved as SVG; the movable, maximizable Drawing popup; SVG as an image again.
Thanks to mimZD and xet7 !
2026-10-25 06:33:03 +02:00
Lauri Ojansivu 54726264d3 Drawing: a new drawing from Add Card and Add attachment, saved as SVG; an eraser and palm rejection.
#6802 asks for handwriting with a pen, saved as a card attachment, and
started while the card is created:

- Add attachment / Drawing, and Add Card's Drawing button (shown while
  the composer adds ONE card: the card is added, opened, and the drawing
  starts on it), open a NEW drawing on the whole window - the Drawing
  popup maximized, without the card's earlier drawings - saved as an SVG
  attachment of the card, its lines kept on the card for editing.
- The eraser takes away every line it touches (measured to the line's
  segments, its width included); Clear now has the trash icon.
- Palm rejection: once a pen has drawn, a touch does not, and while one
  pointer draws no other starts a line. A failed pointer capture no
  longer stops a line from starting.
- A Drawing popup opened from the card menu, which replaces the menu in
  the same popup, now opens maximized when it was left maximized.

The openers live in client/lib/newDrawing.js, which touches no template:
importing the popup's own module from attachments.js ran it before
cardDetails.jade had defined the template, and the client did not start.
Eraser is in every language. docs/Features/Cards/Drawing.md describes
it all. tests/cardDrawingPen.test.cjs tests the eraser and palm rejection
as pure functions and pins the new flows;
tests/playwright/specs/card-drawing-pen.e2e.js draws and saves both new
drawings to SVG attachments, erases a line and rejects a palm in a
browser.

Thanks to mimZD and xet7 !

Fixes #6802.
2026-10-25 06:32:28 +02:00
Lauri Ojansivu 0afb1336db SVG is uploaded and shown as an image: an attachment, a cover, a board background, an avatar.
Since JVN#74210258 (stored XSS) an SVG upload was refused and any stored
SVG was served as a download, so an SVG could not be a card's picture.
Every uploaded SVG is already sanitized on the server before it is
stored - no script, event handler, javascript: URI, foreignObject or
DOCTYPE/ENTITY XML loop (models/lib/sanitizeUploadedFile.js) - so the
refusal goes, for attachments and avatars.

Serving it inline is made safe in one place, server/lib/svgResponse.js,
used by every route that sends file bytes (universalFileServer.js for
attachments, avatars and legacy files, avatarServer.js,
legacyAttachments.js and httpStream.js): the WHOLE file is sanitized
again before it is sent, whatever is stored, and sent as image/svg+xml
under a Content-Security-Policy with default-src 'none' and sandbox, no
script, X-Frame-Options DENY and nosniff, so even opened directly as a
page it runs nothing. A download is sanitized too; a range request gets
the whole file; an SVG over 8 MB stays a sandboxed download. A file whose
active markup is removed on the way out is logged to Admin Panel /
Problems like the stream sanitizer's, and one removed at upload trips
the existing sanitize.dangerous-content canary.

tests/svgServing.test.cjs runs the real sanitizer on a crafted SVG
(script, onload, javascript:, DOCTYPE/ENTITY, foreignObject/iframe) and
checks the body and headers, the download, the oversized case, and that
no file-serving route in server/routes or httpStream.js sends bytes
without the SVG path, and that nothing else sends image/svg+xml;
tests/playwright/specs/svg-attachment.e2e.js uploads one, checks what is
served, that it shows in the card without running anything, and makes
it the cover and the board background.

Thanks to xet7 !
2026-10-25 06:32:25 +02:00
Lauri Ojansivu 4bea319239 Board Settings / Card calls the Whiteboard row Drawing, in its existing translations.
The row shows or hides the Drawing popup's Open as whiteboard button, and
is now named after the card menu's Drawing it belongs to, with that
entry's translations (cardDrawingPopup-title) and its pencil icon.

Thanks to mimZD and xet7 !
2026-10-25 05:41:29 +02:00
Lauri Ojansivu c11976a2e1 The Drawing popup moves by its title bar or a drag handle, and maximizes.
The card's Drawing popup stayed where it opened. Like the opened card it
is now dragged by its title bar or the drag handle beside the title, and
has the opened card's maximize / minimize button: maximized it fills the
window, and minimizing puts it back where it was dragged or resized to; a
maximized popup is not dragged; the drawing keeps its 800 x 500 shape so
the pen lands under the pointer; a popup left maximized opens maximized
again. The shared popup offers both by name (MOVABLE_POPUPS,
MAXIMIZABLE_POPUPS in popup.js), Board View settings staying movable.

The three new strings, Drag to move, Maximize and Minimize, are in every
language. tests/popupMoveMaximize.test.cjs pins the allow-lists, the
header controls, the restored place, the drawing's shape and the strings
in every locale; tests/playwright/specs/popup-drawing-window.e2e.js drags,
maximizes and minimizes it in a browser.

Thanks to mimZD and xet7 !
2026-10-25 05:40:46 +02:00
Lauri Ojansivu c6dfb95002 CHANGELOG: what FerretDB's changes since v12.64 mean for WeKan, and the SQLite conversion at first start.
The oldest release moves to old-CHANGELOG/2026/10.md to stay under the
size budget.

Thanks to xet7 !
2026-10-25 04:48:27 +02:00
Lauri Ojansivu 2c90b26401 CHANGELOG: the speed test report escapes a backslash before the pipe (CodeQL #593-#595).
Thanks to GitHub CodeQL and xet7 !
2026-10-25 04:29:16 +02:00
Lauri Ojansivu a90576bafa The speed test report escapes a backslash before the pipe in a table cell.
CodeQL js/incomplete-sanitization, alerts #593, #594 and #595:
docs/Databases/FerretDB/1/SpeedTest/report.cjs escaped `|` as `\|` in
Markdown table cells but left a backslash alone, so a value ending in a
backslash turned the added one into a literal backslash and its pipe
split the cell. One mdCell() helper escapes the backslash first, then the
pipe, and turns a line break into a space; it is also used for the
database's name and the query's title, which were not escaped at all.
tests/speedTestReportCells.test.cjs checks the escaping, that no value
can add a column to its row, and that nothing in the tree escapes a pipe
without the backslash. The report is generated from local speed test
results at build time, so there is no attempt to record in Admin Panel /
Problems.

Thanks to GitHub CodeQL and xet7 !
2026-10-25 04:29:05 +02:00
Lauri Ojansivu 4c8531589b CHANGELOG: conformance checks the $or, $elemMatch and long $in shapes.
Thanks to xet7 !
2026-10-25 03:59:43 +02:00
Lauri Ojansivu 18878f6f29 Conformance: $or with $elemMatch, an $or of array fields in an $and, and a long $in.
The shapes FerretDB's PostgreSQL and MySQL backends now answer in the
database - the visible boards and the due cards of the dueCards
publication, and an $in longer than the 32 values MySQL compares with
JSON functions - with negatives that must stay in Go (an operator in an
$elemMatch, a regex branch, a missing field, a long list of numbers).
SQLite, PostgreSQL, MySQL and MariaDB answer all 115 cases as MongoDB 8.

Thanks to xet7 !
2026-10-25 03:59:35 +02:00
Lauri Ojansivu ac8889e61c CHANGELOG: @azure/msal-node 7.1.1 by an npm override, for GHSA-xcx6-jx8p-x3h5.
Thanks to xet7 !
2026-10-25 03:27:26 +03:00
Lauri Ojansivu 31332bfed4 Override @azure/msal-node to 7.1.1 for GHSA-xcx6-jx8p-x3h5.
npm audit reported two high severity issues: @azure/msal-node
>= 5.0.0-beta.0, < 7.1.0 may disclose authorization code flow parameters
to an attacker-controlled endpoint, and @azure/identity depends on it.
It reaches WeKan through @tweedegolf/sab-adapter-azure-blob, whose
@azure/identity (4.14.0, the newest, and 4.15.0-beta.1) asks for
msal-node ^6.0.0, so `npm audit fix --force` reported "up to date" and
changed nothing. package.json overrides it to ^7.1.1. Its only breaking
change since 6 removed responseMode from acquireTokenInteractive, which
@azure/identity never passes; DefaultAzureCredential, ManagedIdentity and
ClientSecretCredential construct, and a token request reaches Microsoft
Entra and fails as an authentication error for a made-up tenant. WeKan's
Azure Blob storage uses DefaultAzureCredential, not the authorization
code flow. The lock file also catches up with the installed
@azure/identity 4.14.0. npm audit: 0 vulnerabilities.
tests/msalNodeAdvisory.test.cjs pins the override and that no locked copy
is in the vulnerable range.

Thanks to xet7 !
2026-10-25 03:27:14 +03:00
Lauri Ojansivu d906733f60 CHANGELOG: notification failures say which check failed; the oldest release moves to old-CHANGELOG/2026/10.md to stay under the size budget.
Thanks to xet7 !
2026-10-25 03:18:51 +03:00
Lauri Ojansivu bf7520f972 Activity notification failures say which check failed.
"Activity notification delivery incomplete; pending intent retained" was
logged for nearly every activity - 1,080 times in one test-server run,
and on a production server one line after another - and never said why:
the .catch() dropped the error. It and the recovery and both cleanup
lines now carry the failure's code (activity-notification-intent-invalid,
sync-busy, ...). Only a code is logged as it is; any other error gives its
name and numeric or short code, never its message, which may carry an
address or a card title. tests/notificationFailureReason.test.cjs pins
that, and fails when a notification failure line in server/ drops its
reason again.

Thanks to xet7 !
2026-10-25 03:18:15 +03:00
Lauri Ojansivu d5395fbb4c CHANGELOG: dependency updates, database test changes and the new speed test results in Upcoming.
Thanks to dependabot and xet7 !
2026-10-25 01:55:14 +03:00
Lauri Ojansivu e81d17da80 Speed test results: SQLite, PostgreSQL, MySQL and MariaDB with FerretDB b6c494e9.
Rerun of the four FerretDB v1 backends at twice the production data,
with the MySQL/MariaDB index reads and VIRTUAL index columns, the
multikey side tables for MySQL/MariaDB, the PostgreSQL GIN membership
index and SQLite sorting in the database. MongoDB 6-9 and FerretDB 2
keep their rows from the same directory. Every database returned the
same data as MongoDB.

All Boards count went from 8-9 s to 159 ms on PostgreSQL and from
9.4 s to 1.1 s on MySQL; adding the 44 indexes took 172 s on MySQL and
901 s on MariaDB instead of 20 and 40 minutes. This build is before
the fixes for SQLite's People page (the database sort read its key
three times per row), MySQL/MariaDB All Boards with a starred board, and
Due Cards' $and on PostgreSQL, MySQL and MariaDB, which a rerun now in
progress measures.

Thanks to xet7 !
2026-10-25 01:53:58 +03:00
Lauri Ojansivu 56292d0a0e Merge remote-tracking branch 'origin/main' 2026-10-25 01:53:15 +03:00
Lauri Ojansivu 494cbee598 Conformance: an equality and an $in on an indexed field inside an $and.
WeKan's due cards query wraps its board ids in an $and, which FerretDB's
PostgreSQL, MySQL and MariaDB backends now push down branch by branch;
both shapes answer as on MongoDB 8 on every backend.

Thanks to xet7 !
2026-10-25 01:47:24 +03:00
Lauri Ojansivu 47dcbb5dd2 Merge pull request #6804 from wekan/dependabot/npm_and_yarn/npm_and_yarn-2f01a27508
Bump the npm_and_yarn group across 1 directory with 2 updates
2026-10-25 01:25:50 +03:00
Lauri Ojansivu 353f01a03a Merge pull request #6803 from wekan/dependabot/github_actions/actions/setup-python-7.1.0
Bump actions/setup-python from 7.0.0 to 7.1.0
2026-10-25 01:25:35 +03:00
dependabot[bot] 164aaa03c9 Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [fast-uri](https://github.com/fastify/fast-uri) and [undici](https://github.com/nodejs/undici).


Updates `fast-uri` from 3.1.8 to 3.1.9
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9)

Updates `undici` from 7.30.0 to 7.30.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.30.0...v7.30.1)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 7.30.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-24 22:08:48 +00:00
dependabot[bot] e2efc8ebf9 Bump actions/setup-python from 7.0.0 to 7.1.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/5fda3b95a4ea91299a34e894583c3862153e4b97...6d4fd22b20465ccc0f7354476588e8df7c9ca0e0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-24 20:33:17 +00:00
Lauri Ojansivu 9d2b64273b db-conformance.sh keeps the containers of a run that is still going.
At start it removed every wekan-conformance-db-* container and network as
left behind by an interrupted run, including those of a run still in
progress: a speed test beside a conformance run lost its MySQL in the
middle of Due Cards ("connection refused") and its MariaDB while
loading, and both results were lost. Containers and networks now carry
the process id of their run as a label, and one whose run is alive (a
db-conformance.sh process) is kept; one without the label, of a run that
ended, or whose pid now belongs to something else is removed as before.
tests/dbConformanceStaleContainers.test.cjs runs the script's functions
against a fake docker.

Thanks to xet7 !
2026-10-24 21:17:45 +03:00
Lauri Ojansivu e9fa734853 Conformance: indexed equality, array membership and mixed-type sorting.
Three cases for what FerretDB now answers from indexes or in the
database: an equality or $in on an indexed field (inside an array, a
value differing only by case, special characters, numbers, a long
string), membership in an indexed array of documents in every shape the
array can take and after writes, and a sort over every simple type with
ties, null and missing fields, with and without a limit, then with an
array and a document among the values. SQLite, PostgreSQL, MySQL and
MariaDB answer all 114 cases as MongoDB 8 does.

Thanks to xet7 !
2026-10-24 21:17:13 +03:00
Lauri Ojansivu b3233e5665 CHANGELOG: the import page guard and the Tigre plural.
Thanks to mimZD and xet7 !
2026-10-24 21:03:36 +03:00
Lauri Ojansivu 52bcd74efa Tigre: the import size message uses the attested plural for files.
import-too-large used the provisional ፈይላት, which
tests/tigreCorpusAttestedPlurals.test.cjs rejects in favour of the
corpus-attested ፋይላት used everywhere else in the Tigre file.

Thanks to xet7 !
2026-10-24 21:03:19 +03:00
Lauri Ojansivu 132fecbfd8 The import page guard knows the checked tick is placed physically now.
tests/importPage.test.cjs still required the checked box's 17px label gap
to be margin-inline-end and no margin-right anywhere, which #6786 changed
on purpose: the tick is placed with left/margin-right so it stays on its
square right-to-left, and a [dir="rtl"] rule moves the label gap to the
left. The guard now pins that, and the unchecked box's logical gap; the
comments in import.css name the tick's physical offsets.

Thanks to mimZD and xet7 !
2026-10-24 21:03:19 +03:00
Lauri Ojansivu ae2bb47d3a CHANGELOG: resizing a list follows the pointer right-to-left (#6801).
Thanks to mimZD and xet7 !
2026-10-24 20:37:07 +03:00
Lauri Ojansivu 80208a7141 Resizing a list follows the pointer in right-to-left languages too.
The resize handle is on the list's inline end, its left edge in a
right-to-left language, but the width was the start width plus the
pointer's movement: dragging the handle out of the list narrowed it,
and dragging it in widened it. The movement is now counted with the
list's own direction, as the board sidebar and the left menus already
do. tests/rtlListResize.test.cjs pins it and requires every width
resizer to flip right-to-left; a Playwright spec drags the handle out
and back in, in both directions.

Thanks to mimZD and xet7 !

Fixes #6801.
2026-10-24 20:36:44 +03:00
Lauri Ojansivu 16b9b14898 Merge remote-tracking branch 'origin/main' 2026-10-24 20:30:17 +03:00
Lauri Ojansivu 6d68e9a6ea CHANGELOG: a checked checkbox sits on its own square right-to-left (#6786).
Thanks to mimZD and xet7 !
2026-10-24 20:13:32 +03:00
Lauri Ojansivu 6bb926af19 A checked checkbox sits on its own square in right-to-left languages too.
#6786's first fix drew the tick with physical borders, so it is a check mark
and not "<" right-to-left. But where it sits was still logical: the tick is a
box narrowed from 13px to 7px and rotated about its bottom-right corner, placed
with inset-inline-start: -3px and margin-inline-end: 6px. A right-to-left page
started the narrowed box at the square's RIGHT edge and pushed it further
right, so the tick sat beside its box - "out of box" in the issue's Admin Panel
screenshots. Measured in Chromium, the right-to-left tick spanned 303-323px
beside a square at 291-308px.

The shared tick, the Admin Panel's, the sidebar's and the Clean themes' are
placed with left and margin-right now: 291-311px, on the square, at the same
place as left-to-right, and 7 + 4 + 6 = 17px keeps the label where it was. The
import page, whose rows add a gap before the label, keeps that gap on the
reading side with a right-to-left rule. Toggle switches and the board list's
corner box, which are not this tick, still mirror.

tests/rtlCheckMark.test.cjs fails if any rule that draws a tick places it with
a logical offset or margin; tests/rtl.test.js allows physical margins and a
left offset only inside checked-state rules; the Playwright spec measures that
the tick is on its square in both directions and the label does not move.

Thanks to mimZD and xet7 !

Fixes #6786.
2026-10-24 20:12:50 +03:00
github-actions[bot] 9a4719b67a Bump versions for v12.64 2026-10-24 16:32:58 +00:00
Lauri Ojansivu 76f8d3d3a6 Prepare v12.64 release 2026-10-24 19:31:47 +03:00
Lauri Ojansivu bd049075e1 CHANGELOG: database speed test, Due Cards, FerretDB 2 compose file.
Thanks to xet7 !
2026-10-24 18:19:21 +03:00
Lauri Ojansivu 70485c38df A database speed test: the common WeKan queries on every database, twice a production WeKan of data.
docs/Databases/FerretDB/1/SpeedTest/speedtest.sh (build.sh and build.bat ->
Tests -> Database speed test) is releases/db-conformance.sh with
WEKAN_DB_SUITE=speedtest: the same FerretDB built from source and the same
databases started the same way - SQLite, PostgreSQL, MySQL and MariaDB through
FerretDB v1, MongoDB 6, 7, 8 and 9, and now FerretDB 2 with PostgreSQL and
DocumentDB.

data.cjs generates, deterministically, twice the users, boards, lists,
swimlanes and cards of a production instance (615,070 cards), skewed the way
real ones are; indexes.cjs creates the indexes WeKan creates, read from its
ensureIndex calls; queries.cjs asks 18 common WeKan queries as WeKan's own code
builds them; report.cjs writes Results.md - a row per database, fastest at the
top, a column per query - and fails when a database answered or stored
anything differently from MongoDB. README.md describes it, what it found, what
was improved and what could still be.

db-conformance.sh: WEKAN_DB_BACKENDS runs only the named databases (to repeat
one into an earlier run's WEKAN_LOGDIR), containers are removed with their
volumes, SQLite's data is under .tools/tmp, and FerretDB 2 is a backend of the
speed test, and of the conformance run with WEKAN_CONFORMANCE_FERRETDB2=1. The
conformance catalogue gains a case of writes by string _id and _ids differing
only by case, which found that FerretDB on MariaDB 12.3 lost them.

tests/dbSpeedTest.test.cjs checks the data, the index list, the queries, the
report's order and that a difference fails it, the wiring and the Due Cards
board scope.

Thanks to xet7 !
2026-10-24 18:18:15 +03:00
Lauri Ojansivu f9e1e90131 The FerretDB 2 compose file starts with the current DocumentDB image.
ghcr.io/ferretdb/postgres-documentdb:17 installs pg_cron, which may only be
created in the database cron.database_name names - postgres. With POSTGRES_DB
ferretdb the DocumentDB extension failed to install and the server never
started. POSTGRES_DB and FerretDB's URL use postgres now.

Thanks to xet7 !
2026-10-24 18:17:14 +03:00
Lauri Ojansivu c64b3a3b1d Due Cards reads only the ids and members of the boards it may show, and asks for a plain boardId list.
The publication fetched every board the user may see - every public board of
the instance among them, whole - to read its _id and members. It asks for those
two fields only. And with no assigned-only board, the usual case, its board
scope is { boardId: { $in } } instead of an $or with an empty second branch,
which MongoDB and FerretDB both answer from the { boardId, archived, type,
dueAt } index. A board where the user is assigned-only still shows only the
user's assigned cards. On the database speed test (615,070 cards) Due Cards on
FerretDB + SQLite went from 34 s to 12.8 s, with FerretDB's own changes.

Thanks to xet7 !
2026-10-24 18:17:14 +03:00
Lauri Ojansivu f1e447dd36 The Sandstorm package-import guard escapes the whole package name in its pattern.
tests/incompleteEscaping.test.cjs refuses a RegExp built from a value that only some metacharacters were escaped in; the guard escaped only '-'.

Thanks to xet7 !
2026-10-24 18:17:14 +03:00
Lauri Ojansivu e76b05b12c CHANGELOG: prebuilt SQLite-only FerretDB on every platform.
Moves the oldest release in CHANGELOG.md to old-CHANGELOG/2026/10.md to keep it under the size budget.

Thanks to xet7 !
2026-10-23 21:23:08 +03:00
Lauri Ojansivu 89da0e5be7 Every platform runs the prebuilt SQLite-only ferretdb-sqlite binary from wekan/FerretDB.
wekan/FerretDB now releases ferretdb-sqlite-<arch> beside ferretdb-<arch>: the
same FerretDB with only the SQLite handler, about 5 MB smaller, and SQLite is
all a WeKan bundle, image, snap or .spk runs. releases/ferretdb-asset.sh is the
one place that chooses: the newest release that has ferretdb-sqlite-<arch>,
and, until one does, the full ferretdb-<arch> of the newest release, which
has the SQLite handler too. It says in the log which one it took and why,
downloads it, verifies it against its .sha256sum, and returns FERRET_ASSET,
FERRET_TAG, FERRET_URL and FERRET_SHA for the provenance table (also through
$GITHUB_ENV, cached per job so the check and embed steps agree).

Every FerretDB download goes through it: the amd64, arm64, win64, win-arm64,
win32, mac-arm64, mac-x64 and extra-architecture jobs of release-all.yml (the
Docker images, snap, AppImage, Flatpak, single-file and macOS/Windows
packages are made from those bundles), repack-bundle-for-arch.sh for
release-all-missing.yml, bsd.yml, check-arch-binaries.sh,
build-release-bundle.sh and test-download-urls.sh. require-binaries.sh checks
the resume-login floor against the release the helper chose. The provenance
rows record that release and URL instead of /latest/.

No release workflow builds FerretDB: sandstorm-src/build-deps.sh takes the
prebuilt ferretdb-sqlite-amd64 by default (FERRETDB_SOURCE=download), and
sandstorm.yml and release-all.yml's build-sandstorm no longer set
FERRETDB_SOURCE=build; building from source stays a local option. The default
docker-compose.yml downloads ferretdb-sqlite-<arch> (FERRETDB_BINARY, falling
back to ferretdb-<arch> for a release without it); the PostgreSQL, MySQL,
MariaDB and SAP HANA compose files keep the full binary.

tests/ferretdbSqliteAsset.test.cjs drives the helper against a fake GitHub
(newest, older and no release with the SQLite-only binary, .exe names, a
pinned tag, FERRETDB_VARIANT=full, checksum match and mismatch, a missing
platform, bad names) and checks that every download site uses it, that no
workflow builds FerretDB and that only the SQLite compose file takes the
SQLite-only binary. The tests that pinned the old URLs follow the helper.

Thanks to xet7 !
2026-10-23 21:22:36 +03:00
Lauri Ojansivu 2da820f8ff CHANGELOG: smaller Sandstorm .spk, collection2 before the models.
Thanks to xet7 !
2026-10-23 19:35:00 +03:00
Lauri Ojansivu e413cd4720 The Sandstorm .spk leaves out every login method but Sandstorm's, and FerretDB's other database handlers.
A grain has one login (Sandstorm's), one database (FerretDB on SQLite) and one
mailbox (the grain's own address), but the .spk carried every login package
WeKan has and a FerretDB with the PostgreSQL, MySQL and SAP HANA handlers.

releases/sandstorm-slim-spk.sh, used by sandstorm.yml, release-all.yml's
build-sandstorm job and build.sh's "Make the .spk package", builds with the
package profile of releases/sandstorm-slim.mjs - accounts-password, 2FA,
passwordless, lockout, OAuth2/OIDC, CAS, LDAP, SAML and the seven social
logins removed from a copy of .meteor/packages for that one build, then
restored - trims the bundle for a linux/x64 grain, verifies none of those
packages got in and packs with spk pack (meteor-spk pack rebuilds and would
undo the trim). sandstorm-src/build-deps.sh builds FerretDB from source with
ferretdb_no_postgresql,ferretdb_no_mysql.

Under Sandstorm the Admin Panel hides the E-mail, Login, Locked users, SAML,
LDAP, OAuth2, CAS, Header login, OAuth providers and Passwordless panes,
Attachments / Database migration, the Email-to-card IMAP and webhook settings,
the New user button and the password fields; a hidden pane opened by URL lands
on the first visible one. POST /users/login and /users/register answer 404
sandstorm-sso-only, database migration is refused and the IMAP/webhook
settings are not stored. Without accounts-password the server creates
Accounts.emailTemplates with the package's defaults and marks useraccounts
initialised, which otherwise stopped the first grain.

Against wekan-12.63-sandstorm.spk, built from the same source: 181,307,364
instead of 185,271,476 bytes (-3.8 MiB, -2.1 %), unpacked 26.7 MiB smaller.
Docker, Snap and the bundles build from the full package list, and a test fails
if that list is slimmed. Tested on a local Sandstorm 0.308: a grain starts, a
board, a list and a card are created, and the removed options are not shown.
tests/sandstormSlim.test.cjs and tests/playwright/specs/sandstorm-slim.e2e.js
pin it. Design: docs/Platforms/FOSS/Container/Sandstorm/Smaller-wekan-spk.md.

Thanks to xet7 !
2026-10-23 19:34:34 +03:00
Lauri Ojansivu 2699f4deea Collection2 is loaded before the models, so every write is cleaned and given its defaults.
Collection2's attachSchema lives in the package's lazy main module, which only
aldeed:schema-index loads, through an asynchronous Collection2.load(). Nothing
waited for it: the models attached their schemas with whichever attachSchema
existed first - collection2's, or the bare shim in imports/collectionHelpers.js,
which stores the schema and never cleans a write. The full build happened to
load collection2 first. A build with fewer packages (the Sandstorm .spk) reached
the models sooner, so a new board's swimlane was stored without archived, sort
and type, and the board opened empty. server/main.js now loads
meteor/aldeed:collection2/main before the models in every build.

Thanks to xet7 !
2026-10-23 19:34:12 +03:00
github-actions[bot] a463937cce Bump versions for v12.63 2026-10-22 17:04:47 +00:00
Lauri Ojansivu e1a75d0311 Prepare v12.63 release 2026-10-22 20:00:47 +03:00
Lauri Ojansivu 819ee786ce Merge remote-tracking branch 'origin/main' 2026-10-22 20:00:13 +03:00
Lauri Ojansivu be0d8d9b9c CHANGELOG: copying or linking a card carries only its new board's custom field values (#6800).
Thanks to Raymond-Voogt and xet7 !
2026-10-22 19:58:32 +03:00
Lauri Ojansivu 63ebdb9072 Copying or linking a card carries only the custom field values of its new board.
A card made from a card template, linked from another board, or copied - and
a subtask copied with it - took its whole customFields array along: values of
another board's fields, of deleted fields, and entries with no field id. The
admin-only field guard could not match those to a field of the new card's
board, refused the whole card and recorded AdminFieldBleed against the user.

copy(), its subtask copies and link() now keep only the values of fields
defined on the destination board (models/lib/customFieldsForBoard.cjs), read
from the collection rather than the cache, just before the insert. An
admin-only value of the destination board is kept, so the guard still decides
it. tests/copyForeignCustomFields.test.cjs fails on the code before the fix,
and fails on any card cloned from another and inserted without the filter.

Thanks to Raymond-Voogt and xet7 !

Fixes #6800,
2026-10-22 19:58:16 +03:00
Lauri Ojansivu 6f9454656e The regex-injection guard knows the database connection-lost pattern.
models/lib/databaseErrors.js builds the connection-lost rule's pattern from
CONNECTION_LOST.source (#6799), a constant regex of the same file, so no
caller value is spliced into it. Listed with that reason, as CARD_URL_RE.source
already is.

Thanks to xet7 !
2026-10-22 19:58:16 +03:00
github-actions[bot] c9827049a1 Bump versions for v12.62 2026-10-22 16:33:46 +00:00
Lauri Ojansivu b43c36890c Prepare v12.62 release 2026-10-22 19:32:34 +03:00
Lauri Ojansivu f4c67c6df9 CHANGELOG: refused edits no longer disable accounts (#6798), database pod restart is not a crash (#6799).
Thanks to Raymond-Voogt and xet7 !
2026-10-22 19:31:08 +03:00
Lauri Ojansivu f37204b1c3 A MongoDB pod restarting is a lost connection, not a critical crash.
In Kubernetes a restarting MongoDB pod's DNS name does not resolve for a
few seconds, and the driver throws "getaddrinfo ENOTFOUND
wekan-mongodb-0...". No rule in models/lib/databaseErrors.js knew it, so
it was not transient, and server/lib/uptimeWatch.js recorded every
unhandled rejection as a critical CrashBleed although 00processErrors.js
kept WeKan running: nine "crashes" while WeKan reconnected by itself.

connection-lost now matches getaddrinfo ENOTFOUND and EAI_AGAIN, but only
for the DATABASE's own host - from the caller, or from MONGO_URL and
MONGO_OPLOG_URL (every host of a replica set, mongodb+srv too) - so a
webhook or mail host that does not resolve is still not a database
problem. The crash recorder skips what isTransientDatabaseError already
handles, for both uncaught exceptions and unhandled rejections.

tests/databaseDnsRestart.test.cjs parses connection strings, runs both
guards' decision on the database host and on other hosts, and runs the
crash recorder's own listeners: a database coming back records nothing,
a real crash and a foreign DNS failure still do. It fails on the code
before this commit.

Thanks to Raymond-Voogt and xet7 !

Fixes #6799,
2026-10-22 19:30:41 +03:00
Lauri Ojansivu 425b4ddea7 An ordinary refused edit on one's own board no longer disables the account.
Accounts were disabled for members working on their own board; one
instance lost four in three days, each user then refused at login 81
times.

- A Worker may move a card (#3189) and tick a checklist item (#3307),
  but canUpdateCard and the checklist item rule asked
  canEditCardOrLinkedCard first, which recorded authz.linked-write (high,
  blocked) before the Worker carve-out allowed the change, and that
  record disables the account. Both now ask without recording first, and
  record only a change that is still refused after the carve-outs.
- A read-only or comment-only member whose screen still offered an edit
  (a role changed while the board was open) was refused with the same
  record. authz.linked-write keeps being recorded and the write keeps
  being refused, but it no longer disables the account (blocksAccount:
  false, as ssrf.webhook since #6782).

tests/workerNoSelfBlock.test.cjs runs canUpdateCard and the checklist
item rule from their own source: a Worker's move and tick are allowed
with nothing recorded, other refused edits are still recorded, the
catalog entry never blocks through blockOnSecurityEvent's own decision,
and no rule with a Worker carve-out records before it. It fails on the
code before this commit. boardRoleViews follows the two-step check.

Thanks to Raymond-Voogt and xet7 !

Fixes #6798,
2026-10-22 19:28:49 +03:00
Lauri Ojansivu 368b90d650 Merge remote-tracking branch 'origin/main' 2026-10-22 19:26:24 +03:00
Lauri Ojansivu e417720f8a CHANGELOG: decluttered card and Card Settings (#6796), list move history checksum (#6797).
Thanks to mueschel, Raymond-Voogt and xet7 !
2026-10-22 19:15:51 +03:00
Lauri Ojansivu 76f9d41bf4 A list move with no swimlane no longer fails its own history checksum.
Admin Panel / Problems / Security showed HistoryIntegrity (critical)
81,833 times. A list bound to no one swimlane has swimlaneId '', and the
legacy history append hashed the row with '' and then inserted it
through collection2's default clean, which removes empty strings, after
which the schema's defaultValue stored null: every such list move failed
its own checksum at every audit and was reported as tampering. The
stored-chain path already inserted with { removeEmptyStrings: false,
trimStrings: false }; the legacy path now does too.

Rows already stored that way verify: a null where '' was hashed is
accepted in the id and label fields (boardId, swimlaneId, listId, cardId,
group, batchId, restoredFromId, restoredByUserId) and nowhere else, so a
value replaced by null, a different value, or any change to content, user
or entity still fails.

tests/historyEmptyStringChecksum.test.cjs appends through a collection
that cleans like collection2, verifies already-cleaned rows, fails every
real change, and fails on any ChangeHistory insert in the tree that would
clean or trim; it fails on the code before this commit.

Thanks to Raymond-Voogt and xet7 !

Fixes #6797,
2026-10-22 19:15:36 +03:00
Lauri Ojansivu 2c520ef382 Declutter the opened card, attachments too, and fit Card Settings on the screen.
#6796: an opened card gave most of its height to controls with nothing in
them. Stickers, Location, Dependencies and List already have Show on Card
rows, and Date Format is in Member Settings; what was left:

- A field's add button sits on its title's line (Labels, Stickers,
  Location, the dates, Members, Assignee, Creator, Requested and Assigned
  By, Dependencies); what a field holds follows and wraps. Requested and
  Assigned By put their two buttons side by side.
- Checklists: the controls under the list share a line, and the bottom
  "+" is drawn once there is a checklist; before that it is the title
  line's.
- Attachments: the upload rules ("If filename is invalid, ...", size,
  types) are in the upload popup, not body text above every list. "+" is
  a small button. A file with nothing to preview gets a small file button
  that still opens the preview, instead of a 144x96 box with "txt" in it,
  and the name has a line of its own, so a long one is not hidden behind
  the buttons.
- Two new Board Settings / Card / Show on Card rows, Attachments: Preview
  and Attachments: Details (uploader and date), both on by default, built
  from existing translations.

Board Settings / Card is a centred panel as wide (up to 1800px) and tall
as the window, and each list spreads into as many 10em columns as fit,
read top to bottom: on 1920x1080 nothing is scrolled, on 1366x768 32px.

Its rows are dragged by the handle, the icon or the label when drag
handles are on, and by the icon or the label, with no handle drawn, when
they are off. With handles on only the handle dragged, and the rows were
locked to vertical moves, which the columns made unusable.

tests/declutterCard6796.test.cjs runs the tile decision and the popup
geometry, and checks the gates, the CSS and the drag wiring;
tests/playwright/specs/declutter-card-6796.e2e.js checks the add buttons
on their title's line, the compact attachment, the rules in the upload
popup, Attachments: Details, the new rows saving, no scroll at 1920x1080,
and a drag by label and handle with handles on and by label with them
off. cardSettingsCoverage and narrowWindowLayout follow the new rules.

Thanks to mueschel and xet7 !

Fixes #6796,
2026-10-22 19:15:36 +03:00
github-actions[bot] 3c88e5b13f Bump versions for v12.61 2026-10-22 12:37:41 +00:00
Lauri Ojansivu 8dbc79b89c Prepare v12.61 release 2026-10-22 15:36:25 +03:00
Lauri Ojansivu faa66aecc4 listSortIndexes test escapes every regular expression character of a name.
It escaped only . and $ when building a pattern from an identifier, which
tests/incompleteEscaping.test.cjs reports anywhere in the tree.

Thanks to xet7 !
2026-10-22 15:32:56 +03:00
Lauri Ojansivu 9273d3eb13 CHANGELOG: list sort indexes and wrapped connection errors in Admin Panel / Problems (#6794, #6795).
Thanks to Raymond-Voogt and xet7 !
2026-10-22 15:18:38 +03:00
Lauri Ojansivu a9e680d4b2 A lost database connection inside a validation error is a connection problem.
While MongoDB restarted, Admin Panel / Problems showed
"Exception while invoking method 'usersHeartbeat' ValidationError:
Failed validation Socket 'connect' timed out" as a schema bug, telling
the admin to look for an empty required field and report a WeKan bug.
collection2 wraps whatever failed during a write's validation in
"ValidationError: Failed validation", an unreachable database included;
the schema rule came first, and the driver's connect timeout matched no
rule at all. The same classification decides in 00startupResilience.js
and 00processErrors.js whether an error is transient, so it was fatal at
startup too.

schema-validation now yields when the same message carries a lost
connection, a timeout, a lock or too many connections. connection-lost
also knows the MongoDB driver's own errors: Socket '...' timed out,
MongoNetworkTimeoutError, MongoNetworkError, Server selection timed out,
MongoServerSelectionError, PoolClearedError, NotWritablePrimary and
"not primary" (not SQL's "not primary key").

tests/databaseErrorsWrappedCause.test.cjs runs both guards'
isTransientDatabaseError from their own source on the reported error,
puts every transient cause behind three wrappers and requires its own
rule each time, and keeps a real schema failure a fatal schema failure.

Thanks to Raymond-Voogt and xet7 !

Fixes #6795,
2026-10-22 15:18:30 +03:00
Lauri Ojansivu 058a2dcbce List sort indexes are made from key objects, not from "-modifiedAt" strings.
Admin Panel / Problems showed "Created the missing index "-modifiedAt" on
lists" at every start, from every pod. server/models/users.js passed the
list sort options ('-modifiedAt', 'title', ...) to ensureIndex as
strings: the driver read each as a field name, so lists got indexes on
fields named "-modifiedAt", "-title" and "-sort" (on no document, kept up
to date on every list write), and ensureIndex compared a string's JSON
with an index key object's, which never matched, so all six were
re-created and reported every time.

models/lib/indexKeys.cjs turns the sort options into one single-field
index per field ({ modifiedAt: 1 }, { title: 1 }, { sort: 1 }; one serves
both directions) and finds the stray single-field "-x" indexes, which
startup now drops; a compound index containing such a field is left
alone. ensureIndex turns a string key into the key it means, with a
warning, instead of indexing a field named after it.

tests/listSortIndexes.test.cjs uses the reporter's real index list, runs
ensureIndex itself against a fake collection (a string key creates the
real index once and reports it once), and fails on any ensureIndex call
in the tree whose key is not an index key object.

Thanks to Raymond-Voogt and xet7 !

Fixes #6794,
2026-10-22 15:16:50 +03:00
Lauri Ojansivu 660846dd3c Merge remote-tracking branch 'origin/main' 2026-10-22 15:06:31 +03:00
Lauri Ojansivu e1f8d93d70 CHANGELOG: newest npm dependencies, Blockly 13.4 toolbox search in every language, core26 snap Caddy, ca@valencia.
Thanks to translators and xet7 !
2026-10-22 06:37:58 +03:00
Lauri Ojansivu 6a37160bc8 A saved language with '@' resolves: ca@valencia shows Valencian, not English.
The Valencian file is ca@valencia.i18n.json, but its registry tag is
ca-valencia, and resolveTag and the browser-language matcher only treated
'_' as '-'. A profile saved as the file's name - which is what
tests/playwright/specs/rules-blocks.e2e.js does - resolved to nothing and
the page stayed in English; that case failed on the commit before the
npm updates too. Both now compare through one normalizeLanguageTag
('_' and '@' as '-'), exported from imports/i18n/browserLanguage.js.
tests/browserLanguageFallback.test.cjs uses that function rather than its
own copy of the old rule, checks ca@valencia, and requires every locale
FILE name to resolve to a registry language.

Thanks to xet7 !
2026-10-22 06:34:36 +03:00
Lauri Ojansivu 189adaee52 Update imapflow, mailparser, pdfjs-dist, isomorphic-git and pretty-ms, with runtime tests.
imapflow 2.1.2 -> 2.8.0 and mailparser 3.9.31 -> 3.12.2 (email to card):
bug fixes, and mailparser no longer turns a decoded display name into an
address. pdfjs-dist 6.2.108 -> 6.4.299 (attachment text search),
isomorphic-git 1.42.4 -> 1.44.3 (GitHub wiki import), pretty-ms 9 -> 10
(upload time estimate; its breaking changes are in colonNotation, which
WeKan does not use, and the output it shows is unchanged).

Each now has a test that RUNS it the way WeKan calls it, so the next
update that moves an API fails in seconds:
- imapflowMailparserRuntime: the poller's connect, mailbox lock, unseen
  search, fetchOne, simpleParser and \Seen flag against
  server/lib/tests/fakeImapServer.js; a wrong password is refused, and an
  encoded address in a display name stays a name.
- pdfjsTextRuntime: documentGif.js's legacy build, worker, fonts, CMaps
  and getTextContent on a PDF with known text; a non-PDF is refused.
- isomorphicGitRuntime: githubWiki.js's log, recursive readTree and
  readBlob on a two-commit repository; a missing object is an error.

npm audit reports nothing. The e2e run against a build with these:
rules-blocks (147 languages), attachments, both email specs, and the
security specs pass, except two rules-blocks cases that fail the same way
on a build of the commit before this one.

Thanks to developers of dependencies and xet7 !
2026-10-22 06:34:36 +03:00
Lauri Ojansivu 8a00898252 Update the small npm dependencies, and translate Blockly 13.4's toolbox search.
Within their ranges or pins: @aws-sdk/client-s3 and lib-storage 3.1155.0,
@fastify/busboy 3.2.4, the three @tweedegolf/sab-adapter-* 3.0.5/3.0.3,
blockly 13.4.0, dompurify 3.4.17, file-type 22.2.0, filesize 11.0.28,
i18next 26.5.4, meteor-node-stubs 1.2.32 (pbkdf2 is still 3.1.7 after
the postinstall patch), wasmagic 1.0.12, and for development
@rspack/core 2.2.9, @typescript-eslint/* 8.72.0 and puppeteer 25.15.0.
The lockfile is written by npm 12.2.1, the Dockerfile's. npm audit
reports nothing.

Blockly 13.4 adds a search box to the toolbox, with five new messages.
releases/translations/import-blockly.cjs added them to every locale, and
they are translated directly into all 230 locale files, from each file's
own words for "block" and "search"; %1 is kept in every count. The
importer also replaced Telugu's literal blockly-LOGIC_NULL "null" with a
word meaning "invalid"; it is "null" again, as
tests/vietnameseBulgarianCompletion.test.cjs requires. Corrected while
there: Polish "Szukaj blokad" (search for blockades) from Blockly's own
catalog is "Szukaj bloków", Arabic's singular count drops a stray "(1)",
and Odia's "no-results" had English mixed into it.

Low confidence, for a speaker to correct: ace, ak, an, ary, ay, bi, bm,
bo, bua, chr, co, csb, cv, dz, ee, ff, fj, gn, gv, ha, haw, hsb, ig, iu,
kl, kok, ks, kw, la, lg, lld, mg, mi, nah, nap, nd, nso, ny, om, pap, qu,
rn, rup, rw, sah, sc, se, sm, sn, so, ss, st, szl, tig, tlh, to, tpi,
ts, uz-AR, ve, ve-CC (Venetian), ve-PP (Veps), vo, wa, wa-RR (Waray),
wal, wo, xh, zgh and zu.

All 1988 node suites pass except tigreCorpusAttestedPlurals, which
fails on the commit before this one too.

Thanks to developers of dependencies, translators and xet7 !
2026-10-22 06:01:28 +03:00
Lauri Ojansivu 92001bcfd9 vmactions/openbsd-vm 1.4.8 -> 1.4.9, and actions/setup-node v7.1.0 everywhere.
openbsd-vm 1.4.9 adds OpenBSD 8.0 as the VM's default release and fixes a
non-root user's writes to the NFS share; the BSD bundles keep release 7.8
from releases/bsd/targets.json, so they are still built for 7.8. The three
setup-node@v6 steps in the disabled Playwright workflow are pinned to
v7.1.0 by commit, like the fifteen in the active workflows.

Thanks to xet7 !
2026-10-22 05:23:36 +03:00
Lauri Ojansivu 2ad5999bc7 Snap Caddy: core26 gets the fixed part, and both fall back to 2.11.7.
snapcraft-core26.yaml still had the caddy part snapcraft.yaml had before
v10.71: the version from api.github.com, which answers 403 to a busy CI
runner and, under snapcraft's pipefail, ended the part before its 2.8.4
fallback; a plain curl with no retry and no fallback for a CPU the newest
Caddy does not publish; and a default Caddyfile that proxied to 3000,
where WeKan does not listen when Caddy is on (it listens on
CADDY_BIND_PORT, 3001). Both snaps build from snap-src, so core26 now has
snapcraft.yaml's part unchanged, and both pin 2.11.7 as the fallback.

tests/releaseSnapArches.test.cjs read snapcraft.yaml only, which is how
the core26 copy was missed; it now checks every snapcraft*.yaml, and the
default Caddyfile's port. With the old core26 file it fails.

Thanks to xet7 !
2026-10-22 05:23:36 +03:00
github-actions[bot] 770acdcee6 Bump versions for v12.60 2026-10-22 02:01:02 +00:00
Lauri Ojansivu d30f1dffea Prepare v12.60 release 2026-10-22 04:59:51 +03:00
Lauri Ojansivu cf55c52c64 CHANGELOG: failed imports and exports block only attempts, crypto keys for dynamic-import and meteor shell (CodeQL 591).
Thanks to GitHub CodeQL and xet7 !
2026-10-22 04:57:17 +03:00
Lauri Ojansivu 6efd39a70e dynamic-import and meteor shell keys come from the crypto RNG.
GitHub CodeQL alert 591 (js/insecure-randomness, CWE-338), in the
dynamic-import fork: the key that lets a request read the SERVER's
dynamic modules (?key= on /__meteor__/dynamic-import/fetch) came from
Math.random(), whose xorshift128+ state can be recovered from earlier
outputs, and was compared with ===. meteor shell's connection key, which
runs code in the server process, was made and compared the same way.
Meteor 3.5.3 and 3.6-rc.0 both have this.

Both forks now use crypto.randomBytes (160 and 128 bits) and compare with
crypto.timingSafeEqual. tests/meteorForkRandomKeys.test.cjs runs both, and
fails on any Math.random() in the forks and on a key, secret, token,
nonce, salt, password or code made from Math.random() anywhere in the
tree. A wrong key is not recorded in Admin Panel / Problems: another
WeKan instance behind the same load balancer legitimately sends its own.

Thanks to GitHub CodeQL and xet7 !
2026-10-22 04:56:47 +03:00
Lauri Ojansivu 567f99227b A failed import or export disables an account only when it was an attempt.
secureTransfer recorded every throw as a high "blocked" event naming the
user, and that disables the account. sanitize-html 2.18.0 threw
RangeError on nested <iframe>, so a card planted with the payload locked
out the next member who exported the board; any library fault would do
the same. The transfer boundary now marks its own refusals
(TransferBoundaryError: a cycle, a non-plain object, a size limit), and
models/lib/transferFailure.cjs records only those as blocked - high, and
medium for a size limit as before. Anything else is ImportExportFailure:
detected, still naming whose import or export it was, and never blocking
an account.

tests/transferFailure.test.cjs drives real boundary refusals and a
sanitizer that throws through blockOnSecurityEvent's own decision; with
the old secureTransfer it fails. It also fails if the boundary throws a
plain Error or anything but secureTransfer calls it.

Thanks to xet7 !
2026-10-22 04:56:40 +03:00
Lauri Ojansivu 42965e133b Merge remote-tracking branch 'origin/main' 2026-10-22 04:49:09 +03:00
Lauri Ojansivu 182b6072d0 CHANGELOG: sanitize-html 2.18.1, nested iframes no longer crash imports and exports.
Thanks to xet7 !
2026-10-22 04:33:30 +03:00
Lauri Ojansivu d89bcab8e0 Tests: nested iframes no longer crash the import/export sanitizer.
sanitize-html 2.18.0 re-sanitized the fallback of a discarded <iframe>
with no recursion limit (GHSA-c9rx-x947-fp3h, CWE-674). WeKan's server
sanitizer discards every tag, so a few kilobytes of nested <iframe> threw
RangeError in secureTransfer, on every import and export - and
secureTransfer records a throw as a high blocked event naming the user,
which disables that account. A card planted with the payload locked out
the next member who exported the board. 96ed9ad202 moved to 2.18.1.

tests/sanitizeHtmlNestedIframe.test.cjs runs WeKan's own sanitizeInput
options on three nested-iframe payloads, checks the installed and locked
versions, and fails if anything else imports sanitize-html; with 2.18.0
it fails with RangeError. The Playwright spec exports and imports a board
holding the payload and checks that nobody is locked out.

Thanks to xet7 !
2026-10-22 04:33:04 +03:00
Lauri Ojansivu 96ed9ad202 Updated sanitize-html.
Thanks to developers of dependencies !
2026-10-22 04:29:10 +03:00
github-actions[bot] b5cccd7584 Bump versions for v12.59 2026-10-22 01:18:15 +00:00
Lauri Ojansivu e5d186c2f0 Prepare v12.59 release 2026-10-22 04:17:02 +03:00
Lauri Ojansivu 1624f44ffd CHANGELOG: Meteor 3.5.3's six security fixes on Meteor 3.6-rc.0.
Thanks to Meteor developers, Vext Labs and xet7 !
2026-10-22 04:12:43 +03:00
Lauri Ojansivu e0189bc53b Meteor 3.5.3's six security fixes on Meteor 3.6-rc.0.
Meteor published the same six fixes as 3.3.3, 3.4.2 and 3.5.3 on
2026-10-16. 3.6-rc.0, which WeKan runs, does not have them: its tag is
behind 3.5.3 by exactly the 15 security commits, and neither it nor
Meteor's devel branch has the fixed code. Going back to 3.5.3 would also
mean Node.js 24 and Rspack 1, so WeKan carries the fixes in packages/,
where a package overrides the release's own: 3.6-rc.0's allow-deny,
ddp-server, dynamic-import, facebook-oauth, google-oauth and shell-server
with Meteor's 3.5.3 patch applied unchanged, each marked with a _1 wrap
number. ddp-server leaves out uWebSockets.js, as Meteor's published
package does; it is only for the opt-in uws transport and came from a git
URL.

- DDP: a message named __proto__, toString or not a string, and an async
  handler that rejected, crashed the server.
- allow/deny: a top-level __proto__ in an update was missing from the field
  list the validators saw.
- dynamic import(): requests are bounded in size (413) and complexity
  (400).
- Google and Facebook direct-token login: a token issued to another
  application was accepted; identity now comes from the provider, and a
  Facebook verification error no longer carries the app secret.
- meteor shell: its history and compile cache are private.
- Deploy discovery is in meteor-tool, which an app cannot replace; WeKan is
  never deployed with meteor deploy, and a test keeps it so.

Refused attempts are in Admin Panel / Problems as Meteor DDP, Meteor
allow-deny, Meteor dynamic-import and Meteor OAuth, through a hook the
packages call (server/lib/meteorSecurityHook.js); none blocks an account.
An oversized import() body, an unknown ordinary DDP name and a network
failure are refused without a row, because ordinary use can reach them.

tests/meteor353SecurityFixes.test.cjs runs each fix's code cut out of the
fork and searches the tree for the shape of each fault; all of them fail on
the unfixed 3.6-rc.0 packages. It also fails when .meteor/release moves
on, because a fork would silently keep 3.6-rc.0's code.
tests/playwright/specs/meteor-353-security.e2e.js sends each attack to a
running WeKan built with the forks. The forked files are recorded in the
source audit baseline, and the suites that check WeKan's own methods and
subscriptions skip Meteor's package tests.

Thanks to Meteor developers, Vext Labs and xet7 !
2026-10-22 04:12:13 +03:00
Lauri Ojansivu 8a21e5639b CHANGELOG: every workflow's jobs that run after a cancel end on their own.
Thanks to xet7 !
2026-10-22 03:07:20 +03:00
Lauri Ojansivu 94e1749977 Every workflow's jobs that run after a cancel end on their own.
Cancel workflow does not cancel a job whose if: is still true, and always()
is true in a cancelled run. tests/releaseCancelSafeJobs.test.cjs checked
only release-all.yml; it now fails on a job on always() in any workflow that
is a reusable-workflow call or has no timeout of 60 minutes or less. Every
such job today (release-notes, done, and the AppImage, Flatpak and Mac
summaries) is bounded. node-patches had eleven five-hour resume stages
called this way, and Cancel workflow did not stop them.

Thanks to xet7 !
2026-10-22 03:07:12 +03:00
Lauri Ojansivu be0da24e21 Merge remote-tracking branch 'origin/main' 2026-10-22 02:50:27 +03:00
Lauri Ojansivu 6e1341a2a8 CHANGELOG: release recovery waits out GitHub API errors.
Thanks to xet7 !
2026-10-22 02:39:33 +03:00
Lauri Ojansivu e304e1948a Release recovery waits out GitHub API errors instead of dying on one.
The Recover job of "Release All missing" died after one HTTP 502 from the
GitHub API while `gh run watch --exit-status` listed the rerun's jobs, so
plan and every build after it were skipped. releases/retry-failed-release.py
now polls `gh run view`, retries up to ten API errors in a row, waits for
the new attempt rather than reading the old attempt's failure, and stops
only on the rerun's own conclusion. tests/retryFailedRelease.test.py pins
the 502 case, a failed rerun, a lasting outage and the stale attempt, and
that no `gh run watch` is left.

Thanks to xet7 !
2026-10-22 02:39:18 +03:00
github-actions[bot] 70ede2148a Bump versions for v12.58 2026-10-21 15:29:38 +00:00
Lauri Ojansivu aa26a6adaa Prepare v12.58 release 2026-10-21 18:28:16 +03:00
Lauri Ojansivu 914d95816a CHANGELOG: board to template, large Jira imports, comments follow a moved card, CodeQL alert 590.
Thanks to fabiosalles, GitHub CodeQL and xet7 !
2026-10-21 15:31:40 +03:00
Lauri Ojansivu 696b5b073f Comments, reactions, text notes and attachments follow a card moved to another board.
The board publication finds a board's comments, comment reactions, text
notes, checklists, checklist items and attachments by the board id each
row carries (#6480), but a card that changed board re-homed only its
checklists and checklist items. Its comments stayed published to the
board it left, and came back when the card was moved back. Attachments
followed only when Card.move() ran on the server, and the REST card
move, a hook-less write, re-homed nothing at all.

One list (models/lib/cardChildrenBoard.js) now names every such
collection, and one helper (server/lib/cardChildrenBoardSync.js)
re-homes them from the Cards.after.update hook, the REST move and the
durable rule move. The startup schema upgrade step card-children-boardid
moves rows that a move already left behind to their card's board.

Thanks to fabiosalles and xet7 !

Fixes #6793.
2026-10-21 15:30:41 +03:00
Lauri Ojansivu 14a43da557 Large Jira exports import, and an existing board can be saved as a board template.
From IRC: a Jira import of 25 issues worked and one of 858 failed. The
import boundary refused any document over 250,000 values, and a Jira
search export carries some 250 per issue even without comments - mostly
null custom fields, avatar URLs and status categories - so it stopped at
under 1,000 issues, about 6 MB. The refusal was a plain Error, which the
import page showed as "Internal server error". The cap is now four
million values; a file over it is refused as import-too-large, which the
page translates, with the limit as the reason. The import deadline was a
flat two minutes whatever the file held; it is now WEKAN_IMPORT_TIMEOUT_MS
plus half a second per record (models/lib/importDeadline.js), on the
server and in the import page's watchdog alike. The Jira creator also
collected the file's issue keys once per issue, which was quadratic.

"Is there a way to convert an existing board into a template?" There was
not: a board template could only be built inside the Templates board.
Board menu / Save as Template (saveBoardAsTemplate) copies the board as a
template board, private with the creator as its one member, and adds the
linked card in the user's Templates / Board Templates swimlane that makes
it a template. Board admins only, as Copy Board, and not offered on a
board that already is a template.

The new import-too-large string is translated into every language; the
low-confidence ones are ace, ak, an, ay, bm, bo, bua, chr, csb, cv, dz, ee,
ff, fj, fo, fur, gn, gv, haw, hsb, iu, kl, kok, ks, kw, lg, lld, nah, nap,
nd, rm, rup, sah, sc, scn, se, ss, tig, tlh, to, ve, ve-PP, ve-CC, vo, wa,
wa-RR, wal, wo, wuu-Hans and zgh.

Thanks to xet7 !

Fixes #6792.
2026-10-21 15:30:32 +03:00
Lauri Ojansivu e18794fa46 Invite-by-email e2e escapes the address completely, and a partial character class escape is found anywhere.
CodeQL alert 590 (js/incomplete-sanitization): the #6788 e2e put the
stored address into a $regex with only "." escaped, so a backslash in the
address could cancel the escape. It now uses escape-string-regexp, the
same module the server matches addresses with.

tests/incompleteEscaping.test.cjs only knew single-character patterns
and so missed `.replace(/[.]/g, '\\.')`. It now also reports a character
class without a backslash replaced by a backslash escape, and found this
line before it was fixed.

Thanks to GitHub CodeQL and xet7 !
2026-10-21 14:59:55 +03:00
Lauri Ojansivu 55f1ef7852 Merge pull request #6791 from wekan/dependabot/npm_and_yarn/npm_and_yarn-04be66158d
Bump markdown-it from 15.0.2 to 15.0.3 in the npm_and_yarn group across 1 directory
2026-10-21 14:46:25 +03:00
dependabot[bot] c927b901e8 Bump markdown-it
Bumps the npm_and_yarn group with 1 update in the / directory: [markdown-it](https://github.com/markdown-it/markdown-it).


Updates `markdown-it` from 15.0.2 to 15.0.3
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/15.0.2...15.0.3)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-version: 15.0.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 01:15:42 +00:00
github-actions[bot] 647cbd111f Bump versions for v12.57 2026-10-20 15:04:19 +00:00
Lauri Ojansivu 09db4b64fa Prepare v12.57 release 2026-10-20 18:03:05 +03:00
Lauri Ojansivu 4d8df83fdd Admin Panel validation audit lists the Retention pane.
tests/adminPanelDocsMatchMenu.test.cjs requires docs/Features/Admin-Panel/Validation.md
to inventory every live pane; Attachments / Retention (#6789) was missing
from it. It also crashed on the commit before this one, whose test run was
read by its last line only.

Thanks to xet7 !
2026-10-20 14:05:36 +03:00
Lauri Ojansivu 5c2fc294f3 Tests and docs follow this round's changes: Retention documented, a real regex escape, the snap guards.
The full node suite found three breakages in this round's own work:
- adminPanelDocsMatchMenu: Admin Panel / Attachments / Retention was in the
  product and not in docs/Features/Admin-Panel/Attachments/README.md. It has
  its section and its row in the URL table now.
- incompleteEscaping: the test from the #6788 patch hand-wrote a regex escape.
  It now uses the same escape-string-regexp package the server imports.
- releaseSnapArches: two guards read the Launchpad step's text. infra_failure()
  (the v12.56 Launchpad-outage retries) also reads snapcraft-wekan-*.txt
  before the loop, and the fixed `sleep 60` became `sleep "$pause"`. The
  guards now look for the log printing after the no-snap message, and for the
  pause variable, and say why. These two had been failing since that change.
  An earlier run's report that this suite was only "load-related" was wrong:
  the FAIL lines had been filtered out of the output that was read.

Thanks to xet7 !
2026-10-20 14:05:13 +03:00
Lauri Ojansivu 4b95f7f2cc CHANGELOG: attachment retention, backup status, settings on every platform and in the Admin Panel, member invite by email.
Thanks to Raymond-Voogt and xet7 !
2026-10-20 13:50:44 +03:00
Lauri Ojansivu 718fdb5dac Newest features' settings: on every release platform and in the Admin Panel, and kept that way.
An audit of every environment variable the code reads found 30 that are new
since v12.00. Eleven were already Admin Panel settings and on every platform
the release workflows build. Of the rest:

Now Admin Panel settings, through models/lib/authConfigCatalog.js (a value
there wins, an empty field falls back to the variable):
- Email to card limits, beside its own Admin Panel settings:
  INBOUND_IMAP_POLL_SECONDS, INBOUND_EMAIL_PER_ADDRESS_PER_HOUR,
  INBOUND_EMAIL_MAX_BYTES, INBOUND_EMAIL_MAX_ATTACHMENT_BYTES and
  EMAIL_VIEW_SENDS_PER_HOUR. They were module-level constants, read before
  the Admin Panel values loaded; they are read when used now.
  createRateLimiter takes a limit function, and a changed poll interval
  restarts the IMAP poller.
- ACCOUNTS_COOKIE_REFRESH_RATE_LIMIT in People / Login, like the login
  expiry beside it, applied from the next start.
- Windows SSO (WINDOWS_SSO_ENABLED, _DOMAINS, _ACTIVE_DIRECTORY,
  _MERGE_EXISTING_USERS) in People / Login. It was already read through
  authEnv but had no form. Being Windows-only, its catalog section names
  start-wekan.bat as its one platform.

Listed on every platform (Dockerfile, the docker-compose files, start-wekan.sh
and .bat, the snap config and help), where none of these had been: the
variables above, INBOUND_EMAIL_WEBHOOK_SECRET (its own Admin Panel page
already has it), and the per-process switches that stay environment only -
INBOUND_IMAP_DISABLED (one pod of a multi-pod deployment), WEKAN_DISK_MONITOR,
WEKAN_DISK_WATCH_PATHS, WEKAN_RAM_MONITOR and WEKAN_IMPORT_RUN_STALE_MS.
Left unlisted on purpose: NEXTCLOUD_EXAPP, NEXTCLOUD_URL and
WEKAN_NEXTCLOUD_PROXY_KEY (set by Nextcloud's AppAPI), SANDSTORM_MAILDIR (set
by Sandstorm). DEFAULT_AVATAR_URL keeps no Admin Panel UI, as decided in #824.

tests/envVarCoverage.test.cjs keeps it so. It fails for a variable that is in
no Admin Panel catalog, OWN_PANE or ENV_ONLY with its reason; the variables
read before v12.00 are a frozen LEGACY list that can only shrink. An
environment-only or own-page setting a user sets must be on every platform,
and stale entries fail. tests/authConfigCatalog.test.cjs honours a section's
platform list. tests/adminPanelNewSettings.test.cjs pins the reads and the
forms. tests/cookieRefreshRateLimit.test.cjs's guard now expects the Admin
Panel value, and says why. tests/playwright/specs/admin-new-env-settings.e2e.js
opens the forms.

Thanks to xet7 !
2026-10-20 13:50:10 +03:00
Lauri Ojansivu 06b7ce994a Backup status: every run recorded, failures and overdue backups shown, old local backups pruned - in Admin Panel too.
The result of an Admin Panel backup lived only in the memory of the server
process that ran it. After a restart, or on another pod of a multi-pod
deployment, the Backup pane said "idle", so nobody could see whether last
night's backup had worked. There was no history, no warning when a scheduled
backup failed or did not run, and local backups were never cleaned up.

- Every run, scheduled or "Backup now", is recorded in backupRuns: start and
  end, who or the schedule, scope, storage, status, size, duration and error.
  Records are kept 400 days. A run still "running" after 24 hours counts as
  failed, and a scheduled run skipped for another backup is recorded as failed.
- Admin Panel / Problems / Backup shows the last success (overdue when its
  period has passed), the last failure with its error, the next run and the
  last 30 runs. The same status is above the schedule in Attachments / Backup.
- Problems / Summary lists "Backup failed" and "Backup overdue". /metrics adds
  wekan_backup_last_success_timestamp_seconds,
  wekan_backup_last_run_timestamp_seconds, wekan_backup_last_run_success and
  wekan_backup_overdue for alerting.
- BACKUP_KEEP_DAYS: after a successful backup, local backups of the same scope
  older than N days are deleted. The newest is always kept, and only a
  directory inside the backup root holding a backup.zip is removed.

From the patch by Raymond-Voogt, and in Admin Panel as well:
- BACKUP_KEEP_DAYS is the catalog's backup section
  (models/lib/authConfigCatalog.js), overridable beside the schedule in
  Admin Panel / Attachments / Backup and resolved through authEnv at every
  backup.
- It is listed on every platform: the compose files, start-wekan.sh and .bat,
  the snap config and help, and the Dockerfile, which the patch's version
  had missed. Its platform hunks, written before #6789, were re-applied by
  hand.

tests/backupRuns.test.cjs (21): the run records, overdue and pruning against a
real directory, and the Admin Panel override, including that nothing reads the
variable around it. tests/authConfigCatalog.test.cjs checks every platform.
admin-attachment-retention.e2e.js checks the Backup pane shows the field.

Thanks to Raymond-Voogt and xet7 !

Fixes #6790.
2026-10-20 13:30:00 +03:00
Lauri Ojansivu dcf91e4471 Attachment retention: delete card attachments after N days, purge deleted ones after M days - in Admin Panel too.
Organisations that must not keep files forever (storage limits, GDPR data
minimisation) had only scripts working on MongoDB directly, which leave card
references behind, do not know which storage holds a file, and break on
upgrades. Soft-deleted attachments were also kept forever.

A nightly job (server/attachmentRetention.js, rules in
models/lib/attachmentRetention.js), off unless configured:
- ATTACHMENTS_RETENTION_DAYS soft-deletes card attachments older than N days
  through the card's own Delete (cover unset, activity, card history Restore).
- ATTACHMENTS_RETENTION_MODE: uploaded (default), or card-inactive, which also
  requires the card to have had no activity for as long.
- ATTACHMENTS_PURGE_DELETED_AFTER_DAYS removes attachments soft-deleted at
  least M days ago for good, file included, through Attachments.removeAsync
  so every storage backend is cleaned. The write re-checks deletedAt, so one
  restored meanwhile stays.
- ATTACHMENTS_RETENTION_DRY_RUN only logs; ATTACHMENTS_RETENTION_MAX_PER_RUN
  caps each step per night.
Never touched: attachments without a card, template boards and cards, and
attachments with an unknown upload date.

From the patch by Raymond-Voogt, and in Admin Panel as well:
- The five settings are a section of models/lib/authConfigCatalog.js.
  Admin Panel / Attachments / Retention (/admin/attachments/retention) draws
  the shared override form on it: a value there wins, an empty field falls
  back to the variable, and the source in effect is shown.
- The job resolves them through authEnv on every run, so a change needs no
  restart.
- The new menu label attachment-retention is translated in all 246 locales.
- The catalog's platform test then found that the patch had missed the
  Dockerfile and the snap help; both list all five now, beside
  docker-compose, start-wekan and the snap config.

tests/attachmentRetention.test.cjs (14) covers the rules and the Admin Panel
wiring; negative: the job never reads the environment around the Admin Panel.
tests/authConfigCatalog.test.cjs checks every platform lists each variable.
tests/playwright/specs/admin-attachment-retention.e2e.js opens the pane, and
checks that an ordinary user cannot.

Thanks to Raymond-Voogt and xet7 !

Fixes #6789.
2026-10-20 13:28:17 +03:00
Lauri Ojansivu 193fb449bd Adding a board member: email invite is case-insensitive, and search filters before it limits.
A board member who was not a board admin could not add a colleague from
Board / Members / +.

- inviteUserToBoard matched the typed email address exactly. LDAP and OIDC
  store an address as the directory spells it ("Jan.Jansen@Example.com"), so
  "jan.jansen@example.com" found nobody, and WeKan tried to create a new
  account instead: "This user is not created" with registration off,
  otherwise a raw 403 "Email already exists". The match is now
  case-insensitive, anchored and escaped.
- searchUsers read five users and filtered afterwards: disabled accounts
  (which the invite then refuses, #1894) and, for non-admins, people outside
  the user's organization or team (#6116). Five non-matching users ahead of
  the colleague hid them. It reads 50, filters, then returns 5.

From the patch by Raymond-Voogt. tests/inviteMemberEmailCase.test.cjs pins
both, including that the pattern is anchored and its dot literal.
tests/playwright/specs/invite-member-email-case.e2e.js covers:
- an invite with a lower-cased address adds the stored colleague, and no
  second account is made;
- negative: a similar but different address is not taken for them;
- the search returns the colleague past six disabled matches, offering none
  of them.

Thanks to Raymond-Voogt and xet7 !

Fixes #6788.
2026-10-20 13:24:14 +03:00
Lauri Ojansivu 13dfa18e8b Merge remote-tracking branch 'origin/main' 2026-10-20 13:15:20 +03:00
Lauri Ojansivu 31c5ad3727 CHANGELOG: the Security report's false alarms fixed, its labels translated.
Thanks to xet7 !
2026-10-20 10:37:28 +03:00
Lauri Ojansivu 3d703965c4 Admin Panel / Problems: "IPv6 location" and "Instrumentation" are translated.
The Security report in Finnish read "Sijainti" beside the English "IPv6
location", and the menu row and title bar of the Instrumentation report were
English in every language.

- The IPv6 location column is the translated 'location' with the fixed
  technical suffix " (IPv6)" - "Sijainti (IPv6)" - through a new optional
  labelSuffix on table headers, so no new key is needed.
- instrumentationReportTitle names the report in the menu row and in its
  title bar, which tests/adminUrls.test.cjs requires to say the same words.
  It is translated into all 246 locale files, after instrumentation-other
  in en.i18n.json's key order. French uses "Instrumentation logicielle",
  because the bare word is spelled exactly like the English one.

Low-confidence translations, written from the language's general
vocabulary for "measuring tools" where the technical term has no
established form: ak, ay, bm, bua, chr, cv, ee, ff, fj, gn, gv, ha, haw, ig,
iu, kl, kw, lg, mi, nah, nd, ny, om, qu, rn, rw, sah, se, sm, sn, ss, st,
tig, tlh, to, tpi, ts, ve, vo, wal, wo, zgh.

tests/addressReportColumns.test.cjs checks that every address column header
is translated. The admin-office-location UI spec expects "Location (IPv6)".

Thanks to xet7 !
2026-10-20 10:37:03 +03:00
Lauri Ojansivu 8a23306c2e Locations from Cloudflare keep their letters: "Dinkelsbühl", not "Dinkelsbühl".
Admin Panel / Problems / Security showed a sign-in from "Dinkelsbühl, DE".
Cloudflare sends cf-ipcity as raw UTF-8 bytes, and Node hands HTTP header
values over as Latin-1, one character per byte, so every city with a letter
outside ASCII was stored garbled.

geoHeaders reads a header value made only of byte-sized characters as UTF-8
when its bytes are valid UTF-8, before the existing percent-decoding. Real
Latin-1 that is not valid UTF-8, plain ASCII and percent-encoded names are
left as they were. Rows already stored keep the old text until their place is
seen again.

tests/geoHeadersUtf8.test.cjs: Dinkelsbühl, Zürich, Kraków, Москва, 東京, and
an end-to-end HTTP request carrying the UTF-8 bytes. Negatives: ASCII, a
percent-encoded city and genuine Latin-1 are unchanged. It fails without the
fix.

Thanks to xet7 !
2026-10-20 10:24:32 +03:00
Lauri Ojansivu 52f127c0a5 StorageBleed: a clean file-integrity scan is not a security event.
Admin Panel / Problems / Security showed StorageBleed, "checked 12144 files in
900s; 0 changed, 0 with no record saying why; stopped early: this run has
used its time", 41 times: the background scan reported its own summary on
every run. A clean scan is the scan working, and the Security report is for
attempts. A summary that has nothing to sum up hides the row that matters.

The summary is recorded only when the scan found a changed or missing file,
beside those findings, which are still recorded one by one. A clean run,
including one that ran out of time and continues next time, is written to
the server log as "[file-integrity] ...".

tests/fileIntegrity.test.cjs pins the condition. Negatives: no unconditional
summary record remains, and changed and missing files are still recorded.

Thanks to xet7 !
2026-10-20 10:23:47 +03:00
Lauri Ojansivu c7c541ccc5 FileNameBleed: a percent-encoded name is not an exploit, and opening a file is not an upload.
Admin Panel / Problems / Security showed FileNameBleed, "uploaded a filename
carrying an exploit pattern; URL-encoded name", for a user who had only
opened a board of 2019 photos with Cyrillic names. Reading each file
repaired its stored percent-encoded name (the FileBleed "fileRead" rows,
correct and recorded once per file). The repair's reasons were then passed to
the upload canary, where "URL-encoded name" counted as dangerous. Browsers and
older WeKan percent-encode every non-ASCII name, so every such name tripped it.

- "URL-encoded name" is no longer a dangerous reason.
- What an encoding could hide is named from the decoded name instead:
  sanitizationReasons() now reports "path traversal" and "null byte". Before,
  "URL-encoded name" was the only thing an encoded "../../etc/passwd" tripped;
  it now trips the path-traversal canary under its own name, which
  DANGEROUS_REASON already listed but no reason ever produced.
- A name repaired while a file is read (source fileRead) never trips the upload
  canary: the repair is still recorded, but it is not an attempt.

tests/filenameCanaryReasons.test.cjs loads the real module, with its server
dependencies stubbed:
- the Cyrillic name trips nothing;
- negatives: an encoded traversal and an encoded null byte still trip, and
  markup in a name uploaded now still trips;
- a read-time repair is recorded without a canary.

Thanks to xet7 !
2026-10-20 10:23:16 +03:00
Lauri Ojansivu 4f3ec0afa3 History integrity: a row is hashed as the database stores it, so an unset value is not "tampering".
Admin Panel / Problems / Security reported HistoryIntegrity checksum-mismatch,
severity critical, 89472 times, on a board's history row nobody had touched.
Meteor connects to MongoDB/FerretDB with ignoreUndefined, so a content
property that is undefined is dropped when the row is stored. Writers record
such content routinely, for example `deletedAt: list.deletedAt` for a list
that was never deleted. The row's hash covered `"deletedAt":{"$undefined":true}`,
the stored row has no deletedAt, and every background audit found the row
failing its own checksum and reported it again.

- hashHistoryRow() hashes previousContent and newContent as stored: undefined
  properties dropped, undefined array elements as null, as the driver does.
  New rows verify as the database returns them.
- Rows written before this were hashed with the undefined keys. If the normal
  check fails, the keys present in the other side of the change and absent
  here are re-added as undefined, which reproduces exactly what was hashed;
  nothing else is accepted.

tests/changeHistoryIntegrity.test.cjs stores rows through BSON with
ignoreUndefined, exactly as the driver does: a new row and a pre-fix row both
verify, and a changed value, a removed real value, a change on the other side
and an added key are still caught. The test fails without the fix.

A row that still reports checksum-mismatch after this release changed in a
way an unset value cannot explain, and is worth looking at.

Thanks to xet7 !
2026-10-20 10:21:43 +03:00
Lauri Ojansivu 363cd22916 Launchpad retry: match git's transfer error as 'RPC failed;', not by the word curl.
tests/releaseDownloads.test.cjs flags every workflow line that names curl
as a download that must go through releases/fetch.sh; infra_failure()'s grep
pattern named it only as text in git's error. 'RPC failed;' identifies that
error on its own. tests/snapLaunchpadInfraRetry.test.cjs still runs the
function against v12.56's 'RPC failed; curl 56' log.

Thanks to xet7 !
2026-10-20 10:12:08 +03:00
Lauri Ojansivu e73338e54b CHANGELOG: Bun smoke-test MongoDB waited for, Launchpad outages retried longer.
Thanks to xet7 !
2026-10-20 09:58:08 +03:00
Lauri Ojansivu 727e96e527 Snap on Launchpad: an outage there gets more attempts and longer pauses, not three strikes.
v12.56's ppc64el snap failed after 20 minutes of its five-hour budget. Attempt
1's Launchpad builder lost its TLS connection while cloning our source ("RPC
failed; curl 56 GnuTLS recv error ... fatal: early EOF"). Attempts 2 and 3
followed a minute apart each, and both met Launchpad's own "HTTP Error 503:
Service Unavailable" / "Error: Timeout". s390x, riscv64 and armhf each lost an
attempt the same night and recovered.

infra_failure() recognises an attempt that failed on Launchpad rather than on
WeKan: an HTTP 5xx or timeout from its API, or, in that attempt's own build
log, a dropped git/TLS transfer or the build farm's package proxy failing.
Such an attempt earns another one, up to six, after a pause of 5, 10, 15...
minutes, never running past the shared deadline. Every other failure keeps
three attempts and the 60s pause; a missing stage-package is not retried
longer.

tests/snapLaunchpadInfraRetry.test.cjs runs the workflow's own infra_failure()
against logs shaped like v12.56's, and against the negatives: a missing
stage-package, an earlier attempt's log, a 4xx.

Thanks to xet7 !
2026-10-20 09:57:54 +03:00
Lauri Ojansivu 294a9016d3 Bun bundles: the smoke-test MongoDB is waited for, and a lost one shown and replaced.
v12.56's Bun linux-x64 bundle failed its live smoke test: WeKan stopped with
"Topology is closed", because nothing listened on 127.0.0.1:27019. The
smoke-test MongoDB had been started with `docker run -d --rm` and never
checked, so a container that had died left no log behind, and the job could
not say why. linux-arm64 passed in the same run, and linux-x64 in v12.53, so
this happens only sometimes.

- bun.yml starts the container without --rm, and waits until it answers
  a ping. If it does not answer within 60s, its state and log are printed.
- build-target.sh checks the port again right before it starts WeKan, with
  the new releases/bun/wait-mongo.mjs (a plain TCP check, nothing to install).
  The database holds only this test's data, so if it is gone, its log is
  printed and a new one is started once. A second failure fails the job.

tests/bunSmokeMongo.test.cjs: URL parsing, the wait succeeding and timing
out, the workflow step and the single replacement.
buildScriptParity lists wait-mongo.mjs with the other Bun target-build
internals.

Thanks to xet7 !
2026-10-20 09:56:00 +03:00
github-actions[bot] b160d355ff Bump versions for v12.56 2026-10-20 00:54:06 +00:00
Lauri Ojansivu 3ae98f3059 Prepare v12.56 release 2026-10-20 03:52:54 +03:00
Lauri Ojansivu 84bf7d2dd7 Merge remote-tracking branch 'origin/main' 2026-10-20 03:52:25 +03:00
Lauri Ojansivu 78c453a66d CHANGELOG: #6783 also needed the People table's row projection.
Thanks to Raymond-Voogt and xet7 !
2026-10-19 20:21:40 +03:00
Lauri Ojansivu c4376f92a5 People: every row's Last active reaches the table, not only the publication.
The UI test of #6783 (inactive-users-last-active.e2e.js) still found "-" in
another user's row after the people publication started sending
lastConnectionDate. The table builds its rows from a client-side Users.find
with its own field list (peopleDocs), which left the date out. The row's
lastActiveAt read that copy before the full user document, so the date never
reached the cell. The projection keeps the field now. The two presence
helpers read the whole cached user, as userData() already does, so a field
missing from the projection cannot hide them again.

Verified with Playwright on Chromium against the dev server: the spec failed
before this change and passes with it. tests/lastActive.test.cjs pins both
halves and fails without this commit.

Thanks to Raymond-Voogt and xet7 !

Fixes #6783.
2026-10-19 20:21:32 +03:00
Lauri Ojansivu f1b183ac2a rtl test: a check mark's border-left/-right is the one physical side allowed.
A check mark is not mirrored in a right-to-left language (#6786), so the
tick's rule uses border-right. The guard allows border-left/-right only inside
a rule whose selector is a checked state, the same definition
tests/rtlCheckMark.test.cjs uses. A border anywhere else, and any margin,
padding or float inside such a rule, is still refused.

Thanks to mimZD and xet7 !
2026-10-19 17:55:10 +03:00
Lauri Ojansivu faa56aa1a8 docker-compose.yml lists USERS_DISABLE_INACTIVE_AFTER_DAYS where every backend's compose file does.
Its wekan service must match the other compose files line for line
(tests/dockerComposeBackends.test.cjs): the setting moves from beside
ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS to after LOGOUT_ON_MINUTES, with the
Admin Panel note the others carry.

Thanks to Raymond-Voogt and xet7 !
2026-10-19 17:55:10 +03:00
Lauri Ojansivu 912d414fdf CHANGELOG: inactive accounts disabled, LDAP sync filter, Last active for everyone, RTL check marks, restored list swimlanes kept.
Thanks to Raymond-Voogt, mimZD and xet7 !
2026-10-19 17:42:08 +03:00
Lauri Ojansivu 3469f84c78 Restored list swimlanes stay restored after a WeKan upgrade.
Admin Panel / Problems / "Lists missing their swimlane" -> Restore binds each
list back to the swimlane positionHistory records it was created in. After
every new WeKan release, the startup schema upgrade re-checks old data, and
its merge-per-swimlane-lists step - written for the v7.98 era, which stamped
lists to the default swimlane and duplicated them per swimlane - took those
lists for that era's damage. It ran when a board had two lists of one title
or one list with cards in another swimlane, and then un-bound every list on
the board, the restored ones included. So after each upgrade the count came
back to exactly the same number.

A list bound to the very swimlane it was created in, with none of its cards
in another swimlane, is per-swimlane by design: it is what Restore puts back,
and not a v7.98 stamp, which was applied after the list existed. The step now
leaves those lists out of its check, its merge and its un-binding, so a board
of only such lists does not trigger it. Stamped lists, lists whose cards are
in other swimlanes, and lists bound anywhere but their record are handled as
before. Lists already un-bound by an earlier upgrade are restored by pressing
Restore once more, and now stay.

tests/schemaUpgradeSteps.test.cjs: the issue's board (restored lists kept, a
stamp on the same board still un-bound), a by-design board not triggering the
step, and the two negatives. The first fails without the fix.

Thanks to mimZD and xet7 !

Fixes #6787.
2026-10-19 17:41:32 +03:00
Lauri Ojansivu b4bcded492 A checked checkbox is a check mark in right-to-left languages too, not "<".
The tick is a box rotated 40 degrees, drawn by its bottom and side borders,
and the side was border-inline-end. A right-to-left page puts that on the
left, so the rotated corner opened the other way and every checked checkbox
showed "<" - the Admin Panel's bounced-email table in Persian, in the issue.
A check mark is not mirrored in a right-to-left language, so all 25 rules
that draw one - the shared materialCheckBox, checklists, subtasks, the
sidebar, the Admin Panel and every board theme's colour - use border-right
and border-left now. Where the box sits stays logical.

tests/rtlCheckMark.test.cjs fails if any checked state anywhere in client/
draws its tick with a logical border. tests/playwright/specs/rtl-check-mark.e2e.js
compares the computed borders in ltr and rtl. customFieldsSectionMenu's guard
now expects border-right, and its comment says why.

Thanks to mimZD and xet7 !

Fixes #6786.
2026-10-19 17:37:53 +03:00
Lauri Ojansivu b933a69cd4 People shows everyone's Last active; inactive users can be disabled; LDAP sync honours its search filter.
Admin Panel / People showed "Last active" as "-" for every user but the
administrator: the people publication did not carry lastConnectionDate.

USERS_DISABLE_INACTIVE_AFTER_DAYS disables, once a night, accounts that have
not used WeKan for that many days, and revokes their logins. Admins are
never disabled. An account with no recorded activity starts its count on the
first run rather than from createdAt, and re-enabling an account restarts it.
The setting is in Admin Panel / People / Login as well as in the
environment, and the job resolves it every night, so a change there needs no
restart. Every platform's settings file lists it.

LDAP background sync looked a user up by id without LDAP_USER_SEARCH_FILTER,
so an account the filter excludes - one disabled in Active Directory - could
not log in but was still "found". It was never disabled, and a manual
disable was undone on the next run. The filter is now ANDed into the lookup.
An account disabled for inactivity is not re-enabled by the sync.

From the patch by Raymond-Voogt, with the setting moved into the Admin
Panel's login settings. Tests: tests/inactiveUsers.test.cjs,
tests/ldapBackgroundSync.test.cjs, tests/ldapUserIdFilter.test.cjs,
tests/authConfigCatalog.test.cjs and
tests/playwright/specs/inactive-users-last-active.e2e.js.

Thanks to Raymond-Voogt and xet7 !

Fixes #6783,
Fixes #6784,
Fixes #6785.
2026-10-19 17:35:51 +03:00
github-actions[bot] e4d025e0b4 Bump versions for v12.55 2026-10-18 20:52:17 +00:00
Lauri Ojansivu 28163c90d1 Prepare v12.55 release 2026-10-18 23:51:09 +03:00
Lauri Ojansivu 2ca5d06709 Merge remote-tracking branch 'origin/main' 2026-10-18 23:50:24 +03:00
Lauri Ojansivu 506067b4a1 CHANGELOG: 32-bit ARM single files injected in a 64-bit process, each musl AppImage built once.
Thanks to xet7 !
2026-10-18 23:35:30 +03:00
Lauri Ojansivu 2cbb038719 AppImage: each musl AppImage built once, and every job has its own name.
release-all's glibc "AppImages" call passed no `only`, so it built the musl
rows too, and both groups had an "AppImage x86_64" and an "AppImage aarch64".
Two jobs of one name write to one directory of the run's log archive, whose
files then ask to be overwritten or renamed when it is unpacked.

- The glibc call builds x86_64, aarch64, i686 and armhf only. The musl pair
  stays with the "musl AppImages" call, which waits for the musl bundles.
- The job is named by its file's architecture, so it is "AppImage
  x86_64-musl", which the publish step's "see the AppImage <arch> build job's
  log" names too.
- The publish step checks only the architectures its own run builds. The musl
  run warned that the four glibc AppImages were missing.

tests/muslBundles.test.cjs checks that no two matrix rows share a name, and
that each row is built by exactly one call.

Thanks to xet7 !
2026-10-18 23:35:14 +03:00
Lauri Ojansivu 29dfe94886 Single files: a 32-bit ELF target's blob is injected in a 64-bit process.
v12.54's armv7 single file failed with std::bad_alloc on all three attempts
of the retry added for armhf in v12.53, so the failure is not intermittent.
--build-sea injects with LIEF inside the Node.js that runs it, and a 32-bit
target's blob must be written by a 32-bit Node.js. The executable, the
180 MiB archive and LIEF's copies of both do not fit that process's address
space.

For a 32-bit ELF target the i386 Node.js now writes only the blob
(--experimental-sea-config: the same blob, no LIEF), and the new
releases/single-file/sea-inject-elf.py injects it with LIEF 1.0.0 from PyPI,
the version Node.js vendors. It ports InjectIntoELF() and MarkSentinel()
from src/node_sea_bin.cc step for step. Its output is byte-identical to
--build-sea's, for a non-PIE x64 Node.js and for the PIE node-armv7. win32
(PE) and 64-bit targets keep --build-sea. The retry is removed.

tests/singleFileSeaInject.test.cjs checks the split and the workflow's LIEF.
Where a Python with LIEF 1.0.0 is available it builds both ways, compares
them byte for byte, runs the result, and checks that an executable that
already carries a blob is refused.

Thanks to xet7 !
2026-10-18 23:35:14 +03:00
github-actions[bot] 734e81fb8c Bump versions for v12.54 2026-10-18 15:57:36 +00:00
Lauri Ojansivu 81e609cb69 Prepare v12.54 release 2026-10-18 18:56:32 +03:00
Lauri Ojansivu f166786e7a Merge remote-tracking branch 'origin/main' 2026-10-18 18:55:54 +03:00
Lauri Ojansivu 1a6a96b667 nodeRuntimeDependency: the glibc libatomic requirement still reaches the AppImage check past the musl NEEDED block.
Thanks to xet7 !
2026-10-18 18:52:49 +03:00
Lauri Ojansivu 939addc77a CHANGELOG: Android bundles, the armhf single file and the x86_64 musl AppImage build again.
Thanks to xet7 !
2026-10-18 18:49:48 +03:00
Lauri Ojansivu df5560ad51 musl bundles carry every library their Node.js needs, libatomic too.
Every x86_64 musl AppImage of v12.53 failed to start in a bare Alpine:
"Error loading shared library libatomic.so.1". node-runtime held only
libstdc++ and libgcc_s; the build container had libatomic through g++, so the
bundle's own smoke test passed.

build-bundle.sh now copies every NEEDED library of bundle/node except musl's
libc, and AppImage.yml checks each is in node-runtime before packing.

Thanks to xet7 !
2026-10-18 18:49:28 +03:00
Lauri Ojansivu fa0c03437c Single files: try --build-sea again when it runs out of memory.
The v12.53 armhf single file failed with std::bad_alloc in --build-sea. A
32-bit target is injected by the 32-bit i386 Node.js, and the executable,
the 180 MiB archive and LIEF's copies of both come near its 4 GiB address
space. armv7 and armv6, same archive and host, built in the same run, and
armhf built in v12.52, so the failure is intermittent.

buildSea() runs it up to three times when, and only when, it fails with
std::bad_alloc, removing the partial output between attempts. Any other
failure stops at once.

Thanks to xet7 !
2026-10-18 18:49:28 +03:00
Lauri Ojansivu 519869b561 Android bundles: preload termux-exec, so bcrypt's install script runs.
Every Android bundle of v12.53 stopped in npm-rebuild.js: "sh: 1:
node-gyp-build: not found". Termux has no /usr/bin/env, so a
"#!/usr/bin/env node" script runs only through termux-exec, an LD_PRELOAD
library that Termux's login exports. The workflow starts bash directly,
without login.

build-bundle.sh exports termux-exec's library the way login does, newest
generation first, and keeps it only if a program still starts with it.

Thanks to xet7 !
2026-10-18 18:49:28 +03:00
github-actions[bot] 8a67ca34d6 Bump versions for v12.53 2026-10-18 11:34:09 +00:00
Lauri Ojansivu 9ba22aaca7 Prepare v12.53 release 2026-10-18 14:33:00 +03:00
Lauri Ojansivu bc0991e06f CHANGELOG: #6782, a card move with an outgoing webhook keeps the mover's account.
Thanks to do4me and xet7 !
2026-10-18 14:29:09 +03:00
Lauri Ojansivu b01c233269 Moving a card with an outgoing webhook no longer counts as HookBleed or disables the mover's account.
The activity hook delivered webhooks through Meteor.call('outgoingWebhooks'),
and that method trusted a call without this.connection as the server's own.
Meteor gives a server-side Meteor.call the userId AND connection of the method
it runs in (ddp-server applyAsync), and a card moved from a browser runs in
that browser's method. So the server's own act-moveCard delivery arrived as a
client forging one: it was recorded as ssrf.webhook-forge (high) with the
mover's userId, and blockOnSecurityEvent set loginDisabled.

- deliverActivityWebhook() in server/notifications/outgoing.js is the server's
  delivery: a function, not a method, so no client reaches it and no inherited
  context changes it. It reads the integration again (enabled) and names the
  activity's actor. server/models/activities.js calls it through safeDeliver.
- The outgoingWebhooks method treats every call as a client's: only the
  card-opened notification, rebuilt from the card, as before for clients.
- Server deliveries no longer need the actor to be a member of the hook's
  board, which also skipped a global webhook ('_global' is no board) and an
  activity with no method behind it (REST, timers).
- securityLog.record() now passes the catalog key and blocksAccount to the
  block decision. The stored row carries neither, so every 'high' blocked row
  disabled whoever it named: a hook refused for its own internal address
  disabled the mover too, and ssrf.redirect/ssrf.attachment's
  blocksAccount: false never applied. ssrf.webhook is now blocksAccount:
  false - the person whose move sent it did not write the hook's URL.

tests/hookBleedServerDelivery.test.cjs: the move delivered with nothing
logged, in an inherited context too; a forged call still refused, logged and
blocking; the card-opened notification; failing and internal hooks blocking
nobody, through record() itself; and a whole-tree scan that no server code
calls a this.connection-gated method from inside a client's method without
clearing the invocation. tests/playwright/specs/hook-bleed.e2e.js moves a
card through the Move dialog with a webhook enabled.

Thanks to do4me and xet7 !

Fixes #6782.
2026-10-18 14:28:19 +03:00
github-actions[bot] 3c9f4ba045 Bump versions for v12.52 2026-10-17 23:49:33 +00:00
Lauri Ojansivu e0f8447524 Prepare v12.52 release 2026-10-18 02:48:13 +03:00
Lauri Ojansivu 6809149ef1 CHANGELOG: the nine dependabot updates, checked, and the guards that follow them.
Thanks to dependabot and xet7 !
2026-10-18 02:44:47 +03:00
Lauri Ojansivu 53729d47f5 Guards follow the merged dependabot updates: freebsd-vm 1.5.9, qrcode-generator 2.0.4.
bsdBundles pinned bsd.yml's FreeBSD VM to the old commit; it now reads the
pin and requires node-patches to use the same one, which it now does.
sqrl required qrcode-generator 1.4.4 exactly; it now requires an exact pin,
and 2.0.4 was checked to give sqrlQrSvg's call the same SVG.

Thanks to dependabot and xet7 !
2026-10-18 02:44:40 +03:00
Lauri Ojansivu f063f5043f Merge pull request #6781 from wekan/dependabot/github_actions/vmactions/freebsd-vm-1.5.9
Bump vmactions/freebsd-vm from 1.5.5 to 1.5.9
2026-10-17 23:48:29 +03:00
Lauri Ojansivu 71e942b146 Merge pull request #6780 from wekan/dependabot/github_actions/actions/setup-python-7.0.0
Bump actions/setup-python from 6.3.0 to 7.0.0
2026-10-17 23:48:15 +03:00
Lauri Ojansivu e365a57649 Merge pull request #6779 from wekan/dependabot/npm_and_yarn/i18next-26.4.5
Bump i18next from 26.4.2 to 26.4.5
2026-10-17 23:47:59 +03:00
Lauri Ojansivu 372192a28d Merge pull request #6778 from wekan/dependabot/npm_and_yarn/swc/core-1.16.18
Bump @swc/core from 1.16.13 to 1.16.18
2026-10-17 23:46:57 +03:00
Lauri Ojansivu 9340b25c03 Merge pull request #6777 from wekan/dependabot/npm_and_yarn/temml-0.14.0
Bump temml from 0.13.5 to 0.14.0
2026-10-17 23:46:42 +03:00
Lauri Ojansivu dbf50d67bd Merge pull request #6776 from wekan/dependabot/npm_and_yarn/tests/playwright/playwright/test-1.64.1
Bump @playwright/test from 1.63.0 to 1.64.1 in /tests/playwright
2026-10-17 23:46:24 +03:00
Lauri Ojansivu b022403a55 Merge pull request #6775 from wekan/dependabot/npm_and_yarn/qrcode-generator-2.0.4
Bump qrcode-generator from 1.4.4 to 2.0.4
2026-10-17 23:46:11 +03:00
Lauri Ojansivu 67d0e4fd94 Merge pull request #6774 from wekan/dependabot/github_actions/actions/download-artifact-8.0.2
Bump actions/download-artifact from 7.0.0 to 8.0.2
2026-10-17 23:45:57 +03:00
Lauri Ojansivu ed1aae7cf0 Merge pull request #6773 from wekan/dependabot/npm_and_yarn/rspack/cli-2.2.9
Bump @rspack/cli from 2.2.8 to 2.2.9
2026-10-17 23:45:41 +03:00
Lauri Ojansivu 161f213de0 Merge remote-tracking branch 'origin/main' 2026-10-17 23:44:43 +03:00
dependabot[bot] bdb6e4f875 Bump vmactions/freebsd-vm from 1.5.5 to 1.5.9
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.5.5 to 1.5.9.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.5.5...c46abacb49f09938ca4e1702d15d836285d694cc)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.5.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:33:40 +00:00
dependabot[bot] 4afb4cb0f4 Bump actions/setup-python from 6.3.0 to 7.0.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/ece7cb06caefa5fff74198d8649806c4678c61a1...5fda3b95a4ea91299a34e894583c3862153e4b97)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:33:37 +00:00
dependabot[bot] 04639dd871 Bump i18next from 26.4.2 to 26.4.5
Bumps [i18next](https://github.com/i18next/i18next) from 26.4.2 to 26.4.5.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.4.2...v26.4.5)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:33:26 +00:00
dependabot[bot] 330cd9dab0 Bump @swc/core from 1.16.13 to 1.16.18
Bumps [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core) from 1.16.13 to 1.16.18.
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.16.18/packages/core)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.16.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:33:18 +00:00
dependabot[bot] 034e5ea441 Bump temml from 0.13.5 to 0.14.0
Bumps [temml](https://github.com/ronkok/Temml) from 0.13.5 to 0.14.0.
- [Release notes](https://github.com/ronkok/Temml/releases)
- [Changelog](https://github.com/ronkok/Temml/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ronkok/Temml/compare/v0.13.5...v0.14.0)

---
updated-dependencies:
- dependency-name: temml
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:33:00 +00:00
dependabot[bot] 7ffe87a677 Bump @playwright/test from 1.63.0 to 1.64.1 in /tests/playwright
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.63.0 to 1.64.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.64.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:32:54 +00:00
dependabot[bot] ac97edb4aa Bump qrcode-generator from 1.4.4 to 2.0.4
Bumps [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) from 1.4.4 to 2.0.4.
- [Release notes](https://github.com/kazuhikoarase/qrcode-generator/releases)
- [Commits](https://github.com/kazuhikoarase/qrcode-generator/compare/js1.4.4...js2.0.4)

---
updated-dependencies:
- dependency-name: qrcode-generator
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:32:52 +00:00
dependabot[bot] 43a646b19e Bump actions/download-artifact from 7.0.0 to 8.0.2
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.2.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...9000827ccba6bdab643e8b6fd33ac0654aef8333)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:32:46 +00:00
dependabot[bot] b0ac3b0629 Bump @rspack/cli from 2.2.8 to 2.2.9
Bumps [@rspack/cli](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack-cli) from 2.2.8 to 2.2.9.
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v2.2.9/packages/rspack-cli)

---
updated-dependencies:
- dependency-name: "@rspack/cli"
  dependency-version: 2.2.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-17 20:32:45 +00:00
Lauri Ojansivu 95a1478c8c CHANGELOG: Android, musl and single-file release builds.
Thanks to xet7 !
2026-10-17 23:25:30 +03:00
Lauri Ojansivu c316616ef8 Single files: skip, with the reason, a Node.js LIEF cannot inject; no error for a target not built.
v12.51's i386 single file stopped with std::bad_alloc and freebsd-x64's with
"Failed to relocate ELF program headers": the i386 Node.js is a PIE with
R_386_32 relocations LIEF does not adjust, and the FreeBSD one a non-PIE with
two bss-like segments. releases/single-file/sea-inject.mjs reads that from
the ELF before anything is built, build.mjs exits 3 with the reason, and
single-file.yml turns it into a notice. wekan/node-patches now builds both
the way the injection needs.

A target that was not built leaves a one-line artifact saying why, so its
Start job no longer logs "Artifact not found" errors and publish does not
warn about it as unstarted.

Thanks to xet7 !
2026-10-17 23:24:49 +03:00
Lauri Ojansivu bce305029a musl bundles carry libstdc++ and libgcc_s, so their Node.js starts on a bare Alpine.
Every musl AppImage of v12.51 died at once in Alpine: "Error relocating
.../bundle/node: _ZTVN10__cxxabiv117__class_type_infoE: symbol not found".
The musl Node.js is linked against GCC's C++ runtime, which musl.yml's own
Alpine had installed for building, and a user's Alpine does not. The bundle's
node-runtime now holds that Alpine's libstdc++.so.6 and libgcc_s.so.1 with a
license notice, start-wekan.sh puts it on the library path as for libatomic,
and the AppImage workflow requires both files of a musl bundle.

Thanks to xet7 !
2026-10-17 23:24:49 +03:00
Lauri Ojansivu 0670cc65a3 Android bundles: hand the checkout to Termux's user, so npm may chmod.
Every Android bundle of v12.51 stopped with EPERM: npm chmods each package's
bin when it installs (uuid/dist/bin/uuid), and only a file's owner may chmod
it - a+rwX on the bind mount was not enough. android.yml reads the uid of
Termux's user from the image, chowns the checkout to it before the rebuild,
and takes it back before packaging. An unreadable uid stops the job.

Thanks to xet7 !
2026-10-17 23:24:49 +03:00
github-actions[bot] 95c99e1b5e Bump versions for v12.51 2026-10-17 15:34:41 +00:00
Lauri Ojansivu c1cb54eb3b Prepare v12.51 release 2026-10-17 18:33:31 +03:00
Lauri Ojansivu 3eee7d54df CHANGELOG: CodeQL alert #588, Apple Glass Pastel as Liquid Glass.
Thanks to GitHub CodeQL and xet7 !
2026-10-17 17:56:05 +03:00
Lauri Ojansivu 6090be4dfd Fix GitHub CodeQL alert #588: remove template declarations to a fixed point.
js/incomplete-multi-character-sanitization in
tests/templatesAreRendered.test.cjs: the test removed template declarations
and Template.<name>.helpers/events/onCreated/onRendered/onDestroyed from the
source it scans with a single replace each, so removing one could splice its
neighbours into another (and CodeQL read the remains as a possible on...
attribute). Both removals now repeat until nothing changes, the fix used for
alerts #545 and #570-#582.

tests/tagStrippingFixedPoint.test.cjs keeps the shape out of the whole tree:
it now also finds a multi-character removal of an on<Capital> name outside a
fixed-point loop, and proves it catches alert #588's code and passes the
loop. The code is a test's source scan, never run on user input, so nothing
reaches Admin Panel -> Problems.

Thanks to GitHub CodeQL and xet7 !
2026-10-17 17:55:41 +03:00
Lauri Ojansivu 0c49f7f51b Apple Glass Pastel is Liquid Glass, and every text on it reads.
The theme follows Apple's Liquid Glass: the two header bars, swimlane
headers, lists, minicards, the opened card, the sidebar and popups are
frosted glass over the pastel mesh - a translucent fill,
blur(24px) saturate(180%), a rim of light along the edges (inset highlights,
brightest on top), a diagonal sheen and larger rounded corners. The header
bars are glass now instead of solid blue, with dark text; blue is kept for
the primary action, glossy, and for what is active. Reduce transparency, or
a browser without backdrop-filter, gets near-opaque surfaces.

A new UI test, theme-appleglasspastel-contrast.e2e.js, measures every text
and icon against what is really painted behind it - it hides the text,
screenshots, and reads the pixels under each box - to WCAG AA (4.5:1 text,
3:1 large text and icons), on the board, the opened card, the sidebar, the
board menu and All Boards, at laptop and phone size. It found and the theme
now fixes: white header buttons, icons, username and the stock logo on the
glass; the sidebar hamburger forced white by boardBody.js's accessibility
style; grey #808080 sidebar headings (3.8:1); #a6a6a6 icons (2.4:1); faint
Add Card; white text on the pale-blue hovered menu row; and the list
headers list.css pins grey with !important.

The header guards headerBars and themeAccents now compare a bar's whole
background, so a glass gradient counts, and list appleglasspastel as a glass
header whose accent is its primary blue.

Thanks to xet7 !
2026-10-17 17:55:34 +03:00
Lauri Ojansivu 321e3d7cbf CHANGELOG: Whiteboard; TODO Later is empty again.
Thanks to xet7 !
2026-10-17 16:57:33 +03:00
Lauri Ojansivu 390007e380 Whiteboard: a card's drawing full screen, that several people draw on at once.
Card menu -> Drawing -> Open as whiteboard opens the drawing under the header
bar, over the opened card, with the drawing's and Red Strings' own tools -
the seven colours and four widths, Undo, Clear and Save picture. The 800 x
500 drawing is scaled to the screen, so a line lands in the same place on a
phone and on a 55" screen; Escape or x closes it.

Several people draw at the same time: each line is sent when the pen lifts
as one atomic $push into that drawing (cards.whiteboardAddStroke), with its
own id and its author, and the card's publication brings it to everyone who
has the drawing open; the line stays drawn locally until it comes back.
Undo and Clear take away only the caller's own lines, Save picture writes
only the drawing's PNG, so nothing anybody else is drawing is overwritten.
Who may draw is who may edit the card; the others watch. A Board Settings /
Card row, Whiteboard, hides it on a board, and the Admin Panel's Card fields
everywhere. The drawing popup's PNG upload is shared with Save picture.
Translated in every language.

Thanks to xet7 !
2026-10-17 16:57:12 +03:00
Lauri Ojansivu d9bb7b4f4f docs/Features: Cards / Whiteboard, a drawing full screen that several people draw on at once.
Thanks to xet7 !
2026-10-17 16:27:33 +03:00
Lauri Ojansivu 813c313b1b CHANGELOG: checklist from description, action items, search phrases; TODO Later keeps the whiteboard.
Thanks to xet7 !
2026-10-17 16:26:17 +03:00
Lauri Ojansivu 3cb089b8ca Checklist from description, action items, and search phrases.
The last three of the features other kanban apps call AI, done with plain
code (docs/Features/General/Assistants/Assistants.md):

- Checklists -> From description: the list lines of a card's description
  ("- ", "* ", "1. ", task boxes; never inside code blocks) previewed, then
  made into a checklist - ticked where the line was [x] - or into subtasks
  through addSubtaskCard.
- Create cards from action items, on a comment and in the card menu for its
  description: "TODO: ...", "- [ ] ..." and "@user ..." lines become cards
  at the top of the card's list, linking back to it, with the mentioned
  user as member when they are an active board member.
- Global search understands plain phrases in the reader's language - "my
  overdue cards", "due this week", "unassigned cards" and seven more - and
  runs them as its operators, saying which; a query with an operator in it
  is never rewritten.

Checklist from description and Action items are Show on Card rows of Board
Settings / Card and of the Admin Panel's Card fields; Search phrases is an
Admin Panel / Settings / Visibility / Features tick. Translated in every
language.

Thanks to xet7 !
2026-10-17 16:25:51 +03:00
Lauri Ojansivu 9fa831fb0a Merge remote-tracking branch 'origin/main' 2026-10-17 16:11:10 +03:00
Lauri Ojansivu 777437bc6a CHANGELOG: Summary, Since your last visit, At risk, Suggested members, Forecast.
Thanks to xet7 !
2026-10-17 15:52:51 +03:00
Lauri Ojansivu 083c863fb7 Summary, Since your last visit, At risk, Suggested members and Forecast.
Five more of the features other kanban apps call AI, done with plain code
(docs/Features/General/Assistants/Assistants.md):

- Board menu -> Summary: what each person did in the last day or week -
  done, moved, added, commented - and what they have overdue, from the
  activities and History; Copy as text gives Markdown with card links.
- Board menu -> Since your last visit (N): what others changed since the
  visit before this one, by card. The visit is kept in profile.boardVisits,
  at most 500 boards.
- At risk: a weighted score of overdue, due soon with little done, blocked,
  idle in a middle list and sent back; a triangle on the minicard and the
  reasons in the opened card.
- Suggested members in the Members and Assignees popups, from who handles
  the same labels, list and swimlane.
- Forecast done date: the 50th and 85th percentile of the board's cycle
  times, counting only the cards that took at least as long as this one
  has been open. The server sends the times once per board, numbers only.

Summary and Since your last visit are features of Admin Panel / Settings /
Visibility / Features; At risk, Suggested members and Forecast are rows of
Board Settings / Card and of the Admin Panel's Card fields. Every server
answer applies the board's read rules and the assigned-only role.
Translated in every language.

The guards of the earlier helpers are brought up to date as well: the date
words of models/lib/titleDueDate.js are a reviewed RegExp site,
server/methods/ruleSuggestions.js is a board-admin-only path, the Rules
page's list branch carries the suggestions, and Tigre's new strings use its
own word for a board.

Thanks to xet7 !
2026-10-17 15:52:26 +03:00
Lauri Ojansivu 9a6b3e8859 CHANGELOG: due date from the title, similar cards, rule suggestions; TODO Later gains the whiteboard.
Thanks to xet7 !
2026-10-17 14:43:43 +03:00
Lauri Ojansivu 2211b24119 Rule suggestions from what people on a board keep doing by hand.
Board menu -> Rules shows Suggested rules above the rule list: when the
same person does one thing to a card and, within two minutes, another, that
pair is counted once per card, and a pair seen on three cards in the last 30
days is offered as a rule (moved to a list -> label, member or archive;
added to a list -> label or member; label added -> member). A pair a rule
already does is not offered. Create goes through rules.createRule with the
same trigger and action the wizard builds; Dismiss hides one suggestion for
that admin on that board (profile.dismissedRuleSuggestions).

Only board admins get suggestions, and Admin Panel / Settings / Visibility /
Features has a Rule suggestions tick that turns them off, on the server too.
Translated in every language.

Thanks to xet7 !
2026-10-17 14:42:59 +03:00
Lauri Ojansivu f8267d704e Due date from the card title, and similar cards and possible duplicates.
Two of the features other kanban apps call AI, done with plain code
(docs/Features/General/Assistants/Assistants.md):

- A due date typed at the end of a new card's title ("... tomorrow 14:00",
  "huomenna klo 14", "friday 9:30", "in 3 days", "23.10.") becomes the card's
  due date, read with the browser's own Intl tables so every language works.
  A hint under the field shows the date, and its x keeps the words. The
  opened card's title reads it too, but only when the title changed.
- Similar cards: letter triples and the Dice coefficient (letter pairs for
  scripts without spaces). The add-card form lists up to three close cards,
  marking a possible duplicate, and the opened card has a Similar cards
  section. The server method applies the board's read rules and the
  assigned-only role.

Both are rows of Board Settings / Card and of Admin Panel / Settings /
Visibility / Features / Card fields, so a board admin or a site admin can
hide them. Translated in every language.

Thanks to xet7 !
2026-10-17 14:39:43 +03:00
Lauri Ojansivu e76f12bcdd docs/Features: guides for the eleven helpers that need no AI, filed by menu, and how each is hidden.
Thanks to xet7 !
2026-10-17 13:57:56 +03:00
Lauri Ojansivu 6f6ae210c6 CHANGELOG: TODO Later lists the features other kanban apps call AI, to be done with plain code.
Thanks to xet7 !
2026-10-17 13:52:35 +03:00
github-actions[bot] 0d726aa725 Bump versions for v12.50 2026-10-17 03:44:38 +00:00
Lauri Ojansivu 5cadb33b1d Prepare v12.50 release 2026-10-17 06:43:30 +03:00
Lauri Ojansivu 31de4b40c5 CHANGELOG: Instrumentation bars by area.
Thanks to xet7 !
2026-10-17 06:04:05 +03:00
Lauri Ojansivu 2b8a33022b Problems / Instrumentation: the whole picture as one bar per area of WeKan.
The page listed up to 200 methods and publications on 11 pages. Above the
table there is now one bar per area - Settings, Login, People, Boards,
Cards, Lists, Notifications, Attachments, Problems and the rest - its length
the area's calls against the busiest area's, its red end the share that
failed, with the calls, the average duration and the errors beside it.
Clicking an area shows only its operations in the table; clicking it again
shows all.

models/lib/instrumentationAreas.js groups an operation by the first area
whose pattern matches its name, and weights an area's average duration by
completed calls. tests/instrumentationAreas.test.cjs checks the grouping
against all of WeKan's 614 methods and publications - "Other" holds three -
and the summary arithmetic. The labels are existing translation keys, apart
from the new instrumentation-other ("Other"), translated into every
language. The Finnish "cards" heading read "Korttia" (the form after a
number) in every place it is used as a heading or label; it is "Kortit".

The bars are HTML and CSS, one grid with subgrid rows so names, bars and
numbers line up; the viewport matrix checks the page at every device size.

Thanks to xet7 !
2026-10-17 06:03:55 +03:00
Lauri Ojansivu d9e91cb59a CHANGELOG: Shared labels colour picker, iPhone Duo, the add-list form's spacing.
The file was over its size budget; releases/changelog-archive.mjs moved its
oldest release to old-CHANGELOG/2026/10.md.

Thanks to xet7 !
2026-10-17 05:33:04 +03:00
Lauri Ojansivu 8c3e80240a Shared labels: the board labels' colour picker, at the size it has on a card.
Admin Panel / Settings / Shared labels chose a colour from a dropdown of
colour names. The board label popup's picker - the palette swatches and the
custom colour wheel - is its own template now, labelColorPicker, and the
popup and every Shared labels row use it; a custom colour is saved as its
hex, which the server already accepted. A pick reaches the row through an
object with set(), because Spacebars calls a function argument instead of
passing it.

The swatches grow to fill their row, so on the wide admin page their width
followed the window. There they have the size they have in an opened card's
label popup, 54 x 33px, at every window width.

tests/sharedLabelsColorPicker.test.cjs pins the shared picker and the size;
tests/playwright/specs/shared-labels-color-picker.e2e.js picks a swatch and a
custom colour, saves both, and measures the swatches at 1366, 1920 and
2560px.

Thanks to xet7 !
2026-10-17 05:32:38 +03:00
Lauri Ojansivu 85ab1b79a3 "or Template or link a list": the space before the second "or", and a clean Jade build.
The add-list form's alternatives are one span.quiet each in .edit-controls,
a flex row; a flex item drops the space its text begins with, so it read
"or Templateor link a list from another board". Adjacent .quiet spans get a
margin between them instead.

Every dev-server start also warned "missing space before text for line 26"
of editor.jade: inlineViewer had a bare {{> UI.contentBlock }} line under
+viewer. It is +Template.contentBlock, as elsewhere.
tests/jadeCompilesClean.test.cjs compiles every client template with the
build's own Jade compiler and fails on any warning or error.

Thanks to xet7 !
2026-10-17 05:15:00 +03:00
Lauri Ojansivu 0242249a1c Viewport matrix: the iPhone Duo, open and closed, instead of a Galaxy Fold.
The foldable in the screen-size test was a Galaxy Z Fold 7 standing in for
the iPhone Duo. Apple's specs (apple.com/iphone-duo/specs) give its inner
7.6" display as 1878 x 2670 px at 430 ppi and its outer 5.4" display as
1398 x 2034 px at 460 ppi; at the 3x of every current iPhone that is
626 x 890 and 466 x 678 CSS pixels, and the screenshots come out at exactly
Apple's pixel sizes. Both displays are tested upright and on their side with
the iPhone user agent and touch; all 40 checks pass.

Thanks to xet7 !
2026-10-17 04:53:54 +03:00
Lauri Ojansivu d4c7167928 CHANGELOG: the new UI checked and fixed at ten screen sizes.
Thanks to xet7 !
2026-10-17 04:06:00 +03:00
Lauri Ojansivu e34a488617 Phone, foldable, tablet, laptop and 55" touchscreen: the new UI checked upright and on its side.
tests/playwright/specs/viewport-matrix.e2e.js opens WeKan as an iPhone 15,
an unfolded Galaxy Z Fold 7, an iPad Pro 11, a 1366x768 laptop and a 55"
4K touch panel, each upright and on its side, and checks the board header,
board menu -> Original positions, Problems -> Memory Usage and Disk usage,
the Table, Calendar Mode, Time, Timeline and Map views, and dragscroll by
mouse or by finger. It measures that icons and texts are on screen and not
drawn in the colour behind them, that the page never scrolls sideways, that
every column can be reached and stays readable; screenshots of each step go
to .tools/log/viewports/. What it found, fixed:

- iPhone: the header bar was width: 100% plus its side padding as a content
  box, 24px wider than the screen, and the sidebar hamburger sat past the
  right edge. It measures its padding inside now.
- Admin Panel left menu on a phone: the list's disc bullets showed as a dot
  after every entry, and a rule between groups shrank to a dash beside an
  entry. No bullets, and each group starts a row of its own.
- Admin tables: 13 equal columns were 25px each on a phone and 42px on an
  upright iPad, every header one letter per line. Up to 1024px a column keeps
  a readable floor and the table scrolls sideways in its wrapper.
- Table view: the title-wrap and swimlane-group toggles were white icons on
  white - two empty squares - stretched to 72px by the global button padding.
  And on an upright iPad or foldable, Members, Labels and the date columns lay
  past the edge of an overflow-hidden board with nothing to scroll; the view
  scrolls sideways itself now.

A swipe that starts beside a list's resize handle resizes the list, as it
should, so the dragscroll check starts where a person pans. The file runs in
Chromium only: device emulation and the touch swipe exist only there.

Thanks to xet7 !
2026-10-17 04:05:49 +03:00
Lauri Ojansivu 0b6942687f diskMonitor: name the watched path, not an archive entry.
fixedVulnerabilityClasses reads entry.path as an archive entry and asks for
a containment check; the disk monitor's loop variable was only named that.

Thanks to xet7 !
2026-10-17 01:35:19 +03:00
Lauri Ojansivu f819932e60 CHANGELOG: Memory and Disk usage, Original positions, board view fixes, shared code.
Thanks to xet7 !
2026-10-17 01:24:22 +03:00
Lauri Ojansivu b9e28a8fbb Original positions: a right-sidebar view from the board menu, in every language.
Where each swimlane, list and card was put when it was made has been
recorded on creation for a long time, and shown by nothing: the two
templates for it were drawn nowhere, every string was English and they
printed raw ids, and the client helpers beside them were never called.

Board menu → Original positions (after Archive, open to everyone who sees
the board) opens originalPositionsSidebar: Swimlanes, Lists and Cards, each
by the name it has now, Moved when it is no longer where it was put, where
it was put by the names those have now, the title it was made with when that
changed, and when. The server already narrows the history to what the
reader may see, assigned-only roles included.

The text is existing translation keys plus one new one, original-positions,
translated into every language. models/lib/originalPositions.js builds the
groups and is tested without Meteor; the old templates, the per-item
component and the uncalled helpers are removed, and the design page is now
the feature's guide.

Thanks to xet7 !
2026-10-17 01:23:15 +03:00
Lauri Ojansivu b6a3d98736 Admin Panel / Problems: Memory Usage and Disk usage reports.
The RAM and Disk usage pages were designs that no code carried out. Both
monitors now run beside the CPU one and, like it, record only the start and
the end of a sustained high-usage period:

- Memory: RAM from /proc/meminfo's MemAvailable - os.freemem() is MemFree,
  which counts the page cache as used and shows a healthy server as full -
  and swap, which alone also starts a period. Without /proc/meminfo it uses
  what Node reports. Settings WEKAN_RAM_* and WEKAN_SWAP_HIGH_PERCENT.
- Disk: each filesystem WeKan writes to - the files root, its temp directory
  for uploads and the snap's data directory - measured once per device, with
  an episode of its own. WEKAN_DISK_WATCH_PATHS replaces the paths.
- Each report has a live line above its rows and a URL
  (/admin/problems/ram, /admin/problems/disk). The menu and titles use the
  existing memory-usage and sandstorm-disk-usage keys, which every language
  translates, and the Summary names the cpu, ram and disk streams.

Observe and report only, as designed. The three monitors share one event
recorder (server/lib/resourceLog.js) and one intEnv (models/lib/intEnv.js),
which three files had each defined. The pure halves are tested without
Meteor, and resourceMonitorsWiring checks the wiring and that no copy of
either shared piece returns.

Thanks to xet7 !
2026-10-17 01:14:16 +03:00
Lauri Ojansivu c046afa807 Guides for the Table, Calendar Mode, Time, Timeline and Map views.
Each was described only in Board Settings / Board View. Their guides are
written from the templates and the server code: what each shows, every
control, what can be changed and who may change it, where its settings
live, and the files and tests behind it. Board-Header/README.md links them,
boardViewSettings fails when a Board View entry has no guide of its own, and
TODO Later loses the gap and the code-to-combine item that is done.

Thanks to xet7 !
2026-10-17 01:02:44 +03:00
Lauri Ojansivu 0fff9d4828 Board view fixes, and failed calls reported in the reader's language.
- Calendar, Calendar Mode and Multi Board Calendar showed archived cards
  once the Archive sidebar had loaded them; every calendar selector leaves
  them out now.
- Timeline offered "Restore to this state" to every viewer, and a refused
  restore vanished, since nothing awaited it. The button is shown to those
  who may change the card, and a refusal is reported.
- Table: a swimlane header row spanned a fixed 11 columns, one short with
  all four date columns and too wide on a phone; it spans the columns the
  table has. Its add-card error was an English sentence.
- Map showed the raw "upload-failed" for a failed upload; it is translated.
- client/lib/errorMessage.js turns a failed call into a message in the
  reader's language: the translation when the error code is a translation
  key, otherwise the translated "Something went wrong" with the server's
  reason after it. 25 alert() calls with their own English fallback use it.

tests/errorMessageI18n.test.cjs runs the helper and fails on an alert()
that falls back to English again; calendarFilter tests the archived cards
for all four selectors.

Thanks to xet7 !
2026-10-17 01:01:34 +03:00
Lauri Ojansivu 483f3cd538 The file integrity scan waits for a quiet CPU again.
fileIntegrityScan.js asked cpuMonitor for currentCpuPercent, which it never
exported, so cpuPercent() was always NaN: the hourly scan never skipped a
busy hour and every finding recorded cpuPercent NaN. It reads
getCurrentCpu().percent now.

Five files imported names their modules do not export - UserAvatar,
AttachmentStorage and Settings as a named import - none used; they are
removed. tests/namedImportsExist.test.cjs checks every named import and
destructured require of an app module against what that module exports.

Thanks to xet7 !
2026-10-17 00:56:00 +03:00
Lauri Ojansivu 5908fab5de The Calendar and Multi Board Calendar views share their calendar options.
Both views wrote out the same FullCalendar options - first day of the week
and the week numbers that follow it, time formats, the one-row toolbar,
translated button labels, height, locale and direction - about fifty lines
each. models/lib/calendarBaseOptions.js builds them once, given the
translation, the user's first day and the display options, and each view
adds only what differs: dragging and selecting, navLinks, business hours
and its events.

The tests that read the toolbar or the direction from boardBody.js read the
shared module, and the Multi Board Calendar runtime test runs it for real.

Thanks to xet7 !
2026-10-17 00:54:02 +03:00
Lauri Ojansivu d444a76af2 One list drag-and-drop setup for a swimlane and the Lists view.
Template.swimlane and Template.listsGroup each built the same jQuery UI
sortable for their row of lists - options, placeholder sizing, dragscroll
pause and the autorun that follows the drag-handle and permission changes -
two copies of about fifty lines. initListsSortable builds it once and, as
listsGroup already did, replaces a sortable already on the element.

The guards that counted three list sortables count the shared one and check
that both templates call it; each is still gated by canModifyBoard().

Thanks to xet7 !
2026-10-17 00:51:54 +03:00
Lauri Ojansivu 9a73bf9173 Describe each card date once for its card and minicard badge.
Received, Start, Due and End were each written twice in cardDate.js, once
for the opened card (card*Date) and once for the minicard (minicard*Date):
the same field, the same colouring, the same title and the same editor,
eight copies of about 40 lines. CARD_DATE_KINDS describes each date once
and both templates are built from it; only the due date counts down, as
before. cardDate.js goes from 596 to 410 lines.

The tests that matched each template's own helpers block read the
CARD_DATE_KINDS entry instead, and still fail if Received, Start or End
gains the countdown.

Thanks to xet7 !
2026-10-17 00:50:41 +03:00
Lauri Ojansivu dd9832ce7e CHANGELOG: TODO Later lists what is built but unused, designed, undocumented or to combine.
Thanks to xet7 !
2026-10-17 00:48:11 +03:00
Lauri Ojansivu 0d3d0a407b CHANGELOG: cloning, visitor Scrum views, removed duplicate code, docs examples.
Thanks to xet7 !
2026-10-17 00:25:51 +03:00
Lauri Ojansivu a6aa3fdfb7 One state helper for the board's organization and team pickers.
The members widget and the add/remove organization and team popups of the
Right sidebar each built the same onCreated state by hand - error and
loading flags, a paged subscription and setError/setLoading/isLoading - five
copies of about 25 lines. initGroupPickerState builds it once; each picker
keeps the publications it subscribed to before.

Thanks to xet7 !
2026-10-17 00:24:59 +03:00
Lauri Ojansivu 73e815b4f4 Share the fixed list width helpers between the list and its header.
#5729's fixed (same) width for all lists is read by list.js and turned on
from the list header menu in listHeader.js, and each carried the same five
localStorage/profile helpers. They are client/lib/fixedListWidth.js now,
with the same storage keys, so a viewer's saved choice still applies.
tests/fixedListWidthShared.test.cjs fails if a client file grows its own
copy again.

Thanks to xet7 !
2026-10-17 00:23:19 +03:00
Lauri Ojansivu a896f3ed8c A visitor's Scrum board view opens as chosen, not as Swimlanes.
Without an account the browser keeps the board view, and client/lib/utils.js
had two hand-written copies of the view list for it: setBoardView stored
any of them, but storedBoardView's copy lacked Product Backlog, Sprints,
Sprint Report and Velocity, so those opened as Swimlanes after the reload.
Both now ask isKnownBoardView, i.e. BOARD_VIEWS, which removes the two
lists (about a hundred lines) and the drift with them.

The guards that counted view names in utils.js check the isKnownBoardView
calls instead, and boardViewMenu fails if a hand-written list returns.

Thanks to xet7 !
2026-10-17 00:20:32 +03:00
Lauri Ojansivu 7dc5399dc7 Remove code that a second copy replaced, and templates nothing draws.
- models/cards.js defined assignMember ... toggleAssigner twice in one
  Cards.helpers({}); in an object literal the last key wins, so the first
  twelve methods (about a hundred lines, with their own linked-board
  branches) never ran. models/users.js had two copies of the swimlane-height
  storage helpers, and the header's event map an empty
  'click .js-open-bookmarks' the real handler replaced. Each first copy is
  removed; what runs is unchanged.
- An Admin Panel attachment settings pane: attachmentSettings.jade (four
  templates) and about 300 lines of settingBody.js for S3, transfer limits
  and storage paths. No menu opened it and no template drew it; Admin Panel /
  Attachments (attachments.js) is the one in use.
- Templates drawn by nothing: dateBadge, dateCustomField and
  cardCustomFieldDate with its JS, orgAvatar and teamAvatar, the three
  *ModalTitle templates of My Cards, Due Cards and Global Search, boardLists,
  boardCreateRulePopup, addMemberPopupTest, and subtasksItems with the two
  templates only it used.
- CSS repeated word for word: accessibility.css opened with a copy of
  myCards.css's first 74 lines; list.css repeated its 768px collapsed-list
  block at 1024px and 1200px, which 768px already covers; boardHeader.css
  repeated its .mobile-mode rules inside a 801px media query. The later
  copies decided nothing, so the result is the same.

tests/noDuplicateObjectKeys.test.cjs parses every app and package file and
fails on an object literal that defines a key twice, and
tests/templatesAreRendered.test.cjs fails on a template nothing draws,
apart from runtime-built names and three listed exceptions. Tests that read
the removed copies now read the code that runs.

Thanks to xet7 !
2026-10-17 00:19:00 +03:00
Lauri Ojansivu 7e29641111 Await the member mapping in cloneBoard.
getMembersToMap (models/wekanmapper.js) is async, and cloneBoard kept its
Promise and called membersMapping.forEach on it, so every clone through
that method stopped with "membersMapping.forEach is not a function". The
board menu clones with copyBoard, so this was the cloneBoard method only.

WekanCreator carried an identical getMembersToMap that nothing called; it
is removed, leaving the one in wekanmapper.js.

tests/asyncResultAwaited.test.cjs resolves, file by file, every variable
assigned from an async function - declared there or imported async - and
fails when it is then used as an array or object without await.

Thanks to xet7 !
2026-10-16 23:54:39 +03:00
Lauri Ojansivu 11babe27f2 Docs: snap keys, setting guide and Docker examples that match this WeKan.
- Three snap set lines named keys the snap does not have, which snapd
  stores without a word: MONGO_OPLOG_URL (the key is mongo-oplog-url),
  oauth-enabled (oauth2-enabled) and "snap set wekan set disable-mongodb".
  tests/docsSnapKeysExist.test.cjs checks every snap set in the docs
  against the KEY_ names in snap-src/bin/config.
- Adding-new-Snap-settings-to-code.md used setting names that do not
  exist (LDAP_ENABLED, LDAP_SEARCH_FILTER, a wrong KEY_ line), the edge
  branch and Travis CI. It now follows the real config and wekan-help
  entries, says that a login setting goes through the Admin Panel catalog
  and authEnv(), and lists the other platform files a setting belongs in.
- The LDAP and Traefik Docker examples ran MongoDB 3.2/4.0 with
  --smallfiles, which MongoDB 4.2 removed, and WeKan images from 2018. They
  use mongo:7 and ghcr.io/wekan/wekan:latest; Traefik stays at v1.7, since
  that guide's labels and traefik.toml are v1 syntax.
- The Windows page no longer sets RICHER_CARD_EDITOR, which WeKan does not
  read, and points to build.bat for building from source.

Thanks to xet7 !
2026-10-16 23:52:46 +03:00
Lauri Ojansivu 2e3cff4d7d CHANGELOG: LockoutBleed follow-up, loaded server features, notification attachments, docs by menu.
Thanks to xet7 !
2026-10-16 23:46:42 +03:00
Lauri Ojansivu 262f9194fc Show notification attachments, and translate six labels that showed their key.
The notifications drawer subscribed to 'notificationsAttachments', while the
server publishes 'notificationAttachments'. Meteor reports nothing for an
unknown subscription, so it never became ready and an attachment named in a
notification was never sent to the drawer.

Six template keys did not exist in en.i18n.json, so the page showed the key
itself: the Due group of the sidebar filter ("dueAt"), the Scrum sprint form
("end", "no-cards", "custom-field"), the Jira estimate field on import, the
passkey trash icon ("remove") and two webhook URL fields ("URL"). Each now
uses the existing key with that wording, so every language that already
translates it shows it translated.

tests/subscriptionsArePublished.test.cjs checks every subscribe() in the
tree against every publication, and tests/templateI18nKeysExist.test.cjs
every literal key of every template against en.i18n.json.

Thanks to xet7 !
2026-10-16 23:45:58 +03:00
Lauri Ojansivu b7589737f5 File docs/Features the way the WeKan menus are.
A feature's guide now sits under the menu it is used from, read from the
templates rather than from where somebody once put it: Header-Bar,
All-Boards, Pages, Board-Header (search and every Board View), Board-Sidebar
(Board Settings, Filter, Members, Labels, Archive), Rules, Swimlanes, Lists,
Cards, Member-Settings, Admin-Panel and Login-Page. Topics used from several
menus (editor, history, theme, translations, email, troubleshooting) are in
General.

- Each area has a README mapping its menu entries to their guides and to the
  template or server module that implements them. Board-Header/README.md
  lists all 40 Board View entries with the template boardBody.jade renders;
  five views (Table, Calendar Mode, Time, Timeline, Map) have no guide of
  their own yet and say so.
- Board-View.md listed 25 views while BOARD_VIEWS has 40; it now lists all
  of them with their English labels.
- Pages keep their file names. Where a released changelog links an old path,
  a "# Moved" page stays there and points to the new one; test-menu.sh skips
  folders holding only those.
- Undo was filed under Login; it is General/Undo now.
- Every link in docs, code comments, scripts and tests follows the move;
  docsLinksResolve checks them all.

Tests that read a doc follow it to its new place. boardViewSettings now fails
when Board-View.md or Board-Header/README.md differs from BOARD_VIEWS, or
names a template boardBody.jade does not render.

Thanks to xet7 !
2026-10-16 23:45:35 +03:00
Lauri Ojansivu 70dcc2a134 Remove files nothing uses, and point their tests at the code that runs.
Found by following every import, re-export and require from client/main.js
and server/main.js (meteor.mainModule: only what that reaches is loaded) and
each local package's package.js:

- packages/wekan-fullcalendar/fullcalendar/ (88 files) and update.sh: the
  vendored FullCalendar v3 and the script that updated it as a git
  submodule; the package takes FullCalendar 5 from npm (Npm.depends).
- packages/wekan-fontawesome/fontawesome-free: js/, sprites/, metadata/,
  attribution.js and the .min.css copies; package.js adds only the plain CSS
  and the webfonts. Its package.json no longer names the removed files.
- models/lib/propagateMembers.js and models/lib/teamBoardMemberSync.js:
  replaced on 2026-10-14 by models/lib/groupBoardMembership.js (memberships
  that record their team or org, #4178), which the server uses. Their tests
  exercised the old modules; the same scenarios now run against planGain /
  groupChanges, and the wiring checks are kept.
- models/lib/recoveryPlan.js and its test: a model of the SQLite recovery
  policy that nothing ran; releases/ferretdb/sqlite-recovery.mjs carries it
  out and tests/sqliteVerifiedRecovery.test.cjs tests that. The Recovery
  design page says so.
- packages/wekan-ldap/server/configResolver.js: a vendored copy nothing
  imported - the LDAP package reads its settings through the app's
  accessor. Its sync test is now a check that no package carries such a
  copy; three comments that sent readers to it explain the reason instead.

docs/Design/TeamsOrganizations.md also said losing a team removes nothing;
since #4178 a team takes back what it gave.

Thanks to xet7 !
2026-10-16 22:54:48 +03:00
Lauri Ojansivu 3f3a61df0c Load three finished server features that were never imported.
package.json sets meteor.mainModule (since 2026-03-22), so the server runs
only what an import chain from server/main.js reaches. Three modules added
after that were never imported, so they never ran, while their tests - which
read the file - passed:

- server/loginTimingNormalization.js: the DDP `login` half of the LockoutBleed
  timing fix (GHSA-2g94-9x3m-hv37). Only the REST login was equalized, so the
  web login's response time still told whether a username exists.
- server/cardRecurrenceSchedule.js: the hourly job behind a card's
  Recurrence setting; the setting was saved and nothing ever recurred.
- server/productNameCache.js: the Admin Panel product name for the snap's
  pages shown while the database is down.

They are imported in server/imports.js beside their related modules. The
Meteor internals the login handler uses (Accounts._loginHandlers,
_checkPasswordAsync, _findUserByQuery) exist in this Meteor release.

tests/serverModulesLoaded.test.cjs follows the import chain from
server/main.js and fails when any server module that registers something at
load time - a startup hook, methods, publications, a cron job, a login
handler or a route - is not reached; it fails on the old imports.js.

Thanks to xet7 !
2026-10-16 22:35:18 +03:00
github-actions[bot] 615d0bb0b8 Bump versions for v12.49 2026-10-16 12:26:53 +00:00
Lauri Ojansivu 68f4e21f98 Prepare v12.49 release 2026-10-16 15:25:40 +03:00
Lauri Ojansivu b2705905e9 CHANGELOG: custom roles apply in every board view.
Thanks to xet7 !
2026-10-16 15:23:24 +03:00
Lauri Ojansivu 1020cd7e86 Custom roles apply in every board view: verified, tested and documented.
A custom role from Admin Panel -> People -> Roles reaches every board view
through the same two questions as a built-in role: what the member may do is
asked of the capability table, which answers from the custom role, and which
cards they see is asked of the member's flags, which the custom role writes.
fallbackRoleFor() picks only among built-in roles that see no more cards, so
a role whose "Sees all cards" is off always mirrors an assigned-only role and
one with it on never does. The flags are written when a member gets the
role, rewritten on every board when the role is saved, replaced with the
custom role by setMemberPermission, and kept when the role is deleted; no
other code writes them.

tests/boardRoleViews.test.cjs builds a member for all 32 capability
combinations as the server writes it and checks that the card scope, the
card rule, REST reads and writes, button rules and the real Email View
selector narrow them exactly when the role does not see all cards, and that
nothing sets a role flag without the custom role following.
server/lib/tests/customBoardRoles.tests.js turns "Sees all cards" off and on
for a role in use and checks its member becomes assigned-only and back
(meteor test; not run here). docs/Features/Members/Roles.md explains it in
"Custom roles in every board view".

Thanks to xet7 !
2026-10-16 15:23:16 +03:00
Lauri Ojansivu 7dbde2ea83 CHANGELOG: document how board roles apply to every board view.
Thanks to xet7 !
2026-10-16 15:19:33 +03:00
Lauri Ojansivu 63117b61d9 Docs: how board roles apply to every board view.
docs/Features/Members/Roles.md gains "Roles in every board view": a role is
the member's on the board, not the view's, so every view, the API and every
write ask the same code. A table names each of the 40 board views and the
server source it reads - the board publication, the table page, Scrum, board
status, activities, the Email and Chat views - and what an assigned-only
member gets from it; the board report charts are refused to them, as exports
are, because they are computed from every card. Then search, All Boards,
notifications, position and change history, exports, REST and writes, what
is recorded in Admin Panel -> Problems, and the tests that keep a new view
from skipping the rule. The old "Which cards they see" paragraph, which
named only the board publications, points to it.

Thanks to xet7 !
2026-10-16 15:19:26 +03:00
Lauri Ojansivu 486b2b3034 CHANGELOG: board role permissions apply to every board view (AssignedBleed follow-up).
Thanks to crypto-nidh, binary-lover and xet7 !
2026-10-16 13:04:34 +03:00
Lauri Ojansivu 5b2ed17e6b Board role permissions apply to every board view and every path to a card.
AssignedBleed follow-up, GHSA-r4gm-mjj9-g3x8. An assigned-only member
(isNormalAssignedOnly, isCommentAssignedOnly, isReadAssignedOnly, and custom
roles through those flags) sees only the cards assigned to them. The Board
Email View checked board.isVisibleBy() alone, so it published the title,
description and email metadata of every card in a list or label, counted
them, and searched them. It now merges assignedOnlyCardScope into its folder
and search selector for the page and the count, and a reply on another card
is refused.

An audit of every path that returns or changes a card found the same shape -
"a member of the board, so every card of it" - elsewhere, all fixed here:

- reads: the board publication's ancestor cards, linked source cards with
  their subtasks and dependency targets, and comment reactions; All Boards
  tile counts; a board's activities from linked boards; the Chat view and
  the All Boards feed, which also let card MEMBERS through (assignees is the
  rule); My Attachments' card; GET /api/user/cards; position history of a
  card; the cross-board subtask picker; Scrum rollover counts; and the
  notification publications, which kept sending the full documents of cards
  named in old notifications - also to members removed from the board;
- writes on a card the member cannot see: the canonical
  canEditCardOrLinkedCard / editableCardIds (every Cards.allow update,
  checklists, items, attachments, text notes, custom fields, the shared
  description, card methods) now ask the card - the role's `write` is a
  board answer, and the card is read when a caller passed it without
  assignees; DDP and REST comments, button rules on a card, checklist moves,
  issue state, watching, the card-opened webhook, moveBoardObjects,
  moveSwimlane and attachment moves;
- REST writes: checkBoardWriteAccess refuses assigned-only members, as
  checkBoardAccess already does for reads - those routes act on any card,
  list or swimlane by id. The GHSA-f396 canary still fires for a role
  without the write capability.

A refusal no view would produce is recorded under AssignedBleed in Admin
Panel -> Problems (authz.assigned, medium: never disables); opening a view,
searching and REST calls are narrowed or refused without a record.

tests/boardRoleViews.test.cjs runs the Email View selector, pins every path
above, and fails when any server file that serves card data has no role
rule and no stated exemption; the "assignee or card member" rule is
rejected anywhere in server/ and models/. server/lib/tests/editableCardIds
adds an assigned-only case for both edit checks (meteor test; not run here).
Seven guards that pinned the old lines now pin the new ones.

Thanks to crypto-nidh, binary-lover and xet7 !
2026-10-16 13:03:34 +03:00
github-actions[bot] 83e39fc8d9 Bump versions for v12.48 2026-10-15 20:30:17 +00:00
Lauri Ojansivu f89ce1950c Prepare v12.48 release 2026-10-15 23:28:46 +03:00
Lauri Ojansivu 303b4a09b0 CHANGELOG: release workflow hardening and the bandit findings, found by Qlty.
Thanks to Qlty and xet7 !
2026-10-15 23:20:15 +03:00
Lauri Ojansivu ee2a657977 Python: the five bandit findings Qlty reports.
- B324 docs/Features/Login/ldap-sync/ldap-sync.py: the MD5 of an LDAP photo
  only notices a changed photo, so it says usedforsecurity=False.
- B202 tests/nextcloud/bootstrap.py: the downloaded WeKan release zip is
  extracted only after every member is checked to land inside the target
  (zipfile also refuses '..' and absolute names; now it is explicit).
- B102 tests/backfill-charts-pagination.py: exec of this repository's own
  backfill-charts.sh planner under test, in an empty namespace with the
  network patched out - kept, and marked with its reason.
- B108 tests/release-telemetry.py: '/tmp/check-telemetry.py' is a fake
  __file__ for the path logic; nothing is written there - marked.
- B106 tests/ucsRelease.test.py: the fake portal's expected value is a test
  fixture, passed through a variable rather than as a password literal.

bandit reports none of the five now; the three tests pass.

Thanks to Qlty and xet7 !
2026-10-15 23:19:57 +03:00
Lauri Ojansivu 7f7cc63cde GitHub Actions: no expressions in scripts, actions pinned, least privilege.
Qlty (zizmor) security findings in the workflows that build and publish
every release:

- template-injection (128): ${{ ... }} pasted into a run: script is expanded
  before the shell sees it, so a crafted workflow_dispatch input, tag or step
  output becomes shell code. 251 expressions in 13 workflows now reach their
  scripts through the step's env: and are used as quoted shell variables
  ($env:NAME in the PowerShell steps); ${{ env.X }} is plain $X. Where a
  value sat in single quotes it is double-quoted now, and six unquoted ones
  are quoted.
- unpinned-uses (20 in Qlty, 160 with current zizmor): every action is
  pinned to a commit, with its version as a comment for Dependabot.
  actions/dependency-review-action@v5 was a branch; it is v5.0.0.
- excessive-permissions (8): AppImage, Flatpak, mac, windows,
  release-all and release-all-missing grant contents: read at the workflow
  level, and each job that writes (a release asset, a git push, the docker
  push, gh run rerun) is granted that on the job. Nothing used actions:
  write except recover, which keeps it.
- artipacked (47 -> 13): actions/checkout gets persist-credentials: false,
  except in the jobs that push with the checkout's credentials, directly or
  through releases/version.sh, release-website.sh, release-charts.sh and
  backfill-charts.sh.

actionlint with shellcheck reports 50 notes, the 56 before minus the six
quoted. Every PowerShell step parses (pwsh 7.6.7). Not run on GitHub.

tests/workflowHardening.test.cjs fails on an expression in a run script,
an unpinned action, a workflow-level write grant in a multi-job workflow,
or a checkout keeping credentials outside the pushing jobs, and runs
zizmor when it is installed. Eleven guards that pinned the old text, or ran
the extracted scripts with the expressions substituted, now pin the env
form and pass the values as the step's environment.

Thanks to Qlty and xet7 !
2026-10-15 23:19:50 +03:00
Lauri Ojansivu 4ea6f5b90c CHANGELOG: AssignedBleed follow-up, assigned-only members' file and document reads.
Thanks to crypto-nidh and xet7 !
2026-10-15 22:42:40 +03:00
Lauri Ojansivu 7f3428db8e Keep assigned-only members to their own cards' files on every read path.
AssignedBleed follow-up, GHSA-3cgr-47wm-9cc9. An assigned-only member
(isNormalAssignedOnly, isCommentAssignedOnly, isReadAssignedOnly) sees only
the cards assigned to them. 1a218ead02 applied that to the attachment API,
but six other paths asked only for board membership, which such a member
has:

- the /cdn/storage/attachments and /cfs/files/attachments downloads
  (universalFileServer isAuthorizedForBoard) served any card's file;
- Attachments.protected, ostrio:files' own download check, likewise;
- the legacy /cfs/files/attachments route likewise;
- searchAttachmentDocumentText matched the extracted text of every card's
  PDF/DOCX/XLSX/PPTX on the board and returned snippets with their
  attachment and card ids - no id needed, and it supplied the ids for the
  downloads;
- legacyBoardAttachments without a cardId published every legacy
  attachment record of the board;
- linkedListCards published every card of the source list.

The downloads now also ask mayReadBoardAttachment(), which names the
refusing path in Admin Panel -> Problems (authz.assigned, medium: never
disables). The document search and the legacy publication are narrowed to
the member's cards (assignedOnlyCardIds); asking the publication for a card
not assigned to them is refused and recorded, as card details always pass
the open card. Linked-list cards take the source board's
assignedOnlyCardScope; searching and viewing a linked list are ordinary
use, so those are not recorded.

The board's own files - background (sidebar or API upload) and map image -
carry no card and are part of the board every member sees; they stay
readable, where the attachment API refused them to an assigned-only
member. Any other card-less file is still refused.

tests/assignedOnlyReads.test.cjs pins each path, the board-file rule and
the refusal source, and fails when any file in server/ or models/ that
serves attachment or document data lacks the rule without a stated
exemption. The Playwright spec checks both downloads refuse the hidden
card's file but not the member's own, that the search finds only the
member's card, and that the owner is unaffected. Four source guards that
pinned the old lines now pin the new ones.

Thanks to crypto-nidh and xet7 !
2026-10-15 22:42:14 +03:00
Lauri Ojansivu ceec2e9c5a CHANGELOG: Caddyfile examples are Caddy 2, and the snap's Caddy proxies to WeKan's port.
Thanks to Group-Deed and xet7 !
2026-10-15 22:06:02 +03:00
Lauri Ojansivu 37d2397603 Caddyfile examples are Caddy 2, and the snap's Caddy proxies to where WeKan listens.
The snap runs Caddy 2, and migrate-caddyfile-v1-to-v2.sh treats a Caddyfile
that `caddy adapt` rejects as Caddy v1: it moves it to Caddyfile.v1.bak-<n>
and, when it cannot convert it, writes the default in its place - on every
start. 19 Caddyfile examples in the docs were Caddy v1 (`proxy / localhost:3001
{ websocket transparent }`, `browse`, `fastcgi`, `tls self_signed`, `ext`,
v1 `log`/`errors`, `root /path`), so a file copied from the snap install page
was renamed each time it was put back. All are now Caddy 2 (reverse_proxy,
file_server browse, php_fastcgi, tls internal, try_files, log output) and pass
`caddy adapt` with Caddy 2.11.7. Also:

- `http://X https://X { tls ... }` became `X {`: Caddy 2 refuses TLS on the
  http address and redirects to https by itself;
- the Windows examples `load` the certs folder - Caddy 2's load takes folders;
- the Docker Meteor3 Caddyfile gave the Let's Encrypt and CloudFlare variants
  of one site the same host, which Caddy 2 refuses as ambiguous; each has its
  own host now;
- Caddy.md's logs example and its systemd unit, installer and setcap are
  Caddy 2 (`caddy run --config`, no getcaddy.com, no -conf/-agree/-email).

The fallback template the migration writes proxied to localhost:3000. With
Caddy enabled the snap runs WeKan on caddy-bind-port (default 3001), so a
migrated site pointed at nothing. Both templates now read the snap settings
from caddy-control's environment - `:{$CADDY_PORT:8080}` and `reverse_proxy
localhost:{$CADDY_BIND_PORT:3001}` - so `snap set wekan port=...` reaches
Caddy on the restart the configure hook already does; that hook's sed lines
only ever matched Caddy v1 lines. The commented TLS example in the template
had `protocols h1` inside a site and a stray `}`, so uncommenting it failed;
it uses `alpn http/1.1` now.

tests/caddyfileExamplesV2.test.cjs finds every Caddyfile in the docs and the
snap, fails on Caddy v1 syntax and on a Caddy 1 command line, checks the
templates' ports, and with a Caddy 2 binary runs `caddy adapt` on all 30.
snapStagePackages.test.cjs pinned the wrong port 3000 and now pins 3001.

Thanks to Group-Deed and xet7 !
2026-10-15 22:05:49 +03:00
Lauri Ojansivu 43be0345e6 CHANGELOG: LDAP background sync imports users and keeps its interval.
Thanks to tromlet and xet7 !
2026-10-15 20:46:03 +03:00
Lauri Ojansivu a944be1817 LDAP background sync imports users again, and keeps the interval it is given.
After the 12.47 fix, #6771 comment 6197931378 showed the background sync
running and importing nobody: "filter": "(&(&(objectclass=person))(uid=\2a))",
"Search result count 0". importNewUsers() asked for every user with the string
'*', and the RFC 4515 escaping added for login names on 2026-09-27
(679a8b3499) turned the wildcard into a literal asterisk. The wildcard is now
reachable only through ALL_LDAP_USERS (userSearchValue.js), a Symbol that a
login request cannot carry, so a typed '*' is still escaped.

The sync also ran far more often, or less often, than set: later.js is a
calendar, and every(n).minute() is "minutes 0, n, 2n ... of each hour", an
interval only when n divides 60. 2 hours and 1 day both became hourly, 90 s
became every minute, and 3600 (seconds, typed as milliseconds) every 4
seconds. Milliseconds now become an interval later keeps exactly - seconds
and minutes dividing 60, hours dividing 24, whole days - pinned to the start
of the unit; anything else is rounded to the nearest one with a warning, and
an interval under a minute is warned about as probably seconds.

tests/ldapImportAllUsers.test.cjs reproduces the reported filter and checks
the wildcard, the still-escaped typed '*', and that no code searches the
string '*'. tests/ldapSyncSchedule.test.cjs checks every interval up to 36
hours reaches later only as an every(n) it keeps, and with the real later.js
that 1 min ... 1 week keep their spacing.

Thanks to tromlet and xet7 !
2026-10-15 20:45:49 +03:00
Lauri Ojansivu bd7f9d2848 Merge remote-tracking branch 'origin/main' 2026-10-15 16:39:50 +03:00
Lauri Ojansivu 62166680f1 CHANGELOG: Windows and NetBSD bundle builds.
Thanks to xet7 !
2026-10-15 16:37:36 +03:00
Lauri Ojansivu fa62479245 NetBSD bundle: node-gyp finds pkgsrc's Python.
The NetBSD x64 bundle of v12.47 stopped in npm install: argon2's
native build ran node-gyp, which looks for `python3` or `python`, and
pkgsrc's python313 installs only /usr/pkg/bin/python3.13 - "You need to
install the latest version of Python". OpenBSD's package provides
python3, so only NetBSD failed.

releases/bsd/build-bundle.sh now, when neither is on PATH and PYTHON is
not set, hands node-gyp the versioned interpreter it finds
(/usr/pkg/bin/python3.N, skipping python3.N-config) through PYTHON and
npm_config_python, before the npm install; with no Python at all it
stops with an ::error:: naming the package. Simulated locally with a
pkgsrc-shaped directory and no python3 on PATH; with python3 present
nothing changes. tests/releaseScriptsUtf8.test.cjs pins the lookup and
its place before the install.

Thanks to xet7 !
2026-10-15 16:37:18 +03:00
Lauri Ojansivu 11f97d016e Windows builds: the release scripts read and write text as UTF-8.
All three Windows bundle builds of v12.47 stopped at the risk audit:
"Automated risk indicators found: server/lib/localizeAvatar.js: new URL
origin http://127.0.0.1". The file had not changed. risk-audit.py read
its baseline with a bare read_text(), which on a Windows runner decodes
cp1252, so the baseline's "http://127.0.0.1/…" (a comment in that file)
became "http://127.0.0.1/…" and no longer matched the source. With a
non-UTF-8 default on Linux (LC_ALL=C, -X utf8=0) the old script misread
the baseline the same way and counted all 16688 files as changed; the
fixed one reads it as the build does.

Every text read and write in releases/*.py now names UTF-8: the risk
audit, the telemetry check, remote-release.py (which also reads and
rewrites CHANGELOG.md, with its ®), the dependency, UCS, Nextcloud, Bun,
Mac and Snap scripts, and the GITHUB_ENV/GITHUB_OUTPUT writes.
tests/releaseScriptsUtf8.test.cjs fails on any read_text(), write_text()
or text-mode open() in releases/ without an encoding, multi-line calls
included; tests/bun-release.py follows the checksum write's new
argument. Every Python and node test of these scripts passes.

Thanks to xet7 !
2026-10-15 16:37:18 +03:00
github-actions[bot] 15f1811c97 Bump versions for v12.47 2026-10-15 13:12:13 +00:00
Lauri Ojansivu d390aba29c Prepare v12.47 release 2026-10-15 16:11:02 +03:00
Lauri Ojansivu 83c2ca9985 Three guards follow the restore and 6.09 test changes.
- sidebarArchives.js imports its own template: cardDetails.js now
  imports it for restoreArchivedCard, and a file that registers on a
  template it does not import may run before the template exists
  (clientBundleImports).
- The 6.09 restore spec makes its test ObjectIds with crypto.randomBytes,
  not Math.random (codeqlInsecureRandomness).
- Tigre's new restore strings say ወረቀት ካርድ for card and ምዱድ for board,
  the file's own words, not the bare Tigrinya ካርድ and ሰሌዳ.

Thanks to xet7 !
2026-10-15 13:29:53 +03:00
Lauri Ojansivu ee9ff3bedb CHANGELOG: Restore from Archive into a full list (#6772).
Thanks to jullbo and xet7 !
2026-10-15 13:16:22 +03:00
Lauri Ojansivu b9edbcc858 Restore from Archive into a full list asks for another list, instead of doing nothing.
Restoring an archived card whose list had reached a hard WIP limit did
nothing at all: the Restore handlers checked canBeRestored() and, when it
said no, returned without a word or a method call, and Restore all
skipped such cards just as quietly.

- Restore, in the Archive sidebar and in an open archived card, now goes
  through one function (restoreArchivedCard): a list that is full - or
  gone - opens the restore-to-list popup, which says the list has reached
  its WIP limit and offers only lists with room; with none left it says
  so instead of offering a list that would fail again.
- Restore all restores every card that has room and then says how many
  it left out and why. Its popup opens against the link kept before the
  loop: after the awaits the browser has cleared evt.currentTarget.
- canBeRestored() decides with models/lib/wipRestore.js, which also
  blocks a list that is OVER its hard limit: the old `===` let a card
  into a list whose limit had been lowered below its count. A soft limit
  still only warns.

Tests: tests/wipRestore.test.cjs (the rule, over-limit and soft/disabled
negatives, both Restore paths, Restore all's count, and a scan that no
client code skips a blocked restore without a word);
restore-wip-limit.e2e.js against the production bundle (restore into
another list with the full one not offered, Restore all restoring what
has room and naming the rest, a soft limit restoring at once), 12 of 12
over three repeats with the existing archive restore test. The four new
strings are translated in every locale.

Thanks to jullbo and xet7 !

Fixes #6772.
2026-10-15 13:16:14 +03:00
Lauri Ojansivu 9e1d5f65bd LDAP: the sync interval crash on Snap 12.46 is fixed (#6771).
"[uncaughtException] WeKan is stopping: TypeError: string.toLowerCase is
not a function" from later.parse.text, called by wekan-ldap's sync
schedule, with the snap restarting in a loop - the report on Alma Linux
8.10 with LDAP sign-in. A numeric LDAP_BACKGROUND_SYNC_INTERVAL reached
later's text parser as a Number; 7dfd942ff6 reads milliseconds or
later.js text and never throws, and tests/ldapSyncSchedule.test.cjs
reproduces this exact error with the real later.js.

Thanks to tromlet and xet7 !

Fixes #6771.
2026-10-15 12:54:31 +03:00
Lauri Ojansivu 025630b552 Tests: a board restored from WeKan 6.09 loads.
The upgrade report (one board with attachments loading forever after a
6.09 to 10.85 dump and restore) came without the board's data. The
fixture tests/fixtures/restored/wekan-6.09-board.json is that board's
SHAPE instead, derived from `git show v6.09:models/...`: CollectionFS
attachments (cfs.attachments.filerecord with its GridFS file and chunk),
a card cover pointing at a filerecord, lists with swimlaneId '', members
without the newer role flags, customFields by boardIds, checklists,
comments and activities as 6.09 wrote them.

restored-609-board.e2e.js restores it into the test database as it is
and requires the board to load: the board publication becomes ready with
the board and its cards, and the page shows its lists and cards, not the
spinner or the "could not be loaded" message. It passes against the
production bundle - and also against a bundle built without the client
cover fix, so neither the 6.09 data shape nor that fault is what hung the
reported board; what did is particular to that board's data, which the
board's page now names through the server log.

Thanks to xet7 !
2026-10-15 04:34:03 +03:00
Lauri Ojansivu defb110b18 CHANGELOG: search results of the search typed; browser specs on the bundle.
Thanks to xet7 !
2026-10-15 04:14:21 +03:00
Lauri Ojansivu 49344f32f4 Email view and Public Boards: a search shows its own results, never the last one's.
Found running the Email view's browser test against the production
bundle: once in eight runs, searching for text that matches nothing still
listed the previous search's card. Both views subscribe in an autorun, so
a new search's subscription starts before the old one stops, and both
published their page's ids into ONE shared index document; DDP's merge
box shows the older subscription's value while both exist, so whenever
the old one stopped late the new search showed the old rows.

Each request now has its own index, keyed by its arguments
(models/lib/reportPageIndex.js reportPageKey), built the same way on the
server and the client (emailViewPageArgs normalizes the Email view's
folder, page and search), and each view reads only the index of the
request it made. Admin Panel / Problems stops its subscription before the
next one, so its requests never overlap and keep a fixed id.

Tests: tests/reportPageKey.test.cjs (one id per request, server and
client agree, no autorun view reads a shared id); emailView and
publicBoardsPage pin the keyed ids. Against a rebuilt bundle the Email
view test passed 8 of 8, and with the Public Boards, Chat, Version
Control and import specs 30 of 30.

Thanks to xet7 !
2026-10-15 04:14:10 +03:00
Lauri Ojansivu 160110d1b3 Tests: the Email and Chat browser specs run against the production bundle.
Run against a freshly built bundle, as build.sh's browser run does, two
faults in the spec itself showed:

- Email view: Compose sends real mail, but the spec started no mail sink,
  so Send answered "The email could not be sent" in any run without an
  SMTP server. It catches the mail locally now, as the other mail specs do
  (helpers/smtpSink.js on WEKAN_TEST_SMTP_PORT), and checks that the
  message reached the recipient under its subject.
- Chat view: after page.reload() the new connection signs in again from
  the login cookie, and openView() called setBoardView before that, so on
  a fast server it failed with "Must be logged in". It waits for the
  sign-in, and a refused call names its code and reason instead of the
  minified "r".

Against the bundle: the Email, Chat, Version Control attachments and
import Attachment files specs pass (Chat 3 of 3 repeated, Email 5 of 5).

Thanks to xet7 !
2026-10-15 03:59:46 +03:00
Lauri Ojansivu 66b0952687 CHANGELOG: LDAP sync interval, attachment rename.
Thanks to B2RN and xet7 !
2026-10-15 03:28:19 +03:00
Lauri Ojansivu 515e63d322 Attachments: renaming one without extensionWithDot keeps its extension.
Running the new browser tests showed it: the rename popup appended
this.extensionWithDot, which a migrated attachment can lack, so the file
became "architectureundefined". It falls back to the extension now.

The browser tests were run against a local source-mode server: the
Version Control attachments spec (which now asks for the storage paths
as an administrator, for that call only) and the import Attachment files
spec pass; the Chat channels spec expects no unread message once the card
was opened in the middle, which reads it.

Thanks to xet7 !
2026-10-15 03:17:49 +03:00
Lauri Ojansivu 7dfd942ff6 LDAP: a sync interval in milliseconds no longer stops WeKan.
"[uncaughtException] WeKan is stopping: TypeError: string.toLowerCase is
not a function at later.parse.text ... at Object.schedule
(wekan-ldap/server/sync.js)", in a snap restart loop. The docs give
LDAP_BACKGROUND_SYNC_INTERVAL in milliseconds, settings_get() turns a
numeric value into a Number, and the schedule handed it straight to
later's text parser, which threw from a timer. Text later could not read
did not throw, but made a schedule that never ran - as a quoted
'every 1 mins' from a compose file did, the second report in #2354.

packages/wekan-ldap/server/syncSchedule.js reads the setting and never
throws: milliseconds (a Number or digits) become that interval, later.js
text is read as before, one pair of surrounding quotes is removed, unset
keeps the original default (recur().on(0).minute()), and anything
unreadable logs why and uses the default. The docs, the snap description
and the compose files say both forms work, instead of "leave this unset
so it does not crash".

Tests: tests/ldapSyncSchedule.test.cjs reproduces the crash with the real
later.js from the build, schedules 3600000 ms an hour apart and 10000 ms
ten seconds apart, reads quoted text, keeps the default, never throws on
any value a setting can hold, and fails if any server file hands a setting
to parser.text directly.

Thanks to B2RN and xet7 !

Fixes #2354.
2026-10-15 03:17:49 +03:00
Lauri Ojansivu fae5886319 CHANGELOG: every node suite passes again.
Thanks to xet7 !
2026-10-15 02:58:53 +03:00
Lauri Ojansivu 75534b5f1a Translations: the last thirteen failing guards, fixed on both sides.
The guards that failed before this release's translation work, each
decided by which side is right:

- The locale was wrong: Bislama's email recovery strings say sanem, the
  word the guard pins; Arabic-script Uzbek writes "e" as ې (26 words);
  Romansh says "chaschuna ina perdita da datas", names its own Menu and
  uses one word for the template container; Tigre's card is ወረቀት ካርድ,
  never the bare Tigrinya ካርድ (six strings); Wolaytta's permanent-delete
  label still held English words, now in the file's own Wolaytta; and
  Tamazight's operator-assignee is the portable search keyword again -
  the 2026-10-13 label sweep had translated a parser keyword, so that
  correction is removed and the earlier one stands.
- The guard was stale: Acehnese settled on kad for card on 2026-10-14,
  so the example cards use the file's own word; Uzbek key names may be
  translated as long as the printed label stays beside them; the README's
  coverage count is 234 now; the Transifex pull test hands its own node to
  the script it runs.
- The last English values of nine locales are their own words or settled
  terms (Neapolitan and Volapük "No", Frisian and Flemish "is", Breton
  "Me", Walloon "Pause", Venetian "Sprint", Veps "minus"), recorded as
  reviewed shared words, so the completeness guards and --missing agree:
  0 languages with untranslated strings.

Thanks to xet7 !
2026-10-15 02:58:42 +03:00
Lauri Ojansivu ccc65e7d89 CHANGELOG: every language translated; TODO Later is empty.
Thanks to xet7 !
2026-10-15 02:22:55 +03:00
Lauri Ojansivu d085a30f2d Translations: the last English strings of 43 locales.
Keyboard key names, Blockly's math names, Scrum roles and the S3 and
webhook labels, which the earlier passes had kept in English: the
established locales translate them (Finnish Välilyönti, German
Leertaste), so these do too. Breton keeps the reviewed "Me", Walloon the
reviewed "Pause", Venetian its "Sprint", and Veps writes minus without
the Finnish long vowel. Low confidence for the key names of Nahuatl,
Buryat, Cherokee, Klingon, Tibetan, Dzongkha, Wolaytta and Tigre.

What stays equal to English is the language's own word (Neapolitan
"No", Frisian "is", Volapük "if"), or in Blockly an operating system,
sin/cos/tan, and key labels the language's keyboards print.

Thanks to xet7 !
2026-10-15 02:22:25 +03:00
Lauri Ojansivu 7cbbec8836 Translations: the remaining strings of 12 more locales, Blockly included.
Greenlandic, Kashmiri, Nahuatl, Tigre, Tibetan, Dzongkha, Oromo,
Wolaytta, Northern Sami, Yakut, Inuktitut and Cherokee: every value still
equal to English, translated directly from each language's existing
wording, placeholders checked against English, only English placeholders
filled. Low confidence for Nahuatl, Wolaytta, Yakut, Inuktitut and
Cherokee; a speaker's Transifex translation replaces any of them.

Thanks to xet7 !
2026-10-15 02:14:29 +03:00
Lauri Ojansivu d698d6a1d6 Translations: the remaining strings of 20 more locales, Blockly included.
Translated directly from each language's existing wording, placeholders
checked against English, only English placeholders filled: Tamazight,
Volapük, Acehnese, Wolof, Fijian, Guarani, Veps, Quechua, Chichewa,
Northern Ndebele, Tigrinya, Samoan, Aymara (its last string, the S3
bucket, too), Fula, Bambara, Tswana, Kirundi, Swati, Chuvash and Buryat.
Low confidence for Volapük, Veps, Buryat and Tamazight; a speaker's
Transifex translation replaces any of them. The Veps guard records "ei
%1" for Blockly's "not": Veps negates with ei, as Finnish does.

Thanks to xet7 !
2026-10-15 02:01:26 +03:00
Lauri Ojansivu 7c6a94c734 Translations: the remaining strings of 50 locales, Blockly included.
Every value still equal to English in these locales, translated directly
from each language's existing wording and kanban terms, Blockly's
editor messages included; placeholders checked against English before
anything was written, and only English placeholders filled, so no human
translation is replaced.

All of Klingon, Ewe, Walloon, Luganda, Tongan, Tsonga, Venda, Kinyarwanda,
and the remaining strings of Bashkir, Pashto, Cantonese, Igbo, Xhosa,
Bhojpuri, Romansh, Maori, Breton, Sorani, Faroese, Northern Sotho,
Asturian, Amharic, Akan, Kyrgyz, Sicilian, Burmese, Hausa, Yoruba,
Frisian, Kurdish, Cornish, Uyghur, Aromanian, Sinhala, Sindhi, Manx,
Neapolitan, Assamese and their regional variants. Low confidence for
the constructed and smallest of them (Klingon, Venda, Tongan, Manx,
Cornish, Aromanian); a speaker's Transifex translation replaces any of
them. Walloon writes tangent as tandjinte, not the Waray-shared French.

Thanks to xet7 !
2026-10-15 01:45:28 +03:00
Lauri Ojansivu 0a58d618ce Tests: archiveSection allows the board subscription's onStop.
The board page passes an onStop callback since a failed load is shown;
the guard still pins that it asks for the live lists (false).

Thanks to xet7 !
2026-10-15 01:38:13 +03:00
Lauri Ojansivu 3c92954ab9 CHANGELOG: import attachment files, board load errors; TODO Later down to translations.
Thanks to xet7 !
2026-10-15 01:28:42 +03:00
Lauri Ojansivu 71b862c28c Tests: two guards follow the Chat view and the live-test log fixes.
customBoardRoles pinned Chat's comment check on the open board; the Chat
view opens a card of any board now, so it asks the card's board, and
the guard says so. liveTestsNoSecretLogs built a RegExp escaping only
`$`; it escapes every metacharacter (incompleteEscaping).

Thanks to xet7 !
2026-10-15 01:27:40 +03:00
Lauri Ojansivu f4126b9514 Translations: attachment files on the import page, a board that failed to load.
The five strings in every locale, translated directly from each
language's existing wording; English variants keep English. Bosnian
uses tabla for board and Tigre its established word for name, as their
guards require.

Thanks to xet7 !
2026-10-15 01:27:40 +03:00
Lauri Ojansivu 31615d7f98 Board: a failed load says so; 6.09 attachments no longer break covers.
An upgrade report: after a 6.09 to 10.85 dump and restore, one board with
attachments showed only the loading animation. The data is not
available, but the code had three faults on that path:

- The board page waited for its publication to be ready and had no
  onStop, so a publication that stopped with an error left the spinner
  up forever. It now shows "This board could not be loaded", pointing at
  the server log, which names the cause.
- The client cache fell back to the legacy (CollectionFS) attachment
  lookup, an async stub on the client: its Promise was taken for an
  attachment, so a 6.09 card's coverId became a cover whose link()
  threw, and the plural lookup handed callers a Promise for an array.
  The client no longer calls it; the server cache awaits the real one.
- The legacy attachments publication called startsWith() on a record's
  type, which throws when a 6.09 record holds something other than a
  string; it is read as a string.

Tests: boardRefreshReadiness drives the error path (shown, cleared on
another board, ignored from a board left behind); legacyBoardLoad pins
that no client cache calls an async lookup and that the server awaits
every one. The board itself is not reproduced: that needs the data.

Thanks to xet7 !
2026-10-15 01:27:37 +03:00
Lauri Ojansivu 51238ed8ad Import: attachment files of Jira, Nextcloud Deck, OpenProject, Asana and GitLab.
Their exports name attachments without carrying the files, so the files
were reported as a loss. Each service documents where a file is
downloaded, behind the same sign-in as its API:

- Jira: fields.attachment[].content (/rest/api/3/attachment/content/<id>),
  Basic email:API token; Jira's redirect to its media store is followed
  without the sign-in.
- Deck: /index.php/apps/deck/api/v1.0|v1.1/boards/../stacks/../cards/..
  /attachments/<id>, Basic user:app password, OCS-APIRequest: true.
- OpenProject: _links.downloadLocation, relative to the instance, Basic
  apikey:<key>.
- Asana: GET /attachments/<gid>?opt_fields=download_url with a personal
  access token, then the signed download_url without it; a file the
  export embeds (export_boards.pl) is taken as it is; one kept on Google
  Drive and the like is a link, reported.
- GitLab: a private project's /uploads/ link through
  /api/v4/projects/<id>/uploads/<secret>/<file> (17.4+), Bearer token.

The import page asks for the address, user and token under "Attachment
files" for these five (the REST import takes the same as attachmentAuth).
They go with that one import: held beside the creator in a WeakMap, never
stored or logged, the token field emptied on Import, and sent ONLY to
the origin the member typed - an export names any URL it likes. Every
download is the import's SSRF-guarded fetch, which validates each hop
and drops Authorization on a cross-origin redirect. A file that cannot
be fetched is on the loss report with its reason. Unticking Attachments
leaves them out (Jira's per-issue field is pruned too).

Zenkit is left out: it publishes no way to download a file.

Tests: importAttachmentAuth.test.cjs (each service's request, the
sign-in only to its origin, malformed sign-ins, never stored or
logged), the parser tests pin the attachment lists, a Meteor test
imports an embedded Asana file onto its card and shows a Jira file is
reported without a sign-in and that the sign-in never reaches an address
the guard refuses, and import-attachment-auth.e2e.js the page (not run
here). Not run against the live services.

Thanks to xet7 !
2026-10-15 01:27:26 +03:00
Lauri Ojansivu f95a891c06 CHANGELOG: Version Control view attachments.
Thanks to xet7 !
2026-10-15 00:51:15 +03:00
Lauri Ojansivu 132b341b8d Version Control view: Attachments have the card's controls, and open in the viewer.
The Attachments tab listed each file as a link to a new browser tab. A
file the attachment viewer can show now opens there on a click on its
thumbnail or name, with next and previous walking the tab's files, and
every file has the controls of an opened card's attachments: view,
download, rename, delete (the soft delete, behind the same confirmation)
and the cover / background menu, the writes for the same members as the
card's gallery.

Thanks to xet7 !
2026-10-15 00:51:09 +03:00
Lauri Ojansivu 7c1cffe4b6 CHANGELOG: Jira closed sprints; TODO Later without them.
Thanks to xet7 !
2026-10-15 00:46:51 +03:00
Lauri Ojansivu cc0b8c60ba Jira import: closed sprints, rebuilt from the issues.
Jira's sprint report has no documented API, so a closed sprint was
reported and skipped. It is now imported as closed, with its start and
close snapshots rebuilt from documented issue data: the Sprint field,
status and resolution at the sprint's start and completion dates, from
each issue's change history (expand=changelog), or from the issues as
they are now when there is none. The snapshots are marked partial. A
closed sprint with no start or completion date is still reported.

Thanks to xet7 !
2026-10-15 00:46:39 +03:00
Lauri Ojansivu 51e1a5c04d CHANGELOG: Chat view card in the middle, carets on folded panes.
Thanks to xet7 !
2026-10-15 00:38:04 +03:00
Lauri Ojansivu 230badf48e Chat view: a card from any board opens in the middle; folded panes keep their carets.
- Clicking a card in the channel tree took the member to that card's
  board. It now opens the card in the middle pane with its conversation
  beside it, whatever board it is on, and the page stays where it is. The
  card's conversation comes from a publication of its own
  (chatViewCardComments) with the tree's limits - a board the member sees,
  a role that reads comments, only their cards on an assigned-only board -
  and its writers' names; the comment box follows the card's own board.
- Folding the Channels or Comments pane left only an icon: the double
  caret stays now, pointing the way the pane opens, mirrored with the
  others right to left.

Tests: chatView.test.cjs (no navigation, the subscription, the card's
board for commenting, the publication's limits, the carets),
email-chat-views.e2e.js (another board's card in the middle without
leaving the page, carets visible on folded panes).

Thanks to xet7 !
2026-10-15 00:38:03 +03:00
Lauri Ojansivu 60317a8501 CHANGELOG: CodeQL #586, Jira sync from a Sandstorm grain, the closed issues.
Thanks to xet7 !
2026-10-15 00:33:59 +03:00
Lauri Ojansivu 1dc5b82369 Sandstorm: Jira List Sync works from a grain.
It was recorded as impossible: Jira signs in only with an Authorization
header, and the bridge proxy takes that header for the grain's grant. But
Sandstorm's HTTP driver can keep the sign-in in the grant itself
(shell/imports/server/drivers/external-ui-view.js in the Sandstorm
source): the member connects the Powerbox to "any address" and enters the
Jira address with the credentials in it - https://EMAIL:API-TOKEN@site
(the e-mail's @ as %40), or https://jira.example.org#TOKEN for a Server /
Data Center token - and Sandstorm moves them into the grant and adds
Authorization: Basic (or Bearer) to every request the grain sends through
it. WeKan never sees them.

- egress.mjs syncApiRoot grants a Jira list at its own address, and the
  Powerbox request asks for Basic authentication
  (ApiSession.PowerboxTag.authentication; apiSessionDescriptor takes it).
- In a grain the Jira fetcher sends no Authorization of its own, and a
  Jira source needs no WeKan token there: a marker credential says the
  grant holds the sign-in. Only for Jira - GitLab sends its token in the
  query, where a marker would reach GitLab.
- The List Sync popup tells a Jira list in a grain what to enter in the
  Powerbox, translated in all locales; the Sandstorm docs say how, and
  that the same #token form may let a private GitHub, Gitea or Forgejo
  through (not tried yet).
- TODO Later drops "Not possible from a Sandstorm grain", its last
  category naming an issue; changelogFormat no longer needs one.

Not run in a real grain here. Tests: sandstormSyncEgress.test.mjs (the
root and the Basic descriptor, and an address with credentials in it
still refused), sandstormJiraGrant.test.cjs (the server, the fetcher,
the marker only for Jira, the popup and the docs).

Thanks to xet7 !
2026-10-15 00:33:30 +03:00
Lauri Ojansivu de95789e45 Live tests: no log line carries anything from a login (CodeQL #586).
GitHub CodeQL js/clear-text-logging: releases/live-tests/basecamp-import.mjs
logged the Basecamp account id read from WEKAN_LIVE_BASECAMP_ACCOUNT. The
live tests run with the maintainer's own logins and keep a log under
.tools/log/, so nothing from a login or its configuration belongs in it.
The same shape was in two more tests and is gone there too: Planka logged
its configured address, and the GitLab write-back test its GitLab address
and scratch project path, also in the message that says it is skipped.
Each now logs what it does in fixed words.

These are human-run scripts, not part of the WeKan server, so there is no
attempt for Admin Panel -> Problems to record.

tests/liveTestsNoSecretLogs.test.cjs follows every value read from the
environment, a login or the credentials into the names it lands in, and
fails on any log line of any live test that interpolates one; a negative
case pins the reported shape.

Thanks to GitHub CodeQL and xet7 !
2026-10-15 00:25:21 +03:00
Lauri Ojansivu aad7e1414a TODO Later: no speaker review, no formats without documentation.
Maintainer decision 2026-10-15:

- Translating from dictionaries and each language's own wording is
  enough; speakers review it later if they have the time. The category
  listing terminology for a speaker to check is removed.
- Formats with no published documentation are not taken on: Restyaboard,
  YouTrack, Airtable, Leantime, Things 3, and Taskcafe (an archived
  project whose GraphQL API has no documentation beyond its source).
  Attachment contents from Jira, Deck, OpenProject, Asana, Zenkit and
  GitHub/GitLab stay: each documents its attachment API.

Thanks to xet7 !
2026-10-15 00:15:56 +03:00
Lauri Ojansivu 0d68c3536b Close #4790: User Filter is covered by what WeKan has.
#4790 was a collection of ideas ("I don't know what use cases it would
cover"), and a split into six new feature requests was prepared on
2026-09-29. Checked against the code, they describe what WeKan already
does under other names, so nothing needs filing:

- labels on organizations, teams, people and boards: organizations and
  teams are the groups of people; boards are grouped into All Boards
  workspaces and have their colours;
- a label from an LDAP group: LDAP groups sync as organizations and teams
  (#4737), with LDAP_SYNC_ADMIN_GROUPS and LDAP_SYNC_GROUP_ROLES;
- adding or keeping users out of boards by label: a board is shared with
  an organization or a team, and a private board not shared with one
  keeps it out;
- a board role for an LDAP group: the group syncs as a team, and the team
  is shared to the board with its role;
- finer roles: custom board roles (#3022, closed);
- users by e-mail domain: done (orgAutoAddByDomain.js, #5850, #4178);
- rules by label, meteor-roles and multitenancy were thoughts in the
  comments, not requests.

The prepared split (docs/Features/User-Filter-4790-Split.md) is removed,
TODO Later no longer waits on it, and docs/Design/TeamsOrganizations.md
says how its rows are covered - also that custom roles exist now.

Thanks to xet7 !

Fixes #4790.
2026-10-15 00:07:39 +03:00
Lauri Ojansivu 99d4ee4d91 Merge remote-tracking branch 'origin/main' 2026-10-14 23:56:55 +03:00
Lauri Ojansivu 707021fee2 TODO Later: only what is not built yet; close the issues that are.
What is built leaves the backlog, also where it is not yet tested against a
live service:

- #5758 Windows SSO: built in a594a58117 (v12.46), for a Windows host in a
  domain; the handshake was not run here.
- #2698 GitLab integration: GitLab sync and import (2026-09-30), and the
  write-back of edits, comments, closing and reopening in d6c1d6fce1
  (v12.46), with a live test in build.sh / build.bat -> Tests -> Live
  tests; GitLab login to WeKan through OAuth2 is documented, and card
  numbers with board keys give the #number references.
- #337 accessibility: move buttons, focus trap, landmarks, labels and
  every icon-only control named and guarded; a screen-reader pass is
  testing, not a missing part.
- #2208 Sandstorm: everything that can work from a grain is built (email
  sending, outgoing webhooks, list sync, the REST API through Sandstorm);
  Jira sync from a grain cannot, and stays recorded as such.

Also out: the continuous backup's live-account tests, the SMTP, browser
and identity-provider test matrix, the UCS / Nextcloud ExApp variable
check, the History switch (decided on 2026-10-08), Zenkit's unverified
export, the translation batch history, and the importers built on
2026-10-14 (Planka, Microsoft To Do, KanbanFlow, Basecamp). Left: Jira
closed sprints, the #4790 split to file, translations and their
terminology review, the attachment-board report, the formats not read
yet, and the Sandstorm limits.

changelogFormat's count of categories that link issues follows the
smaller backlog; it still fails on an issue named without its link.

Thanks to nikalasmd, ndarilek and xet7 !

Fixes #5758,
Fixes #2698,
Fixes #337,
Fixes #2208.
2026-10-14 23:56:19 +03:00
github-actions[bot] 6466686289 Bump versions for v12.46 2026-10-14 20:50:19 +00:00
Lauri Ojansivu 8263ca936d Prepare v12.46 release 2026-10-14 23:49:01 +03:00
Lauri Ojansivu 857b6db4f4 CHANGELOG: the guard fixes after the importers and the feed.
Thanks to xet7 !
2026-10-14 23:23:14 +03:00
Lauri Ojansivu a6a1d3c1a6 Fix three guards the last commits tripped.
- The activity feed's new stylesheet was not imported, so it would not
  have been in the client bundle (clientBundleImports); client/features/
  main.js imports it beside the feed's template.
- The Basecamp and Microsoft To Do importers stripped HTML tags in one
  pass, which a crafted body survives ("<<b>b>" leaves "<b>"); each
  removal now repeats until nothing changes (tagStrippingFixedPoint).
- renderedTitles escaped only the first "." of a field name; it escapes
  every special character now (noIncompleteRegExpEscaping).

Thanks to xet7 !
2026-10-14 23:23:07 +03:00
Lauri Ojansivu d27dbe651f CHANGELOG: API importers, Chat channels, the History fork and the views that showed raw text.
The Upcoming section's In short covers them; TODO Later drops the third
round of decisions, all of which are built now.

Thanks to xet7 !
2026-10-14 23:09:31 +03:00
Lauri Ojansivu 2ee3e83793 Chat view: channels are the cards with a conversation, on every board, with unread counts.
The Chat view's tree listed the open board's swimlanes, lists and cards,
and every other board as a link that left for that board. Now:

- The tree is workspaces > boards > swimlanes > lists > cards, holding
  the cards that have a conversation on every board the member sees
  (server/chatView.js chatViewChannels). Every board without one is under
  Other boards; its first message moves it up to its place.
- Each level has a caret before its icon - after it, mirrored, in a
  right-to-left language - and folds open and shut with a slide (none
  for a member who asked for reduced motion). Clicking a board folds it;
  it no longer leaves the Chat view. The board the member is on, its
  workspaces, swimlanes and lists start open; what they open or close is
  kept in their browser.
- The board the member is on shows all its cards, from the board's own
  data, so a conversation can start on any card and a dragged card moves
  in the tree at once; its button shows only the cards with messages.
  Another board opened from Other boards shows all its cards
  (chatViewBoardChannels) for the same reason.
- Every level shows the messages this member has not read: others'
  messages after they last opened the card, all of them on a card never
  opened; opening the card - or a message arriving while it is open -
  reads it.
- Every name, and the chat's header, renders as Markdown (inlineViewer).
- Only what the member may read: boards they see in All Boards, only
  their own cards on an assigned-only board, no conversation where their
  role reads no comments.

Tests: tests/chatView.test.cjs (the tree, Other boards, a first message
moving a board up, unread sums, the full board, carets, folding rows,
no navigation), server/lib/tests/chatViewChannels.tests.js (unread
counts, never my own, the role and assigned-only limits, a board I do
not see refused), email-chat-views.e2e.js (folding without leaving the
view, the move up, unread badges, Markdown names). New strings
translated in all locales.

Thanks to xet7 !
2026-10-14 23:08:22 +03:00
Lauri Ojansivu 2cc6b93ed9 Version Control, the activity feed and the email list render titles as Markdown.
Titles, names and labels are Markdown in WeKan - links, emphasis, emoji -
but Board View / Version Control showed "Azure [issues](https://...)"
and the like as text, the all-boards activity feed did the same with the
cards and boards it names, and the email view's sidebar showed its lists
and labels raw ("Demo + Wishes 🌈").

inlineViewer (client/components/main/editor.jade) is the viewer itself -
Markdown, emoji, mentions, and the Admin Panel's security settings for
links and code - laid out inline at the size of the line it is in. These
views use it for every issue, milestone, label, release, wiki page and
board title, the activity sentence and its card and board, and the email
folders and row labels. An attachment's file name in its download link
stays a file name.

A rendered title can hold a link, and a link inside a link is not one:
the titles and labels that open or filter are spans with role=link now,
which Enter and Space open too, and a click on a link inside a title
opens that link instead.

tests/renderedTitles.test.cjs fails on any title-like value interpolated
as text in these views and pins the role=link spans.

Thanks to xet7 !
2026-10-14 23:07:30 +03:00
Lauri Ojansivu f12b718d81 Activity feed: every sentence names what it is about.
The all-boards activity feed showed "checked __checklistItem__ of
checklist __checklist__", "from list __oldList__ at swimlane
__oldSwimlane__", "added attachment __attachment__", an empty "at
swimlane", and raw names such as "removedChecklistItem": it filled only
the card, list and board of the notification sentence.

- server/activityFeed.js reads the ids the sentence needs and looks the
  names up a batch at a time: swimlanes, old list and swimlane, checklist
  and item, attachment, label, member and assignee, comment, old board.
  Never a field's value (an admin-only custom field stays out), a
  comment's text only for a member who may read comments, and another
  board only when the member sees it too.
- A rename or a restore names its new title itself, and a swimlane's
  activity keeps the swimlane's id in listId; activityTitleParams fills
  those for the feed and the notifications alike.
- A placeholder nothing could fill - a deleted checklist item - is left
  out instead of shown as __name__.
- Ten kinds of activity had no sentence at all, in the feed or in
  notifications: removedChecklistItem, moveCardBoard, deleteBoard, the
  board, list and swimlane renames and restores, joinAssignee and
  unjoinAssignee. They have one now, translated in all locales; the
  restores, moves and checklist ones join their notification groups.

tests/activityFeed.test.cjs fills every name, refuses comments and
hidden boards, and fails on any stored activity type without a
sentence; activity-feed-sentences.e2e.js checks the feed shows the names
and no __placeholder__.

Thanks to xet7 !
2026-10-14 22:57:31 +03:00
Lauri Ojansivu 46e30f01e4 Import: Planka, Microsoft To Do, KanbanFlow, Basecamp, Google Tasks and Google Keep from their APIs.
Decided on 2026-10-14. On the import page each of the six asks for what
its service needs - a self-hosted Planka its address, username and
password; the others an access token (Basecamp also its account id) -
lists what that account can import, and fetches the chosen board, list
or project. The credentials go with each call and are used for that
call's requests only; they are never stored or logged, and the page
clears secret fields once a fetch has used them. The fetched JSON fills
the import text box and the import goes on as for a pasted export: the
service's parser (models/lib/<key>ApiFormat.js) into the generalized
importer, with the members step and the loss report. The same JSON can
be pasted, or posted to POST /api/boards/import/<key>.

- server/apiImport.js: apiImportList and apiImportFetch, signed in only,
  off with the Admin Panel's import switch, not in Sandstorm, rate
  limited; credentials checked against the service's own fields
  (models/lib/apiImportServices.js).
- server/lib/apiImportRequest.js: every request through the SSRF guard -
  pinned address, no redirects, bounded in time and size - so a Planka
  address cannot point the server at its own network; an administrator
  can allow such a host with API_IMPORT_ALLOWED_PRIVATE_HOSTS. An error
  names only the origin and the status.
- models/lib/apiImport/<key>.js: each service's client, plain Node with
  the request injected, paging bounded and the token only in a header.
- Each has a docs page under docs/Features/ImportExport/, listed in the
  coverage index and External-Tools.md.

Tests: tests/<key>ApiImport.test.cjs (client with a fake request, every
parsed field, wrong shapes), tests/apiImport.test.cjs (credentials, the
gate, the wiring, the shapes refused), api-import.e2e.js (the form for
every service, fetched JSON imported through the page, refusals incl. a
private address). Live tests, read only, run from build.sh / build.bat ->
Tests -> Live tests with your own accounts (releases/live-tests/README.md);
they were not run here. New strings translated in all locales.

Also: the release risk baseline allows the services' API addresses, and
the guards that count import creators, list the import sources and pin
the platform fetch() calls know the new ones.

Thanks to xet7 !
2026-10-14 22:57:15 +03:00
Lauri Ojansivu 5fee137f01 Tests: three guards follow the History fork and GitLab write-back fixes.
- syncHistoryAdmission pinned the line that walked successors to the
  chain's tip inside appendSyncHistoryRow; 66dd9b0938 moved that walk to
  historyChainTip and made the append take turns, so it pins those.
- The write-back's injectable GitLab read was a parameter named fetch,
  which syncBleed's "no new platform fetch()" guard rightly could not
  tell from the real one; it is readIssues now.
- syncClosedByWeKan, the card field the write-back added, is in the card
  e-mail field inventory as internal provenance, excluded like the other
  sync fields.

Thanks to xet7 !
2026-10-14 22:56:34 +03:00
Lauri Ojansivu 66dd9b0938 History: two changes at once no longer fork a board's History chain.
Admin Panel -> Problems showed HistoryIntegrity, critical, "history-fork",
89,062 times. Each History row names the row before it, and the legacy
append (every board not migrated to the coordinated chain) read "the
newest row" and inserted after it as two separate steps: two changes
saved at the same moment on one board - two members, or one action whose
hooks record twice at once - both named the same row. Nothing was
tampered with, but the audit called it tampering, and undo, redo and
restore were refused for both rows. The writer gate around the legacy
append counts its writers for a migration; it does not make them take
turns.

- models/lib/historyLegacyAppend.js: a board's appends take turns in
  this server, and each links after the chain's tip - the newest row,
  then its successors, as Sync History already did - so a createdAt tie
  does not fork either. The ordinary recorder, the Scrum restore writer
  and Sync's legacy append all use it. Several servers writing one board
  at the same moment can still race; the coordinated chain is the answer
  to that.
- The hourly audit reported every broken row again every hour, which is
  where 89,062 came from; each finding is now reported once
  (historyIntegrityReported). It reads the History board by board instead
  of all at once, and a fork of two intact rows saved within a minute of
  each other is reported as what it is - integrity.historyConcurrentFork,
  medium, "a History writer race, not tampering" - while anything else
  stays critical.

Existing forks stay as they are (a row's hash cannot be changed), and
undo of their two rows is still refused.

tests/historyLegacyAppend.test.cjs: the old append forks under
concurrency (negative control), twenty appends at once on two boards
make two straight chains, a createdAt tie follows the chain, a failed
append does not stop the next, the audit tells a race from a changed or
late row, and no append in models/ or server/ links by "newest" without
taking turns.

Thanks to xet7 !
2026-10-14 22:39:36 +03:00
Lauri Ojansivu 5481268558 Email view: subjects and previews render Markdown.
Board View / Email showed a card's title and description as text: "# Demo
card 👍", "**Markdown**", raw <font> tags and links, where every
other view renders them. The row's subject and preview now go through
the viewer like every card title and description in WeKan, so the Admin
Panel's settings for links and code apply to them as everywhere.

The preview was the description squeezed to one line and cut at 120
characters, which cut inside a link or a tag; it is now the start of the
message, the From line left out, cut at a line end (previewMarkdown),
and the row shows two lines of it. Headings and paragraphs in a row read
at the row's size.

tests/emailView.test.cjs pins previewMarkdown and that neither cell is
interpolated raw; email-chat-views.e2e.js checks the bold, the link and
that no ** or ]( is left in the row.

Thanks to xet7 !
2026-10-14 22:36:21 +03:00
Lauri Ojansivu fc24caadcc CHANGELOG: GitLab write-back, and the oauthProviders guard.
TODO Later's third-round block keeps only the API importers.

Thanks to xet7 !
2026-10-14 22:08:54 +03:00
Lauri Ojansivu d6c1d6fce1 List Sync: write WeKan's changes back to GitLab.
#2698. A list synced from GitLab gets a Sync option of its own, off by
default: Write changes back to GitLab. With it, every Sync run first
sends what members did on the list's cards, then reads GitLab, so the
read finds both sides agreeing:

- a title or description edited in WeKan, when the issue still has the
  text both sides last agreed on - the local-only edit the reconcile
  already keeps. The Source footer Sync adds is not sent; when both sides
  changed, nothing is written and it stays a conflict to resolve;
- a card archived in WeKan closes its open issue, and restoring a card
  WeKan closed reopens it (syncClosedByWeKan); an issue GitLab closed is
  never reopened by WeKan;
- a comment written in WeKan becomes a note, and keeps the note's id
  (syncCommentId) so the comments sync does not bring it back twice.

Only what happens after it was switched on is sent (writeBackSince), at
most 100 writes a run. Every write is fenced by the list's Sync
reservation, goes through the same address checks as the reads (no
redirects, PUT or POST to an issue or its notes only), and records on
the WeKan side only while the card or comment is still as planned. A
refused write is shown in the Sync popup (syncWriteBackError, beside
syncSource because the run's list writes match syncSource whole) and the
read sync still runs. Not in Sandstorm, whose bridge only reads.

Tests: tests/listSyncWriteBack.test.cjs (the plan with GitLab's shapes,
the parser's footer, every negative - conflict, legacy card, old
archive or comment, foreign card, issue GitLab closed - and the wiring),
server/lib/tests/listSyncWriteBack.tests.js (end to end through
syncOneList against a fake GitLab). The live test,
releases/live-tests/gitlab-write-back.mjs, runs from build.sh / build.bat
-> Tests -> Live tests with the glab login releases/mirror.sh uses, on one
scratch issue in a private project of that account; it is human-run and
was not run here. releases/live-tests/run.mjs runs every live test and
skips a service with no login.

Also: the English variants get the SQRL and Windows sign-in strings
(their key order follows en.i18n.json); buildScriptParity accounts for
the scripts CI runs inside their own environments; new strings
translated in all locales.

Thanks to xet7 !
2026-10-14 22:08:34 +03:00
Lauri Ojansivu b38532ed72 Tests: oauthProviders follows the translated login-error list.
a539429fce made the sign-in page translate SQRL's sqrl-only and
sqrl-hardlock refusals the way it already translated
oauth-account-conflict, by one list of codes, so the guard's pin on the
old single comparison failed. It now pins the list - the conflict among
the codes, matched on err.error or the reason, shown as text - so the
behaviour it protects is the same.

Thanks to xet7 !
2026-10-14 22:08:19 +03:00
Lauri Ojansivu e18f6db741 CHANGELOG: SQRL locks, Windows SSO, faster Scrum batch undo, #6549 docs.
The Upcoming section opens with them and the Bosnian and Tsonga
terminology; TODO Later keeps only the two decided items still to build
(GitLab write-back, the API importers) and drops the finished batch
undo/redo entry.

Thanks to xet7 !
2026-10-14 21:47:06 +03:00
Lauri Ojansivu a594a58117 Login: Windows single sign-on with Kerberos or NTLM.
#5758. On a WeKan running on Windows in a domain, members already signed
in to Windows can sign in with "Sign in with Windows", without a
password: /sso/windows runs the browser's Negotiate handshake with
node-expose-sspi, WeKan finds or creates the member, and the browser
comes back to /sign-in with a one-time token (random, kept as its
SHA-256, one minute, used once) that the page signs in with through a
login handler - so every login check applies, deactivated accounts and
SQRL's sqrlonly included.

- WINDOWS_SSO_ENABLED=true turns it on, on Windows only.
- WINDOWS_SSO_DOMAINS lists the domains allowed; empty means nobody, and
  machine accounts never.
- WINDOWS_SSO_ACTIVE_DIRECTORY=true takes name and e-mail from AD.
- WINDOWS_SSO_MERGE_EXISTING_USERS=true signs in to the account with the
  same username or e-mail; otherwise a new account is made beside a
  local one of the same name, since a local alice is not necessarily the
  domain's alice. A Windows account signs in to the account it linked to
  before.

node-expose-sspi is Windows-only and not a dependency every platform
downloads: the admin installs it into the bundle, and WeKan looks for it
there (docs/Features/Login/WindowsSSO.md; start-wekan.bat has the
variables).

Built without a Windows domain to test against, as decided: the
decisions - names, domains, profile, which member - are tested in plain
Node in tests/windowsSso.test.cjs, with negatives for every malformed or
foreign name, an empty allowlist and a same-named local account; the
handshake and the server wiring are pinned from source. New strings
translated in all locales.

Thanks to nikalasmd and xet7 !
2026-10-14 21:46:17 +03:00
Lauri Ojansivu 5e671c7034 Scrum: batch undo and redo of a large sprint is 2.6 times faster.
Undoing a 10,500-card sprint close took 282 s and redoing it 578 s, about
27 ms a card: each record of a History row cost some thirty database
round trips - a read per record in the preflight, two ownership checks
(two reads each) around every read, the same again around a readback
after every write, and the whole row read back record by record twice
more at the end.

Now a row's records are read in one query per type (readTargets), and
inspectScrumHistoryWrites takes that as readMany, bracketed by one
ownership check before and one after: the worker's claim and the source
row only ever go from current to displaced, so current before and after
the read means current throughout it. Every write keeps its ownership
check right before it, after the read it is computed from; the readback
after a write confirms the value landed and leaves ownership to the next
write's fence and the batch's final readback. A sprint or release that
thousands of cards name is looked up once, not once a card. The
permission check stays per card.

The 10,500-card start / close / undo / redo test went from 14 min 27 s
to 6 min 7 s on the same machine; all 18 Scrum tests pass.
tests/scrumHistoryWriteState.test.cjs pins one read and three checks
for a bracketed read, ownership lost during it, a short answer, a missing
record and identities checked before any read (negative), and that every
write in the path is still fenced after its read.

Thanks to xet7 !
2026-10-14 21:45:54 +03:00
Lauri Ojansivu 03b83c14c1 Docs: how WeKan, Rocket.Chat and a G Suite SAML login chain together.
WeKan -> Rocket.Chat (OAuth2) -> Google (SAML) is three sign-ins in a row,
and with the popup login style the chain broke whenever Rocket.Chat was
not signed in yet: its SAML login left the authorize page the popup was
waiting on, so WeKan never got its code, although Google and Rocket.Chat
both logged a successful login. Logging in to Rocket.Chat first only
avoided the detour.

docs/Features/Login/OAuth2.md now says how to configure the chain: the
redirect login style (the default now; oauth2-login-style /
OAUTH2_LOGIN_STYLE / Admin Panel), Rocket.Chat's SAML entry point and
issuer, Google's NameID as the e-mail address, the exact
/_oauth/oidc redirect URI, https everywhere, how to read the logs, and the
direct alternatives (Google OIDC, WeKan's own SAML).

tests/oauth2RocketChatSamlChainDocs.test.cjs pins that redirect stays the
default in the server and the snap, and that the documented section names
the settings - and never recommends popup.

Thanks to timmy-mac and xet7 !

Fixes #6549.
2026-10-14 21:45:44 +03:00
Lauri Ojansivu a539429fce SQRL: sqrlonly and hardlock lock an account to SQRL.
The member's SQRL client sends both options with every sign-in, and WeKan
now follows them (it used to say they were not supported):

- sqrlonly: every other way of signing in is refused - password, OIDC,
  LDAP, SAML, CAS, passkey, Nextcloud (validateLoginAttempt), the REST
  /users/login and header login, which mint their tokens without it. A
  resume token stays valid; a session already open stays open.
- hardlock: no password-reset e-mail is sent (the answer is the same as
  for a sent one), a reset link sent before does not sign in, and the
  member cannot change their e-mail address.

An ident records the options only once it succeeds; a flag the client
leaves out is switched off. Neither applies while the identity is disabled
from the SQRL client (the emergency stop), nor while the admin has SQRL
off, and an admin can still change the e-mail, set a password and create
a token: those are the ways back. Member Settings / SQRL shows which locks
are on; the sign-in page and the e-mail change say why they refused.

tests/sqrl.test.cjs drives both options through the protocol with a real
Ed25519 client (set, cleared, never by a query, a refused or a forged
ident), the refusal decision for every login type, and pins every path
that signs a member in - and lists every file that mints a login token,
so a new one cannot skip the check. New strings translated in all
locales.

Thanks to xet7 !
2026-10-14 21:45:34 +03:00
Lauri Ojansivu 0502261090 Translations: Bosnian says Tabla for board, Tsonga keeps archive apart from storage.
Bosnian used both ploča and tabla for a board. Every board string now uses
tabla and its cases (genitive plural tabli); ploča stays only where it
means the dashboard (kontrolna ploča) or the Admin Panel. The board
search operator abbreviation is tb, because t is already traka
(swimlane).

Tsonga used vuhlayiselo for both the Archive and storage. The Archive is
now "vuhlayiselo bya swa khale", while storage stays "vuhlayiselo" and the
database "vuhlayiselo bya data". The archive popups ask their question.

Bosnian and Croatian s3-port translated (S3 port).

The audited-corrections rows, the Bosnian audit test and the archive
filter guard follow the decision. wrongLanguageCorrections20261014 pins
Tabla, the tb abbreviation, the dashboard and Admin Panel exceptions, and
the Tsonga archive word wherever English says archive.

Thanks to xet7 !
2026-10-14 21:26:28 +03:00
Lauri Ojansivu 84c6d2db7f Merge remote-tracking branch 'origin/main' 2026-10-14 21:13:44 +03:00
Lauri Ojansivu ddcdd295bb TODO Later: the third round of design decisions.
GitLab write-back with live tests in the build menus (#2698), Windows SSO
(#5758), documenting the Rocket.Chat SAML chain (#6549), SQRL's sqrlonly
and hardlock (#2460), faster batch undo/redo, API importers for Planka,
Microsoft To Do, KanbanFlow, Basecamp, Google Tasks and Google Keep, and
the Bosnian and Tsonga terms.

Thanks to xet7 !
2026-10-14 21:11:33 +03:00
github-actions[bot] c4c8484738 Bump versions for v12.45 2026-10-14 18:00:50 +00:00
Lauri Ojansivu 7ee94d47d8 Prepare v12.45 release 2026-10-14 20:59:39 +03:00
Lauri Ojansivu ea9007d8a8 Scrum: the 10,500-card test gets the time it takes, measured.
It timed out in every full Meteor run. Profiled at full size it passes, in
15 min 25 s: start and close 2 s, the rollover 36 s, the undo batch 282 s
and the redo 578 s - past the 15 minutes it had. The next test, which
interrupts a rollover, then met this one's unfinished writes and failed
too. The test has 30 minutes now, with the measurements beside the
limit, and the Scrum snapshot rows group passes (3 passing, 14 min).

TODO Later now records the real follow-up: batch undo/redo writes about
27 ms a card.

Thanks to xet7 !
2026-10-14 20:10:07 +03:00
Lauri Ojansivu 686995c204 CHANGELOG: the release build fixes in Upcoming.
musl, Android, FreeBSD, the Windows single files, Bun and the Quay copy,
grouped as Release builds under the bug fixes, with In short updated.

Thanks to xet7 !
2026-10-14 19:36:35 +03:00
Lauri Ojansivu ce3d4310d5 Docker: the Quay copy retries a dropped connection.
v12.44's copy to Quay failed on "stream error: ... PROTOCOL_ERROR;
received from peer" - an HTTP/2 reset on the way, not a refusal. A
transport error is retried up to four times; a refusal still fails at
once. tests/releaseDockerRegistryOutage.test.cjs runs the step with a
docker that resets twice, one that refuses, and one that never recovers.

Thanks to xet7 !
2026-10-14 19:28:27 +03:00
Lauri Ojansivu 41399b7518 Bun bundles: FreeBSD's libvips, a retried download, and LF checksum files.
- FreeBSD: sharp 0.35.5's source refuses libvips older than 8.18.7, and
  FreeBSD packages 8.18.6 (latest and quarterly alike), so both FreeBSD
  Bun builds stopped at that #error. A user's FreeBSD runs the packaged
  libvips through the same soname whatever was compiled against, so a
  patch-release gap is accepted with a notice; a larger one still stops.
- windows-x64: one HTTP 500 from the CDN failed the asset download; a
  server error is now retried, a 4xx is not.
- The .sha256sum files were written in Windows text mode, CRLF, and the
  windows-arm64 Bun single file then looked for "<zip>\r". They are LF on
  every platform now, and the reader strips a CR from the ones already
  published.

Thanks to xet7 !
2026-10-14 19:28:27 +03:00
Lauri Ojansivu 0966a90d39 Windows single files start: Meteor's paths, and 32-bit targets built by a 32-bit Node.js.
The win64, win-arm64 and win32 single files stopped at once since v12.41,
for two reasons:

- win64 and win-arm64: Node.js mounts the archive at \\.\NUL\vfs\<n>, and
  Meteor's mini-files.js turns a path beginning with a backslash into
  C:\... - so Npm.require looked in the wrong place and the server
  stopped with "Cannot find module 'denque'". The single file now carries
  mini-files.js patched to keep a UNC or device path (two leading
  separators) as it is; drive paths are unchanged, and the build stops if
  Meteor's code is not the code the patch was written for. It also makes
  a bundle runnable from a \\server\share.
- win32: the SEA blob stores its lengths as size_t, so a blob written by
  the x64 Node.js on the build host is unreadable to a 32-bit node.exe
  (createZipProvider got no archive). build.mjs now refuses a target whose
  pointer width differs from its builder's, and single-file.yml builds
  win32, i386 and 32-bit ARM with the release's i386 Node.js, checksum
  verified - the i386 and ARM ones had the same fault but no runner starts
  them.

tests/singleFile.test.cjs runs Meteor's path functions on Windows path
semantics before and after the patch, refuses win32 from x64, and builds
and starts a real single executable with the patched file.

Thanks to xet7 !
2026-10-14 19:28:27 +03:00
Lauri Ojansivu 0d23746441 FreeBSD bundles start: cpu-exec is used only where it can run.
start-wekan.sh ran every binary through cpu-exec, a #!/bin/bash script for
Linux's /proc/cpuinfo and qemu-user. FreeBSD has no /bin/bash, so it
failed "cpu-exec: not found" and WeKan never answered (the FreeBSD
bundles since v12.41). It is now used only on Linux with a /bin/bash -
the same holds for Alpine without bash and for Termux - and the BSD
bundles drop it, as the Windows and macOS ones already do.

Thanks to xet7 !
2026-10-14 19:28:14 +03:00
Lauri Ojansivu 5331e06199 musl and Android bundles: sharp gets its platform package where it is.
Both builds failed every release since v12.41. sharp is an application
package (programs/server/npm/node_modules), but the scripts removed its
glibc platform packages from programs/server/node_modules, where they are
not, and the npm install there never touches the application's packages.

- musl: releases/platform-native-packages.mjs and check-addon-arch.mjs
  take --libc musl: the glibc packages go and sharp's
  @img/sharp-linuxmusl-<arch> and canvas's -musl package arrive,
  sha512-checked. "Cannot find module 'sharp'" ended the job, from a
  require() run in programs/server; it runs in programs/server/npm now.
- Android: the glibc packages go from the right directory, and sharp is
  built from source against Termux's libvips in its own directory; should
  that fail it runs as WebAssembly, with a warning. Termux's user does not
  own the bind mount, and "chmod: ... Operation not permitted" stopped
  v12.43 and v12.44 - only what is not already executable is chmodded.

tests/platformNativePackages.test.cjs installs the musl packages through
the real script; muslBundles and androidBundles pin the paths, the
--libc musl call and that nothing chmods unconditionally.

Thanks to xet7 !
2026-10-14 19:28:14 +03:00
Lauri Ojansivu 52b4c87127 TODO Later: the 10,500-card Scrum test, with what was measured.
It times out in every full Meteor run, before this round as well; the
profile at 3,000 cards and why it cannot simply run smaller are recorded
so the next step - profiling the board data read and the rollover at full
size - starts from there.

Thanks to xet7 !
2026-10-14 18:58:39 +03:00
Lauri Ojansivu 25d906d93c attachmentApi test: the upload-limit refusal no longer times out.
It failed in every full Meteor run, before this round too, at Mocha's two
seconds. Timing the handler showed it refuses in 2 ms; the time was its
console.error of the refusal - the first error stack of the run, whose
printing loads the server bundle's source map. The test now captures that
log (and checks it was written) instead of printing it, and the group runs
in under half a second.

Thanks to xet7 !
2026-10-14 18:53:37 +03:00
Lauri Ojansivu 60bd70a636 Merge remote-tracking branch 'origin/main' 2026-10-14 18:37:50 +03:00
Lauri Ojansivu 0440454675 CHANGELOG: Tsonga, Bosnian and Aromanian in Upcoming.
Thanks to xet7 !
2026-10-14 18:34:21 +03:00
Lauri Ojansivu ac2410fead Tsonga, Bosnian and Aromanian: the values left for a speaker, translated.
- Tsonga: 618 values were the filler "Hi Xitsonga: mhaka", and 831 more
  were Xitsonga words padded with a repeated "mhaka" wherever the
  generator had no word ("engetela bodo swirho mhaka ku suka mhaka mhaka
  nhlangano"). All are translated from the English, in the file's own
  words (bodo, khadi, nxaxamelo, ndlela, xirho, nhlangano, ntlawa); the
  planning-poker numbers and search symbols are literal again.
- Bosnian: the 748 values that were Serbian, transliterated in v12.44, are
  ijekavian Bosnian now (premjestio, Neraspoređene, Historija, sedmica),
  with the board, card and list words of the rest of the file instead of
  the court-file metaphor they came with (spisi, predmet, postupak).
  Several mistranslations went with it (Export card said convert, Edit WIP
  limit said remove it).
- Aromanian "calendar" replaces the Italian "Calendario"; it is spelled as
  in English, so it joins Aromanian's list of such words.

Low confidence, for a speaker to correct: Tsonga throughout, especially
colour names, migration (ku rhurhisa), backup, lock/unlock, "Xiluva" for
the letter greeting Dear, and Vuhlayiselo used for both storage and
archive; Bosnian ploča where the board title itself says Tabla.

tests/wrongLanguageCorrections20261014.test.cjs fails on any Tsonga filler
or padding, any ekavian form in the Bosnian file, and a lost placeholder
in either.

Thanks to xet7 !
2026-10-14 18:34:09 +03:00
Lauri Ojansivu dc18a2eb00 CHANGELOG: the Sync notification plan fix in Upcoming.
Thanks to xet7 !
2026-10-14 17:49:00 +03:00
Lauri Ojansivu e85eaa3ad3 Sync notifications are planned again with the clear e-mail layout.
v12.44 made the clear HTML layout the default for notification e-mail
(#4434), and with it every e-mail job carries `text`, its plain-text part.
The Sync notification plan validator accepted exactly seven job fields, so
it refused every plan: a rule or Sync run's notifications failed with
sync-notification-plan-invalid. groupKey and deliverAt, from non-default
delivery settings, were refused the same way before that.

The validator now takes what emailOutbox.enqueue takes - the seven fields
and the three optional ones, checked as enqueue checks them - so a plan
never holds a job its replay would refuse. tests/syncNotificationPlan
.test.cjs plans a job with all three and refuses bad ones, and pins that
the optional list is exactly enqueue's parameters; the full Meteor run
found it (Shared notification preparation, Stored Sync notification
delivery).

Thanks to xet7 !
2026-10-14 17:48:45 +03:00
Lauri Ojansivu 5f78baa324 CHANGELOG: the attachment viewer's alt text in Upcoming, bug fixes before translations.
Thanks to xet7 !
2026-10-14 17:34:07 +03:00
Lauri Ojansivu a530a3c673 Accessibility: the attachment viewer's picture has alt text.
The one image in the templates without alt was the attachment viewer's:
it is "Preview" until a picture opens, and then the file's name.
tests/iconOnlyControlLabels.test.cjs now also fails on any img without
alt. Part of #337.

Thanks to ndarilek and xet7 !
2026-10-14 17:33:48 +03:00
Lauri Ojansivu 439add85f1 CHANGELOG: a new Upcoming for the Latvian and other language fixes.
v12.44 shipped while they were in progress; the rest of the wrong-language
replacements and the two guard updates after it go in a new Upcoming
section, as the release rules say.

Thanks to xet7 !
2026-10-14 17:33:07 +03:00
Lauri Ojansivu c11fd215c4 problemsMenuOrder and sandstormWebhookEgress follow two v12.43 changes.
Both failed before this round, on changes that were deliberate:

- The activity notification recovery list (#6768) has Retry all and Cancel
  older as toolbar buttons, written `actions: [{ id, labelKey }]` like the
  menu's panes; the guard now leaves toolbar actions out and still
  requires every menu pane to render.
- Webhooks may reach internal hosts a site admin lists (b4d2604e61):
  off Sandstorm, webhookFetch is fetchSafe except for those hosts, which
  internalWebhookHosts.test.cjs pins; the guard now matches that branch
  and still requires fetchSafe for everything else.

Thanks to xet7 !
2026-10-14 17:32:51 +03:00
Lauri Ojansivu e0649b0cfb Finish the wrong-language replacements: Latvian, and the same words elsewhere.
The first half shipped in v12.44 with "Updated translations"
(b7346d2429): Spanish Save is Guardar, the Bosnian file is in Latin
script, Romanian's Italian words are Romanian, and single values in a
dozen locales. This finishes it:

- Latvian: 225 values were Lithuanian; they are Latvian now, in the
  file's own words (dēlis, kartiņa, darbvieta, migrācija). Low confidence:
  fuksīns, the Monte Carlo note's statistics wording, Teksta sagatave.
- The same wrong words under other keys: Macedonian Настройки in the All
  Boards menu, the Persian settings typo there too, Cantonese 依戀 in the
  attachment search operator, and Occitan "mot de Santa-Clara" for
  password in six values (senhal).

tests/wrongLanguageCorrections20261014.test.cjs now also fails on any
Lithuanian left in the Latvian file and checks its placeholders. Left for
later, with a speaker: the Tsonga file's 596 "Hi Xitsonga: mhaka" filler
values, Aromanian "Calendario", and Bosnian ekavian forms.

Thanks to xet7 !
2026-10-14 17:32:45 +03:00
github-actions[bot] 3522f68226 Bump versions for v12.44 2026-10-14 14:30:51 +00:00
Lauri Ojansivu 832d83cf7a Prepare v12.44 release 2026-10-14 17:29:37 +03:00
Lauri Ojansivu b7346d2429 Updated translations 2026-10-14 17:29:25 +03:00
Lauri Ojansivu 030c8c524c Update five guards for this round's deliberate changes.
The full node suite run found them; each guard keeps its assertion and
says why it changed:

- boardPermission lists ActivityPub's public-only check (#4198): only a
  public board is a fediverse actor, never an instance board.
- cardSectionCollapse reads the End date item to the next template, and
  cardTitleEditClose lets the close anchor carry attributes: the #337
  titles moved the icon past a fixed window and a bare-anchor pattern.
- oidcAutoRedirectLoop's harness provides FlowRouter, which the sign-in
  page's onCreated now reads for SQRL's one-time sign-in (#2460).
- sharedDescription's own whole-tree check skips every _build* directory,
  rspack's bundled copy of every file.

Thanks to xet7 !
2026-10-14 17:22:39 +03:00
Lauri Ojansivu 1aa6de7356 Fix the seven guards this round's features broke.
Each was checked for which side was wrong:

- The LibreJS labels page sent Cache-Control: public; it is private now,
  as fileCacheHeaders requires of every response.
- Closing a card marks it seen (#2111): that code moved below the #6686
  popup handling, which labelsPopupStaleCardContext reads from the top of
  onDestroyed; unreadChanges now finds it anywhere in onDestroyed.
- The shared header helper (#2805) moved below the swimlane's currentUser
  helper, where largeBoardDataLoading pins it.
- The Story/Task kind, column set places and drawing strokes (#2012, #211,
  #1178) get their rule e-mail dispositions: none is e-mail content.
- Guards updated for deliberate changes, each saying why: the Rules group
  in Admin Panel Visibility (#2217), the rule under the team tabs in All
  Boards (#802, the two around Workspaces still pinned where they are),
  and a linked list's card clearing the title draft only after it was
  added (#2809, every clear checked against the add before it).

Thanks to xet7 !
2026-10-14 17:07:35 +03:00
Lauri Ojansivu 0bac415d87 Admin Panel: the SQRL and ActivityPub switches are in the Features group.
Each came with a group of its own, with an untranslated title and two more
rules in Admin Panel -> Settings -> Visibility. Both are optional features
this WeKan offers, which is what the Features group lists, so they are
there now, each still off by default and saved when switched.
tests/boardsVisibilitySettings.test.cjs counts the groups and their rules
again. The SQRL page and the Upcoming entries name the new place.

Thanks to xet7 !
2026-10-14 17:07:27 +03:00
Lauri Ojansivu 8e57bf6c22 CHANGELOG: the decided TODO Later items, SQRL, the CodeQL and #6770 fixes.
Upcoming gets the work since the last update: the CodeQL alerts first, as
security fixes; SQRL under Sign-in; browser push, the activity feed and the
webhook activity picker under Notifications; the linked list, live shared
descriptions, drawing, unread changes, column sets and the team tabs in
their groups; a new Nextcloud and the fediverse group for CalDAV, WebDAV
and ActivityPub; bug fixes for the subtask index, the cardKinds startup
schema and the icon-only controls; and the translations. In short is
rewritten to cover them.

TODO Later: the "decided, waiting to be implemented" category is gone -
everything in it is built - and #337 keeps a category of its own as
ongoing work. #2460 leaves "needs a maintainer decision". #4790 is linked
where it was named bare.

tests/changelogFormat.test.cjs asked at least half of the TODO Later
categories to link issues. That was a proxy, and it stopped holding once
the decided issues were built: the rest are mostly status records no
single issue tracks. It now checks the thing itself - the triage
categories exist, and no category names an issue without linking it.

Thanks to xet7 !
2026-10-14 17:04:26 +03:00
Lauri Ojansivu b0e47537e0 Translate the SQRL strings into every locale.
The 15 strings of SQRL sign-in - the Admin Panel switch, the sign-in
button and its QR code, and Member Settings -> SQRL - in all 234 locales,
from each locale's own words for sign in, password, add, remove and
Member Settings. "Rescue code" is SQRL's printed recovery code and
"identity" the member's SQRL key, not an ID card. ja-HI is written in
hiragana, as its tag says.

Low confidence: chr, tlh, wal, ay, ace, rn, sm, st, csb, kw, fur, uz-AR,
iu, nah, gn, zgh, wo, sn, tn, rup, se, cv, kl, to, bua, bi, nap, la, ak,
bm, ff, mg, ny, ve, xh, tig, tpi, sah, vo, gv, dz, ee, ig, ha, lg, nd, qu,
mi, wa, yi, ks, fj, haw, ve-PP, wa-RR, ss, ts, om, lld, scn, bo, yo and
ku; "identity", "client" and "rescue code" were coined where the language
has no settled word.

Thanks to xet7 !
2026-10-14 17:03:03 +03:00
Lauri Ojansivu c940b3e301 Sign in with SQRL, from a phone or a computer.
SQRL (https://www.grc.com/sqrl/sqrl.htm) signs in with a SQRL app or client
instead of a password. A site admin turns it on in Admin Panel -> Settings,
off by default; WeKan must be at an https:// ROOT_URL. A member adds their
identity once in Member Settings -> SQRL, by scanning the QR code or
opening their SQRL client; the sign-in page then offers Sign in with SQRL,
shows the sqrl:// link and its QR code, and signs in by itself once the
client has answered. With the client on the same computer (cps), the
client opens WeKan with a one-time token instead.

The protocol is GRC's SQRL Semantics: the client posts signed requests to
/sqrl, each verified with Ed25519 by Node.js's own crypto, no new
dependency for it. Every nut is random and used once, taken atomically;
each request must carry the server's previous reply unchanged; a request
from another address than the sign-in page's is refused unless the client
says noiptest; disable needs the identity's signature, and enable, remove
and rekeying need the rescue code's unlock key. An identity WeKan does not
know can neither make an account nor sign in. The page learns of success
by polling with a secret only it holds, and signing in uses that once.
The identity's public keys are a user field never published and not
writable from the client. Not offered on Sandstorm. sqrlonly and hardlock
are not supported. The QR code is qrcode-generator 1.4.4 (MIT, pinned).

models/lib/sqrl.js is the protocol with storage and signature check passed
in; tests/sqrl.test.cjs drives it with a software SQRL client using real
Ed25519 keys - adding an identity, the waiting page, cps, another network,
forged signatures, replayed nuts, altered replies, unknown identities,
another member's identity, disable/enable, rekey and remove - and pins the
server's single-use and visibility rules and the UI.
server/lib/tests/sqrl.tests.js runs in Meteor (2 passing): off by
default, then an identity added and signed in with once, through the real
/sqrl route and MongoDB. Documented in docs/Features/Login/SQRL.md. The
new strings are English placeholders until the translations that follow.

Thanks to xet7 !

Fixes #2460.
2026-10-14 17:02:20 +03:00
Lauri Ojansivu 8d71c6f925 Accessibility: every icon-only link and button has a name for screen readers.
21 controls showed only an icon and had no title, so a screen reader
announced them as an unnamed link: the swimlane and list drag handles, the
close buttons of the inline forms, the received, start, due and end date
buttons of a card, the add-attachment and edit-custom-field buttons, the
notification drawer's menu and close, the avatar's edit button, the list
open and back carets on a phone, the import's add-member and History's
previous and next. Each now has a translated title, which
client/lib/accessibility.js also makes its aria-label; the existing
translations are used, so no new strings.

tests/iconOnlyControlLabels.test.cjs scans every template for a link or
button whose only content is an icon and fails on one without a name.

This is part of #337, which stays open for the rest.

Thanks to ndarilek and xet7 !
2026-10-14 16:59:57 +03:00
Lauri Ojansivu 9138e85091 Edit a card description together, live, with Yjs.
Everybody who has a card's description open now edits one shared Yjs
document, so two people typing at once both keep what they typed; before,
the later Save replaced the earlier one. The editor stays the Markdown
textarea: each change in it becomes the smallest Y.Text edit, and the
others' changes are written into it with the caret kept where it was.
Save writes card.description exactly as before.

The server keeps the document in a server-only collection and publishes it
only to people who may edit the card. Updates are merged with a write that
is conditional on the state merged into, so two editors at once cannot lose
each other's typing; each update is limited to 256 KB and the document to
1 MB, and rate limited. A description changed some other way (the API, a
rule, an import) starts the document again from the new text, under a new
generation, so an editor never merges the old document into the new one.
A document nobody has touched for a day is started again from the saved
description the next time it is opened.

yjs 13.6.33 (MIT) is pinned. tests/sharedDescription.test.cjs runs the
real Yjs: concurrent edits merge, a repeated state is harmless, the caret
follows remote edits, an emoji is never split; it pins the server's checks
and that nothing else touches the document.

Thanks to dalareo and xet7 !

Fixes #140.
2026-10-14 16:49:21 +03:00
Lauri Ojansivu 5f2dd80f41 noIdentityReplacement: skip every _build* output directory, not only _build.
A test run with METEOR_LOCAL_DIR writes rspack's output to
_build-local-test/, a bundled second copy of every source file. The guard
walked into it and reported a replacement the source had already fixed.

Thanks to xet7 !
2026-10-14 16:47:04 +03:00
Lauri Ojansivu 4234081752 WebDAV attachment storage: attachments in Nextcloud Files or any WebDAV server.
Admin Panel -> Attachments has a WebDAV (Nextcloud Files) pane beside S3,
Azure and Google Cloud Storage: a folder address, a user and an app
password, with the same read and write switches, connection test, default
storage choice and Move Attachment target as the others. Files are stored
with PUT, read with GET and removed with DELETE; the folders are made with
MKCOL as needed, and the connection test is a PROPFIND of the folder.

The password is a secret field like the other backends' keys: never
published, and kept when the form is saved without it. The address is the
site admin's own setting, so a Nextcloud on WeKan's own network can be
used; redirects are refused so it cannot be pointed somewhere else.

With the CalDAV sync of the previous commit this completes #687. The
Nextcloud ExApp is built by .github/workflows/nextcloud.yml; publishing it
to the Nextcloud app store is the maintainer's own step.

Thanks to scriptPilot and xet7 !

Fixes #687.
2026-10-14 16:46:59 +03:00
Lauri Ojansivu c76f3e2e98 CalDAV: a board's card due dates in Nextcloud Calendar or any CalDAV calendar.
Board Settings -> Date has Calendar sync. A board admin gives a calendar
collection's address, a user and an app password made for WeKan, and turns
it on. Each card with a due date is one event, <cardId>.ics, UID
<cardId>@wekan, an hour long, with the card's title and link; a card that
loses its due date or is archived has its event removed. Every 15 minutes
the events are read back, and a due date moved in the calendar comes back
to its card. Sync all now pushes every card at once.

The link and its password are in a server-only collection that is never
published, and only a board admin can read or change it. Requests go
through webhookFetch: the SSRF guard, so a calendar on WeKan's own network
is reached only when a site admin lists it under Internal destinations.
Text values are escaped as RFC 5545 requires - a semicolon included, which
the first version of the test did not catch.

Thanks to scriptPilot and xet7 !
2026-10-14 16:46:29 +03:00
Lauri Ojansivu bf633b9c72 ActivityPub: a public board can be followed from Mastodon and the fediverse.
Admin Panel -> Settings has a switch, off by default. With it on, every
public board is an ActivityPub actor, @board-<id> at this WeKan's address:
webfinger finds it, its actor document carries its public key, and its
outbox and followers collections can be read. New card activity on the
board is sent to its followers as signed Notes.

It is read-only. The inbox acts only on Follow and Undo Follow, and only
with a valid HTTP signature from the actor that sent it; nothing a
fediverse server sends changes a board. Private boards are never actors:
every board route goes through one helper that serves only a public,
unarchived board while the feature is on, and
tests/restBoardDomainsAccess.test.cjs now knows that helper. The keys are
in a server-only collection, and every outgoing request goes through the
SSRF guard.

Thanks to ASLLR and xet7 !

Fixes #4198.
2026-10-14 16:46:15 +03:00
Lauri Ojansivu 2c9df4db88 Add the ActivityPub, CalDAV and WebDAV strings, translated into every locale.
The 16 strings of the next three commits: the Admin Panel switch that lets
public boards publish to the fediverse, Board Settings' calendar sync, and
the WebDAV attachment storage pane - in English, pending on Transifex, and
in all 234 other locales from each locale's own words for board, card,
due date, storage, folder and password.

Low confidence: tlh, chr, wal, ay, ace, rn, sm, st, kw, csb, uz-AR, iu, zgh,
nah, gn, wo, rup, se, sn, tn, kl, bua, bi, to, ti, nso, la, tig, ak, bm, ff,
gv, vo, sah, ve, tpi, dz, qu, ee, lg, ks, wa, ve-CC, bo, fj, haw, lld, om,
ss, ts, ve-PP and wa-RR; "sync", "folder", "server" and "app password" were
coined where the language has no settled word. "WebDAV (Nextcloud Files)"
names the storage in each language, so it is not left in English.

Thanks to xet7 !
2026-10-14 16:45:44 +03:00
Lauri Ojansivu 0c6c0c6021 Index cards.parentIds, so finding subtasks no longer scans every card.
Since a card can be a subtask of several parents (v12.10), its children are
found with { $or: [{ parentId }, { parentIds }] }. Only parentId had an
index, and a $or uses indexes only when every branch has one, so the query
was a collection scan - in the card remove hook for every removed card, in
card copy and in the Excel and PDF exporters. parentIds now has its own
index beside parentId's.

tests/cardQueryIndexes.test.cjs reads the branches of every $or in
models/lib/cardParents.js and requires an index for each field, so a new
branch cannot arrive without one.

Thanks to Raymond-Voogt and xet7 !

Fixes #6770.
2026-10-14 16:43:35 +03:00
Lauri Ojansivu 587efab82b Fix GitHub CodeQL alerts #584 and #585.
#584 (bad HTML filtering regexp): tests/accountEmailHtml.test.cjs checked
that the e-mail layout escapes markup with /<img|<script|<b>/, which a
<SCRIPT> passes. It now lists every tag in the output, in any letter case,
and allows only the layout's own div, p, a and br; an upper- and mixed-case
input is tried as well. The renderer was already escaping them.

#585 (incomplete regular expression for hostnames): the same test made the
reset address into a RegExp; it compares the text instead.

Both are pinned as shapes in tests/securityAlerts20261012.test.cjs, which
found more of them: a case-sensitive <script> pattern in jsWebLabels.test.cjs,
a path spliced into generated code in singleFile.test.cjs (now passed through
the child's environment), and twelve test patterns with a host name's dot
unescaped. No regex literal in the tree leaves one unescaped now.

These are in tests, so there is no attempt at runtime to record in Admin
Panel -> Problems.

Thanks to GitHub CodeQL and xet7 !
2026-10-14 16:43:00 +03:00
Lauri Ojansivu cd4c4ad9ce Close #1273: keeping #label and @member in a new card's title is done.
It shipped in v12.43 (a6b2938e1c): Member Settings -> "Keep #label and
@member in new card titles". This pins the issue's own example: with the
switch on, "fix menu for #PROJECT by @member" stays whole; off, the default,
it still becomes "fix menu for by".

Thanks to biji and xet7 !

Fixes #1273.
2026-10-14 16:31:45 +03:00
Lauri Ojansivu 2b84e6b616 Translate the push, linked-list, column-set, drawing and team-tab strings into every locale.
The 39 strings of browser push notifications, the webhook activity picker,
the activity feed, linked whole lists, column sets, card drawing and the
organization/team tabs, in all 234 non-English locales, from each locale's
own board, card, list and member terms.

Low confidence: ace, ay, chr, csb, kw, rn, sm, st, tlh, wal, zgh, iu, nah,
gn, wo, tn, sn, se, rup, cv, bo, fj, haw, lld, om, ss, ts, ve-PP and wa-RR;
"column", "browser", "device" and "drawing" were coined where the language
has no settled word.

Wrong-language values corrected: Macedonian had the Russian "Уведомления"
and the Bulgarian "Сваляне" (now Известувања, Преземи); Catalan, its Spain
variant and Valencian had "Membres d'Afegeix" for Add members; Klingon had
s3-bucket in English. Catalan "No" is the Catalan word and is now accepted
as complete, as Aragonese "no" already was.

Thanks to xet7 !
2026-10-14 16:31:41 +03:00
Lauri Ojansivu edc5ecd477 Merge remote-tracking branch 'origin/main' 2026-10-14 16:08:55 +03:00
Lauri Ojansivu d04b8444a6 All Boards: a tab per organization and team, where their admins manage members.
The left menu of All Boards gets a tab for each organization and team
the member is in. A tab shows the boards shared with that group (through
the facet filter of #4791) and who is in it; the group's admins -
orgs.$.isAdmin, and now teams.$.isAdmin for teams - and site admins add
members by user name and remove them there. A group whose membership
comes from the sign-in provider (LDAP and other directory sync) is shown
read-only, even to a site admin: WeKan reads directory groups and never
writes them back. A member sees only their own groups; organization and
team memberships stay out of what a client may write.

models/lib/groupMembers.js decides access and builds the membership
changes; tests/groupMembers.test.cjs covers admins, site admins, members,
strangers, a directory-synced group, double membership and the server's
checks. The new strings are English placeholders until the next
translation pass.

Thanks to xet7 !

Fixes #802.
2026-10-14 15:52:07 +03:00
Lauri Ojansivu c772769c0e Draw or write by hand on a card, saved as PNG and SVG and editable later.
Card menu / Drawing opens a drawing surface: pick a colour and a pen
width and draw with a mouse, pen or finger; undo the last stroke or clear.
Saving keeps the strokes on the card (drawings), so the drawing opens
again for editing from the same popup, and uploads a PNG of it as a
card attachment through the normal upload - so it shows wherever a
picture does. "Download as SVG" builds the SVG from the strokes. The
drawing code is the dependency overlay's ("Red Strings", #3392): strokes
are SVG paths.

Only numbers, the allowed colours and pen widths ever reach the SVG -
a point or colour carrying text is dropped - so nothing typed becomes
markup, and the SVG is not uploaded as a file at all (uploads treat SVG
as dangerous content). models/lib/cardDrawing.js cleans strokes, builds
the SVG and replaces an edited drawing; tests/cardDrawing.test.cjs
covers paths, dots, edits, injected colours, widths and points, clamping
and the bound. A Meteor run started with the new card field. The new
strings are English placeholders until the next translation pass.

Thanks to aleroot and xet7 !

Fixes #1178.
2026-10-14 15:51:47 +03:00
Lauri Ojansivu 03d0144538 Column sets: the same cards in several named sets of columns.
A new board view, Column sets. A board keeps named column sets - say
Completion, Priority, Milestone - each with its own coloured columns, and
every card is in one column of each set, or "Not placed". Picking a set
shows its columns; dragging a card to another column changes only where
it is in that set - its list, swimlane, labels and members stay - and
each card shows, as coloured labels, its column in the other sets, as the
issue asked. Board admins make and delete sets and columns; moving cards
needs the right to edit a card. Clicking a title opens the card.

Registered like every view: the Board View menu (appended after Version
Control), the default order and VIEWS_ADDED_LATER, the user's boardView
values and the API description, and an instance feature group so the
Admin Panel can turn it off. models/lib/columnSets.js holds the logic;
tests/columnSets.test.cjs covers places, removed columns, the other
sets' badges, foreign columns, dotted keys and the bounds, and
boardViewMenu / boardViewSettings now expect the view last. A Meteor run
started with the new fields. Documented in
docs/Features/Board/Column-Sets.md. The new strings are English
placeholders until the next translation pass.

Thanks to kentonv and xet7 !

Fixes #211.
2026-10-14 15:48:42 +03:00
Lauri Ojansivu 2fd3993538 A linked whole list: a live mirror of another board's list.
The add-list form gets "or link a list from another board": pick a board
you can see and one of its lists, and this board gets a list that shows
that list's cards as they are, live. They stay the source board's cards,
under its rules - opened, edited, moved by whoever may write the source
board - and a card typed into the mirror is added to the source list by
addCardToLinkedList, which checks the right to write in that list. In
the Swimlanes view the mirror shows its cards once, in the first
swimlane. The source cards come through linkedListCards, published only
to a subscriber who may see both boards, so the board publication is not
widened. A list cannot mirror its own board, an archived or foreign list,
or another mirror.

models/lib/linkedList.js decides; tests/linkedList.test.cjs covers the
selector, the swimlane rule, the refused links, both visibility checks
and the source board's write rule. A Meteor runtime run started with the
new list fields. The new strings are English placeholders until the next
translation pass.

Thanks to javen9881 and xet7 !

Fixes #2809.
2026-10-14 15:44:38 +03:00
Lauri Ojansivu 0a5ae3e205 Browser push notifications, per device.
Member Settings / Notification Settings / "Browser notifications on this
device": the browser asks for permission and gives a push subscription,
and from then on every notification the member gets in WeKan - the same
recipients, watching and muted kinds as the tray and e-mail - also pops
up from that browser even with WeKan closed; a click opens the card. Turn
it on per browser and device, up to ten per account.

web-push 3.6.7 (pinned) sends it with this WeKan's own VAPID key pair,
made once and kept in a server-only collection; only the public key
leaves the server. A subscription's endpoint is chosen by the browser and
POSTed to by the server, so it is accepted only over https and only at
the push services browsers use - an internal address or another site is
refused when saved and again before every send. Gone subscriptions are
forgotten. The service worker opens only paths of this WeKan. Not
offered on Sandstorm.

models/lib/webPush.js decides; tests/webPush.test.cjs covers the push
services, internal and look-alike hosts, foreign click targets and the
bound; server/lib/tests/webPush.tests.js runs in Meteor (passing): one
key pair, only the public key out, an internal endpoint refused. With
the webhook activity picker (c3321926ab) and the all-boards activity
feed (656af74b2c), this completes both issues. The new strings are
English placeholders until the next translation pass.

Thanks to saurabharch, hingerl and xet7 !

Fixes #2026,
Fixes #1377.
2026-10-14 15:44:01 +03:00
Lauri Ojansivu d3966fa6b9 Fix the custom field cardKinds schema: WeKan stopped at startup.
The Story and Task commit (0d77882c3b) declared cardKinds as
`type: [String]`, which the schema library rejects at startup ("Invalid
definition for cardKinds field: type may not be an array"), so WeKan
stopped. It is `type: Array` with a 'cardKinds.$' entry of the two kinds
now. Found by a Meteor runtime test run; tests/cardKinds.test.cjs now
pins the form and checks that no array shorthand type is left in the
schemas these changes touched.

Thanks to xet7 !
2026-10-14 15:41:26 +03:00
Lauri Ojansivu 656af74b2c An activity feed across all of a member's boards.
Member menu / "Activity on all my boards" (/activity) lists what
happened on every board the member sees in All Boards, newest first, a
page of 50 at a time: who, what - in the same sentence the notifications
use - on which card and board, and when, with the card linked. It never
carries a field's value, so an admin-only custom field cannot leak
through it; on a board where the member sees only cards assigned to
them, only those cards' activity is listed; and it is empty when the
Admin Panel hides activities or turns them off.

models/lib/activityFeed.js shapes and filters the entries;
tests/activityFeed.test.cjs covers the entry, an unseen board, the
assigned-only rule, the bound and the server's switches. Part of #2026
and #1377, with the webhook activity picker and browser push. The new
strings are English placeholders until the next translation pass.

Thanks to xet7 !
2026-10-14 15:37:31 +03:00
Lauri Ojansivu c3321926ab Webhooks: pick which activities a webhook is sent.
An outgoing webhook was sent every activity, or whatever its activities
list said with no way to change it in the interface. Its form now has
"Send this webhook:" with "Every activity" and a tick per kind of
activity - the same groups a member mutes notifications by (#572):
labels, members, assignees, comments, moves, dates, checklists,
attachments, custom field values, archive and created. Ticks are saved as
the act-<type> descriptions the activity hook matches webhooks by;
nothing ticked, or Every activity, stays "all", the default.

models/lib/webhookActivityPicker.js maps both ways;
tests/webhookActivityPicker.test.cjs checks that a picked group selects
exactly its activities through webhookActivitySelection, the form state,
and that a webhook never silently gets nothing. Part of #2026 and #1377,
with the activity feed and browser push. The new strings are English
placeholders until the next translation pass.

Thanks to xet7 !
2026-10-14 15:37:16 +03:00
Lauri Ojansivu d8690a26f5 Cards changed since a member last opened them are marked, as a Member Settings switch.
Member Settings / "Mark cards changed since I last opened them", off by
default. With it on, a minicard gets a blue edge when anything of the
card changed after the member last opened it - its own fields (title,
description, dates, labels, members), its checklists and their items,
attachments and comments - until they open it. Turning it on records
when, so the cards a member never opened are not all marked at once, and
closing a card counts as having seen it, so their own edits made while
it was open do not mark it.

models/lib/unreadChanges.js takes the latest change across those
documents and compares it; tests/unreadChanges.test.cjs covers each kind
of change, seen since, off, older than the switch and the wiring. The
new strings are English placeholders until the next translation pass.

Thanks to Lacour20, martinpospech and xet7 !

Fixes #2111,
Fixes #1604.
2026-10-14 15:37:02 +03:00
Lauri Ojansivu 946f032883 Translate the cross-board subtask, checklist owner, shared header, facet and card kind strings.
The 16 strings added as English placeholders by the #1931, #1618, #2805,
#4791 and #2012 commits are translated in all 234 locales, each with that
locale's own words for board, card, list, swimlane and label, and its
usual agile terms for Story and Task (where that is the English word, a
still-correct variant such as "User-Story").

Low confidence, for a native speaker to check: ace, ak, am, as, ay, ba,
bi, bm, bo, bua, chr, co, csb, cy-GB, dz, ee, ff, fj, fo, fur, ga, gd, gn,
gv, ha, haw, hsb, ht, ig, iu, kl, km, km-KH, km_KH, kok, ks, kw, la, lg,
lld, mai, mg, mi, nah, nap, nd, nso, ny, oc, om, or_IN, ps, qu, rm, rn,
rup, rw, sah, sc, scn, sd, se, sm, sn, so, ss, st, szl, tg, ti, tig,
tk_TM, tlh, tn, to, tpi, ts, tt, uz-AR, ve, ve-CC, ve-PP, vo, wa, wa-RR,
wal, wo, wuu-Hans, xh, yi, yo, zgh.

Thanks to xet7 !
2026-10-14 15:36:32 +03:00
Lauri Ojansivu 1647ed5a78 Record Story and Task cards, cross-board subtasks, checklist owners and the rest in Upcoming.
The commits since: story and task cards, an existing card from another
board as a subtask, a person responsible for each checklist item with
dated items in the calendar and Gantt, adding and dragging cards in the
Table view, one shared list header row, All Boards facets, and a board
admin adding only their own organizations and teams. #5141, #864, #1931,
#1618, #2805, #4791 and #2012 leave TODO Later.

Thanks to xet7 !
2026-10-14 15:27:12 +03:00
Lauri Ojansivu 0d77882c3b Story and Task cards, with custom fields of their own.
A card can be a Story or a Task: card menu / Card kind (or an ordinary
card again), with a book or a checked-box badge on the minicard. A custom
field can be limited to card kinds - "Only on these card kinds", none
ticked being every card as before. A kind's own fields are added to a
card when it gets that kind, and shown only on cards of it; a field of
no kind shows on every card. The one choke point every render uses,
customFieldsWD, leaves out fields of other kinds, so the card details and
minicard agree.

models/lib/cardKinds.js decides; tests/cardKinds.test.cjs covers the
fields a kind brings, not twice, other kinds' fields staying off, unknown
kinds, and the card menu, badge and the field's ticks. The new strings
are English placeholders until the next translation pass.

Thanks to nightwind27 and xet7 !

Fixes #2012.
2026-10-14 15:26:00 +03:00
Lauri Ojansivu 000bc1c490 All Boards groups boards by first character, organization, team and board label.
A facet bar above All Boards shows the first characters of the board
names (one emoji or letter, any case), the organizations and teams the
boards are shared with, and the member's own board labels - each with
its count - and a click shows only that group, in the server-paged
sorted views and the drag-ordered one alike; again, or "Show all boards",
clears it. Board labels are the member's own (profile.boardLabels):
"apply label X to the boards whose name includes Y" files many at once,
and a label can be removed. Counts are over the boards the member sees
in All Boards - the same visibility selector as getAllBoardsPage, hidden
boards left out - never more.

models/lib/allBoardsFacets.js counts, builds each group's selector and
applies labels; tests/allBoardsFacets.test.cjs covers characters, an
inactive organization link, labels of boards no longer visible, escaped
and injected facet values, empty labels and the server's visibility. The
new strings are English placeholders until the next translation pass.

Thanks to xet7 !

Fixes #4791.
2026-10-14 15:25:36 +03:00
Lauri Ojansivu 9d95bda245 One shared, sticky list header row for all swimlanes, as a board setting.
Board Settings / Swimlane gets "One shared list header row", off by
default. With it on, the Swimlanes view shows the list headers once - the
first swimlane's - and the other swimlanes no longer repeat them; that
row stays at the top while the board scrolls, through the existing
sticky list header rule of #3847, which the setting turns on, rather than
a second sticky mechanism (tests/stickyListHeadersSwimlanesView keeps
that so). The only new CSS rule is scoped to boards with the setting.

tests/sharedListHeaders.test.cjs checks the board field, the toggle, the
first-swimlane class, the reuse of the sticky rule and the scoping. The
new strings are English placeholders until the next translation pass.

Thanks to mikesutton and xet7 !

Fixes #2805.
2026-10-14 15:25:17 +03:00
Lauri Ojansivu 7ca1489279 Checklist items get a person responsible, and dated items show in the calendar and Gantt.
Each checklist item gets a "who is responsible" button: the board's
active members to pick from, or nobody; the avatar then shows on the
item. A deny rule keeps the responsible person an active member of the
item's board. Checklist items with a due date (#4755) now appear in the
calendar and in the Frappe and dhtmlx Gantt views as "card: item", with
their own `ci:` ids: opening one opens its card, a calendar drag of one is
refused, and moving one in a Gantt moves only the item's own due date -
never the card's dates.

models/lib/checklistItemSchedule.js collects dated items (not of archived
or missing cards) and decides who may be responsible;
tests/checklistItemSchedule.test.cjs covers both and the wiring of the
three views. tests/checklistItemDueDate.test.cjs, which pinned #4755 as
"due dates only", now says the assignee is #1618's own field. The new
strings are English placeholders until the next translation pass.

Thanks to martinbruno90 and xet7 !

Fixes #1618.
2026-10-14 15:24:57 +03:00
Lauri Ojansivu e9af50ff07 An existing card from another board as a subtask.
The "add existing card as subtask" picker offered only cards of the same
board. It now has a board picker: the card's own board first, then the
other boards the member may write. Another board's cards come from the
server (crossBoardSubtaskCandidates), and picking one attaches it through
addCrossBoardSubtask, which checks on the server, with server data, that
the member may write both the parent's and the child's board, that the
child is an ordinary, not archived card, and that no loop forms through
any parent. The card keeps the parents it has. Same-board picks work as
before.

tests/crossBoardSubtasks.test.cjs runs the loop check across boards and
checks both write rights, the card types, the escaped search and the
popup. The new string is an English placeholder until the next
translation pass.

Thanks to TiibCD and xet7 !

Fixes #1931.
2026-10-14 15:24:23 +03:00
Lauri Ojansivu 5409e059ae Translate the Mattermost, Share to WeKan, passkey and hidden board strings.
The 27 strings added as English placeholders by the #1297, #1293, #5847
and #4265 commits are translated in all 234 locales, each with that
locale's own words for board, card, members and its Member Settings menu
name; WeKan, Mattermost and https:// are kept as they are. Where a
language's word for passkey is the English one, the popup title is
"Your passkeys", so it is not left equal to the English.

Low confidence, for a native speaker to check: ace, ak, am, as, ay, ba,
bi, bm, bo, bua, chr, csb, cv, dz, ee, ff, fj, fo, gd, gn, gv, ha, haw,
hsb, ig, iu, kl, km_KH, kok, ks, kw, la, lg, lld, mai, mg, mi, nah, nap,
nd, nso, ny, om, qu, rn, rup, sah, sc, se, si, sm, sn, ss, st, szl, ti,
tig, tk_TM, tlh, tn, to, tpi, ts, tt, ug, uz-AR, ve, ve-CC, ve-PP, vo, wa,
wal, wo, wuu-Hans, xh, yi, zgh.

Thanks to xet7 !
2026-10-14 15:23:58 +03:00
Lauri Ojansivu 0a566dc9ac Table view: add cards, and drag a row onto another to move its card.
The Table view only listed cards. Now a member who may write gets an add
row under the table - a title, a list and a swimlane - and can drag a row
onto another: the dragged card goes just before the target card, in the
target's list and swimlane, through the same card.move the board uses.
Adding goes through tableViewAddCard, which checks that list and swimlane
are on that board and not archived and that the member may write in that
list, as for any card added. Rows are not draggable without the right.

models/lib/tableViewDrop.js places the card; tests/tableViewDrop.test.cjs
covers between, first and same-list drops, a drop onto itself, the
server's checks and that only writers get the controls.

Thanks to guakamole and xet7 !

Fixes #864.
2026-10-14 15:13:09 +03:00
Lauri Ojansivu ed2649f702 A board admin may add only the organizations and teams they belong to.
setBoardOrgs and setBoardTeams refused nothing but non-admins: a board
admin could link any organization or team of the instance to a board,
and with it give every one of its members the board. Now an organization
or team that is new to the board must be one the board admin belongs to;
a site administrator may add any. Links already on the board stay -
somebody else may have added them - and removing or switching one off is
always allowed. The pickers offer only the admin's own groups, and the
org and team publications, which gave non-admins nothing (so the pickers
were empty for them), now give a signed-in user their own groups' names.

models/lib/boardGroupLinks.js decides; tests/boardGroupLinks.test.cjs
covers own groups, existing links, removals, site admins, a foreign group
refused and not offered, and that both methods check before writing.

Thanks to chrisi51 and xet7 !

Fixes #5141.
2026-10-14 15:04:41 +03:00
Lauri Ojansivu d9e2d7bedd Record passkeys, ntfy, Share to WeKan and the other decided features in a new Upcoming.
v12.43 was cut before these commits, so they go into a new Upcoming above
it: passkey login and Log out everywhere, the login webhook, ntfy, the
clear HTML e-mail default, Mattermost direct messages, read-only Rules
for members, the rule e-mail member picker, the subtask landing swimlane,
hidden team boards, Share to WeKan on Android, Sandstorm card sharing and
the LibreJS labels page. #1953, #2217, #2290, #2435, #4434, #5148, #1040,
#1188, #1297, #1293, #5847 and #4265 leave TODO Later.

Thanks to xet7 !
2026-10-14 15:02:41 +03:00
Lauri Ojansivu 1acc9f2b9f Leaving a board that came through a team hides it, restorably.
A board a member reaches through one of their teams or organizations, or
their e-mail domain, could not be left: without a member entry quitBoard
refused it, and with one the board stayed in All Boards through the team.
Now leaving such a board puts it in the member's own hidden list
(profile.hiddenBoards): All Boards leaves it out, in the paged sorted
views and the drag-ordered one, and Member Settings / Hidden boards
brings it back with Restore. The team or organization membership is not
touched, and access stays the board's own. An All Boards tile of a board
the member is not on gets an eye-slash "leave" button; a member who also
has the board through a team is both removed and hidden. A board reached
only by membership leaves as before.

models/lib/boardInvites.js decides (planQuitBoard's hide,
reachesBoardOtherwise, hiddenBoardsAfter); tests/hiddenBoards.test.cjs
covers team, organization and domain reach, an inactive link, the bound,
that the team is not touched and the All Boards filters.
tests/boardInvites.test.cjs expects the plan's hide: false where nothing
else reaches the board. The new strings are English placeholders until
the next translation pass.

Thanks to LaureHades and xet7 !

Fixes #4265.
2026-10-14 15:01:14 +03:00
Lauri Ojansivu 63ef900d56 Passkey (WebAuthn) login, and Log out everywhere.
Member Settings gets Passkeys: add a passkey on this device under a name,
see the account's passkeys and remove one. The sign-in page gets "Sign in
with a passkey": the browser offers this WeKan's passkeys, no user name
needed. Verification is @simplewebauthn/server 14.0.3 (MIT, pinned)
against ROOT_URL's host and origin, so a passkey cannot sign in to another
site or through another address; each sign-in signs a fresh challenge
that is used once and expires in five minutes, and the signature counter
must move forward, so a copied passkey is refused. A disabled account
cannot sign in with one (the existing validateLoginAttempt hooks apply).
Passkeys are a top-level user field, never published and not writable
from the client; up to 20 per account. Not offered on Sandstorm.

Member Settings also gets Log out everywhere: after a confirmation every
login token of the account goes, so Meteor ends every session - other
browsers, phones, the desktop app - and this one. Nothing else changes.

models/lib/passkeys.js holds the relying party, base64url and the
browser conversions; client/lib/passkeys.js uses navigator.credentials
directly. tests/passkeys.test.cjs signs assertions with a software ES256
authenticator: a real signature verifies, and another key, another
origin, another challenge or a counter that went back are refused; it
also checks single-use challenges, the unpublished field and the pin.
server/lib/tests/passkeys.tests.js runs in Meteor (2 passing): the module
bundles, login options name no account, and registration needs a user.
Documented in docs/Features/Login/Passkeys.md. The new strings are
English placeholders until the next translation pass.

Thanks to xet7 !

Fixes #5847.
2026-10-14 14:59:25 +03:00
Lauri Ojansivu a4727e6764 Allow the Sandstorm share helper's two addresses in the risk baseline.
models/lib/sandstormShare.js (#1188) parses a page path against
http://grain.invalid, a reserved name that is never contacted, and cites
Sandstorm's documentation of startSharing.

Thanks to xet7 !
2026-10-14 14:56:50 +03:00
Lauri Ojansivu 52b0ea0d81 Android: share to WeKan from other apps.
The web app manifest declares a share_target, so on a phone where WeKan
is installed as an app it appears in Android's Share menu. Sharing a web
page, a note or a message opens /share-target, "Add to WeKan": pick a
board and a list, and what was shared becomes a card there - the page's
title or the text's first line as the title, the text and the address in
its description, the address once - and the card opens. Only boards the
member may add cards to are offered, and the server checks the right to
write in that list, on that board, as for any card added.

models/lib/shareTarget.js turns a share into a card and holds the
manifest's declaration; tests/shareTarget.test.cjs covers what browsers,
notes and chat apps share, an empty share, javascript: addresses, the
manifest, the route and the server's checks. Documented in
docs/Platforms/FOSS/OS/Android.md. The four new strings are English
placeholders until the next translation pass.

Thanks to memeplex and xet7 !

Fixes #1293.
2026-10-14 14:55:04 +03:00
Lauri Ojansivu 349d6dc5e6 Webhooks: also send a Mattermost direct message to the card's people.
A saved outgoing webhook gets "Also send a Mattermost direct message to
the card's members and assignees". With it ticked, every message about a
card also goes to each of its members and assignees as the webhook's own
message with channel "@username" - Mattermost's direct-message channel -
so they hear about their cards without following the channel. The person
who made the change is not messaged, nobody twice, a name that is not a
valid Mattermost user name is skipped, and a card stops at 50. A board
webhook does this only for its own board's cards. The direct messages go
at once, best effort, through webhookFetch; the channel message keeps its
grouping and schedule. Two-way webhooks are left as they are.

models/lib/mattermostDirectMessages.js picks the people and builds the
messages; tests/mattermostDirectMessages.test.cjs covers them, the actor,
bad names, the bound, and that it is off unless ticked and sent through
the SSRF guard. docs/Features/Webhooks/Chat/Mattermost.md says what
Mattermost needs. The two new strings are English placeholders until the
next translation pass.

Thanks to xet7 !

Fixes #1297.
2026-10-14 14:54:38 +03:00
Lauri Ojansivu fdc52787e1 Translate the subtask swimlane, read-only Rules, login webhook, rule email member and ntfy strings.
The 24 strings added as English placeholders by the #1953, #2217, #2290,
#2435 and #5148 commits are translated in all 234 locales, with each
locale's own words for rules, members, board, swimlane and subtasks;
@username and the ntfy.sh example addresses are kept as they are, and the
quoted label in the Rules option's description is that locale's own
translation of the board admin's tick.

The Acehnese "keep #label" strings from the earlier pass used Malay
"simpan"; they say "keubah", the file's own word, now
(tests/wrongLanguageLocales). The Maori, Northern Sotho, Somali and Tok
Pisin recovery suites count 50 recovery keys since the four bulk actions
of #6768.

Low confidence, for a native speaker to check: ace, ak, am, ay, ba, bi,
bm, bo, bua, chr, co, csb, cv, dz, ee, ff, fj, fo, fur, fy-NL, gd, gn, gv,
ha, haw, hsb, ig, iu, kl, kok, ks, kw, la, lb, lg, lld, mai, mg, mi, my,
nah, nap, nd, nso, ny, oc, om, or_IN, pap, ps, qu, rn, rup, rw, sah, sc,
scn, sd, se, sm, sn, so, ss, st, szl, tg, ti, tig, tk_TM, tlh, tn, to,
tpi, ts, tt, ug, uz-AR, ve, ve-CC, ve-PP, vo, wa, wa-RR, wal, wo,
wuu-Hans, xh, yi, yo, zgh.

Thanks to xet7 !
2026-10-14 14:54:17 +03:00
Lauri Ojansivu d1d806de2c Sandstorm: Copy card URL shares the card through Sandstorm.
Inside a grain WeKan's own address reaches nobody, so the copied card link
was useless to anybody else. On Sandstorm, Copy card URL - in the card
menu and in the card's link popup - now sends the shell
{ startSharing: { pathname } }, which opens Sandstorm's Share Access
dialog; its sharing link opens the grain at that card. The shell accepts
only a path starting with "/", so only the card's path is sent. Outside
Sandstorm both buttons copy as before.

models/lib/sandstormShare.js builds the message;
tests/sandstormShare.test.cjs checks it, that nothing else reaches the
shell, and that both buttons share only inside a grain.

Thanks to AlexanderZobkov and xet7 !

Fixes #1188.
2026-10-14 14:41:13 +03:00
Lauri Ojansivu 631a7fdfe2 LibreJS: a JS Web Labels page for the browser bundle.
LibreJS runs a page's JavaScript only when it can tell the code is free.
A Meteor bundle is generated, minified and named by hash, so it cannot
carry per-file @license comments; LibreJS's other way is a JS Web Labels
table, linked with rel="jslicense". WeKan now serves /jslicense.html,
built from the client bundle it is actually serving (web.browser and
web.browser.legacy, under ROOT_URL's path), labelling each script with
the free licenses the bundle combines - Expat (MIT), Apache-2.0, Modified
BSD, FreeBSD and ISC, under LibreJS's own names and addresses - and with
WeKan's source at the running version. Every page links it from <head>,
whether or not the custom head is on.

models/lib/jsWebLabels.js builds the table and the link;
tests/jsWebLabels.test.cjs checks the scripts chosen from a manifest, the
table LibreJS reads, escaping and the wiring. releases/risk-baseline.json
allows the license addresses, and ntfy.sh, which the ntfy channel
(d873af70e8) names in a placeholder and comments.

Thanks to alexgleason and xet7 !

Fixes #1040.
2026-10-14 14:39:40 +03:00
Lauri Ojansivu d873af70e8 ntfy as a notification channel, per member.
Member Settings / Notification Settings gets "ntfy phone notifications":
a member pastes their ntfy topic's address, and an access token if the
topic needs one, and each notification they get in WeKan - the same
recipients, watching and muted activity kinds - also arrives in the ntfy
app, with the board and card as the title, what happened as the message
and a tap opening the card.

It uses ntfy's JSON publishing, so a title in any language needs no
header encoding, and goes through webhookFetch: the SSRF guard, Internal
destinations for a self-hosted ntfy, the Powerbox on Sandstorm. An
admin-only custom field value still reaches only board admins. One read
per activity finds the recipients who turned it on; sending is deferred
and best effort, so ntfy being down never touches e-mail or the tray.
The address and token are a top-level user field, saved and read only
through the member's own methods, not client-writable and never
published.

models/lib/ntfy.js parses topic addresses and builds the request;
tests/ntfy.test.cjs covers the request, bad addresses, tokens with header
injection, javascript: links, off, and that the setting is unpublished
and sent only through webhookFetch. Documented in
docs/Features/Notifications/ntfy.md. The new strings are English
placeholders until the next translation pass.

Thanks to xet7 !

Fixes #5148.
2026-10-14 14:37:00 +03:00
Lauri Ojansivu 058677e078 E-mails in the clear HTML layout by default: notifications and account mail.
Notification e-mail now defaults to the clearly arranged layout of #5171 -
board first, board, actor and card in bold and linked, with a plain-text
alternative. One sentence (classic) is still one choice away at the
Admin Panel, board and member levels, and the tray keeps the classic line.

Account e-mails - reset password, verify e-mail, enroll account, and the
board and registration invitations - get an HTML part in the same style,
built from the same translated text, which is still sent as the text
part: no translation changes. Every piece of text is escaped first, the
link paragraph becomes a button, and only an http(s) address is linked. A
caller's own HTML and texts that are not WeKan's e-mail strings are left
as they were, and a failure to build the HTML leaves the text alone.

models/lib/accountEmailHtml.js renders it; tests/accountEmailHtml.test.cjs
lays out the real English texts and checks that markup in a name, a board
title or the site name stays text and only http(s) is linked.
notificationDelivery's default assertions now expect clear for e-mail,
and emailLocalizationPreparation's harness allows the new module, checks
the HTML part, and checks the absent-HTML case with literal text.

Thanks to Meeques and xet7 !

Fixes #4434.
2026-10-14 14:33:10 +03:00
Lauri Ojansivu 75e260966b Rule emails: pick board members for To from a list with autocomplete.
The send-email action gets a member picker under To: typing filters the
board's active members by user name or full name, and a pick adds
@username to To. Addresses are still typed into To as before. When the
rule runs, each @username becomes that member's current email address,
so the rule - and whoever edits it - holds a user name, never somebody's
address, and a member who changes address is still reached. A @name of
somebody who is not an active member of the board, or has no address, is
dropped rather than sent as text, and nobody is mailed twice.

models/lib/ruleEmailMembers.js holds the parsing, the choices and the
expansion; tests/ruleEmailMembers.test.cjs covers them, non-members,
duplicates, an address with @ inside, and that the picker never reads an
email address. rulesHelper imports it, so the ruleEmailFailure harness,
which runs rulesHelper without require, is given it as it is given
ruleParts. The new strings are English placeholders until the next
translation pass.

Thanks to chenghongxi and xet7 !

Fixes #2435.
2026-10-14 14:30:10 +03:00
Lauri Ojansivu ea256dd772 Admin Panel: a login and registration webhook, off by default.
Admin Panel / Settings / Global Webhooks gets one address WeKan tells
when somebody logs in, when a new account is made, or both - nothing is
sent until an event is ticked. Each message is a JSON POST with the
event, the user name and the time; the email, the login's IP address,
the full name and the sign-in method are added only when ticked. A token
goes in X-Wekan-Token, as for board webhooks.

It is sent after the login, through webhookFetch - the SSRF guard,
Internal destinations and, on Sandstorm, the Powerbox - and a receiver
that is down only leaves a log line. The setting is read and saved
through site-admin methods and never published.

models/lib/accountWebhook.js cleans the setting and builds the message;
tests/accountWebhook.test.cjs checks the default carries name and time
only, each extra field only when ticked, the events separately, bad
addresses refused, and that no hash, services or user id is sent.
Documented in docs/Features/Webhooks/Login-and-Registration-Webhook.md.
The new strings are English placeholders until the next translation pass.

Thanks to KrunalDholakiya and xet7 !

Fixes #2290.
2026-10-14 14:25:19 +03:00
Lauri Ojansivu f4a3690256 Rules can be shown to board members, read-only, when the Admin Panel allows it.
Admin Panel / Settings / Visibility / Rules gets "Boards may show their
Rules to members, read-only", off by default (Settings
rulesReadOnlyForMembers, site admin only). With it on, a board admin ticks
"Show these Rules to board members, read-only" on the board's Rules page
(rulesVisibleToMembers). Members of that board then get Rules in Board
Settings and see the rules list and each rule's details, with no control
that changes a rule; the methods that change rules keep their board-admin
checks, so nothing is granted. Without both ticks only board and site
admins see the Rules page, which until now drew its list for any member
who opened the address.

models/lib/rulesReadOnly.js decides 'edit', 'read' or 'none', and
client/lib/rulesAccess.js is the one place the Rules page, Board Settings
and rule details ask. tests/rulesReadOnly.test.cjs covers both switches,
each alone, a non-member, and that a reader's page has no edit control.
boardMenuOrder now leaves the reader's alternative branch out of the
admin's sequence and pins it alone; instanceFeatures counts the fourth
site-admin method; roleHelperRegressions follows the canAdmin check to
rulesAccess.js. The new strings are English placeholders until the next
translation pass.

Thanks to justinr1234 and xet7 !

Fixes #2217.
2026-10-14 14:23:31 +03:00
Lauri Ojansivu 740c3920eb Board Settings / Subtasks: pick the swimlane new subtasks land in.
Beside the landing list, Board Settings / Subtasks now has a landing
swimlane, stored as subtasksDefaultSwimlaneId. As with the list, the main
board's choice wins on a shared deposit board, then the deposit board's
own; a swimlane that is archived, gone or on another board is never used.
With none picked, nothing changes: the swimlane with the parent card's
swimlane title, else the default one (#2732). Changing the deposit board
clears the choice, and a copied board does not keep its template's.

models/lib/subtaskLandingSwimlane.js decides; tests/subtaskLandingSwimlane
covers it, and tests/subtaskSwimlaneInheritance2732 now runs the chooser
for the title match instead of reading the inline lookup it replaced. The
two new strings are English placeholders until the next translation pass.

Thanks to BjoernLudwig and xet7 !

Fixes #1953.
2026-10-14 14:19:52 +03:00
github-actions[bot] f6b97816c6 Bump versions for v12.43 2026-10-14 11:13:43 +00:00
Lauri Ojansivu 533c8c273c Prepare v12.43 release 2026-10-14 14:12:36 +03:00
Lauri Ojansivu e7ceb217ea Move the unreleased entries out of v12.42 into a new Upcoming; add #1273 and #2662.
v12.42 was cut on origin while Bots, board search reveal, label order,
avatar order, internal webhook destinations, the how-tos and the
notification recovery fixes were being recorded locally, and the merge
left their entries inside the v12.42 section although that release does
not contain those commits (tests/changelogEntriesBelongToTheirRelease
caught all eleven). The v12.42 section is restored exactly as released,
and those entries move to a new Upcoming, with the keep #label / @member
setting and the FreeBSD rc.d script. Both leave TODO Later.

Thanks to biji and xet7 !
2026-10-14 14:10:56 +03:00
Lauri Ojansivu 9a29dbd8b7 FreeBSD bundle: an rc.d script, so WeKan runs as a service.
releases/bsd/rc.d/wekan runs the bundle's own start-wekan.sh (bundled
Node.js and FerretDB on SQLite) under daemon(8) as an unprivileged
wekan_user, configured from rc.conf: wekan_dir, wekan_port,
wekan_root_url, wekan_data (WRITABLE_PATH), wekan_logfile and wekan_env.
It has no -r, because start-wekan.sh already restarts WeKan, and stop
signals the whole process group so FerretDB stops with node.

releases/bsd/build-bundle.sh puts it in the FreeBSD bundle as
bundle/rc.d/wekan, and bsd.yml fails a FreeBSD zip without it. The
FreeBSD doc has the install steps, the rc.conf table and an outline of a
ports Makefile to propose to FreeBSD.

tests/freebsdRcScript.test.cjs checks the rc.subr contract with sh -n,
that it never runs as root or restarts twice, and that the bundle ships it.

Thanks to xet7 !

Fixes #2662.
2026-10-14 14:09:11 +03:00
Lauri Ojansivu 8fd04f9984 Translate the keep #label / @member title setting into every locale.
The two Member Settings strings added for #1273 were committed as English
placeholders; this fills them in all 234 locales, with each locale's own
words for label, member, card and title. "#" and "@" are kept as they are.

Low confidence, for a native speaker to check: ace, ak, am, ay, ba, bi,
bm, bo, bua, chr, csb, cv, dz, ee, ff, fj, fo, fur, gd, gn, gv, ha, haw,
ig, iu, kl, km-KH, kok, ks, kw, la, lg, lld, mai, mg, mi, nah, nap, nd,
nso, ny, om, pap, qu, rn, rup, rw, sah, sc, se, sm, so, ss, st, szl, ti,
tig, tk_TM, tlh, to, tpi, ts, tt, uz-AR, ve, ve-CC, ve-PP, vl-SS, vo, wa,
wa-RR, wal, wuu-Hans, yi, zgh.

Thanks to xet7 !
2026-10-14 14:09:11 +03:00
Lauri Ojansivu a6b2938e1c Updates 2026-10-14 14:04:07 +03:00
Lauri Ojansivu 331d1e347b Updated translations. 2026-10-14 14:03:00 +03:00
Lauri Ojansivu caa9a29f92 Merge remote-tracking branch 'origin/main' 2026-10-14 14:02:03 +03:00
Lauri Ojansivu cbbc9a7338 Record the notification and cron fixes, internal webhook destinations and the how-tos in Upcoming.
#632, #1324, #1126, #1100 and the internal webhook destinations leave TODO Later.

Thanks to Raymond-Voogt, thuanpq, killua-eu, Dreampie and xet7 !
2026-10-14 14:00:47 +03:00
Lauri Ojansivu e0317a9ff3 Drain the activity notification recovery list; index synced-cron's lookup.
#6768: the recovery scan retried every pending activity notification each
round, and one whose delivery fails for good - its activity deleted or
changed since, a recipient who lost access, a plan with nothing or too much
to send - failed the same way every round, forever, so the list only grew.
Such an error is terminal now: after a ten-minute grace (an activity being
written is never touched) the scan cancels that notification through the
same cancellation an administrator uses, as the system, which compacts it
to a cancelled receipt. A pending notification older than
ACTIVITY_NOTIFICATION_MAX_AGE_DAYS (7) is cancelled as expired instead of
being delivered a week late. Network and other transient failures are still
retried. Admin Panel -> Problems gets "Retry all" and "Cancel older..." for
the whole list at once, site administrators only.

#6769: before each run synced-cron asks whether a job of that name is still
running - { name, finishedAt: { $exists: false } } - every minute for every
job on every instance, and only { intendedAt, name } and the TTL on
startedAt were indexed, so it scanned the job history. { name, finishedAt }
is created at startup with WeKan's ensureIndex.

The new strings are translated in every locale.

Thanks to Raymond-Voogt and xet7 !

Fixes #6768,
Fixes #6769.
2026-10-14 14:00:21 +03:00
Lauri Ojansivu b4d2604e61 Outgoing webhooks may reach internal hosts a site admin lists.
Admin Panel -> Settings -> Global Webhooks -> Internal destinations: the
hosts on WeKan's own network - a Node-RED, a chat server, a Matrix hookshot
- that outgoing webhooks may reach, one per line, a name or an address,
with a port to allow only that port. Empty by default. Only site
administrators read and change it, through methods, and it is never
published, so a board member still can not point a webhook inward and the
list does not describe the network to anyone else. A listed host is called
directly without following redirects; every other address stays behind the
SSRF guard, and the REST routes that save a webhook accept a listed host as
well. The Node-RED how-to (#2017) says how.

The new strings are translated in every locale.

Thanks to xet7 !
2026-10-14 13:59:19 +03:00
Lauri Ojansivu dff0856585 How-to: Matrix notifications through an outgoing webhook, no bot.
matrix-hookshot's generic webhooks post a JSON body's top-level `text` into
a Matrix room, and WeKan's outgoing webhook already sends the activity as
a sentence in `text` - so a board's (or the instance's) outgoing webhook
pointed at a hookshot address is all it takes. WeKan needs no Matrix
account, token or bot.

Thanks to killua-eu and xet7 !

Fixes #1100.
2026-10-14 13:48:27 +03:00
Lauri Ojansivu c47fe98ab0 How-to: RFID tags moving cards through the REST API.
A USB or serial RFID reader and a short Python script, as a WeKan user with
write access to the board: the scanned tag finds its card through an RFID
custom field (GET .../cardsByCustomField) and moves it to another list, or
comments on it, or creates a card for an unknown tag - and a Rule can do
the rest.

Thanks to xet7 !

Fixes #1126.
2026-10-14 13:47:50 +03:00
Lauri Ojansivu f715e95b40 How-to: Bitbucket login through the generic OAuth2 settings.
Bitbucket Cloud needs no provider of its own: an OAuth consumer with the
callback /_oauth/oidc, and the OAuth2 settings pointing at bitbucket.org's
login pages and api.bitbucket.org's /2.0/user, mapping uuid, nickname and
display_name. Checked against the code: an endpoint given as a full URL is
used as it is, and the client secret goes in the token request as Bitbucket
accepts it. Bitbucket's user record has no email, which the how-to says,
with what that means for OAUTH2_ALLOWED_EMAIL_DOMAINS.

Thanks to thuanpq and xet7 !

Fixes #1324.
2026-10-14 13:47:50 +03:00
Lauri Ojansivu 28477b5a85 Rancher: install WeKan with its Helm chart on current Rancher.
The page described Rancher 1 with Rancher Active Proxy and the
wekanteam/wekan:meteor-1.4 image. It now describes Rancher v2: adding the
WeKan chart repository (wekan.github.io/charts), installing with ROOT_URL,
a release tag, a persistent volume for FerretDB's files under
WRITABLE_PATH and the cluster's ingress and certificate issuer, and
upgrading - in Rancher's own screens and with helm.

Thanks to Dreampie and xet7 !

Fixes #632.
2026-10-14 13:47:50 +03:00
Lauri Ojansivu f8f0f6c5ac Record Bots, board search reveal, label order sort and avatar order in Upcoming.
#2110, #2171 and #2040 leave TODO Later.

Thanks to HolgerGH, rlach, mnantel and xet7 !
2026-10-14 13:45:53 +03:00
Lauri Ojansivu 7dd484f8c2 Board search steps back too: Shift+Enter and the arrows beside the count.
The decision for #2171 was next AND previous; only next was there. Now
Shift+Enter in the search box steps to the previous card found, the arrows
beside "2 of 7" step either way for a mouse or a touch screen, and both
wrap round - back from the first is the last.

Thanks to rlach and xet7 !
2026-10-14 13:45:49 +03:00
Lauri Ojansivu cba618e2f9 Sort a list by the board's label order.
A board's labels have an order now - a board admin moves a label up or down
when editing it (the same write, and permission, as editing a label) - and
Sort cards has "Label order": each card comes where its first label comes
in that order, cards without a label last, and cards that rank the same
keep the order they were dragged into. Like "Sort by votes" it is a display
order only: no card's manual position changes, so turning it off gives the
dragged order back.

The new string is translated in every locale.

Thanks to mnantel and xet7 !

Fixes #2040.
2026-10-14 13:44:33 +03:00
Lauri Ojansivu c239177cd8 Board search shows each card found on the board, and Enter steps on.
A board search found cards and listed them in the sidebar, but finding the
card on the board was still the reader's job; a browser's own find did not
help either, as the issue shows. Now a result opened from the sidebar is
also revealed on the board - scrolled into the middle of the view and
highlighted for a moment - and pressing Enter again in the search box steps
to the next card found, wrapping round, with "2 of 7" under the box, the
way finding on a page works. A list found is revealed the same way. The
highlight does not pulse for people who ask for reduced motion.

The new string is translated in every locale.

Thanks to rlach and xet7 !

Fixes #2171.
2026-10-14 13:43:08 +03:00
Lauri Ojansivu c56c03d202 Minicard avatars in the order people were added, each with its own colour.
The minicard floated every avatar to the end of its row, which drew them
in reverse: the person added first, whom a team treats as the responsible
one, came last, while the opened card showed them first. The avatars are a
wrapping row aligned to the end now, in the order they were added - still
wrapping right-aligned as #5081 asked. Each person's initials avatar has a
colour of their own, the same everywhere and every time
(models/lib/avatarColor.js), every colour keeping the initials readable at
WCAG AA, and the avatar's tooltip names the person.

Thanks to HolgerGH and xet7 !

Fixes #2110.
2026-10-14 13:40:15 +03:00
Lauri Ojansivu aa48a2b71d Board Settings / Bots: hand cards to external runners, as kanbots does.
Board Settings -> Bots, below Workflow view, is the Rules page's view for
bots: runners on your own server - an AI agent, a CI job, a script - that
the board hands cards to. It is laid out like kanbots in the board's own
theme, Font Awesome and drag and drop: a rail of bots, live runs and
activity; the board's lists as columns, each card with its run's state
(Queued, Running, Waiting for you, Ready to review, Done, Failed, Stopped),
the step it is on and the answers to a question it asked; and a drawer with
the card's thread, Hand to bot, Stop and a reply box. Board admins only,
like the rest of the Rules page, and never a board or public view.

A bot has no rights of its own, like Rules and workflows. WeKan only sends
to it: a bot is an outgoing webhook marked as a bot, subscribed to no board
activity and no reminders, and a dispatch goes through the ordinary webhook
queue - the SSRF guard, its signing token, retries, Sandstorm's Powerbox.
The runner works on the board through WeKan's REST API with its own user's
token and board role, and reports its run at one REST route that needs
write access to the board. Handing over is a board admin's choice, in the
view or with the new Rule action "Hand the card to bot"; an answer or a
reply is the admin's own comment, under their right to comment.

The GitHub- and Gitea-compatible APIs' routes are regular expression
literals now, and an instance board is Trello's "org" level.

The new strings are translated in every locale.

Thanks to xet7 !
2026-10-14 13:39:18 +03:00
github-actions[bot] 39c3e64563 Bump versions for v12.42 2026-10-14 10:16:44 +00:00
Lauri Ojansivu 84e402638c Prepare v12.42 release 2026-10-14 13:15:39 +03:00
Lauri Ojansivu 09ecd5f66a Record the optional APIs and the board key backfill fix in Upcoming.
#4930 leaves TODO Later.

Thanks to Raymond-Voogt and xet7 !
2026-10-14 13:14:21 +03:00
Lauri Ojansivu d0378f33c2 Give old boards their keys in one pass, not one query per board.
v12.41 gave every board without a key one at startup by re-reading every
key of the instance for each board, and counting up from 2 again for every
board that shares a title: quadratic, and on 32,500 boards hours of slow
`{ key: { $exists: true } }` queries. Now the keys in use are read once,
the new keys are planned in memory with a counter per base
(models/lib/boardKey.js planBoardKeys, linear), and written in batches of
1000, each only where the board still has no key, so a key a board admin
set meanwhile is never replaced. A single new board reads only the keys
that could collide with its suggestion (an index range on the new `key`
index), not all of them. Planning 32,500 boards takes well under a second;
600 boards are written in under one on a running server.

Thanks to Raymond-Voogt and xet7 !

Fixes #6767.
2026-10-14 13:13:55 +03:00
Lauri Ojansivu 86ec72fade Optional APIs: Trello, Jira, GitHub and Gitea compatible, off by default.
Admin Panel / Settings / Visibility / Optional APIs ticks REST APIs in the
shape of Trello's, Jira's, GitHub's and Gitea's, so apps and scripts written
for them - Trellinator, Jira and GitHub clients - can use WeKan as their
backend, each described on the page with the address a client uses.

None has rights of its own. Each is a translator: a request in the other
tracker's shape becomes the matching request to WeKan's own REST API, made
with the caller's own WeKan token on the loopback address, and the answer
becomes the other tracker's JSON. Every WeKan permission therefore applies
unchanged - read-only and comment-only roles, assigned-only members, list
permissions - and an API that is not ticked answers 404. All of them sit
under /api, so WITH_API turns them off with the rest of the REST API.

Trello: members, boards, lists, cards, labels, comments and checklists; a
deleted card is archived. Jira: projects are boards (the board key), issues
are cards (DEV-7), statuses are lists and transitions move cards, with
simple JQL. GitHub and Gitea: repositories are boards and issues are cards,
open or closed as in Board View / Version Control, with labels, milestones,
comments and Link pagination.

The new strings are translated in every locale. A runtime test over real
HTTP shows a read-only member refused and a Trello-created card readable
through Jira and GitHub.

Thanks to xet7 !

Fixes #4930.
2026-10-14 13:11:23 +03:00
Lauri Ojansivu 0b22fb6b69 Record the GitHub organization import and the Rules changes in Upcoming.
Thanks to xet7 !
2026-10-14 12:44:00 +03:00
Lauri Ojansivu 5eb3d45655 Merge remote-tracking branch 'origin/main' 2026-10-14 12:43:43 +03:00
Lauri Ojansivu 935e68dfdd GitHub organization import: repositories, issues, releases, Actions, wiki.
Import -> GitHub -> Import a whole GitHub organization makes every
repository of an organization or user its own board, opening in Board View
/ Version Control. Issues go through the ordinary importBoard, as the person
who started the job, with their comments, labels and files. Releases are
cards in a Releases list, their files attached only when chosen. Each
.github/workflows file becomes Rules. The wiki is read from its git
repository with isomorphic-git (1.42.4, pinned), every request through the
SSRF guard and only to github.com: a page is a card in the Wiki list, its
[[links]] point at and are attached to the linked pages' cards, and each
commit that changed it is a dated edit in the card's history, so Restore
brings back an older version. The job shows progress, can be cancelled,
pauses on a restart and stops at a rate limit; the token is kept in memory
only.

Version Control gains Releases and Wiki tabs: the wiki as GitHub shows it,
with the page list, _Sidebar and _Footer, links that stay in the tab, and
each page's history with Restore.

GitHub Actions workflows map to Rules: issue events to triggers, label,
close, reopen, comment, assign and create steps to actions, actions/stale to
an aging rule. Code - builds, tests, scripts - is never run: such a workflow
is a disabled rule with its YAML as the description. WeKan's Rules gain what
they lacked for it: a "commented on" trigger, an "add comment" action written
as the person whose change ran the rule and only where they may comment,
cron schedules in UTC, and a rule description shown on the rule. A disabled
rule no longer fires on a schedule or from its button, which it did.

The new strings are translated in every locale.

Thanks to xet7 !
2026-10-14 12:43:36 +03:00
Lauri Ojansivu 842a5a8781 Record Version Control and the Android bundles in a new Upcoming.
v12.41 was already released when the Android bundles were committed, so
their entry was in the wrong section: it moves to a new Upcoming, and
v12.41's In short says again what was released. Upcoming records the
Version Control view, issue files, synced issue details, the public views
of a public board and the card QR code. #2015 leaves TODO Later.

Thanks to xet7 !
2026-10-14 12:10:21 +03:00
Lauri Ojansivu 326d9bbf61 Board View / Version Control, issue files and synced issue details.
Version Control shows the board as a GitHub or Gitea issue tracker in the
board's own theme: cards are issues (open, or closed when archived, in a
Closed or Done list, or closed upstream), the board's labels are labels,
labels with a due date and the importers' milestone: labels are
milestones, comments are the conversation and attachments are the files.
The issue list takes GitHub's search words (is:open, label:, milestone:,
author:, assignee:@me, no:label, sort:). New issue, Close and Reopen write
through the ordinary collections, so the History tab - the board's
history table - shows them and Restore undoes them. Import links open the
GitHub, GitLab, Gitea and Forgejo importers, and each synced list has a
Sync now button.

The GitHub, GitLab, Gitea and Forgejo importers bring each issue's files:
user-attachments and user-images links, GitLab uploads, and Gitea and
Forgejo assets, downloaded through the import's SSRF guard and stored as
card attachments in Admin Panel / Attachments / Default Storage.

List sync has a new option, Comments, labels, state and files, off by
default: comments come from one paged repository-wide list (GitHub,
Gitea) or each issue's notes (GitLab), keyed by the tracker's ids, so a
second run writes only what changed, and nothing written in WeKan is
changed. Gitea issues are fetched without pull requests, and a rate
limited answer is waited out once or ends the run with the time to retry.

A public board's Show on / Default on Public Board settings are for the
public: visitors and non-members get them, read-only, and the board's own
members get the private side, so any view - Version Control, Email, Chat
and the rest - can be the read-only public view.

The card menu has a QR code of the card's link, drawn in the browser by
WeKan's own encoder, which the tests check module for module against
qrcode-svg.

The new strings are translated in every locale.

Thanks to xet7 !

Fixes #2015.
2026-10-14 12:09:34 +03:00
Lauri Ojansivu b6bdae765d Record the maintainer's answers to every open design question in TODO Later.
TODO Later's design questions block is now the decisions, each to be built
as decided: organization and team tabs, team-board hiding, read-only Rules
behind an Admin Panel option, log out everywhere and passkeys, linked lists,
search scrolling, avatars, label ordering, QR codes, subtasks across boards,
checklist item owners, Table view editing, column sets, Yjs co-editing,
drawing (PNG and SVG, editable), board grouping, card types, unread marks
for every card field, push notifications and an activity feed, Mattermost,
Matrix, ntfy and ActivityPub, a Trellinator-compatible API, an Android share
target, Nextcloud, FreeBSD rc.d, LibreJS and internal webhook destinations.
Gogs beside WeKan is closed as WONTFIX.

Thanks to xet7 !

Closes #6554.
2026-10-14 11:30:09 +03:00
github-actions[bot] 5588931bf2 Bump versions for v12.41 2026-10-14 08:08:00 +00:00
Lauri Ojansivu ae0d3170ec Record the Android bundles; TODO Later's Planned block is done.
Everything requested on 2026-10-13 is now built. What stays in TODO Later is
the one thing that cannot work, Jira list sync from a Sandstorm grain, so it
is not asked again.

Thanks to xet7 !
2026-10-14 11:08:00 +03:00
Lauri Ojansivu b8d9cb2565 Android bundles for Termux, for all four Android CPUs.
Each release's Linux bundle of the matching CPU is rebuilt inside Termux's
own image, on a runner of that CPU, with wekan/node-patches'
node-android-<cpu> of the same version, checked against its SHA256: npm
reinstalls the server packages run by that Node.js, so every native addon is
built for it, and sharp from source against Termux's libvips. Each addon's
architecture is checked, sharp must load, and the bundle must start and
answer HTTP 200 before it is published. A CPU whose Android Node.js is not
published yet is skipped and builds by itself once it is; the single-file
workflow, which already makes Android single files from these bundles, now
waits for them.

Thanks to xet7 !
2026-10-14 11:07:36 +03:00
Lauri Ojansivu 9993d8e0db Prepare v12.41 release 2026-10-14 11:06:54 +03:00
Lauri Ojansivu bc79748232 Record the CodeQL #583 fix in Upcoming.
Thanks to GitHub CodeQL and xet7 !
2026-10-14 11:05:20 +03:00
Lauri Ojansivu 73fcfb8545 Answer a test's HTTPS request without echoing it back.
CodeQL alert #583 (js/reflected-xss): tests/httpsServer.test.cjs's server
answered `served ${req.url} ...`, sending the request back as the body. It
now names the page by comparison, as plain text.
tests/noReflectedRequest.test.cjs keeps the shape out of every file: no
res.end/res.write/res.send writes a request's url, query, params, headers or
body into the response. Test code only, so nothing for Admin Panel / Problems
to record.

Thanks to GitHub CodeQL and xet7 !
2026-10-14 11:05:15 +03:00
Lauri Ojansivu b8a6fc371b Record the #6766 fix in Upcoming.
Thanks to Raymond-Voogt and xet7 !
2026-10-14 11:04:06 +03:00
Lauri Ojansivu ebf2a019a6 "Lists missing their swimlane" no longer counts board-wide lists, and Restore never hides cards.
The check counted every board-wide list whose position history recorded
the swimlane it was created in, so lists made since the automatic
un-binding stopped (#6515, released in v10.31) were counted too, and the
count kept growing. Restore would have bound them back to that swimlane,
hiding the cards they hold in other swimlanes - 490 cards in 14 lists on
the reporter's instance.

Now a list created after v10.31's release is never offered, being
board-wide by design, and a list with any card (archived ones too) in a
swimlane other than the one it would be bound to is never restored. A list
that really lost its binding, with cards only in its own swimlane, is still
restored. tests/listSwimlaneRestore.test.cjs and a runtime test on a real
database cover both rules and that case.

Thanks to Raymond-Voogt and xet7 !

Fixes #6766.
2026-10-14 11:03:53 +03:00
Lauri Ojansivu 5466872bf1 Accept the musl AppImage's bundle check in the node-runtime guard.
AppImage.yml wraps the musl bundles too, whose Node.js needs no glibc
libraries; tests/nodeRuntimeDependency.test.cjs still requires
node-runtime/libatomic.so.1 for every glibc bundle.

Thanks to xet7 !
2026-10-14 10:56:56 +03:00
Lauri Ojansivu f4d2b3a7ef Record musl bundles, Sandstorm list sync and two locale fixes; Planned is down to Android.
Upcoming gains the musl bundles and AppImages, list sync through the
Sandstorm Powerbox, and Hong Kong Chinese and Flemish in their own
languages. TODO Later's Planned block now holds only the Android bundles,
which wait for wekan/node-patches, and records that Jira list sync cannot
work from a grain, so it is not asked again.

The Sandstorm REST API works with a grain API key and is documented in
docs/Platforms/FOSS/Container/Sandstorm/Powerbox-External-APIs.md since
commit 2cc0b59a51.

Thanks to xet7 !

Fixes #2167.
2026-10-14 10:45:00 +03:00
Lauri Ojansivu f3c92273ae Hong Kong Chinese in Traditional characters, and Flemish in Dutch.
zh-HK had 90 values in Simplified Chinese, found with OpenCC; they are now
Traditional with Hong Kong usage. vl-SS (Flemish) held 158 values in about a
dozen other languages - Venda, Vietnamese, Venetian, Walloon, Papiamento and
more - now in Flemish, and its 225 values still in English are filled.
Values a test pins or an audit corrected are kept, and
tests/wrongLanguageLocales.test.cjs now covers both files.

Thanks to xet7 !
2026-10-14 10:43:39 +03:00
Lauri Ojansivu a1d2994c1c musl bundles and AppImages for Alpine and the other musl systems.
Each release's Linux bundle of x64 and arm64 is rebuilt inside Alpine on a
runner of that CPU: the musl Node.js of exactly the bundle's version,
checked against its SHA256, replaces the glibc one and its node-runtime
libraries; npm reinstalls the server packages so every native addon is
compiled against musl and sharp's musl package is the one installed; then
every addon's architecture is checked, sharp must load, and the bundle must
start and answer HTTP 200 before it is published. FerretDB and the MongoDB
Database Tools are static and stay. The AppImage workflow wraps them too, as
x86_64-musl and aarch64-musl, and starts each inside Alpine to check it.

Thanks to xet7 !
2026-10-14 10:38:39 +03:00
Lauri Ojansivu d53b4da799 On Sandstorm, sync lists from GitHub, GitLab, Gitea and Forgejo through the Powerbox.
A grain's only way out is the bridge proxy, so list sync never reached a
tracker. A board member who may write saves the list's source and allows
the grain to reach that tracker's API root in the Powerbox; the grant
belongs to the tracker, found by the URL being fetched, so the periodic job
needs no signed-in user. GitLab's token goes as private_token, since the
bridge drops PRIVATE-TOKEN; GitHub, Gitea and Forgejo read public
repositories, since their token headers cannot pass; Jira, which signs in
only with an Authorization header, is refused with that reason. Off
Sandstorm list sync still goes through the SSRF guard, unchanged.

New strings translated in all languages.

Thanks to xet7 !
2026-10-14 10:35:13 +03:00
Lauri Ojansivu f22af91967 Count the rewritten locale files correctly in In short.
Fifteen held another language and six machine-fill prefixes.

Thanks to xet7 !
2026-10-14 10:29:38 +03:00
Lauri Ojansivu 2679f77f86 Record Bun single files, Sandstorm webhooks and API keys, and the locale rewrites.
Upcoming gains them; TODO Later's Planned block now holds only what is
left - Android bundles, Sandstorm sync and import (whose credentials the
bridge proxy cannot carry) and Node.js musl bundles, with the questions
that decide them - and the translation block names the languages that need
a native speaker.

Thanks to xet7 !
2026-10-14 10:29:25 +03:00
Lauri Ojansivu ba6d0014f7 Rewrite locale files that held another language in their own.
The locale tag decides the language. Found on 2026-10-14 and rewritten:
ro and ro-RO (Italian, French and Spanish blocks), rup, lld, rm and ve-CC
(Italian; ve-CC also Zulu and Venda), gl and gl-ES (Portuguese), ace
(Malay/Indonesian), ve (Zulu), sk (Czech and Polish), hsb (Czech), vo
(Esperanto), bi (Tok Pisin), uz-AR (half Latin script, Uyghur and Kazakh
forms; now Uzbek in Arabic script). Machine-fill prefixes and notes are
gone from to ("Faka-Tonga:"), qu ("Kay willaymi:"), wal ("Wolayttatto:"),
hsb ("WeKan:"), an, fur and lg, each value now in its own language. cmn
and yue_CN no longer call a board a wooden plank; List actions means the
actions of a list in pa, pt, pt-BR, ru_RU, vi-VN, hu and ko; "No" is the
word in pa, lt and te-IN; et-EE's board is a tahvel.

Values a person or an earlier audit had already reviewed are kept: every
value a regression test pins, every audited correction, and every label
another string quotes. Poker cards show their numbers everywhere.
tests/wrongLanguageLocales.test.cjs keeps the old languages and prefixes
out and checks every placeholder against English; two older tests that
expected Venda in the Venetian file, and a prefix in Quechua, now expect
the language of the tag.

Low confidence, for a native speaker: to, qu, wal, lg, bi, vo, lld, rup,
ve, ace, uz-AR (its use of ې is for a speaker to settle), hsb and rm.

Thanks to xet7 !
2026-10-14 10:28:43 +03:00
Lauri Ojansivu 35e216b846 Review the list-permission open-board check; no example URL in egress.mjs.
tests/boardPermission.test.cjs lists every public-board check in the server
and models, so none silently leaves instance boards out: #4527's "everyone
signed in" is one, and it covers public AND instance boards. The Powerbox
webhook comment in imports/sandstorm/egress.mjs gave an example address,
which the release risk audit reports as an outbound URL; it now describes
the shape without one.

Thanks to xet7 !
2026-10-14 09:51:41 +03:00
Lauri Ojansivu 2cc0b59a51 On Sandstorm, send webhooks through the Powerbox, and accept the REST API with a grain API key.
A grain has no network but the bridge proxy, so an outgoing webhook now
leaves only through an API a board admin approved in the Powerbox for that
webhook's address: each saved webhook offers "Allow this grain to reach this
address". The grant belongs to the destination, not to a person, is found
by the URL being sent to, and is approved as the address's parent path so
the bridge delivers to the exact address. Every sender - immediate, grouped
and stored - goes through webhookFetch, which off Sandstorm, or for an
address no grant covers, is exactly the SSRF-guarded fetchSafe.

The REST API called with a Sandstorm API key acts as the key's user: in a
grain the bridge sets X-Sandstorm-User-Id itself. Only in a grain, only
after a WeKan token, only for an existing enabled account, and a key without
participate reads only.

New strings translated in all languages.

Thanks to xet7 !
2026-10-14 09:49:42 +03:00
Lauri Ojansivu b1e58094f4 Bun single files: one executable per Bun platform, musl included.
Each is the platform's published Bun bundle ZIP, unchanged, inside that
bundle's own Bun: built on the platform itself (Alpine for musl), so the
runtime is exactly the one the bundle was tested with. The first start
extracts the bundle once into a per-version cache directory, without a
second copy of Bun, and every start runs index.mjs with the executable
itself as Bun. Like the Bun bundle it needs MONGO_URL and ROOT_URL.

The launcher reads the ZIP itself (stored, deflated, ZIP64, Unix modes,
symbolic links) and refuses any entry or link that would land outside the
cache directory. Before publishing, each file is checked: it names itself,
refuses to start without a database, extracts once and reuses that, and
loads every native addon of the bundle. Linux x64/arm64 (glibc and musl),
macOS and Windows; FreeBSD and Android have no runner to build on.

Thanks to xet7 !
2026-10-14 09:48:50 +03:00
Lauri Ojansivu f14274e3ca Record board editing for everyone, list permissions, board keys, shared labels and HTTPS.
Upcoming gains the features, the list archive fix, the CodeQL fix and the
Node-RED and Uberspace docs. TODO Later's decided block is done and removed;
the open requests are rewritten as the design questions that decide them,
and the locale files found in another language are listed.

Thanks to xet7 !
2026-10-14 09:28:57 +03:00
Lauri Ojansivu a0de507143 Document webhooks to Node-RED and WeKan on Uberspace 7.
Node-RED: the http in flow, and that an internal address is refused by the
SSRF guard, so Node-RED needs a public name. Uberspace 7: the UberLab guide,
the update script, and email settings on the one environment line.

Thanks to yeurplc, jeyca and xet7 !

Fixes #2017,
Fixes #2009.
2026-10-14 09:28:45 +03:00
Lauri Ojansivu 167ccf9e2d Turn an email's HTML into text in one pass, with no pattern to take apart.
CodeQL alerts #581 and #582: htmlToText still removed script elements and
tags with regular expressions, looped through a helper CodeQL does not
follow. A one-pass scanner drops tags whole, script/style/head elements with
their content, and unfinished tags. The tree-wide guard no longer accepts a
fixed-point helper and also catches a whole element removed by pattern.

Thanks to GitHub CodeQL and xet7 !
2026-10-14 09:28:45 +03:00
Lauri Ojansivu 0a3797dbf6 Archiving a list archives its cards, and restoring it brings them back.
Only template lists archived their cards, so a normal list's cards stayed
visible in the Lists, Table and Calendar views. A server hook on the list's
archived field now archives the cards of every list, whichever way it is
archived, and restore brings back exactly those - the rule #2292 gave
swimlanes. Cards archived with the list get the list's archivedAt, so a
browser clock that differs from the server's cannot keep them archived.

Thanks to Raymond-Voogt and xet7 !

Fixes #6765.
2026-10-14 09:28:45 +03:00
Lauri Ojansivu 7595727917 Boards every signed-in user may edit, per-list permissions, board keys and shared labels.
A board can let every signed-in user edit it, when Admin Panel / Settings /
Visibility allows it: a non-member then comments, edits and moves cards like
a Normal member, and their first edit makes them one. Board-wide settings
stay members-only.

Lists can allow or refuse editing and commenting per role (list menu, board
admins): built-in roles, custom roles and, on an open board, everyone signed
in. The server applies it to card, checklist, attachment and comment writes
and to moves into the list; the REST API honours a list's refusal on every
per-list card route. A direct client write of the setting is refused and
shown in Admin Panel / Problems.

Every board has a key, unique in the instance, suggested from its title and
set for existing boards at startup; card numbers show as KEY-7 and searching
for KEY-7 finds the card.

Shared labels in Admin Panel / Labels, for every board or an organization's
or team's boards; a board uses them beside its own, and a rename or colour
change reaches every board that uses it.

New strings translated in all languages. Low confidence: chr, tlh, wal, ay,
ace, nah, gn, iu, zgh, rup, wo, se, ff, tig, vo, bm, kl, bua, nap, mai, to,
ks, qu, bo, fj, haw, om, ss, ts, ve-PP, lld, scn.

Thanks to Meeques, justinr1234, tiFabGrinch, ejlkemper and xet7 !

Fixes #4575,
Fixes #4527,
Fixes #2449,
Fixes #2450,
Fixes #2761,
Fixes #4698.
2026-10-14 09:28:45 +03:00
Lauri Ojansivu 1bf7a62cab Serve HTTPS from WeKan itself, when a certificate is configured.
#916 (decided 2026-10-14): HTTPS_PORT with HTTPS_CERT_FILE and
HTTPS_KEY_FILE (a path or the PEM itself) makes WeKan serve HTTPS; without
them nothing changes, and a proxy stays the recommendation for
production. Meteor's HTTP server - pages, DDP, websockets - is not
replaced: a TLS listener hands each decrypted connection to it, so
everything works the same over HTTPS. A renewed certificate is loaded
within a minute without a restart (a broken one keeps the old).
HTTPS_REDIRECT=true redirects plain HTTP to ROOT_URL when that is https,
else the same host on the HTTPS port, keeping only the path. The snap
gets https-port, https-cert-file, https-key-file, https-bind-ip and
https-redirect.

tests/httpsServer.test.cjs makes a real certificate with openssl, serves
a page and a websocket upgrade through the bridge, swaps the certificate
without a restart, and refuses incomplete settings, an untrusted
certificate, an injected host and a scheme-relative redirect.

Thanks to 68040 and xet7 !

Fixes #916.
2026-10-14 08:25:25 +03:00
Lauri Ojansivu 1becf7a9c1 Record custom roles, group memberships, the webhook proxy and the role helper fixes.
Upcoming gets the three features and the bug fixes; TODO Later's
decided-and-waiting list drops #3022, #4178 and #1455, which are done.

Thanks to xet7 !
2026-10-14 08:22:52 +03:00
Lauri Ojansivu 548834c4ee Fix five role helpers that answered the same for everybody.
Found while mapping every role check for custom board roles:
- canImportIntoBoard tested the role helpers as references
  (`!user.isWorker`, always false), so Import was offered to nobody; it
  asks the write capability now, as the server does.
- the rules list's canAdmin used `user.isBoardAdmin` without calling it,
  so every member saw the rule controls.
- the role popup's isLastAdmin compared activeAdmins() - a list - with 1,
  so the last admin was never stopped from giving the role away.
- the members widget's isWorker called Boards.hasWorker, which is not a
  collection function.
- OIDC's DEFAULT_BOARD_ID used Array#contains, which does not exist, so
  the configured role flags threw; isCommentOnly works next to the
  documented isCommentsOnly spelling.

tests/roleHelperRegressions.test.cjs pins each, and fails for any role
helper tested as a bare reference anywhere in client/, models/ and
packages/, and for a list compared with a number.

Thanks to xet7 !
2026-10-14 08:22:12 +03:00
Lauri Ojansivu 50bb989eb8 Custom board roles, and team memberships that leave with the team.
#3022 (decided 2026-10-14): a site admin builds named roles in Admin
Panel / People / Roles from the capability matrix - sees all cards,
comments, edits, moves cards, invites - and a board admin gives one to a
member in the same popup as the built-in roles. Board admin stays
built-in. Every permission check asks boardRoleCapabilities, which
answers a custom role from the role; the member's built-in flags carry
the strongest built-in role that grants nothing more (fallbackRoleFor,
checked for every combination), so code reading a flag never grants
more, and a deleted role leaves exactly that. Where a role is asked by
name (hasWorker, isCommentOnly, ...) a custom role reads as the built-in
role that does what it does; commenting is asked as a capability
everywhere. Invites, REST whole-board reads, imports (a foreign custom
role is dropped) and member mapping (all eight flags now, not four)
follow it. The six new strings are translated in all 234 locales.

#4178 (decided 2026-10-14): a team or organization that gives board
memberships takes them away again when the user leaves it - only those:
each records where it came from (grantedBy), a direct membership is
never touched, a board admin is kept, and a board admin's own add or
removal is final. Teams through user edits, LDAP, OIDC and the board
sidebar; organizations where they propagate members.

Also: the Roles Status table gets a Move cards column; the comment form,
reactions and chat ask the comment capability (a No comments member was
offered reactions the server refused).

Tests: customBoardRoles and groupBoardMembership node suites, and the
runtime suites (9 passing) against a real database.

Thanks to xet7 and mfnalex !

Fixes #3022,
Fixes #4178.
2026-10-14 08:20:49 +03:00
Lauri Ojansivu 78957e14cb Send outgoing webhooks through an HTTP proxy, with the SSRF checks kept.
HTTP_PROXY, HTTPS_PROXY and NO_PROXY now work for every outgoing request
that goes through the SSRF guard: webhooks, imports, avatar downloads,
list sync. The destination is still resolved once and checked against
the blocked ranges first, and the proxy is handed that checked address,
never the name: an http:// request goes to the proxy in absolute form on
the pinned address with the real Host, an https:// one through a CONNECT
tunnel to the pinned address with TLS still verified against the real
name (and WEBHOOK_TLS_CA_CERT). A name resolving to an internal address
is refused before the proxy is asked anything. Proxy credentials in the
URL become Proxy-Authorization. The snap gets http-proxy, https-proxy and
no-proxy.

tests/outboundProxy.test.cjs runs a real proxy on loopback: the pinned
address, the Host header, the credentials, the CONNECT authority, and
that a refused destination never reaches the proxy.

Thanks to gbarre and xet7 !

Fixes #1455.
2026-10-14 08:20:30 +03:00
Lauri Ojansivu cf41e221ea Record the CodeQL #570-#580 fixes in Upcoming.
Thanks to GitHub CodeQL and xet7 !
2026-10-14 07:35:43 +03:00
Lauri Ojansivu 2352f1f44f Fix GitHub CodeQL alerts #570-#580.
#570, #571 (incomplete multi-character sanitization): email to card
turned an HTML-only email into text with one pass of each removal, so a
script element spliced together by removing an inner one survived and its
contents reached the card ("<scr<script></script>ipt>alert(1)</script>").
Both removals now run to a fixed point.

#574 (regular expression injection): the single file's wekan.env writer
spliced the setting name into a RegExp. A setting must be an environment
variable name, is compared literally, and its value must be one line. The
kept-versions reader built a half-escaped RegExp from the file extension;
it now reads wekan-<version><exe> literally.

#572, #573 (incomplete string escaping): two runtime tests escaped only
some metacharacters; they compare literally now. #575-#580 (URL substring
and hostname patterns): translation tests matched example hostnames with
unescaped dots or substring checks; the dots are escaped and the hosts
bounded.

tests/tagStrippingFixedPoint.test.cjs missed the <[^>]+> spelling; it now
catches both, accepts a fixed-point helper only when it really loops, and
records the search-index text of server/lib/documentGif.js as never
rendered. tests/emailToCard.test.cjs fails on the old htmlToText;
tests/singleFile.test.cjs refuses regex and newline input in setting
names and values, and reads only real version files.

Nothing here is an attempt a user can make at runtime that the server
refuses, so there is no Admin Panel / Problems key to add.

Thanks to GitHub CodeQL and xet7 !
2026-10-14 07:35:20 +03:00
Lauri Ojansivu e8eeaa4956 Record the 2026-10-14 design decisions in TODO Later.
Shared labels (#2761, #4698), board keys for card numbers (#2449,
#2450), membership removal on leaving a team or organization (#4178),
webhooks through an HTTP proxy (#1455), custom roles (#3022), editing by
every signed-in user (#4575), per-list permissions (#4527) and optional
HTTPS in WeKan's own server (#916) are decided; each moves from the open
requests to a list of decisions waiting to be implemented.

Thanks to xet7 !
2026-10-14 07:24:21 +03:00
Lauri Ojansivu f240a3d056 Record the OpenBSD 7.8 bundle build in Upcoming.
Thanks to xet7 !
2026-10-14 07:16:34 +03:00
Lauri Ojansivu 209e3b0c47 Build the OpenBSD bundles on OpenBSD 7.8.
node-patches now builds OpenBSD's Node.js on 7.8, the first release whose
base Clang (19.1.7) builds Node 26 - 7.7's Clang 16 has no
std::atomic_ref. The bundle is built and started where that binary runs.
The BSD page says OpenBSD 7.8 or newer is needed, and that NetBSD's
bundled OpenSSL gets FreeBSD's configuration; tests/bsdBundles.test.cjs
fails for an OpenBSD target older than 7.8.

Thanks to xet7 !
2026-10-14 07:16:24 +03:00
Lauri Ojansivu 52f0bc1271 Document Igbo interrupted import recovery translations
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 07:14:51 +03:00
Lauri Ojansivu 8c25ce78b9 Translate Igbo interrupted import recovery messages
Preserve permanent deletion, retained partial boards, source-file limitations and untouched foreign boards. Add catalog and browser coverage. Recovery prose needs fluent review.

Thanks to xet7 !
2026-10-14 07:14:15 +03:00
Lauri Ojansivu 269a96aa7d Merge remote-tracking branch 'origin/main' 2026-10-14 07:12:30 +03:00
Lauri Ojansivu 6e3678552a Document Igbo stopped sync recovery translations
Record passing translation checks and unfinished fluent and browser verification.

Thanks to xet7 !
2026-10-14 07:11:47 +03:00
Lauri Ojansivu 5c5391fb98 Translate Igbo stopped list sync recovery messages
Preserve applied changes, discarded pending writes, replay restrictions and display limits. Add catalog assertions and a browser case. Recovery prose needs fluent review.

Thanks to xet7 !
2026-10-14 07:10:22 +03:00
Lauri Ojansivu 30e3c65169 Bring TODO Later up to date with the open issues.
Remove what is no longer open: #3318 (closed as not planned), #6692 and
#6509 (closed as completed), and email to card and the Node.js single
files from Planned (shipped in v12.40). The September translation-repair
checkpoints are cut to what is still open.

Every one of the 62 open issues was read with its comments and checked
against the code on 2026-10-14. None other than #2141 is fully done; each
is now listed under its topic with what exists and what is missing.
Record the #2141 closure in Upcoming.

Thanks to xet7 !
2026-10-14 07:07:52 +03:00
Lauri Ojansivu 19aa5fd7dd Document Igbo workflow and permission translations
Record passing translation checks and remaining language and browser verification. Keep concurrent TODO Later edits separate.

Thanks to xet7 !
2026-10-14 07:07:29 +03:00
Lauri Ojansivu 3f88fb39a5 Pin the per-member notification frequency #2141 asked for.
The issue asked for Trello's Send Emails choice per member: Never,
Periodically (about once an hour) or Instantly. All three already exist:
email off is the member's Notification Settings override (bde66a5c7d,
2026-09-10), and the member's delivery schedule is immediate, every 5
minutes to 24 hours, or daily, with quiet hours (ab785acd1e, 2026-10-10).
A member's choice wins over the board's and the Admin Panel's.

tests/notificationFrequency2141.test.cjs pins each option at member
level, that an unknown member value is ignored rather than trusted, and
that the switch and the schedule picker are in the member's settings.

Thanks to saurabharch and xet7 !

Fixes #2141.
2026-10-14 07:06:15 +03:00
Lauri Ojansivu daac3ead0d Translate Igbo workflow and permission controls
Preserve Wrike status behavior, directory-sync identifiers and subtask permission distinctions. Add catalog and browser coverage. Technical prose is lower confidence and needs fluent review.

Thanks to xet7 !
2026-10-14 07:04:56 +03:00
Lauri Ojansivu cb7aa2bf6b Add required Upcoming summary and translation heading
Thanks to xet7 !
2026-10-14 06:59:04 +03:00
Lauri Ojansivu cfde3385b8 Document Igbo notification translations in Upcoming
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:58:05 +03:00
Lauri Ojansivu 238354f6a5 Translate Igbo notification delivery and integration settings
Preserve grouping, scheduling, inherited webhook settings, identity omission and literal link syntax. Add catalog assertions and browser coverage. Technical wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:57:02 +03:00
github-actions[bot] 265238550f Bump versions for v12.40 2026-10-14 03:56:18 +00:00
Lauri Ojansivu addf08f52c Prepare v12.40 release 2026-10-14 06:54:55 +03:00
Lauri Ojansivu daf8549cdd Document Igbo transfer-help translations
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:54:31 +03:00
Lauri Ojansivu 93ef17b326 Translate Igbo Trello and Sandstorm transfer help
Preserve literal swimlane labels and distinguish access requirements, partial export warnings and selected-board exports. Add catalog and browser checks. Technical wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:54:00 +03:00
Lauri Ojansivu 7306d06919 Document Igbo import mapping and bulk transfer translations
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:51:09 +03:00
Lauri Ojansivu 1cea7b89f1 Translate Igbo import mapping and bulk transfer controls
Preserve user mapping choices, missing-column handling and single-board versus multi-board ZIP behavior. Add catalog and browser coverage. Technical prose needs fluent review.

Thanks to xet7 !
2026-10-14 06:50:36 +03:00
Lauri Ojansivu 8bc9d15be1 Document second Igbo import-guide translation batch
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:47:33 +03:00
Lauri Ojansivu 7e8ebcd458 Translate twelve more Igbo import guides
Preserve exclusions, English-header requirements, subtask syntax and fallback mapping. Extend catalog and browser coverage. Technical prose is lower confidence and needs fluent review.

Thanks to xet7 !
2026-10-14 06:46:50 +03:00
Lauri Ojansivu 93ded63395 Document first Igbo import-guide translation batch
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:43:23 +03:00
Lauri Ojansivu 29282c6ddf Translate ten Igbo import guides with format limits
Preserve mappings, first-board limits, completed items and literal Org mode identifiers. Add catalog assertions and browser cases. Longer technical prose is lower confidence and needs fluent review.

Thanks to xet7 !
2026-10-14 06:42:44 +03:00
Lauri Ojansivu e1b92a011e Document Igbo linked-field translation progress
Record passing checks, preserved Chinese values and remaining placeholders and review.

Thanks to xet7 !
2026-10-14 06:39:42 +03:00
Lauri Ojansivu 77218ed315 Translate Igbo linked fields and board controls
Distinguish link directions and permission limits, translate announcement controls, and repair the custom-fields label. Add regression assertions and browser coverage. Direct technical wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:39:13 +03:00
Lauri Ojansivu 5416b7f672 Document Igbo attachments and refresh translation backlog counts
Record passing checks and distinguish the filtered inventory from pending keys and semantic review.

Thanks to xet7 !
2026-10-14 06:36:42 +03:00
Lauri Ojansivu 4e80dcf3ba Translate Igbo card attachment controls and storage identifier
Add positive and negative regression coverage for attachment visibility, limits, self-links and detachment. Direct technical wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:35:51 +03:00
Lauri Ojansivu 9fb2559fd8 Name the regression tests of the BSD and FreeBSD Bun entries in Upcoming.
Thanks to xet7 !
2026-10-14 06:33:48 +03:00
Lauri Ojansivu f6d7aafc5f Document Scottish Gaelic import and tooltip corrections
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:32:07 +03:00
Lauri Ojansivu 399e9a19d7 Restore Scottish Gaelic import instructions and header help
Preserve Jira pagination, required fields and automation rules, Excel mapping and per-sheet boards, and notification toggle behavior. Add catalog and browser assertions. Direct technical wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:31:30 +03:00
Lauri Ojansivu 2834c752f4 Document Scottish Gaelic report control corrections
Record passing checks and outstanding fluent and browser verification.

Thanks to xet7 !
2026-10-14 06:29:11 +03:00
Lauri Ojansivu 999922d6a6 Updated translations. 2026-10-14 06:28:02 +03:00
Lauri Ojansivu 2dd80befcf Document Scottish Gaelic report translation review
Record passing checks and outstanding statistical terminology and browser verification.

Thanks to xet7 !
2026-10-14 06:26:34 +03:00
Lauri Ojansivu 8b46431477 Restore Scottish Gaelic report methodology and limits
Replace shorthand report notes with translated assumptions, minimum samples, missing-history handling and fallback dates. Clarify headings and error guidance. Statistical terminology is lower confidence and requires fluent review.

Thanks to xet7 !
2026-10-14 06:26:02 +03:00
Lauri Ojansivu 7b3b9f00e1 Document Scottish Gaelic date and linking corrections
Record verified checks, unchanged Chinese values and outstanding language and browser review.

Thanks to xet7 !
2026-10-14 06:23:20 +03:00
Lauri Ojansivu 19f5807e04 Clarify Scottish Gaelic date triggers and card linking
Restore initial date setting, Gregorian ISO-week details and destination versus card-derived board actions. Add positive and negative catalog assertions and browser coverage. Direct wording needs fluent review.

Thanks to xet7 !
2026-10-14 06:22:56 +03:00
Lauri Ojansivu add0493214 Document Scottish Gaelic meaning corrections and review limits
Record passing translation and changelog checks, preserved Chinese values, and pending browser and fluent verification.

Thanks to xet7 !
2026-10-14 06:20:44 +03:00
Lauri Ojansivu 1fda1c4dd0 Correct Scottish Gaelic action meanings and stale import guides
Translate Todoist and Planner instructions and distinguish recurring cards from resets and move reasons from descriptions. Add catalog and browser regression coverage. Direct technical translations still need fluent review.

Thanks to xet7 !
2026-10-14 06:19:59 +03:00
Lauri Ojansivu faed3b3048 Document Scottish Gaelic recovery translations and full placeholder check
Record passing checks and unfinished semantic and browser verification.

Thanks to xet7 !
2026-10-14 06:17:18 +03:00
Lauri Ojansivu 53be36536d Translate Scottish Gaelic recovery messages and close placeholder gap
Preserve retained sync changes, permanent partial-board deletion and server-only settings. Add full placeholder and browser checks; technical prose needs fluent review.

Thanks to xet7 !
2026-10-14 06:14:42 +03:00
Lauri Ojansivu 80bac12b8f Document Scottish Gaelic workflow translation progress
Record validation and unfinished fluent and browser review.

Thanks to xet7 !
2026-10-14 06:12:00 +03:00
Lauri Ojansivu 73bdd4061b Translate Scottish Gaelic workflow and permission messages
Retain status transitions, environment names and authorization restrictions. Technical prose needs fluent review; extend regression coverage.

Thanks to xet7 !
2026-10-14 06:11:29 +03:00
Lauri Ojansivu 154a3d6c71 Document Scottish Gaelic integration translation progress
Record passing checks and unfinished fluent and runtime review.

Thanks to xet7 !
2026-10-14 06:09:16 +03:00
Lauri Ojansivu 9724c40077 Translate Scottish Gaelic integration and notification controls
Fill 53 strings while preserving placeholders, Trello labels, inherited settings and delivery choices. Technical prose needs fluent review; extend regression coverage.

Thanks to xet7 !
2026-10-14 06:08:44 +03:00
Lauri Ojansivu 6c5d109754 Document Scottish Gaelic import-control translation progress
Record verification and unfinished fluent and browser review.

Thanks to xet7 !
2026-10-14 06:06:13 +03:00
Lauri Ojansivu 6a028b4327 Translate Scottish Gaelic import mapping and bulk export controls
Fill 27 English strings, preserving user choices, ZIP exceptions and workbook layout. Technical prose needs fluent review; extend regressions.

Thanks to xet7 !
2026-10-14 06:05:43 +03:00
Lauri Ojansivu 6543da6538 Document the second Scottish Gaelic import-guide batch
Record checks, lower-confidence prose and remaining verification work.

Thanks to xet7 !
2026-10-14 06:03:21 +03:00
Lauri Ojansivu c5f86df1a3 Translate twelve more Scottish Gaelic import guides
Retain export prerequisites, field mapping and omission warnings. Technical prose is lower confidence and needs fluent review; extend regression coverage.

Thanks to xet7 !
2026-10-14 06:02:41 +03:00
Lauri Ojansivu f69c05e75b Document Scottish Gaelic import-guide translation progress
Record lower-confidence prose, passing checks and unfinished review.

Thanks to xet7 !
2026-10-14 05:59:53 +03:00
Lauri Ojansivu 7f86d9f396 Translate ten Scottish Gaelic import guides
Preserve mapping, completion dates and first-board limitations. Technical prose is lower confidence and needs fluent review; add regression coverage.

Thanks to xet7 !
2026-10-14 05:59:20 +03:00
Lauri Ojansivu 4fc3e3b1e2 Document Scottish Gaelic field-link translation progress
Record checks and remaining fluent and browser verification work.

Thanks to xet7 !
2026-10-14 05:56:45 +03:00
Lauri Ojansivu 4f35ee5751 Translate Scottish Gaelic field links and board announcements
Fill 26 English placeholders with direction, visibility and permission conditions intact. Add regression coverage; fluent review remains needed.

Thanks to xet7 !
2026-10-14 05:56:12 +03:00
Lauri Ojansivu d2766886bf Document Scottish Gaelic attachment translation progress
Record validation, lower-confidence terminology and remaining review work.

Thanks to xet7 !
2026-10-14 05:54:00 +03:00
Lauri Ojansivu 4f5f5d0d33 Translate Scottish Gaelic attachment and legacy storage messages
Fill 41 English placeholders and add attachment and credential regressions. Technical storage wording is lower confidence and needs fluent review.

Thanks to xet7 !
2026-10-14 05:53:28 +03:00
Lauri Ojansivu 1238df0faa Document Irish rule grammar corrections in Upcoming
Record regression coverage and unfinished runtime and semantic verification.

Thanks to xet7 !
2026-10-14 05:50:16 +03:00
Lauri Ojansivu 503d439ddc Correct Irish rule subjects and omit duplicated linking verbs
Use assignee event wording and retain subjects in rule fragments. Omit the extra copula after Irish temporal clauses, with locale-boundary and assembled-rule regressions.

Thanks to xet7 !
2026-10-14 05:49:37 +03:00
Lauri Ojansivu b6d2a1dc5f Document Irish import and header help corrections
Record verification and remaining semantic review work.

Thanks to xet7 !
2026-10-14 05:46:38 +03:00
Lauri Ojansivu 32ae43650c Restore omitted Irish import and header help details
Explain Jira pagination and required fields, Excel sheet mapping and header toggle scope. Extend regression coverage.

Thanks to xet7 !
2026-10-14 05:46:06 +03:00
Lauri Ojansivu fc1f67cdff Document Irish date and card-action translation corrections
Record checks and remaining semantic review work.

Thanks to xet7 !
2026-10-14 05:43:52 +03:00
Lauri Ojansivu 301fd32e09 Correct Irish date triggers and card-link actions
Restore setting-or-changing date triggers, ISO weeks and explicit source/destination actions with regression checks.

Thanks to xet7 !
2026-10-14 05:43:15 +03:00
Lauri Ojansivu 825f93aec6 Document Irish report metric translation corrections
Record regression checks and remaining verification work.

Thanks to xet7 !
2026-10-14 05:40:54 +03:00
Lauri Ojansivu 36579f0e7c Clarify Irish report metrics and numeric summary labels
Replace shorthand with explicit quantities, forecast limits and moving-range descriptions. Cover report controls and numeric sums.

Thanks to xet7 !
2026-10-14 05:40:22 +03:00
Lauri Ojansivu 9af9c31670 Document Irish analytics translation corrections in Upcoming
Record coverage and remaining semantic and runtime verification work.

Thanks to xet7 !
2026-10-14 05:37:54 +03:00
Lauri Ojansivu 7676469d0e Restore complete Irish analytics explanations
Correct report titles and replace shorthand with methodology, uncertainty, history requirements and date fallbacks. Add regression coverage.

Thanks to xet7 !
2026-10-14 05:37:21 +03:00
Lauri Ojansivu b05ed07828 Document Irish semantic translation corrections in Upcoming
Record regression coverage and the limits of placeholder detection.

Thanks to xet7 !
2026-10-14 05:34:41 +03:00
Lauri Ojansivu e4d4b2125c Correct Irish action meanings and stale import guidance
Distinguish recurrence from reset, restore optional move reasons and permanent anonymization wording, and translate stale Todoist and Planner guides.

Thanks to xet7 !
2026-10-14 05:34:08 +03:00
Lauri Ojansivu 081cd3d683 Document Irish recovery translations and full placeholder check
Record passing checks and unfinished semantic and browser runtime verification.

Thanks to xet7 !
2026-10-14 05:31:36 +03:00
Lauri Ojansivu ee99da3ddf Translate Irish recovery messages and close the placeholder gap
Preserve retained sync changes, permanent partial-board deletion and server-only settings. Add full catalog placeholder and browser regression checks.

Thanks to xet7 !
2026-10-14 05:30:56 +03:00
Lauri Ojansivu 5bfa145f29 Document Irish workflow translation progress in Upcoming
Record verification and outstanding all-language work.

Thanks to xet7 !
2026-10-14 05:28:03 +03:00
Lauri Ojansivu 9ccf484c6d Translate Irish workflow and permission messages
Retain Wrike status transitions, LDAP setting names and subtask authorization restrictions. Extend regression coverage.

Thanks to xet7 !
2026-10-14 05:27:33 +03:00
Lauri Ojansivu c8cbdd9f16 Document Irish notification translation progress in Upcoming
Record checks and outstanding translation and browser verification work.

Thanks to xet7 !
2026-10-14 05:25:06 +03:00
Lauri Ojansivu b3a554e748 Translate Irish integration and notification controls
Preserve link-rule syntax, inherited webhook settings, grouping and quiet-hour behavior. Extend translation and browser coverage.

Thanks to xet7 !
2026-10-14 05:24:32 +03:00
Lauri Ojansivu a02c11a116 Document Irish import and export translation progress
Record passing checks and outstanding translation and browser verification work.

Thanks to xet7 !
2026-10-14 05:21:58 +03:00
Lauri Ojansivu 1c92a11f39 Translate Irish import and export controls
Preserve user matching choices, column mapping, bulk export layout and Trello access requirements. Add translation and browser regressions.

Thanks to xet7 !
2026-10-14 05:21:22 +03:00
Lauri Ojansivu b89f49c0fe Document the second Irish import-guide translation batch
Record checks, preserved Chinese values and remaining verification work.

Thanks to xet7 !
2026-10-14 05:18:50 +03:00
Lauri Ojansivu bc97a96117 Translate twelve more Irish import guides
Preserve export prerequisites, field mapping and omitted content warnings. Extend import help regression coverage.

Thanks to xet7 !
2026-10-14 05:18:12 +03:00
Lauri Ojansivu 9190084351 Document Irish import guidance translations in Upcoming
Record verified coverage and remaining browser runtime and translation work.

Thanks to xet7 !
2026-10-14 05:15:31 +03:00
Lauri Ojansivu 6be0ca89dc Translate ten Irish import guides
Preserve format mappings, completed items and first-board limitations. Extend translation checks and import help browser coverage.

Thanks to xet7 !
2026-10-14 05:14:52 +03:00
Lauri Ojansivu ddecce6236 Document Irish card-link and storage translations in Upcoming
Record regression coverage and remaining translation work.

Thanks to xet7 !
2026-10-14 05:12:03 +03:00
Lauri Ojansivu f71a482f02 Translate Irish card links and remaining legacy storage labels
Fill 67 English placeholders, preserving link directions, permission checks and storage key distinctions. Extend translation and browser regression coverage.

Thanks to xet7 !
2026-10-14 05:11:28 +03:00
Lauri Ojansivu 40f8826fba Document Malagasy action-label corrections and remaining inventory
Record verification, Chinese preservation and outstanding all-language work.

Thanks to xet7 !
2026-10-14 05:05:40 +03:00
Lauri Ojansivu aaaa3391bc Restore Malagasy action and status label meaning
Clarify optional move reasons, version-check failure and header toggle scope with regression coverage.

Thanks to xet7 !
2026-10-14 05:04:57 +03:00
Lauri Ojansivu b5974db90e Document Malagasy storage translation audit in Upcoming
Record verification and remaining review work.

Thanks to xet7 !
2026-10-14 05:02:48 +03:00
Lauri Ojansivu 7ec01cb14e Distinguish Malagasy storage credentials from permissions
Correct Google Cloud authentication labels and backup help, preserving optional credential fallback choices.

Thanks to xet7 !
2026-10-14 05:02:18 +03:00
Lauri Ojansivu 3593121b7f Document Malagasy scheduling translation corrections in Upcoming
Record checks and outstanding browser runtime and semantic review work.

Thanks to xet7 !
2026-10-14 04:59:56 +03:00
Lauri Ojansivu bd274fb312 Correct Malagasy recurrence and time-adjustment explanations
Restore set-or-change date triggers, calendar labels and time attribution limits. Cover recurrence and time report wording with regression checks.

Thanks to xet7 !
2026-10-14 04:59:18 +03:00
Lauri Ojansivu fb02fa3f1c Document Malagasy report-label translation audit in Upcoming
Record corrected terminology, regression checks and pending browser runtime verification.

Thanks to xet7 !
2026-10-14 04:56:46 +03:00
Lauri Ojansivu e6d688867c Correct Malagasy report labels and WIP group terminology
Clarify numeric value sums, report measures, forecasts and swimlane selection. Add translation regressions and browser coverage.

Thanks to xet7 !
2026-10-14 04:56:14 +03:00
Lauri Ojansivu fdeebd05c3 Document Malagasy account and report translation repairs
Record semantic corrections, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:51:48 +03:00
Lauri Ojansivu 01204a94f0 Restore Malagasy account warning and report explanations
Replace unrelated export text in account anonymization and expand five report methodology notes. Preserve destructive consequences and analytical limits with regression coverage.

Thanks to xet7 !
2026-10-14 04:51:15 +03:00
Lauri Ojansivu 7e299300e0 Document Malagasy import guidance corrections
Record stale-English findings, semantic checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:48:21 +03:00
Lauri Ojansivu 0e518992bb Repair stale Malagasy import guidance
Translate old English Todoist and Planner guides, restore Jira pagination and Excel worksheet behavior, and correct CSV delimiter terminology. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:47:49 +03:00
Lauri Ojansivu a996de4e17 Document Malagasy recovery translation milestone
Record placeholder coverage, semantic limits, preservation checks and remaining all-language work.

Thanks to xet7 !
2026-10-14 04:45:24 +03:00
Lauri Ojansivu ef2e446d98 Translate Malagasy import and Scrum recovery messages
Translate 29 entries preserving permanent deletion, unchanged records and rollback safeguards. Add a full-catalog placeholder gate and extend recovery browser coverage.

Thanks to xet7 !
2026-10-14 04:44:53 +03:00
Lauri Ojansivu 11895f38fa Document Malagasy sync recovery translations
Record preserved recovery semantics, verification limits and remaining all-language work.

Thanks to xet7 !
2026-10-14 04:42:22 +03:00
Lauri Ojansivu 8422cc349c Translate Malagasy stopped sync recovery messages
Translate 18 recovery entries preserving retained changes, abandoned writes, access restrictions and replayable-operation safeguards. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:41:50 +03:00
Lauri Ojansivu f43e3288fd Document Malagasy Scrum planning translations
Record semantic coverage, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:39:41 +03:00
Lauri Ojansivu 36647667e6 Translate Malagasy Scrum planning transfer messages
Translate 21 entries preserving matching rules, preview outcomes, unchanged cards and synchronization safeguards. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:39:12 +03:00
Lauri Ojansivu a84e61caec Document Malagasy workflow translation coverage
Record preserved semantics, regression checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:36:26 +03:00
Lauri Ojansivu 9c09c273d4 Translate Malagasy workflow and administration messages
Translate 25 entries for Wrike, LDAP, OAuth, subtask permissions and Blockly fragments. Preserve identifiers and placeholders and extend regression coverage.

Thanks to xet7 !
2026-10-14 04:35:56 +03:00
Lauri Ojansivu b7795367f6 Document Malagasy notification delivery translations
Record preserved semantics, regression checks and remaining all-language work.

Thanks to xet7 !
2026-10-14 04:33:38 +03:00
Lauri Ojansivu 519e99e4f3 Translate Malagasy integrations and notification delivery
Translate 45 messages while preserving inherited defaults, webhook privacy, grouping, quiet hours and link-rule syntax. Add semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:33:06 +03:00
Lauri Ojansivu b9c0e5739e Document Malagasy bulk transfer translations
Record translated mapping choices, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:30:44 +03:00
Lauri Ojansivu ef60786045 Translate Malagasy bulk import and export choices
Translate 35 messages covering member and column mapping, bulk transfer layouts, Trello export and Sandstorm access. Preserve technical markers and add semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:30:03 +03:00
Lauri Ojansivu 27f29d2cd5 Document Malagasy project import translations
Record translated import limits, regression coverage and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:27:30 +03:00
Lauri Ojansivu b3eaaf9f4e Translate Malagasy project import guidance
Translate twelve project import guides with explicit omission warnings, English column requirements and nested-task markers. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:26:56 +03:00
Lauri Ojansivu b09a406d9f Document Malagasy outline and task import translations
Record semantic coverage, preservation checks and remaining translation work.

Thanks to xet7 !
2026-10-14 04:24:20 +03:00
Lauri Ojansivu 211c67ad11 Translate Malagasy outline and task import guides
Translate ten import guides while preserving format names, data mappings, completion dates and first-board restrictions. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:23:43 +03:00
Lauri Ojansivu 239190a02d Document Malagasy linked-card and storage translations
Record placeholder coverage, regression checks and remaining all-language work.

Thanks to xet7 !
2026-10-14 04:21:21 +03:00
Lauri Ojansivu 7d10b098f8 Translate Malagasy linked-card and storage messages
Fill 67 English placeholders including field-link permissions and directions, announcements, storage settings and rule connectors. Extend catalog and browser regression coverage.

Thanks to xet7 !
2026-10-14 04:20:46 +03:00
Lauri Ojansivu 244331c775 Document Haitian action labels and translation inventory
Record semantic corrections, verification limits and the current remaining all-language inventory.

Thanks to xet7 !
2026-10-14 04:16:14 +03:00
Lauri Ojansivu 50955434aa Clarify Haitian card actions and bounce thresholds
Distinguish destination linking from creating a board from the current card, explain version lookup failure and configured bounce thresholds, and extend regression coverage.

Thanks to xet7 !
2026-10-14 04:15:35 +03:00
Lauri Ojansivu 4bfec2b6b4 Document Haitian import and sync guidance review
Record updated guidance, semantic review and regression checks with the remaining all-language work still open.

Thanks to xet7 !
2026-10-14 04:12:53 +03:00
Lauri Ojansivu 4063ccbbc8 Update Haitian import and synchronization guidance
Restore Jira pagination and Excel worksheet mapping explanations. Use Haitian preview terminology and clarify external tracking. Add semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:12:13 +03:00
Lauri Ojansivu 5d3b6f99fe Document Haitian analytics translation repairs
Record restored methodology, regression coverage and preservation checks while keeping all-language review open.

Thanks to xet7 !
2026-10-14 04:08:25 +03:00
Lauri Ojansivu 8a4cf08d65 Restore Haitian analytics explanations and report labels
Replace incomplete shorthand with translated methodology, missing-history rules, forecast limits and date fallbacks. Clarify movement reasons and never-synced state; add semantic and browser coverage.

Thanks to xet7 !
2026-10-14 04:07:50 +03:00
Lauri Ojansivu bf8d41363c Document Haitian WIP and numeric summary translation repairs
Record semantic regression coverage, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 04:04:50 +03:00
Lauri Ojansivu dcb7064e49 Correct Haitian WIP groups and numeric summary labels
Distinguish groups from enabling limits, clarify swimlane selection and describe numeric value sums instead of field counts. Add catalog and browser regression coverage.

Thanks to xet7 !
2026-10-14 04:04:12 +03:00
Lauri Ojansivu 46fd33defc Document Haitian backup and credential translation repairs
Record technical-literal coverage, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:59:43 +03:00
Lauri Ojansivu 9bc9a3fe7e Correct Haitian backup and credential terminology
Repair eight storage-help entries while preserving encryption-key requirements, restoration warnings, backup paths and provider permission labels. Add semantic and cloud-settings browser coverage.

Thanks to xet7 !
2026-10-14 03:59:09 +03:00
Lauri Ojansivu 13e861f476 Document Haitian account anonymization warning repair
Record the corrected consequences, regression evidence and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:56:49 +03:00
Lauri Ojansivu cdcbe1e430 Repair Haitian account anonymization warning
Replace incorrectly duplicated export guidance with the actual permanent identity changes, avatar removal, disabled login and preserved history. Add semantic and confirmation-popup regression coverage.

Thanks to xet7 !
2026-10-14 03:56:19 +03:00
Lauri Ojansivu 98b66c25f8 Document Haitian recurrence translation corrections
Record semantic regression coverage, preservation checks and remaining all-language work.

Thanks to xet7 !
2026-10-14 03:54:14 +03:00
Lauri Ojansivu 95f0938b36 Distinguish Haitian card recurrence from checklist reset
Correct eight labels for recurring cards, automatic unchecking, checking all items and known accounts. Add semantic and popup regression coverage.

Thanks to xet7 !
2026-10-14 03:53:34 +03:00
Lauri Ojansivu 6a5f4a16c4 Document Haitian due-card and search translation repairs
Record production-parser coverage, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:50:51 +03:00
Lauri Ojansivu 7c5571248e Correct Haitian due-card scope and attachment search terms
Repair nine labels and verify the production parser accepts attachment presence and absence queries while rejecting invalid predicates. Extend browser search coverage.

Thanks to xet7 !
2026-10-14 03:50:19 +03:00
Lauri Ojansivu 5a5787c517 Document Haitian administration translation corrections
Record tested semantics, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:47:29 +03:00
Lauri Ojansivu 54c2af0993 Correct Haitian administration and notification precedence
Repair fourteen hostname, permission, override and interface labels. Preserve tenant prerequisites and administrator boundaries; extend semantic and notification UI coverage.

Thanks to xet7 !
2026-10-14 03:46:58 +03:00
Lauri Ojansivu d43e17624d Document Haitian settings translation repairs
Record validation, Chinese preservation and remaining all-language semantic review.

Thanks to xet7 !
2026-10-14 03:44:27 +03:00
Lauri Ojansivu a7346d6a58 Correct Haitian visibility and email settings wording
Repair fourteen settings strings, including SMTP host, clickable URL schemes, starred swimlanes and template labels. Preserve fallback behavior and add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-14 03:43:55 +03:00
Lauri Ojansivu 3dcedf2a8e Document Haitian upload and archive wording corrections
Record regression coverage, preservation checks and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:41:38 +03:00
Lauri Ojansivu 08b5bbf36f Correct Haitian upload direction and archive guidance
Correct ten upload and control labels. Preserve map administrator permissions and point archive restoration to All Boards; extend semantic and map-upload browser coverage.

Thanks to xet7 !
2026-10-14 03:40:58 +03:00
Lauri Ojansivu e0eb420fc2 Document Haitian calendar and core-interface corrections
Record wrong-language repairs, regression results and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:38:34 +03:00
Lauri Ojansivu 6227e6b908 Replace French calendar labels in Haitian Creole
Correct 20 calendar and core-interface entries, preserving calendar variants and restoring account, snapshot and roadmap meanings. Add semantic and calendar-selector regression coverage.

Thanks to xet7 !
2026-10-14 03:38:02 +03:00
Lauri Ojansivu 276d173d01 Document Haitian subtask and checklist corrections
Record validation and remaining all-language review in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-14 03:35:30 +03:00
Lauri Ojansivu 5dd13b7808 Unify Haitian subtask and checklist terminology
Correct 30 entries across card controls, activity text and settings. Restore matching and template-copy wording, preserve attachment retention, and extend subtask browser assertions.

Thanks to xet7 !
2026-10-14 03:34:49 +03:00
Lauri Ojansivu 70981a0812 Document Haitian semantic translation corrections
Record the 81 corrected entries, regression coverage and remaining all-language review.

Thanks to xet7 !
2026-10-14 03:32:36 +03:00
Lauri Ojansivu bcdf935521 Correct Haitian attachment and rule terminology
Correct 81 entries: attachment terminology throughout the catalog, checkbox actions, date initialization triggers and assignee grammar. Preserve placeholders and deletion distinctions, and add rule-builder and attachment-settings UI coverage.

Thanks to xet7 !
2026-10-14 03:32:06 +03:00
Lauri Ojansivu c60d616ccb Document Haitian placeholder milestone and remaining review
Record the stale-English corrections, current catalog gate and validation limits in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-14 03:27:18 +03:00
Lauri Ojansivu eca6c80825 Finish Haitian English placeholders and repair stale import guides
Translate eight current placeholders and two stale-English guides. Preserve Scrum recovery permissions and data retention, add the current catalog gate and extend six browser cases. Semantic review remains separate.

Thanks to xet7 !
2026-10-14 03:26:43 +03:00
Lauri Ojansivu 048f4cc5fd Document Haitian planning sync and recovery translations
Record coverage and remaining all-language work in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-14 03:23:58 +03:00
Lauri Ojansivu 940aee620b Translate Haitian planning sync and recovery messages
Translate 48 entries with retained-data, permanent-deletion and replay restrictions intact. Extend catalog and browser coverage for planning sync and interrupted operations.

Thanks to xet7 !
2026-10-14 03:23:28 +03:00
Lauri Ojansivu 5287a6800a Document Haitian workflow and planning translations
Record regression coverage and remaining translation work in Upcoming and the audit.

Thanks to xet7 !
2026-10-14 03:21:01 +03:00
Lauri Ojansivu 8c8c00546d Translate Haitian workflow and planning messages
Translate 45 administration, workflow, Blockly and Scrum entries. Preserve permissions, completion groups, source matching and placeholders; cover subtask editing and denied access in the browser suite.

Thanks to xet7 !
2026-10-14 03:20:34 +03:00
Lauri Ojansivu b8f2925f1b Document Haitian integration and notification translations
Record regression results, preserved Chinese values and the remaining all-language inventory in Upcoming and the audit.

Thanks to xet7 !
2026-10-14 03:17:56 +03:00
Lauri Ojansivu 38e2004f8e Translate Haitian integration and notification settings
Translate 49 remaining entries while preserving rule syntax, inherited settings, identity controls and scheduling distinctions. Register Haitian Creole in notification UI regression coverage.

Thanks to xet7 !
2026-10-14 03:17:25 +03:00
Lauri Ojansivu cbd2d0ec2c Document Haitian Creole bulk import and export translations
Record coverage and remaining all-language review in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-14 03:15:16 +03:00
Lauri Ojansivu 7687970a9d Translate Haitian Creole bulk import and export controls
Preserve member mapping limits, project boundaries, export layouts and placeholders; add catalog and browser regression coverage.

Thanks to xet7 !
2026-10-14 03:14:42 +03:00
Lauri Ojansivu 4598f254a4 Document Haitian Creole import mapping translations
Thanks to xet7 !
2026-10-14 03:10:35 +03:00
Lauri Ojansivu a79388c894 Translate Haitian Creole import mapping controls
Thanks to xet7 !
2026-10-14 03:10:01 +03:00
Lauri Ojansivu 84cd1a7e17 Document Haitian Creole project import translations
Thanks to xet7 !
2026-10-14 03:07:51 +03:00
Lauri Ojansivu e26b9f041b Translate Haitian Creole project import guides
Thanks to xet7 !
2026-10-14 03:07:19 +03:00
Lauri Ojansivu 724ff27a10 Document Haitian Creole import guide translations
Thanks to xet7 !
2026-10-14 03:04:37 +03:00
Lauri Ojansivu 80e756b223 Translate Haitian Creole outline and task import guides
Thanks to xet7 !
2026-10-14 03:04:08 +03:00
Lauri Ojansivu 8f94222d92 Document Haitian Creole linked-card translation progress
Thanks to xet7 !
2026-10-14 03:01:55 +03:00
Lauri Ojansivu 4ac5621e64 Translate Haitian Creole linked cards and storage controls
Thanks to xet7 !
2026-10-14 03:01:22 +03:00
Lauri Ojansivu ffd8494ee1 Document shared-word translation review
Thanks to xet7 !
2026-10-14 02:56:30 +03:00
Lauri Ojansivu db56686e35 Preserve reviewed shared words in translation inventory
Thanks to xet7 !
2026-10-14 02:56:03 +03:00
Lauri Ojansivu d47eaf3b13 Document Latin import repairs and translation inventory
Thanks to xet7 !
2026-10-14 02:53:51 +03:00
Lauri Ojansivu 96e2c6f620 Translate stale Latin Todoist and Planner import guidance
Thanks to xet7 !
2026-10-14 02:53:21 +03:00
Lauri Ojansivu 849fcfdb94 Document Latin prefix cleanup and remaining review gaps
Thanks to xet7 !
2026-10-14 02:51:10 +03:00
Lauri Ojansivu d1e8c955e3 Repair remaining prefixed Latin report translations
Thanks to xet7 !
2026-10-14 02:50:41 +03:00
Lauri Ojansivu c0d8555395 Document Latin status and account translation review
Thanks to xet7 !
2026-10-14 02:48:05 +03:00
Lauri Ojansivu 6f0bbb9a34 Repair Latin status account and repair-result translations
Thanks to xet7 !
2026-10-14 02:47:36 +03:00
Lauri Ojansivu ddcd625c3c Document Latin monitoring translation review
Thanks to xet7 !
2026-10-14 02:45:07 +03:00
Lauri Ojansivu 2e4f49297e Repair Latin monitoring and conversion progress translations
Thanks to xet7 !
2026-10-14 02:44:38 +03:00
Lauri Ojansivu 9697d3ac00 Document Latin migration confirmation translation review
Thanks to xet7 !
2026-10-14 02:42:11 +03:00
Lauri Ojansivu 7991925d05 Repair Latin migration confirmations and progress labels
Thanks to xet7 !
2026-10-14 02:41:40 +03:00
Lauri Ojansivu de5c695347 Document Latin board migration translation review
Thanks to xet7 !
2026-10-14 02:39:03 +03:00
Lauri Ojansivu 5c32f58834 Repair Latin board migration translations
Thanks to xet7 !
2026-10-14 02:38:33 +03:00
Lauri Ojansivu ce05a0d6bc Document Latin migration status translation review
Thanks to xet7 !
2026-10-14 02:36:10 +03:00
Lauri Ojansivu d9f565bdb7 Repair Latin migration status and S3 translations
Thanks to xet7 !
2026-10-14 02:35:44 +03:00
Lauri Ojansivu 621ab16e82 Document Latin cloud guidance translation review
Thanks to xet7 !
2026-10-14 02:33:48 +03:00
Lauri Ojansivu efbff09543 Repair Latin cloud configuration guidance
Thanks to xet7 !
2026-10-14 02:33:20 +03:00
Lauri Ojansivu 46106f55d4 Document Latin backup translation review
Thanks to xet7 !
2026-10-14 02:31:10 +03:00
Lauri Ojansivu 6bfdbc9179 Repair Latin backup scope and credential translations
Thanks to xet7 !
2026-10-14 02:30:44 +03:00
Lauri Ojansivu 280b32db97 Document Latin security translation review
Thanks to xet7 !
2026-10-14 02:28:49 +03:00
Lauri Ojansivu 386fbba5ec Repair Latin security setting translations
Thanks to xet7 !
2026-10-14 02:28:21 +03:00
Lauri Ojansivu 37cbaeff48 Document Latin migration translation review
Thanks to xet7 !
2026-10-14 02:25:42 +03:00
Lauri Ojansivu e949142731 Repair Latin migration and card loading translations
Thanks to xet7 !
2026-10-14 02:25:14 +03:00
Lauri Ojansivu f329147f45 Document Latin scheduling and cloud translation review
Thanks to xet7 !
2026-10-14 02:23:09 +03:00
Lauri Ojansivu 8cf28c4464 Repair Latin scheduling and cloud storage translations
Thanks to xet7 !
2026-10-14 02:22:43 +03:00
Lauri Ojansivu c997c3ad89 Document Latin support and lockout translation review
Thanks to xet7 !
2026-10-14 02:20:29 +03:00
Lauri Ojansivu 08137b1d4a Repair Latin support and lockout translations
Thanks to xet7 !
2026-10-14 02:20:00 +03:00
Lauri Ojansivu f3d436febb Document Latin maintenance translation review
Thanks to xet7 !
2026-10-14 02:17:11 +03:00
Lauri Ojansivu 5002df4c9d Repair Latin maintenance and account translations
Thanks to xet7 !
2026-10-14 02:16:42 +03:00
Lauri Ojansivu d0efe6565a Document Latin storage translation review
Thanks to xet7 !
2026-10-14 02:14:26 +03:00
Lauri Ojansivu 104480b6cc Repair Latin storage and attachment repair translations
Thanks to xet7 !
2026-10-14 02:13:52 +03:00
Lauri Ojansivu c6c5b4b56c Document Latin checklist and attachment translation review
Thanks to xet7 !
2026-10-14 02:11:16 +03:00
Lauri Ojansivu a83a794b12 Repair Latin checklist and attachment action translations
Thanks to xet7 !
2026-10-14 02:10:44 +03:00
Lauri Ojansivu 681e7a4b07 Document Latin invitation and memory translation review
Thanks to xet7 !
2026-10-14 02:08:05 +03:00
Lauri Ojansivu 2dd8c45cab Repair Latin invitation and Node memory translations
Translate 35 mixed-language entries, preserving invitation conditions, organization controls, allocated/used/peak memory and diagnostic overwrite semantics. Verify V8 terminology and add regression and browser coverage.

Thanks to xet7 !
2026-10-14 02:05:19 +03:00
Lauri Ojansivu 82d73334e0 Document Latin waiting and ticket translation audit
Thanks to xet7 !
2026-10-14 02:01:56 +03:00
Lauri Ojansivu 5df2e188cc Repair Latin waiting indicators and ticket labels
Translate 30 mixed-language controls and messages, preserving deletion prerequisites, Cc syntax, window-action distinctions, ticket states and history actions. Add regression and browser coverage.

Thanks to xet7 !
2026-10-14 02:01:13 +03:00
Lauri Ojansivu 03029bf0a4 Document Latin office API and recovery translation audit
Thanks to xet7 !
2026-10-14 01:57:37 +03:00
Lauri Ojansivu 1c0204e23e Repair Latin office API and recovery reports
Translate 25 mixed-language entries, preserving API aggregation and configuration, recovery actions and automatic continuation. Add regression and office-report browser coverage.

Thanks to xet7 !
2026-10-14 01:56:48 +03:00
Lauri Ojansivu 779d5ffa7e Document Latin field and report translation audit
Thanks to xet7 !
2026-10-14 01:54:18 +03:00
Lauri Ojansivu bd9d654aae Repair Latin string template and report translations
Translate 30 mixed-language entries, preserving value substitution, space entities, invisible-character filtering and successful-login reporting scope. Extend custom-field browser coverage.

Thanks to xet7 !
2026-10-14 01:53:41 +03:00
Lauri Ojansivu 04351489ad Document Latin location and background translation audit
Thanks to xet7 !
2026-10-14 01:51:09 +03:00
Lauri Ojansivu 08c7d5314b Repair Latin location and background translations
Translate 30 mixed-language entries, preserving coordinate distinctions, size placeholders, sorting direction and literal troubleshooting commands. Add positive and negative location-detection browser coverage.

Thanks to xet7 !
2026-10-14 01:50:32 +03:00
Lauri Ojansivu 2e8e089819 Document Latin sorting and dependency translation audit
Thanks to xet7 !
2026-10-14 01:48:14 +03:00
Lauri Ojansivu 959bfbcedc Repair Latin sorting and dependency translations
Translate 37 mixed-language entries, preserving dependency direction, completion polarity, import formats and unmatched counts. Keep the exact shared Latin word Color and add regression and browser coverage.

Thanks to xet7 !
2026-10-14 01:47:33 +03:00
Lauri Ojansivu 7ea0448f23 Document Latin search help translation audit
Thanks to xet7 !
2026-10-14 01:43:28 +03:00
Lauri Ojansivu 30c12e510c Repair Latin search instructions and examples
Translate 32 mixed-language entries, preserving query placeholders while explaining OR/AND, negation, sorting, date ranges and archive defaults. Localize user, absence and card-number examples with regression and browser coverage.

Thanks to xet7 !
2026-10-14 01:42:49 +03:00
Lauri Ojansivu b791d8d054 Document Latin search predicates translation audit
Thanks to xet7 !
2026-10-14 01:39:28 +03:00
Lauri Ojansivu e3ea440a31 Repair Latin search predicates and introductory help
Translate 40 mixed-language search entries, preserve validation constraints and placeholders, and use a localized list example. Verify parser registration and shared selector wording; add overdue-search browser coverage.

Thanks to xet7 !
2026-10-14 01:38:48 +03:00
Lauri Ojansivu 90349ce081 Document Latin search translation audit
Thanks to xet7 !
2026-10-14 01:36:43 +03:00
Lauri Ojansivu cb3da6ac52 Repair Latin search messages and operator names
Translate 48 mixed-language entries, retaining search access boundaries and placeholders. Preserve shorthand aliases and verify distinct localized operators are registered in the parser, with browser coverage for title filtering.

Thanks to xet7 !
2026-10-14 01:36:05 +03:00
Lauri Ojansivu 976e0893f1 Document Latin template sharing and card view audit
Thanks to xet7 !
2026-10-14 01:33:49 +03:00
Lauri Ojansivu 13f06820f7 Repair Latin template sharing and card view translations
Translate 40 mixed-language entries, preserving template scope and nonempty filtering, domain validation examples, permission boundaries and incomplete due-card semantics.

Thanks to xet7 !
2026-10-14 01:33:12 +03:00
Lauri Ojansivu c062eb7b74 Document Latin roles and weekdays translation audit
Thanks to xet7 !
2026-10-14 01:31:06 +03:00
Lauri Ojansivu 61480096ba Repair Latin role explanations and weekday labels
Translate 35 mixed-language entries, preserving global-admin rights, unsaved role previews, assigned-only visibility and linked-card deletion prerequisites. Add regression and browser coverage.

Thanks to xet7 !
2026-10-14 01:30:31 +03:00
Lauri Ojansivu bc28b2f978 Document Latin reminder and preference translation audit
Thanks to xet7 !
2026-10-14 01:28:14 +03:00
Lauri Ojansivu ee4ce8a1eb Repair Latin reminders and preference descriptions
Translate 35 mixed-language entries, preserving reminder states, irreversible deletion, self-account scope, keyboard combinations and enabled/disabled preference behavior.

Thanks to xet7 !
2026-10-14 01:27:35 +03:00
Lauri Ojansivu f51ded9566 Document Latin authentication and date translation audit
Thanks to xet7 !
2026-10-14 01:25:23 +03:00
Lauri Ojansivu d0676f4a76 Repair Latin authentication and date activity labels
Translate 30 mixed-language entries, preserving irreversible swimlane deletion, date placeholders and relative card/list placement. Add regression and browser warning coverage.

Thanks to xet7 !
2026-10-14 01:24:47 +03:00
Lauri Ojansivu 408883859c Document Latin rule hints and branding audit
Thanks to xet7 !
2026-10-14 01:22:56 +03:00
Lauri Ojansivu c59113bebf Repair Latin rule hints and branding settings
Translate 40 mixed-language entries, preserving empty-field matching, comma-separated checklist input, distinct date fields, authentication names, JSON filenames and HTML tags.

Thanks to xet7 !
2026-10-14 01:22:23 +03:00
Lauri Ojansivu 1c2d02f1f2 Document Latin rule description translation audit
Thanks to xet7 !
2026-10-14 01:20:10 +03:00
Lauri Ojansivu fd921c8fef Repair Latin rule actions and saved descriptions
Translate 50 action fragments and descriptions with matching movement and checklist wording. Recognize the exact shared Latin imperative Remove without exempting longer English prose. Add regression and browser coverage.

Thanks to xet7 !
2026-10-14 01:19:34 +03:00
Lauri Ojansivu 637e7e0902 Document Latin trigger grammar translation audit
Thanks to xet7 !
2026-10-14 01:17:29 +03:00
Lauri Ojansivu 61a2f68df0 Repair Latin rule trigger sentences and linking verbs
Translate 33 prefixed trigger fragments and repair three unprefixed assignee/date entries. Omit the redundant linking verb for Latin finite predicates, preserving other locales and testing assembled and saved descriptions.

Thanks to xet7 !
2026-10-14 01:16:39 +03:00
Lauri Ojansivu 9114224715 Document Latin rule action translation audit
Thanks to xet7 !
2026-10-14 01:14:08 +03:00
Lauri Ojansivu 5e94c5e2a0 Repair Latin rule action and duration labels
Translate 24 mixed-language controls while preserving completion polarity, relative dates, all-card scope and distinct duration units. Add workflow toolbox browser coverage.

Thanks to xet7 !
2026-10-14 01:13:31 +03:00
Lauri Ojansivu e575b6d559 Document Latin rule import and scheduling audit
Thanks to xet7 !
2026-10-14 01:11:38 +03:00
Lauri Ojansivu 1f11631552 Repair Latin rule import and scheduling translations
Translate 30 mixed-language entries. Preserve partial workflow conversion, unmapped reporting, weekday boundaries and distinct due-date states, with regression coverage.

Thanks to xet7 !
2026-10-14 01:11:03 +03:00
Lauri Ojansivu 3c3a3a2cc1 Document Latin rule builder translation audit
Thanks to xet7 !
2026-10-14 01:09:10 +03:00
Lauri Ojansivu 6150377ef5 Repair Latin rule builder and activity translations
Translate 40 mixed-language rule controls and activity entries, preserving trigger and action distinctions, import and export direction, archive restoration and placeholders. Extend workflow builder browser coverage to Latin.

Thanks to xet7 !
2026-10-14 01:08:23 +03:00
Lauri Ojansivu 9af473f39c Document Latin deletion and subtask translation audit
Thanks to xet7 !
2026-10-14 01:06:20 +03:00
Lauri Ojansivu 0da6274347 Repair Latin deletion warnings and subtask settings
Translate 40 mixed-language deletion, hierarchy, minicard and activity entries. Preserve irreversible deletion warnings, empty duplicate-list scope, full ancestry versus immediate parent, and activity placeholders.

Thanks to xet7 !
2026-10-14 01:05:39 +03:00
Lauri Ojansivu 5ffbfa083f Document Latin organization translation audit
Thanks to xet7 !
2026-10-14 01:03:23 +03:00
Lauri Ojansivu 7729efb8e0 Repair Latin organization permissions and tenant labels
Translate 25 mixed-language organization, membership, date and color labels. Preserve tenant configuration requirements and the limits of delegated administration, with regression and browser coverage.

Thanks to xet7 !
2026-10-14 01:02:48 +03:00
Lauri Ojansivu 70e696edca Document Latin field policy translation audit
Thanks to xet7 !
2026-10-14 00:58:10 +03:00
Lauri Ojansivu f8a09add9c Repair Latin field policies and system metrics
Correct 30 prefixed labels, preserve new/all-card scope, memory distinctions and private-only visibility, and extend custom-field browser coverage.

Thanks to xet7 !
2026-10-14 00:57:47 +03:00
Lauri Ojansivu fdec6bb6ba Document Latin system information translation audit
Thanks to xet7 !
2026-10-14 00:56:09 +03:00
Lauri Ojansivu 0dda250d79 Repair Latin webhook and system information labels
Correct 30 prefixed labels, preserve product names and runtime configuration identifiers, and distinguish outgoing from bidirectional webhooks.

Thanks to xet7 !
2026-10-14 00:55:49 +03:00
Lauri Ojansivu c86732f73c Document Latin transfer limit and SMTP translation audit
Thanks to xet7 !
2026-10-14 00:54:08 +03:00
Lauri Ojansivu 49dc81ea2b Repair Latin transfer limits and SMTP messages
Correct 40 prefixed strings, restore literal size units and invitation variables, preserve separate server safety caps and extend attachment-limit browser coverage.

Thanks to xet7 !
2026-10-14 00:53:46 +03:00
Lauri Ojansivu 0db0ad3d12 Document Latin upload and WIP translation audit
Thanks to xet7 !
2026-10-14 00:51:48 +03:00
Lauri Ojansivu 7842c44d16 Repair Latin upload and WIP messages
Correct 30 prefixed strings, distinguish upload states and logo fields, preserve WIP recovery choices, and add warning browser coverage.

Thanks to xet7 !
2026-10-14 00:51:28 +03:00
Lauri Ojansivu 58dd1771f6 Document Latin sidebar and tracking translation audit
Thanks to xet7 !
2026-10-14 00:49:47 +03:00
Lauri Ojansivu 42ab802edd Repair Latin sidebar and tracking controls
Correct 30 prefixed strings, preserve default-board behavior, shortcut ranges and add/remove distinctions, and extend shortcut-page coverage.

Thanks to xet7 !
2026-10-14 00:49:27 +03:00
Lauri Ojansivu dbe9712bd9 Document Latin member removal and shortcut audit
Thanks to xet7 !
2026-10-14 00:47:48 +03:00
Lauri Ojansivu 7f256bce30 Repair Latin member removal and shortcut messages
Correct 30 prefixed strings, retain Sandstorm access revocation warning and all-card removal notification, and distinguish self-membership from assignment in shortcut coverage.

Thanks to xet7 !
2026-10-14 00:47:27 +03:00
Lauri Ojansivu bcacf5d58b Document Latin visibility translation audit
Thanks to xet7 !
2026-10-14 00:45:42 +03:00
Lauri Ojansivu 48a62a615e Repair Latin visibility and notification descriptions
Correct 30 prefixed strings, preserve public-read/member-edit and assigned-card boundaries, and extend archive-tab browser coverage to Latin.

Thanks to xet7 !
2026-10-14 00:45:18 +03:00
Lauri Ojansivu 9673e6e0c9 Document Latin list action translation audit
Thanks to xet7 !
2026-10-14 00:43:20 +03:00
Lauri Ojansivu 5cb027f4c4 Repair Latin list actions and board departure messages
Correct 30 prefixed strings, preserve all-card scope and permanent deletion versus archiving, and add list warning browser coverage.

Thanks to xet7 !
2026-10-14 00:42:59 +03:00
Lauri Ojansivu 7892d7dac8 Document Latin mapping and validation translation audit
Thanks to xet7 !
2026-10-14 00:41:07 +03:00
Lauri Ojansivu 0225b90529 Repair Latin member mapping and validation messages
Correct 25 prefixed strings, preserve current-user fallback, four-digit years and the last-administrator restriction, and extend keyboard-shortcut browser coverage.

Thanks to xet7 !
2026-10-14 00:40:48 +03:00
Lauri Ojansivu dfc88ff47f Document Latin Trello import translation audit
Thanks to xet7 !
2026-10-14 00:39:06 +03:00
Lauri Ojansivu 2e641f3fb7 Repair Latin Trello import messages
Correct 40 prefixed strings, preserve ZIP limits, credential requirements and destructive cancellation scope, and add file and credential-label browser coverage.

Thanks to xet7 !
2026-10-14 00:38:44 +03:00
Lauri Ojansivu b28afc5f6d Document Latin advanced filter and import translation audit
Thanks to xet7 !
2026-10-14 00:36:38 +03:00
Lauri Ojansivu 7f26a7e7a9 Repair Latin advanced filter and import instructions
Correct 30 prefixed messages, update stale Jira pagination and Excel sheet guidance, preserve literal filter examples, and extend Latin import browser coverage to nine more formats.

Thanks to xet7 !
2026-10-14 00:36:11 +03:00
Lauri Ojansivu beb06e9944 Document Latin filter translation audit
Thanks to xet7 !
2026-10-14 00:33:18 +03:00
Lauri Ojansivu 9860c8a248 Repair Latin sorting and filter controls
Correct 30 prefixed strings, retain distinct due periods and missing-value filters, and distinguish members, assignees and creators. Add date-filter browser coverage.

Thanks to xet7 !
2026-10-14 00:32:57 +03:00
Lauri Ojansivu ff6dc11156 Document Latin export translation audit
Thanks to xet7 !
2026-10-14 00:31:01 +03:00
Lauri Ojansivu 855b54d55a Repair Latin export labels and name collision errors
Correct 30 prefixed strings, preserve export permissions, uploader/time distinctions and disk-space requirements, and add export-popup browser coverage.

Thanks to xet7 !
2026-10-14 00:30:40 +03:00
Lauri Ojansivu f9161d0096 Document Latin account email and import error audit
Thanks to xet7 !
2026-10-14 00:29:02 +03:00
Lauri Ojansivu 737e8cd4d0 Repair Latin account emails and import errors
Correct 25 prefixed messages, retain invitation variables and paragraph breaks, and clarify permission and linked-card restrictions. Add malformed-JSON browser coverage.

Thanks to xet7 !
2026-10-14 00:28:42 +03:00
Lauri Ojansivu 843c1b2b8b Document Latin custom field translation audit
Thanks to xet7 !
2026-10-14 00:26:51 +03:00
Lauri Ojansivu 9c5f1c6ad4 Repair Latin custom fields and account email messages
Correct 50 prefixed strings, restore date-format tokens and enrollment email paragraphs, and preserve field deletion and global administrator warnings. Add form browser coverage.

Thanks to xet7 !
2026-10-14 00:26:32 +03:00
Lauri Ojansivu 592537acc0 Document Latin role and comment translation audit
Thanks to xet7 !
2026-10-14 00:24:32 +03:00
Lauri Ojansivu 9b890bbe16 Repair Latin role and comment controls
Correct 30 prefixed strings, preserve assigned-card and read-only restrictions, clarify worker actions and restore the empty editor placeholder. Extend semantic and browser coverage.

Thanks to xet7 !
2026-10-14 00:24:13 +03:00
Lauri Ojansivu 20b654a113 Document Latin color and navigation translation audit
Thanks to xet7 !
2026-10-14 00:22:24 +03:00
Lauri Ojansivu 30024f1e66 Repair Latin colors and navigation labels
Translate 32 prefixed strings, including all 25 color names, and update the stale archive restoration location. Add color distinctions and skip-link coverage. Less common shade descriptions remain lower-confidence pending linguistic review.

Thanks to xet7 !
2026-10-14 00:22:04 +03:00
Lauri Ojansivu 1a0c035cde Document Latin font and card aging translation audit
Thanks to xet7 !
2026-10-14 00:20:06 +03:00
Lauri Ojansivu 35d2caec77 Repair Latin font and card aging controls
Correct 35 prefixed strings, preserve font size distinctions, idle-day fading levels and movement directions, and extend font preview browser coverage.

Thanks to xet7 !
2026-10-14 00:19:47 +03:00
Lauri Ojansivu dd0800dce5 Document Latin member mapping translation audit
Thanks to xet7 !
2026-10-14 00:17:55 +03:00
Lauri Ojansivu 8e336c5a13 Repair Latin member mapping and appearance labels
Correct 40 mixed-language strings, preserve imported-role limits and mapping scope, and distinguish theme categories from commands. Add semantic checks and font-popup browser coverage.

Thanks to xet7 !
2026-10-14 00:17:26 +03:00
Lauri Ojansivu 9ec1afce50 Document Latin voting and Planning Poker translation audit
Thanks to xet7 !
2026-10-14 00:15:38 +03:00
Lauri Ojansivu 9c55d0f589 Repair Latin voting and Planning Poker labels
Correct 45 prefixed strings, restore numeric poker choices, distinguish account anonymization from import, and cover positive and negative voting labels.

Thanks to xet7 !
2026-10-14 00:15:17 +03:00
Lauri Ojansivu 374334e63c Document Latin card archive translation audit
Thanks to xet7 !
2026-10-14 00:13:26 +03:00
Lauri Ojansivu d3ea074216 Repair Latin card archive and voting messages
Correct 40 prefixed strings and preserve archive restoration, irreversible deletion, member scope and voting roles. Add card archive confirmation browser coverage.

Thanks to xet7 !
2026-10-14 00:13:06 +03:00
Lauri Ojansivu a7d577b9fc Document Latin board display translation audit
Thanks to xet7 !
2026-10-14 00:11:10 +03:00
Lauri Ojansivu 57534c06e9 Repair Latin board appearance and view labels
Correct 44 prefixed mixed-language strings, preserve visibility and assignee scope, and add semantic checks and calendar menu browser coverage.

Thanks to xet7 !
2026-10-14 00:10:41 +03:00
Lauri Ojansivu b770cba5d1 Document Latin sizing and archive translation audit
Thanks to xet7 !
2026-10-14 00:08:47 +03:00
Lauri Ojansivu f6f05040ae Repair Latin sizing and archive controls
Correct 55 mixed-language strings and test personal/shared scope, toggle direction, data-loss warnings and permanent attachment deletion. Add a browser toggle regression.

Thanks to xet7 !
2026-10-14 00:08:26 +03:00
Lauri Ojansivu a27c4b84fb Document Latin checklist and workspace translation audit
Thanks to xet7 !
2026-10-14 00:06:01 +03:00
Lauri Ojansivu 51421f5384 Repair Latin checklist and workspace translations
Correct 60 mixed-language strings, retain the valid shared Latin word Color, and test argument order, opposite states and non-destructive Home removal. Add workspace navigation browser coverage.

Thanks to xet7 !
2026-10-14 00:05:38 +03:00
Lauri Ojansivu c4f575324b Document Latin activity translation audit
Thanks to xet7 !
2026-10-14 00:02:12 +03:00
Lauri Ojansivu 1e254be892 Repair mixed-language Latin activity messages
Replace 65 prefixed strings with Latin, preserving positional title arguments, move direction, checklist state and comment permissions. Add semantic regression checks and card-history browser coverage.

Thanks to xet7 !
2026-10-14 00:01:48 +03:00
Lauri Ojansivu 5a763705ca Document Latin recovery translation coverage
Thanks to xet7 !
2026-10-13 23:59:17 +03:00
Lauri Ojansivu dc4ea4984b Translate Latin synchronization and recovery messages
Fill 51 pending strings and test matching precedence, retained changes, permanent deletion and recovery restrictions. Register Latin recovery browser scenarios.

Thanks to xet7 !
2026-10-13 23:58:54 +03:00
Lauri Ojansivu bf595d117b Document Latin workflow and Scrum translation coverage
Thanks to xet7 !
2026-10-13 23:57:13 +03:00
Lauri Ojansivu 1212eb97c7 Translate Latin workflow and Scrum import messages
Fill 35 pending translations and repair preview and history labels. Preserve import restrictions, configuration names and placeholders, and extend regression and browser coverage.

Thanks to xet7 !
2026-10-13 23:56:46 +03:00
Lauri Ojansivu ab677e9ace Document Latin notification and link settings translations
Record 46 fills, preservation and regression coverage, and 86 remaining Latin placeholders.

Thanks to xet7 !
2026-10-13 23:52:51 +03:00
Lauri Ojansivu 08eb3aabba Translate Latin notification webhook and link settings
Fill 46 pending strings while preserving link-rule variables, webhook configuration names, quiet-hour behavior and identity choices. Extend semantic and notification settings coverage.

Thanks to xet7 !
2026-10-13 23:52:20 +03:00
Lauri Ojansivu 2e142f7d8b Document Latin import mapping and export translations
Record 35 fills, validation results and 132 remaining Latin placeholders.

Thanks to xet7 !
2026-10-13 23:50:33 +03:00
Lauri Ojansivu 6f71d68317 Translate Latin import mapping and export controls
Fill 35 pending labels and instructions while preserving member mapping, ZIP boundaries, field placeholders and Trello round-trip behavior. Extend semantic and browser checks.

Thanks to xet7 !
2026-10-13 23:50:00 +03:00
Lauri Ojansivu 15d42c6dcf Document the next Latin import-guide batch
Record ten translated guides, coverage and 167 remaining Latin placeholders.

Thanks to xet7 !
2026-10-13 23:48:17 +03:00
Lauri Ojansivu 8d76a58ff8 Translate ten more Latin import guides
Preserve attachment exclusions, source menu and column names, hierarchy markers, archive state and import format choices. Extend semantic and browser instruction checks.

Thanks to xet7 !
2026-10-13 23:47:42 +03:00
Lauri Ojansivu c6791fd51f Document Latin import-guide translation coverage
Record twelve fills, the Trello repair, validation results and 177 remaining Latin placeholders.

Thanks to xet7 !
2026-10-13 23:46:06 +03:00
Lauri Ojansivu ec0e77480e Translate Latin import guides and repair Trello instructions
Translate twelve pending import guides while preserving source menus, format markers, hierarchy mappings and limits. Replace mixed English in the Trello guide and add thirteen browser cases plus semantic checks.

Thanks to xet7 !
2026-10-13 23:45:32 +03:00
Lauri Ojansivu 354ca7aa83 Document Latin card-link translations and pending work
Record 34 fills, two guarded repairs, test coverage and 189 remaining Latin placeholders.

Thanks to xet7 !
2026-10-13 23:42:56 +03:00
Lauri Ojansivu 90ed19f207 Translate Latin card attachments and field links
Fill 34 pending English strings and repair two malformed card-link labels. Cover link direction, permissions and field matching in semantic tests and add Latin browser cases.

Thanks to xet7 !
2026-10-13 23:42:14 +03:00
Lauri Ojansivu 626605259a Document short-rule translations in four more locales
Record 40 fills, preservation and regression checks, and the remaining translation queue.

Thanks to xet7 !
2026-10-13 23:39:12 +03:00
Lauri Ojansivu abf6ab4137 Translate remaining short rules in Javanese Kazakh Tajik and Somali
Fill 40 English placeholders for sorting, actors, recipients, self-selection, counts and Somali Blockly controls. Preserve existing translations and extend alias and token checks.

Thanks to xet7 !
2026-10-13 23:38:31 +03:00
Lauri Ojansivu a42f9e6bef Document short Blockly translations and Khmer alias coverage
Record 126 file-value fills, ten locale names, validation results and the remaining translation inventory.

Thanks to xet7 !
2026-10-13 23:37:00 +03:00
Lauri Ojansivu 0e554efcaf Translate remaining short Blockly controls in ten locales
Fill 126 English file values, covering 140 locale entries through the Khmer alias. Keep control aliases and placeholders consistent and extend regression and browser coverage.

Thanks to xet7 !
2026-10-13 23:36:24 +03:00
Lauri Ojansivu fc8858bc13 Document short rule translations and remaining review
Record 40 fills, the guarded Tagalog endpoint repair and test coverage in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:33:30 +03:00
Lauri Ojansivu 8685cab6a7 Translate short rule labels in six languages
Fill 40 English placeholders in Bengali, Nepali, Hungarian, Icelandic, Albanian and Tagalog. Use numeric separators where inflection cannot fit the count template, and correct the Tagalog sublist endpoint. Extend semantic, token and Blockly coverage.

Thanks to xet7 !
2026-10-13 23:32:58 +03:00
Lauri Ojansivu fff86bafba Document short-label translations across 22 locales
Record 52 fills, preservation checks, browser discovery and the remaining all-language inventory.

Thanks to xet7 !
2026-10-13 23:30:13 +03:00
Lauri Ojansivu 6b97949c4a Translate short labels and S3 fields in 22 locales
Fill 52 remaining English placeholders while preserving correct shared words and existing Chinese translations. Add token and meaning checks and extend S3 and Blockly browser coverage.

Thanks to xet7 !
2026-10-13 23:29:33 +03:00
Lauri Ojansivu 5493914bd6 Document Southern Sotho catalog review and remaining inventory
Record email corrections, validation limits and the refreshed all-language translation queue.

Thanks to xet7 !
2026-10-13 23:26:17 +03:00
Lauri Ojansivu 94f4cf2409 Correct Southern Sotho email instructions and limits
Clarify message content, labels, recipient maximum and SES sender direction. Remove mixed English in email help and add semantic and settings UI regressions.

Thanks to xet7 !
2026-10-13 23:25:37 +03:00
Lauri Ojansivu 74ace78ef7 Document Southern Sotho Scrum and recovery review
Record review through key 4069 and regression results in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:23:21 +03:00
Lauri Ojansivu 6d74dfa66f Correct Southern Sotho Scrum estimate and progress labels
Distinguish estimates from size and translate the sprint progress term. Extend backlog UI and semantic coverage for Scrum and recovery boundaries.

Thanks to xet7 !
2026-10-13 23:22:51 +03:00
Lauri Ojansivu 81124c23c2 Document Southern Sotho Blockly semantic review
Record review through key 3729, six guarded corrections and validation limits in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:20:38 +03:00
Lauri Ojansivu 0056f57d79 Clarify Southern Sotho Blockly angles and movement
Translate inverse tangent, use angular-degree terminology and repair vertical movement shortcuts. Extend semantic checks while preserving valid loop, list and text instructions.

Thanks to xet7 !
2026-10-13 23:20:04 +03:00
Lauri Ojansivu 6948989dac Document Southern Sotho analytics translation review
Record 54 guarded repairs, semantic and browser coverage, and remaining review limitations in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:17:31 +03:00
Lauri Ojansivu 2bc93bdfde Restore Southern Sotho analytics explanations and sync labels
Replace abbreviated chart notes with methodology, missing-data rules and uncertainty caveats. Clarify time corrections, synchronization and credentials, with semantic regressions and seven browser cases. Statistical terminology remains subject to native-speaker review.

Thanks to xet7 !
2026-10-13 23:16:54 +03:00
Lauri Ojansivu e941257634 Document Southern Sotho WIP translation review
Record historical review through key 2999 and its regression coverage in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:13:30 +03:00
Lauri Ojansivu 77f401cdee Correct Southern Sotho WIP groups and field summaries
Restore group selection, severity and API endpoint meanings. Add semantic regressions and a read-only WIP dialog browser case.

Thanks to xet7 !
2026-10-13 23:12:58 +03:00
Lauri Ojansivu 95626f68c1 Document Southern Sotho storage and migration review
Record seven guarded repairs, regression coverage and remaining translation work in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 23:11:10 +03:00
Lauri Ojansivu a14536c444 Clarify Southern Sotho storage and migration translations
Distinguish archived records from saved data and orphaned cards from ownership. Translate remaining S3 prose and extend semantic and browser coverage.

Thanks to xet7 !
2026-10-13 23:10:39 +03:00
Lauri Ojansivu 1bbee3cab7 Document Southern Sotho anonymization translation repair
Record four corrections, confirmation coverage and ongoing all-language review.

Thanks to xet7 !
2026-10-13 23:06:21 +03:00
Lauri Ojansivu 77c1437808 Restore Southern Sotho account anonymization warning
Review source keys 2480–2599. Replace the unrelated export description with the account effects and irreversible-action warning. Clarify whole-instance backup scope and add semantic and browser checks.

Thanks to xet7 !
2026-10-13 23:05:42 +03:00
Lauri Ojansivu 801176fe2b Document Southern Sotho recurrence translation review
Record eight corrections, regression coverage and ongoing all-language work.

Thanks to xet7 !
2026-10-13 23:03:47 +03:00
Lauri Ojansivu e9479f0b07 Distinguish Southern Sotho reset and recurrence controls
Review source keys 2350–2479. Correct automatic checklist reset, card recurrence, last activity and migration severity labels. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 23:03:08 +03:00
Lauri Ojansivu b8ebd2d842 Document Southern Sotho report translation review
Record five corrections, validation and continued all-language work.

Thanks to xet7 !
2026-10-13 23:00:54 +03:00
Lauri Ojansivu 26090b467a Clarify Southern Sotho report metrics and checklist templates
Review source keys 2135–2349. Correct recovery severity, template-copy labels, translation counts and time until due. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 23:00:12 +03:00
Lauri Ojansivu cb31f8a042 Document Southern Sotho search translation review
Record three numeric clarification repairs and validation under Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 22:58:03 +03:00
Lauri Ojansivu d50db7b0bc Clarify Southern Sotho numeric search constraints
Review source keys 1990–2134. Express positive and negative integer signs explicitly and identify the maximum allowed background image size. Preserve query syntax and add semantic and browser checks.

Thanks to xet7 !
2026-10-13 22:57:23 +03:00
Lauri Ojansivu 53957ac74c Document Southern Sotho template translation review
Record five corrections, regression coverage and the ongoing all-language review.

Thanks to xet7 !
2026-10-13 22:55:13 +03:00
Lauri Ojansivu 19cc07f1ed Correct Southern Sotho template and table guidance
Review source keys 1845–1989. Clarify nonempty shared-template eligibility, personal attachments, swimlane grouping and positive integer limits. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:54:35 +03:00
Lauri Ojansivu 390574a643 Document Southern Sotho configuration translation review
Record seven corrections, final validation and remaining all-language work.

Thanks to xet7 !
2026-10-13 22:52:24 +03:00
Lauri Ojansivu 08871f2aa6 Clarify Southern Sotho configuration and visibility guidance
Review source keys 1590–1844. Correct rule-field terminology, OAuth precedence and server storage, board visibility labels and header-toggle guidance; add semantic and browser checks.

Thanks to xet7 !
2026-10-13 22:51:23 +03:00
Lauri Ojansivu e0704813b3 Document short-string inventory repair and Southern Sotho fill
Record 20 translations, exact symbol exemptions, validation and the remaining candidate inventory.

Thanks to xet7 !
2026-10-13 22:47:15 +03:00
Lauri Ojansivu 15643364b0 Count short translation phrases and fill Southern Sotho words
Replace the blanket short-letter exemption with exact key/value symbols. Translate 20 Southern Sotho rule, Blockly and personal-filter strings. Add inventory and browser regression coverage while preserving existing Chinese values.

Thanks to xet7 !
2026-10-13 22:46:14 +03:00
Lauri Ojansivu 740a85b7f5 Document Southern Sotho automation translation review
Record 13 corrections, regression checks and the remaining short-string inventory gap.

Thanks to xet7 !
2026-10-13 22:43:29 +03:00
Lauri Ojansivu 9dc53b66f9 Correct Southern Sotho automation conditions and labels
Review source keys 1417–1589. Restore date-set triggers, auto-detection, rule states, assignee conditions and removal of labels. Translate short English fragments and add semantic and browser checks.

Thanks to xet7 !
2026-10-13 22:42:27 +03:00
Lauri Ojansivu d38dc5021e Document corrected translation inventory and storage fill
Record exact technical-name exemptions, 22 Southern Sotho translations, validation and the newly visible remaining backlog.

Thanks to xet7 !
2026-10-13 22:39:55 +03:00
Lauri Ojansivu d223f4fd53 Expose untranslated storage prose and fill Southern Sotho labels
Replace broad MongoDB, S3 and Azure Blob prefix exemptions with exact technical names. Translate 22 Southern Sotho storage strings, preserve product names and add positive/negative inventory tests plus S3 settings browser coverage.

Thanks to xet7 !
2026-10-13 22:39:08 +03:00
Lauri Ojansivu ba380bfc2b Document Southern Sotho status translations and inventory gap
Record six repairs and identify broad technical-prefix exemptions hiding untranslated prose across locale files.

Thanks to xet7 !
2026-10-13 22:36:19 +03:00
Lauri Ojansivu 12b380b74d Translate Southern Sotho database status and field ordering
Review source keys 1280–1416. Translate MongoDB status descriptions and correct card-field ordering terminology and movement labels. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:35:13 +03:00
Lauri Ojansivu e587c5ac5c Document Southern Sotho link and email translation review
Record ten corrections and validation in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 22:32:47 +03:00
Lauri Ojansivu 8cb58645cb Clarify Southern Sotho link and email settings
Review source keys 1130–1279. Correct automatic-link behavior, tracker URL templates, positive numeric limits and invitation/activity email template labels. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:32:01 +03:00
Lauri Ojansivu ec3661b22f Document Southern Sotho visibility translation review
Record nine corrections, regression checks and the remaining semantic review under Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 22:29:29 +03:00
Lauri Ojansivu 9b7942b661 Correct Southern Sotho labels and visibility controls
Review source keys 930–1129. Repair label terminology, board linking, public/private description controls and the empty starred-items message. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:28:27 +03:00
Lauri Ojansivu 27407f2b14 Document Southern Sotho stale import translation repairs
Record six corrections, browser coverage and the limits of exact-match English placeholder counts.

Thanks to xet7 !
2026-10-13 22:26:18 +03:00
Lauri Ojansivu 126aaebb91 Translate stale Southern Sotho import instructions
Review source keys 814–929. Translate older English Todoist and Planner instructions, restore Jira pagination and Excel sheet guidance, and correct Taskwarrior and Focalboard label terminology. Add semantic checks and six browser cases.

Thanks to xet7 !
2026-10-13 22:24:42 +03:00
Lauri Ojansivu 1ec26bd3a7 Document Southern Sotho filter and import translation corrections
Record reviewed source keys, five guarded repairs and regression checks under Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 22:22:25 +03:00
Lauri Ojansivu 44cc4c30ee Repair Southern Sotho filter syntax and import guidance
Review source keys 590–813. Restore literal escape examples and plain-bullet Markdown import guidance; distinguish unused disk space from price and data fields from agricultural fields. Add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:21:35 +03:00
Lauri Ojansivu 6066341684 Document Southern Sotho archive and template translation review
Record the three repairs, regression coverage and remaining all-language work under Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 22:18:39 +03:00
Lauri Ojansivu e69d2c7311 Correct Southern Sotho archive help and template terminology
Review source keys 455–589, preserving valid translations. Repair the archive restore location, worker role and save-template action; add semantic and browser regression coverage.

Thanks to xet7 !
2026-10-13 22:17:53 +03:00
Lauri Ojansivu b8e3594acb Document Southern Sotho board and card control review
Record guarded meaning corrections and validation. Archive the oldest retained release by size and repartition October archives without changing released entries.

Thanks to xet7 !
2026-10-13 22:13:15 +03:00
Lauri Ojansivu 397ea42870 Repair Southern Sotho board and card control meanings
Review source keys 220 through 454 and fix nine misleading labels covering avatar limits, historical time, grouping, pulse, unread comments, due dates, account anonymization and card labels. Add guarded corrections and a read-only popup-title browser case.

Thanks to xet7 !
2026-10-13 22:12:22 +03:00
Lauri Ojansivu a0f42a284a Document Southern Sotho activity and control review
Record guarded meaning corrections, argument-order evidence, validation and remaining historical review.

Thanks to xet7 !
2026-10-13 22:09:25 +03:00
Lauri Ojansivu ef05274b3c Repair Southern Sotho activity and control meanings
Review the first 220 source strings and correct four semantic errors: reversed title arguments, automatic template application, the positive-integer height constraint and matching-card search scope. Record guarded repairs and extend the activity browser case.

Thanks to xet7 !
2026-10-13 22:08:47 +03:00
Lauri Ojansivu b22ca6b710 Document Southern Sotho full placeholder coverage
Record remaining recovery translations, full-inventory checks, pending fluency and UI validation, and the all-language backlog.

Thanks to xet7 !
2026-10-13 22:06:14 +03:00
Lauri Ojansivu 80df25497d Fill remaining Southern Sotho recovery translations
Translate the final 72 English placeholders and promote the full-inventory no-placeholder check. Preserve cancellation, discard and rollback constraints, already-applied changes and foreign-board protection. Register six existing recovery browser cases for Southern Sotho; fluency and runtime UI review remain pending.

Thanks to xet7 !
2026-10-13 22:05:37 +03:00
Lauri Ojansivu b6133bd072 Document Southern Sotho sync diagnostics translations
Record retention and estimate checks, browser registration and remaining translation work.

Thanks to xet7 !
2026-10-13 22:02:12 +03:00
Lauri Ojansivu b7fb228486 Translate Southern Sotho sync diagnostics and planning
Fill 45 source-report, retention, estimate-mapping, email-failure and planning placeholders. Preserve omitted-versus-null handling, access requirements and no-resume/no-undo warnings; extend semantic checks and the sync-planning browser case.

Thanks to xet7 !
2026-10-13 22:01:34 +03:00
Lauri Ojansivu 8bd53731c6 Document Southern Sotho Scrum import and sync translations
Record semantic checks, browser registration and remaining language work.

Thanks to xet7 !
2026-10-13 21:59:10 +03:00
Lauri Ojansivu 1128608ba5 Translate Southern Sotho Scrum import and sync guidance
Fill 50 placeholders, retaining no-duplicate import matching, unchanged unmatched cards, source read-only behavior, assigned-card review scope, local-copy preservation and retry reuse. Extend semantic checks and the existing Scrum import browser case.

Thanks to xet7 !
2026-10-13 21:58:32 +03:00
Lauri Ojansivu 296b41a478 Document Southern Sotho Scrum planning translations
Record report semantics, browser coverage registration and remaining language review.

Thanks to xet7 !
2026-10-13 21:56:12 +03:00
Lauri Ojansivu 59b761f444 Translate Southern Sotho Scrum planning and report guidance
Fill 75 planning, role, sprint, release and report placeholders. Preserve cancellation and rollover behavior, partial-report limits, unknown estimates and first-observation caveats. Extend semantic checks and the backlog paging browser case; Scrum terminology needs native review.

Thanks to xet7 !
2026-10-13 21:55:33 +03:00
Lauri Ojansivu 42776e5b1f Document Southern Sotho Blockly text translations
Record the Blockly placeholder gate, semantic checks and remaining language work.

Thanks to xet7 !
2026-10-13 21:52:59 +03:00
Lauri Ojansivu b6740c17a0 Translate Southern Sotho Blockly text and rule editor messages
Fill 115 text, shortcut, workspace and rule-editor placeholders. Preserve search keys, replacement scope, text indexing, concatenated whitespace and rule constraints. Add a full-inventory Blockly no-placeholder gate; descriptive programming terminology still needs native review.

Thanks to xet7 !
2026-10-13 21:52:26 +03:00
Lauri Ojansivu 66a67154d0 Document Southern Sotho Blockly procedure translations
Record mathematical and procedure checks, remaining review limits and translation inventory.

Thanks to xet7 !
2026-10-13 21:49:01 +03:00
Lauri Ojansivu b54fe95288 Translate Southern Sotho Blockly procedures and math controls
Fill 95 math, procedure, accessibility and shortcut placeholders. Verify random bounds, degrees rather than radians, return values, disabled definitions and screen-reader toggles. Mathematical and programming terminology remains lower confidence pending native review.

Thanks to xet7 !
2026-10-13 21:48:28 +03:00
Lauri Ojansivu d6e7a87532 Document Southern Sotho Blockly mathematics translations
Record comparison and numeric checks, native-review uncertainty and remaining language work.

Thanks to xet7 !
2026-10-13 21:45:55 +03:00
Lauri Ojansivu fb193ebb80 Translate Southern Sotho Blockly logic and mathematics
Fill 90 placeholders for list sorting, Boolean logic, arithmetic and statistics. Verify strict and inclusive comparisons, logical negation, copy behavior, constants and numeric bounds. Statistical and trigonometric terminology remains lower confidence pending native review.

Thanks to xet7 !
2026-10-13 21:45:13 +03:00
Lauri Ojansivu 37f1a98aae Document Southern Sotho Blockly list translations
Record semantic and placeholder validation, pending native and browser review, and remaining language inventory.

Thanks to xet7 !
2026-10-13 21:42:38 +03:00
Lauri Ojansivu c9fdb2f34c Translate Southern Sotho Blockly accessibility and list operations
Fill 110 placeholders for input labels, keyboard navigation and list operations. Check numbered tokens, dividend/divisor, get/remove distinctions, copy semantics, index direction and not-found behavior. Programming terminology remains lower confidence pending native review.

Thanks to xet7 !
2026-10-13 21:42:02 +03:00
Lauri Ojansivu 1774dcf0af Document Southern Sotho Blockly control translations
Record keyboard exemptions, token and semantic checks, native-review uncertainty and remaining translation backlog.

Thanks to xet7 !
2026-10-13 21:39:27 +03:00
Lauri Ojansivu 73e233a13b Translate Southern Sotho Blockly editor and control flow
Fill 75 editor, accessibility, colour and control-flow placeholders. Preserve 18 physical keyboard legends with exact-source exemptions and check numbered placeholders, loop conditions and bounds. Variable and programming terminology uses descriptive wording with lower confidence and needs native review.

Thanks to xet7 !
2026-10-13 21:38:51 +03:00
Lauri Ojansivu 84d85ecb21 Document Southern Sotho notification settings translations
Record semantic checks, browser registration and remaining translation work.

Thanks to xet7 !
2026-10-13 21:35:45 +03:00
Lauri Ojansivu 7c10778dac Translate Southern Sotho notification and automation settings
Fill 46 placeholders for delivery timing and grouping, workflow states, LDAP sync, card field visibility and subtask completion. Preserve token inventories and positive and negative permission semantics; extend notification and subtask browser cases.

Thanks to xet7 !
2026-10-13 21:35:07 +03:00
Lauri Ojansivu 04172e20f6 Document Southern Sotho import control translations
Record semantic validation, browser coverage registration and remaining translation inventory.

Thanks to xet7 !
2026-10-13 21:32:45 +03:00
Lauri Ojansivu d33245d26b Translate Southern Sotho import and webhook controls
Fill 55 placeholders for member mapping, CSV columns, batch import and export, Trello access, link rules and webhook payloads. Preserve substitution tokens, literal interchange markers and permission limits, and extend the existing CSV and member-mode browser cases.

Thanks to xet7 !
2026-10-13 21:32:06 +03:00
Lauri Ojansivu 6274990a6b Document Southern Sotho import instruction translations
Record format and exclusion checks, browser registration, protected Chinese values and remaining translation work.

Thanks to xet7 !
2026-10-13 21:29:31 +03:00
Lauri Ojansivu 42d5187bf6 Translate Southern Sotho import format instructions
Fill 22 instructions, retaining file formats, literal menu and column names, field mappings, first-board selection and excluded content. Extend semantic checks and register the existing import-help browser cases for Southern Sotho.

Thanks to xet7 !
2026-10-13 21:28:47 +03:00
Lauri Ojansivu 6ceef2e227 Document Southern Sotho card-link translations
Record validation and distinguish the pending-feature fills from the unchanged filtered backlog.

Thanks to xet7 !
2026-10-13 21:25:49 +03:00
Lauri Ojansivu c6a5484608 Translate Southern Sotho card attachment and field-link controls
Fill 34 English placeholders, preserving link directions, shared-field matching, unavailable-card privacy and edit permissions. Extend translation checks and both existing card interaction browser cases.

Thanks to xet7 !
2026-10-13 21:25:10 +03:00
Lauri Ojansivu eed374c9f9 Document completed Corsican source-ordered review
Record the final recovery and email label corrections, validation, protected Chinese values and remaining all-language backlog.

Thanks to xet7 !
2026-10-13 21:22:37 +03:00
Lauri Ojansivu e88d2ebc11 Complete Corsican catalog review with recovery label fixes
Replace five remaining Italian labels in recovery and email controls. Preserve existing translated recovery warnings and email permissions, and cover the refresh labels in the recovery browser case.

Thanks to xet7 !
2026-10-13 21:21:48 +03:00
Lauri Ojansivu 95692438c3 Document remaining Corsican Blockly review
Record reviewed operations, mathematical regression checks and pending Corsican Blockly browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 21:16:13 +03:00
Lauri Ojansivu 6d22052060 Review Corsican Blockly math and text operations
Align remaining physical keyboard names and workspace search shortcuts, repair sorting wording, and verify mathematical distinctions. Add Corsican to existing Blockly interaction coverage.

Thanks to xet7 !
2026-10-13 21:15:32 +03:00
Lauri Ojansivu cde9fa96d9 Document Corsican Blockly review and placeholder checks
Record preserved Blockly wording, corrected keyboard legends and map example, passing checks and pending browser execution.

Thanks to xet7 !
2026-10-13 21:12:47 +03:00
Lauri Ojansivu 0b753e316d Review Corsican Blockly strings and preserve keyboard legends
Preserve recognizable physical key names and numbered placeholder inventories, including multiplicity. Correct the map floor-plan example and add Corsican to the existing map browser suite.

Thanks to xet7 !
2026-10-13 21:11:59 +03:00
Lauri Ojansivu aa6ec76dcf Document Corsican flow-analysis translation coverage
Record full methodology translations, lower-confidence statistical terminology, passing checks and pending browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 21:09:15 +03:00
Lauri Ojansivu 0034964d9a Record single-file WeKan in Upcoming.
Thanks to xet7 !
2026-10-13 21:08:44 +03:00
Lauri Ojansivu 1b73b3a0b9 Restore full Corsican flow-analysis explanations
Replace abbreviated substitutes with forecast assumptions, missing-history behavior and date fallbacks. Repair WIP group and authentication wording and register five chart-help browser cases. Corsican statistical terminology is lower confidence and needs native review.

Thanks to xet7 !
2026-10-13 21:08:26 +03:00
Lauri Ojansivu 31e569a18e Single-file WeKan for every platform with a Node.js bundle.
One executable per platform: that platform's published bundle repackaged
as a Node.js single executable application whose ZIP Node 26.11 mounts as
a virtual file system (useVfs + vfsArchive). Only FerretDB is copied out,
once and verified, to wekan-files/bin; native addons load from the VFS.

Everything WeKan writes is in wekan-files beside the executable, which
WRITABLE_PATH points at: attachments, avatars, db, bin, backup, logs,
versions. WeKan now treats a WRITABLE_PATH named wekan-files as the files
root (models/lib/filesRoot.js, shared by every place that derived it).

Started in a terminal it shows a menu: start, stop, status, version info
as Admin Panel / Version shows it, update to the newest release or change
to any other (checksum verified, backed up first, the old executable
kept), backup to wekan-files/backup/YYYY-MM-DD_HH-MM-SS, run at boot
(systemd, launchd, Task Scheduler, cron, Termux:Boot), log, settings.
Every entry is also a command. start runs the start-wekan.sh loop:
recovery, requested restore, FerretDB, recovery page, WeKan, restart.

Meteor's main.js chdir()s into the bundle, which a VFS cannot be, so the
server role keeps a virtual working directory. Node 26.11.1 breaks
fs.writev/readv while any VFS is mounted; they are rebuilt on fs.write
and fs.read.

single-file.yml builds every target in releases/single-file/targets.json
from its verified bundle (Macs on a Mac, to re-sign), starts it where a
runner exists with browser tests on amd64, and attaches what started.
Release All and Release All Missing call it.

Thanks to xet7 !
2026-10-13 21:08:09 +03:00
Lauri Ojansivu 7073878380 Document Corsican account and import translation coverage
Record passing parser and translation regressions, conditional browser coverage and preserved Chinese values in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 21:04:12 +03:00
Lauri Ojansivu 105eb49d8c Correct Corsican account, report and import wording
Preserve repair limitations, acknowledgement scope and scoped-import choices. Test the native card-number search keyword and register conditional report-help browser coverage.

Thanks to xet7 !
2026-10-13 21:03:31 +03:00
Lauri Ojansivu bbecce3c50 Document Corsican monitoring translation coverage
Record passing regressions, preserved Chinese values and the current filtered translation backlog in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:59:51 +03:00
Lauri Ojansivu c1cc3c49cd Correct Corsican monitoring and job-control translations
Preserve migration thresholds, background behavior and literal storage names. Extend positive and negative translation regression coverage.

Thanks to xet7 !
2026-10-13 20:59:06 +03:00
Lauri Ojansivu e2320d1e4b Document Corsican board-migration translation coverage
Record repair conditions, passing regression checks and pending rendered validation in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:56:06 +03:00
Lauri Ojansivu f4d12f7b0f Correct Corsican board-migration and progress translations
Preserve duplicate-empty-list conditions, shared-list conversion ordering and independent repair actions. Extend positive and negative translation regression coverage.

Thanks to xet7 !
2026-10-13 20:55:16 +03:00
Lauri Ojansivu 494e7f5da2 Document Corsican cloud translation coverage
Record restore and cloud configuration repairs, passing regressions and pending browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:52:19 +03:00
Lauri Ojansivu 56dd1cbf08 Correct Corsican restore and cloud-storage translations
Preserve restore modes, credential alternatives, provider menu names and migration states. Extend language regressions and register cloud-help browser coverage.

Thanks to xet7 !
2026-10-13 20:51:35 +03:00
Lauri Ojansivu ce9e1e05df Document Corsican feature and backup translation coverage
Record corrected meanings, passing regressions and unavailable browser runtime in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:48:58 +03:00
Lauri Ojansivu cf03f236b5 Correct Corsican feature and backup guidance
Restore automatic loading instructions and the actual irreversible account-anonymization confirmation. Preserve feature scopes, backup boundaries and literal markup, with regression checks and browser help coverage.

Thanks to xet7 !
2026-10-13 20:48:13 +03:00
Lauri Ojansivu e422bbad24 Document Corsican migration translation coverage
Record guarded scheduling and storage repairs, passing regressions and pending browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:42:26 +03:00
Lauri Ojansivu f8c5783f41 Correct Corsican scheduling and migration translations
Preserve cloud product names, migration prerequisites and literal configuration commands. Extend regression checks and register migration-help browser coverage.

Thanks to xet7 !
2026-10-13 20:41:42 +03:00
Lauri Ojansivu 8e78cee2af Document Corsican recurrence and account translation checks
Record regression coverage, pending lockout browser execution and preservation of existing Chinese values in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:39:11 +03:00
Lauri Ojansivu 9cb558de5f Correct Corsican recurrence and account settings translations
Distinguish card recurrence from reset and known-user password failures from nonexistent usernames. Repair upload and support messages and register lockout-settings browser coverage.

Thanks to xet7 !
2026-10-13 20:38:31 +03:00
Lauri Ojansivu be7f8d5916 Document Corsican checklist and storage translation coverage
Record guarded repairs, passing checks and pending default-storage browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:35:44 +03:00
Lauri Ojansivu 588e39a31e Correct Corsican checklist and storage translations
Preserve item ordering, backend names, compaction warning and overtime meaning. Extend translation regressions and register default-storage browser coverage.

Thanks to xet7 !
2026-10-13 20:34:57 +03:00
Lauri Ojansivu c4bd61b171 Document Corsican report translation coverage
Record guarded repairs, regression checks and pending recovery-report browser execution in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:32:33 +03:00
Lauri Ojansivu 5c96e8b59e Correct Corsican report and ticket translations
Preserve reporting scope, recovery operations and technical literals. Add language regression checks and register recovery-report browser coverage.

Thanks to xet7 !
2026-10-13 20:31:40 +03:00
Lauri Ojansivu 785599fdb9 Document Corsican search and control translation checks
Record parser regression coverage, pending browser execution and preservation of existing Chinese wording in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:28:49 +03:00
Lauri Ojansivu 5fce837d4e Correct Corsican search help and dependency controls
Preserve query logic, proper names and map controls. Exercise translated ascending and descending sort predicates and register search-help browser coverage.

Thanks to xet7 !
2026-10-13 20:28:02 +03:00
Lauri Ojansivu 6cd487d4b7 Document Corsican search translation coverage
Record parser checks, pending browser execution and preservation of existing Chinese translations in Upcoming and the translation audit.

Thanks to xet7 !
2026-10-13 20:25:19 +03:00
Lauri Ojansivu d017d184dc Correct Corsican search keywords and instructions
Preserve executable search syntax and the direction of recent-date filters. Verify translated predicates with the search parser and register overdue-search browser coverage.

Thanks to xet7 !
2026-10-13 20:24:35 +03:00
Lauri Ojansivu a2a1a84a1a Record Corsican template and search translation audit in Upcoming 2026-10-13 20:18:41 +03:00
Lauri Ojansivu 930fd9fe2b Correct Corsican templates and search result wording
Preserve authorized-board and member-or-assignee scopes, result counts and parser abbreviations. Add guarded repairs and shared-template guidance coverage.
2026-10-13 20:17:51 +03:00
Lauri Ojansivu 6eead7c780 Record Corsican preferences and weekday translation audit in Upcoming 2026-10-13 20:14:39 +03:00
Lauri Ojansivu 0343ff48a0 Correct Corsican preferences, roles and weekday names
Preserve editor shortcuts, global-admin rights, linked-card deletion ordering and notification placeholders. Add guarded repairs and member-preference UI coverage.
2026-10-13 20:13:51 +03:00
Lauri Ojansivu 889da2fdb4 Record Corsican login and date translation audit in Upcoming 2026-10-13 20:10:28 +03:00
Lauri Ojansivu f2b4ea8984 Correct Corsican rule summaries, login settings and date activities
Preserve authentication prerequisite identifiers, old and new date values, deletion warnings and directional placement. Add guarded repairs and passwordless-settings UI coverage.
2026-10-13 20:09:22 +03:00
Lauri Ojansivu b098519df6 Record Corsican date-trigger translation audit in Upcoming 2026-10-13 20:06:22 +03:00
Lauri Ojansivu ed0c2c3ffc Correct Corsican date triggers and action fragments
Restore set-or-changed date conditions and assignee trigger wording. Preserve checklist negations and destination direction, with guarded repairs and date-trigger UI coverage.
2026-10-13 20:05:41 +03:00
Lauri Ojansivu fbf8b30069 Record Corsican workflow translation audit in Upcoming 2026-10-13 20:03:18 +03:00
Lauri Ojansivu 2c787355da Correct Corsican automation workflow and scheduling labels
Preserve import limitations and unmapped-node reporting, trigger/action distinctions, scheduling direction and completion negation. Extend guarded repairs and workflow UI coverage.
2026-10-13 20:02:30 +03:00
Lauri Ojansivu 24bd386ad9 Record Corsican hierarchy and rule translation audit in Upcoming 2026-10-13 19:59:31 +03:00
Lauri Ojansivu 6ad1329613 Correct Corsican card hierarchy, activity and rule labels
Preserve card-parent paths, movement direction, checklist counts and activity placeholders. Add guarded repairs and an empty-rules-page browser case.
2026-10-13 19:58:37 +03:00
Lauri Ojansivu d175d4e463 Record Corsican organization translation audit in Upcoming 2026-10-13 19:56:05 +03:00
Lauri Ojansivu 1eb90ef43f Correct Corsican invitation and organization administration wording
Preserve invitation tokens, tenant routing configuration, organization-admin permission boundaries and permanent board deletion warnings. Extend guarded repairs and custom-field UI coverage.
2026-10-13 19:55:25 +03:00
Lauri Ojansivu 5da4c323b5 Record Corsican upload and attachment translation audit in Upcoming 2026-10-13 19:52:12 +03:00
Lauri Ojansivu 84e25df892 Correct Corsican uploads and attachment limit wording
Preserve distinct upload and download limits, separate API buffering safety caps, WIP recovery choices and notification scope. Restore invariant unit labels and add attachment-limit UI coverage.
2026-10-13 19:51:25 +03:00
Lauri Ojansivu e756dc369c Record Corsican settings and shortcut translation audit in Upcoming 2026-10-13 19:48:25 +03:00
Lauri Ojansivu 6f2e4740ab Correct Corsican settings, shortcuts and membership wording
Preserve member-removal notifications, unsaved-description recovery and overtime semantics. Keep the Pomodoro technique name unchanged and add guarded repairs and shortcut UI coverage.
2026-10-13 19:47:33 +03:00
Lauri Ojansivu d8ab7af7e9 Record Corsican board and member translation audit in Upcoming 2026-10-13 19:43:21 +03:00
Lauri Ojansivu 01e3a9542e Correct Corsican board, archive and member controls
Distinguish personal overrides from cancellation and retain assigned-only access, notification conditions and deletion warnings. Add guarded repairs and an archive-tab browser case.
2026-10-13 19:42:31 +03:00
Lauri Ojansivu 87918c29fb Record Corsican Trello translation audit in Upcoming 2026-10-13 19:39:48 +03:00
Lauri Ojansivu b03a4afca6 Correct Corsican Trello import and cancellation wording
Preserve archive limits, optional workspace placement, member mapping and irreversible deletion scope. Add guarded repairs and Trello import UI coverage.
2026-10-13 19:39:05 +03:00
Lauri Ojansivu 9725a4d617 Record Corsican filter and importer audit in Upcoming 2026-10-13 19:35:43 +03:00
Lauri Ojansivu 8b9c88ba25 Correct Corsican filtering and import guidance
Translate stale English Todoist and Planner instructions against current source. Preserve importer mappings, field names and attachment limits, and add six importer browser cases.
2026-10-13 19:34:45 +03:00
Lauri Ojansivu b1e4363759 Record Corsican field and account translation audit in Upcoming 2026-10-13 19:31:21 +03:00
Lauri Ojansivu 4f89cd92e3 Correct Corsican field, account and export wording
Keep email placeholders and account actions distinct, preserve role restrictions and explain that enabling permanent deletion does not itself delete content. Add guarded repairs and custom-field UI coverage.
2026-10-13 19:30:26 +03:00
Lauri Ojansivu cc41a7af66 Record the BSD bundles in Upcoming.
Thanks to xet7 !
2026-10-13 19:26:05 +03:00
Lauri Ojansivu 58f8b67904 BSD: existing Node.js patch sets per BSD, and OpenBSD/NetBSD bundle builds ready.
docs/Platforms/FOSS/OS/BSD.md now lists the patches each BSD's packagers
keep for Node.js - FreeBSD ports www/node26, OpenBSD ports lang/node,
pkgsrc lang/nodejs (already 26.11.1), DragonFly DeltaPorts - and what WeKan
does with each, plus Bun, sharp/libvips, bcrypt and FerretDB on each BSD, and
how to install on OpenBSD (under /usr/local, wxallowed) and NetBSD (paxctl).

bsd.yml: the OpenBSD job mounts its workspace wxallowed before starting the
bundle there, and the NetBSD job compiles the native modules with pkgsrc's
GCC 14, as Node 26 needs C++20. Both build by themselves once wekan/node-
patches publishes node-openbsd-* and node-netbsd-x64.

Thanks to xet7 !
2026-10-13 19:25:44 +03:00
Lauri Ojansivu c337bb3896 Record Corsican navigation translation audit in Upcoming 2026-10-13 19:22:33 +03:00
Lauri Ojansivu dbaa663280 Correct Corsican navigation, color and role translations
Restore directional controls, comment and worker permission limits, and the deliberately empty comment placeholder. Preserve newer translations through guarded repairs and extend the color-rule browser case.

Specialized color shade paraphrases are lower confidence and remain open to native-speaker refinement.
2026-10-13 19:21:30 +03:00
Lauri Ojansivu 79a82b4a2f Document Corsican profile translation audit in Upcoming 2026-10-13 19:18:00 +03:00
Lauri Ojansivu 18278cce84 Correct Corsican profile and appearance wording
Preserve imported roles and permission limits, favorite toggles, automatic width and card inactivity thresholds. Add guarded repairs and regression coverage for profile and font controls.
2026-10-13 19:17:09 +03:00
Lauri Ojansivu 1cbd7df31f WeKan bundles for the BSDs: FreeBSD Node.js bundle, and the plan for each BSD.
docs/Platforms/FOSS/OS/BSD.md says where FreeBSD, OpenBSD, NetBSD and
DragonFly BSD stand - the Bun bundles FreeBSD already has, which FerretDB
and Node.js binaries exist for each, and the plan, easiest first.

.github/workflows/bsd.yml, called by Release All, builds WeKan's Node.js
bundle for a BSD: it takes the release's amd64 bundle, verifies it and adds
that BSD's Node.js (wekan/node-patches) and FerretDB, then inside a VM of
that BSD compiles the native modules, runs the same bundle steps as every
other platform, and starts the bundle with its own start-wekan.sh, which
must answer HTTP 200 before the zip is attached. FreeBSD x64 builds now,
from node-freebsd-x64 and ferretdb-freebsd-amd64. FreeBSD arm64, OpenBSD
amd64/arm64 and NetBSD amd64 are skipped, not failed, until node-patches
publishes their Node.js. The Node.js resolver, the native addon ELF check
and the platform packages step know the BSD names.

Thanks to xet7 !
2026-10-13 19:16:40 +03:00
Lauri Ojansivu a566e2cd0d Record Corsican card and Planning Poker review 2026-10-13 19:10:02 +03:00
Lauri Ojansivu a63e83ec5e Correct Corsican card controls and Planning Poker scores
Repair 47 mixed-language or misleading values, including literal Planning Poker scores and destructive-action warnings. Regional prose remains provisional pending fluent review.
2026-10-13 19:09:21 +03:00
Lauri Ojansivu fb6391d0e3 Record Corsican board view and timeline review 2026-10-13 19:06:24 +03:00
Lauri Ojansivu b61c0df17f Correct Corsican board view and timeline labels
Repair 26 values, retain literal Gantt naming and restore Gregorian ISO week and timeline meanings. Regional phrasing remains provisional pending fluent review.
2026-10-13 19:05:41 +03:00
Lauri Ojansivu 7b7269bd14 Record Corsican board display wording review 2026-10-13 19:03:11 +03:00
Lauri Ojansivu b1c1c68040 Correct Corsican board display and attachment warnings
Repair 27 mixed-language values, retaining offline data-loss warnings and archive versus deletion semantics. Regional phrasing remains provisional pending fluent review.
2026-10-13 19:02:15 +03:00
Lauri Ojansivu 49fb7219d3 Record Corsican workspace and card control review 2026-10-13 18:59:25 +03:00
Lauri Ojansivu 9869b00d25 Correct Corsican workspace and card controls
Repair 37 mixed-language values while preserving selection limits, Home-board retention and numeric validation. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:58:35 +03:00
Lauri Ojansivu 30f7c19d3f Record Corsican repair and recovery wording review 2026-10-13 18:55:25 +03:00
Lauri Ojansivu d7c43eeb4b Correct Corsican repair and recovery guidance
Repair 20 mixed-language values, including account anonymization and recovery scope. Retain literal identifiers and reversibility limits. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:54:34 +03:00
Lauri Ojansivu 7937697628 Record bounces from the other sending services in Upcoming.
Thanks to xet7 !
2026-10-13 18:54:28 +03:00
Lauri Ojansivu 57e05c4304 Email bounces from Mailgun, SendGrid, Postmark, Brevo, Mailjet, SparkPost, Mandrill and Resend.
Besides Amazon SES, WeKan now takes the bounce and spam-complaint
webhooks of the other common sending services, at
/api/inbound-email/bounces/<service>, each proven the way that service
signs: Mailgun's HMAC-SHA256, SendGrid's ECDSA signature over the raw body,
Mandrill's HMAC-SHA1 over the URL and fields, Resend's Svix signature; and
for Postmark, Brevo, Mailjet and SparkPost, which do not sign, a password
in the webhook address (Basic auth or ?secret=). Each service's events
become WeKan's hard, soft, automatic-reply and complaint categories, so the
same Admin Panel rules act on them. Bounces -> Other sending services
turns each on with its key or password, written only when typed and never
shown back; a forged or unsigned request is refused and logged in Problems
-> Security. These endpoints, like the SES one, work without WITH_API.

Mail systems with no webhook keep working as before: their bounce emails
reach WeKan by IMAP and are recognised as delivery status notifications.

Thanks to xet7 !
2026-10-13 18:54:06 +03:00
Lauri Ojansivu a7da9876df Record Corsican storage and migration wording review 2026-10-13 18:52:32 +03:00
Lauri Ojansivu 78d684e364 Correct Corsican storage migration and avatar guidance
Repair 25 mixed-language values while preserving migration preconditions, retained data and irreversible deletion warnings. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:51:50 +03:00
Lauri Ojansivu 3a9725ca42 Record Corsican administrative wording review 2026-10-13 18:49:26 +03:00
Lauri Ojansivu 1e10d606b5 Correct Corsican administrative and storage wording
Repair 25 mixed-language values while preserving roles, shared-template eligibility, search conjunction and storage scope. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:48:41 +03:00
Lauri Ojansivu 1b7dd9eb8c Record Corsican settings and confirmation review 2026-10-13 18:45:43 +03:00
Lauri Ojansivu f4e5519c87 Correct Corsican settings rules and deletion confirmations
Repair 25 mixed-language values with exact token and semantic checks. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:44:53 +03:00
Lauri Ojansivu ff160f28b7 Record Corsican filter and access wording corrections 2026-10-13 18:42:23 +03:00
Lauri Ojansivu e7a4eb0ffd Correct Corsican filter help and access guidance
Repair 20 mixed-language values, preserve exact filter examples and distinguish public visibility, editing rights and Sandstorm access revocation. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:41:41 +03:00
Lauri Ojansivu dfa02cd8c9 Record Corsican board wording and importer corrections 2026-10-13 18:38:58 +03:00
Lauri Ojansivu d07120761b Correct Corsican board warnings and import guidance
Repair 33 mixed-language values and restore Jira pagination and Excel mapping instructions. Regional wording remains provisional pending fluent review.
2026-10-13 18:38:10 +03:00
Lauri Ojansivu b7bbbdbdf7 Record Corsican permission wording corrections 2026-10-13 18:35:12 +03:00
Lauri Ojansivu 724fd0730d Correct Corsican membership permissions and board controls
Repair 37 mixed-language labels and descriptions, including account anonymization and logged-in user access. Preserve role restrictions and archive semantics. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:34:25 +03:00
Lauri Ojansivu 2453838acd Record Corsican notification wording review 2026-10-13 18:31:32 +03:00
Lauri Ojansivu f7e256b997 Correct Corsican activity notification wording
Replace 39 mixed-language values with guarded repairs, preserving direction and placeholder case. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:30:32 +03:00
Lauri Ojansivu 15eb5b7910 Record email bounces, Problems -> Email and the runtime tests in Upcoming.
Thanks to xet7 !
2026-10-13 18:27:44 +03:00
Lauri Ojansivu 79ac301675 Email bounces with Amazon SES, Problems -> Email, and runtime tests for sending and receiving.
Email that comes back is handled in WeKan instead of landing in the
administrator's own inbox. Admin Panel -> E-mail -> Bounces: hard bounces
(a mistyped invited address), soft bounces, automatic replies and spam
complaints, each with actions done after a count - list it, stop emailing
the address, tell who invited it, delete the invitation, ignore - as
listmonk and Discourse do it, and the list of addresses that bounced, which
an admin clears to email them again. Amazon SES Bounce and Complaint
notifications and mail SES receives arrive through SNS at
/api/inbound-email/ses, signature-checked against Amazon's certificate and
accepted only from the topics the Admin Panel lists. Delivery status
notifications and out-of-office replies arriving by IMAP, a provider
webhook or a Sandstorm grain are bounces too, never cards.

Admin Panel -> Problems -> Email shows what went wrong sending or
receiving: a send the mail server refused or could not reach, an IMAP
mailbox that cannot be read (never with its password), an unreadable
email, an email for a board that no longer takes email. Attempts stay in
Problems -> Security. The inbound email endpoints no longer need the whole
REST API turned on.

Runtime tests run WeKan's own transports against local SMTP and IMAP
servers over TCP, a real Sandstorm Maildir, the webhook over HTTP and
signed SNS messages; UI tests drive the Email and Chat views, the
email-to-card settings and the Bounces page. They found and this fixes:
every whole-message parse failed in the server bundle (nodemailer's
addressparser resolved to its ESM build), the Chat view's drag and drop
never started (dragscroll took the mouse) and dropped a card into the
target's previous list, and the Chat panes did not follow a change to a
right-to-left language.

Thanks to xet7 !

Fixes #794,
Fixes #3948.
2026-10-13 18:27:25 +03:00
Lauri Ojansivu b5250c6fae Record Corsican activity wording corrections 2026-10-13 18:26:04 +03:00
Lauri Ojansivu e3c22d9e92 Correct mixed Italian wording in Corsican activity messages
Record 22 exact before/after repairs, preserve placeholder inventories and distinguish checked and unchecked actions. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:25:21 +03:00
Lauri Ojansivu cc416e5ab0 Record Corsican importer translation inventory 2026-10-13 18:22:09 +03:00
Lauri Ojansivu 9198e84b39 Complete Corsican import instructions and filtered fill inventory
Preserve format literals, import mappings and unsupported-data warnings. Regional phrasing remains provisional pending fluent review; older mixed-language values remain to be corrected.
2026-10-13 18:21:31 +03:00
Lauri Ojansivu 4c7660d2de Record Corsican import export and controls translation progress 2026-10-13 18:18:23 +03:00
Lauri Ojansivu 342e479ed9 Translate Corsican import export and board settings controls
Fill 65 English values with token and semantic checks and extend seven browser workflows. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:17:29 +03:00
Lauri Ojansivu 28e9e9d61f Record Corsican notification translation progress 2026-10-13 18:14:17 +03:00
Lauri Ojansivu 877a3a7e81 Translate Corsican notification delivery and webhook settings
Preserve inherited defaults, identity omission, grouping and quiet-hour waiting. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:13:00 +03:00
Lauri Ojansivu 26a6176270 Record Corsican recovery translation progress 2026-10-13 18:10:58 +03:00
Lauri Ojansivu ef77f5a3f3 Translate Corsican synchronization and import recovery messages
Preserve partial-data retention, irreversible discard warnings and recovery refusal reasons. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:10:10 +03:00
Lauri Ojansivu d7c606c29e Record Corsican card-link translation progress 2026-10-13 18:07:10 +03:00
Lauri Ojansivu cdb2405e93 Translate Corsican card attachments and linked fields
Fill 30 English placeholders and cover link direction, edit permission, paused links and inaccessible cards. Regional phrasing remains provisional pending fluent review.
2026-10-13 18:06:13 +03:00
Lauri Ojansivu eb1b966225 Record completed Swahili importer translation inventory 2026-10-13 18:02:57 +03:00
Lauri Ojansivu d963a78bf4 Complete Swahili import instructions and current fill inventory 2026-10-13 18:01:44 +03:00
Lauri Ojansivu 6d88440dd3 Record Swahili export and settings translation progress 2026-10-13 17:54:25 +03:00
Lauri Ojansivu e02fd383b5 Translate Swahili export guidance and settings controls 2026-10-13 17:53:39 +03:00
Lauri Ojansivu 7d8939271e Record Swahili control translation progress 2026-10-13 17:50:28 +03:00
Lauri Ojansivu 387b88c959 Translate Swahili notification, mapping and Scrum controls 2026-10-13 17:49:44 +03:00
Lauri Ojansivu 9a6f00e927 Record Swahili card-link and recovery translation progress 2026-10-13 17:46:13 +03:00
Lauri Ojansivu c4f4e465f2 Translate Swahili card links and recovery decisions 2026-10-13 17:45:29 +03:00
Lauri Ojansivu 3a459697f2 Record Maltese exempt-prose audit and repairs 2026-10-13 17:40:28 +03:00
Lauri Ojansivu 6d5ae79420 Translate Maltese storage prose and short labels excluded from fill reports 2026-10-13 17:39:31 +03:00
Lauri Ojansivu a26aa63978 Record Maltese filtered inventory completion and remaining review 2026-10-13 17:32:50 +03:00
Lauri Ojansivu f26aaac01e Complete Maltese import-format instructions and current fill inventory 2026-10-13 17:31:44 +03:00
Lauri Ojansivu 54dfa466df Record Maltese settings translation progress 2026-10-13 17:27:50 +03:00
Lauri Ojansivu ce113b5a50 Translate Maltese settings and workflow rules 2026-10-13 17:26:56 +03:00
Lauri Ojansivu 208f43bab3 Record Maltese export translation progress 2026-10-13 17:24:13 +03:00
Lauri Ojansivu bbe5b139d2 Translate Maltese batch import and export guidance 2026-10-13 17:23:27 +03:00
Lauri Ojansivu 2892fdcd3d Record Maltese Scrum translation progress 2026-10-13 17:20:42 +03:00
Lauri Ojansivu c87fcb37ec Translate Maltese Scrum import and subtask controls 2026-10-13 17:20:02 +03:00
Lauri Ojansivu 95c1ad0304 Record Maltese import mapping translation progress 2026-10-13 17:17:21 +03:00
Lauri Ojansivu f0c5c03cb1 Translate Maltese import people and column mapping controls 2026-10-13 17:16:25 +03:00
Lauri Ojansivu 5a4cc61ba5 Record Maltese notification translation progress 2026-10-13 17:14:04 +03:00
Lauri Ojansivu 1e6b6340df Translate Maltese notification delivery and planning synchronization 2026-10-13 17:13:18 +03:00
Lauri Ojansivu d1e226e867 Record Maltese recovery translation progress 2026-10-13 17:09:09 +03:00
Lauri Ojansivu 57a10d1ac9 Translate Maltese recovery decisions and data-retention warnings 2026-10-13 17:08:15 +03:00
Lauri Ojansivu af8342d859 Record Maltese card-link translation progress 2026-10-13 17:04:13 +03:00
Lauri Ojansivu 60528034c1 Translate Maltese attached-card and linked-field messages
Fill 30 English placeholders while retaining existing translations. Verify matching fields, propagation direction, edit permission, unavailable cards and self-link refusal. Extend the existing attachment and field-link browser flows to Maltese.
2026-10-13 17:03:12 +03:00
Lauri Ojansivu 50f00961c5 Record Khmer and regional time-history translation review 2026-10-13 16:58:01 +03:00
Lauri Ojansivu 1a7fa67d94 Restore time-history meanings in nine more catalog paths
Complete Khmer and its aliases, Burmese, Amharic, Yiddish, Bhojpuri, Maithili and Konkani notes. Preserve the Khmer symlink, map its browser tag, and check Yiddish RTL layout.

Technical prose in these languages is provisional pending fluent review. Preserve existing Chinese values and unrelated new feature translations.
2026-10-13 16:56:59 +03:00
Lauri Ojansivu 767d469750 Record the Email and Chat board views in Upcoming.
Thanks to xet7 !
2026-10-13 16:56:52 +03:00
Lauri Ojansivu ddb33d21d0 Board views Email and Chat.
Email shows the board as a Gmail-like mailbox in the board's own theme:
the Inbox is the list email to card puts new cards in, Sent and All mail
follow the cards' email, the board's lists are folders and its labels are
Gmail labels, coloured on every message. The messages are the shared
table page, newest first, paged on the server; a message opens as the
card popup, its comments the conversation. Compose and Reply send From
the member's own board address, so the answer returns to this board, and
by In-Reply-To to the same card as a comment.

Chat shows the board as a Slack / Rocket.Chat workspace in the current
theme: a tree of Workspaces, boards, swimlanes, lists and cards as
channels; the selected card in the middle; its comments as chat on the
other side. Tree | card | chat left to right, mirrored right to left.
Each pane folds, the two borders drag (and move with the arrow keys),
and widths and folds are saved on the member's profile. Swimlanes, lists,
cards, workspaces and boards reorder by drag and drop through the same
paths the board and All Boards use.

The new strings are translated in every locale.

Thanks to xet7 !
2026-10-13 16:56:29 +03:00
Lauri Ojansivu 56eefe8a76 Record eight further time-history translation repairs 2026-10-13 16:51:09 +03:00
Lauri Ojansivu 021d016f5d Restore time-history explanations in eight more catalogs
Use Hiragana for the Japanese Hiragana catalog and its registered browser language tag. Extend direction checks to Sorani, Pashto, Sindhi and Uyghur. Record exact before/after repairs and all four source distinctions.

Kurmanji, Sorani, Pashto, Sindhi, Uyghur, Hausa and Somali technical phrasing is provisional pending fluent review. Existing Chinese values remain unchanged.
2026-10-13 16:50:05 +03:00
Lauri Ojansivu 5e90f13750 Record email to card in Upcoming.
Thanks to xet7 !
2026-10-13 16:47:45 +03:00
Lauri Ojansivu 988b177e59 Email to card: board addresses, IMAP mailboxes, provider webhooks and Sandstorm grain email.
An email to a member's personal board address, board+<token>@<domain>,
becomes a card on that board, as Trello's email-to-board does (#794,
#3948). The subject is the title; the body (quoted replies stripped, HTML
as text) is the description, with a From line; attachments are attached
(no SVG, as for uploads). The author is the address's member, never the
From address, and they must still have write access when the email
arrives. A reply whose In-Reply-To/References matches a card's thread
becomes a comment on it.

Notification Settings, at three levels: the Admin Panel turns it on and
holds the domain, the provider webhook secret and the IMAP mailboxes with
their passwords (never published, never sent back to a browser); a board
admin turns the board on and picks the list and swimlane; board and
member choose which parts of an email are used. The member sees and
replaces their own address.

It arrives by POST /api/inbound-email (Mailgun, SendGrid, Postmark, JSON,
forms, multipart with attachments, raw MIME), by IMAP polling with
imapflow and mailparser under a database lease so one replica polls, or
on Sandstorm through the grain's own address: the bridge's Maildir is
read, a member's board token in the subject routes it, and WeKan's
outgoing email goes through HackSessionContext.send, since a grain has no
network. The email and hack-session capnp schemas are generated with the
other Sandstorm bindings.

Unknown addresses, wrong webhook secrets and over-limit addresses are
logged under authn.inbound-email. Rate limit per address, size limits,
Message-ID deduplication. The new strings are translated in every locale.

Thanks to xet7 !
2026-10-13 16:47:16 +03:00
Lauri Ojansivu f08367b5cf Record Chinese email translation review and refreshed backlog 2026-10-13 16:43:57 +03:00
Lauri Ojansivu f84a8e3a83 Verify Chinese email-to-card translation meanings and controls 2026-10-13 16:42:41 +03:00
Lauri Ojansivu 706c9f34c1 Record regional time-history translation review 2026-10-13 16:27:26 +03:00
Lauri Ojansivu c6aa8490d0 Complete time-history explanations in 23 more catalogs
Regional technical prose in Faroese, Frisian, Scottish Gaelic, Aragonese, Corsican, Sardinian and Sicilian remains provisional pending fluent review.
2026-10-13 16:25:29 +03:00
Lauri Ojansivu f1505aebd0 Record further time-history language repairs 2026-10-13 16:14:29 +03:00
Lauri Ojansivu 44d8210a69 Restore time-history meaning in 48 more catalogs 2026-10-13 16:12:52 +03:00
Lauri Ojansivu 2c9bfdb500 Record time-history language repairs and refreshed translation backlog 2026-10-13 16:04:38 +03:00
Lauri Ojansivu 3fe68ad266 Restore complete time-history explanations in 54 catalogs 2026-10-13 16:02:05 +03:00
Lauri Ojansivu 16926454f7 Record Uzbek terminology repairs and archive the oldest retained release 2026-10-13 15:54:56 +03:00
Lauri Ojansivu 6c10d035c2 Correct Uzbek assigned-person labels and time-history explanation 2026-10-13 15:53:32 +03:00
Lauri Ojansivu 7b312dd563 Record Latin Uzbek fills and remaining language review 2026-10-13 15:49:13 +03:00
Lauri Ojansivu 6825f00ac7 Fill Latin Uzbek feature and recovery translations 2026-10-13 15:47:42 +03:00
Lauri Ojansivu 9be8b3b087 Record the planned email, Sandstorm, Android and single-file work in TODO Later.
Thanks to xet7 !
2026-10-13 15:39:42 +03:00
Lauri Ojansivu 8c7973e0a9 Record Azerbaijani feature fills and remaining translation review 2026-10-13 15:37:33 +03:00
Lauri Ojansivu 224b4cac07 CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.

Thanks to xet7 !
2026-10-13 15:37:23 +03:00
Lauri Ojansivu 9d8a48c4d8 Fill Azerbaijani feature, recovery and accessibility strings 2026-10-13 15:35:40 +03:00
Lauri Ojansivu 31932e08ab Record Azerbaijani terminology repairs in Upcoming 2026-10-13 15:24:21 +03:00
Lauri Ojansivu 30a92983f5 Correct Azerbaijani board and assigned-person terminology
Repair 38 values in each of the az, az-AZ and az-LA catalogs. Use lövhə for a Kanban board rather than şura (council), distinguish the assigned person, and correct the card-to-board command. Preserve query literals, invitation tokens and visibility emphasis; keep activity predicates compatible with the actor prefix.

Record all 114 exact corrections. Terminology and correction-ledger checks pass, including newer-translation preservation. Three browser cases register but have not been executed.

Thanks to xet7 !
2026-10-13 15:19:53 +03:00
Lauri Ojansivu 1376c99ca2 Record Malayalam fills and Scrum recovery review in Upcoming 2026-10-13 15:06:52 +03:00
Lauri Ojansivu 0ae7f04e55 Fill Malayalam feature strings and correct rule linking-verb insertion
Translate 223 current placeholders and thirteen exempt labels, preserving existing non-English values. Cover Scrum import matching, first-sync preservation, conflicts and recovery consequences. Omit the redundant linking verb before already inflected Malayalam predicates and share the Kannada/Malayalam browser check.

Six focused suites and exact source-token checks pass. Nine Malayalam browser cases register; execution and broader native wording review remain open.

Thanks to xet7 !
2026-10-13 15:05:33 +03:00
Lauri Ojansivu 9c8d74c955 Record Urdu fills and RTL verification limits in Upcoming 2026-10-13 14:51:09 +03:00
Lauri Ojansivu 762bb1df85 Fill Urdu feature strings and extend RTL browser coverage
Translate 193 current English placeholders while preserving existing non-English text. Verify permissions, import exclusions, recovery consequences, technical identifiers and mixed-direction interpolation. Register Urdu feature flows and five existing RTL layout cases.

Longer technical wording remains provisional pending speaker review; browser execution is still outstanding.

Thanks to xet7 !
2026-10-13 14:50:09 +03:00
Lauri Ojansivu 5d839ea1a5 Record Kannada fills and rule grammar review in Upcoming 2026-10-13 14:42:43 +03:00
Lauri Ojansivu 10c4cc3bfa Fill Kannada feature strings and omit redundant rule linking verb
Translate 193 current placeholders and nine labels hidden by fill exemptions. Preserve existing non-English values. Omit the separate linking verb before already inflected Kannada action predicates; retain the helper behavior for other languages. Use numerator/denominator notation for fixed-order progress counts.

Check permissions, import exclusions, recovery consequences, source literals, locale boundaries and runtime interpolation. Register six Kannada browser cases. Longer technical wording and full rule clause grammar remain open to speaker review.

Thanks to xet7 !
2026-10-13 14:41:25 +03:00
Lauri Ojansivu 02cfa64e83 Record Thai translation progress and remaining audit scope 2026-10-13 14:27:38 +03:00
Lauri Ojansivu 0a7eba3e4e Fill Thai feature strings and untranslated byte unit
Translate 193 current feature placeholders and the exempt Bytes label, preserving all existing non-English values. Check field directions, permissions, import exclusions, recovery consequences, source literals and runtime interpolation. Register five Thai browser cases for the affected feature flows.

Longer technical wording remains provisional pending speaker review.

Thanks to xet7 !
2026-10-13 14:26:00 +03:00
Lauri Ojansivu 7e4a409db2 Add Trello export, Sandstorm Powerbox and release build fixes to ChangeLog.
Thanks to xet7 !
2026-10-13 14:21:32 +03:00
Lauri Ojansivu ccff887734 Find release assets reliably before wrapping or building them.
Both v12.39 Mac apps were skipped although their bundles were attached:
`gh release view ... | grep -q` let grep exit at the first match while gh
was still writing, gh got SIGPIPE, pipefail failed the pipeline, and the
job said the bundle had not been published. The Windows EXE, Docker image
and snap-launchpad checks had the same shape. Each now reads the asset
list whole before matching it.

Thanks to xet7 !
2026-10-13 14:20:57 +03:00
Lauri Ojansivu 121bf673a5 Keep waiting for a Launchpad snap build through network blips.
The armhf and s390x builds succeeded, then the runner's connection to the
Launchpad API dropped or a poll hung, and snapcraft ended each attempt as
"Network is unreachable" or "Timed out waiting for build". A failed
build-state poll is now retried for up to 30 minutes; other errors and the
workflow's own deadline are unchanged.

Thanks to xet7 !
2026-10-13 14:20:57 +03:00
Lauri Ojansivu 13c9bd27d5 Build Sharp natively for the FreeBSD Bun bundles.
Both FreeBSD jobs stopped at "native check: sharp": Sharp's only FreeBSD
prebuilt is WebAssembly with Worker threads, and under Bun on FreeBSD the
first image operation never returned, while the same build works under
Bun on Linux. Sharp is now compiled against the system libvips there, as
on Android, and the FreeBSD VM installs vips.

Thanks to xet7 !
2026-10-13 14:20:57 +03:00
Lauri Ojansivu 88d00e02f1 Export boards to Trello, keep swimlanes as labels, and reach Trello from Sandstorm.
Export to Trello: Board Settings -> Export -> Trello creates the board in
Trello through its API with the saved key and token: labels, lists, cards,
checklists, comments, attachment files, and archived lists and cards. It
runs as a server job from the board export, with the same access check.

Swimlanes: Trello has none, so each swimlane is exported as a label named
"Swimlane: <title>". The Swimlanes by Blue Cat Power-Up groups Trello cards
into rows by label, and every Trello import turns those labels back into
swimlanes.

Sandstorm: a grain has no network of its own. Allow access to Trello opens
the Sandstorm Powerbox for https://api.trello.com/1, like the Sandstorm
WordPress app reaches wordpress.org; the access token is kept per user on
the server, and Trello import and export go through the bridge proxy.

Thanks to xet7 !
2026-10-13 14:20:56 +03:00
Lauri Ojansivu 5e82284450 Record Tamil translation fills and calendar review in Upcoming 2026-10-13 14:19:34 +03:00
Lauri Ojansivu 5de70e0399 Fill Tamil feature strings and correct calendar spelling
Translate 193 current feature placeholders and the exempt Bytes label. Correct four existing calendar labels using the long vowel already used elsewhere in the catalog, with exact ledger entries. Cover permission direction, import exclusions, recovery consequences, Trello markers and runtime interpolation; register Tamil feature and calendar browser cases.

Longer technical wording remains provisional pending speaker review.

Thanks to xet7 !
2026-10-13 14:18:20 +03:00
Lauri Ojansivu e9b0de11df Record Nepali and new Trello translation progress in Upcoming 2026-10-13 14:06:44 +03:00
Lauri Ojansivu 66bd077c36 Translate Marathi Trello API labels and cover new permission wording
Fill eleven new Marathi strings without changing existing values. Extend Bengali checks for the concurrently supplied translations and register Bengali and Marathi export-dialog browser cases.

Thanks to xet7 !
2026-10-13 14:04:50 +03:00
Lauri Ojansivu 9384c350fa Fill Nepali feature strings and missing Chinese Trello labels
Translate 193 Nepali placeholders and 11 new Trello/Sandstorm strings in each of nine Chinese catalog paths. Preserve all existing non-English values, including every earlier zh translation. Check import exclusions, permissions, recovery consequences, literal swimlane markers, exact tokens and runtime interpolation. Register browser coverage without contacting Trello.

Thanks to xet7 !
2026-10-13 14:01:22 +03:00
Lauri Ojansivu db282dd372 Record eight-language storage translation progress
Update Upcoming, audit evidence and the supplemental review count while keeping broader language review and browser execution open.

Thanks to xet7 !
2026-10-13 13:43:49 +03:00
Lauri Ojansivu 49b1e07a48 Translate storage settings in eight more languages
Fill 200 English MongoDB and S3 values in Nepali, Kannada, Malayalam, Javanese, Kazakh, Kyrgyz, Tajik and Somali, using the existing catalogs for terminology. Preserve existing translations and verified zh wording.

Thirteen locale and supplemental-audit suites pass, including token, state and runtime checks. Fourteen storage-settings browser cases register; a running rebuilt app is still needed. Complete software compounds remain provisional pending speaker review.

Thanks to xet7 !
2026-10-13 13:42:27 +03:00
Lauri Ojansivu 7004356e1f Record four-language storage translation review in Upcoming
Update the supplemental candidate count and document preserved wording, Tagalog corrections, validation and remaining review.

Thanks to xet7 !
2026-10-13 13:34:59 +03:00
Lauri Ojansivu 3e10d0aa1d Translate four storage settings catalogs and repair Tagalog instructions
Fill 100 English MongoDB and S3 values in Bengali, Icelandic, Tagalog and Albanian. Repair 15 mixed English/Tagalog clauses, retaining literal AWS console labels and the one-time secret display warning. Preserve all other translations, including existing zh wording.

Locale, correction-ledger, supplemental audit and runtime checks pass. Six storage-settings browser cases register; execution needs a running rebuilt app. Complete software wording remains provisional pending speaker review.

Thanks to xet7 !
2026-10-13 13:33:29 +03:00
Lauri Ojansivu f502638236 Record Marathi fills and the supplemental translation audit
Update Upcoming and TODO Later with the filtered backlog and the additional review queue. Keep native-language review and browser execution explicitly open.

Thanks to xet7 !
2026-10-13 13:22:50 +03:00
Lauri Ojansivu 9ec7e44a1d Merge remote-tracking branch 'origin/main' 2026-10-13 13:19:53 +03:00
Lauri Ojansivu f263dd0b8d Translate Marathi feature and settings prose and expose hidden review candidates
Fill 212 current placeholders and 55 English labels hidden by technical-name and short-word exemptions. Keep existing Marathi translations and preserve source tokens, product names and physical keyboard legends.

Add a read-only audit for product-prefixed prose, generic labels and keyboard names. Candidates require language-specific review; existing human-verified zh wording must not be overwritten.

Marathi, supplemental audit, shared-label and Scrum recovery checks pass. Register Marathi linked-field, notification, recovery and storage-settings browser coverage. Browser execution and broader native language review remain unfinished.

Thanks to xet7 !
2026-10-13 13:19:35 +03:00
Lauri Ojansivu 22ebf0a9b4 Record Bengali translation completion and remaining audit work
Update Upcoming and TODO Later after the current Bengali fill list became empty. Preserve the distinction between filled strings and completed language-quality review.

Thanks to xet7 !
2026-10-13 13:00:22 +03:00
Lauri Ojansivu 012ee1eb0c Translate remaining Bengali feature strings
Fill 182 English placeholders for linked fields, import formats and mapping, notifications, webhooks, LDAP, Wrike and recovery. Preserve existing Bengali wording. The full current fill inventory is empty; older wording and complex technical phrases still need native review.

Verify exact placeholders, permissions, import exclusions, recovery consequences and i18next interpolation. Three focused suites pass. Four Bengali browser cases register; execution needs a running application.

Thanks to xet7 !
2026-10-13 12:59:20 +03:00
Lauri Ojansivu 12e44340b2 Record shared-label translations in Upcoming and refresh audit counts
Track remaining fills and language-quality review separately. Document preservation of human-verified Chinese wording and the unavailable browser app.

Thanks to xet7 !
2026-10-13 12:49:46 +03:00
Lauri Ojansivu 67d1ad324f Translate shared UI labels and repair seeded terminology
Fill 3,129 English values and repair 167 seeded values across 130 locale paths. Preserve existing translations and human-verified zh catalogs. Retire 15 completed pending keys.

Record provisional terminology in Luganda, Tswana, Tongan, Tsonga, Aymara, Quechua, Wolaytta, Venda, Acehnese, Walloon, Wolof and Tamazight as low confidence pending native review.

All 307 translation/Blockly suites and 21 human-preference checks pass. Source tokens match across all 246 catalogs. Expanded browser coverage registers 125 tests; execution requires a running app.

Thanks to xet7 !
2026-10-13 12:47:44 +03:00
github-actions[bot] d2134fe15d Bump versions for v12.39 2026-10-13 09:16:21 +00:00
Lauri Ojansivu f968e5b466 Prepare v12.39 release 2026-10-13 12:15:14 +03:00
Lauri Ojansivu 6aa2ad2c91 Add the SAML qualifier fix to ChangeLog.
Thanks to xet7 !
2026-10-13 12:12:19 +03:00
Lauri Ojansivu 9949bca0b9 Log in a SAML binding that lacks the optional qualifiers.
A binding stored with issuer, nameID and nameIDFormat but no nameQualifier
or spNameQualifier keys was not found by identity, because a query for ''
does not match a missing field. The user was then found by username and
refused as account-already-bound-to-saml. An empty assertion qualifier now
also matches a missing one, and the next login writes them as empty
strings. A non-empty qualifier and a missing issuer still fail closed.

Thanks to xet7 !
2026-10-13 12:12:12 +03:00
Lauri Ojansivu 8cfb5ce71e Add the Bun build and release check fixes to ChangeLog.
Thanks to xet7 !
2026-10-13 01:07:14 +03:00
Lauri Ojansivu 68c9f2bcd0 Record the Bun and UCS internals in the build script parity guard.
tests/buildScriptParity.test.cjs failed because the Bun target-build
scripts, which bun.yml runs on each target runner, and the UCS migration's
docker-exec.mjs are not build.sh menu entries. They are listed as internals
with the reason, like the other release-workflow helpers.

Thanks to xet7 !
2026-10-13 01:06:51 +03:00
Lauri Ojansivu fb12ea42c8 Fix the Android and FreeBSD Bun builds and the false windows-arm64 error.
Android: Termux's user does not own the bind-mounted bundle, which the
workflow makes writable for everyone, so chmod failed with EPERM before an
allowed write. Files are now chmodded only when not writable already.

FreeBSD: the final Bun native check was killed at its deadline with no
output, its buffered console lost with it. It now writes each step
unbuffered and exits explicitly, and the supervisor tells a hung operation
(failure, naming the step) from every check passing and Bun then hanging on
exit (oven-sh/bun#44595), which passes with a warning.

windows-arm64 built and attached its bundle but showed an error: the
expected no-prebuilt-addon probe printed Bun's raw "error:" line, which
GitHub turns into an annotation. It is now reported as one plain line.

Thanks to xet7 !
2026-10-13 01:06:51 +03:00
Lauri Ojansivu 163ebf260a Check the bundle version in an rspack bundle, and word a cancelled snap job right.
The Docker job's bundle-version check looked for "name":"wekan" in app.js,
but rspack keeps only the version field the server reads, so the check was
skipped with a warning on every release. It now also reads the one
v-prefixed version-only module, and still only warns when that is absent
or ambiguous. A cancelled snap-launchpad job no longer claims it ran out of
time: the v12.38 jobs were cancelled at 1h49m of a 360-minute cap.

Thanks to xet7 !
2026-10-13 01:06:51 +03:00
Lauri Ojansivu fbd0267226 Add the image scan fixes to ChangeLog.
Thanks to xet7 !
2026-10-13 00:27:06 +03:00
Lauri Ojansivu 169670abd5 Raise eight npm packages inside Meteor's packages in the bundle.
The Artifact Hub scan of the v12.38 image found advisories in browserslist,
baseline-browser-mapping, colord, svgo, source-map-js (minifier-css),
compression and qs (webapp, oauth) and lodash.template (simple-schema), all
fixed in the same major. bump-bundle-npm-deps.mjs now raises them, and
gives a bumped package its own copy of a dependency whose sibling is a
different version, so compression 1.8.2 runs with the bytes, negotiator and
safe-buffer it declares. nodemailer and postcss-selector-parser fixes need a
new major and are recorded as not fixable.

Thanks to xet7 !
2026-10-13 00:26:22 +03:00
Lauri Ojansivu be2a4053be Remove docker-cli from the Docker image.
Debian's docker-cli carries Go 1.24.4, whose standard library advisories
were most of the Quay and Artifact Hub scans of the image, and only the UCS
migration used it, for docker exec. The migration's mongo and mongoexport
wrappers now run releases/ucs-migration/docker-exec.mjs, which speaks the
Docker Engine API over the mounted socket with the same stdout, stderr and
exit status, and the image installs no Docker CLI.

Thanks to xet7 !
2026-10-13 00:26:22 +03:00
github-actions[bot] bd5a374182 Bump versions for v12.38 2026-10-12 19:33:00 +00:00
Lauri Ojansivu cfc34c125a Prepare v12.38 release 2026-10-12 22:31:56 +03:00
Lauri Ojansivu 460e10f848 Merge remote-tracking branch 'origin/main' 2026-10-12 22:31:31 +03:00
Lauri Ojansivu 0930e9b314 Add CodeQL alert 569, SAML linking, favorites and list drag fixes to ChangeLog.
Thanks to GitHub CodeQL, Raymond-Voogt, mimZD and xet7 !
2026-10-12 22:22:04 +03:00
Lauri Ojansivu a9c75efe60 Explain why SAML could not link an existing account, and find it by email.
Every refusal to link a SAML login to an existing (e.g. LDAP) account threw
the same "SAML identity does not match this account" and logged nothing.
Each condition now has its own reason in the server log, and a missing email
or email_verified=true attribute in the assertion is named on the sign-in
page. The refusal still fails closed. With merging on, a login name that is
the assertion's email also finds the account by a verified email, instead of
trying to create a new user. email_verified, the matching rules and the
unused SAML_ATTRIBUTES are documented.

Thanks to xet7 !
2026-10-12 22:21:30 +03:00
Lauri Ojansivu 715eb2b826 Match HTML end tags that carry attributes (CodeQL alert 569).
Browsers end a script at </script foo="bar"> too, so an end-tag pattern
written as <\/script\s*> or <\/script> misses it. The reported test, the
server input sanitizer's style pattern and one more test now use
<\/tag\b[^>]*>, and tests/securityAlerts20261012.test.cjs fails on the
loose shape for script, noscript, style, iframe, textarea and title
anywhere in the tree.

Thanks to GitHub CodeQL and xet7 !
2026-10-12 22:21:30 +03:00
Lauri Ojansivu bcf2305448 Show private starred boards in the header Favorites popup.
The starredPublicBoards publication only sent public and instance boards,
so a user whose favorites are all private boards saw an empty popup. It now
sends every starred board the user may see - public/instance, or an active
membership, organization, team or domain share - through the shared
visibility builder, so a revoked share or stale star still never brings a
private board back.

Thanks to Raymond-Voogt and xet7 !

Fixes #6764.
2026-10-12 22:21:30 +03:00
Lauri Ojansivu 205110e3a5 Show clearly where a dragged list will land.
The list drop placeholder had no --list-width, so none of the list width
rules matched it and it kept flex-shrink: 1 with no content; in a swimlane
wider than the window flexbox shrank it to a thin shadow. It now never
shrinks, every list sortable pins its min-width to the dragged list's width,
and it is drawn as a tinted, dashed drop area.

Thanks to mimZD and xet7 !

Fixes #6763.
2026-10-12 22:21:30 +03:00
Lauri Ojansivu 05adb51be2 Clarify Upcoming build fixes and regression coverage.
Thanks to xet7 !
2026-10-12 22:18:28 +03:00
Lauri Ojansivu 6c04af3bcf Document target build and release packaging fixes.
Record regression coverage and platform validation limits for the v12.37 build-log fixes.

Thanks to xet7 !
2026-10-12 22:06:48 +03:00
Lauri Ojansivu 929e3051d4 Fix Bun target builds and release asset checks.
Preserve Android build metadata through Termux's environment reset and build Sharp against libvips. Bound native addon probes and final checks, read complete AppImage asset lists, and follow authenticated chart pagination. Keep expected negative smoke diagnostics out of CI error annotations.

Thanks to xet7 !
2026-10-12 22:05:36 +03:00
github-actions[bot] 7dd26551ed Bump versions for v12.37 2026-10-12 10:57:07 +00:00
Lauri Ojansivu bd54d98963 Prepare v12.37 release 2026-10-12 13:56:03 +03:00
Lauri Ojansivu e7db57832c Merge remote-tracking branch 'origin/main' 2026-10-12 13:55:39 +03:00
Lauri Ojansivu dd04694e52 Document the Bun Rspack dependency fix and successful ESM build 2026-10-12 13:50:16 +03:00
Lauri Ojansivu 3d20a9e4a6 Install Rspack build dependencies explicitly in Bun CI
Include development and optional dependencies in the locked npm install and verify the Meteor Rspack config before compilation. Exercise both omitted and included dependency installs with an offline fixture.
2026-10-12 13:47:41 +03:00
github-actions[bot] 299087d912 Bump versions for v12.36 2026-10-12 10:05:20 +00:00
Lauri Ojansivu 59d823ea66 Prepare v12.36 release 2026-10-12 13:04:17 +03:00
Lauri Ojansivu ac59abff4c Clarify Upcoming Bun fix summary and regression coverage 2026-10-12 13:03:33 +03:00
Lauri Ojansivu bb9b3f0af3 Document the Bun installer version fix 2026-10-12 12:53:00 +03:00
Lauri Ojansivu f24448ebd1 Pass a semantic version to the Bun setup action 2026-10-12 12:52:43 +03:00
Lauri Ojansivu 5985117db6 Updated Sandstorm WeKan install docs. 2026-10-12 12:42:12 +03:00
github-actions[bot] 9f0644fa02 Bump versions for v12.35 2026-10-12 09:06:29 +00:00
Lauri Ojansivu 02fd967566 Prepare v12.35 release 2026-10-12 12:05:23 +03:00
Lauri Ojansivu 06ee936e84 Align Upcoming changelog with release note conventions 2026-10-12 12:03:51 +03:00
Lauri Ojansivu 7d1d7b45f5 Document ZIP format for Bun release downloads 2026-10-12 12:02:29 +03:00
Lauri Ojansivu 05d45e219f Package Bun release bundles as ZIP archives
Preserve executable modes and dereference dependency links. Update release uploads, expected assets and recovery checks for ZIP filenames.
2026-10-12 12:02:03 +03:00
Lauri Ojansivu 870259d285 Document the Bun release workflow and its validation 2026-10-12 11:55:19 +03:00
Lauri Ojansivu 1d1db1c234 Build and attach Bun release bundles for every supported OS and CPU
Build one Meteor ESM application, rebuild native addons on each target, and attach verified archives and checksums. Use native hosted runners, Alpine and Termux containers, and FreeBSD VMs. Cover release planning, failed-build gates, native modules, HTTP/DDP, and Chromium signup and board creation.

Thanks to dupontbertrand and xet7 !
2026-10-12 11:53:26 +03:00
Lauri Ojansivu 907b85711d Merge remote-tracking branch 'origin/main' 2026-10-12 11:50:10 +03:00
Lauri Ojansivu cd0e7d19b4 Document the CodeQL 565 to 568 fixes and the Windows node-gyp fix.
Thanks to GitHub CodeQL and xet7 !
2026-10-12 11:43:57 +03:00
Lauri Ojansivu b34d36e8f5 Windows win32 and ARM64 bundles: find node-gyp from inside each package.
The jobs run compile-native-addons.mjs from programs/server with the bundle
as `../..`, and node-gyp runs with each package as its working directory.
The relative node-gyp path was resolved from there, so v12.34's build-win32
and build-win-arm64 failed with "Cannot find module ...accounts-password/
programs/server/node_modules/node-gyp/bin/node-gyp.js". The bundle path is
made absolute first. The new test calls it the way the jobs do, with a fake
node-gyp that resolves paths the way the child process does, which the old
fake did not.

Thanks to xet7 !
2026-10-12 11:43:02 +03:00
Lauri Ojansivu 900267451c Fix CodeQL alerts 565 to 568, and the same shapes elsewhere.
Alert 565 (ReDoS): the package-specifier pattern in
releases/prune-unreachable-npm.mjs allowed `/` both between and inside path
segments, so a quoted `-/-/-/...` could be split in exponentially many ways;
es-module-lexer's minified lexer alone took over two seconds. A `/` now only
separates segments. Over the 23,812 files in node_modules it finds every
package it found before, and also `require('querystring-es3/')`, the
trailing-slash form the old pattern missed.

Alert 566 (bad tag filter): the Sandstorm readiness test matched <script>
case-sensitively. It and three negative checks that said "no <script" in
sanitized output now match any case; one of those also collected tag names
with a case-sensitive pattern that an <SCRIPT> tag would have escaped.

Alerts 567 and 568 (code sanitization): a test wrote dictionary paths into
generated JavaScript with JSON.stringify. It copies the dictionaries beside
its entry so the source is constant, and four more test probes take module
paths as arguments instead. The i18n loader the test drives, which writes an
asset name into generated server code, refuses a name outside a locale
tag's characters and escapes the literal.

tests/securityAlerts20261012.test.cjs fails on any case-sensitive script,
noscript or iframe pattern and on JSON.stringify spliced into an import,
require or asset path anywhere in the tree.

Thanks to GitHub CodeQL and xet7 !
2026-10-12 11:43:02 +03:00
github-actions[bot] 833e52b5da Bump versions for v12.34 2026-10-12 08:21:13 +00:00
Lauri Ojansivu adbd19b1ba Prepare v12.34 release 2026-10-12 11:20:11 +03:00
Lauri Ojansivu a1f09eb58b Document the LDAPS company-CA fix, and repair Upcoming's structure.
Merging #6761 left a stray "This release fixes the following bugs:" above
the security header and the Sandstorm entries ahead of the security fixes.
Security comes first again, the Sandstorm entries follow the bug fixes under
one topic line with their contributor credited, two one-word wrapped lines
are reflowed, and In short is back to four sentences.

Thanks to fvb-sb, mnutt and xet7 !
2026-10-12 11:14:55 +03:00
Lauri Ojansivu 8ab7d46714 Trust the operating system's certificate authorities for TLS.
Since v12.15 the LDAP server's certificate is verified by default, and
Node.js verified it only against the Mozilla list it is built with. A
company root CA installed on the server with update-ca-certificates was
unknown to WeKan, so after upgrading from v12.14 the snap refused LDAPS
logins with UNABLE_TO_VERIFY_LEAF_SIGNATURE.

server/main.js now adds the operating system's CAs to Node's own before any
application code runs: tls.getCACertificates('system'), and the distribution
CA bundle files Go's crypto/x509 reads. The bundle file matters in the snap:
on Debian and Ubuntu snapd keeps the host's /etc/ssl, but the per-CA links
update-ca-certificates makes point into /usr/local/share/ca-certificates,
which the snap sees from its base. Verification is not relaxed - a CA the
system does not have is still refused - and --use-bundled-ca keeps Node's
list only. It covers LDAPS, mail, webhooks and OAuth2 in every launcher.

A refused LDAP certificate now says what to do, in the log and in Admin
Panel's Test connection: install the CA on the server, or set LDAP_CA_CERT.
No hint suggests turning verification off.

tests/systemCaCertificates6762.test.cjs runs a TLS server with a certificate
from a test private CA (its key is not kept) and checks the reported error
without the CA, verification with it in the system bundle, refusal without
it, --use-bundled-ca, and the hints. Two LDAP guards follow the extended
error handling; prune-unreachable-npm.mjs leaves the regex guard's reviewed
list, as it no longer builds a RegExp.

Thanks to fvb-sb and xet7 !

Fixes #6762.
2026-10-12 11:14:34 +03:00
Lauri Ojansivu 8a947993f3 Merge pull request #6761 from mnutt/sandstorm-startup-timing
Sandstorm startup timing improvements
2026-10-12 10:52:52 +03:00
Lauri Ojansivu e3ed0ef801 Merge branch 'main' into sandstorm-startup-timing 2026-10-12 10:52:05 +03:00
Michael Nutt 9647c07554 Fix startup test assumptions and release documentation checks 2026-10-12 00:07:58 -04:00
Michael Nutt 11db7365a5 Record Sandstorm startup and packaging improvements
Link the six implementation commits from Upcoming and keep released
changelog sections unchanged.
2026-10-11 23:30:56 -04:00
Michael Nutt bab0c13988 Reduce server bundle file count for Sandstorm extraction
Strengthen dependency analysis and expose retention reasons in an audit
report. Prune verified browser inputs, unused client polyfills and server
type declarations. Include safety regressions and packaged-runtime results.
2026-10-11 23:30:56 -04:00
Michael Nutt 2b8a4a234f Defer SAML and LDAP dependencies until first use
Load SAML, LDAP and transliteration implementations when their operations
need them. Preserve registration and authentication checks, with tests
and documentation accompanying the change.
2026-10-11 23:30:56 -04:00
Michael Nutt 8c26d75e31 Load server source maps lazily from sidecar files
Extract inline maps during bundle trimming so startup avoids scanning
large base64 comments. Preserve mapped stacks and include packaging
regressions and measured production results.
2026-10-11 23:30:56 -04:00
Michael Nutt df1c240edd Load server translations from private assets on demand
Keep English embedded and load other server dictionaries on first use,
without changing client loading or translation contents. Include loader
regressions, build integration checks and profiling documentation.
2026-10-11 23:30:56 -04:00
Michael Nutt 3df146c164 Defer ZIP and Excel initialization until first use
Load archive libraries only when an import, export or restore needs them.
Keep lightweight timing enabled and make detailed Meteor profiling opt-in.
Include first-use regression checks and production measurements.
2026-10-11 23:30:56 -04:00
Michael Nutt 22d7d23118 Measure Sandstorm startup and signal database and HTTP readiness
Add launcher timings and a local production-path profiling runner. Replace
the fixed database delay with listener readiness and notify the waiting
page through a held request. Include tests and profiling documentation.
2026-10-11 23:30:56 -04:00
Lauri Ojansivu 17a551ac80 Document the mongo-tools-only change.
Thanks to xet7 !
2026-10-12 02:25:46 +03:00
Lauri Ojansivu 41bc82713e Use only the one-program mongo-tools-<platform> from mongo-tools-patches.
fetch-mongo-tools.sh, the local build-release-bundle.sh and the
check-arch-binaries.sh preflight fell back to the eight separate
<tool>-<platform> binaries when a release had no mongo-tools-<platform>.
wekan/mongo-tools-patches now publishes only the one program, so every
bundle, image and snap gets that or no tools, and says so. The Windows and
macOS jobs also remove the inherited amd64 mongo-tools with the launchers.

tests/fetchMongoTools.test.cjs serves a release that has only the per-tool
binaries and checks that none is installed or even requested, and fails on
any script or workflow that builds a per-tool download address.

Thanks to xet7 !
2026-10-12 02:25:32 +03:00
Lauri Ojansivu 147c83d953 Document the CodeQL 562, 563 and 564 fixes.
Thanks to GitHub CodeQL and xet7 !
2026-10-12 02:18:53 +03:00
Lauri Ojansivu 4caa929cfe Fix CodeQL alerts 562, 563 and 564, and the same shapes elsewhere.
Alert 564 (regex injection): releases/platform-native-packages.mjs compiled
its command-line platform and arch into a RegExp. The canvas package names
are now compared as strings. The upload naming of attachments and avatars,
the attachment rename popup and tools/mirror-menu.mjs built a RegExp from a
file extension or a name the same way; they use literal suffix and prefix
checks now. tests/noUnescapedDynamicRegExp.test.cjs parses every source file
and fails on a RegExp built from a value that is not escaped, unless the
site is listed with the reason its values are constants.

Alert 563 (incomplete escaping): name.replace('/', '%2F') replaced only the
first slash. registryPackagePath() replaces every one and refuses anything
that is not an npm package name. Seven tests escaped a regex the same
first-only way; 'g++'.replace('+', '\\+') let "- g+" pass a guard. They use
the complete escapeRegExp, and tests/noIncompleteRegExpEscaping.test.cjs
fails on any string-pattern replace that performs an escape.

Alert 562 (ReDoS): the CasaOS reverse-domain id pattern in
tests/platformTemplates.test.cjs let '.' be matched by both groups, so
'0.0.0...!' backtracked exponentially. Each separator now belongs to one
group, and the test runs CodeQL's attack string against it.

Thanks to GitHub CodeQL and xet7 !
2026-10-12 02:18:33 +03:00
Lauri Ojansivu 534d411aca Document the UCS avatar fix and the GitHub CLI 2.48 fix.
Thanks to xet7 !
2026-10-12 01:30:56 +03:00
Lauri Ojansivu 0516826f75 Release scripts work with a GitHub CLI older than 2.48.
They read every page of a GitHub list with gh api --paginate --slurp,
and --slurp exists only from gh 2.48 on: an older gh stopped the release
with "unknown flag: --slurp". --paginate alone prints the pages back to
back, and gh_pages() now reads them one after another. A truncated page
is still an error, never a shorter list.

The tests feed the scripts pages the way gh prints them, check that no
gh call asks for --slurp, and (negative) that a truncated page fails.

Thanks to xet7 !
2026-10-12 01:30:07 +03:00
Lauri Ojansivu 7514d6e175 UCS: avatars show again after an upgrade, and lazily loaded code is fetched from the page's own address.
On Univention WeKan is served at /wekan. profile.avatarUrl holds
/cdn/storage/avatars/<id> - what Change Avatar and the server's own
migrations store - and it was rendered as stored, so the browser asked
the UCS web server for a path outside WeKan, got 404, and every avatar
was an empty circle, while Change Avatar, which builds its own link,
showed the picture. models/lib/avatarDisplayUrl.js treats the stored
value as the avatar's name: whatever host or old path prefix stood
before /cdn/storage/avatars/ or /cfs/files/avatars/ is replaced with the
path WeKan is served at now, relative to the page's origin. The avatar,
the People page and the report tables draw through it; the default
avatar gets the prefix too. External pictures and data: URLs are left
alone.

The same session, opened through 127.0.0.1:8443/wekan, fetched lazily
loaded code from ROOT_URL's host ucs-2275.wekan.intranet, which the
browser could not resolve (NS_ERROR_UNKNOWN_HOST, "Cross-Origin Request
Blocked"). Meteor's dynamic-import setting useLocationOrigin is now on
by default, so that code comes from the page's own origin and ROOT_URL
path; an explicit value in METEOR_SETTINGS still wins. Links keep using
ROOT_URL as before.

tests/avatarDisplayUrl.test.cjs covers every stored shape, (negative)
leaves external, data: and query-string URLs alone, checks that no
template puts a stored avatar URL in src without avatarSrc, and pins the
dynamic-import default and its override. The Playwright spec
avatar-sub-path.e2e.js checks the drawn src and the setting in a browser;
it was not run here.

Thanks to xet7 !
2026-10-12 01:30:07 +03:00
Lauri Ojansivu 1eb9eb43ed Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Dockerfile
2026-10-12 01:13:26 +03:00
Lauri Ojansivu d1a2c39cf2 Document the stripped Node.js and the one-program MongoDB Database Tools.
Thanks to xet7 !
2026-10-12 01:09:05 +03:00
Lauri Ojansivu 44af4f3e72 Bundles carry the MongoDB Database Tools as one 15 MB program instead of eight.
The eight tools were eight Go programs, each with its own copy of the Go
runtime, the MongoDB driver and the tools' shared packages: about 105 MB
in every bundle, image and snap. wekan/mongo-tools-patches now also
publishes mongo-tools-<platform>, all eight in one program of about 15 MB.

releases/fetch-mongo-tools.sh puts it in a bundle with a launcher per
tool - a three-line shell script on Linux and macOS, a .cmd on Windows -
so mongodump keeps working from $SNAP/bin, the bundle and PATH. Launchers
rather than symlinks, because a symlink has to survive zip -r, Docker
COPY, snapcraft and every user's unzip. Until the newest
mongo-tools-patches release has mongo-tools-<platform>, it fetches the
eight per-tool assets as before. It first removes the tools a repacked
bundle inherited from amd64, so a platform without tools ships none
rather than the wrong CPU's.

Every bundle job in release-all.yml, repack-bundle-for-arch.sh and
build-release-bundle.sh use it; the snaps move mongo-tools into bin/
with its launchers; the telemetry audit scans mongo-tools as a tool; the
Windows single EXE unpacks the .cmd launchers; the preflight and the URL
check accept the one program.

tests/fetchMongoTools.test.cjs serves the release from a local HTTP
server and checks the launchers run the program with their arguments,
the Windows .cmd files, the per-tool fallback, and (negative) that
inherited amd64 tools are removed, that a checksum mismatch is refused,
and that no workflow or snap recipe downloads the tools itself.
releaseBinaryFreshness now finds the newest-release URL in the helper;
workflowFailureDiagnostics required an ::error:: beside each new exit.

Thanks to xet7 !
2026-10-12 01:08:48 +03:00
Lauri Ojansivu 8a78749061 boardPermission guard: record the commented-out Sandstorm migration it now finds.
ed5502edfa brought server/sandstorm.js back, and with it a commented-out
v0.8 migration that sets permission: 'public'. It is a write, not a read
gate, so it cannot leave instance boards out; it is listed as reviewed.

Thanks to xet7 !
2026-10-12 01:08:38 +03:00
Lauri Ojansivu 3eb549058c Strip the symbol table and debug data from every bundled node.
nodejs.org's node executables carry their full symbol table and DWARF debug
data, which only a debugger reads. Node 26.11.1 for linux-x64 is 150.0 MB
and 116.7 MB without them; linux-arm64 goes from 150.2 to 114.6 MB. Every
bundle, Docker image, snap, AppImage, Flatpak and the Sandstorm package
ships one.

releases/strip-node.sh runs after the download has been verified against its
source's checksum, so the file verified and recorded is the original:
- ELF: eu-strip (elfutils). GNU strip only reads the host's own CPU and
  fails on an arm64 node on an x64 runner. Inside an emulated target
  container the native strip is the fallback.
- Mach-O: strip -x keeps the 39006 exported symbols addons bind to and
  drops the 901035 local ones. It then re-signs ad hoc, because stripping
  breaks the signature and an arm64 Mac runs no unsigned binary.
- PE: nothing. node.exe has no COFF symbols, and its debug data is a
  separate .pdb that is not shipped.
It refuses a result that lost .dynsym, the table native addons resolve
N-API against. The stripped linux-x64 node was run with bcrypt, argon2,
sharp and @napi-rs/canvas, and each hashed, verified or rendered correctly.

embed-verified-node.sh calls it for amd64, arm64, Windows, macOS, Sandstorm
and the local build, and install-node-for-arch.sh for the emulated
architectures. build-amd64, build-arm64 and the extra-arch containers
install elfutils.

tests/stripNode.test.cjs strips real gcc -g binaries (it checks that
.symtab and .debug_* are gone, .dynsym stays, and the binary still runs),
refuses an ELF without .dynsym and a non-binary, leaves a .exe untouched,
and pins every call site after its checksum check.

tests/buildScriptParity.test.cjs lists every releases/ script: it named
compile-native-addons.mjs, added by 34a33f7b14 and not registered, and now
strip-node.sh. Both are registered as release-workflow helpers, and the
suite passes again. Its earlier failure was reported as pre-existing; it was
not.

Thanks to xet7 !
2026-10-12 00:40:41 +03:00
Lauri Ojansivu c9dad1ecbe Document the release build speed-ups and the 32-bit ARM sharp fix
Adds the Docker, Launchpad and history, and native-module entries to Upcoming, and notes in the released v12.33 sharp entry that its 32-bit ARM zips can crash on ARMv6 and some ARMv7 boards, and where the fix is.

Thanks to xet7 !
2026-10-12 00:11:16 +03:00
Lauri Ojansivu 628bb95619 32-bit ARM: use the WebAssembly sharp, not a native build that can SIGILL; add canvas elsewhere.
Inspecting the bundles to find what could be removed found a crash risk
added in v12.33 by 943bfd9197. That commit gave the armv6, armhf and armv7
bundles sharp's @img/sharp-linux-arm package. readelf shows its .node is
built for ARMv7 (Tag_CPU_arch: v7) and its libvips for ARMv8. The armv6
bundle runs on Raspberry Pi 1 and Zero, and the armhf/armv7 bundles on
ARMv7 boards. Executing an instruction the CPU lacks raises SIGILL, which
kills the whole WeKan process the first time a thumbnail is made. Before
that commit the bundles had only the linux-x64 package, which fails to load
and is handled. 32-bit ARM now downloads no native package and keeps the
WebAssembly sharp, which runs on any CPU (thumbnails and GIFs were made
with it under Node 26).

The same inspection found that @napi-rs/canvas is not dead weight. pdfjs-dist
6 builds a DOMMatrix while loading, so without canvas the import throws and
PDF attachment text is never indexed for search. This was checked: a PDF
made with pdfkit gives its text back with canvas and fails without it.
Every bundle except Linux x64 had only canvas-linux-x64-gnu, which
platform-native-packages then removed. The script now adds the glibc/MSVC
canvas package for the target as it does for sharp, verified by sha512 and
by its binary header. That covers linux x64/arm64/riscv64, darwin x64/arm64
and win32 x64/arm64. It never installs a musl package, nor anything on
32-bit ARM, where canvas is ARMv7 too. Where none is published (ppc64,
s390x, ia32, loong64), that is logged.

Checked against the npm registry: linux-arm64 and win32-x64 got sharp and
canvas, and both passed the ELF/PE check. linux-arm downloaded nothing.
tests/platformNativePackages.test.cjs covers 32-bit ARM, the canvas choice
(no musl, no ARM, none where unpublished) and the canvas download.

Thanks to xet7 !
2026-10-12 00:09:03 +03:00
Lauri Ojansivu 5f60152d08 Release workflows: upload 1 MB instead of 237 MB to Launchpad, and stop fetching unused history.
Launchpad snap legs (95-104 minutes in v12.32): prepare-launchpad-source.sh
archived the whole tree, 237 MB of docs, tests, translations and client
code, for every architecture. The wekan part (source: .) then copied it
again on Launchpad's slowest builders, and the recipe uses none of it,
because the app comes from the release .zip. The snapshot now holds only
what snapcraft.yaml reads: snapcraft.yaml, snap/, snap-src/, and
releases/fetch.sh, check-telemetry.py, risk-baseline.json (its deny-hash
list) and migrate-mongodb-to-ferretdb.mjs. That is 1.1 MB.
tests/releaseSnapLaunchpadFlatten.test.cjs checks the snapshot holds the
listed files and nothing else, and checks against the real snapcraft.yaml
that every project path it reads is in the list.

Git history that was fetched and never read:
- snap-launchpad cloned the full history (fetch-depth: 0, 447 MB) because
  remote-build rejects shallow repositories. What it uploads is the fresh
  one-commit repository made from git archive, so a shallow checkout is
  enough.
- bump only reads the tree and pushes one commit on top: shallow.
- prepare fetched history to see whether the release tag already existed.
  It now asks GitHub with git ls-remote, which also works on a re-run.
- update-ondra-gantt-repos cloned ~160 MB of history per variant repository
  to add one commit: shallow.
- release.yml (chart-releaser) ran on every push to main with a full clone,
  but this repository has no charts/; the chart is published to
  wekan/charts by release-all.yml. It now runs only when charts/ changes.

The wekan/charts checkouts keep fetch-depth: 0 because they need the
gh-pages branch, and that repository is 15 MB.

Thanks to xet7 !
2026-10-12 00:03:32 +03:00
Lauri Ojansivu 3c34e65403 Docker image: build from the published .zip on the build machine, not under QEMU.
In v12.32 "Build and push multi-arch image" took 27 of the docker job's 29
minutes. For each of seven platforms, six of them emulated, the Dockerfile
installed a compiler toolchain, resolved and downloaded Node.js, reinstalled
programs/server with npm, re-ran the prune, bump, trim and prune passes, and
purged the toolchain again. That platform's .zip leg in release-all.yml had
already done all of it, with the same scripts.

Stage 1 (FROM --platform=$BUILDPLATFORM) now runs natively on the runner for
each target. It downloads that target's .zip, checks it against the
release's own .sha256sum, re-runs the telemetry check, reads bundle/node's
ELF header to confirm the target CPU, and lays out /build and
/usr/local/bin/node. The image's node is the .zip's own node: the file the
leg resolved and verified, rather than a second download that had to match
it. Stage 2 is the only part that runs as the target CPU. It adds the WeKan
user, applies an OS update, installs file, libatomic1 and docker-cli, and
copies stage 1's result. No npm and no compiler ever enter the image.

The docker job read the base image from the first FROM; it now reads the
last one, since the first is the build-machine stage.

Stage 1 was run outside Docker against the real v12.32 s390x zip: the
checksum, the telemetry check (47979 files) and the ELF check (machine 22)
passed, and the layout was right. The ELF check rejects the same node when
expected as ppc64le, and rejects a non-ELF file. No Docker build ran here.

tests/dockerImageFromBundle.test.cjs pins the two stages, the verification,
the per-architecture ELF numbers (shared with check-addon-arch.mjs), and
that no reinstall, Node.js download or toolchain comes back. It fails on
the previous Dockerfile. Nine tests pinned the old in-image steps; each now
pins the new behaviour, with the reason recorded beside it.

Thanks to xet7 !
2026-10-12 00:01:07 +03:00
Lauri Ojansivu 403d485af9 Document the Windows win32 and ARM64 addon compile fix
Adds an Upcoming section for it, and notes in the released v12.33 entries that their win32 and win-arm64 zips were not published, and where the fix is.

Thanks to xet7 !
2026-10-11 23:35:08 +03:00
Lauri Ojansivu 34a33f7b14 Windows win32 and ARM64 bundles: compile bcrypt and argon2 with node-gyp itself.
v12.33's build-win32 and build-win-arm64 both failed their addon check:

  ...accounts-password/node_modules/argon2 has no arm64 Windows addon to load

The jobs relied on npm to compile the addons that have no prebuild for their
CPU (argon2 0.44 for win32-arm64; bcrypt and argon2 for win32-ia32) during
the programs/server install, through Meteor's npm-rebuild.js. The release
jobs use npm 12, which ignores npm_config_arch, npm_config_target_arch and
npm_config_nodedir ("Unknown env config", which it says will become an
error) and blocks install scripts outside allowScripts ("install scripts
blocked"). So nothing was compiled.

releases/compile-native-addons.mjs now runs node-gyp itself, using the
node-gyp that the install brings (pinned to 13.x, which finds Visual
Studio 2026), with --arch for the target CPU. win32 also passes the nodedir
that holds the fork's ia32 node.lib. It does this for every copy of each
named package that has a binding.gyp and no win32-<arch> prebuild, before
prune-build-only-modules removes node-gyp. It fails if node-gyp fails or
leaves no .node. Both jobs no longer set the npm_config_ variables.

Checked with the real node-gyp 13.0.2, argon2 0.44.0 and bcrypt 6.0.0
(with their win32 prebuilds removed, compiled for this machine): both
compiled, loaded, and hashed and verified a password.
tests/compileNativeAddons.test.cjs covers the plan, the node-gyp arguments
and every way the compile could quietly not happen.
tests/win32BundleBuild.test.cjs pins the step in both jobs and refuses
npm_config_arch/_target_arch/_nodedir in any workflow.
tests/releaseBundleCoverage.test.cjs used to require npm_config_arch: ia32,
and now requires the node-gyp cross-build, with the reason recorded in the
test.

Thanks to xet7 !
2026-10-11 23:34:51 +03:00
github-actions[bot] 798e314150 Bump versions for v12.33 2026-10-11 20:10:57 +00:00
Lauri Ojansivu 34924a5f3d Prepare v12.33 release 2026-10-11 23:09:17 +03:00
Lauri Ojansivu be09a819c3 Merge remote-tracking branch 'origin/main' 2026-10-11 23:08:50 +03:00
Lauri Ojansivu a21c5ad2ef Tidy the Upcoming CHANGELOG section
Shorten In short to three sentences covering every topic, and name the renamed platformNativePackages test in the Windows entry.

Thanks to xet7 !
2026-10-11 23:03:48 +03:00
Lauri Ojansivu 322270701f Document the UCS LDAP login fix
Thanks to xet7 !
2026-10-11 22:59:32 +03:00
Lauri Ojansivu 5f0716fc74 UCS: refuse to publish an app whose LDAP group settings refuse every login.
After the 12.32 UCS upgrade every LDAP login failed with "LDAP authentication
failed". The UCS compose (wekan/univention) kept the 6.09 setting
LDAP_GROUP_FILTER_ENABLE=true with no group id attribute and no group name.
6.09 read that as "in any group". WeKan now refuses an incomplete group
filter, as intended since #3700, so isUserInGroup refused everyone. The
compose is fixed in wekan/univention: the filter is off, because UCS grants
access through (wekanActivated=TRUE), and the group id attribute is cn, so
Domain Admins become admins again.

releases/ucs-release.py now applies WeKan's own group-setting rule to the
compose before it prepares a portal upload. With the group filter or admin
sync on, it needs the three group lookup settings, and an enabled filter
needs a group name. A compose that would refuse every LDAP login is not
published. It refuses the 12.32 compose and passes the fixed one.
tests/ucsRelease.test.py covers both, plus the rule.
tests/ldapGroupFilterConfig.test.cjs shows that WeKan refuses the 6.09 UCS
settings and needs nothing missing with the fixed ones.

The UCS README describes the failure and gives a workaround for installs that
already upgraded: two appcenter/env/wekan UCR values and univention-app
reinitialize. It also explains the cross-origin error seen when WeKan is
opened through a host other than ROOT_URL.

Thanks to xet7 !
2026-10-11 22:59:15 +03:00
Lauri Ojansivu 2fb604bc25 Document verified Nextcloud acceptance coverage and deployment limits
Record the 40 passing real browser checks across Nextcloud 32-35 and the mutual-TLS FRP subpath run, companion and release regressions, and the remaining Docker and ARM64 validation.

Thanks to xet7.
2026-10-11 22:49:33 +03:00
Lauri Ojansivu 91bfd08097 Test real Nextcloud deployments and preserve signed SockJS identities
Keep verified gateway identities bound to their actual upgrade sockets before SockJS filters custom headers. Add repository-local Nextcloud 32-35 acceptance runs for authentication, isolation, uploads, lifecycle, source-built upgrades, restored volumes and mutual-TLS FRP. Harden release archive checks and exercise signing and workflow failures.

Thanks to xet7.
2026-10-11 22:46:45 +03:00
Lauri Ojansivu 0b44d78d4a Document sharp on every platform
Thanks to xet7 !
2026-10-11 22:41:03 +03:00
Lauri Ojansivu 943bfd9197 Make sharp load on every WeKan platform: add each bundle's own sharp package.
sharp makes the attachment thumbnails. Every bundle except amd64 is made
from the amd64 Linux bundle, and npm installed only @img/sharp-linux-x64
there. Reading the published v12.32 zips showed that every other bundle
carried that one package and nothing it could load: arm64, ppc64le, s390x,
riscv64, i386, armhf, armv7, armv6, mac-arm64 and mac-x64, as well as the
Docker images and snaps built from them. Thumbnails fell back to the
full-size image, and nothing failed.

releases/windows-native-packages.mjs becomes platform-native-packages.mjs,
<bundle> <win32|linux|darwin> <arch>. It removes the packages npm would not
install on that target (os, cpu, and on Linux libc), downloads
@img/sharp-<platform>-<arch> and its own libvips at the versions pinned,
checks each against the registry's sha512, and checks every native addon's
header for the target. sharp publishes nothing native for Linux i386 or
loong64, so there it uses @img/sharp-wasm32. That package is now a root
optional dependency in package.json, at sharp's own range. It was run
under Node 26 with no native package: it made a 512 px WebP thumbnail and
a GIF.

check-addon-arch.mjs now reads ELF (with byte order) and Mach-O (including
universal binaries) as well as PE, and follows npm's libc rule on Linux.

The script now runs in build-amd64, build-arm64 (in its arm64 container),
both macOS jobs, all three Windows jobs, and build-extra-arches through
install-node-for-arch.sh. There it runs with the target node's own
process.arch, so armv6/armv7/armhf are arm, ppc64le is ppc64 and i386 is
ia32. build-release-bundle.sh runs it for the local machine.

Against the published v12.32 zips, given their native addons, it added the
right sharp and libvips for all eleven Linux and macOS bundles, kept wasm32
for i386, and passed every bcrypt, argon2 and sharp binary. Checked the
other way, the s390x bundle was refused as ppc64.
tests/platformNativePackages.test.cjs covers the script with a mocked
registry (libvips, wasm fallback, wrong CPU, tampered tarball, path
traversal). tests/sharpEveryPlatform.test.cjs requires every job that makes
a bundle zip to run it for its own target, after the prune and before the
zip, and the wasm32 dependency to stay at sharp's version.

Thanks to xet7 !
2026-10-11 22:40:47 +03:00
Lauri Ojansivu 0e9e62cfa1 Document the Windows sharp and ARM64 argon2 fix
Thanks to xet7 !
2026-10-11 21:59:20 +03:00
Lauri Ojansivu b5e96ac682 Windows bundles: add sharp for their own CPU, and build argon2 for ARM64.
Every Windows bundle is made from the amd64 Linux bundle, so sharp, which
makes the attachment thumbnails, had only @img/sharp-linux-x64 there. It
could never load on Windows, and every thumbnail fell back to the full-size
image. sharp publishes @img/sharp-win32-ia32, -x64 and -arm64 with the
libvips DLLs included, so nothing needs compiling.
releases/windows-native-packages.mjs puts in the package at the version
sharp pins, checks it against the registry's sha512, and removes the
packages whose os/cpu name another platform (sharp-linux-x64, its libvips,
@napi-rs/canvas-linux-x64-gnu). canvas is only pdfjs-dist's optional
dependency for rendering pages; WeKan only extracts text with pdfjs, and
there is no win32-ia32 canvas.

Running the addon check on the published v12.32 zips found a worse fault:
the win-arm64 zip has no arm64 argon2. argon2 0.44 publishes no win32-arm64
prebuild, and the job set no npm_config_arch, so node-gyp-build's install
test passed in the x64 runner node with the win32-x64 prebuild and compiled
nothing. accounts-password imports argon2 with no fallback. The rebuild
step now sets npm_config_arch=arm64, so argon2 is cross-compiled with
Visual Studio's ARM64 tools, which the runner image includes.

All three Windows jobs now run windows-native-packages.mjs and then
check-addon-arch.mjs for bcrypt, argon2 and @img/sharp-win32-<arch>, before
the zip is made. check-addon-arch accepts scoped package names. Against the
published v12.32 zips, win64 passes after the script, and win-arm64 still
fails on argon2, which the npm_config_arch fix addresses at build time.
tests/windowsNativePackages.test.cjs covers the script with a mocked
registry, including a tampered tarball and a path-traversal entry.
tests/win32BundleBuild.test.cjs pins the steps in all three jobs and the
target CPU of the cross-built ones.

Thanks to xet7 !
2026-10-11 21:58:43 +03:00
Lauri Ojansivu f08acbea8a Document the win32 bundle build fix
Adds an Upcoming section for it, and notes in the released v12.32 entry that its win32 bundle was not published after all, and where the fix is.

Thanks to xet7 !
2026-10-11 21:45:19 +03:00
Lauri Ojansivu df48ef2aeb Fix the win32 bundle build: no drive-letter path for tar, and check the ia32 addons.
v12.32 was the first release whose build-win32 got past the Node.js lookup,
and it died at the first step it had never run:

  tar: D\:\a\\_temp/node-ia32: Cannot open: No such file or directory

RUNNER_TEMP is a Windows path, and GNU tar in Git Bash reads anything before
a colon as a remote host. The step now builds the nodedir on the POSIX form
of the path (cygpath -u), unpacks the headers by relative name inside it,
checks that include/node/node.h arrived, and gives node-gyp the Windows form
(cygpath -w).

The step after that had never run either. bcrypt and argon2 publish no
win32-ia32 prebuild, so they must be compiled for ia32, and accounts-password
imports both with no fallback. A cross-build that silently did nothing would
still zip and pass 7z t, and fail only at the first sign-in.
releases/check-addon-arch.mjs reads each shipped addon's PE header and fails
the job unless bcrypt and argon2 each have one for ia32. It skips packages
whose os/cpu name another platform, as npm does. On the published v12.31
win64 zip it passes as x64 and refuses the same zip as ia32.

tests/win32BundleBuild.test.cjs pins the new step, checks that no Windows
job in any workflow gives tar a RUNNER_TEMP or GITHUB_WORKSPACE path, and
tests the checker on synthetic PE and ELF files, positive and negative.

Thanks to xet7 !
2026-10-11 21:44:57 +03:00
github-actions[bot] a3ad3b8673 Bump versions for v12.32 2026-10-11 18:11:05 +00:00
Lauri Ojansivu eb457235fb Prepare v12.32 release 2026-10-11 21:08:51 +03:00
Lauri Ojansivu 0b38901ea8 Wrap the two over-long lines in the v12.31 CHANGELOG entry
Whitespace only: the Nextcloud entry's two paragraphs are rewrapped at 80 columns, with the same words in the same order, so tests/changelogFormat.test.cjs passes again.

Thanks to xet7 !
2026-10-11 21:08:06 +03:00
Lauri Ojansivu 807abaf1b3 Tidy the Upcoming CHANGELOG section
Shorten In short to three sentences covering all four topics, remove a UCS sentence pasted into the docs reorganization entry, describe the UCS topic as a whole, and wrap its long lines at 80 columns.

Thanks to xet7 !
2026-10-11 21:06:39 +03:00
Lauri Ojansivu 7175f2b9ff Document the card drop flicker fix
Thanks to mimZD and xet7 !
2026-10-11 21:04:51 +03:00
Lauri Ojansivu a8323cc39c Fix card flicker on drop: never show a dropped card in both lists.
The drop handler must call sortable('cancel'), which puts the dragged card
back at its source until Blaze re-renders the move. The drop preview from
b687f37272 fills the target slot during that gap, but the real card stayed
visible at the source, so the card was drawn in both lists. The reporter's
newest video shows that frame: card "2" already in the target list and still
in the source list with its old count, then the source catches up.

retireCardDropPreview() now also hides the node that sortable('cancel') put
back, with an inline style, which Blaze does not manage there:
- a move to another list keeps it hidden until Blaze removes it, even when
  the new card renders a flush earlier;
- a reorder within one list shows only the preview until Blaze moves the
  node beside it, instead of jumping the card back to its old place first;
- a rejected move, or one that never arrives within 10 seconds, shows the
  card again where it is, so no card is left invisible.

tests/cardDropPreviewSource.test.cjs drives the helper on a minimal DOM: the
video's cross-list drop, a reorder, a drop in place, a rejected move, a
timeout, and a source copy that is never removed. It also checks that every
caller passes the node to hide. It fails on the video's frame without this
fix. 37-card-drag-sort.e2e.js now checks the source while the preview is
shown, and adds a reorder within one list.

Thanks to mimZD and xet7 !

Fixes #6430,
2026-10-11 21:04:30 +03:00
Lauri Ojansivu 76129475a1 Document bundled UCS migration and registry fallback
Thanks to xet7 !
2026-10-11 21:01:43 +03:00
Lauri Ojansivu 7f4a8fa6c1 Bundle UCS migration tools in the WeKan image and use available registry mirrors
Run the temporary migration worker from the ordinary versioned application image. Prefer the target App Center compose images, then try the same upstream version on GHCR, Docker Hub and Quay before downtime. Reject inaccessible or incomplete images before portal upload. Cover UCS-only access, fallback, failure reporting, cleanup and migration integrity with regression tests.

Thanks to xet7 !
2026-10-11 21:01:12 +03:00
Lauri Ojansivu 23ff4fb200 Document the docs reorganization
Moves the oldest release that no longer fits CHANGELOG.md's size budget to old-CHANGELOG/2026/10.md.

Thanks to xet7 !
2026-10-11 19:54:00 +03:00
Lauri Ojansivu 5b04b4b47c Reorganize docs: platform pages under docs/Platforms, and update their links.
The autoupdate design pages move from docs/Design/Autoupdate to where their
platform lives: the per-OS pages to docs/Platforms/FOSS/OS/OS-future-alternatives,
Snap-Core and Snap-Ondra-Gantt to Container/Snap, UCS and Nextcloud to
Container/Docker, and Platforms.md to docs/Platforms. docs/Platforms/FOSS/Cloud
is split into Container/Docker (Helm, OpenShift, CloudFoundry) and
FOSS/CloudFlare; Friend and UCS move under Container/Docker; Leo moves to
Features/ImportExport/Leo.

Every link to a moved page is updated: relative links in the docs, including
the moved pages' own links to the rest of the tree, full docs/ paths in the
workflows, tests and comments, and the Upcoming CHANGELOG entry. Released and
archived CHANGELOG sections are records and keep their original links.
tests/docsLinksResolve.test.cjs passes.

Thanks to xet7 !
2026-10-11 19:53:18 +03:00
Lauri Ojansivu 95386b0f7b Document the win32 bundle fix
Thanks to xet7 !
2026-10-11 19:38:26 +03:00
Lauri Ojansivu 24e5cc4300 Build the win32 bundle again: find node-patches' Windows checksum sidecar.
wekan/node-patches publishes node-win32.exe with its checksum in
node-win32.sha256sum (without .exe), as the line `<hash> *node-win32.exe`.
releases/resolve-node-source.sh looked only for node-win32.exe.sha256sum and an
unmarked name, so it answered node_found=false and build-win32 skipped itself
every release, although both binaries were published. The resolver now also
tries the sidecar without .exe and accepts sha256sum's `*` binary-mode marker,
and strips CR. Verified against v26.11.1: win32 resolves, and the downloaded
node-win32.exe matches the published SHA256. tests/releaseNodeSources.test.cjs
runs the matcher on real sidecar lines, both positive and negative.

Thanks to xet7 !
2026-10-11 19:38:11 +03:00
Lauri Ojansivu 926845d97b Merge remote-tracking branch 'origin/main' 2026-10-11 18:58:03 +03:00
Lauri Ojansivu a742976538 Document the CPU platform table update
Thanks to xet7 !
2026-10-11 18:57:20 +03:00
Lauri Ojansivu 4ad51ec8eb Update the CPU platform table from the node-patches and FerretDB releases.
docs/Design/Autoupdate/Platforms.md now lists every binary that the newest
wekan/node-patches (v26.11.1, 16 targets) and wekan/FerretDB (v1.92.0, 25
targets) releases publish. Its "what is built" table matches what v12.30 and
v12.31 shipped and ghcr.io/wekan/wekan:v12.30 contains: armv7, armv6, win64,
win-arm64, mac-x64 and mac-arm64 bundles are added, riscv64's Node.js comes from
node-patches for v26.11.1, and loong64 is skipped. The doc also explains why
win32 is not built: the resolver looks for node-win32.exe.sha256sum, but
node-patches publishes node-win32.sha256sum. FreeBSD amd64 is listed as having
both binaries now.

Thanks to xet7 !
2026-10-11 18:56:55 +03:00
github-actions[bot] 482c7f05e8 Bump versions for v12.31 2026-10-11 15:25:46 +00:00
Lauri Ojansivu f7e925e42f Prepare v12.31 release 2026-10-11 18:23:51 +03:00
Lauri Ojansivu 81d2f85f59 Document the possible additional platforms
Thanks to xet7 !
2026-10-11 18:12:44 +03:00
Lauri Ojansivu ece7830a8e Document Sandstorm migration port handoff fix 2026-10-11 18:11:57 +03:00
Lauri Ojansivu 2c4c89e794 Preserve replacement Sandstorm bridge until Meteor takes its port 2026-10-11 18:11:42 +03:00
Lauri Ojansivu bb9c4c7044 Document the possible additional platforms, with ready files for five of them.
docs/Design/Autoupdate/Platforms.md opens with the open source platforms that
WeKan is not on yet, checked in each catalog on 2026-10-11, and what is still
missing for each. ZimaOS/CasaOS, TrueNAS, Coolify, Dokploy and the AUR get
the files their catalogs take and a README with the pull request each
expects, under docs/Platforms/. Each was tested with that catalog's own tools
in .tools/ where they run without the platform: CasaOS's validator and store
builder, the TrueNAS library render, Dokploy's three validators, and Arch's
makepkg, whose package started WeKan. tests/platformTemplates.test.cjs keeps
the image tag and settings the same across them.

Thanks to xet7 !
2026-10-11 18:11:35 +03:00
Lauri Ojansivu 65918a9570 Document Mac smoke cleanup and UCS credential storage fixes 2026-10-11 17:49:53 +03:00
Lauri Ojansivu a1e97a18c4 Pass UCS provider credentials through stdin instead of a plaintext file 2026-10-11 17:49:25 +03:00
Lauri Ojansivu 3c085a2d71 Fix Mac app smoke cleanup for exited process groups 2026-10-11 17:48:18 +03:00
Lauri Ojansivu 465aaa219e Merge remote-tracking branch 'origin/main' 2026-10-11 17:43:29 +03:00
Lauri Ojansivu c3a13373c0 Document Nextcloud ExApp support in the upcoming release 2026-10-11 17:41:50 +03:00
Lauri Ojansivu a325cfe991 Support Nextcloud ExApp identities and publish WeKan with embedded SQLite 2026-10-11 17:41:28 +03:00
github-actions[bot] 9592d6d820 Bump versions for v12.30 2026-10-11 14:17:51 +00:00
Lauri Ojansivu d76a4f4489 Prepare v12.30 release 2026-10-11 17:15:35 +03:00
Lauri Ojansivu 3042cf06f2 Document the Sandstorm client half and Powerbox button
Thanks to xet7 !
2026-10-11 17:02:52 +03:00
Lauri Ojansivu ed5502edfa Sandstorm: the client half runs again, and the Powerbox button adds people to the board.
sandstorm.js was imported only by server/imports.js since mainModule turned
eager loading off, so its browser code never ran in a grain: the grain URL and
title did not follow the board, the Powerbox picker could not open, and the
isSandstorm helper was undefined, so email, password, logout and avatar
settings that Sandstorm handles showed anyway. The server half - which loads
the Cap'n Proto bridge client and node:net - is now server/sandstorm.js, and
the client imports the shared sandstorm.js. Its shell listener ignores
messages that are not its replies instead of throwing on them.

The Powerbox button is back in the members widget, for people who may invite.
It passes the open board, and the claimed person is invited to it through
inviteUserToBoard, with the member picker's own permission checks; #2405
removed the old button because the identity it claimed was never attached to
a board.

Thanks to xet7 !
2026-10-11 17:02:20 +03:00
Lauri Ojansivu 2e927f59f3 Resolve latest published WeKan and FerretDB versions for manual UCS releases 2026-10-11 16:46:49 +03:00
Lauri Ojansivu 6b121be9da Run UCS and Nextcloud publishing only as standalone manual workflows 2026-10-11 16:37:58 +03:00
Lauri Ojansivu 3ad350b786 Document the Sandstorm port handoff fix
Thanks to xet7 !
2026-10-11 16:34:22 +03:00
Lauri Ojansivu 6e6b5d2e30 Fix Sandstorm grain showing an empty response while WeKan starts.
start.js killed the please-wait bridge and then loaded main.js, but
Meteor 3 awaits every Meteor.startup hook before webapp calls listen, so
nothing answered on port 4000 for all of WeKan's startup. The
sandstorm-http-bridge got connection-refused and the browser showed
NS_ERROR_NET_EMPTY_RESPONSE. The bridge now keeps the port until Meteor
binds it: the first listen on the app port waits for the bridge process
to exit and then binds, and logs "WeKan is listening on port 4000".

Thanks to xet7 !
2026-10-11 16:34:21 +03:00
Lauri Ojansivu b0d1397b16 Document the Import page sources in columns
Thanks to xet7 !
2026-10-11 16:15:49 +03:00
Lauri Ojansivu f44e52242c Import page: show every source under "Import from:" at once, in columns.
When there is horizontal room, the source list and the parts list flow
into as many columns as fit (a column width, not a count), so WeKan,
Trello, Jira and the rest are all on screen at once instead of below
the fold. A phone keeps one column.

Thanks to xet7 !
2026-10-11 16:15:07 +03:00
Lauri Ojansivu de6dcebffa Document UCS writable storage for attachments avatars and SQLite 2026-10-11 16:10:41 +03:00
Lauri Ojansivu 64002ea6e5 Prepare UCS releases with FerretDB version tags 2026-10-11 16:08:49 +03:00
Lauri Ojansivu 546d0e759f Document the Windows 7-Zip fix
Thanks to xet7 !
2026-10-11 16:06:00 +03:00
Lauri Ojansivu 6cb7e45b92 Windows bundles use the runner's 7-Zip instead of installing it from Chocolatey.
v12.29's build-win64 failed before building anything: community.chocolatey.org
answered 504 and `choco install -y 7zip` could not resolve 7zip.install.
windows-latest already has 7-Zip, so build-win64, build-win-arm64 and
build-win32 use it, and fall back to Chocolatey with five retries only on an
image without it.

Thanks to xet7 !
2026-10-11 16:05:42 +03:00
github-actions[bot] 6061337628 Bump versions for v12.29 2026-10-11 12:44:19 +00:00
Lauri Ojansivu 209957a811 Prepare v12.29 release 2026-10-11 15:42:32 +03:00
Lauri Ojansivu 16f1eb5606 Merge remote-tracking branch 'origin/main' 2026-10-11 15:41:51 +03:00
Lauri Ojansivu 8bb445604e Share UCS and Nextcloud publishing workflows across release and recovery 2026-10-11 15:35:33 +03:00
Lauri Ojansivu 6a695cdda8 Document Sandstorm Cap'n Proto in JavaScript and the capnp-es fork
Thanks to xet7 !
2026-10-11 15:30:52 +03:00
Lauri Ojansivu ee63d478e3 Sandstorm activity events and Powerbox identity claims work again on Node 26.
Both are Cap'n Proto calls on sandstorm-http-bridge's /tmp/sandstorm-api;
the bridge's HTTP proxy has no route for either. They went through
node-capnp (capnp.node), built for Node 14, so a grain ran without them.
imports/sandstorm/bridgeClient.mjs now makes the same calls in plain
JavaScript with @wekanteam/capnp-es, using schema classes generated from
Sandstorm's .capnp files by releases/sandstorm-capnp-schemas.sh. The
activity hook reads the board from the activity instead of the removed
hard-coded 'sandstorm' board, and a notification that cannot be sent no
longer fails the change. The .spk drops the 33.6 MB capnp.node.

tests/sandstormCapnpRpc.test.cjs drives every RPC message from a
hand-built peer, and runs the same cases plus WeKan's bridge calls against
the reference C++ RPC implementation with Sandstorm's schemas.

Thanks to xet7 !
2026-10-11 15:29:45 +03:00
Lauri Ojansivu 3eec76b366 Fork capnp-es as @wekanteam/capnp-es with complete RPC Resolve and Release.
npm capnp-es 0.0.16 (upstream commit 439f564), vendored like the other
npm-packages forks with its tarball SHA256 and npm integrity recorded. Its
experimental RPC Conn could not talk to a C++ peer such as
sandstorm-http-bridge: it ignored Resolve, Release and Disembargo, never
released an import, left tail-call answers (takeFromOtherQuestion) pending
forever, released question ids instead of export ids, answered bootstrap
with a null capability, and its DeferredTransport lost a message whenever
two arrived together. The fork handles all of them, and its compiler no
longer regenerates structs declared in another file, which made generated
Sandstorm schema modules a SyntaxError on load. Every change is in
npm-packages/capnp-es/patches/wekan.patch.

Thanks to xet7 !
2026-10-11 15:29:36 +03:00
Lauri Ojansivu 847106ea03 Document UCS 12.28 migration and remaining release steps 2026-10-11 14:55:02 +03:00
Lauri Ojansivu 97df539519 Document the Sandstorm grain log warnings fix
Thanks to xet7 !
2026-10-11 14:20:16 +03:00
Lauri Ojansivu 5e25090e8f Sandstorm grain logs no longer show false warnings.
start-memory.js turns Node's Web Storage off with
--no-experimental-webstorage, so Node 26 no longer prints "localStorage is
not available because --localstorage-file was not provided" into every
grain log, and it keeps an administrator's heap size or Web Storage choice
while still adding what is missing. The ROOT_URL email warning is skipped
in a grain, whose ROOT_URL is always its loopback address behind the bridge
and which has no SMTP of its own.

Thanks to xet7 !
2026-10-11 14:19:00 +03:00
github-actions[bot] b4f6618376 Bump versions for v12.28 2026-10-11 10:45:48 +00:00
Lauri Ojansivu b077ee722e Prepare v12.28 release 2026-10-11 13:43:42 +03:00
Lauri Ojansivu 9bec7602b9 Document the node suite fixes
Thanks to xet7 !
2026-10-11 13:38:02 +03:00
Lauri Ojansivu d6bee1bdaf Fix the six failing node suites.
Validation.md links models/eventLog.js by its real name, the README counts
the 166 catalogs now over 90% translated, and CHANGELOG.md is wrapped at 80
columns (whitespace only). The guards follow deliberate changes: Supported.md
belongs directly in docs/Databases, build.sh's macOS branch writes
export PATH="$HOME/.meteor:$PATH" since the npx meteor change, and the
wasmagic pin is checked against the lockfile instead of a copied number.

Thanks to xet7 !
2026-10-11 13:36:32 +03:00
Lauri Ojansivu a8a264a23b Document the Sandstorm runtime versions fix
Thanks to xet7 !
2026-10-11 13:12:37 +03:00
Lauri Ojansivu 242c4b097c Sandstorm uses the same Node.js, npm and FerretDB as other platforms.
The .spk now carries the exact Node.js the Dockerfile pins (26.11.1), which
is what https://wekan.fi/version.txt advertises, downloaded and verified
with releases/embed-verified-node.sh like the zip bundles, instead of
Meteor's dev-bundle node or the newest Node 24. FerretDB is fetched by the
release tag version.txt names, and the Sandstorm and Windows workflows
install the Dockerfile's npm with releases/use-release-npm.sh instead of a
hard-coded npm 12.0.2. Comments and docs describing the current runtime
say Node 26.

Thanks to xet7 !
2026-10-11 13:12:21 +03:00
Lauri Ojansivu 866d44da54 Document the Sandstorm start-memory.js fix
Thanks to xet7 !
2026-10-11 12:44:42 +03:00
Lauri Ojansivu d6427a5afb Fix Sandstorm grains crash-looping on ./start-memory.sh.
A grain has no shell: the meteor-spk 0.6.0 base ships only mongod, niscud
and node in bin/, so the pkgdef's #!/bin/sh start-memory.sh failed with
"execvp(argvp[0], argvp): No such file or directory" and no grain booted.
The memory-derived heap launcher is now start-memory.js, run by node.
start.js also no longer spawns the missing /bin/sleep, which had made every
wait return at once.

Thanks to xet7 !
2026-10-11 12:44:23 +03:00
Lauri Ojansivu d86a0556a8 Document installing Meteor with npx meteor
Thanks to xet7 !
2026-10-11 03:17:07 +03:00
Lauri Ojansivu 0dbf9d8502 Install Meteor with npx meteor, as the Meteor documentation says.
https://docs.meteor.com/about/install.html installs Meteor on Linux, macOS
and Windows alike with `npx meteor`, as the user and never with sudo; the
global npm install is only its fallback. npx runs the meteor package's bin,
which installs into ~/.meteor (%LOCALAPPDATA%\.meteor on Windows) and puts
it on PATH, so npm 12's refusal to run install scripts does not apply, and
the documented npm fallback flags that npm 12 rejects are not needed.

build.sh's Linux branch and build.bat now run npx -y meteor unless Meteor
is installed already, as the macOS branch did. build.sh removes the global
installer package an earlier setup left, and gives ~/.npm back to the user
if an old sudo npm made it root's. releases/rebuild-release.sh does the
same instead of sudo npm on Linux and brew install meteor on macOS.

Checked with npm 12.2.0: npx -y meteor installed Meteor 3.5.2 into a
scratch HOME and added it to .bashrc and .bash_profile.

Thanks to xet7 !
2026-10-11 03:17:01 +03:00
Lauri Ojansivu 9cf0b7b398 Document the npm 12 Meteor install fixes as one entry
Thanks to xet7 !
2026-10-11 03:12:50 +03:00
Lauri Ojansivu 9c3692f832 Let npm 12 run the Meteor installer's install script.
npm 12 runs no package's install script unless allow-scripts names it, and
only warns when it blocks one. The meteor npm package is just an installer:
its install script is what downloads Meteor, so Setup -> Install dependencies
finished with no Meteor. build.sh, build.bat and releases/rebuild-release.sh
now pass --allow-scripts=meteor, which npm 12.2.0 matches against the
registry URL's package name, so it allows that one script and nothing else.
Checked with npm 12.2.0 on a registry package: blocked without the flag, run
with it. npx -y meteor (macOS) runs the installer as the package's bin and
needs no change.

tests/npmNoUnsafePerm.test.cjs becomes tests/npmMeteorInstall.test.cjs and
also fails on a global npm install of meteor without --allow-scripts=meteor.

Thanks to xet7 !
2026-10-11 03:12:41 +03:00
Lauri Ojansivu 3286f05bb2 Document the npm 12 --foreground-scripts fix
Thanks to xet7 !
2026-10-11 03:07:50 +03:00
Lauri Ojansivu 3c755c1855 Install Meteor with --foreground-scripts, the full flag name npm 12 requires.
5c9334b76c passed --foreground-script, as Meteor's install documentation
spells it. npm 11 read that as an abbreviation; npm 12.2.0, which build.sh
installs from the Dockerfile's NPM_VERSION before installing Meteor, stops
with 'Invalid abbreviated flag'. Checked against npm 12.2.0: it accepts
--foreground-scripts and refuses both --foreground-script and --unsafe-perm.
The test now also fails on the abbreviated flag, in the scripts and in the
.agents notes it was copied from.

Thanks to xet7 !
2026-10-11 03:07:38 +03:00
Lauri Ojansivu c65bf12f05 Updated vscodium sandbox 2026-10-11 03:05:28 +03:00
Lauri Ojansivu 163149a94b Document the npm 11 Meteor install fix
Thanks to xet7 !
2026-10-11 03:04:38 +03:00
Lauri Ojansivu 5c9334b76c Install Meteor with --foreground-script, which npm 11 accepts.
Setup -> Install dependencies stopped with EUNKNOWNCONFIG right after n
installed the pinned Node and npm 11: npm 7 removed --unsafe-perm and npm 11
refuses an unknown cli flag. build.sh and releases/rebuild-release.sh now
pass --foreground-script, as Meteor's own install documentation does, and
tests/npmNoUnsafePerm.test.cjs fails when any script passes the old flag.

Thanks to xet7 !
2026-10-11 03:03:53 +03:00
Lauri Ojansivu da9c08db64 Merge pull request #6760 from wekan/dependabot/npm_and_yarn/aws-sdk/lib-storage-3.1147.0
Bump @aws-sdk/lib-storage from 3.1137.0 to 3.1147.0
2026-10-10 23:45:31 +03:00
dependabot[bot] f83b2555ec Bump @aws-sdk/lib-storage from 3.1137.0 to 3.1147.0
Bumps [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) from 3.1137.0 to 3.1147.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1147.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.1147.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:44:35 +00:00
Lauri Ojansivu a93b97ff97 Merge pull request #6759 from wekan/dependabot/npm_and_yarn/sanitize-html-2.18.0
Bump sanitize-html from 2.17.7 to 2.18.0
2026-10-10 23:42:33 +03:00
Lauri Ojansivu 946a2cc4ba Merge pull request #6758 from wekan/dependabot/npm_and_yarn/enhanced-resolve-5.26.0
Bump enhanced-resolve from 5.25.1 to 5.26.0
2026-10-10 23:37:31 +03:00
Lauri Ojansivu 5820a04e68 Merge pull request #6757 from wekan/dependabot/npm_and_yarn/aws-sdk/client-s3-3.1147.0
Bump @aws-sdk/client-s3 from 3.1137.0 to 3.1147.0
2026-10-10 23:37:10 +03:00
Lauri Ojansivu ec8d448944 Merge pull request #6756 from wekan/dependabot/npm_and_yarn/wasmagic-1.0.11
Bump wasmagic from 1.0.10 to 1.0.11
2026-10-10 23:36:21 +03:00
dependabot[bot] 77b53613d2 Bump sanitize-html from 2.17.7 to 2.18.0
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.7 to 2.18.0.
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/sanitize-html@2.18.0/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:33:58 +00:00
dependabot[bot] a08793d82c Bump enhanced-resolve from 5.25.1 to 5.26.0
Bumps [enhanced-resolve](https://github.com/webpack/enhanced-resolve) from 5.25.1 to 5.26.0.
- [Release notes](https://github.com/webpack/enhanced-resolve/releases)
- [Changelog](https://github.com/webpack/enhanced-resolve/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/enhanced-resolve/compare/v5.25.1...v5.26.0)

---
updated-dependencies:
- dependency-name: enhanced-resolve
  dependency-version: 5.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:33:43 +00:00
dependabot[bot] 89da3af177 Bump @aws-sdk/client-s3 from 3.1137.0 to 3.1147.0
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.1137.0 to 3.1147.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1147.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1147.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:33:31 +00:00
dependabot[bot] 3d713c0205 Bump wasmagic from 1.0.10 to 1.0.11
Bumps [wasmagic](https://github.com/moshen/wasmagic) from 1.0.10 to 1.0.11.
- [Commits](https://github.com/moshen/wasmagic/compare/v1.0.10...v1.0.11)

---
updated-dependencies:
- dependency-name: wasmagic
  dependency-version: 1.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:33:00 +00:00
Lauri Ojansivu 0ad5089742 Merge remote-tracking branch 'origin/main' 2026-10-10 21:21:21 +03:00
Lauri Ojansivu d09ade4419 Document Gujarati translation progress
Record the completed current catalog, validation and remaining all-language inventory.

Thanks to xet7 !
2026-10-10 21:15:26 +03:00
Lauri Ojansivu 91ebd76c18 Translate remaining current Gujarati strings
Fill 196 English placeholders while preserving existing translations and exact source tokens. Verify script, import exclusions, field permissions, notification delivery and destructive versus resumable recovery actions. Longer Gujarati technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Five browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 21:15:00 +03:00
Lauri Ojansivu d68ced18dc Document Hindi translation progress
Record both completed current catalogs, validation and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 21:09:13 +03:00
Lauri Ojansivu 08f1aa2cfd Translate remaining current Hindi strings
Fill 392 English placeholders across both Hindi catalogs while preserving existing translations and exact source tokens. Verify import exclusions, field permissions, notification delivery and destructive versus resumable recovery actions. Longer Hindi technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Ten browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 21:08:44 +03:00
Lauri Ojansivu 02df92478b Document Traditional Chinese translation progress
Record three completed current catalogs, validation and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 21:03:38 +03:00
Lauri Ojansivu 9c4477ef32 Translate remaining current Traditional Chinese strings
Fill 678 English placeholders across three catalogs while preserving existing translations and exact tokens. Verify script, import exclusions, field permissions, notifications, Scrum and recovery meanings. Keep specialist wording provisional pending speaker review and broader language auditing open.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Fifteen browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 21:03:08 +03:00
Lauri Ojansivu 348e35574b Document Simplified Chinese translation progress
Record six completed current catalogs, validation and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:57:58 +03:00
Lauri Ojansivu ee2626fe2e Translate remaining current Simplified Chinese strings
Fill 1356 English placeholders across six catalogs while preserving existing translations and exact tokens. Verify import exclusions, field permissions, notifications, Scrum and recovery meanings. Keep specialist wording provisional pending speaker review and broader language auditing open.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Thirty browser cases pass syntax and discovery, including the zh-SG runtime tag; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:57:27 +03:00
Lauri Ojansivu ad646fc444 Document Arabic legacy-language corrections
Record the reviewed candidate queue, retained Arabic values, regression coverage and remaining broader language audit.

Thanks to xet7 !
2026-10-10 20:52:10 +03:00
Lauri Ojansivu 3481a310cb Replace legacy Persian text in Arabic catalogs
Review all 456 remaining seed-language candidates. Correct 358 values per catalog and retain 98 valid/shared Arabic values, for 1074 corrections across ar, ar-DZ and ar-EG. Restore activity, email, rule, import and analytics meanings while preserving exact tokens and unchanged translations. Longer rule fragments, color shades and analytics wording remain low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three Arabic rule-color browser cases pass syntax and discovery; runtime remains unverified. Broader unflagged language review remains open.

Thanks to xet7 !
2026-10-10 20:51:41 +03:00
Lauri Ojansivu 118dceb7e2 Document Arabic translation progress and remaining language audit
Record current placeholder fills, Persian core-label repairs, validation and the still-open older wrong-language review.

Thanks to xet7 !
2026-10-10 20:45:42 +03:00
Lauri Ojansivu 2204c5693d Translate current Arabic strings and repair Persian core labels
Fill 678 English placeholders across three Arabic catalogs and replace 18 wrong-language core values. Preserve other translations and exact source tokens. Add permission, import, notification, Scrum and recovery regressions. Longer technical wording remains low-confidence pending speaker review; the broader Persian-seed audit remains open.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Fifteen browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:45:09 +03:00
Lauri Ojansivu 35090748e5 Document Hebrew translation progress
Record both completed current catalogs, regression coverage and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:39:31 +03:00
Lauri Ojansivu 6f11e15f7d Translate remaining current Hebrew strings
Fill 396 English placeholders across both Hebrew catalogs while preserving existing translations and exact source tokens. Verify import exclusions, field permissions, notification delivery and destructive versus resumable recovery actions. Longer Hebrew technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Ten browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:39:05 +03:00
Lauri Ojansivu 7647ed2ec0 Document Persian translation progress
Record both completed current catalogs, regression coverage and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:34:31 +03:00
Lauri Ojansivu 973215e72e Translate remaining current Persian strings
Fill 398 English placeholders across both Persian catalogs while preserving existing translations and exact source tokens. Check import exclusions, field permissions, notification delivery and destructive versus resumable recovery actions. Longer Persian technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Ten browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:34:03 +03:00
Lauri Ojansivu 21525ad916 Document Estonian translation progress
Record the completed current catalog, regression coverage and remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:29:02 +03:00
Lauri Ojansivu 2a19a286ab Translate remaining current Estonian strings
Fill 110 English placeholders while preserving existing translations and exact source tokens. Verify permissions, synchronization directions, import semantics, notification timing and workflow completion states. Longer technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:28:33 +03:00
Lauri Ojansivu 8e9236776a Document Afrikaans translation progress
Record both completed current catalogs, regression coverage and the remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:25:01 +03:00
Lauri Ojansivu 4c5bf48b53 Translate remaining current Afrikaans strings
Fill 240 English placeholders across both Afrikaans catalogs while preserving existing translations and source tokens. Check field permissions, import exclusions, notification delivery and workflow completion states. Longer technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Six browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:24:32 +03:00
Lauri Ojansivu 284b8a96d4 Document Belarusian translation progress
Record the translated current catalog, regression coverage and remaining all-language inventory.

Thanks to xet7 !
2026-10-10 20:20:17 +03:00
Lauri Ojansivu 7c187e7318 Translate remaining current Belarusian strings
Fill 130 English placeholders while preserving existing translations and exact source tokens. Cover permissions, import exclusions, notification delivery and workflow states. Longer Belarusian technical wording remains low-confidence pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime remains unverified on this host.

Thanks to xet7 !
2026-10-10 20:19:27 +03:00
Lauri Ojansivu aee55ee2d7 Document Icelandic translation fills and validation
Record the 130-value batch and refresh the full remaining placeholder count.

Thanks to xet7.
2026-10-10 20:11:51 +03:00
Lauri Ojansivu 4aa0bd17b3 Translate remaining current Icelandic strings
Fill 130 English placeholders, including 20 import-format instructions, while preserving existing translations and exact tokens. Replace the historical completion gate with the full current catalog. Longer Icelandic technical wording remains provisional pending native-speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 20:11:09 +03:00
Lauri Ojansivu bb7c69e417 Document Bosnian translation fills and validation
Record the 110-value batch and refresh the full remaining placeholder count.

Thanks to xet7.
2026-10-10 20:05:34 +03:00
Lauri Ojansivu 7f0ee4463c Translate remaining current Bosnian strings
Fill 110 English placeholders while preserving existing translations, established Bosnian terminology and exact tokens. Longer technical wording remains provisional pending native-speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 20:05:04 +03:00
Lauri Ojansivu c215db4f68 Document Albanian translation fills and validation
Record the 110-value batch and refresh the full remaining placeholder count.

Thanks to xet7.
2026-10-10 20:01:32 +03:00
Lauri Ojansivu 9cd88a43c4 Translate remaining current Albanian strings
Fill 110 English placeholders while preserving existing translations and exact tokens. Replace the historical completion gate with the full current catalog and verify action meanings. Longer Albanian technical wording remains provisional pending native-speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 20:00:58 +03:00
Lauri Ojansivu 19b001e2de Document Latvian translation fills and validation
Record the 110-value batch and refresh the full remaining placeholder count.

Thanks to xet7.
2026-10-10 19:56:56 +03:00
Lauri Ojansivu e4908540ef Translate remaining current Latvian strings
Fill 110 English placeholders while preserving existing translations and exact tokens. Check link directions, permissions, import semantics, notification settings and completion states. Longer Latvian technical wording remains provisional pending native-speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 19:56:25 +03:00
Lauri Ojansivu b5a3042c1a Document Macedonian vocabulary review results
Record 262 corrections, 55 retained shared values and validation after updating the historical JSON example record.

Thanks to xet7.
2026-10-10 19:52:09 +03:00
Lauri Ojansivu 5f005ecfe1 Correct remaining reviewed Macedonian vocabulary matches
Review 317 long values matching Bulgarian or Serbian: correct 262 wrong-language or meaning-drifted values and retain 55 valid shared terms. Preserve all other values and exact tokens. Update the existing JSON example correction record after translating its Serbian sample prose into Macedonian. Specialized Macedonian wording remains lower confidence pending native-speaker review.

The 306-suite run passed 305 suites and exposed the stale audit record in one. After correcting the record, that suite and the focused audit and Macedonian checks pass on rerun. All 246 catalog inventories and 21 preservation checks pass. Existing browser cases pass syntax and discovery; runtime remains unverified.

Thanks to xet7.
2026-10-10 19:51:36 +03:00
Lauri Ojansivu 0b08c66446 Document Macedonian fills and language corrections
Record the 130 fills, 154 reviewed corrections, validation and remaining semantic audit.

Thanks to xet7.
2026-10-10 19:46:17 +03:00
Lauri Ojansivu 496acb14e7 Fill Macedonian strings and correct mixed-language values
Translate 130 English placeholders and replace 154 reviewed wrong-language or mixed-language values with Macedonian. Preserve all other translations, exact placeholders, parsed constants and markup. Longer Macedonian technical descriptions and legacy corrections have lower confidence pending native-speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies. Record 317 additional cross-locale matches for individual language review rather than treating placeholder completion as fluency.

Thanks to xet7.
2026-10-10 19:45:44 +03:00
Lauri Ojansivu 206057b690 Document Bulgarian translation fills and validation
Record the 110-value batch and current full placeholder backlog.

Thanks to xet7.
2026-10-10 19:39:40 +03:00
Lauri Ojansivu bcd043e676 Translate remaining current Bulgarian strings
Fill 110 English placeholders while preserving existing translations, established Bulgarian terminology and exact tokens. Longer technical wording remains provisional pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Three browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 19:39:10 +03:00
Lauri Ojansivu e046eac309 Document Croatian Serbian and Slovenian translation fills
Record the 440-value batch and validation, and refresh the full remaining placeholder count.

Thanks to xet7.
2026-10-10 19:35:51 +03:00
Lauri Ojansivu 5a68457ad6 Translate current Croatian Serbian and Slovenian strings
Fill 440 English placeholders across four catalogs, preserving existing translations, established terminology and exact placeholders. Longer technical wording remains provisional pending speaker review.

All 306 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Twelve browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 19:35:22 +03:00
Lauri Ojansivu 044288ad5b Document Vietnamese fills and full translation backlog
Record the 414-value batch and validation. Include pending Transifex keys in the reported full remaining scope.

Thanks to xet7.
2026-10-10 19:30:07 +03:00
Lauri Ojansivu 5a35e57aaf Translate remaining current Vietnamese strings
Fill 414 English placeholders across vi and vi-VN, preserving existing translations and exact tokens. Cover imports, linked fields, notifications, Scrum planning and recovery choices. Longer technical wording remains provisional pending speaker review.

All 305 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Six browser cases pass syntax and discovery; runtime execution remains unverified without the app and browser system dependencies. Record the full remaining placeholder inventory, including pending Transifex keys.

Thanks to xet7.
2026-10-10 19:29:36 +03:00
github-actions[bot] 7e707e8c63 Bump versions for v12.27 2026-10-10 16:26:16 +00:00
Lauri Ojansivu 9498840c55 Document Basque translation review and validation
Record the current fill completion and follow-up corrections without editing the released section.

Thanks to xet7.
2026-10-10 19:25:31 +03:00
Lauri Ojansivu 480e3639f6 Refine and validate current Basque translations
Review the 196-placeholder batch included in e595f9a83b and correct noun forms in 23 newly filled values. Preserve all pre-existing translations. Longer technical wording remains provisional pending speaker review.

All 305 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass; the focused Basque regression passes after corrections. Three browser cases pass syntax and discovery, with runtime execution unverified without the app and browser system dependencies.

Thanks to xet7.
2026-10-10 19:24:59 +03:00
Lauri Ojansivu f3a231fe38 Prepare v12.27 release 2026-10-10 19:23:05 +03:00
Lauri Ojansivu e595f9a83b Updated translations. 2026-10-10 19:22:45 +03:00
Lauri Ojansivu 0cc6997440 Merge remote-tracking branch 'origin/main' 2026-10-10 19:22:22 +03:00
Lauri Ojansivu f208bf1b29 Document current Galician translation fills
Record the 220-value batch, validation and remaining all-language audit work.

Thanks to xet7.
2026-10-10 19:19:18 +03:00
Lauri Ojansivu c5f1ea975c Translate remaining current Galician strings
Fill 220 English placeholders across both Galician catalogs while preserving existing translations, placeholders and parsed constants. Longer technical wording is provisional pending speaker review.

All 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks pass. Six browser cases pass syntax and discovery; runtime execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 19:18:47 +03:00
Lauri Ojansivu 36ad88b5f7 Document current Valencian translation completion
Record regional fills, shared native label handling, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 19:15:08 +03:00
Lauri Ojansivu b6261f8a46 Translate remaining current Valencian strings
Fill 109 English placeholders using reviewed regional forms, preserving existing translations and exact tokens. Retain the native shared Dates label with locale- and source-specific guards. Longer technical and regional wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register three browser cases using ca-valencia; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 19:14:36 +03:00
Lauri Ojansivu b7fa26f929 Document current Catalan translation completion
Record Catalan fills, shared native label handling, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 19:11:08 +03:00
Lauri Ojansivu 9485d19719 Translate remaining current Catalan strings
Fill 218 English placeholders across ca and ca_ES, preserving existing translations and exact tokens. Retain the native shared Dates label with locale- and source-specific guards. Longer technical wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register six browser cases; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 19:10:35 +03:00
Lauri Ojansivu fea58e1312 Document which databases WeKan supports, and how each is tested.
docs/Databases/Supported.md is the one list: FerretDB v1 with SQLite (the
default everywhere), PostgreSQL (works), MySQL, MariaDB and SAP HANA
(experimental), FerretDB 2 (not tested), and MongoDB 6, 7, 8 and 9 - with
what checks each: the 110-query conformance catalogue answers the same on
all of them, and WeKan's own tests run on Meteor's bundled MongoDB 8 and on
FerretDB. It also says what to know about MongoDB versions: the Linux
kernel 6.19-7.0.13 refusal and its fixes, AVX, the driver, and upgrades.

The README and the Status section of CHANGELOG.md gain a Databases list,
docs/Databases/README.md links the page, the Meteor 2.14 compatibility guide
is marked historical, and the conformance page counts 110 cases and the
MongoDB backends. CLAUDE.md and AGENTS.md describe the new Status block.

Thanks to xet7 !
2026-10-10 19:07:14 +03:00
Lauri Ojansivu 61586e3da5 Document current Greek translation completion
Record Greek fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 19:05:58 +03:00
Lauri Ojansivu 7f26c8ee12 Translate remaining current Greek strings
Fill 220 English placeholders across el and el-GR while preserving existing translations and exact tokens. Longer technical wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register six browser cases; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 19:05:30 +03:00
Lauri Ojansivu e7affef910 Updated ChangeLog: MongoDB 6 and 7 conformance.
Thanks to xet7 !
2026-10-10 19:02:50 +03:00
Lauri Ojansivu debd1d876d Database conformance: compare MongoDB 6 and 7 too.
Beside MongoDB 8 and 9, the conformance run that EVERYTHING runs now also
asks the real MongoDB 6 (mongo:6.0) and MongoDB 7 - the mongo:7 image read
from docker-compose-mongodb-v7.yml, the one WeKan's own MongoDB install runs.
All eight agree on all 110 cases: MongoDB 6.0.28, 7.0.43, 8.0.32 and 9.0.2,
and FerretDB on SQLite, PostgreSQL, MySQL and MariaDB.

Thanks to xet7 !
2026-10-10 19:02:36 +03:00
Lauri Ojansivu 0b5e938db4 Document current Malay translation completion
Record Malay fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 19:02:02 +03:00
Lauri Ojansivu a0c7a0a40a Translate remaining current Malay strings
Fill 352 English placeholders across ms and ms-MY while preserving existing translations and exact tokens. Cover current linking, import and notification features plus LDAP and recovery. Longer technical wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register six browser cases; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 19:01:33 +03:00
Lauri Ojansivu 937cc77218 Document current Indonesian translation completion
Record Indonesian fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:57:54 +03:00
Lauri Ojansivu ef6a8178d1 Translate remaining current Indonesian strings
Fill 176 English placeholders while preserving existing translations and exact tokens. Cover current linking, import and notification features plus LDAP and recovery. Longer technical wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register three browser cases; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:57:18 +03:00
Lauri Ojansivu 7bd1042417 Updated ChangeLog: FerretDB matches MongoDB, MongoDB 8 and 9 conformance, build.bat parity.
The Upcoming section after v12.26 gets its bug fix and developer-facing
subsections ahead of the translations, a translations subsection header, the
closing line thanking translators, and an In short covering all of it. Long
TODO Later lines are wrapped at 80 columns (whitespace only).

Thanks to xet7 !
2026-10-10 18:56:09 +03:00
Lauri Ojansivu c454f8567d build.bat: do what build.sh does, not only offer the same menu.
The menu checks already matched every entry; these labels existed but did
less than their build.sh twins:

- Mocha runs with --full-app, EMAIL_RECEIPT_SWEEP_INTERVAL_MS, WRITABLE_PATH
  and its own METEOR_LOCAL_DIR, and returns its exit status instead of 0.
- `build.bat --run-everything [mode]` works, as `./build.sh --run-everything`.
- The floating-promises guard fails instead of only warning.
- Playwright browsers live in .tools\ms-playwright and a single-browser run
  sets WEKAN_PLAYWRIGHT_PROJECT; .tools\.meteor is on PATH.
- CUSTOM PORT + SUBDOMAIN: a bare name is NAME.localhost, the port is
  checked, a trailing slash is removed, and the WEKAN_DEV_* overrides work.
- The trace dev server uses the computed heap, not a fixed 8192 MB.
- The Docker image tag is read from the compose file; "Kill all dev
  servers" also stops `rspack build --watch`.
- The build sets the same diagnostics (npm verbose, METEOR_PROFILE, module
  resolution, the command-output tracer), supports
  WEKAN_BUILD_HEAP_SNAPSHOT, and explains an out-of-heap failure.
- Setup installs Node.js and npm at the Dockerfile's NODE_VERSION and
  NPM_VERSION. build.sh pinned npm@12.0.2 by hand while the Dockerfile says
  12.2.0; it now reads NPM_VERSION too.

tests/buildScriptParity.test.cjs checks each of these in both scripts.

Thanks to xet7 !
2026-10-10 18:55:04 +03:00
Lauri Ojansivu a7ed9ac57f Document current Korean translation completion
Record Korean fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:53:43 +03:00
Lauri Ojansivu a56a3abb9e Translate remaining current Korean strings
Fill 412 English placeholders across ko and ko-KR, preserving existing translations and exact tokens. Cover current linking, import and notification features plus planning and recovery. Longer technical wording remains provisional pending speaker review.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register six browser cases; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:53:23 +03:00
Lauri Ojansivu 277445e6f2 Document current Hiragana fills and remaining script audit
Record completed English placeholder fills, script regressions and older review candidates in Upcoming.

Thanks to xet7.
2026-10-10 18:49:30 +03:00
Lauri Ojansivu fb336423fd Translate current Japanese Hiragana placeholders
Fill 206 English entries with hiragana prose and preserve technical literals and tokens. Longer technical wording and spacing remain provisional pending reader review. Record 3641 older kanji or katakana values as script-review candidates, not completed conversion.

Verify 305 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register three browser cases using the ja-Hira runtime tag; syntax and discovery pass, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:49:04 +03:00
Lauri Ojansivu 14b01778b4 Document standard Japanese translation completion
Record Japanese fills, regression coverage and the remaining Hiragana script review in Upcoming.

Thanks to xet7.
2026-10-10 18:43:52 +03:00
Lauri Ojansivu 08cba19eac Database conformance: compare MongoDB 8 and 9 too, in EVERYTHING.
releases/db-conformance.sh, which EVERYTHING runs from build.sh and, through
releases/run-everything.sh, from build.bat, now also runs the query catalogue
against the real MongoDB 8 and MongoDB 9 servers (mongo:8.0 and mongo:9.0),
queried directly with no FerretDB in between. All FerretDB backends could
agree with each other and still all be wrong; beside MongoDB they did not:
$group gave every accumulator after the first an empty group, $min/$max took
null as a value, and capped max was not enforced at once - all fixed in
wekan/FerretDB. WeKan's e-mail outbox report, one $group with several
accumulators, was affected on FerretDB.

The $text case now creates the text index MongoDB requires, and records the
score by type only, since FerretDB's score is its own by design. On a Linux
kernel MongoDB refuses (6.19 to 7.0.13), its backend is a SKIP with the
reason, not an error. The run: SQLite, MongoDB 8.0.32, MongoDB 9.0.2,
PostgreSQL, MySQL and MariaDB answer all 110 cases the same.

buildScriptParity: releases/translations/audit-short-prose.mjs is a
read-only review queue, run directly, like audit-progress.mjs.

Thanks to xet7 !
2026-10-10 18:43:44 +03:00
Lauri Ojansivu 9b4d16b1ce Translate remaining current standard Japanese strings
Fill 412 English placeholders across ja and ja-JP, preserving existing translations and exact tokens. Cover current linking, import and notification features plus planning and recovery. Longer technical wording remains provisional pending speaker review. The Hiragana catalog remains a separate script and translation follow-up.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Add five browser cases alongside existing Japanese notification coverage; six cases pass syntax and discovery, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:43:31 +03:00
Lauri Ojansivu b338c8e171 Document current Russian translation completion
Record Russian fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:38:41 +03:00
Lauri Ojansivu e1774f913c Translate remaining current Russian strings
Fill 330 English placeholders across three Russian catalogs while preserving existing translations and exact source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Add eight browser cases alongside existing Russian notification coverage; nine cases pass syntax and discovery, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:38:19 +03:00
Lauri Ojansivu cb8594f682 Document current Ukrainian translation completion
Record Ukrainian fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:35:09 +03:00
Lauri Ojansivu 782a778f28 Translate remaining current Ukrainian strings
Fill 220 English placeholders across both Ukrainian catalogs while preserving existing translations and exact source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Add five browser cases alongside existing Ukrainian notification coverage; six cases pass syntax and discovery, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:34:49 +03:00
Lauri Ojansivu 6a34711770 Document current Turkish translation completion
Record Turkish fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:31:17 +03:00
Lauri Ojansivu 05a077cbbe Translate remaining current Turkish strings
Fill 110 English placeholders while preserving existing translations and exact source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Add two browser cases alongside existing Turkish notification coverage; three cases pass syntax and discovery, but execution remains unverified without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:30:57 +03:00
Lauri Ojansivu 2b823a9125 Document Romanian fills and field label corrections
Record Romanian translation scope, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:27:46 +03:00
Lauri Ojansivu ab22344f91 Fill current Romanian strings and correct Italian field labels
Translate 220 English placeholders across Romanian catalogs and correct two Italian custom-field labels. Preserve other existing translations and exact source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register six browser cases, checked by syntax and discovery; execution remains unavailable without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:27:25 +03:00
Lauri Ojansivu da36723bc0 Document current Hungarian translation completion
Record Hungarian fills, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:24:06 +03:00
Lauri Ojansivu 0ec3ab7731 Translate remaining current Hungarian strings
Fill 110 English placeholders while preserving existing translations and exact source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register three browser cases, checked by syntax and discovery; execution remains unavailable without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:23:47 +03:00
Lauri Ojansivu 361ae847ea Document Slovak fills and Czech wording corrections
Record translation scope, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:20:27 +03:00
Lauri Ojansivu 1d76d08c7f Fill current Slovak strings and correct Czech seed wording
Translate 110 English placeholders and replace 198 Czech or mixed-language values with Slovak. Preserve other existing values and every source token. Longer technical phrasing remains provisional pending speaker review; this is not a full audit of older Slovak text.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Add three browser cases, verified by syntax and discovery only; execution remains unavailable without the local app and browser dependencies.

Thanks to xet7.
2026-10-10 18:20:05 +03:00
Lauri Ojansivu 6a07089fdd Document current Czech translation completion
Record Czech catalog fills, reuse provenance, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:15:27 +03:00
Lauri Ojansivu 0ac133b2f4 Translate remaining current Czech strings
Fill 316 English placeholders across Czech catalogs, including 96 regional values reused from existing base Czech translations. Preserve existing translated values and source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register and syntax-check six browser cases; browser execution remains unverified without the local app and system dependencies.

Thanks to xet7.
2026-10-10 18:15:07 +03:00
Lauri Ojansivu 0d80fb4d7e Document current Polish translation completion
Record Polish catalog fills, reuse provenance, regression coverage and remaining review limits in Upcoming.

Thanks to xet7.
2026-10-10 18:11:31 +03:00
Lauri Ojansivu ff097539bf Translate remaining current Polish strings
Fill 316 English placeholders across Polish catalogs, including 96 regional values reused from existing base Polish translations. Preserve all existing translated values and source tokens. Longer technical wording remains provisional pending speaker review.

Verify 304 translation/Blockly suites, 246 catalog inventories and 21 human-preference checks. Register and syntax-check six browser cases; browser execution remains unverified without the local app and system dependencies.

Thanks to xet7.
2026-10-10 18:11:06 +03:00
Lauri Ojansivu 35c26ae472 Document current Dutch translation completion
Record the 220-string batch, validation and remaining all-language work in Upcoming.

Thanks to xet7.
2026-10-10 18:07:29 +03:00
Lauri Ojansivu 6f18ec01d9 Translate remaining current Dutch strings
Fill 220 English values across nl and nl-NL. Preserve existing translations, exact source tokens and workflow literals. Extend full-current-fill checks and representative UI coverage for linked cards, imports and notifications.

All 304 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Six browser cases pass syntax and discovery; execution remains unverified because the local app and browser dependencies are unavailable. Longer technical wording remains provisional pending speaker review.

Thanks to xet7.
2026-10-10 18:07:04 +03:00
Lauri Ojansivu 911278d7ad Document current Portuguese translation completion
Record the 440-string regional batch, validation and remaining all-language work in Upcoming.

Thanks to xet7.
2026-10-10 18:03:54 +03:00
Lauri Ojansivu 465b62605d Translate current strings across Portuguese locale variants
Fill 440 English values across pt, pt-BR, pt-PT and pt_PT. Preserve existing translations, exact source tokens and workflow literals. Use European and Brazilian terminology in the corresponding catalogs and extend full-current-fill and regional meaning checks.

All 304 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Twelve browser cases pass syntax and discovery; execution remains unverified because the local app and browser dependencies are unavailable. Longer technical and regional wording remains provisional pending speaker review.

Thanks to xet7.
2026-10-10 18:03:27 +03:00
Lauri Ojansivu 11a8aa948e Document current Spanish translation completion
Record the 990-string batch, validation and remaining all-language work in Upcoming.

Thanks to xet7.
2026-10-10 17:59:54 +03:00
Lauri Ojansivu 5146f9365c Translate current strings across Spanish locale variants
Fill 990 English values across nine Spanish catalogs. Preserve existing regional translations, source tokens and parsed constants. Extend full-current-fill and meaning checks for linked cards, imports, notifications and workflows.

All 304 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Twenty-seven browser cases pass syntax and discovery; execution remains unverified because the local app and browser dependencies are unavailable. New wording uses neutral Spanish; regional style and longer technical wording remain provisional pending speaker review.

Thanks to xet7.
2026-10-10 17:59:31 +03:00
Lauri Ojansivu a700ebab7e Document current French translation completion
Record the 540-string batch, exact shared French terms and validation in Upcoming.

Thanks to xet7.
2026-10-10 17:56:16 +03:00
Lauri Ojansivu 78bceed7d7 Translate current strings across French locale variants
Fill 540 English values across five French catalogs. Preserve existing translations, tokens and parsed constants. Retain Dates and __count__ minutes as correct French with locale-specific exact-source exceptions, and test source changes and locale isolation.

All 304 translation/Blockly suites, 246 catalog inventories and 21 preservation checks pass. Fifteen representative browser cases pass syntax and discovery; execution remains unverified because the local app and browser dependencies are unavailable. Longer technical and regional wording remains provisional pending speaker review.

Thanks to xet7.
2026-10-10 17:55:50 +03:00
Lauri Ojansivu 4b1d675096 Document current German translations in Upcoming release
Record the 440-string fill and 86 Swiss spelling corrections separately from the prepared v12.26 release.

Thanks to xet7.
2026-10-10 17:49:29 +03:00
Lauri Ojansivu ee9fbe1cfd Translate current strings across German locale variants
Fill 440 English values across de, de-AT, de-CH and de_DE. Normalize 86 older Swiss German values from ß to ss without otherwise changing their wording. Preserve all other existing translations and exact source placeholders.

All 304 translation/Blockly suites pass; 16 German/Blockly suites pass after spelling corrections. All 246 catalog inventories and 21 preservation checks pass. Twelve browser cases pass syntax and discovery; execution remains unverified because the local app and browser dependencies are unavailable. Longer technical wording remains provisional pending speaker review.

Thanks to xet7.
2026-10-10 17:48:37 +03:00
3481 changed files with 632278 additions and 476379 deletions

No files matched your search

@@ -72,7 +72,7 @@ installer permanently to global npm packages.
| Latest supported CLI | Run `npx meteor`. |
| Pinned installer package | Run `npx meteor@<version> install`, then verify the reported Meteor release. |
| Linux or macOS alternative | `curl https://install.meteor.com/ \| sh` remains documented. Keep `npx meteor` as the primary cross-platform command. |
| npm fallback | Run `npm install -g meteor --foreground-script` only after the documented `npx` recovery steps fail. |
| npm fallback | Run `npm install -g meteor --foreground-scripts --allow-scripts=meteor` only after the documented `npx` recovery steps fail. |
| Existing project needs another release | Use the project's pin or a Meteor `--release` command. Do not replace the user-wide installation first. |
Never run `npx install meteor`. Never add the npm `meteor` installer to an
@@ -48,7 +48,7 @@ Do not offer the shell installer on Windows. If `npx` installation still fails
after cache recovery, use the documented npm fallback:
```bash
npm install -g meteor --foreground-script
npm install -g meteor --foreground-scripts --allow-scripts=meteor
```
Do not place `meteor` in an application's dependencies or devDependencies.
+18
View File
@@ -0,0 +1,18 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": [
"./tsconfig.lint.json"
]
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error"
}
}
+140 -34
View File
@@ -81,8 +81,9 @@ on:
default: ''
type: string
# Read-only by default; the job that writes is granted it below.
permissions:
contents: write
contents: read
concurrency:
group: wekan-appimage-${{ inputs.tag || github.ref }}
@@ -97,19 +98,23 @@ jobs:
tag: ${{ steps.find.outputs.tag }}
version: ${{ steps.find.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: find
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ inputs.tag }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
tag='${{ inputs.tag }}'
tag="${INPUT_TAG}"
# --repo on every gh call, whether or not the job happens to have
# checked out: a call that relies on an inferred remote breaks the day
# somebody removes the checkout, and the error it gives - "fatal: not
# a git repository" - names neither gh nor the workflow.
[ -n "$tag" ] || tag="$(gh release list --repo "${{ github.repository }}" \
[ -n "$tag" ] || tag="$(gh release list --repo "${GITHUB_CTX_REPOSITORY}" \
--limit 1 --json tagName --jq '.[0].tagName')"
if [ -z "$tag" ]; then
echo "::error::No release to wrap, and no tag was given."
@@ -122,7 +127,13 @@ jobs:
echo "Wrapping the bundles from $tag (version $version)"
build:
name: AppImage ${{ matrix.appimage_arch }}
permissions:
contents: write
# The file's architecture, -musl included: two jobs of one name write their
# logs to one directory of the run's log archive, and its files then ask
# to be overwritten when it is unpacked (v12.54: AppImage x86_64 and
# aarch64, twice each).
name: AppImage ${{ matrix.name_arch || matrix.appimage_arch }}
needs: release
runs-on: ${{ matrix.runs_on }}
# A 32-bit job that cannot run on the runner it was given must not fail the
@@ -161,6 +172,20 @@ jobs:
bundle_arch: armhf
runs_on: ubuntu-24.04-arm
tolerate_failure: true
# Alpine and the other musl systems: the musl bundles musl.yml builds
# (releases/musl/build-bundle.sh). The same AppImage runtime - it is
# static, so it runs on musl - around a musl Node.js, named apart from
# the glibc AppImage and started inside Alpine to be checked.
- appimage_arch: x86_64
name_arch: x86_64-musl
bundle_arch: linux-x64-musl
runs_on: ubuntu-24.04
musl: true
- appimage_arch: aarch64
name_arch: aarch64-musl
bundle_arch: linux-arm64-musl
runs_on: ubuntu-24.04-arm
musl: true
# WHICH entries this run wants, decided here because it cannot be decided
# where it belongs. A job-level `if:` cannot see `matrix` - GitHub exposes
# that context to runs-on, env, name, steps and a few others, but not to a
@@ -170,10 +195,14 @@ jobs:
# A job-level env CAN see matrix, so the answer is computed once here and
# every step below asks for it.
env:
BUILD_THIS: ${{ inputs.only == '' || contains(fromJSON(inputs.only), matrix.appimage_arch) }}
BUILD_THIS: ${{ inputs.only == '' || contains(fromJSON(inputs.only), matrix.name_arch || matrix.appimage_arch) }}
# The file's name: the CPU, plus -musl for a musl AppImage.
NAME_ARCH: ${{ matrix.name_arch || matrix.appimage_arch }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: ${{ env.BUILD_THIS == 'true' }}
# An AppImage only WRAPS an existing release bundle
@@ -188,14 +217,21 @@ jobs:
if: ${{ env.BUILD_THIS == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_BUNDLE_ARCH: ${{ matrix.bundle_arch }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
zip="wekan-${{ needs.release.outputs.version }}-${{ matrix.bundle_arch }}.zip"
if gh release view --repo "${GITHUB_REPOSITORY:-wekan/wekan}" '${{ needs.release.outputs.tag }}' --repo "${{ github.repository }}" \
--json assets --jq '.assets[].name' 2>/dev/null | grep -qx "$zip"; then
echo "OK: $zip is on ${{ needs.release.outputs.tag }}; building its AppImage."
zip="wekan-${NEEDS_RELEASE_VERSION}-${MATRIX_BUNDLE_ARCH}.zip"
# Read the complete response before searching: grep -q can close a
# pipe early and turn a present asset into a SIGPIPE failure.
assets=$(gh release view --repo "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" \
--json assets --jq '.assets[].name')
if grep -Fxq "$zip" <<< "$assets"; then
echo "OK: $zip is on ${NEEDS_RELEASE_TAG}; building its AppImage."
else
echo "::notice::$zip is not on ${{ needs.release.outputs.tag }} yet - skipping this AppImage (it can only wrap a bundle that exists)."
echo "::notice::$zip is not on ${NEEDS_RELEASE_TAG} yet - skipping this AppImage (it can only wrap a bundle that exists)."
echo "BUILD_THIS=false" >> "$GITHUB_ENV"
fi
@@ -203,15 +239,37 @@ jobs:
if: ${{ env.BUILD_THIS == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_BUNDLE_ARCH: ${{ matrix.bundle_arch }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
MATRIX_MUSL: ${{ matrix.musl }}
run: |
set -euo pipefail
zip="wekan-${{ needs.release.outputs.version }}-${{ matrix.bundle_arch }}.zip"
gh release download '${{ needs.release.outputs.tag }}' \
--repo "${{ github.repository }}" --pattern "$zip" --clobber
zip="wekan-${NEEDS_RELEASE_VERSION}-${MATRIX_BUNDLE_ARCH}.zip"
gh release download "${NEEDS_RELEASE_TAG}" \
--repo "${GITHUB_CTX_REPOSITORY}" --pattern "$zip" --clobber
unzip -q "$zip"
# A zip that exists is not a bundle that runs; these are the two files
# the AppImage is built around.
for f in bundle/main.js bundle/node bundle/node-runtime/libatomic.so.1 bundle/ferretdb bundle/start-wekan.sh; do
# A musl bundle's node-runtime holds GCC's C++ runtime, which its
# Node.js needs and a bare Alpine lacks; a glibc one holds libatomic.
required="bundle/main.js bundle/node bundle/ferretdb bundle/start-wekan.sh"
if [ "${MATRIX_MUSL}" = true ]; then
required="$required bundle/node-runtime/libstdc++.so.6 bundle/node-runtime/libgcc_s.so.1"
else
required="$required bundle/node-runtime/libatomic.so.1"
fi
# And every other library a musl Node.js needs: x86-64's also needs
# libatomic, and the v12.53 x86_64 musl AppImage did not start in a
# bare Alpine without it.
if [ "${MATRIX_MUSL}" = true ]; then
for lib in $(readelf -dW bundle/node | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p'); do
case "$lib" in libc.musl-*|ld-musl-*|libc.so) continue ;; esac
required="$required bundle/node-runtime/$lib"
done
fi
for f in $required; do
if [ ! -e "$f" ]; then
echo "::error::$zip has no $f - it is not a runnable WeKan bundle."
exit 1
@@ -223,10 +281,13 @@ jobs:
- name: Build the AppDir
if: ${{ env.BUILD_THIS == 'true' }}
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_APPIMAGE_ARCH: ${{ matrix.appimage_arch }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
arch='${{ matrix.appimage_arch }}'
version="${NEEDS_RELEASE_VERSION}"
arch="${MATRIX_APPIMAGE_ARCH}"
rm -rf AppDir
mkdir -p AppDir/usr/lib/wekan AppDir/usr/share/applications \
AppDir/usr/share/icons/hicolor/512x512/apps
@@ -344,14 +405,21 @@ jobs:
APPRUN
sed -i "s/@APPIMAGE_ARCH@/$arch/" AppDir/AppRun
chmod +x AppDir/AppRun
# The AppImage runs as whoever starts it, not as this runner, so
# everything in it must be readable by all. v12.69's FerretDB and
# mongo-tools were 0711 and appimagetool refused to pack them.
chmod -R a+rX AppDir
- name: Pack the AppImage
if: ${{ env.BUILD_THIS == 'true' }}
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_APPIMAGE_ARCH: ${{ matrix.appimage_arch }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
arch='${{ matrix.appimage_arch }}'
name="WeKan-${version}-${arch}"
version="${NEEDS_RELEASE_VERSION}"
arch="${MATRIX_APPIMAGE_ARCH}"
name="WeKan-${version}-${NAME_ARCH}"
curl -fSL --retry 5 --retry-delay 10 -o appimagetool \
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
@@ -377,15 +445,39 @@ jobs:
- name: Check that it starts
id: smoke
if: ${{ env.BUILD_THIS == 'true' }}
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_MUSL: ${{ matrix.musl }}
MATRIX_APPIMAGE_ARCH: ${{ matrix.appimage_arch }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
app="dist/WeKan-${version}-${{ matrix.appimage_arch }}.AppImage"
version="${NEEDS_RELEASE_VERSION}"
app="dist/WeKan-${version}-${NAME_ARCH}.AppImage"
# An AppImage that packs is not an AppImage that runs. Start it, wait
# for WeKan to answer on its port, then stop it - so a broken bundle
# is caught here rather than by whoever downloads it.
export WRITABLE_PATH="$PWD/smoke-data" PORT=8080
# A musl AppImage runs its musl Node.js, which an Ubuntu runner cannot,
# so it is started where it is meant to run: inside Alpine.
if [ "${MATRIX_MUSL}" = true ]; then
docker run --rm --network host -v "$PWD:/work" -w /work -e APP="$app" alpine:3.23 sh -ec '
apk add --no-cache curl >/dev/null
export WRITABLE_PATH=/work/smoke-data PORT=8080
APPIMAGE_EXTRACT_AND_RUN=1 "./$APP" > smoke.log 2>&1 &
pid=$!
for _ in $(seq 1 90); do
if curl -fsS -o /dev/null "http://127.0.0.1:8080/sign-in"; then echo "WeKan answered on port 8080 in Alpine."; kill $pid; exit 0; fi
kill -0 $pid 2>/dev/null || break
sleep 2
done
kill $pid 2>/dev/null || true
echo "::error::the musl AppImage did not answer on port 8080 in Alpine; its log follows"
tail -n 100 smoke.log
exit 1'
exit 0
fi
# A CHECK THAT CANNOT RUN IS NOT A FAILED CHECK. Probe the INNER Node,
# not the AppImage runtime: on the arm64 runner the armhf AppImage
# runtime itself executes, while its bundled hard-float Node cannot
@@ -394,7 +486,7 @@ jobs:
# AppDir is the exact tree just packed, so this also avoids extracting
# the large image merely to make the capability decision.
if ! LD_LIBRARY_PATH="$PWD/AppDir/usr/lib/wekan/bundle/node-runtime${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" AppDir/usr/lib/wekan/bundle/node --version >/dev/null 2>&1; then
echo "::warning::this runner cannot execute the bundled ${{ matrix.appimage_arch }} Node binary, so the AppImage was packed but not started. It is uploaded unchecked."
echo "::warning::this runner cannot execute the bundled ${MATRIX_APPIMAGE_ARCH} Node binary, so the AppImage was packed but not started. It is uploaded unchecked."
exit 0
fi
@@ -415,10 +507,10 @@ jobs:
fi
echo "WeKan answered on port 8080."
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ env.BUILD_THIS == 'true' }}
with:
name: appimage-${{ matrix.appimage_arch }}
name: appimage-${{ matrix.name_arch || matrix.appimage_arch }}
path: dist/
if-no-files-found: error
@@ -434,6 +526,8 @@ jobs:
if: ${{ always() && env.BUILD_THIS == 'true' && steps.smoke.outcome == 'success' && inputs.publish != false }}
env:
GH_TOKEN: ${{ github.token }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
@@ -444,7 +538,7 @@ jobs:
fi
# --repo inside the script, so gh never has to infer the repository
# from a git remote. Retried, bounded, and verified by name and size.
bash releases/github-release-upload.sh "${{ github.repository }}" '${{ needs.release.outputs.tag }}' "${files[@]}"
bash releases/github-release-upload.sh "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" "${files[@]}"
# The last job no longer uploads anything: each build job above attached its
# own AppImage as soon as it was built and checked. This one only says, at the
@@ -460,23 +554,35 @@ jobs:
- name: Say what was built, and what was not
env:
GH_TOKEN: ${{ github.token }}
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
NEEDS_BUILD_RESULT: ${{ needs.build.result }}
INPUTS_ONLY: ${{ inputs.only }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
gh release view --repo "${{ github.repository }}" '${{ needs.release.outputs.tag }}' \
version="${NEEDS_RELEASE_VERSION}"
# The architectures THIS run builds: the musl run asked after the
# glibc AppImages, which another run attaches, and warned of all
# four as missing (v12.54).
wants="x86_64 aarch64 i686 armhf x86_64-musl aarch64-musl"
if [ -n "${INPUTS_ONLY}" ]; then
wants="$(python3 -c 'import json, sys; print(" ".join(json.loads(sys.argv[1])))' "${INPUTS_ONLY}")"
fi
gh release view --repo "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" \
--json assets --jq '.assets[].name' > published.txt
if grep -qE "^WeKan-${version}-.*\.AppImage$" published.txt; then
# Every architecture the matrix builds. A missing one WARNS rather
# than fails: the 32-bit pair is allowed not to build, and an
# architecture whose base bundle was never published has nothing to
# wrap. What must not happen silently is one going missing unnoticed.
for want in x86_64 aarch64 i686 armhf; do
for want in $wants; do
for f in "WeKan-${version}-${want}.AppImage" "WeKan-${version}-${want}.AppImage.sha256sum"; do
grep -qxF "$f" published.txt || \
echo "::warning::$f is not on ${{ needs.release.outputs.tag }} - see the AppImage $want build job's log"
echo "::warning::$f is not on ${NEEDS_RELEASE_TAG} - see the AppImage $want build job's log"
done
done
echo "AppImages on $(gh release view --repo "${{ github.repository }}" '${{ needs.release.outputs.tag }}' --json url --jq .url):"
echo "AppImages on $(gh release view --repo "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" --json url --jq .url):"
grep -E "^WeKan-${version}-.*\.AppImage" published.txt || true
else
# No AppImage was attached. That is only a FAILURE if there was a base
@@ -487,10 +593,10 @@ jobs:
grep -qx "wekan-${version}-${arch}.zip" published.txt && base_present=true
done
if [ "$base_present" = false ]; then
echo "::notice::No base bundle (wekan-${version}-{amd64,arm64}.zip) is on ${{ needs.release.outputs.tag }} yet, so there is nothing to wrap - skipping. release-all.yml builds the AppImages once the base bundles are published."
echo "::notice::No base bundle (wekan-${version}-{amd64,arm64}.zip) is on ${NEEDS_RELEASE_TAG} yet, so there is nothing to wrap - skipping. release-all.yml builds the AppImages once the base bundles are published."
exit 0
fi
if [ "${{ needs.build.result }}" = "cancelled" ]; then
if [ "${NEEDS_BUILD_RESULT}" = "cancelled" ]; then
echo "::warning::The run was cancelled before any AppImage finished building; nothing was attached."
exit 0
fi
+46 -21
View File
@@ -83,8 +83,9 @@ on:
default: ''
type: string
# Read-only by default; the job that writes is granted it below.
permissions:
contents: write
contents: read
concurrency:
group: wekan-flatpak-${{ inputs.tag || github.ref }}
@@ -99,17 +100,21 @@ jobs:
tag: ${{ steps.find.outputs.tag }}
version: ${{ steps.find.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: find
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ inputs.tag }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
tag='${{ inputs.tag }}'
tag="${INPUT_TAG}"
# --repo on every gh call, whether or not the job happens to have
# checked out: one that relies on an inferred remote breaks the day
# somebody removes the checkout.
[ -n "$tag" ] || tag="$(gh release list --repo "${{ github.repository }}" \
[ -n "$tag" ] || tag="$(gh release list --repo "${GITHUB_CTX_REPOSITORY}" \
--limit 1 --json tagName --jq '.[0].tagName')"
if [ -z "$tag" ]; then
echo "::error::No release to wrap, and no tag was given."
@@ -121,6 +126,8 @@ jobs:
echo "Wrapping the bundles from $tag (version $version)"
build:
permissions:
contents: write
name: flatpak ${{ matrix.flatpak_arch }}
needs: release
runs-on: ${{ matrix.runs_on }}
@@ -147,7 +154,9 @@ jobs:
BUILD_THIS: ${{ inputs.only == '' || contains(fromJSON(inputs.only), matrix.flatpak_arch) }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: ${{ env.BUILD_THIS == 'true' }}
- name: Install flatpak
@@ -164,11 +173,15 @@ jobs:
if: ${{ env.BUILD_THIS == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_BUNDLE_ARCH: ${{ matrix.bundle_arch }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
zip="wekan-${{ needs.release.outputs.version }}-${{ matrix.bundle_arch }}.zip"
gh release download '${{ needs.release.outputs.tag }}' \
--repo "${{ github.repository }}" --pattern "$zip" --clobber
zip="wekan-${NEEDS_RELEASE_VERSION}-${MATRIX_BUNDLE_ARCH}.zip"
gh release download "${NEEDS_RELEASE_TAG}" \
--repo "${GITHUB_CTX_REPOSITORY}" --pattern "$zip" --clobber
unzip -q "$zip"
for f in bundle/main.js bundle/node bundle/node-runtime/libatomic.so.1 bundle/ferretdb bundle/start-wekan.sh; do
if [ ! -e "$f" ]; then
@@ -186,10 +199,13 @@ jobs:
# export it into a repository.
- name: Build the Flatpak
if: ${{ env.BUILD_THIS == 'true' }}
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_FLATPAK_ARCH: ${{ matrix.flatpak_arch }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
arch='${{ matrix.flatpak_arch }}'
version="${NEEDS_RELEASE_VERSION}"
arch="${MATRIX_FLATPAK_ARCH}"
rm -rf build-dir repo
flatpak build-init --arch="$arch" build-dir \
@@ -273,13 +289,16 @@ jobs:
- name: Make the single-file bundle
id: bundle
if: ${{ env.BUILD_THIS == 'true' }}
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
MATRIX_FLATPAK_ARCH: ${{ matrix.flatpak_arch }}
run: |
set -euo pipefail
version='${{ needs.release.outputs.version }}'
name="WeKan-${version}-${{ matrix.flatpak_arch }}"
version="${NEEDS_RELEASE_VERSION}"
name="WeKan-${version}-${MATRIX_FLATPAK_ARCH}"
mkdir -p dist
flatpak build-bundle repo "dist/${name}.flatpak" \
--arch='${{ matrix.flatpak_arch }}' io.github.wekan.WeKan stable
--arch="${MATRIX_FLATPAK_ARCH}" io.github.wekan.WeKan stable
# Checksum keeps the .flatpak extension in its name, matching the zip
# convention and what releases/expected-assets.sh looks for; dropping it
# meant "what is missing" never found the checksum and rebuilt the
@@ -289,14 +308,14 @@ jobs:
sha256sum "${name}.flatpak" > "${name}.flatpak.sha256sum" )
ls -lh dist
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ env.BUILD_THIS == 'true' }}
with:
name: flatpak-${{ matrix.flatpak_arch }}
path: dist/
if-no-files-found: error
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ env.BUILD_THIS == 'true' }}
with:
name: flatpak-repo-${{ matrix.flatpak_arch }}
@@ -316,6 +335,8 @@ jobs:
if: ${{ always() && env.BUILD_THIS == 'true' && steps.bundle.outcome == 'success' && inputs.publish != false }}
env:
GH_TOKEN: ${{ github.token }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
@@ -325,7 +346,7 @@ jobs:
exit 1
fi
# Retried, bounded, and verified by name and size from the release side.
bash releases/github-release-upload.sh "${{ github.repository }}" '${{ needs.release.outputs.tag }}' "${files[@]}"
bash releases/github-release-upload.sh "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" "${files[@]}"
# This job no longer attaches the bundles: each build job above attached its
# own as soon as it was built. What is left for the end is what needs BOTH
@@ -343,9 +364,11 @@ jobs:
# the workspace - so it has to happen before the artifacts are downloaded
# into it. Without the checkout the step fails with "No such file or
# directory", which is how the v10.88 run ended.
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
# A cancelled run may have uploaded none; the next step says so.
continue-on-error: true
with:
@@ -354,11 +377,13 @@ jobs:
merge-multiple: true
- name: Say what was built, and what was not
env:
NEEDS_BUILD_RESULT: ${{ needs.build.result }}
run: |
set -euo pipefail
ls -lh assets 2>/dev/null || true
if ! ls assets/*.flatpak >/dev/null 2>&1; then
if [ "${{ needs.build.result }}" = "cancelled" ]; then
if [ "${NEEDS_BUILD_RESULT}" = "cancelled" ]; then
echo "::warning::The run was cancelled before any Flatpak bundle finished building."
exit 0
fi
@@ -371,7 +396,7 @@ jobs:
done
# ---- the repository -------------------------------------------------
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
if: ${{ inputs.publish_repo != false }}
with:
pattern: flatpak-repo-*
@@ -443,7 +468,7 @@ jobs:
TXT
du -sh flatpak-repo
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ inputs.publish_repo != false }}
with:
name: flatpak-repository
+204
View File
@@ -0,0 +1,204 @@
name: Android bundles
# WeKan on Android, for Termux: the published Linux bundle of each CPU,
# rebuilt inside Termux's own image (real Bionic and Android clang, the ARM
# ones on ARM runners) by releases/android/build-bundle.sh with wekan/
# node-patches' node-android-<cpu> of the same version, then started and
# required to answer HTTP 200. The same pattern as the musl and BSD bundles.
# A target whose Linux bundle, or whose Android Node.js, is not published yet
# is SKIPPED, not failed, and builds by itself once it is.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach the Android bundles and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["android-arm64"] (empty = all)'
type: string
default: ''
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
permissions:
contents: read
concurrency:
group: wekan-android-${{ inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
tag: ${{ steps.plan.outputs.tag }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
matrix="$(python3 -c '
import json, os
targets = json.load(open("releases/android/targets.json"))
only = json.loads(os.environ.get("ONLY") or "[]")
if only:
targets = [t for t in targets if t["target"] in only]
print(json.dumps({"include": targets}))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Release $tag; targets: $matrix"
build:
needs: plan
name: Android ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
# 32-bit ARM runs on the arm64 runner only when its kernel can; a target
# that cannot run there must not fail the others.
continue-on-error: ${{ matrix.tolerate_failure || false }}
timeout-minutes: 150
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TAG: ${{ needs.plan.outputs.tag }}
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Fetch and verify the Linux bundle and the Android Node.js
id: fetch
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ matrix.base }}
run: |
set -euo pipefail
version="${TAG#v}"
zip="wekan-${version}-${BASE}.zip"
if ! gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$zip" --pattern "$zip.sha256sum" || [ ! -f "$zip" ]; then
echo "::notice::$TARGET: $TAG has no $zip; skipped."
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
fi
[ -f "$zip.sha256sum" ] || { echo "::error::$zip has no published .sha256sum; a bundle is never rebuilt from an unverified one."; exit 1; }
sha256sum -c "$zip.sha256sum"
unzip -q "$zip"
rm -f "$zip" "$zip.sha256sum"
node_version="$(grep -aoE 'nodejs\.org/download/release/v[0-9]+\.[0-9]+\.[0-9]+/' bundle/node | head -n1 | sed -E 's#.*/(v[0-9.]+)/#\1#')"
[ -n "$node_version" ] || { echo "::error::Could not read the Node.js version of bundle/node."; exit 1; }
rc=0; out="$(bash releases/resolve-node-source.sh "$TARGET" "$node_version")" || rc=$?
if [ "$rc" = 3 ] || ! printf '%s\n' "$out" | grep -q '^node_found=true'; then
echo "::notice::$TARGET: wekan/node-patches has no node-$TARGET for $node_version yet; skipped."
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
fi
eval "$(printf '%s\n' "$out" | grep -E '^node_(url|sha256|kind|member)=')"
mkdir -p android-tools
bash releases/fetch.sh -o android-node.download "$node_url"
[ -n "$node_sha256" ] || { echo "::error::$node_url has no published SHA256."; exit 1; }
echo "$node_sha256 android-node.download" | sha256sum -c -
case "$node_kind" in
binary) mv android-node.download android-tools/node ;;
tar.xz) tar -xJf android-node.download -C android-tools && mv "android-tools/$node_member" android-tools/node && rm -f android-node.download ;;
*) echo "::error::unexpected Android Node.js kind: $node_kind"; exit 1 ;;
esac
chmod +x android-tools/node
file android-tools/node bundle/ferretdb
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Rebuild and start it in Termux
if: steps.fetch.outputs.skip != 'true'
env:
TERMUX_ARCH: ${{ matrix.termux_arch }}
ARCH: ${{ matrix.arch }}
run: |
# Termux's entrypoint clears the Docker environment when it switches
# to its unprivileged user, so the values go after the switch (as
# bun.yml does), and that user needs a writable bind mount. Writable
# is not enough: npm chmods every package's bin when it installs, and
# only a file's OWNER may chmod it (EPERM on uuid/dist/bin/uuid,
# v12.51), so the checkout is handed to that user. Package takes the
# bundle back.
chmod -R a+rwX .
owner="$(docker run --rm "termux/termux-docker:$TERMUX_ARCH" sh -c 'echo "$(id -u):$(id -g)"' | tr -d '\r' | tail -n1)"
case "$owner" in
*[0-9]:[0-9]*) sudo chown -R "$owner" . ;;
*) echo "::error::Could not read the uid of Termux's user (got: $owner)."; exit 1 ;;
esac
docker run --rm --security-opt seccomp=unconfined -v "$PWD:/work" -w /work \
"termux/termux-docker:$TERMUX_ARCH" env "ARCH=$ARCH" DO_NOT_TRACK=1 bash -ec '
apt-get update
apt-get install -y nodejs-lts python make clang pkg-config libvips curl procps file
bash releases/android/build-bundle.sh "$ARCH"'
- name: Package
if: steps.fetch.outputs.skip != 'true'
env:
VERSION: ${{ steps.fetch.outputs.version }}
run: |
set -euo pipefail
sudo chown -R "$(id -u):$(id -g)" .
out="wekan-${VERSION}-${TARGET}.zip"
zip -qry "$out" bundle
sha256sum "$out" > "$out.sha256sum"
ls -la "$out" "$out.sha256sum"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: steps.fetch.outputs.skip != 'true'
with:
name: android-${{ matrix.target }}
path: wekan-*-${{ matrix.target }}.zip*
retention-days: 3
compression-level: 0
publish:
needs: [plan, build]
if: ${{ !cancelled() && inputs.publish }}
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: android-*
path: artifacts
- name: Attach every Android bundle that was built and started
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.plan.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
published=0
for f in artifacts/android-*/wekan-*.zip; do
(cd "$(dirname "$f")" && sha256sum -c "$(basename "$f").sha256sum")
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$TAG" "$f" "$f.sha256sum"
published=$((published + 1))
done
echo "Published $published Android bundles to $TAG."
+223
View File
@@ -0,0 +1,223 @@
name: BSD bundles
# WeKan Node.js bundles for the BSDs (docs/Platforms/FOSS/OS/BSD.md): the
# release's amd64 bundle, with this BSD's Node.js (wekan/node-patches) and
# FerretDB (wekan/FerretDB), its native modules compiled inside a VM of that
# BSD, started there with its own start-wekan.sh, and attached to the release.
#
# A target whose Node.js is not published anywhere yet is SKIPPED, not failed:
# the same rule as every other bundle (releases/resolve-node-source.sh). So
# OpenBSD and NetBSD start building by themselves the day node-patches
# publishes their Node.js.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach tested BSD bundles and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["freebsd-x64"] (empty = all)'
type: string
default: ''
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
permissions:
contents: read
concurrency:
group: wekan-bsd-${{ inputs.tag || github.ref }}
cancel-in-progress: false
env:
NODE_VERSION: '26'
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
tag: ${{ steps.plan.outputs.tag }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
matrix="$(python3 -c '
import json, os
targets = json.load(open("releases/bsd/targets.json"))
only = json.loads(os.environ.get("ONLY") or "[]")
if only:
targets = [t for t in targets if t["target"] in only]
print(json.dumps({"include": targets}))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Release $tag; targets: $matrix"
build:
needs: plan
name: BSD ${{ matrix.target }}
runs-on: ubuntu-24.04
timeout-minutes: 180
permissions:
contents: write
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TAG: ${{ needs.plan.outputs.tag }}
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Node.js first: no Node.js for this BSD anywhere means no bundle this
# release, which is a skip, not a failure.
- name: Where this BSD's Node.js comes from
id: node
run: |
set -euo pipefail
out="$(bash releases/resolve-node-source.sh "$TARGET" "$NODE_VERSION")" || rc=$?
if [ "${rc:-0}" = 3 ] || ! printf '%s\n' "$out" | grep -q '^node_found=true'; then
echo "::notice::$TARGET is not built this release: wekan/node-patches has no node-$TARGET yet."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "$out" | grep -E '^node_(full|from|url|kind|sha256)=' >> "$GITHUB_OUTPUT"
printf '%s\n' "$out"
- name: Fetch and verify the release bundle, Node.js, npm and FerretDB
if: steps.node.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
NODE_URL: ${{ steps.node.outputs.node_url }}
NODE_SHA256: ${{ steps.node.outputs.node_sha256 }}
NODE_FULL: ${{ steps.node.outputs.node_full }}
FERRETDB_ARCH: ${{ matrix.ferretdb_arch }}
run: |
set -euo pipefail
version="${TAG#v}"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "wekan-${version}-amd64.zip" --pattern "wekan-${version}-amd64.zip.sha256sum"
sha256sum -c "wekan-${version}-amd64.zip.sha256sum"
unzip -q "wekan-${version}-amd64.zip"
mkdir -p bsd-tools/bin bsd-tools/npm
bash releases/fetch.sh -o bsd-tools/bin/node "$NODE_URL"
[ -z "$NODE_SHA256" ] || echo "$NODE_SHA256 bsd-tools/bin/node" | sha256sum -c -
# npm is JavaScript: the one in nodejs.org's Linux tarball of the same
# version runs on any Node.js 26.
bash releases/fetch.sh -o npm.tar.xz "https://nodejs.org/dist/${NODE_FULL}/node-${NODE_FULL}-linux-x64.tar.xz"
tar -xJf npm.tar.xz --strip-components=4 -C bsd-tools/npm "node-${NODE_FULL}-linux-x64/lib/node_modules/npm"
rm -f npm.tar.xz
# The prebuilt SQLite-only ferretdb-sqlite-<bsd>-<arch> from the newest
# wekan/FerretDB release that has it (the full ferretdb-<bsd>-<arch> of
# the newest release until one does), verified against its .sha256sum;
# releases/ferretdb-asset.sh says in the log which one it took.
ferret_meta="$(bash releases/ferretdb-asset.sh "$FERRETDB_ARCH" -o bundle/ferretdb)" \
|| { echo "::error::FerretDB for ${FERRETDB_ARCH} could not be put in the bundle; the lines above say why."; exit 1; }
eval "$ferret_meta"
# The inherited amd64 Linux MongoDB Database Tools are the wrong OS
# here: replaced by this BSD's, or removed when there are none.
rc=0; bash releases/fetch-mongo-tools.sh bundle "$FERRETDB_ARCH" || rc=$?
[ "$rc" != 2 ] || { echo "::error::A MongoDB Database Tools download did not match its checksum."; exit 1; }
[ "$rc" = 0 ] || echo "::warning::No MongoDB Database Tools for ${FERRETDB_ARCH}; the bundle ships without them."
file bsd-tools/bin/node bundle/ferretdb
- name: Build and start in FreeBSD
if: steps.node.outputs.skip != 'true' && matrix.os == 'freebsd'
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc
with:
release: ${{ matrix.release }}
arch: ${{ matrix.vm_arch }}
sync: rsync
copyback: true
prepare: pkg install -y bash python3 gmake pkgconf curl
run: bash releases/bsd/build-bundle.sh freebsd ${{ matrix.arch }}
- name: Build and start in OpenBSD
if: steps.node.outputs.skip != 'true' && matrix.os == 'openbsd'
uses: vmactions/openbsd-vm@f1ca8bc8ecc5b6ef7218e5b914836e067e1546d3 # v1.4.9
with:
release: ${{ matrix.release }}
arch: ${{ matrix.vm_arch }}
sync: rsync
copyback: true
prepare: pkg_add bash python%3 gmake curl
# V8 writes and runs machine code: OpenBSD allows that only from a
# filesystem mounted wxallowed (/usr/local is, by default). The
# bundle is started here, from the workspace, so that is mounted so.
run: |
mount -u -o wxallowed "$(df -P . | awk 'NR==2 {print $6}')" || true
bash releases/bsd/build-bundle.sh openbsd ${{ matrix.arch }}
- name: Build and start in NetBSD
if: steps.node.outputs.skip != 'true' && matrix.os == 'netbsd'
uses: vmactions/netbsd-vm@c8a0d7ddb84619a7a8cc4e652efa7950b3fd9f92 # v1.5.2
with:
release: ${{ matrix.release }}
sync: rsync
copyback: true
prepare: |
/usr/sbin/pkg_add pkgin
pkgin -y install bash python313 gmake curl gcc14
# Node 26's headers need C++20, which NetBSD 10's own GCC lacks: the
# native modules are compiled with pkgsrc's GCC 14, as Node.js itself
# is (wekan/node-patches dist/netbsd).
run: |
CC=/usr/pkg/gcc14/bin/gcc CXX=/usr/pkg/gcc14/bin/g++ \
bash releases/bsd/build-bundle.sh netbsd ${{ matrix.arch }}
- name: Zip, checksum and attach
if: steps.node.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
PUBLISH: ${{ inputs.publish }}
MATRIX_OS: ${{ matrix.os }}
run: |
set -euo pipefail
version="${TAG#v}"
zip="wekan-${version}-${TARGET}.zip"
python3 releases/check-telemetry.py --bundle bundle
zip -qr "$zip" bundle
unzip -l "$zip" > zip-listing.txt
for want in bundle/main.js bundle/node bundle/ferretdb bundle/start-wekan.sh; do
grep -q " $want$" zip-listing.txt || { echo "::error::$zip has no $want."; exit 1; }
done
if [ "${MATRIX_OS}" = freebsd ]; then
grep -q " bundle/rc.d/wekan$" zip-listing.txt || { echo "::error::$zip has no bundle/rc.d/wekan (#2662)."; exit 1; }
fi
sha256sum "$zip" > "$zip.sha256sum"
cat "$zip.sha256sum"
if [ "$PUBLISH" = true ]; then
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$TAG" "$zip" "$zip.sha256sum"
fi
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: steps.node.outputs.skip != 'true'
with:
name: bundle-${{ matrix.target }}
path: wekan-*-${{ matrix.target }}.zip*
retention-days: 3
compression-level: 0
+181
View File
@@ -0,0 +1,181 @@
name: Bun single-file WeKan
# One executable per Bun platform (docs/Platforms/Single-File.md, "Bun single
# files"): that platform's published Bun bundle ZIP, unchanged, inside its own
# Bun, made by releases/single-file/bun/build.mjs. Each one is built ON its
# platform with the bundle's own bun - Alpine for musl, as bun.yml does - so
# nothing is cross-compiled and the runtime is the one the bundle was tested
# with. A target whose Bun bundle the release does not have is SKIPPED, not
# failed. FreeBSD and Android are not built: GitHub has no runner for them,
# and their bundles are used unpacked.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach the single files and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["linux-x64"] (empty = all)'
type: string
default: ''
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
permissions:
contents: read
concurrency:
group: wekan-bun-single-file-${{ inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
tag: ${{ steps.plan.outputs.tag }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
matrix="$(python3 -c '
import json, os
targets = [t for t in json.load(open("releases/bun/targets.json"))
if t["kind"] in ("native", "musl")]
only = json.loads(os.environ.get("ONLY") or "[]")
if only:
targets = [t for t in targets if t["target"] in only]
print(json.dumps({"include": targets}))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Release $tag; targets: $matrix"
build:
needs: plan
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TAG: ${{ needs.plan.outputs.tag }}
TARGET: ${{ matrix.target }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.plan.outputs.tag }}
- name: Fetch and verify ${{ matrix.target }}'s published Bun bundle
id: fetch
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="${TAG#v}"
zip="wekan-${version}-bun-${TARGET}.zip"
if ! gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$zip" --pattern "$zip.sha256sum" || [ ! -f "$zip" ]; then
echo "::notice::$TARGET: $TAG has no $zip; skipped."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
[ -f "$zip.sha256sum" ] || { echo "::error::$zip has no published .sha256sum; a single file is never built from an unverified bundle."; exit 1; }
# A .sha256sum written on Windows before releases/bun/assets.py wrote LF
# (v12.44 windows-arm64) ends in CRLF; the line is the same without it.
tr -d '\r' < "$zip.sha256sum" > "$zip.sha256sum.lf" && mv "$zip.sha256sum.lf" "$zip.sha256sum"
if command -v sha256sum >/dev/null; then sha256sum -c "$zip.sha256sum"; else shasum -a 256 -c "$zip.sha256sum"; fi
# Only the bundle's own Bun, to build with - the ZIP itself is
# embedded as it is.
case "$TARGET" in windows-*) bun=bun.exe ext=.exe ;; *) bun=bun ext= ;; esac
unzip -q -j "$zip" "bundle/$bun" -d builder
chmod +x "builder/$bun"
echo "zip=$zip" >> "$GITHUB_OUTPUT"
echo "bun=builder/$bun" >> "$GITHUB_OUTPUT"
echo "out=wekan-${version}-bun-${TARGET}-single-file${ext}" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Build and start it (Linux, macOS, Windows)
if: steps.fetch.outputs.skip != 'true' && matrix.kind == 'native'
env:
ZIP: ${{ steps.fetch.outputs.zip }}
BUN: ${{ steps.fetch.outputs.bun }}
OUT: ${{ steps.fetch.outputs.out }}
VERSION: ${{ steps.fetch.outputs.version }}
run: bash releases/single-file/bun/build-and-check.sh
- name: Build and start it (musl, in Alpine)
if: steps.fetch.outputs.skip != 'true' && matrix.kind == 'musl'
env:
ZIP: ${{ steps.fetch.outputs.zip }}
BUN: ${{ steps.fetch.outputs.bun }}
OUT: ${{ steps.fetch.outputs.out }}
VERSION: ${{ steps.fetch.outputs.version }}
run: |
docker run --rm -v "$PWD:/work" -w /work -e ZIP -e BUN -e OUT -e VERSION -e TARGET \
alpine:3.23 sh -ec 'apk add --no-cache bash coreutils libstdc++ libgcc && bash releases/single-file/bun/build-and-check.sh'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: steps.fetch.outputs.skip != 'true'
with:
name: bun-single-file-${{ matrix.target }}
path: wekan-*-bun-${{ matrix.target }}-single-file*
retention-days: 3
compression-level: 0
publish:
needs: [plan, build]
if: ${{ !cancelled() && inputs.publish }}
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: bun-single-file-*
path: artifacts
- name: Attach every Bun single file that was built and started
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.plan.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
published=0
for f in artifacts/bun-single-file-*/wekan-*-single-file*; do
case "$f" in *.sha256sum) continue ;; esac
(cd "$(dirname "$f")" && sha256sum -c "$(basename "$f").sha256sum")
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$TAG" "$f" "$f.sha256sum"
published=$((published + 1))
done
echo "Published $published Bun single files to $TAG."
+271
View File
@@ -0,0 +1,271 @@
name: Bun
# Build one ESM app, then rebuild its native addons on each Bun target.
# Experimental until meteor/meteor#14751 ships; the isolated 3.6 port preserves
# WeKan's release/package pins and is never used by the ordinary Node builds.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach tested Bun archives and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["linux-arm64", "android-arm64"] (empty = all)'
type: string
default: ''
missing_only:
description: 'Skip targets already carrying an archive AND checksum'
type: boolean
default: false
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
missing_only:
type: boolean
default: false
permissions:
contents: read
concurrency:
group: wekan-bun-${{ inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
tag: ${{ steps.plan.outputs.tag }}
bun_tag: ${{ steps.plan.outputs.bun_tag }}
bun_version: ${{ steps.plan.outputs.bun_version }}
matrix: ${{ steps.plan.outputs.matrix }}
count: ${{ steps.plan.outputs.count }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
MISSING_ONLY: ${{ inputs.missing_only }}
run: python3 releases/bun/assets.py plan
esm:
needs: plan
if: needs.plan.outputs.count != '0'
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
# Keep workflow helpers from this workflow revision, and the app from the
# exact release tag. Never put current main's code on an older release.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: build-tools
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ needs.plan.outputs.tag }}
path: app
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '26'
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ needs.plan.outputs.bun_version }}
- name: Prepare isolated Meteor with ESM support
working-directory: app
run: |
mkdir -p releases/bun
cp -R ../build-tools/releases/bun/. releases/bun/
bash releases/bun/prepare-meteor.sh
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.meteor
~/.npm
app/.tools/meteor-bun/dev_bundle
app/.tools/meteor-bun/.babel-cache
app/.tools/meteor-bun/packages/*/.npm
key: bun-meteor-${{ runner.os }}-${{ hashFiles('app/.meteor/release', 'app/.meteor/versions', 'build-tools/releases/bun/meteor-3.6-rc.1.patch') }}
- name: Build the release's ESM application once
working-directory: app
env:
DO_NOT_TRACK: '1'
run: |
export TMPDIR="$PWD/.tools/tmp"
# Rspack and its native compiler are devDependencies needed to build.
meteor npm ci --include=dev --include=optional
test -s node_modules/@meteorjs/rspack/rspack.config.js
meteor build ../esm-output --directory --server-only --format=esm
test -s ../esm-output/bundle/index.mjs
git rev-parse HEAD > ../esm-output/bundle/wekan-commit.txt
node --test .tools/meteor-bun/scripts/ci/__tests__/*.node-test.js
tar -cf - -C ../esm-output bundle | gzip -1 > ../esm-bundle.tar.gz
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: bun-esm-source
path: esm-bundle.tar.gz
compression-level: 0
if-no-files-found: error
retention-days: 3
build:
needs: [plan, esm]
permissions:
contents: write
actions: read
name: Bun ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TARGET: ${{ matrix.target }}
TARGET_PLATFORM: ${{ matrix.platform }}
TARGET_ARCH: ${{ matrix.arch }}
RELEASE_TAG: ${{ needs.plan.outputs.tag }}
BUN_TAG: ${{ needs.plan.outputs.bun_tag }}
DO_NOT_TRACK: '1'
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: bun-esm-source
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.13'
- name: Unpack ESM and fetch verified target Bun
run: |
tar -xzf esm-bundle.tar.gz
export WEKAN_COMMIT="$(cat bundle/wekan-commit.txt)"
python3 releases/bun/assets.py fetch "$BUN_TAG" "$TARGET"
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.kind == 'native'
with:
node-version: '26'
architecture: ${{ matrix.arch }}
- name: Install Chromium smoke test dependencies
if: matrix.kind == 'native' && matrix.platform == 'linux'
run: |
mkdir -p .tools/tmp
export TMPDIR="$PWD/.tools/tmp"
# Where releases/tools-env.sh, which build-target.sh loads, looks for
# them. Installed in ~/.cache instead, the smoke test found no browser
# (linux-arm64, v12.66).
export PLAYWRIGHT_BROWSERS_PATH="$PWD/.tools/ms-playwright"
npm ci --prefix tests/playwright --ignore-scripts
tests/playwright/node_modules/.bin/playwright install --with-deps chromium
- name: Start an isolated MongoDB for Linux HTTP and DDP tests
if: matrix.kind == 'native' && matrix.platform == 'linux'
run: |
# Not --rm: a container that dies must leave its log behind. v12.56
# linux-x64 found nothing on 27019 ("Topology is closed") and there
# was no way to see why. Wait until it answers, or show why not.
# 9.0.4, not mongo:8: on the runner's 6.19+ kernel mongo:8 refused to
# start ("Linux kernel versions 6.19 and newer has a known
# incompatibility", v12.66). 9.0.4 recognises Ubuntu kernels that
# have the fix; see docs/Databases/Supported.md.
docker run -d --name bun-smoke-mongo -p 127.0.0.1:27019:27017 mongo:9.0.4
for i in $(seq 1 60); do
if docker exec bun-smoke-mongo mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' 2>/dev/null | grep -qx 1; then
echo "MongoDB answers on 127.0.0.1:27019 after ${i}s."; exit 0
fi
sleep 1
done
echo "::error::the smoke-test MongoDB did not answer within 60s; its state and log follow."
docker ps -a --filter name=bun-smoke-mongo
docker logs bun-smoke-mongo 2>&1 | tail -n 80
exit 1
- name: Native Linux, macOS or Windows build and smoke tests
if: matrix.kind == 'native'
env:
BUN_SMOKE_MONGO_URL: ${{ matrix.platform == 'linux' && 'mongodb://127.0.0.1:27019/bun_smoke' || '' }}
BUN_SMOKE_BROWSER: ${{ matrix.platform == 'linux' && '1' || '0' }}
run: bash releases/bun/build-target.sh
- name: Native musl container build and smoke tests
if: matrix.kind == 'musl'
run: |
docker run --rm -v "$PWD:/work" -w /work \
-e TARGET -e TARGET_PLATFORM -e TARGET_ARCH -e RELEASE_TAG \
alpine:3.23 sh -ec '
apk add --no-cache bash nodejs npm python3 make g++ linux-headers
bash releases/bun/build-target.sh'
- name: Native Android Bionic build and smoke tests
if: matrix.kind == 'android'
env:
TERMUX_ARCH: ${{ matrix.termux_arch }}
run: |
# Termux uses Android libc/linker and native Android clang, not glibc.
# Its entrypoint clears Docker env when switching to the system user.
# Pass build metadata as env arguments AFTER that switch.
# Its unprivileged system user needs a writable bind mount.
chmod -R a+rwX bundle
mkdir -p dist .tools/tmp
chmod a+rwx dist .tools .tools/tmp
docker run --rm --security-opt seccomp=unconfined \
-v "$PWD:/work" -w /work \
-e TARGET -e TARGET_PLATFORM -e TARGET_ARCH -e RELEASE_TAG \
"termux/termux-docker:$TERMUX_ARCH" env \
"TARGET=$TARGET" "TARGET_PLATFORM=$TARGET_PLATFORM" \
"TARGET_ARCH=$TARGET_ARCH" "RELEASE_TAG=$RELEASE_TAG" \
DO_NOT_TRACK=1 bash -ec '
apt-get update
apt-get install -y nodejs-lts python make clang pkg-config libvips
bash releases/bun/build-target.sh'
- name: FreeBSD VM build and smoke tests
if: matrix.kind == 'freebsd'
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
with:
release: '14.5'
arch: ${{ matrix.vm_arch }}
envs: TARGET TARGET_PLATFORM TARGET_ARCH RELEASE_TAG
sync: rsync
copyback: true
cache-after-prepare: true
prepare: pkg install -y bash node24 npm-node24 python3 gmake pkgconf vips
run: bash releases/bun/build-target.sh
- name: Check finished archive exists
id: ready
run: |
files=(dist/*.zip)
[ "${#files[@]}" -eq 1 ]
test -s "${files[0]}"
test -s "${files[0]}.sha256sum"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always() && steps.ready.outcome == 'success'
with:
name: wekan-bun-${{ matrix.target }}
path: dist/*
compression-level: 0
if-no-files-found: error
retention-days: 7
- name: Attach this target immediately after validation
if: ${{ always() && steps.ready.outcome == 'success' && inputs.publish }}
env:
GH_TOKEN: ${{ github.token }}
run: |
files=(dist/*.zip dist/*.sha256sum)
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$RELEASE_TAG" "${files[@]}"
+4 -2
View File
@@ -10,6 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 'Dependency Review'
uses: actions/dependency-review-action@v5
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
+12 -4
View File
@@ -33,7 +33,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The Dockerfile DOWNLOADS the prebuilt release zip that release-all.yml
# publishes (and `exit 8`s if it is missing), so this image can only be
@@ -47,12 +49,18 @@ jobs:
id: rel
env:
GH_TOKEN: ${{ github.token }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
VERSION="$(sed -n 's/^ARG VERSION=//p' Dockerfile | head -1)"
asset="wekan-${VERSION}-amd64.zip"
if gh release view "v${VERSION}" --repo "${{ github.repository }}" \
--json assets --jq '.assets[].name' 2>/dev/null | grep -qx "$asset"; then
# The list is read whole BEFORE matching. `gh … | grep -q` lets grep exit
# at the first match while gh is still writing; gh gets SIGPIPE, pipefail
# makes the pipeline fail, and a PUBLISHED asset was reported missing - the
# v12.39 Mac apps were skipped that way, with the bundles already attached.
assets="$(gh release view "v${VERSION}" --repo "${GITHUB_CTX_REPOSITORY}" \
--json assets --jq '.assets[].name' 2>/dev/null || true)"
if grep -qx "$asset" <<< "$assets"; then
echo "build=yes" >> "$GITHUB_OUTPUT"
echo "OK: $asset is on the v${VERSION} release; building the image."
else
@@ -64,7 +72,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
+23 -12
View File
@@ -62,6 +62,7 @@ jobs:
- name: Check DOCKERHUB_AUTH is configured and works
env:
DOCKERHUB_AUTH: ${{ secrets.DOCKERHUB_AUTH }}
INPUT_IMAGE: ${{ inputs.image }}
run: |
set -uo pipefail
if [ -z "${DOCKERHUB_AUTH:-}" ]; then
@@ -79,16 +80,19 @@ jobs:
user="${creds%%:*}"
token="${creds#*:}"
if ! printf '%s' "$token" | docker login docker.io -u "$user" --password-stdin; then
echo "::error::docker-variant: DOCKERHUB_AUTH was REJECTED by docker.io (login failed as user '$user'). The token is wrong, expired, or lacks push rights for wekanteam/${{ inputs.image }}."
echo "::error::docker-variant: DOCKERHUB_AUTH was REJECTED by docker.io (login failed as user '$user'). The token is wrong, expired, or lacks push rights for wekanteam/${INPUT_IMAGE}."
exit 1
fi
echo "docker-variant: DOCKERHUB_AUTH works for docker.io (user '$user')."
- name: Work out which version to publish
id: v
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_IMAGE: ${{ inputs.image }}
run: |
set -euo pipefail
version="${{ inputs.version }}"
version="${INPUT_VERSION}"
if [ -z "$version" ]; then
# The newest release of wekan/wekan, asked of GitHub rather than guessed.
version="$(curl -sSL -H 'Accept: application/vnd.github+json' \
@@ -101,16 +105,20 @@ jobs:
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Publishing wekanteam/${{ inputs.image }} from wekanteam/wekan:v$version"
echo "Publishing wekanteam/${INPUT_IMAGE} from wekanteam/wekan:v$version"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Retag wekanteam/wekan as wekanteam/${{ inputs.image }}
env:
STEP_V_VERSION: ${{ steps.v.outputs.version }}
INPUT_IMAGE: ${{ inputs.image }}
INPUT_ALSO_LATEST: ${{ inputs.also_latest }}
run: |
set -euo pipefail
src="wekanteam/wekan:v${{ steps.v.outputs.version }}"
dst="wekanteam/${{ inputs.image }}:v${{ steps.v.outputs.version }}"
src="wekanteam/wekan:v${STEP_V_VERSION}"
dst="wekanteam/${INPUT_IMAGE}:v${STEP_V_VERSION}"
# The source must exist and be the full multi-arch manifest, or the
# variant tag would quietly cover fewer architectures than wekan does.
@@ -128,8 +136,8 @@ jobs:
done
tags=(-t "$dst")
if [ "${{ inputs.also_latest }}" = "true" ]; then
tags+=(-t "wekanteam/${{ inputs.image }}:latest")
if [ "${INPUT_ALSO_LATEST}" = "true" ]; then
tags+=(-t "wekanteam/${INPUT_IMAGE}:latest")
fi
# imagetools create copies the MANIFEST, so the variant tag points at the
# very digests the release built and verified. Nothing is rebuilt.
@@ -149,12 +157,15 @@ jobs:
- name: Job result
if: always()
env:
JOB_STATUS: ${{ job.status }}
INPUT_IMAGE: ${{ inputs.image }}
run: |
case "${{ job.status }}" in
case "${JOB_STATUS}" in
success) mark='OK ' ;;
failure) mark='FAILED ' ;;
cancelled) mark='CANCELLED' ;;
*) mark='${{ job.status }}' ;;
*) mark="${JOB_STATUS}" ;;
esac
echo "$mark docker-variant wekanteam/${{ inputs.image }}"
echo "$mark docker-variant wekanteam/${{ inputs.image }}" >> "$GITHUB_STEP_SUMMARY"
echo "$mark docker-variant wekanteam/${INPUT_IMAGE}"
echo "$mark docker-variant wekanteam/${INPUT_IMAGE}" >> "$GITHUB_STEP_SUMMARY"
+11 -3
View File
@@ -16,7 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The Dockerfile does not build WeKan from source — it DOWNLOADS the prebuilt
# release zip (wekan-<VERSION>-amd64.zip) that release-all.yml publishes, and
@@ -31,12 +33,18 @@ jobs:
id: rel
env:
GH_TOKEN: ${{ github.token }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
VERSION="$(sed -n 's/^ARG VERSION=//p' Dockerfile | head -1)"
asset="wekan-${VERSION}-amd64.zip"
if gh release view "v${VERSION}" --repo "${{ github.repository }}" \
--json assets --jq '.assets[].name' 2>/dev/null | grep -qx "$asset"; then
# The list is read whole BEFORE matching. `gh … | grep -q` lets grep exit
# at the first match while gh is still writing; gh gets SIGPIPE, pipefail
# makes the pipeline fail, and a PUBLISHED asset was reported missing - the
# v12.39 Mac apps were skipped that way, with the bundles already attached.
assets="$(gh release view "v${VERSION}" --repo "${GITHUB_CTX_REPOSITORY}" \
--json assets --jq '.assets[].name' 2>/dev/null || true)"
if grep -qx "$asset" <<< "$assets"; then
echo "build=yes" >> "$GITHUB_OUTPUT"
echo "OK: $asset is on the v${VERSION} release; building the image."
else
+19 -6
View File
@@ -34,8 +34,9 @@ on:
default: ''
type: string
# Read-only by default; the job that writes is granted it below.
permissions:
contents: write
contents: read
concurrency:
group: wekan-mac-app-${{ inputs.tag || github.ref }}
@@ -65,6 +66,8 @@ jobs:
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
build:
permissions:
contents: write
name: Mac app ${{ matrix.arch }}
needs: release
runs-on: ${{ matrix.runner }}
@@ -82,7 +85,9 @@ jobs:
env:
BUILD_THIS: ${{ inputs.only == '' || contains(fromJSON(inputs.only), matrix.arch) }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: ${{ env.BUILD_THIS == 'true' }}
- name: Download and verify the native macOS bundle
if: ${{ env.BUILD_THIS == 'true' }}
@@ -95,8 +100,13 @@ jobs:
run: |
set -euo pipefail
zip="wekan-${VERSION}-${BUNDLE_ARCH}.zip"
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name' | grep -qxF "$zip"; then
# The list is read whole BEFORE matching. `gh … | grep -q` lets grep exit
# at the first match while gh is still writing; gh gets SIGPIPE, pipefail
# makes the pipeline fail, and a PUBLISHED asset was reported missing - the
# v12.39 Mac apps were skipped that way, with the bundles already attached.
assets="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name')"
if ! grep -qxF "$zip" <<< "$assets"; then
echo "::notice::$zip has not been published yet; this app cannot be wrapped."
echo "available=false" >> "$GITHUB_OUTPUT"
exit 0
@@ -136,6 +146,9 @@ jobs:
MAC_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
mkdir -p "$PWD/.tools/tmp"
export TMPDIR="$PWD/.tools/tmp"
python3 -B tests/mac-smoke-app.py
app="$PWD/dist/WeKan-${VERSION}-mac-${MAC_ARCH}.app"
command="$app/Contents/Resources/launch-wekan.command"
export WRITABLE_PATH="$PWD/.tools/tmp/mac-smoke-data"
@@ -145,14 +158,14 @@ jobs:
"$PWD/.tools/tmp/mac-smoke.log"
- name: Keep Mac smoke log
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ always() && env.BUILD_THIS == 'true' && steps.bundle.outputs.available == 'true' }}
with:
name: mac-smoke-${{ matrix.arch }}
path: .tools/tmp/mac-smoke.log
if-no-files-found: ignore
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ env.BUILD_THIS == 'true' && steps.bundle.outputs.available == 'true' }}
with:
name: mac-app-${{ matrix.arch }}
+13 -19
View File
@@ -32,23 +32,16 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.inputs.ref || 'main' }}
persist-credentials: false
- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Use npm 12.0.2
run: npm install --global npm@12.0.2
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
# Same as sandstorm.yml: Ubuntu 24.04's AppArmor blocks the unprivileged user
# namespaces the Sandstorm install relies on; relax it so the install (whose
# sandstorm-http-bridge build-deps.sh copies in) can run on the runner.
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
- name: Use release npm version
run: bash releases/use-release-npm.sh
- name: Install system dependencies
run: |
@@ -58,13 +51,13 @@ jobs:
# build-deps.sh's [bridge] step copies the modern
# /opt/sandstorm/latest/bin/sandstorm-http-bridge into the deps, so Sandstorm
# must be installed first — same ordering as sandstorm.yml.
run: curl https://install.sandstorm.io | sudo bash -s -- -d
run: curl https://install.sandstorm.org | sudo bash -s -- -d
- name: Assemble modern meteor-spk.deps (Node 26 + FerretDB + Mongo 3.x CLIs)
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io; build-deps.sh swaps in
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.org; build-deps.sh swaps in
# Node 26, FerretDB, the Mongo 3.x CLIs, the migration launcher/importer, keeps
# niscud, and swaps in the modern sandstorm-http-bridge. Produces
# $HOME/projects/meteor-spk/meteor-spk-0.6.0 (the meteor-spk tool + deps).
# $GITHUB_WORKSPACE/.tools/projects/meteor-spk/meteor-spk-0.6.0 (the meteor-spk tool + deps).
run: bash sandstorm-src/build-deps.sh
- name: Zip the meteor-spk bundle
@@ -73,7 +66,7 @@ jobs:
set -euo pipefail
STAMP="$(date +'%Y-%m-%d_%H_%M_%S')"
ZIP="meteor-spk-${STAMP}.zip"
BUNDLE_DIR="$HOME/projects/meteor-spk"
BUNDLE_DIR="$GITHUB_WORKSPACE/.tools/projects/meteor-spk"
if [ ! -d "$BUNDLE_DIR/meteor-spk-0.6.0" ]; then
echo "::error::Expected meteor-spk bundle at $BUNDLE_DIR/meteor-spk-0.6.0 was not produced by build-deps.sh."
exit 1
@@ -87,7 +80,7 @@ jobs:
ls -la "$GITHUB_WORKSPACE/$ZIP"
echo "zip=$ZIP" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ steps.zip.outputs.zip }}
path: ${{ steps.zip.outputs.zip }}
@@ -97,6 +90,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
STEP_ZIP_ZIP: ${{ steps.zip.outputs.zip }}
run: |
set -euo pipefail
TAG="$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || true)"
@@ -104,5 +98,5 @@ jobs:
echo "::warning::No existing GitHub Release found; the meteor-spk bundle is available only as a workflow artifact."
exit 0
fi
echo "Uploading ${{ steps.zip.outputs.zip }} to the newest release: $TAG"
gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "$TAG" "${{ steps.zip.outputs.zip }}" --clobber
echo "Uploading ${STEP_ZIP_ZIP} to the newest release: $TAG"
gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "$TAG" "${STEP_ZIP_ZIP}" --clobber
+184
View File
@@ -0,0 +1,184 @@
name: musl bundles
# WeKan for Alpine and the other musl systems: the published Linux bundle of
# each CPU, rebuilt inside Alpine by releases/musl/build-bundle.sh with the
# musl Node.js of the same version and every native addon compiled against
# musl, then started and required to answer HTTP 200. The same pattern as
# the BSD bundles (bsd.yml). A target whose Linux bundle or musl Node.js the
# release does not have is SKIPPED, not failed.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach the musl bundles and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["linux-x64-musl"] (empty = all)'
type: string
default: ''
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
permissions:
contents: read
concurrency:
group: wekan-musl-${{ inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
tag: ${{ steps.plan.outputs.tag }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
matrix="$(python3 -c '
import json, os
targets = json.load(open("releases/musl/targets.json"))
only = json.loads(os.environ.get("ONLY") or "[]")
if only:
targets = [t for t in targets if t["target"] in only]
print(json.dumps({"include": targets}))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Release $tag; targets: $matrix"
build:
needs: plan
name: musl ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TAG: ${{ needs.plan.outputs.tag }}
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Fetch and verify the Linux bundle and the musl Node.js
id: fetch
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ matrix.base }}
NODE_PLATFORM: ${{ matrix.node }}
run: |
set -euo pipefail
version="${TAG#v}"
zip="wekan-${version}-${BASE}.zip"
if ! gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$zip" --pattern "$zip.sha256sum" || [ ! -f "$zip" ]; then
echo "::notice::$TARGET: $TAG has no $zip; skipped."
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
fi
[ -f "$zip.sha256sum" ] || { echo "::error::$zip has no published .sha256sum; a bundle is never rebuilt from an unverified one."; exit 1; }
sha256sum -c "$zip.sha256sum"
unzip -q "$zip"
rm -f "$zip" "$zip.sha256sum"
# The musl Node.js of exactly the bundle's version.
node_version="$(grep -aoE 'nodejs\.org/download/release/v[0-9]+\.[0-9]+\.[0-9]+/' bundle/node | head -n1 | sed -E 's#.*/(v[0-9.]+)/#\1#')"
[ -n "$node_version" ] || { echo "::error::Could not read the Node.js version of bundle/node."; exit 1; }
rc=0; out="$(bash releases/resolve-node-source.sh "$NODE_PLATFORM" "$node_version")" || rc=$?
if [ "$rc" = 3 ] || ! printf '%s\n' "$out" | grep -q '^node_found=true'; then
echo "::notice::$TARGET: no musl Node.js $node_version is published yet; skipped."
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
fi
eval "$(printf '%s\n' "$out" | grep -E '^node_(url|sha256|kind)=')"
[ "$node_kind" = tar.xz ] || { echo "::error::expected a tar.xz musl Node.js, got $node_kind"; exit 1; }
bash releases/fetch.sh -o node-musl.tar.xz "$node_url"
[ -n "$node_sha256" ] || { echo "::error::$node_url has no published SHA256."; exit 1; }
echo "$node_sha256 node-musl.tar.xz" | sha256sum -c -
mkdir -p musl-tools
tar -xJf node-musl.tar.xz --strip-components=1 -C musl-tools
rm -f node-musl.tar.xz
file musl-tools/bin/node bundle/ferretdb
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Rebuild and start it in Alpine
if: steps.fetch.outputs.skip != 'true'
env:
ARCH: ${{ matrix.arch }}
run: |
docker run --rm -v "$PWD:/work" -w /work -e ARCH \
alpine:3.23 sh -ec '
apk add --no-cache bash coreutils curl file python3 make g++ linux-headers binutils libstdc++ libgcc libatomic
bash releases/musl/build-bundle.sh "$ARCH"'
- name: Package
if: steps.fetch.outputs.skip != 'true'
env:
VERSION: ${{ steps.fetch.outputs.version }}
run: |
set -euo pipefail
sudo chown -R "$(id -u):$(id -g)" bundle
out="wekan-${VERSION}-${TARGET}.zip"
zip -qry "$out" bundle
sha256sum "$out" > "$out.sha256sum"
ls -la "$out" "$out.sha256sum"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: steps.fetch.outputs.skip != 'true'
with:
name: musl-${{ matrix.target }}
path: wekan-*-${{ matrix.target }}.zip*
retention-days: 3
compression-level: 0
publish:
needs: [plan, build]
if: ${{ !cancelled() && inputs.publish }}
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: musl-*
path: artifacts
- name: Attach every musl bundle that was built and started
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.plan.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
published=0
for f in artifacts/musl-*/wekan-*.zip; do
(cd "$(dirname "$f")" && sha256sum -c "$(basename "$f").sha256sum")
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$TAG" "$f" "$f.sha256sum"
published=$((published + 1))
done
echo "Published $published musl bundles to $TAG."
+169
View File
@@ -0,0 +1,169 @@
name: Nextcloud App Store
run-name: Nextcloud ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
required: true
type: string
description: 'Existing WeKan release version, without v'
permissions:
contents: write
packages: write
concurrency:
group: wekan-nextcloud-${{ inputs.version }}
cancel-in-progress: false
jobs:
nextcloud:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
VERSION: ${{ inputs.version }}
steps:
- name: Check required secrets
id: guard
env:
REPO_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
NC_KEY: ${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}
NC_CRT: ${{ secrets.NEXTCLOUD_APP_PUBLIC_CRT }}
NC_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
REGISTRY_AUTH: ${{ secrets.GHCR_AUTH }}
run: |
set -eu
if [ -z "$REPO_TOKEN" ] || [ -z "$NC_KEY" ] || [ -z "$NC_CRT" ] || [ -z "$NC_TOKEN" ] || [ -z "$REGISTRY_AUTH" ]; then
echo 'ok=false' >> "$GITHUB_OUTPUT"
echo '::notice::Nextcloud publishing not configured. See docs/Platforms/FOSS/Container/Docker/Nextcloud/README.md.'
else
echo 'ok=true' >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: steps.guard.outputs.ok == 'true'
- name: Prepare temporary directory and validate release
if: steps.guard.outputs.ok == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$GITHUB_WORKSPACE/.tools/tmp"
echo "TMPDIR=$GITHUB_WORKSPACE/.tools/tmp" >> "$GITHUB_ENV"
python3 -c 'import sys; sys.path.insert(0,"releases"); import importlib; importlib.import_module("nextcloud-release").app_version(sys.argv[1])' "$VERSION"
gh release view "v$VERSION" --repo wekan/wekan --json tagName
- name: Checkout the ExApp definition
if: steps.guard.outputs.ok == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wekan/nextcloud
token: ${{ secrets.WEKAN_REPO_TOKEN }}
persist-credentials: false
path: exapp
- name: Check App Store and prepare versioned metadata
id: plan
if: steps.guard.outputs.ok == 'true'
run: python3 releases/nextcloud-release.py --version "$VERSION"
- name: Set up QEMU
if: steps.guard.outputs.ok == 'true' && steps.plan.outputs.skip != 'true'
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up Docker Buildx
if: steps.guard.outputs.ok == 'true' && steps.plan.outputs.skip != 'true'
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Publish the missing versioned ExApp image
if: steps.guard.outputs.ok == 'true' && steps.plan.outputs.skip != 'true'
env:
GHCR_AUTH: ${{ secrets.GHCR_AUTH }}
run: |
set +x
set -euo pipefail
credentials="$(printf '%s' "$GHCR_AUTH" | base64 -d)"
[[ "$credentials" == *:* && -n "${credentials%%:*}" && -n "${credentials#*:}" ]]
printf '%s' "${credentials#*:}" | docker login ghcr.io -u "${credentials%%:*}" --password-stdin
unset credentials
docker pull "ghcr.io/wekan/wekan:v$VERSION"
# Old releases lack the connection-bound Nextcloud authentication.
docker run --rm --entrypoint /bin/sh "ghcr.io/wekan/wekan:v$VERSION" -ec \
'test -f /build/nextcloud-exapp-capability.json && test -x /build/ferretdb'
node --test exapp/tests/*.test.mjs
image="ghcr.io/wekan/wekan-exapp:v$VERSION"
if docker buildx imagetools inspect "$image" > .tools/exapp-image.txt 2> .tools/exapp-image-error.txt; then
echo 'Keeping the existing versioned ExApp image.'
elif grep -Eqi 'manifest unknown|name unknown|not found' .tools/exapp-image-error.txt; then
docker buildx build --push --provenance=mode=max --platform linux/amd64,linux/arm64 \
--tag "$image" --build-arg "WEKAN_VERSION=$VERSION" exapp
else
cat .tools/exapp-image-error.txt >&2
echo '::error::Cannot inspect the Nextcloud ExApp image; refusing to treat a registry error as an absent image.' >&2
exit 1
fi
# No latest tag or companion-branch update: recovery of an older release
# must not roll back a newer deployment or the companion source tree.
- name: Package, sign and publish to the Nextcloud App Store
if: steps.guard.outputs.ok == 'true' && steps.plan.outputs.skip != 'true'
env:
APP_ID: ${{ steps.plan.outputs.app_id }}
APP_PRIVATE_KEY: ${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}
APP_PUBLIC_CRT: ${{ secrets.NEXTCLOUD_APP_PUBLIC_CRT }}
APPSTORE_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set +x
set -euo pipefail
umask 077
task_dir="$(mktemp -d "$TMPDIR/nextcloud-sign.XXXXXX")"
trap 'rm -rf -- "$task_dir"' EXIT
archive="${APP_ID}.tar.gz"
new_archive=false
gh release view --repo wekan/wekan "v$VERSION" --json assets > "$task_dir/assets.json"
if jq -e --arg name "$archive" '.assets | any(.name == $name)' "$task_dir/assets.json" >/dev/null; then
gh release download --repo wekan/wekan "v$VERSION" --pattern "$archive" --dir "$task_dir"
else
releases/nextcloud-pack.sh exapp "$APP_ID" "$task_dir/$archive"
new_archive=true
fi
python3 releases/nextcloud-release.py --version "$VERSION" --archive "$task_dir/$archive" --app-id "$APP_ID"
if [ "$new_archive" = true ]; then
gh release upload --repo wekan/wekan "v$VERSION" "$task_dir/$archive"
fi
printf '%s' "$APP_PRIVATE_KEY" > "$task_dir/app.key"
printf '%s' "$APP_PUBLIC_CRT" > "$task_dir/app.crt"
openssl x509 -in "$task_dir/app.crt" -pubkey -noout > "$task_dir/public.key"
openssl dgst -sha512 -sign "$task_dir/app.key" -out "$task_dir/signature" "$task_dir/$archive"
openssl dgst -sha512 -verify "$task_dir/public.key" -signature "$task_dir/signature" "$task_dir/$archive"
signature="$(openssl base64 -A -in "$task_dir/signature")"
download="https://github.com/wekan/wekan/releases/download/v${VERSION}/${archive}"
jq -n --arg download "$download" --arg signature "$signature" \
'{download: $download, signature: $signature, nightly: false}' > "$task_dir/release.json"
curl --fail-with-body --silent --show-error --max-time 300 \
-X POST https://apps.nextcloud.com/api/v1/apps/releases \
-H "Authorization: Token $APPSTORE_TOKEN" -H 'Content-Type: application/json' \
--data-binary "@$task_dir/release.json"
- name: Job result
if: always()
env:
ENABLED: ${{ steps.guard.outputs.ok }}
SKIPPED: ${{ steps.plan.outputs.skip }}
RESULT: ${{ job.status }}
run: |
if [ "$ENABLED" != true ]; then
echo 'Nextcloud: not configured; no publication attempted.' >> "$GITHUB_STEP_SUMMARY"
elif [ "$RESULT" != success ]; then
echo 'Nextcloud: FAILED; inspect this job.' >> "$GITHUB_STEP_SUMMARY"
elif [ "$SKIPPED" = true ]; then
echo 'Nextcloud: already published; left unchanged.' >> "$GITHUB_STEP_SUMMARY"
else
echo 'Nextcloud: App Store upload accepted.' >> "$GITHUB_STEP_SUMMARY"
fi
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v7
- name: Setup Node 26
uses: actions/setup-node@v6
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '26'
cache: 'npm'
@@ -92,7 +92,7 @@ jobs:
- uses: actions/checkout@v7
- name: Setup Node 26
uses: actions/setup-node@v6
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '26'
cache: 'npm'
@@ -220,7 +220,7 @@ jobs:
- uses: actions/checkout@v7
- name: Setup Node 26
uses: actions/setup-node@v6
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '26'
cache: 'npm'
+92 -28
View File
@@ -10,7 +10,11 @@ name: Release all missing
# those six without running the whole thing again, version bump and all.
#
# This finishes a release that already exists. It never bumps a version, never
# tags, and never touches Docker, snap or the website.
# tags, and does not rebuild the main Docker images, snap or the website.
# UCS and Nextcloud portal publishing runs separately through their manual workflows.
# UCS migration tools are part of the main WeKan Docker image. recover retries
# its failed Docker build/publish job, including the bundled-tools smoke check;
# there is no separate migration image to build or registry package to publish.
#
# It DOES publish the Helm chart, because that is a thing a release can be
# missing like any other: the chart entry is written once, during the release it
@@ -53,9 +57,9 @@ on:
required: false
type: string
# Read-only by default; the job that writes is granted it below.
permissions:
contents: write
actions: write
contents: read
env:
DO_NOT_TRACK: '1'
@@ -70,10 +74,15 @@ concurrency:
jobs:
recover:
permissions:
contents: read
actions: write
runs-on: ubuntu-latest
timeout-minutes: 360
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check dependency risk indicators before recovery
run: python3 releases/remote-release.py --audit
- name: Retry failed jobs of the matching full release
@@ -88,6 +97,12 @@ jobs:
plan:
needs: recover
# The planner runs whatever recover's rerun ended with. A rerun that still
# fails - Launchpad answering 503, a fault that is in the tagged code and
# so in every rerun of it (v12.68's OpenBSD start) - used to skip the
# planner and every build after it, so not even the assets that COULD be
# filled were. recover stays red and names the jobs; the rest goes on.
if: ${{ !cancelled() }}
timeout-minutes: 15
name: what the release is missing
runs-on: ubuntu-latest
@@ -95,13 +110,16 @@ jobs:
version: ${{ steps.plan.outputs.version }}
extra: ${{ steps.plan.outputs.extra }}
appimage: ${{ steps.plan.outputs.appimage }}
bun: ${{ steps.plan.outputs.bun }}
mac: ${{ steps.plan.outputs.mac }}
flatpak: ${{ steps.plan.outputs.flatpak }}
windows: ${{ steps.plan.outputs.windows }}
manual: ${{ steps.plan.outputs.manual }}
total: ${{ steps.plan.outputs.total }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check dependency risk indicators before release work
shell: bash
@@ -114,10 +132,11 @@ jobs:
- id: plan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
V='${{ inputs.version }}'
V="${INPUT_VERSION}"
V="${V#v}"
if [ -z "$V" ]; then
V="$(gh release view --repo "${GITHUB_REPOSITORY:-wekan/wekan}" --json tagName --jq .tagName)"
@@ -149,7 +168,7 @@ jobs:
total="$(wc -l < missing.txt | tr -d ' ')"
echo "total=$total" >> "$GITHUB_OUTPUT"
for kind in extra appimage mac flatpak windows; do
for kind in extra appimage mac flatpak windows bun; do
sels="$(awk -v k="$kind" '$1 == k { print $2 }' missing.txt)"
json="$(printf '%s\n' $sels | sed '/^$/d' | jq -R . | jq -sc .)"
[ -n "$json" ] || json='[]'
@@ -184,16 +203,21 @@ jobs:
- name: Say plainly what this run will not do
if: ${{ steps.plan.outputs.manual != '' }}
env:
STEP_PLAN_VERSION: ${{ steps.plan.outputs.version }}
STEP_PLAN_MANUAL: ${{ steps.plan.outputs.manual }}
run: |
echo "::error::v${{ steps.plan.outputs.version }} still lacks ${{ steps.plan.outputs.manual }} after recovery. Inspect the matching full-release run; older unnamed runs need their failed jobs retried manually."
echo "::error::v${STEP_PLAN_VERSION} still lacks ${STEP_PLAN_MANUAL} after recovery. Inspect the matching full-release run; older unnamed runs need their failed jobs retried manually."
exit 1
# The six architectures nobody else builds for, repacked from the PUBLISHED
# amd64 bundle - which is what makes this possible without a Meteor build.
extra-arches:
permissions:
contents: write
name: ${{ matrix.arch }}
needs: plan
if: ${{ needs.plan.outputs.extra != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.extra != '[]' }}
runs-on: ubuntu-24.04
timeout-minutes: 300
strategy:
@@ -222,15 +246,20 @@ jobs:
steps:
- name: Skip the architectures this release already has
id: want
env:
NEEDS_PLAN_EXTRA: ${{ needs.plan.outputs.extra }}
MATRIX_ARCH: ${{ matrix.arch }}
run: |
if printf '%s' '${{ needs.plan.outputs.extra }}' | jq -e --arg a '${{ matrix.arch }}' 'index($a)' >/dev/null; then
if printf '%s' "${NEEDS_PLAN_EXTRA}" | jq -e --arg a "${MATRIX_ARCH}" 'index($a)' >/dev/null; then
echo "build=yes" >> "$GITHUB_OUTPUT"
else
echo "build=no" >> "$GITHUB_OUTPUT"
echo "v${VERSION} already has ${{ matrix.arch }}; nothing to do."
echo "v${VERSION} already has ${MATRIX_ARCH}; nothing to do."
fi
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Audit telemetry source before release work
shell: bash
@@ -243,7 +272,7 @@ jobs:
- name: Set up QEMU for ${{ matrix.arch }} emulation
if: ${{ steps.want.outputs.build == 'yes' }}
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
# Before the download, because actions/checkout cleans the workspace. if: ${{ steps.want.outputs.build == 'yes' }}
@@ -296,18 +325,33 @@ jobs:
if: ${{ always() && steps.want.outputs.build == 'yes' && steps.repack.outcome == 'success' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MATRIX_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
asset="wekan-${VERSION}-${{ matrix.arch }}.zip"
asset="wekan-${VERSION}-${MATRIX_ARCH}.zip"
# Retried, bounded, and verified by name AND size from the release
# side - an upload that reports success and leaves no asset behind is
# the failure nobody notices until a download 404s.
bash releases/github-release-upload.sh "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" "$asset" "${asset}.sha256sum"
bun:
name: Missing Bun runtime bundles
needs: plan
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.bun != '[]' }}
uses: ./.github/workflows/bun.yml
permissions:
contents: write
actions: read
with:
tag: v${{ needs.plan.outputs.version }}
publish: true
only: ${{ needs.plan.outputs.bun }}
missing_only: true
appimage:
name: AppImages
needs: plan
if: ${{ needs.plan.outputs.appimage != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.appimage != '[]' }}
uses: ./.github/workflows/AppImage.yml
permissions:
contents: write
@@ -319,7 +363,7 @@ jobs:
mac:
name: Mac apps
needs: plan
if: ${{ needs.plan.outputs.mac != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.mac != '[]' }}
uses: ./.github/workflows/mac.yml
permissions:
contents: write
@@ -331,7 +375,7 @@ jobs:
flatpak:
name: Flatpaks
needs: plan
if: ${{ needs.plan.outputs.flatpak != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.flatpak != '[]' }}
uses: ./.github/workflows/Flatpak.yml
permissions:
contents: write
@@ -343,7 +387,7 @@ jobs:
windows-exe:
name: Windows single EXE
needs: plan
if: ${{ needs.plan.outputs.windows != '[]' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.windows != '[]' }}
uses: ./.github/workflows/windows.yml
permissions:
contents: write
@@ -351,6 +395,22 @@ jobs:
tag: v${{ needs.plan.outputs.version }}
publish: true
# A single file per platform, for every platform whose bundle the release
# has and whose single file it does not (docs/Platforms/Single-File.md).
# Not in expected-assets.sh: a platform without a bundle has no single file
# to miss. After extra-arches, which may have just published a bundle.
single-file:
name: Missing single-file WeKan
needs: [plan, extra-arches]
if: ${{ !cancelled() && needs.plan.result == 'success' }}
uses: ./.github/workflows/single-file.yml
permissions:
contents: write
with:
tag: v${{ needs.plan.outputs.version }}
publish: true
missing_only: true
# ─── The Helm chart, for a release that never got one ───────────────────────
#
@@ -371,12 +431,11 @@ jobs:
timeout-minutes: 30
name: Helm chart
needs: plan
if: ${{ !cancelled() && needs.plan.outputs.version != '' }}
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.version != '' }}
runs-on: ubuntu-latest
steps:
- name: Checkout wekan (for releases/release-charts.sh)
if: steps.check.outputs.skip != 'true'
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: wekan
@@ -418,7 +477,7 @@ jobs:
- name: Checkout wekan/charts (main + gh-pages)
if: steps.check.outputs.skip != 'true'
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wekan/charts
token: ${{ secrets.WEKAN_REPO_TOKEN }}
@@ -427,12 +486,13 @@ jobs:
- name: Install Helm
if: steps.check.outputs.skip != 'true'
uses: azure/setup-helm@v5
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
- name: Publish the chart for this release
if: steps.check.outputs.skip != 'true'
env:
CHARTS_DIR: ${{ github.workspace }}/charts
NEEDS_PLAN_VERSION: ${{ needs.plan.outputs.version }}
run: |
set -euo pipefail
cd charts
@@ -442,16 +502,18 @@ jobs:
git branch --track gh-pages origin/gh-pages 2>/dev/null || true
git checkout main
cd ..
./wekan/releases/release-charts.sh "${{ needs.plan.outputs.version }}"
./wekan/releases/release-charts.sh "${NEEDS_PLAN_VERSION}"
- name: Job result
if: always()
env:
JOB_STATUS: ${{ job.status }}
run: |
case "${{ job.status }}" in
case "${JOB_STATUS}" in
success) mark='OK ' ;;
failure) mark='FAILED ' ;;
cancelled) mark='CANCELLED' ;;
*) mark='${{ job.status }}' ;;
*) mark="${JOB_STATUS}" ;;
esac
echo "$mark charts" >> "$GITHUB_STEP_SUMMARY"
@@ -463,11 +525,13 @@ jobs:
done:
timeout-minutes: 15
name: what is still missing
needs: [plan, extra-arches, appimage, mac, flatpak, windows-exe, charts]
needs: [plan, extra-arches, appimage, mac, flatpak, windows-exe, charts, bun]
if: ${{ always() && needs.plan.result == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Audit telemetry source before release work
shell: bash
run: python3 ./releases/check-telemetry.py --source .
File diff suppressed because it is too large. Load diff
+8 -2
View File
@@ -4,6 +4,11 @@ on:
push:
branches:
- main
# This repository has no charts/ directory: the Helm chart is published to
# wekan/charts by release-all.yml. Without this filter every push to main
# cloned the full history (447 MB) to find no chart to release.
paths:
- 'charts/**'
permissions:
contents: read
@@ -16,9 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Configure Git
run: |
@@ -26,6 +32,6 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.7.0
uses: helm/chart-releaser-action@df287104677278f869ca5480bd260358397cc3f9 # v1.8.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+12 -12
View File
@@ -38,18 +38,15 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Use npm 12.0.2
run: npm install --global npm@12.0.2
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
- name: Use release npm version
run: bash releases/use-release-npm.sh
- name: Install system dependencies
run: |
@@ -58,12 +55,12 @@ jobs:
- name: Install Sandstorm (dev, unattended)
# Provides the `spk` command used to publish, and the sandstorm-http-bridge that
# build-deps.sh copies into the meteor-spk deps.
run: curl https://install.sandstorm.io | sudo bash -s -- -d
run: curl https://install.sandstorm.org | sudo bash -s -- -d
- name: Assemble modern meteor-spk.deps (same as sandstorm.yml)
run: |
bash sandstorm-src/build-deps.sh
echo "$HOME/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
echo "$GITHUB_WORKSPACE/.tools/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
- name: Restore the Sandstorm signing keyring
env:
@@ -143,4 +140,7 @@ jobs:
fi
echo "Publishing $SPK with $SPK_BIN ..."
# Same command as releases/release-sandstorm.sh: spk publish wekan-<VERSION>.spk
"$SPK_BIN" publish "$SPK"
# Publish to apps.sandstorm.org
"$SPK_BIN" publish --webkey=https://api-b72c46193b266fd6c33e1299dccc9bfa.oasis.sandstorm.org#iCDu7VYl4qZxVbDLXLIE2XSU0bEadaBgApEMqgCkLmP "$SPK"
# Publish to apps.sandstorm.io
"$SPK_BIN" publish --webkey=https://alpha-api.sandstorm.io/#Rs-0TT13YrNSbv7Fiz5K9bBkLaJn3E5TB0PU1GSn1HE "$SPK"
+31 -23
View File
@@ -5,6 +5,11 @@ name: Build Sandstorm .spk
# Release as an attachment, so you can download it, test whether the Sandstorm
# build works, and see any errors — without running a full release.
#
# It builds only what a grain runs (docs/Platforms/FOSS/Container/Sandstorm/
# Smaller-wekan-spk.md): WeKan with the Sandstorm package profile - Sandstorm
# login only, no password/OAuth2/OIDC/CAS/LDAP/SAML packages - FerretDB from
# source with only its SQLite handler, and the deps meteor-spk.deps needs.
#
# gh workflow run sandstorm.yml
#
# EXPERIMENTAL in CI (same caveats as the build-sandstorm job in release-all.yml):
@@ -32,27 +37,18 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Build the dispatched ref so a fix branch can be tested before it is
# merged to main; defaults to main for a plain `gh workflow run`.
ref: ${{ github.event.inputs.ref || 'main' }}
persist-credentials: false
- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Use npm 12.0.2
run: npm install --global npm@12.0.2
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
# Ubuntu 24.04's AppArmor defaults kernel.apparmor_restrict_unprivileged_userns=1,
# which blocks the unprivileged user namespaces that the Sandstorm install and
# the spk supervisor rely on (the "fragile part" noted above). Relax it so
# `install.sandstorm.io` and meteor-spk can create sandboxes on the runner.
# `|| true`: on kernels/runners without these knobs, setting them is a no-op.
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
- name: Use release npm version
run: bash releases/use-release-npm.sh
- name: Install system dependencies and Meteor
run: |
@@ -64,17 +60,24 @@ jobs:
# Installed BEFORE assembling the deps: build-deps.sh's [bridge] step copies
# the modern /opt/sandstorm/latest/bin/sandstorm-http-bridge over the ancient
# bundled one, so the Sandstorm install must be in place first.
run: curl https://install.sandstorm.io | sudo bash -s -- -d
run: curl https://install.sandstorm.org | sudo bash -s -- -d
- name: Assemble modern meteor-spk.deps (Node 26 + FerretDB + Mongo 3.x CLIs)
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io (has mongod 3.0 +
- name: Assemble modern meteor-spk.deps (Node 26 + SQLite-only FerretDB + Mongo 3.x CLIs)
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.org (has mongod 3.0 +
# niscud). build-deps.sh swaps in Node 26, adds FerretDB + the Mongo 3.x
# CLIs + the migration launcher/importer, keeps niscud, and swaps in the
# modern sandstorm-http-bridge from the Sandstorm install above. No dependency
# on the retired releases.wekan.team or the old projects.7z.
# FerretDB is the PREBUILT SQLite-only ferretdb-sqlite-amd64 from the newest
# wekan/FerretDB release that has it (the full ferretdb-amd64 of the newest
# release until one does), picked by releases/ferretdb-asset.sh - nothing
# is compiled here (docs/Platforms/FOSS/Container/Sandstorm/Smaller-wekan-spk.md).
env:
FERRETDB_SOURCE: download
GH_TOKEN: ${{ github.token }}
run: |
bash sandstorm-src/build-deps.sh
echo "$HOME/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
echo "$GITHUB_WORKSPACE/.tools/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
- name: Restore the Sandstorm signing keyring
env:
@@ -90,7 +93,7 @@ jobs:
chmod 600 "$HOME/.sandstorm-keyring"
echo "Restored ~/.sandstorm-keyring ($(wc -c < "$HOME/.sandstorm-keyring") bytes)."
else
echo "::error::SANDSTORM_KEYRING secret is not set. meteor-spk pack signs the .spk with the WeKan Sandstorm app's private key, which lives in this keyring, so packing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
echo "::error::SANDSTORM_KEYRING secret is not set. spk pack signs the .spk with the WeKan Sandstorm app's private key, which lives in this keyring, so packing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
exit 1
fi
@@ -112,7 +115,11 @@ jobs:
set -euo pipefail
STAMP="$(date +'%Y_%m_%d-%H_%M_%S')"
SPK="wekan-sandstorm-${STAMP}.spk"
meteor-spk pack "$SPK"
# The Sandstorm package profile, the linux/x64 sockjs trim and a
# `spk pack` of the trimmed trees (not `meteor-spk pack`, which would
# rebuild the bundle and undo the trim). The script restores
# .meteor/packages and .meteor/versions when it ends.
bash releases/sandstorm-slim-spk.sh "$SPK"
# Report the packed size and warn if it exceeds Cloudflare's 100 MB upload
# cap (a proxied browser upload to a Cloudflare-fronted Sandstorm 413s above
# this; install via a DNS-only host / SSH tunnel, or trim further).
@@ -124,7 +131,7 @@ jobs:
ls -la "$SPK"
echo "spk=$SPK" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ steps.build.outputs.spk }}
path: ${{ steps.build.outputs.spk }}
@@ -134,6 +141,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
STEP_BUILD_SPK: ${{ steps.build.outputs.spk }}
run: |
set -euo pipefail
TAG="$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || true)"
@@ -141,5 +149,5 @@ jobs:
echo "::error::No existing GitHub Release found to attach the .spk to."
exit 1
fi
echo "Uploading ${{ steps.build.outputs.spk }} to the newest release: $TAG"
gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "$TAG" "${{ steps.build.outputs.spk }}" --clobber
echo "Uploading ${STEP_BUILD_SPK} to the newest release: $TAG"
gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "$TAG" "${STEP_BUILD_SPK}" --clobber
+392
View File
@@ -0,0 +1,392 @@
name: Single-file WeKan
# One executable per platform (docs/Platforms/Single-File.md): that
# platform's published WeKan bundle - its Node.js, FerretDB and native
# addons, all built where they run - repackaged by releases/single-file/
# build.mjs as a Node.js single executable application whose ZIP Node mounts
# as a virtual file system. Nothing is compiled here.
#
# Per platform: verify the bundle's checksum, build the single file (on a Mac
# for macOS, which has to re-sign it), start it on a runner of that platform
# where GitHub has one (with a browser test on amd64), and attach it to the
# release. A platform whose bundle the release does not have yet - the BSDs
# until their Node.js is published, Android until it has a bundle - is
# SKIPPED, not failed, and starts building by itself once it does.
on:
workflow_dispatch:
inputs:
tag:
description: 'Existing WeKan release tag (empty = latest published release)'
type: string
required: false
publish:
description: 'Attach the single files and checksums to that release'
type: boolean
default: true
only:
description: 'JSON target list, e.g. ["amd64","win64"] (empty = all)'
type: string
default: ''
missing_only:
description: 'Only the platforms whose single file the release does not have yet'
type: boolean
default: false
workflow_call:
inputs:
tag:
type: string
required: true
publish:
type: boolean
default: true
only:
type: string
default: ''
missing_only:
type: boolean
default: false
permissions:
contents: read
concurrency:
group: wekan-single-file-${{ inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
tag: ${{ steps.plan.outputs.tag }}
matrix: ${{ steps.plan.outputs.matrix }}
any: ${{ steps.plan.outputs.any }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: plan
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
ONLY: ${{ inputs.only }}
MISSING_ONLY: ${{ inputs.missing_only }}
run: |
set -euo pipefail
tag="${TAG:-$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
: > existing.txt
if [ "$MISSING_ONLY" = true ]; then
gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' > existing.txt
fi
matrix="$(VERSION="${tag#v}" python3 -c '
import json, os
targets = json.load(open("releases/single-file/targets.json"))
only = json.loads(os.environ.get("ONLY") or "[]")
if only:
targets = [t for t in targets if t["target"] in only]
existing = set(open("existing.txt").read().split())
def name(t):
ext = ".exe" if t["target"].startswith("win") else ""
return "wekan-%s-%s-single-file%s" % (os.environ["VERSION"], t["target"], ext)
targets = [t for t in targets if not (name(t) in existing and name(t) + ".sha256sum" in existing)]
print(json.dumps({"include": targets}))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "any=$([ "$matrix" = '{"include": []}' ] && echo false || echo true)" >> "$GITHUB_OUTPUT"
echo "Release $tag; targets: $matrix"
build:
needs: plan
if: needs.plan.outputs.any == 'true'
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.build_on }}
timeout-minutes: 60
permissions:
contents: write
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TAG: ${{ needs.plan.outputs.tag }}
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.plan.outputs.tag }}
- name: Fetch and verify ${{ matrix.target }}'s published bundle
id: fetch
env:
GH_TOKEN: ${{ github.token }}
ZIP_PATTERN: ${{ matrix.zip }}
run: |
set -euo pipefail
version="${TAG#v}"
zip="${ZIP_PATTERN//\{version\}/$version}"
if ! gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$zip" --pattern "$zip.sha256sum"; then
echo "::notice::$TARGET is not built this release: $TAG has no $zip."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
[ -f "$zip" ] || { echo "::notice::$TARGET: $TAG has no $zip."; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
[ -f "$zip.sha256sum" ] || { echo "::error::$zip has no published .sha256sum; a single file is never built from an unverified bundle."; exit 1; }
if command -v sha256sum >/dev/null; then sha256sum -c "$zip.sha256sum"; else shasum -a 256 -c "$zip.sha256sum"; fi
unzip -q "$zip"
rm -f "$zip"
node_bin=bundle/node
[ -f "$node_bin" ] || node_bin=bundle/node.exe
node_version="$(grep -aoE 'nodejs\.org/download/release/v[0-9]+\.[0-9]+\.[0-9]+/' "$node_bin" | head -n1 | sed -E 's#.*/(v[0-9.]+)/#\1#')"
[ -n "$node_version" ] || { echo "::error::Could not read the Node.js version of $node_bin."; exit 1; }
echo "node_version=$node_version" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "$TARGET: bundle Node.js $node_version"
# The SEA blob format belongs to one Node.js version: the builder runs
# on the same version the bundle embeds (build.mjs checks it too).
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: steps.fetch.outputs.skip != 'true'
with:
node-version: ${{ steps.fetch.outputs.node_version }}
# A 32-bit target's SEA blob must be written by a 32-bit Node.js: the blob
# stores its lengths as size_t (build.mjs refuses otherwise). The i386
# bundle's Node.js, of the same version, writes it for every 32-bit
# target - win32, i386 and 32-bit ARM read the same little-endian format.
- name: A 32-bit Node.js to build a 32-bit target
id: host32
if: steps.fetch.outputs.skip != 'true' && contains(fromJSON('["win32","i386","armv7","armhf","armv6"]'), matrix.target)
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.fetch.outputs.version }}
run: |
set -euo pipefail
zip="wekan-${VERSION}-i386.zip"
mkdir -p host32
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$zip" --pattern "$zip.sha256sum" --dir host32 \
|| { echo "::error::$TARGET needs the i386 bundle's Node.js to build, and $TAG has no $zip."; exit 1; }
(cd host32 && sha256sum -c "$zip.sha256sum" && unzip -q "$zip" 'bundle/node' 'bundle/node-runtime/*' && rm -f "$zip")
sudo dpkg --add-architecture i386
sudo apt-get update -qq
sudo apt-get install -y -qq libc6:i386 libstdc++6:i386 >/dev/null
host="$PWD/host32/bundle/node"
[ -d host32/bundle/node-runtime ] && echo "libs=$PWD/host32/bundle/node-runtime" >> "$GITHUB_OUTPUT"
echo "node=$host" >> "$GITHUB_OUTPUT"
# A 32-bit ELF target's blob is injected in a 64-bit process, by
# LIEF 1.0.0 - the version Node.js vendors - not inside this 32-bit
# Node.js, which ran out of memory doing it (build.mjs, seaSplit).
if [ "$TARGET" != win32 ]; then
python3 -m venv "$RUNNER_TEMP/lief"
"$RUNNER_TEMP/lief/bin/python" -m pip install --quiet 'lief==1.0.0'
echo "python=$RUNNER_TEMP/lief/bin/python" >> "$GITHUB_OUTPUT"
fi
- name: Build the single file
id: build
if: steps.fetch.outputs.skip != 'true'
env:
VERSION: ${{ steps.fetch.outputs.version }}
HOST_NODE: ${{ steps.host32.outputs.node }}
HOST_LIBS: ${{ steps.host32.outputs.libs }}
SEA_PYTHON: ${{ steps.host32.outputs.python }}
run: |
set -euo pipefail
case "$TARGET" in win*) ext=.exe ;; *) ext= ;; esac
out="wekan-${VERSION}-${TARGET}-single-file${ext}"
# The i386 Node.js finds its own libraries; the libraries are ignored by
# the x64 Node.js that runs build.mjs (another architecture).
# Exit 3: this Node.js is one LIEF cannot inject the SEA into
# correctly (releases/single-file/sea-inject.mjs says which and why).
# That is a missing single file, not a failed release.
rc=0
LD_LIBRARY_PATH="${HOST_LIBS:-${LD_LIBRARY_PATH:-}}" \
node releases/single-file/build.mjs --bundle bundle --target "$TARGET" --version "$VERSION" --out "$out" \
${HOST_NODE:+--host-node "$HOST_NODE"} 2> build.err || rc=$?
cat build.err >&2
rm -rf bundle
if [ "$rc" = 3 ]; then
reason="$(sed -n 's/^single-file build: //p' build.err | head -n1)"
echo "::notice::$reason"
printf '%s\n' "$reason" > "single-file-skipped-$TARGET.txt"
rm -f build.err; exit 0
fi
rm -f build.err
[ "$rc" = 0 ] || exit "$rc"
ls -la "$out" "$out.sha256sum"
# A target that was not built still leaves an artifact - one line saying
# why - so its Start job finds something to read instead of logging an
# "Artifact not found" error for a release that is fine.
- name: Say why there is no single file
if: steps.fetch.outputs.skip == 'true'
run: |
[ -f "single-file-skipped-$TARGET.txt" ] || echo "$TARGET: this release has no bundle to build it from." > "single-file-skipped-$TARGET.txt"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: single-file-${{ matrix.target }}
path: |
wekan-*-${{ matrix.target }}-single-file*
single-file-skipped-${{ matrix.target }}.txt
if-no-files-found: error
retention-days: 3
compression-level: 0
smoke:
needs: [plan, build]
if: ${{ !cancelled() && needs.plan.outputs.any == 'true' }}
name: Start ${{ matrix.target }}
runs-on: ${{ matrix.smoke == 'none' && 'ubuntu-24.04' || matrix.smoke }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
TARGET: ${{ matrix.target }}
defaults:
run:
shell: bash
steps:
- name: Get the single file
id: get
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
name: single-file-${{ matrix.target }}
path: sf
- name: Is there one to start here?
id: what
env:
STEP_GET_OUTCOME: ${{ steps.get.outcome }}
MATRIX_SMOKE: ${{ matrix.smoke }}
run: |
set -euo pipefail
if [ "${STEP_GET_OUTCOME}" != success ]; then
echo "run=false" >> "$GITHUB_OUTPUT"; echo "::notice::$TARGET was not built this release."; exit 0
fi
if [ -f "sf/single-file-skipped-$TARGET.txt" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"; echo "::notice::$(cat "sf/single-file-skipped-$TARGET.txt")"; exit 0
fi
if [ "${MATRIX_SMOKE}" = none ]; then
echo "run=false" >> "$GITHUB_OUTPUT"
echo "::notice::GitHub has no runner for $TARGET: the single file is published without being started (its bundle was tested where it was built)."
mkdir -p ok && echo "$TARGET untested" > "ok/$TARGET"; exit 0
fi
exe="$(ls sf/wekan-*-single-file* | grep -v '\.sha256sum$' | head -n1)"
chmod +x "$exe"
echo "exe=$PWD/$exe" >> "$GITHUB_OUTPUT"
echo "run=true" >> "$GITHUB_OUTPUT"
- name: Start it, as a user starts it
if: steps.what.outputs.run == 'true'
env:
EXE: ${{ steps.what.outputs.exe }}
run: |
set -euo pipefail
cd "$(dirname "$EXE")"
"$EXE" version
"$EXE" set PORT 3456
# Fast polling, so a browser test sees a change made in the database
# within its timeout; the default is 30 s.
"$EXE" set METEOR_POLLING_INTERVAL_MS 2000
"$EXE" start --background
for i in $(seq 1 120); do
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3456/ || true)"
[ "$code" = 200 ] && break
sleep 3
done
"$EXE" status
if [ "$code" != 200 ]; then
echo "::error::$TARGET: WeKan did not answer HTTP 200 within 6 minutes."
tail -n 200 wekan-files/logs/wekan.log || true
exit 1
fi
ls wekan-files wekan-files/bin
echo "WeKan answered HTTP 200 from the $TARGET single file."
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.what.outputs.run == 'true' && matrix.target == 'amd64'
with:
persist-credentials: false
ref: ${{ needs.plan.outputs.tag }}
path: src
- name: Browser test against the single file
if: steps.what.outputs.run == 'true' && matrix.target == 'amd64'
env:
EXE: ${{ steps.what.outputs.exe }}
run: |
set -euo pipefail
files="$(dirname "$EXE")/wekan-files"
cd src/tests/playwright
npm ci
npx playwright install --with-deps chromium
WEKAN_BASE_URL=http://localhost:3456 \
WEKAN_MONGO_URL=mongodb://127.0.0.1:27017/wekan \
WEKAN_SINGLE_FILE_FILES="$files" \
WEKAN_FILES_PATH="$files" \
WEKAN_PLAYWRIGHT_PROJECT=chromium \
npx playwright test specs/single-file.e2e.js specs/01-boards-users.e2e.js specs/07-attachments-links.e2e.js --reporter=line
- name: Stop it
if: always() && steps.what.outputs.run == 'true'
env:
EXE: ${{ steps.what.outputs.exe }}
run: |
cd "$(dirname "$EXE")"
"$EXE" stop || true
"$EXE" status || true
[ -f wekan-files/logs/wekan.log ] && tail -n 60 wekan-files/logs/wekan.log || true
mkdir -p ok && echo "$TARGET started" > "ok/$TARGET"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: success() && (steps.what.outputs.run == 'true' || matrix.smoke == 'none')
with:
name: single-file-ok-${{ matrix.target }}
path: |
ok
sf/ok
if-no-files-found: ignore
retention-days: 3
publish:
needs: [plan, build, smoke]
if: ${{ !cancelled() && inputs.publish && needs.plan.outputs.any == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: single-file-*
path: artifacts
- name: Attach every single file that started, or has no runner to start on
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.plan.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
published=0
for dir in artifacts/single-file-*; do
target="${dir#artifacts/single-file-}"
case "$target" in ok-*) continue ;; esac
# Not built, and its build job said why in a notice already.
[ -f "$dir/single-file-skipped-$target.txt" ] && continue
if [ ! -d "artifacts/single-file-ok-$target" ]; then
echo "::warning::$target: not published - it did not start on its runner."
continue
fi
for f in "$dir"/wekan-*-single-file*; do
case "$f" in *.sha256sum) continue ;; esac
(cd "$dir" && sha256sum -c "$(basename "$f").sha256sum")
bash releases/github-release-upload.sh "$GITHUB_REPOSITORY" "$TAG" "$f" "$f.sha256sum"
published=$((published + 1))
done
done
echo "Published $published single files to $TAG."
+141
View File
@@ -0,0 +1,141 @@
name: UCS App Center
run-name: UCS ${{ inputs.version || 'latest' }}
on:
workflow_dispatch:
inputs:
version:
description: 'WeKan release version without v; leave empty for latest published release'
required: false
type: string
permissions:
contents: read
# This publisher is started separately with workflow_dispatch. Do not race
# creation of a portal version or overwrite an image built by another attempt.
concurrency:
group: wekan-ucs-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
VERSION: ${{ inputs.version }}
UCS_SOURCE_APP: ${{ vars.UCS_SOURCE_APP }}
UCS_TARGET_UCS: ${{ vars.UCS_TARGET_UCS }}
steps:
- name: Check UCS publishing configuration
id: config
env:
PROVIDER_USER: ${{ secrets.UNIVENTION_APP_PROVIDER_USER }}
PROVIDER_PASSWORD: ${{ secrets.UNIVENTION_APP_PROVIDER_PASSWORD }}
run: |
set -eu
if [ -z "$PROVIDER_USER" ] || [ -z "$PROVIDER_PASSWORD" ] || [ -z "$UCS_SOURCE_APP" ] || [ -z "$UCS_TARGET_UCS" ]; then
echo '::notice::UCS publishing not configured. See docs/Platforms/FOSS/Container/Docker/UCS/UCS-upgrades.md.'
echo 'enabled=false' >> "$GITHUB_OUTPUT"
else
echo 'enabled=true' >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
if: steps.config.outputs.enabled == 'true'
- name: Resolve published WeKan and FerretDB versions
if: steps.config.outputs.enabled == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/.tools/tmp"
echo "TMPDIR=$GITHUB_WORKSPACE/.tools/tmp" >> "$GITHUB_ENV"
if [ -n "$VERSION" ]; then
python3 -c 'import sys; sys.path.insert(0,"releases"); import importlib; importlib.import_module("ucs-release").version(sys.argv[1])' "$VERSION"
gh release view "v$VERSION" --repo wekan/wekan --json tagName,isDraft,isPrerelease > .tools/tmp/ucs-wekan-release.json
else
gh release view --repo wekan/wekan --json tagName,isDraft,isPrerelease > .tools/tmp/ucs-wekan-release.json
fi
gh release view --repo wekan/FerretDB --json tagName,isDraft,isPrerelease > .tools/tmp/ucs-ferretdb-release.json
python3 releases/ucs-release.py resolve \
--wekan-release .tools/tmp/ucs-wekan-release.json \
--ferretdb-release .tools/tmp/ucs-ferretdb-release.json
- name: Report selected versions
if: steps.config.outputs.enabled == 'true'
run: |
printf 'WeKan: ghcr.io/wekan/wekan:v%s\nFerretDB: %s\nUCS section: %s\n' \
"$VERSION" "$UCS_FERRETDB_IMAGE" "$UCS_TARGET_UCS" >> "$GITHUB_STEP_SUMMARY"
- name: Get the official Provider Portal CLI
if: steps.config.outputs.enabled == 'true'
run: |
bash releases/fetch.sh -o .tools/univention-appcenter-control \
https://provider-portal.software-univention.de/appcenter-selfservice/univention-appcenter-control
# The official CLI imports distutils.version (Python 3.12 removed it).
python3 -m pip install 'setuptools==80.9.0'
python3 .tools/univention-appcenter-control upload --help
- name: Check whether UCS already published this version
id: portal
if: steps.config.outputs.enabled == 'true'
env:
UNIVENTION_APP_PROVIDER_USER: ${{ secrets.UNIVENTION_APP_PROVIDER_USER }}
UNIVENTION_APP_PROVIDER_PASSWORD: ${{ secrets.UNIVENTION_APP_PROVIDER_PASSWORD }}
run: python3 releases/ucs-release.py plan --version "$VERSION"
- name: Checkout the app definition
if: steps.config.outputs.enabled == 'true' && steps.portal.outputs.skip != 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wekan/univention
ref: master
path: univention
persist-credentials: false
- name: Prepare versioned compose and migration hook
id: prepare
if: steps.config.outputs.enabled == 'true' && steps.portal.outputs.skip != 'true'
run: python3 releases/ucs-release.py prepare --version "$VERSION"
# The standard WeKan release already published the migration tools inside
# the app image on Docker Hub, GHCR and Quay. upload checks anonymous pulls
# with mirror fallback and runs start.sh --check before any portal write.
- name: Upload or finish the UCS portal draft
id: upload
if: steps.config.outputs.enabled == 'true' && steps.portal.outputs.skip != 'true'
env:
UNIVENTION_APP_PROVIDER_USER: ${{ secrets.UNIVENTION_APP_PROVIDER_USER }}
UNIVENTION_APP_PROVIDER_PASSWORD: ${{ secrets.UNIVENTION_APP_PROVIDER_PASSWORD }}
run: python3 releases/ucs-release.py upload --version "$VERSION"
- name: Keep the exact submitted app definition
if: steps.upload.outputs.result == 'uploaded'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ucs-${{ env.VERSION }}
path: .tools/ucs-release/component
- name: Report UCS result
if: always()
env:
ENABLED: ${{ steps.config.outputs.enabled }}
EXISTING: ${{ steps.portal.outputs.result }}
UPLOADED: ${{ steps.upload.outputs.result }}
RESULT: ${{ job.status }}
run: |
if [ "$ENABLED" != true ]; then
echo 'UCS: not configured; no upload attempted.' >> "$GITHUB_STEP_SUMMARY"
elif [ "$RESULT" != success ]; then
echo 'UCS: FAILED. See this job; portal publication was not verified.' >> "$GITHUB_STEP_SUMMARY"
elif [ "$EXISTING" = published ]; then
echo 'UCS: already published; left unchanged.' >> "$GITHUB_STEP_SUMMARY"
elif [ "$UPLOADED" = uploaded ]; then
echo 'UCS: draft uploaded and read back. UCS testing and provider/Univention approval remain.' >> "$GITHUB_STEP_SUMMARY"
else
echo 'UCS: no verified upload.' >> "$GITHUB_STEP_SUMMARY"
fi
+14 -6
View File
@@ -38,36 +38,44 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout newest WeKan content
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wekan/wekan
ref: ${{ inputs.source_ref }}
path: source
- name: Checkout wekan/${{ matrix.repository }}
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wekan/${{ matrix.repository }}
token: ${{ secrets.WEKAN_REPO_TOKEN }}
ref: main
path: target
fetch-depth: 0
# Shallow: one commit is added on top of main and pushed. The full
# history of these repositories is ~160 MB each and is not read.
- name: Copy WeKan and restore ${{ matrix.package }} package identities
env:
MATRIX_PACKAGE: ${{ matrix.package }}
MATRIX_TITLE: ${{ matrix.title }}
run: |
source/releases/prepare-variant-repo.sh \
source target "${{ matrix.package }}" "${{ matrix.title }}"
source target "${MATRIX_PACKAGE}" "${MATRIX_TITLE}"
- name: Commit and push wekan/${{ matrix.repository }}
working-directory: target
env:
MATRIX_REPOSITORY: ${{ matrix.repository }}
INPUT_SOURCE_REF: ${{ inputs.source_ref }}
MATRIX_PACKAGE: ${{ matrix.package }}
run: |
set -euo pipefail
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
if git diff --cached --quiet; then
echo "wekan/${{ matrix.repository }} already matches ${{ inputs.source_ref }}."
echo "wekan/${MATRIX_REPOSITORY} already matches ${INPUT_SOURCE_REF}."
exit 0
fi
git commit -m "Sync newest WeKan as ${{ matrix.package }}"
git commit -m "Sync newest WeKan as ${MATRIX_PACKAGE}"
git push origin HEAD:main
+49 -24
View File
@@ -46,8 +46,9 @@ on:
default: true
type: boolean
# Read-only by default; the job that writes is granted it below.
permissions:
contents: write
contents: read
concurrency:
group: wekan-windows-exe-${{ inputs.tag || github.ref }}
@@ -63,21 +64,30 @@ jobs:
version: ${{ steps.find.outputs.version }}
available: ${{ steps.find.outputs.available }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: find
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ inputs.tag }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
tag='${{ inputs.tag }}'
[ -n "$tag" ] || tag="$(gh release list --repo "${{ github.repository }}" \
tag="${INPUT_TAG}"
[ -n "$tag" ] || tag="$(gh release list --repo "${GITHUB_CTX_REPOSITORY}" \
--limit 1 --json tagName --jq '.[0].tagName')"
[ -n "$tag" ] || { echo "::error::No release to wrap."; exit 1; }
version="${tag#v}"
available=false
if gh release view "$tag" --repo "${{ github.repository }}" --json assets \
--jq '.assets[].name' | grep -qx "wekan-${version}-win64.zip"; then
# The list is read whole BEFORE matching. `gh … | grep -q` lets grep exit
# at the first match while gh is still writing; gh gets SIGPIPE, pipefail
# makes the pipeline fail, and a PUBLISHED asset was reported missing - the
# v12.39 Mac apps were skipped that way, with the bundles already attached.
assets="$(gh release view "$tag" --repo "${GITHUB_CTX_REPOSITORY}" --json assets \
--jq '.assets[].name')"
if grep -qx "wekan-${version}-win64.zip" <<< "$assets"; then
available=true
else
echo "::notice::wekan-${version}-win64.zip is not published; there is no Windows bundle to wrap."
@@ -87,28 +97,35 @@ jobs:
echo "available=$available" >> "$GITHUB_OUTPUT"
build:
permissions:
contents: write
name: self-extracting win64 EXE
needs: release
if: ${{ needs.release.outputs.available == 'true' }}
runs-on: windows-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Fetch and verify the published win64 bundle
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
$ErrorActionPreference = 'Stop'
$zip = 'wekan-${{ needs.release.outputs.version }}-win64.zip'
$assets = gh release view --repo '${{ github.repository }}' `
'${{ needs.release.outputs.tag }}' --json assets --jq '.assets[].name'
$zip = "wekan-$($env:NEEDS_RELEASE_VERSION)-win64.zip"
$assets = gh release view --repo $env:GITHUB_CTX_REPOSITORY `
$env:NEEDS_RELEASE_TAG --json assets --jq '.assets[].name'
if ($assets -notcontains $zip) {
throw "$zip is not on ${{ needs.release.outputs.tag }}. The single EXE can only wrap a published win64 bundle."
throw "$zip is not on $($env:NEEDS_RELEASE_TAG). The single EXE can only wrap a published win64 bundle."
}
gh release download '${{ needs.release.outputs.tag }}' `
--repo '${{ github.repository }}' --pattern "$zip*" --clobber
gh release download $env:NEEDS_RELEASE_TAG `
--repo $env:GITHUB_CTX_REPOSITORY --pattern "$zip*" --clobber
# The ZIP is the payload, so it is checked as a whole against the
# checksum published beside it rather than unpacked here.
if (Test-Path "$zip.sha256sum" -PathType Leaf) {
@@ -140,24 +157,26 @@ jobs:
python releases/check-telemetry.py --archive $zip
if ($LASTEXITCODE -ne 0) { throw 'Telemetry audit rejected the Windows bundle.' }
- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
- name: Use npm 12.0.2
run: npm install --global npm@12.0.2
- name: Use release npm version
run: bash releases/use-release-npm.sh
- name: Work out which members cannot be virtual
shell: pwsh
# The launcher unpacks these and nothing else. The list is computed from
# the archive, not written by hand, so a bundle that gains an addon or a
# database tool does not silently lose it at runtime.
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
run: |
$ErrorActionPreference = 'Stop'
node releases/append-windows-payload.mjs `
--manifest "wekan-${{ needs.release.outputs.version }}-win64.zip" `
--manifest "wekan-$($env:NEEDS_RELEASE_VERSION)-win64.zip" `
--header wekan-real-files.h
- uses: ilammy/msvc-dev-cmd@v1
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
- name: Compile the native launcher
shell: cmd
@@ -169,9 +188,11 @@ jobs:
- name: Append the bundle and its trailer to the launcher
shell: pwsh
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
run: |
$ErrorActionPreference = 'Stop'
$version = '${{ needs.release.outputs.version }}'
$version = $env:NEEDS_RELEASE_VERSION
New-Item -ItemType Directory -Force dist | Out-Null
node releases/append-windows-payload.mjs `
--launcher wekan-launcher.exe `
@@ -235,9 +256,11 @@ jobs:
# loop still answers on port 8080 eventually and a poll-until-ready
# check calls it healthy. This one fails on the restart line and on
# a SyntaxError, so a bundle Node.js cannot load can no longer ship.
env:
NEEDS_RELEASE_VERSION: ${{ needs.release.outputs.version }}
run: |
$ErrorActionPreference = 'Stop'
$version = '${{ needs.release.outputs.version }}'
$version = $env:NEEDS_RELEASE_VERSION
# Run from its own directory: the EXE unpacks ~700 MB beside itself
# and dist/ is what gets uploaded as the release artifact.
New-Item -ItemType Directory -Force smoke | Out-Null
@@ -319,7 +342,7 @@ jobs:
Start-Once 'second' 120
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: windows-single-exe-win64
path: dist/
@@ -336,6 +359,8 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GITHUB_CTX_REPOSITORY: ${{ github.repository }}
NEEDS_RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
@@ -345,6 +370,6 @@ jobs:
exit 1
fi
# Retried, bounded, and verified by name and size from the release side.
bash releases/github-release-upload.sh '${{ github.repository }}' '${{ needs.release.outputs.tag }}' "${files[@]}"
gh release view --repo '${{ github.repository }}' \
'${{ needs.release.outputs.tag }}' --json url --jq .url
bash releases/github-release-upload.sh "${GITHUB_CTX_REPOSITORY}" "${NEEDS_RELEASE_TAG}" "${files[@]}"
gh release view --repo "${GITHUB_CTX_REPOSITORY}" \
"${NEEDS_RELEASE_TAG}" --json url --jq .url
+27 -27
View File
@@ -6,12 +6,12 @@
meteor-base@1.5.2
# Build system
ecmascript@0.19.2-rc360.0
standard-minifier-js@3.3.0-rc360.0
rspack@1.4.0-rc360.0
ecmascript@0.19.2-rc360.1
standard-minifier-js@3.3.0-rc360.1
rspack@1.4.0-rc360.1
# Declared explicitly because server/modernBrowsers.js imports it, to say that
# Yandex Browser is modern and must not be served the legacy bundle (#6557).
modern-browsers@0.2.4-rc360.0
modern-browsers@0.2.4-rc360.1
blaze@3.0.2
@@ -19,11 +19,11 @@ blaze@3.0.2
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
mongo@2.5.2-rc360.0
mongo@2.5.2-rc360.1
# Account system
accounts-password@3.3.2-rc360.0
accounts-2fa@3.1.1-rc360.0
accounts-password@3.3.2-rc360.1
accounts-2fa@3.1.1-rc360.1
accounts-express@1.0.0
communitypackages:core
communitypackages:unstyled
@@ -39,19 +39,19 @@ wekan-oidc
wekan-accounts-oidc
# Utilities
check@1.5.1-rc360.0
random@1.2.3-rc360.0
reactive-dict@1.3.3-rc360.0
session@1.2.4-rc360.0
tracker@1.3.5-rc360.0
check@1.5.1-rc360.1
random@1.2.3-rc360.1
reactive-dict@1.3.3-rc360.1
session@1.2.4-rc360.1
tracker@1.3.5-rc360.1
audit-argument-checks@1.0.8
# UI components
ostrio:i18n
reactive-var@1.0.13
shell-server@0.7.1-rc360.0
email@3.2.1-rc360.0
dynamic-import@0.7.5-rc360.0
shell-server@0.7.2-rc360.1
email@3.2.1-rc360.1
dynamic-import@0.8.1-rc360.1
wekan-markdown
quave:synced-cron
ostrio:cookies
@@ -59,21 +59,21 @@ ostrio:files
# lmieulet:meteor-coverage
meteortesting:mocha@3.0.0
matb33:collection-hooks
service-configuration@1.3.6-rc360.0
service-configuration@1.3.6-rc360.1
minifier-css@2.0.2
ejson@1.2.1-rc360.0
logging@1.3.7-rc360.0
ejson@1.4.1-rc360.1
logging@1.3.7-rc360.1
wekan-fullcalendar
wekan-fontawesome
accounts-google@1.4.2-rc360.0
accounts-github@1.5.2-rc360.0
accounts-facebook@1.3.5-rc360.0
accounts-twitter@1.5.3-rc360.0
accounts-meteor-developer@1.5.2-rc360.0
accounts-weibo@1.4.2-rc360.0
accounts-meetup@1.5.2-rc360.0
accounts-passwordless@3.1.2-rc360.0
instrumentation@0.0.1-rc360.0
accounts-google@1.4.2-rc360.1
accounts-github@1.5.2-rc360.1
accounts-facebook@1.3.5-rc360.1
accounts-twitter@1.5.3-rc360.1
accounts-meteor-developer@1.5.2-rc360.1
accounts-weibo@1.4.2-rc360.1
accounts-meetup@1.5.2-rc360.1
accounts-passwordless@3.1.2-rc360.1
instrumentation@0.0.1-rc360.1
# Prevent Meteor build/run tools from sending application package statistics.
package-stats-opt-out@1.0.8
+1 -1
View File
@@ -1 +1 @@
METEOR@3.6-rc.0
METEOR@3.6-rc.1
+74 -74
View File
@@ -1,130 +1,130 @@
accounts-2fa@3.1.1-rc360.0
accounts-base@3.4.0-rc360.0
accounts-2fa@3.1.1-rc360.1
accounts-base@3.4.0-rc360.1
accounts-express@1.0.0
accounts-facebook@1.3.5-rc360.0
accounts-github@1.5.2-rc360.0
accounts-google@1.4.2-rc360.0
accounts-meetup@1.5.2-rc360.0
accounts-meteor-developer@1.5.2-rc360.0
accounts-oauth@1.4.8-rc360.0
accounts-password@3.3.2-rc360.0
accounts-passwordless@3.1.2-rc360.0
accounts-twitter@1.5.3-rc360.0
accounts-weibo@1.4.2-rc360.0
accounts-facebook@1.3.5-rc360.1
accounts-github@1.5.2-rc360.1
accounts-google@1.4.2-rc360.1
accounts-meetup@1.5.2-rc360.1
accounts-meteor-developer@1.5.2-rc360.1
accounts-oauth@1.4.8-rc360.1
accounts-password@3.3.2-rc360.1
accounts-passwordless@3.1.2-rc360.1
accounts-twitter@1.5.3-rc360.1
accounts-weibo@1.4.2-rc360.1
aldeed:collection2@4.2.2
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.1-rc360.0
allow-deny@2.1.2-rc360.1_1
audit-argument-checks@1.0.8
autoupdate@2.0.2-rc360.0
babel-compiler@7.15.2-rc360.0
autoupdate@2.0.2-rc360.1
babel-compiler@7.15.2-rc360.1
babel-runtime@1.5.2
base64@1.0.15-rc360.0
binary-heap@1.0.14-rc360.0
base64@1.0.15-rc360.1
binary-heap@1.0.14-rc360.1
blaze@3.0.3
blaze-tools@2.0.1
boilerplate-generator@2.2.0-rc360.0
boilerplate-generator@2.2.0-rc360.1
caching-compiler@2.0.1
caching-html-compiler@2.0.1
callback-hook@1.8.1-rc360.0
check@1.5.1-rc360.0
callback-hook@1.8.1-rc360.1
check@1.5.1-rc360.1
communitypackages:core@2.0.3
communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.3-rc360.0
ddp-client@3.4.2-rc360.0
ddp-common@1.4.6-rc360.0
ddp-rate-limiter@1.3.1-rc360.0
ddp-server@3.4.1-rc360.0
diff-sequence@1.1.4-rc360.0
dynamic-import@0.7.5-rc360.0
ecmascript@0.19.2-rc360.0
ddp@1.4.3-rc360.1
ddp-client@3.4.2-rc360.1
ddp-common@1.4.6-rc360.1
ddp-rate-limiter@1.3.1-rc360.1
ddp-server@3.6.0-rc360.1_1
diff-sequence@1.2.1-rc360.1
dynamic-import@0.8.1-rc360.1_1
ecmascript@0.19.2-rc360.1
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.13.0
ecmascript-runtime-server@0.11.1
ejson@1.2.1-rc360.0
email@3.2.1-rc360.0
ejson@1.4.1-rc360.1
email@3.2.1-rc360.1
es5-shim@4.8.1
facebook-oauth@1.11.7-rc360.0
facts-base@1.0.3-rc360.0
fetch@0.2.1-rc360.0
geojson-utils@1.0.13-rc360.0
github-oauth@1.4.3-rc360.0
google-oauth@1.4.6-rc360.0
facebook-oauth@1.12.1-rc360.1_1
facts-base@1.0.3-rc360.1
fetch@0.2.1-rc360.1
geojson-utils@1.0.13-rc360.1
github-oauth@1.4.3-rc360.1
google-oauth@1.5.1-rc360.1_1
hot-code-push@1.0.5
html-tools@2.0.1
htmljs@2.0.2
id-map@1.2.1-rc360.0
instrumentation@0.0.1-rc360.0
inter-process-messaging@0.1.4-rc360.0
id-map@1.2.1-rc360.1
instrumentation@0.0.1-rc360.1
inter-process-messaging@0.1.4-rc360.1
jquery@3.0.2
lai:collection-extensions@1.0.0
localstorage@1.2.1
logging@1.3.7-rc360.0
logging@1.3.7-rc360.1
matb33:collection-hooks@2.1.0
meetup-oauth@1.1.4-rc360.0
meteor@2.3.2-rc360.0
meetup-oauth@1.1.4-rc360.1
meteor@2.3.2-rc360.1
meteor-base@1.5.2
meteor-developer-oauth@1.3.4-rc360.0
meteor-developer-oauth@1.3.4-rc360.1
meteortesting:browser-tests@1.8.0
meteortesting:mocha@3.4.0
meteortesting:mocha-core@8.2.0
minifier-css@2.0.2
minifier-js@3.3.2-rc360.0
minimongo@2.2.1-rc360.0
modern-browsers@0.2.4-rc360.0
minifier-js@3.3.2-rc360.1
minimongo@2.2.1-rc360.1
modern-browsers@0.2.4-rc360.1
modules@0.20.3
modules-runtime@0.13.2
mongo@2.5.2-rc360.0
mongo@2.5.2-rc360.1
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.10-rc360.0
npm-mongo@6.16.3
oauth@3.0.5-rc360.0
oauth1@1.5.4-rc360.0
mongo-id@1.0.10-rc360.1
npm-mongo@6.18.1-rc360.1
oauth@3.0.5-rc360.1
oauth1@1.5.4-rc360.1
oauth2@1.3.3
observe-sequence@2.0.1
ordered-dict@1.2.1-rc360.0
ordered-dict@1.2.1-rc360.1
ostrio:cookies@2.9.2
ostrio:cstorage@4.0.1
ostrio:files@3.0.1
ostrio:flow-router-extra@3.15.3
ostrio:i18n@3.2.1
package-stats-opt-out@1.0.8
promise@1.0.1-rc360.0
promise@1.0.1-rc360.1
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.3-rc360.0
rate-limit@1.2.1-rc360.0
random@1.2.3-rc360.1
rate-limit@1.2.1-rc360.1
react-fast-refresh@0.3.0
reactive-dict@1.3.3-rc360.0
reactive-dict@1.3.3-rc360.1
reactive-var@1.0.13
reload@1.3.3-rc360.0
retry@1.1.2-rc360.0
reload@1.3.3-rc360.1
retry@1.1.2-rc360.1
reywood:publish-composite@1.9.0
routepolicy@1.1.3-rc360.0
rspack@1.4.0-rc360.0
service-configuration@1.3.6-rc360.0
session@1.2.4-rc360.0
sha@1.0.11-rc360.0
shell-server@0.7.1-rc360.0
socket-stream-client@0.7.2-rc360.0
routepolicy@1.1.3-rc360.1
rspack@1.4.0-rc360.1
service-configuration@1.3.6-rc360.1
session@1.2.4-rc360.1
sha@1.0.11-rc360.1
shell-server@0.7.2-rc360.1_1
socket-stream-client@0.7.2-rc360.1
spacebars@2.0.1
spacebars-compiler@2.0.1
standard-minifier-js@3.3.0-rc360.0
standard-minifier-js@3.3.0-rc360.1
templating@1.4.5
templating-compiler@2.0.1
templating-runtime@2.0.2
templating-tools@2.0.1
tools-core@1.4.0-rc360.0
tracker@1.3.5-rc360.0
twitter-oauth@1.3.5-rc360.0
typescript@5.11.2-rc360.0
url@1.3.7-rc360.0
webapp@2.3.1-rc360.0
webapp-hashing@1.1.3-rc360.0
weibo-oauth@1.3.4-rc360.0
tools-core@1.4.0-rc360.1
tracker@1.3.5-rc360.1
twitter-oauth@1.3.5-rc360.1
typescript@5.11.2-rc360.1
url@1.3.7-rc360.1
webapp@2.3.1-rc360.1
webapp-hashing@1.1.3-rc360.1
weibo-oauth@1.3.4-rc360.1
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
wekan-accounts-oidc@1.0.10
+1 -1
View File
@@ -8,7 +8,7 @@ npm-packages/
# would hide those suites, making `meteor test` report "0 passing".
/tests/
# Build/tool scratch space: a full Node 24 install (headers for EVERY openssl arch),
# Build/tool scratch space: a full Node 26 install (headers for EVERY openssl arch),
# a Go toolchain with its module/build caches, build logs and repro dirs. 20,535
# DIRECTORIES, none of it app code. Meteor's watcher takes one inotify watch per
# directory, and the per-user limit (fs.inotify.max_user_watches, 65536 here) is
+77 -14
View File
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
The maintainer's half is the release flow, not taste. Releases here are frequent —
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
released section links a commit that release does not contain.
@@ -340,7 +340,7 @@ directly after the merge.
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
- Directory structure:
- `wekan` — this repo (https://github.com/wekan/wekan); see
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
contributor works on a branch and opens a pull request — see "Where this
@@ -373,7 +373,7 @@ directly after the merge.
| --- | --- | --- | --- |
| `.tools/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `<tool>-<arch>` |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/sandstorm` | sandstorm-io/sandstorm | — | upstream Sandstorm, when present — somebody else's project, cloned for reference |
@@ -414,7 +414,7 @@ directly after the merge.
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
@@ -426,6 +426,52 @@ directly after the merge.
out and would break the tooling that parses them. When this file and the file being
edited disagree, **the file being edited wins** — and everything below in this
section is about the WeKan format specifically.
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
full, in the `<details>` format below - every rule in the rest of this section
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
and never edited by hand:
| | `CHANGELOG.md` |
| --- | --- |
| Status, TODO Later | none |
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
It exists because the big file is for whoever writes and audits a release,
and most readers - GitHub's front page, the Sandstorm App Market
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
what changed lately, at a glance. `releases/release-all.sh` regenerates it
right after naming the new release, so it is in the same release commit, and
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
when it is not exactly what the generator writes. So: write
`CHANGELOG-big.md`, run the generator, commit both - and never edit
`CHANGELOG.md` to fix something, because the next run overwrites it.
- **"Update wekan changelog" means both files, in this order:**
1. Find what is not in the changelog yet: `git log` since the newest commit
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
release's `Prepare v… release` commit) already covers, by anybody - a
merged pull request and the maintainer's own commits alike.
2. Check whether a release was cut since the last update (`release-all.sh`
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
`# Upcoming WeKan ® release` heading, add it from
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
above the newest release - never add an entry to a released section.
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
format described below (subsections, topic groups, `<details>` entries
linking each commit), and update its **In short** paragraph so it still
names the release's main outcomes.
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
Upcoming **In short** first and the five newest releases below it, with the
longer details only in `CHANGELOG-big.md`.
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
`tests/changelogFormat.test.cjs`,
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
`node tests/run-node-suites.cjs changelog`) - and commit
`CHANGELOG-big.md` and `CHANGELOG.md` together.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
@@ -438,10 +484,10 @@ directly after the merge.
add and syntax-check/register the test and state clearly what could not be run.
Do not duplicate an existing test merely to satisfy the audit — extend it when
that keeps one behavior in one suite.
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
Markdown up to 500 KiB; above that it shows only plain source text, and above
about 2 MB nothing at all ("we can't show files that are this big right
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
and keeping only the current month still came to 2.1 MB, because releases
here are frequent and large. So the cut is by SIZE: 450,000 bytes
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
@@ -449,7 +495,7 @@ directly after the merge.
| | |
| --- | --- |
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
@@ -464,11 +510,11 @@ directly after the merge.
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
to open is worth more.
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
after a release is the natural moment. It is idempotent — a run with nothing
to move only refreshes the tables and parts — and it decides from the FILE
rather than the clock, so two people running it agree.
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
An archived section is never edited, for the same reason a released one is
not.
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
@@ -482,7 +528,9 @@ directly after the merge.
whose `<summary>` is `Newest WeKan at these platforms`, holding the line
`Newest WeKan at these platforms:` and the Install / Upgrade / Docs / Mac
ChangeLog bullets, plus the `Older releases:` bullet linking the per-year
archives. Then a `<details>` whose `<summary>` is `Version` holding
archives. Then a `<details>` whose `<summary>` is `Databases` listing
which databases WeKan supports and how each is tested, kept in step with
`docs/Databases/Supported.md`. Then a `<details>` whose `<summary>` is `Version` holding
"which WeKan version uses what". Then a `<details>` whose `<summary>` is
`TODO Later`, itself holding a `<details>` whose `<summary>` is `Carried
to a future release.` explaining the list, then one `<details>` per
@@ -562,7 +610,7 @@ directly after the merge.
`<details>` blocks below carry that information. Keep the paragraph current as
topics change, and shorten it when added commits make it grow. A finished release
keeps the paragraph it was written with.
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
- **Neither changelog nor the release notes may contain a binaries provenance
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
Do not add "Binaries in these bundles" at the top or bottom of either.
Release notes summarize each Translations group as only a list of updated
@@ -575,7 +623,7 @@ directly after the merge.
no security fixes - is left out rather than added empty.
Release notes have only In short, Security, Translations (language list),
the standard thanks line and More details at ChangeLog with the release
anchor link. Other details stay in CHANGELOG.md. Keep download
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
verification and `provenance.tsv` build artifacts for diagnostics; do not
append their contents or `releases/provenance-table.sh` output to notes.
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
@@ -634,7 +682,7 @@ directly after the merge.
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
subsection becomes `and …` per the rule above.
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
`CHANGELOG.md` (above the version sections), a triage backlog for open
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
issues that were **investigated but not fixed here**, each
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
@@ -715,7 +763,7 @@ have cost a released section its accuracy:
- **Work continues immediately after a release**, so an entry written right
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
release` to `# v<NEW> …` and stops there — it does **not** create a new,
empty `# Upcoming` section. CHANGELOG.md never carries an
empty `# Upcoming` section. CHANGELOG-big.md never carries an
`**In short:** nothing here yet.` placeholder sitting between releases; it
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
meant the file always had a section saying nothing, from the moment a
@@ -867,6 +915,21 @@ have cost a released section its accuracy:
sizes them at 24px, and keeps the icon and its red drop on one line at every width
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
Hall of Fame, it checks each 3-star row whose Process says the report came
without a GHSA against [WeKan Security
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
that report now exists, it sets the row to 5 stars on the index AND on the row's
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
rewrites the Process cell to say the report later arrived as a GHSA, and records
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
collapsed.** Those two columns carry the prose — how the report arrived, and the
whole story of the vulnerability — so the page would be one wall of text and the
+8121
View File
File diff suppressed because it is too large. Load diff
+43 -7971
View File
File diff suppressed because it is too large. Load diff
+77 -14
View File
@@ -35,7 +35,7 @@ contributor — that offer is the mistake, not the rule.
The maintainer's half is the release flow, not taste. Releases here are frequent —
several a day when a fault is being chased — and `releases/release-all.sh` cuts one
from whatever is on `main`, taking the version from the `# Upcoming WeKan ® release`
section of `CHANGELOG.md`. Work parked on a branch is work that is not in the next
section of `CHANGELOG-big.md`. Work parked on a branch is work that is not in the next
release, and `tests/changelogEntriesBelongToTheirRelease.test.cjs` fails when a
released section links a commit that release does not contain.
@@ -340,7 +340,7 @@ directly after the merge.
the two patch repositories under `.tools/` — node-patches and mongo-tools-patches.
- Directory structure:
- `wekan` — this repo (https://github.com/wekan/wekan); see
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG.md` at root. It is at
`docs/DeveloperDocs/Directory-Structure.md`; `CHANGELOG-big.md` and `CHANGELOG.md` at root. It is at
`~/repos/wekan` on Linux, `~/Documents/repos/wekan` on macOS and
`Downloads\repos\wekan` on Windows. The maintainer commits on `main`; a
contributor works on a branch and opens a pull request — see "Where this
@@ -373,7 +373,7 @@ directly after the merge.
| --- | --- | --- | --- |
| `.tools/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `<tool>-<arch>` |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/sandstorm` | sandstorm-io/sandstorm | — | upstream Sandstorm, when present — somebody else's project, cloned for reference |
@@ -414,7 +414,7 @@ directly after the merge.
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/wekan` | `CHANGELOG-big.md` (and the generated `CHANGELOG.md`, below) | the WeKan format this section describes: `# Status` (Platforms/Version/TODO Later nested as `<details>`), then `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` sections of `<details>` entries whose `<summary>` links the commit |
| `wekan/node-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming node-patches release` |
| `wekan/mongo-tools-patches` | `CHANGELOG.md` | the same WeKan format, with `# Upcoming mongo-tools-patches release` |
| `wekan/FerretDB` | `CHANGELOG.md` | **upstream FerretDB's** format, not WeKan's: `## [v1.48.0](tag URL) (YYYY-MM-DD)` and `### New Features 🎉` / `### Fixed 🐛` / `### Other Changes 🤖` bullets ending `by @xet7. Thanks to xet7.` |
@@ -426,6 +426,52 @@ directly after the merge.
out and would break the tooling that parses them. When this file and the file being
edited disagree, **the file being edited wins** — and everything below in this
section is about the WeKan format specifically.
- **WeKan has TWO changelogs, and only one is written.** `CHANGELOG-big.md` is
the changelog: `# Status`, `# Upcoming WeKan ® release` and every release in
full, in the `<details>` format below - every rule in the rest of this section
is about it, and every entry, fix and release goes into it. `CHANGELOG.md` is
the SHORT changelog, generated from it by `node releases/changelog-short.mjs`
and never edited by hand:
| | `CHANGELOG.md` |
| --- | --- |
| Status, TODO Later | none |
| First | the unreleased `# Upcoming WeKan ® release`, when there is one: its **In short** paragraph only, then `[All changes so far](CHANGELOG-big.md#upcoming-wekan--release)` |
| Below it | the five newest `# v<MAJOR>.<MINOR> YYYY-MM-DD WeKan ® release` |
| Per release | its **In short** paragraph only, then `[All changes in v…](CHANGELOG-big.md#anchor)` |
| Markup | plain Markdown: no `<details>`, `<summary>`, `</summary>` or `</details>`, and every HTML link a Markdown link |
It exists because the big file is for whoever writes and audits a release,
and most readers - GitHub's front page, the Sandstorm App Market
(`sandstorm-pkgdef.capnp` embeds it), the Helm chart's changelog link - want
what changed lately, at a glance. `releases/release-all.sh` regenerates it
right after naming the new release, so it is in the same release commit, and
`releases/changelog.sh` does the same; `tests/changelogShort.test.cjs` fails
when it is not exactly what the generator writes. So: write
`CHANGELOG-big.md`, run the generator, commit both - and never edit
`CHANGELOG.md` to fix something, because the next run overwrites it.
- **"Update wekan changelog" means both files, in this order:**
1. Find what is not in the changelog yet: `git log` since the newest commit
`CHANGELOG-big.md`'s Upcoming section (or, when it has none, the newest
release's `Prepare v… release` commit) already covers, by anybody - a
merged pull request and the maintainer's own commits alike.
2. Check whether a release was cut since the last update (`release-all.sh`
renames Upcoming to `# v<NEW> …` and opens no new one). If there is no
`# Upcoming WeKan ® release` heading, add it from
[docs/DeveloperDocs/Changelog-Upcoming-Template.md](docs/DeveloperDocs/Changelog-Upcoming-Template.md)
above the newest release - never add an entry to a released section.
3. Write the entries into that Upcoming section of `CHANGELOG-big.md`, in the
format described below (subsections, topic groups, `<details>` entries
linking each commit), and update its **In short** paragraph so it still
names the release's main outcomes.
4. Run `node releases/changelog-short.mjs`: `CHANGELOG.md` then shows that
Upcoming **In short** first and the five newest releases below it, with the
longer details only in `CHANGELOG-big.md`.
5. Run the changelog tests - `tests/changelogShort.test.cjs`,
`tests/changelogFormat.test.cjs`,
`tests/changelogEntriesBelongToTheirRelease.test.cjs` (or
`node tests/run-node-suites.cjs changelog`) - and commit
`CHANGELOG-big.md` and `CHANGELOG.md` together.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
@@ -438,10 +484,10 @@ directly after the merge.
add and syntax-check/register the test and state clearly what could not be run.
Do not duplicate an existing test merely to satisfy the audit — extend it when
that keeps one behavior in one suite.
- **CHANGELOG.md stays small enough for GitHub to show it.** GitHub renders
- **CHANGELOG-big.md stays small enough for GitHub to show it.** GitHub renders
Markdown up to 500 KiB; above that it shows only plain source text, and above
about 2 MB nothing at all ("we can't show files that are this big right
now"). CHANGELOG.md reached 2.6 MB over 1,100 releases back to 2015 (#6580),
now"). The changelog reached 2.6 MB over 1,100 releases back to 2015 (#6580),
and keeping only the current month still came to 2.1 MB, because releases
here are frequent and large. So the cut is by SIZE: 450,000 bytes
(`BUDGET` in `releases/changelog-archive.mjs`), under GitHub's limit with
@@ -449,7 +495,7 @@ directly after the merge.
| | |
| --- | --- |
| `CHANGELOG.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `CHANGELOG-big.md` | `# Status` (Platforms/TODO Later inside it), `# Upcoming`, and the newest releases that fit the budget |
| `old-CHANGELOG/<year>/<MM>.md` | a month's releases, newest first; a month over the budget continues in `<MM>-part2.md`, `<MM>-part3.md`, ... |
| `old-CHANGELOG/<year>.md` | 2015–2025, whole |
@@ -464,11 +510,11 @@ directly after the merge.
still in git (`gitk`, `git-gui`, `git log --follow`), and being small enough
to open is worth more.
Run `node releases/changelog-archive.mjs` whenever CHANGELOG.md has grown -
Run `node releases/changelog-archive.mjs` whenever CHANGELOG-big.md has grown -
after a release is the natural moment. It is idempotent — a run with nothing
to move only refreshes the tables and parts — and it decides from the FILE
rather than the clock, so two people running it agree.
`tests/changelogArchive.test.cjs` fails when CHANGELOG.md is over the budget.
`tests/changelogArchive.test.cjs` fails when CHANGELOG-big.md is over the budget.
An archived section is never edited, for the same reason a released one is
not.
- **The file's shape, top to bottom** — keep it exactly as it is now. There is
@@ -482,7 +528,9 @@ directly after the merge.
whose `<summary>` is `Newest WeKan at these platforms`, holding the line
`Newest WeKan at these platforms:` and the Install / Upgrade / Docs / Mac
ChangeLog bullets, plus the `Older releases:` bullet linking the per-year
archives. Then a `<details>` whose `<summary>` is `Version` holding
archives. Then a `<details>` whose `<summary>` is `Databases` listing
which databases WeKan supports and how each is tested, kept in step with
`docs/Databases/Supported.md`. Then a `<details>` whose `<summary>` is `Version` holding
"which WeKan version uses what". Then a `<details>` whose `<summary>` is
`TODO Later`, itself holding a `<details>` whose `<summary>` is `Carried
to a future release.` explaining the list, then one `<details>` per
@@ -562,7 +610,7 @@ directly after the merge.
`<details>` blocks below carry that information. Keep the paragraph current as
topics change, and shorten it when added commits make it grow. A finished release
keeps the paragraph it was written with.
- **Neither CHANGELOG.md nor release notes may contain a binaries provenance
- **Neither changelog nor the release notes may contain a binaries provenance
section or a Platform/Bundle/Binary/From/Version/Checked/SHA256 table.**
Do not add "Binaries in these bundles" at the top or bottom of either.
Release notes summarize each Translations group as only a list of updated
@@ -575,7 +623,7 @@ directly after the merge.
no security fixes - is left out rather than added empty.
Release notes have only In short, Security, Translations (language list),
the standard thanks line and More details at ChangeLog with the release
anchor link. Other details stay in CHANGELOG.md. Keep download
anchor link into CHANGELOG-big.md. Other details stay in CHANGELOG-big.md. Keep download
verification and `provenance.tsv` build artifacts for diagnostics; do not
append their contents or `releases/provenance-table.sh` output to notes.
- **Inside a subsection, entries are GROUPED BY TOPIC/AREA.** A release touches a handful
@@ -634,7 +682,7 @@ directly after the merge.
Because CRITICAL comes first, it keeps the `This release ` prefix; a following non-security
subsection becomes `and …` per the rule above.
- **TODO Later** — a `<details>` nested inside `# Status` near the TOP of
`CHANGELOG.md` (above the version sections), a triage backlog for open
`CHANGELOG-big.md` (above the version sections), a triage backlog for open
issues that were **investigated but not fixed here**, each
recorded with a concrete REASON so whoever picks it up next knows why. Use it when working
through open issues (the "Fix open issues" process): for each issue, either **fix it** (commit
@@ -715,7 +763,7 @@ have cost a released section its accuracy:
- **Work continues immediately after a release**, so an entry written right
after one has to go somewhere. `release-all.sh` renames `# Upcoming WeKan ®
release` to `# v<NEW> …` and stops there — it does **not** create a new,
empty `# Upcoming` section. CHANGELOG.md never carries an
empty `# Upcoming` section. CHANGELOG-big.md never carries an
`**In short:** nothing here yet.` placeholder sitting between releases; it
used to (via a now-deleted `releases/changelog-open-next.mjs`), and that
meant the file always had a section saying nothing, from the moment a
@@ -867,6 +915,21 @@ have cost a released section its accuracy:
sizes them at 24px, and keeps the icon and its red drop on one line at every width
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
process, and the row's 3 stars become 5. So whenever an AI updates or audits the
Hall of Fame, it checks each 3-star row whose Process says the report came
without a GHSA against [WeKan Security
Advisories](https://github.com/wekan/wekan/security/advisories): when a GHSA for
that report now exists, it sets the row to 5 stars on the index AND on the row's
own page, links the GHSA (and the CVE in the CVE cell once one is assigned),
rewrites the Process cell to say the report later arrived as a GHSA, and records
the change in Audit-Stars.md. Match by the report's subject and reporter, not by
a nearby advisory - a GHSA about something else is no upgrade. Until the GHSA
exists, the row stays at 3 and its Process cell says the GHSA is awaited.
- **On the Hall of Fame index page, the Process and Vulnerabilities cells are
collapsed.** Those two columns carry the prose — how the report arrived, and the
whole story of the vulnerability — so the page would be one wall of text and the
+1 -1
View File
@@ -13,7 +13,7 @@ For all code at WeKan GitHub Organization https://github.com/wekan
- All known vulnerabilities have been fixed at newest WeKan, please upgrade https://wekan.fi/upgrade/
- There is no known backdoors. If WeKan is installed by someone else, first registered user is Admin and has Admin Panel access.
- Maintainer of WeKan has no access to someone elses installed WeKan.
- For someone elses installed WeKan, see [Forgot Password](https://github.com/wekan/wekan/blob/main/docs/Features/Login/Forgot-Password.md)
- For someone elses installed WeKan, see [Forgot Password](https://github.com/wekan/wekan/blob/main/docs/Features/Login-Page/Forgot-Password.md)
# Reporting about violations or something else
+173 -202
View File
@@ -11,27 +11,147 @@
# glibc cannot run the hard-float node-armv6 in the armv6 bundle. The docker job
# therefore asks this base what it publishes and drops a platform it lacks; see
# docs/Platforms/FOSS/Container/Docker/CPU-platforms.md.
# ── Stage 1: the release bundle, prepared ON THE BUILD MACHINE ───────────────
#
# Everything a release bundle needs is already done when this runs: each
# wekan-<version>-<arch>.zip was built by its own release-all.yml leg, with
# programs/server installed, its native modules built for that CPU, the
# security bumps applied, the tree trimmed and pruned, the telemetry check
# passed, and the Node.js for that CPU embedded as bundle/node - resolved by
# releases/resolve-node-source.sh and verified against the SHA256 its source
# published (embed-verified-node.sh / install-node-for-arch.sh).
#
# This image used to do all of it AGAIN, inside the target platform: apt-get
# a compiler toolchain, resolve and download Node.js, npm install, prune, bump,
# trim, prune, and purge the toolchain - seven times, six of them under QEMU.
# That was 27 of the docker job's 29 minutes in v12.32, for a result the .zip
# already was. So this stage runs with --platform=$BUILDPLATFORM, natively on
# the runner, for whichever TARGET it is building: it downloads that target's
# .zip, checks it against the release's own .sha256sum, re-runs the telemetry
# check, and checks that bundle/node really is a binary for the target CPU.
# Nothing in it executes target code, so nothing is emulated.
#
# The image's node is the bundle's own node: the same file the .zip ships,
# rather than a second download that had to agree with it.
FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=12.94
ARG DEBIAN_FRONTEND=noninteractive
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
COPY releases/risk-baseline.json /tmp/risk-baseline.json
RUN <<EOR
set -o xtrace
# Fail hard on any error so a missing release zip / failed download can never
# produce a "successful" image with an empty /build (Cannot find /build/main.js).
set -eo pipefail
apt-get update --assume-yes
apt-get install --assume-yes --no-install-recommends ca-certificates wget unzip python3
# Multi-arch mapping: Docker TARGETARCH -> WeKan's own platform name, which is
# the bundle .zip's name. amd64/arm64 have MongoDB Community;
# ppc64le/s390x/riscv64 have no MongoDB server and ship FerretDB v1 instead (the
# ferretdb binary is baked into their .zip and started by wekan-entrypoint.sh).
# Debian's 32-bit ARM port is armhf (ARMv7, VFPv3-D16), which is what
# linux/arm/v7 runs. ELF_MACHINE is the e_machine the bundle's node must carry.
case "${TARGETARCH}" in
"amd64") WEKAN_ARCH="amd64"; ELF_MACHINE=62 ;;
"arm64") WEKAN_ARCH="arm64"; ELF_MACHINE=183 ;;
"ppc64le") WEKAN_ARCH="ppc64le"; ELF_MACHINE=21 ;;
"s390x") WEKAN_ARCH="s390x"; ELF_MACHINE=22 ;;
"riscv64") WEKAN_ARCH="riscv64"; ELF_MACHINE=243 ;;
"386") WEKAN_ARCH="i386"; ELF_MACHINE=3 ;;
# BOTH linux/arm/v6 and linux/arm/v7 arrive here as TARGETARCH=arm - the CPU
# generation is in TARGETVARIANT, not in TARGETARCH - so this branch MUST read
# the variant. Mapping "arm" straight to armhf, as it did, would hand an ARMv6
# board (Raspberry Pi 1, Zero) the armhf bundle, which is built to Debian's
# ARMv7-A baseline and whose instructions that board cannot execute.
"arm")
ELF_MACHINE=40
case "${TARGETVARIANT}" in
"v6") WEKAN_ARCH="armv6" ;;
# v7, and an unset variant, are Debian's armhf port: ARMv7-A,
# VFPv3-D16 hard-float. That is what linux/arm/v7 runs.
"v7"|"") WEKAN_ARCH="armhf" ;;
# v5 is armel: FerretDB and the MongoDB tools publish it (they are Go),
# but Node.js does not exist for ARMv5 at all, so there is no bundle to
# put in an image and refusing is the only honest answer.
*) echo "Unsupported 32-bit ARM variant: ${TARGETVARIANT} (only v6 and v7 have a WeKan bundle)"; exit 1 ;;
esac ;;
*) echo "Unsupported architecture: ${TARGETARCH}${TARGETVARIANT:+/${TARGETVARIANT}}"; exit 1 ;;
esac
mkdir -p /work
cd /work
# Retry the release-asset download: even though the CI `docker` job needs the
# `release` job (so the asset is already uploaded), GitHub's
# releases/download/<tag>/<asset> URL can briefly return 404 right after upload
# (CDN/propagation lag). A plain wget treats 404 as fatal, which failed the
# build; retry on transient HTTP errors so propagation lag no longer breaks it.
WEKAN_ZIP="wekan-${VERSION}-${WEKAN_ARCH}.zip"
WEKAN_ZIP_URL="https://github.com/wekan/wekan/releases/download/v${VERSION}/${WEKAN_ZIP}"
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 "${WEKAN_ZIP_URL}" \
|| { echo "Failed to download ${WEKAN_ZIP_URL} after retries"; exit 8; }
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 "${WEKAN_ZIP_URL}.sha256sum" \
|| { echo "Failed to download ${WEKAN_ZIP_URL}.sha256sum after retries"; exit 8; }
# The file the image is built from is the file the release published.
sha256sum -c "${WEKAN_ZIP}.sha256sum"
unzip -q "${WEKAN_ZIP}"
rm "${WEKAN_ZIP}" "${WEKAN_ZIP}.sha256sum"
python3 /tmp/check-telemetry.py --bundle ./bundle
# The bundle's own node is the image's node, so it must be FOR the target CPU:
# a .zip of the wrong architecture would otherwise become an image whose node
# cannot start, and nothing here runs it to find out.
test -s bundle/node || { echo "${WEKAN_ZIP} has no bundle/node"; exit 1; }
python3 - "${ELF_MACHINE}" bundle/node <<'EOP'
import struct, sys
want, path = int(sys.argv[1]), sys.argv[2]
head = open(path, 'rb').read(20)
if head[:4] != b'\x7fELF':
sys.exit(f'{path} is not an ELF binary')
got = struct.unpack('>H' if head[5] == 2 else '<H', head[18:20])[0]
if got != want:
sys.exit(f'{path} is for ELF machine {got}, not {want}: the bundle is for another CPU')
print(f'{path}: ELF machine {got}, as expected')
EOP
# /out is laid out as the image's root: the app at /build, node on PATH.
mkdir -p /out/usr/local/bin
mv bundle/node /out/usr/local/bin/node
chmod 755 /out/usr/local/bin/node
ln -s "/usr/local/bin/node" "/out/usr/local/bin/nodejs"
# The .zip's self-contained launchers start its bundled node; the image starts
# wekan-entrypoint.sh instead. /build/ferretdb (used by the entrypoint) and the
# per-arch MongoDB Database Tools stay, for backup/restore inside the container.
rm -f bundle/start-wekan.sh bundle/start-wekan.bat
mv bundle /out/build
EOR
# ── Stage 2: the image, for the TARGET platform ──────────────────────────────
#
# The only work left that runs as the target CPU (under QEMU for all but amd64):
# the WeKan user, an OS update, and the three runtime packages the app needs.
FROM debian:trixie
LABEL maintainer="wekan"
LABEL org.opencontainers.image.ref.name="debian"
LABEL org.opencontainers.image.version="trixie"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=12.26
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV \
DEBUG=false \
DDP_TRANSPORT=sockjs \
NODE_VERSION=v26.11.1 \
METEOR_RELEASE=METEOR@3.6-rc.0 \
NODE_VERSION=v26.13.0 \
METEOR_RELEASE=METEOR@3.6-rc.1 \
USE_EDGE=false \
NPM_VERSION=12.2.0 \
NPM_VERSION=12.2.1 \
SRC_PATH=./ \
WITH_API=true \
MONGO_OPLOG_URL="" \
@@ -171,6 +291,33 @@ ENV \
LOGOUT_IN="" \
LOGOUT_ON_HOURS="" \
LOGOUT_ON_MINUTES="" \
USERS_DISABLE_INACTIVE_AFTER_DAYS="" \
LOGOUT_IDLE_MINUTES="" \
REQUIRE_2FA="" \
PASSWORD_MIN_LENGTH="" \
PASSWORD_MIN_LENGTH_ENFORCE="" \
PASSWORD_CHECK_PWNED="" \
HSTS_MAX_AGE="" \
HSTS_INCLUDE_SUBDOMAINS="" \
HSTS_PRELOAD="" \
ATTACHMENTS_RETENTION_DAYS="" \
ATTACHMENTS_RETENTION_MODE="" \
ATTACHMENTS_PURGE_DELETED_AFTER_DAYS="" \
ATTACHMENTS_RETENTION_DRY_RUN="" \
ATTACHMENTS_RETENTION_MAX_PER_RUN="" \
BACKUP_KEEP_DAYS="" \
ACCOUNTS_COOKIE_REFRESH_RATE_LIMIT="" \
INBOUND_IMAP_POLL_SECONDS="" \
INBOUND_EMAIL_PER_ADDRESS_PER_HOUR="" \
INBOUND_EMAIL_WEBHOOK_SECRET="" \
INBOUND_EMAIL_MAX_BYTES="" \
INBOUND_EMAIL_MAX_ATTACHMENT_BYTES="" \
EMAIL_VIEW_SENDS_PER_HOUR="" \
INBOUND_IMAP_DISABLED="" \
WEKAN_DISK_MONITOR="" \
WEKAN_DISK_WATCH_PATHS="" \
WEKAN_RAM_MONITOR="" \
WEKAN_IMPORT_RUN_STALE_MS="" \
CORS="" \
CORS_ALLOW_HEADERS="" \
CORS_EXPOSE_HEADERS="" \
@@ -236,221 +383,38 @@ ENV \
MONGO_URL_FILE="" \
S3_SECRET_KEY_FILE=""
# Where this image's Node.js comes from is decided by the SAME script the .zip
# bundles and the snap use - official nodejs.org, then unofficial-builds, then
# wekan/node-patches - so the image and the bundle of the same architecture can
# never end up on Node.js from different places. It is copied in rather than
# reimplemented here; a second copy of that order would drift from the first.
COPY --chmod=755 releases/resolve-node-source.sh /tmp/resolve-node-source.sh
# It asks nodejs.org and github.com which builds exist, through releases/fetch.sh
# - which retries a 503 instead of reading it as "that build does not exist".
# The two travel together: without this line the resolve step dies on the first
# lookup. tests/releaseDownloads.test.cjs pins the pair.
COPY --chmod=755 releases/fetch.sh /tmp/fetch.sh
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
COPY releases/risk-baseline.json /tmp/risk-baseline.json
COPY --chmod=755 releases/prepare-bundle-npm.mjs /tmp/prepare-bundle-npm.mjs
# The bundle's `npm install` leaves node-gyp's whole tree - 83 of the 120
# packages in programs/server/node_modules - in a bundle that compiles nothing at
# run time, and a scan of the published image reads it as what it is. The same
# script runs in every per-arch leg of release-all.yml, so the .zip bundles and
# this image are pruned identically. tests/imageBuildOnlyModules.test.cjs pins it.
COPY --chmod=755 releases/prune-build-only-modules.mjs /tmp/prune-build-only-modules.mjs
# Its companion, and its manifest: the npm packages Meteor's own packages bundle,
# which nothing in package.json can reach. The .zip is built with these already
# applied; what this run has to redo is the part `npm install` puts back.
COPY --chmod=755 releases/bump-bundle-npm-deps.mjs /tmp/bump-bundle-npm-deps.mjs
COPY --chmod=644 releases/bundle-npm-security-bumps.json /tmp/bundle-npm-security-bumps.json
# And the third of the same kind: WeKan talks DDP over sockjs on every platform,
# so no bundle carries uWebSockets.js. ddp-server requires that module only
# inside the uws transport's setup(), which a sockjs server never calls, and it
# is 121M of prebuilt binaries for OS/CPU/ABI combinations one machine cannot
# use. The entrypoint coerces DDP_TRANSPORT=uws to sockjs, so an existing
# compose file that asks for uws keeps working rather than crash-looping.
COPY --chmod=755 releases/bundle-trim.mjs /tmp/bundle-trim.mjs
# Its companion: the same idea applied to programs/server/npm/node_modules.
COPY --chmod=755 releases/prune-unreachable-npm.mjs /tmp/prune-unreachable-npm.mjs
RUN <<EOR
set -o xtrace
# Fail hard on any error so a missing release zip / failed download can never
# produce a "successful" image with an empty /build (Cannot find /build/main.js).
set -eo pipefail
# Create Wekan user. --create-home is required because --system users do not
# get a home directory by default; without it /home/wekan never exists and the
# later `chown ... /home/wekan/` aborts the build (now that set -e is active).
# get a home directory by default.
useradd --user-group --system --create-home --home-dir /home/wekan wekan
# OS Updates
apt-get update --assume-yes
apt-get upgrade --assume-yes
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
# Runtime MIME detection and Node.js atomic operations survive build cleanup.
# Runtime MIME detection and Node.js atomics. Nothing in this image compiles
# anything, so no compiler is installed (and none has to be purged).
# No docker-cli: it is a Go program whose Go 1.24 standard library was most of
# the image's vulnerability scan, for a client only the UCS migration used. That
# talks to the Docker socket itself now (releases/ucs-migration/docker-exec.mjs).
apt-get install --assume-yes --no-install-recommends file libatomic1
# Multi-arch mapping: Docker TARGETARCH -> WeKan's own platform name, which is
# both the bundle .zip's name and what resolve-node-source.sh is asked about.
# amd64/arm64 have MongoDB Community; ppc64le/s390x/riscv64 have no MongoDB
# server and ship FerretDB v1 instead (the ferretdb binary is baked into their
# .zip and started by wekan-entrypoint.sh). Debian's 32-bit ARM port is armhf
# (ARMv7, VFPv3-D16), which is what linux/arm/v7 runs.
case "${TARGETARCH}" in
"amd64") WEKAN_ARCH="amd64" ;;
"arm64") WEKAN_ARCH="arm64" ;;
"ppc64le") WEKAN_ARCH="ppc64le" ;;
"s390x") WEKAN_ARCH="s390x" ;;
"riscv64") WEKAN_ARCH="riscv64" ;;
"386") WEKAN_ARCH="i386" ;;
# BOTH linux/arm/v6 and linux/arm/v7 arrive here as TARGETARCH=arm - the CPU
# generation is in TARGETVARIANT, not in TARGETARCH - so this branch MUST read
# the variant. Mapping "arm" straight to armhf, as it did, would hand an ARMv6
# board (Raspberry Pi 1, Zero) the armhf bundle, which is built to Debian's
# ARMv7-A baseline and whose instructions that board cannot execute.
"arm")
case "${TARGETVARIANT}" in
"v6") WEKAN_ARCH="armv6" ;;
# v7, and an unset variant, are Debian's armhf port: ARMv7-A,
# VFPv3-D16 hard-float. That is what linux/arm/v7 runs.
"v7"|"") WEKAN_ARCH="armhf" ;;
# v5 is armel: FerretDB and the MongoDB tools publish it (they are Go),
# but Node.js does not exist for ARMv5 at all, so there is no bundle to
# put in an image and refusing is the only honest answer.
*) echo "Unsupported 32-bit ARM variant: ${TARGETVARIANT} (only v6 and v7 have a WeKan bundle)"; exit 1 ;;
esac ;;
*) echo "Unsupported architecture: ${TARGETARCH}${TARGETVARIANT:+/${TARGETVARIANT}}"; exit 1 ;;
esac
# Node.js installation - official nodejs.org, then unofficial-builds.nodejs.org,
# then wekan/node-patches, in that order, for EVERY architecture.
#
# The choice is made by releases/resolve-node-source.sh, copied in above and used
# by the .zip bundles and the snap as well, so the image and the bundle of one
# architecture always carry the same Node.js from the same place. It is asked
# about the MAJOR, so a CPU whose newest build lags a release still gets its
# newest: it answers with the exact file, what shape that file is, and the
# SHA256 the source published for it.
#
# npm is arch-independent JavaScript, so it is grafted from the official amd64
# tarball of the version that was resolved - a build-time tool, not the shipped
# node. (node-patches publishes a bare node binary and no npm at all, which is
# why npm is fetched separately rather than taken from the archive above.)
cd /tmp
NODE_MAJOR="${NODE_VERSION#v}"; NODE_MAJOR="${NODE_MAJOR%%.*}"
if ! NODE_META="$(bash /tmp/resolve-node-source.sh "${WEKAN_ARCH}" "${NODE_MAJOR}")"; then
echo "No Node.js ${NODE_MAJOR}.x for ${WEKAN_ARCH} at nodejs.org, unofficial-builds.nodejs.org or wekan/node-patches, so this image cannot be built for it. Build node-${WEKAN_ARCH} in wekan/node-patches, or drop ${TARGETARCH} from the image's platform list." >&2
exit 1
fi
eval "${NODE_META}"
echo "Node.js ${node_full} for ${WEKAN_ARCH}: ${node_from} (${node_url})"
wget --tries=20 --waitretry=20 --retry-on-http-error=403,500,502,503 -O node-download "${node_url}"
echo "${node_sha256} node-download" | sha256sum -c -
case "${node_kind}" in
binary)
cp node-download /usr/local/bin/node
;;
tar.xz|tar.gz|tar)
# bsdtar (libarchive-tools) is installed below for the bundle; plain tar
# here, letting it detect the compression rather than naming it.
tar -xf node-download --no-same-owner "${node_member}"
cp "${node_member}" /usr/local/bin/node
;;
*)
echo "resolve-node-source.sh returned an unknown kind '${node_kind}'." >&2
exit 1
;;
esac
chmod +x /usr/local/bin/node
# npm + npx from the official amd64 tarball of the SAME version; extract only
# those paths, not the amd64 node binary.
# Same flags as the download above: a bare wget treats a 503 as fatal, and
# nodejs.org has them.
wget --tries=20 --waitretry=20 --retry-on-http-error=403,500,502,503 \
"https://nodejs.org/dist/${node_full}/node-${node_full}-linux-x64.tar.gz"
wget --tries=20 --waitretry=20 --retry-on-http-error=403,500,502,503 \
"https://nodejs.org/dist/${node_full}/SHASUMS256.txt"
grep " node-${node_full}-linux-x64.tar.gz\$" SHASUMS256.txt | sha256sum -c -
tar xzf "node-${node_full}-linux-x64.tar.gz" -C /usr/local --strip-components=1 --no-same-owner \
"node-${node_full}-linux-x64/lib/node_modules/npm" \
"node-${node_full}-linux-x64/bin/npm" \
"node-${node_full}-linux-x64/bin/npx"
rm -rf node-download "node-${node_full}-linux-x64.tar.gz" "node-${node_full}-linux-x64" SHASUMS256.txt
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
# NPM configuration
npm install -g npm@${NPM_VERSION}
chown --recursive wekan:wekan /home/wekan/
# Temporary Tar swap for Meteor bundle
cp $(which tar) $(which tar)~
ln -sf $(which bsdtar) $(which tar)
# WeKan Bundle Installation
mkdir -p /home/wekan/app
cd /home/wekan/app
# Retry the release-asset download: even though the CI `docker` job needs the
# `release` job (so the asset is already uploaded), GitHub's
# releases/download/<tag>/<asset> URL can briefly return 404 right after upload
# (CDN/propagation lag). A plain wget treats 404 as fatal, which failed the
# build; retry on transient HTTP errors so propagation lag no longer breaks it.
WEKAN_ZIP_URL="https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 "${WEKAN_ZIP_URL}" \
|| { echo "Failed to download ${WEKAN_ZIP_URL} after retries"; exit 8; }
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
node /tmp/prepare-bundle-npm.mjs ./bundle
npm install --prefix ./bundle/programs/server
# node-gyp and @mapbox/node-pre-gyp compiled nothing here - every native module
# in the bundle is a prebuilt .node - and nothing in boot.js reaches them. Their
# tree is what shipped `tar` 6.2.1 (CRITICAL) and npm's networking stack to the
# published image, so it goes now that the install is done.
node /tmp/prune-build-only-modules.mjs ./bundle
# The .zip already carries the bumped meteor/ tree; this install put back
# meteor-dev-bundle's own underscore 1.13.7 pin (CVE-2026-27601) over it, so the
# same pass runs here.
node /tmp/bump-bundle-npm-deps.mjs ./bundle
# No uWebSockets.js, no legacy client, no source maps: this image runs sockjs,
# serves web.browser to every browser, and has no debugger attached to it.
node /tmp/bundle-trim.mjs ./bundle --transport sockjs --drop-legacy-client
# And the npm tree rspack cannot tree-shake, because Atmosphere packages load it
# through Npm.require(). Only what it can prove nothing requires.
node /tmp/prune-unreachable-npm.mjs ./bundle
python3 /tmp/check-telemetry.py --bundle ./bundle
mv /home/wekan/app/bundle /build
# The .zip bundle now ships a self-contained launcher + its own Node.js for the
# offline downloads; the Docker image installs its own Node and uses
# wekan-entrypoint.sh, so drop the redundant bundled node + launchers. Keeps
# /build/ferretdb (used by the entrypoint) and the per-arch MongoDB Database Tools
# (bsondump, mongodump, mongorestore, … from wekan/mongo-tools-patches, embedded in the
# bundle) for backup/restore inside the container. Saves ~80 MB per arch.
rm -f /build/node /build/start-wekan.sh /build/start-wekan.bat
# Restore original tar
mv $(which tar)~ $(which tar)
# Cleanup
# npm is a BUILD tool in this image and nothing else: the only thing it does is
# the `npm install` above, and the container starts bash + wekan-entrypoint.sh,
# which never calls it. Shipping it shipped npm's own bundled dependencies - its
# `tar` (CRITICAL), `sigstore`, `@sigstore/*`, `ip-address`, `brace-expansion` -
# as image content that no code path can reach. node itself stays, because that
# is what runs WeKan.
rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
# Remove unused Go-based pebble binary shipped by base image to reduce CVE surface.
apt-get remove --purge --assume-yes pebble || true
rm -f /usr/bin/pebble
apt-get remove --purge --assume-yes ${BUILD_DEPS}
apt-get autoremove --assume-yes
apt-get clean --assume-yes
rm -Rf /tmp/*
rm -Rf /var/lib/apt/lists/*
rm -Rf /home/wekan/app
mkdir -p /data
chown wekan:wekan --recursive /data
EOR
# The bundle and its node, prepared in stage 1. npm is not in this image at all:
# nothing here installs anything, and the entrypoint never calls it.
COPY --from=bundle /out/ /
# Database-backend selector entrypoint. Every arch's bundle now ships a FerretDB
# binary (baked into the .zip and moved to /build/ferretdb above); this script
# starts FerretDB v1 (SQLite) or leaves the DB external based on WEKAN_DB and the
@@ -465,6 +429,13 @@ COPY --chmod=644 releases/ferretdb/recovery-bridge.mjs /build/recovery-bridge.mj
# for a reverse proxy to time out on.
COPY --chmod=644 releases/ferretdb/db-ready.mjs /build/db-ready.mjs
# UCS runs this same image with an explicit migration command. Only that
# temporary container receives the Docker socket; normal startup is unchanged.
COPY releases/ucs-migration /opt/ucs-migration
RUN chmod 755 /opt/ucs-migration/start.sh /opt/ucs-migration/tools/*
COPY releases/nextcloud/capability.json /build/nextcloud-exapp-capability.json
USER wekan
ENV PORT=8080
EXPOSE $PORT
+13 -3
View File
@@ -24,14 +24,24 @@ that by providing one-click installation on various platforms.
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
- WeKan ® largest user has 30k users using WeKan ® in their company.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) into 234 locale catalogs;
164 have non-English text for over 90% of source keys. This measures text coverage,
234 have non-English text for over 90% of source keys. This measures text coverage,
not translation quality; language review and remaining translations are ongoing.
- [Databases](https://github.com/wekan/wekan/blob/main/docs/Databases/Supported.md):
- FerretDB v1 with SQLite, the default on every platform - bundle, Docker,
Snap and Kubernetes - so no database server is needed.
- FerretDB v1 with PostgreSQL works; with MySQL, MariaDB or SAP HANA it is
experimental.
- MongoDB 6, 7, 8 and 9. MongoDB refuses to start on Linux kernels 6.19 to
7.0.13, which FerretDB is not affected by.
- All of them - SAP HANA only when asked - answer the same 110-query
conformance catalogue the same way, checked by EVERYTHING in build.sh and
build.bat.
- [Features](https://github.com/wekan/wekan/tree/main/docs/Features):
- Real-time user interface
- All Boards page, Drag drop reorder with one or Multi-Selection:
- Board icons at Remaining, (Sub)Workspaces, Archive
- Shared Templates: Boards, Lists, Cards
- Add Board / Import ([all import and export formats](docs/Features/ImportExport/External-Tools.md)):
- Add Board / Import ([all import and export formats](docs/Features/Board-Sidebar/Board-Settings/Import-Export/External-Tools.md)):
- WeKan JSON/.zip, CSV/TSV, Excel, Markdown, Obsidian Kanban, Leo, OPML, Org mode, todo.txt
- Trello, Jira, Asana, Zenkit, Focalboard, Todoist, Microsoft Planner, MeisterTask, Linear, TickTick, ClickUp, Nullboard, Kanri, Pivotal Tracker, Redmine, Tasks.org, monday.com,
Super Productivity, Taiga, Vikunja, Wrike (Excel and workflow), Quire, Teamwork.com, Businessmap, Notion, Plane
@@ -80,7 +90,7 @@ that by providing one-click installation on various platforms.
- Import / Export IFTTT Rules; import n8n, Node-RED and Trello Butler; import / export Wrike workflows
- Export board :
- Select what to include: Card details, Board, Activities, Labels, People (Creator, Owner, Members, Assignees), Board Info (Board, List, Swimlane), Dates (Created, Received, Start, Due, End), Description, Custom Fields, Checklists, Subtasks, Comments, Attachments, Votin, Plannin Poker, Stickers, Location (link to map based on coordinates), Dependencies, Sort, Scrum Settings
- Export to ([all formats](docs/Features/ImportExport/External-Tools.md)): PDF, Excel, HTML, Calendar feed (iCal), Dependencies JSON/SVG,
- Export to ([all formats](docs/Features/Board-Sidebar/Board-Settings/Import-Export/External-Tools.md)): PDF, Excel, HTML, Calendar feed (iCal), Dependencies JSON/SVG,
CSV , ; TSV, JSON with/without attachmeents, .zip (Attachments),
Kanboard, Markdown, Leo, OPML, Org mode, todo.txt, Taskwarrior, Focalboard, Todoist,
Microsoft Planner, MeisterTask, Obsidian Kanban, Linear, TickTick, ClickUp, Nullboard, Kanri, Pivotal Tracker, Redmine, Tasks.org, monday.com,
+13 -5
View File
@@ -29,7 +29,15 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
- **Remediation**: Please wait for a new WeKan release that addresses the issue.
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- **Recognition**: We will acknowledge your contribution by adding you to our
[Hall of Fame](https://wekan.fi/hall-of-fame/).
[Hall of Fame](https://wekan.fi/hall-of-fame/). The stars follow the
[Stars Audit](https://github.com/wekan/wekan.fi/blob/main/hall-of-fame/Audit-Stars.md):
a GitHub security advisory (GHSA) with details and a suggested fix gets 5 stars,
a report sent to security@wekan.fi or some other way, with a suggested fix, gets 3.
- **Adding the GHSA later**: if you sent your report to security@wekan.fi, or some
other way, and are later able to add it as a "New draft security advisory" at
[WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories),
please do. The maintainer of WeKan then requests the CVE through that GHSA, and
your Hall of Fame row is upgraded from 3 stars to 5.
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
### 3. Post-Release and Public Disclosure:
@@ -264,13 +272,13 @@ Meteor.startup(() => {
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
[Sandstorm](https://sandstorm.io) is a separate Open Source platform that has been
[security audited](https://sandstorm.io/news/2017-03-02-security-review) and found bugs fixed.
[Sandstorm](https://sandstorm.org) is a separate Open Source platform that has been
[security audited](https://sandstorm.org/news/2017-03-02-security-review) and found bugs fixed.
Sandstorm also has passwordless login, LDAP, SAML, Google, and other auth options already.
On Sandstorm, code is read-only and signed by app maintainers; only grain content can be modified.
WeKan on Sandstorm runs in a sandboxed grain; it does not have access elsewhere without a user-visible PowerBox request or opening a randomly-generated API key URL.
Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/using/security-practices/) and
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
Also read [Sandstorm Security Practices](https://docs.sandstorm.org/en/latest/using/security-practices/) and
[Sandstorm Security non-events](https://docs.sandstorm.org/en/latest/using/security-non-events/).
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
## What WeKan bugs are eligible?
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v12.26.0"
appVersion: "v12.94.0"
files:
userUploads:
- README.md
+46 -53
View File
@@ -8,13 +8,6 @@
# TODO:
# addcustomfieldtoboard: There is error: Settings must be object. So adding does not work yet.
try:
# python 3
from urllib.parse import urlencode
except ImportError:
# python 2
from urllib import urlencode
import json
import requests
import sys
@@ -77,7 +70,7 @@ If *nix: chmod +x api.py => ./api.py users
python3 api.py importboard EXPORT.json # Import a whole board (with rules/workflows) from a WeKan export
python3 api.py migratefromwekan REMOTE_URL REMOTE_USER REMOTE_PASS # Import ALL boards+workflows+rules from another WeKan
python3 api.py exportboardpdf BOARDID OUTPUT.pdf # Export a whole board to PDF
python3 api.py importboardfrom SOURCE FILE [--csv-mapping MAPPING.json] # Import a tool's export file (csv and excel: --csv-mapping says which column is which field); SOURCE is a key of the import page (trello, jira, csv, excel, planner, wrike, notion, ...), see docs/Features/ImportExport
python3 api.py importboardfrom SOURCE FILE [--csv-mapping MAPPING.json] # Import a tool's export file (csv and excel: --csv-mapping says which column is which field); SOURCE is a key of the import page (trello, jira, csv, excel, planner, wrike, notion, ...), see docs/Features/Board-Sidebar/Board-Settings/Import-Export
python3 api.py exportboardformat BOARDID FORMAT OUTPUTFILE # Export to a tool's format; FORMAT is a key of the export menu (kanboard, trello, planner, wrike, wrikeworkflow, ...)
python3 api.py importboardsfrom SOURCE [--split] FILE_OR_DIR ... # Import many boards: each file (or each file of a directory or .zip) becomes a board; --split makes one board per project
python3 api.py exportallboards FORMAT OUTPUTFILE [--boards ID1,ID2] # Export all boards: Excel as one workbook with a sheet per board, other formats as a .zip with a file per board
@@ -163,7 +156,7 @@ If *nix: chmod +x api.py => ./api.py users
if arguments == 0:
print(syntax)
exit
sys.exit()
# TODO:
# print(" python3 api.py attachmentjson BOARDID ATTACHMENTID # One attachment as JSON base64")
@@ -329,7 +322,7 @@ if arguments == 8:
if new_card_id:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + new_card_id
put_data = {'labelIds': labelIds}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -372,7 +365,7 @@ if arguments == 6:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'title': '{}'.format(newcardtitle), 'description': '{}'.format(newcarddescription)}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}',"}\n")
@@ -411,7 +404,7 @@ if arguments == 5:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'labelIds': labelIds}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== ADD LABEL ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}',"}\n")
@@ -437,7 +430,7 @@ if arguments == 5:
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'color': '{}'.format(newcolor)}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD COLOR ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -456,7 +449,7 @@ if arguments >= 4:
print(edlist)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'title': '{}'.format(newlisttitle)}
body = requests.put(edlist, data=put_data, headers=headers)
requests.put(edlist, data=put_data, headers=headers)
print("=== EDIT LIST ===\n")
body = requests.get(edlist, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -482,7 +475,7 @@ if arguments >= 4:
print(edlist)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'color': '{}'.format(newcolor)}
body = requests.put(edlist, data=put_data, headers=headers)
requests.put(edlist, data=put_data, headers=headers)
print("=== EDIT LIST COLOR ===\n")
body = requests.get(edlist, headers=headers)
data2 = body.text.replace('}', "}\n")
@@ -1050,7 +1043,7 @@ if arguments >= 1:
if arguments < 6:
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
print("Storage backends: fs, gridfs, s3")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
@@ -1064,7 +1057,7 @@ if arguments >= 1:
import os, mimetypes
if not os.path.isfile(filepath):
print(f"Error: File '{filepath}' not found")
exit(1)
sys.exit(1)
filename = os.path.basename(filepath)
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
params = {'boardId': boardid, 'swimlaneId': swimlaneid, 'listId': listid, 'cardId': cardid,
@@ -1092,7 +1085,7 @@ if arguments >= 1:
# ------- DOWNLOAD ATTACHMENT START -----------
if arguments < 3:
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
outputpath = sys.argv[3]
@@ -1121,7 +1114,7 @@ if arguments >= 1:
# ------- ATTACHMENT INFO START -----------
if arguments < 2:
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
@@ -1160,7 +1153,7 @@ if arguments >= 1:
# ------- LIST CARD ATTACHMENTS START -----------
if arguments < 5:
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
@@ -1199,7 +1192,7 @@ if arguments >= 1:
# ------- COPY/MOVE ATTACHMENT START -----------
if arguments < 6:
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
targetboardid = sys.argv[3]
@@ -1210,7 +1203,7 @@ if arguments >= 1:
if operation not in ['copy', 'move']:
print("Operation must be 'copy' or 'move'")
exit(1)
sys.exit(1)
# Prepare request data
request_data = {
@@ -1253,7 +1246,7 @@ if arguments >= 1:
# ------- DELETE ATTACHMENT START -----------
if arguments < 2:
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
exit(1)
sys.exit(1)
attachmentid = sys.argv[2]
@@ -1282,7 +1275,7 @@ if arguments >= 1:
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
# These commands wrap REST endpoints that already exist in Wekan.
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
# See CHANGELOG-big.md "Upcoming" section for related issues and design notes.
# Map a human readable ROLE to the board member permission boolean flags
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
@@ -1322,7 +1315,7 @@ if arguments >= 1:
# ------- ADD BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py addboardmember BOARDID USERID ROLE")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4] if arguments > 3 else 'normal'
@@ -1339,7 +1332,7 @@ if arguments >= 1:
# ------- REMOVE BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py removeboardmember BOARDID USERID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
remove_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'remove'
@@ -1354,7 +1347,7 @@ if arguments >= 1:
# ------- SET BOARD MEMBER ROLE START -----------
if arguments < 4:
print("Usage: python3 api.py setboardmemberrole BOARDID USERID ROLE")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4]
@@ -1370,7 +1363,7 @@ if arguments >= 1:
# ------- LIST BOARD DOMAINS START -----------
if arguments < 2:
print("Usage: python3 api.py boarddomains BOARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domains_url = wekanurl + apiboards + boardid + s + 'domains'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -1383,7 +1376,7 @@ if arguments >= 1:
# ------- ADD BOARD DOMAIN START -----------
if arguments < 3:
print("Usage: python3 api.py addboarddomain BOARDID DOMAIN")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domain = sys.argv[3]
add_domain_url = wekanurl + apiboards + boardid + s + 'domains'
@@ -1398,7 +1391,7 @@ if arguments >= 1:
# ------- REMOVE BOARD DOMAIN START -----------
if arguments < 3:
print("Usage: python3 api.py removeboarddomain BOARDID DOMAIN")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
domain = sys.argv[3]
remove_domain_url = wekanurl + apiboards + boardid + s + 'domains' + s + domain
@@ -1413,7 +1406,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py {} BOARDID LISTID CARDID IDS".format(sys.argv[1]))
print("IDS = comma-separated userIds, or '' to clear")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1435,7 +1428,7 @@ if arguments >= 1:
print("Usage: python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE DATEVALUE")
print("DATETYPE: received, start, due, end")
print("DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1453,7 +1446,7 @@ if arguments >= 1:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {datemap[datetype]: datevalue}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== SET CARD DATE {} ===\n".format(datetype.upper()))
print(body.text)
# ------- SET CARD DATE END -----------
@@ -1463,7 +1456,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS")
print("LABELIDS = comma-separated labelIds, or '' to clear")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1472,7 +1465,7 @@ if arguments >= 1:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
# implemented server-side, see Issue #5819 in CHANGELOG-big.md "Upcoming".
put_data = {'labelIds': labelids if labelids else []}
body = requests.put(edcard, json=put_data, headers=headers)
print("=== SET CARD LABELS ===\n")
@@ -1483,7 +1476,7 @@ if arguments >= 1:
# ------- MOVE CARD START -----------
if arguments < 7:
print("Usage: python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1497,7 +1490,7 @@ if arguments >= 1:
'newSwimlaneId': newswimlaneid,
'newListId': newlistid,
}
body = requests.put(edcard, data=put_data, headers=headers)
requests.put(edcard, data=put_data, headers=headers)
print("=== MOVE CARD ===\n")
print(body.text)
# ------- MOVE CARD END -----------
@@ -1508,7 +1501,7 @@ if arguments >= 1:
print("Usage: python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE")
print("CARDSJSONFILE = JSON file with a list of titles, e.g. [\"Card A\", \"Card B\"],")
print(" or a list of card objects [{\"title\": \"Card A\", \"description\": \"...\"}, ...]")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
authorid = sys.argv[4]
@@ -1516,16 +1509,16 @@ if arguments >= 1:
cardsjsonfile = sys.argv[6] if arguments > 5 else None
if not cardsjsonfile:
print("CARDSJSONFILE is required")
exit(1)
sys.exit(1)
try:
with open(cardsjsonfile, 'r') as f:
raw_cards = json.load(f)
except Exception as e:
print("Error reading CARDSJSONFILE: {}".format(e))
exit(1)
sys.exit(1)
if not isinstance(raw_cards, list) or len(raw_cards) == 0:
print("CARDSJSONFILE must contain a non-empty JSON list")
exit(1)
sys.exit(1)
cards = []
for item in raw_cards:
if isinstance(item, str):
@@ -1534,7 +1527,7 @@ if arguments >= 1:
cards.append(item)
else:
print("Each card must be a string (title) or an object")
exit(1)
sys.exit(1)
bulk_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + 'bulk'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'cards': cards}
@@ -1548,7 +1541,7 @@ if arguments >= 1:
if arguments < 3:
print("Usage: python3 api.py bulkdeletecards BOARDID CARDIDS")
print("CARDIDS = comma-separated cardIds")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
cardids_raw = sys.argv[3]
cardids = [x for x in cardids_raw.split(',') if x]
@@ -1565,7 +1558,7 @@ if arguments >= 1:
if arguments < 5:
print("Usage: python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS")
print("CARDIDS / ADDLABELIDS / REMOVELABELIDS = comma-separated, or '' for none")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
cardids = [x for x in sys.argv[3].split(',') if x]
add_labelids = [x for x in sys.argv[4].split(',') if x]
@@ -1582,7 +1575,7 @@ if arguments >= 1:
# ------- CREATE LINKED CARD START -----------
if arguments < 6:
print("Usage: python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
swimlaneid = sys.argv[4]
@@ -1600,7 +1593,7 @@ if arguments >= 1:
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE START -----------
if arguments < 5:
print("Usage: python3 api.py {} BOARDID LISTID CARDID USERID".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
@@ -1620,14 +1613,14 @@ if arguments >= 1:
# ------- COPY CARD START -----------
if arguments < 4:
print("Usage: python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION]")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
toswimlaneid = sys.argv[5] if arguments > 4 else None
if not toswimlaneid:
print("TOSWIMLANEID is required")
exit(1)
sys.exit(1)
toboardid = sys.argv[6] if arguments > 5 else boardid
tolistid = sys.argv[7] if arguments > 6 else listid
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'copy'
@@ -1644,7 +1637,7 @@ if arguments >= 1:
# ------- COPY/MOVE SWIMLANE START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID SWIMLANEID [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
toboardid = sys.argv[4] if arguments > 3 else boardid
@@ -1663,7 +1656,7 @@ if arguments >= 1:
# ------- COPY/MOVE LIST START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
sys.exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
toswimlaneid = sys.argv[4] if arguments > 3 else None
@@ -1702,7 +1695,7 @@ if arguments >= 1:
# ------- GET CARD SETTINGS START -----------
if arguments < 2:
print("Usage: python3 api.py getcardsettings BOARDID")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
url = wekanurl + apiboards + boardid + s + 'cardSettings'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
@@ -1717,7 +1710,7 @@ if arguments >= 1:
if arguments < 4:
print("Usage: python3 api.py setcardsetting BOARDID KEY VALUE")
print("e.g. python3 api.py setcardsetting BOARDID allowsDueDate true")
exit(1)
sys.exit(1)
boardid = sys.argv[2]
key = sys.argv[3]
value = sys.argv[4]
@@ -1945,7 +1938,7 @@ if arguments >= 3 and sys.argv[1] == 'uploadbackground':
response = requests.post(url, headers=headers, params=params, data=f)
except Exception as e:
print(f"Error reading file: {e}")
exit(1)
sys.exit(1)
print(response.text)
if arguments >= 3 and sys.argv[1] == 'downloadbackground':
+192 -31
View File
@@ -39,6 +39,11 @@ if /I "%~1"=="help" goto cli_help_exit
if /I "%~1"=="-l" goto cli_list_exit
if /I "%~1"=="--list" goto cli_list_exit
if /I "%~1"=="list" goto cli_list_exit
REM The same non-interactive EVERYTHING as `./build.sh --run-everything [mode]`.
if /I "%~1"=="--run-everything" (
if "%~2"=="" (set "WEKAN_EVERYTHING_MODE=two-worker") else (set "WEKAN_EVERYTHING_MODE=%~2")
goto test_everything
)
call :cli_run %*
exit /b %errorlevel%
:cli_help_exit
@@ -55,6 +60,10 @@ exit /b 0
REM --- Repo root = folder of this script (strip trailing backslash) ---
set "REPO=%~dp0"
if "%REPO:~-1%"=="\" set "REPO=%REPO:~0,-1%"
REM As build.sh: everything this script writes - temporary files, downloads,
REM Node.js, Meteor, the npm cache, Go and the Playwright browsers - stays in
REM .tools\, and those tools come first on PATH (releases\tools-env.bat).
call "%REPO%\releases\tools-env.bat"
cd /d "%REPO%"
REM Give the Meteor build tool and Node processes a larger heap so long
@@ -189,6 +198,11 @@ echo 14^) All databases ^(sequential^): build newest FerretDB v1, run every que
echo against every database with an image for this CPU, compare the answers
echo 15^) Run all FerretDB tests - SEQUENTIAL ^(unit, vet, integration^)
echo 16^) EVERYTHING source one by one ^(Dev server nobuild, all browsers^)
echo 17^) Live tests with your own accounts: GitLab write-back with the mirror's
echo glab login ^(only scratch items it creates and removes^) and the API
echo imports, read only ^(releases\live-tests\README.md^)
echo 18^) Database speed test: the common WeKan queries on every database with an
echo image for this CPU, compared, written to SpeedTest\Results.md
set "choice="
set /p "choice=Choose: "
if "%choice%"=="1" goto test_everything_two
@@ -207,6 +221,8 @@ if "%choice%"=="13" goto count_tests
if "%choice%"=="14" goto test_all_databases
if "%choice%"=="15" goto test_ferretdb
if "%choice%"=="16" goto test_everything_source
if "%choice%"=="17" goto test_live
if "%choice%"=="18" goto test_db_speed
if "%choice%"=="0" goto menu
goto menu_tests
@@ -816,7 +832,13 @@ REM as the image the compose file references, so the following "up -d" runs your
REM freshly built container instead of a possibly-stale prebuilt one. All WeKan
REM compose files reference ghcr.io/wekan/wekan:latest.
if not exist "%REPO%\Dockerfile" ( echo ERROR: Dockerfile not found in %REPO%. & goto end )
set "WK_IMG=ghcr.io/wekan/wekan:latest"
REM The wekan-app image tag THIS compose file uses, as build.sh
REM wekan_docker_build_image reads it; ghcr.io/wekan/wekan:latest only if the
REM file names none. A commented-out image line is not one.
set "WK_IMG="
for /f "tokens=1,* delims=:" %%a in ('findstr /r /c:"^ *image: *[^#]*wekan/wekan" %CF% 2^>nul') do if not defined WK_IMG set "WK_IMG=%%b"
if defined WK_IMG for /f "tokens=* delims= " %%i in ("!WK_IMG!") do set "WK_IMG=%%i"
if not defined WK_IMG set "WK_IMG=ghcr.io/wekan/wekan:latest"
echo ==^> Building wekan-app image from local source, tagging it as: %WK_IMG%
docker build -t %WK_IMG% -f "%REPO%\Dockerfile" "%REPO%"
if errorlevel 1 ( echo ERROR: Docker build failed. & goto end )
@@ -829,17 +851,65 @@ REM ===========================================================================
echo Installing WeKan dependencies for Windows.
rem Full MIME detection uses the libmagic WASM engine/database installed by npm.
rem No external file.exe installation is needed.
echo This uses Chocolatey to install git, curl, Node.js LTS and MongoDB tools,
echo then installs Meteor via npm.
echo This uses Chocolatey to install git, curl and the MongoDB tools, then puts
echo Node.js and Meteor into .tools\ of this checkout - not into the system.
where choco >nul 2>&1
if errorlevel 1 (
echo Installing Chocolatey package manager ...
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
)
call choco install -y git curl nodejs-lts mongodb-shell
echo Installing Meteor (npm -g install meteor) ...
call npm -g install meteor
echo Done. Open a new terminal so PATH changes take effect, then re-run this script.
REM Node.js and npm are the versions the Dockerfile builds with (NODE_VERSION,
REM NPM_VERSION), as build.sh installs them - not whatever LTS Chocolatey has.
set "WK_NODE_VERSION="
set "WK_NPM_VERSION="
for /f "tokens=2 delims== " %%v in ('findstr /r /c:"NODE_VERSION=v[0-9]" "%REPO%\Dockerfile"') do if not defined WK_NODE_VERSION set "WK_NODE_VERSION=%%v"
for /f "tokens=2 delims== " %%v in ('findstr /r /c:"NPM_VERSION=[0-9]" "%REPO%\Dockerfile"') do if not defined WK_NPM_VERSION set "WK_NPM_VERSION=%%v"
if defined WK_NODE_VERSION set "WK_NODE_VERSION=%WK_NODE_VERSION:v=%"
call choco install -y git curl mongodb-shell
REM Node.js from nodejs.org's zip, unpacked into .tools\node-v<version>-win-<arch>
REM and verified against its published SHASUMS256.txt first - not a system-wide
REM Chocolatey install. curl.exe and tar.exe come with Windows 10 and 11.
if not defined WK_NODE_VERSION (
echo ERROR: could not read NODE_VERSION from the Dockerfile.
goto end
)
set "WK_NODE_ARCH=x64"
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "WK_NODE_ARCH=arm64"
set "WK_NODE_NAME=node-v%WK_NODE_VERSION%-win-%WK_NODE_ARCH%"
if exist "%WEKAN_TOOLS_DIR%\%WK_NODE_NAME%\node.exe" (
echo Node.js %WK_NODE_VERSION% is already in .tools\%WK_NODE_NAME%.
) else (
echo Installing Node.js %WK_NODE_VERSION% ^(Dockerfile NODE_VERSION^) into .tools ...
curl -fsSL -o "%TEMP%\%WK_NODE_NAME%.zip" "https://nodejs.org/dist/v%WK_NODE_VERSION%/%WK_NODE_NAME%.zip" || goto end
curl -fsSL -o "%TEMP%\SHASUMS256.txt" "https://nodejs.org/dist/v%WK_NODE_VERSION%/SHASUMS256.txt" || goto end
powershell -NoProfile -Command "$want = ((Select-String -Path $env:TEMP\SHASUMS256.txt -Pattern ([regex]::Escape($env:WK_NODE_NAME + '.zip') + '$')).Line -split '\s+')[0]; $got = (Get-FileHash -Algorithm SHA256 ($env:TEMP + '\' + $env:WK_NODE_NAME + '.zip')).Hash; if (-not $want -or $got -ne $want.ToUpper()) { Write-Error ('SHA256 mismatch for ' + $env:WK_NODE_NAME + '.zip'); exit 1 }" || goto end
tar -xf "%TEMP%\%WK_NODE_NAME%.zip" -C "%WEKAN_TOOLS_DIR%" || goto end
del "%TEMP%\%WK_NODE_NAME%.zip" "%TEMP%\SHASUMS256.txt"
)
REM npm's global installs (.tools\npm-global) before Node.js's own npm, so the
REM npm installed next is the one found.
set "PATH=%npm_config_prefix%;%WEKAN_TOOLS_DIR%\%WK_NODE_NAME%;%PATH%"
if defined WK_NPM_VERSION (
echo Installing npm %WK_NPM_VERSION% ^(Dockerfile NPM_VERSION^) ...
call npm install -g npm@%WK_NPM_VERSION%
)
REM Meteor as https://docs.meteor.com/about/install.html installs it on Windows,
REM Linux and macOS alike: npx meteor. The meteor npm package is only an
REM installer; npx runs it directly, into %LOCALAPPDATA%\.meteor - so
REM LOCALAPPDATA is .tools for the installer only, and Meteor lands in
REM .tools\.meteor. npm -g install meteor ran the same installer as an install
REM script, which npm 12 refuses to run.
if exist "%WEKAN_TOOLS_DIR%\.meteor\meteor.bat" (
echo Meteor is already installed in .tools\.meteor.
) else (
echo Installing Meteor ^(npx meteor^) into .tools\.meteor ...
setlocal
set "LOCALAPPDATA=%WEKAN_TOOLS_DIR%"
call npx -y meteor
endlocal
)
call "%REPO%\releases\tools-env.bat"
echo Done. build.bat puts .tools\ Node.js and Meteor on PATH itself; re-run this script.
goto end
REM ===========================================================================
@@ -918,6 +988,40 @@ powershell -NoProfile -Command "& cmd /d /c $env:WEKAN_LOG_COMMAND 2>&1 | Tee-Ob
exit /b %errorlevel%
:buildcommon
REM The same diagnostics as build.sh build_wekan: npm verbose output and
REM foreground install scripts, METEOR_PROFILE and the resolver command/output
REM tracer, so a failed Windows build leaves the same evidence in its log. Node
REM module resolution (one line per require, gigabytes a minute) only with
REM WEKAN_BUILD_TRACE_MODULES=1. WEKAN_BUILD_HEAP_SNAPSHOT=1 caps the heap at
REM WEKAN_BUILD_HEAP_SNAPSHOT_MB (4096) and writes a heap snapshot near the limit.
setlocal
set "npm_config_loglevel=verbose"
set "npm_config_foreground_scripts=true"
if not defined METEOR_PROFILE set "METEOR_PROFILE=100"
if "%WEKAN_BUILD_TRACE_MODULES%"=="1" (
if defined NODE_DEBUG (set "NODE_DEBUG=!NODE_DEBUG!,module") else (set "NODE_DEBUG=module")
)
if "%WEKAN_BUILD_HEAP_SNAPSHOT%"=="1" (
if not defined WEKAN_BUILD_HEAP_SNAPSHOT_MB set "WEKAN_BUILD_HEAP_SNAPSHOT_MB=4096"
set "TOOL_NODE_FLAGS=--max-old-space-size=!WEKAN_BUILD_HEAP_SNAPSHOT_MB! --heapsnapshot-near-heap-limit=1"
set "NODE_OPTIONS=--max-old-space-size=!WEKAN_BUILD_HEAP_SNAPSHOT_MB! --heapsnapshot-near-heap-limit=1"
echo Heap snapshot: ON, heap capped at !WEKAN_BUILD_HEAP_SNAPSHOT_MB! MB; the build will fail sooner and write Heap.*.heapsnapshot.
)
set "NODE_OPTIONS=%NODE_OPTIONS% --require="%REPO%\tools\build-command-output.cjs""
echo Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=%METEOR_PROFILE% ^(milliseconds^).
call :buildcommon_steps
set "BUILD_RC=%errorlevel%"
if not "%BUILD_RC%"=="0" if defined WEKAN_BUILD_LOG (
findstr /c:"JavaScript heap out of memory" "%WEKAN_BUILD_LOG%" >nul 2>&1 && (
echo.
echo The build ran out of JavaScript heap ^(allowed %WEKAN_BUILD_HEAP_MB% MB^).
echo To see what fills it, run once with WEKAN_BUILD_HEAP_SNAPSHOT=1 and open the
echo Heap.*.heapsnapshot it writes in Chrome DevTools: Memory -^> Load.
)
)
endlocal & exit /b %BUILD_RC%
:buildcommon_steps
if exist "%REPO%\node_modules" call :build_logged rmdir /s /q "%REPO%\node_modules"
if exist "%REPO%\node_modules\.cache" call :build_logged rmdir /s /q "%REPO%\node_modules\.cache"
if exist "%REPO%\.meteor\local" call :build_logged rmdir /s /q "%REPO%\.meteor\local"
@@ -972,7 +1076,7 @@ goto end
call :ensure_dirs
call :set_dev_env
set "WARN_WHEN_USING_OLD_API=true"
set "NODE_OPTIONS=--trace-warnings --max-old-space-size=8192"
set "NODE_OPTIONS=--trace-warnings --max-old-space-size=%WEKAN_BUILD_HEAP_MB%"
set "ROOT_URL=http://localhost:3000"
call :runlog --port 3000
goto end
@@ -1019,24 +1123,40 @@ call :runlog --port %PORT%
goto end
:dev_customurl
REM Parity with build.sh's "CUSTOM PORT + SUBDOMAIN": ask for the port and the
REM ROOT_URL host. An empty answer or a bare name is local, so the port is added;
REM a full URL or a dotted name is public/proxied and the port is NOT added -
REM the same rule build.sh's ask_dev_url applies.
REM Parity with build.sh's "CUSTOM PORT + SUBDOMAIN" (ask_dev_url): ask for the
REM port and the ROOT_URL host, unless WEKAN_DEV_PORT / WEKAN_DEV_ROOT_URL /
REM WEKAN_DEV_HOST already say. A port that is not a number from 1 to 65535 is
REM 3000. Empty is http://localhost:PORT; a bare name is a subdomain of
REM localhost, http://NAME.localhost:PORT; a full URL is used as it is; a dotted
REM name is public/proxied, https://NAME, and the port is NOT added. A trailing
REM slash is removed, as it would double up in built URLs.
call :ensure_dirs
set "DEV_PORT="
set /p "DEV_PORT=Port for the dev server to listen on [3000]: "
set "DEV_PORT=%WEKAN_DEV_PORT%"
if not defined DEV_PORT set /p "DEV_PORT=Port for the dev server to listen on [3000]: "
if not defined DEV_PORT set "DEV_PORT=3000"
echo ROOT_URL: empty or a bare name = local ^(the port is added^);
echo a full URL or a dotted name = public/proxied ^(the port is NOT added^).
set "DEV_HOST="
set /p "DEV_HOST=ROOT_URL [http://localhost:%DEV_PORT%]: "
if not defined DEV_HOST set "DEV_HOST=localhost"
echo %DEV_PORT%| findstr /r /x "[0-9][0-9]*" >nul
if errorlevel 1 (
echo Not a number: '%DEV_PORT%' - using 3000.
set "DEV_PORT=3000"
)
set /a DEV_PORT_N=%DEV_PORT% 2>nul
if %DEV_PORT_N% LSS 1 (echo Port out of range: %DEV_PORT% - using 3000.& set "DEV_PORT=3000")
if %DEV_PORT_N% GTR 65535 (echo Port out of range: %DEV_PORT% - using 3000.& set "DEV_PORT=3000")
set "DEV_HOST=%WEKAN_DEV_ROOT_URL%"
if not defined DEV_HOST set "DEV_HOST=%WEKAN_DEV_HOST%"
if not defined DEV_HOST (
echo ROOT_URL: empty or a bare name = local ^(the port is added^);
echo a full URL or a dotted name = public/proxied ^(the port is NOT added^).
set /p "DEV_HOST=ROOT_URL [http://localhost:%DEV_PORT%]: "
)
if defined DEV_HOST if "!DEV_HOST:~-1!"=="/" set "DEV_HOST=!DEV_HOST:~0,-1!"
set "DEV_ROOT_URL="
echo %DEV_HOST% | findstr /r /c:"^^https*://" >nul && set "DEV_ROOT_URL=%DEV_HOST%"
if not defined DEV_ROOT_URL echo %DEV_HOST% | findstr /r /c:"\." >nul && set "DEV_ROOT_URL=https://%DEV_HOST%"
if not defined DEV_ROOT_URL set "DEV_ROOT_URL=http://%DEV_HOST%:%DEV_PORT%"
echo ROOT_URL=%DEV_ROOT_URL%
if not defined DEV_HOST set "DEV_ROOT_URL=http://localhost:%DEV_PORT%"
if not defined DEV_ROOT_URL echo !DEV_HOST!| findstr /r /c:"://" >nul && set "DEV_ROOT_URL=!DEV_HOST!"
if not defined DEV_ROOT_URL echo !DEV_HOST!| findstr /r /c:"\." >nul && set "DEV_ROOT_URL=https://!DEV_HOST!"
if not defined DEV_ROOT_URL set "DEV_ROOT_URL=http://!DEV_HOST!.localhost:%DEV_PORT%"
echo ==^> Meteor listens on port %DEV_PORT%
echo ==^> ROOT_URL=%DEV_ROOT_URL%
call :set_dev_env
set "ROOT_URL=%DEV_ROOT_URL%"
call :runlog --port %DEV_PORT%
@@ -1335,10 +1455,19 @@ call :tee test_mocha_body
goto end
:test_mocha_body
echo Running Mocha tests: meteor test --once --driver-package meteortesting:mocha --port 3100
echo Running Mocha tests: meteor test --full-app --once --driver-package meteortesting:mocha --port 3100
echo (server-side unit/security/API-logic tests; browser/client tests are covered by Playwright options)
call meteor test --once --driver-package meteortesting:mocha --port 3100
exit /b 0
REM The same run as build.sh: --full-app so the integration tests that need the
REM app's hooks run instead of skipping themselves, the receipt sweep fast enough
REM for its tests, and a build directory of its own so a running dev server's
REM .meteor\local is not shared. Its exit status is the run's, so a failing test
REM is not reported as success.
setlocal
set "EMAIL_RECEIPT_SWEEP_INTERVAL_MS=1000"
if not defined WRITABLE_PATH set "WRITABLE_PATH=.."
set "METEOR_LOCAL_DIR=.meteor\local-test"
call meteor test --full-app --once --driver-package meteortesting:mocha --port 3100
endlocal & exit /b %errorlevel%
:test_import
call :onelog import
@@ -1397,7 +1526,10 @@ if /i "%INSTALL_DEPS%"=="y" (
call meteor npm exec playwright install %PW_PROJECT%
)
call :onelog playwright-%PW_PROJECT%
call meteor npm exec playwright test -- --project=%PW_PROJECT% 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%ONELOG%'"
REM tests\playwright\playwright.config.js reads WEKAN_PLAYWRIGHT_PROJECT to run
REM only this browser's project, as build.sh run_playwright_single sets it.
set "WEKAN_PLAYWRIGHT_PROJECT=%PW_PROJECT%"
call meteor npm exec playwright test -- --project=%PW_PROJECT% 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%ONELOG%'; exit $LASTEXITCODE"
goto end
REM ===========================================================================
@@ -1464,17 +1596,22 @@ echo Ensuring .eslintrc.json includes @typescript-eslint plugin and no-floating-
node -e "const fs=require('fs');const p='.eslintrc.json';const c=JSON.parse(fs.readFileSync(p,'utf8'));c.plugins=Array.isArray(c.plugins)?c.plugins:[];if(!c.plugins.includes('@typescript-eslint'))c.plugins.push('@typescript-eslint');c.rules=c.rules||{};c.rules['@typescript-eslint/no-floating-promises']='error';fs.writeFileSync(p,JSON.stringify(c,null,2)+'\n');"
echo Checking whether @typescript-eslint/no-floating-promises is configured in .eslintrc.json
REM FAIL, not a warning, as in build.sh floating_promises_checks: a guard that
REM only warns lets an unawaited permission check through.
set "FLOATING_RC=0"
findstr /c:"@typescript-eslint/no-floating-promises" .eslintrc.json >nul 2>&1
if errorlevel 1 (
echo WARNING: Rule @typescript-eslint/no-floating-promises is NOT configured in .eslintrc.json
echo FAIL: Rule @typescript-eslint/no-floating-promises is NOT configured in .eslintrc.json
set "FLOATING_RC=1"
) else (
echo OK: Rule @typescript-eslint/no-floating-promises is configured in .eslintrc.json
)
echo.
echo Scanning for unawaited Authentication.checkBoardAccess/checkBoardWriteAccess in server\models
node -e "const fs=require('fs'),path=require('path');function walk(d,acc){for(const e of fs.readdirSync(d,{withFileTypes:true})){const fp=path.join(d,e.name);if(e.isDirectory())walk(fp,acc);else if(/\.js$/.test(e.name))acc.push(fp);}return acc;}let found=false;for(const f of walk('server/models',[])){const lines=fs.readFileSync(f,'utf8').split(/\r?\n/);lines.forEach((ln,i)=>{if(/Authentication\.checkBoard(Access|WriteAccess)\(/.test(ln)&&!/await Authentication\.checkBoard/.test(ln)){found=true;console.log(f+':'+(i+1)+': '+ln.trim());}});}console.log(found?'WARNING: Found possible unawaited board auth checks above':'OK: No unawaited board auth checks found');"
exit /b 0
node -e "const fs=require('fs'),path=require('path');function walk(d,acc){for(const e of fs.readdirSync(d,{withFileTypes:true})){const fp=path.join(d,e.name);if(e.isDirectory())walk(fp,acc);else if(/\.js$/.test(e.name))acc.push(fp);}return acc;}let found=false;for(const f of walk('server/models',[])){const lines=fs.readFileSync(f,'utf8').split(/\r?\n/);lines.forEach((ln,i)=>{if(/Authentication\.checkBoard(Access|WriteAccess)\(/.test(ln)&&!/await Authentication\.checkBoard/.test(ln)){found=true;console.log(f+':'+(i+1)+': '+ln.trim());}});}console.log(found?'FAIL: these board auth checks are not awaited':'OK: No unawaited board auth checks found');process.exit(found?1:0);"
if errorlevel 1 set "FLOATING_RC=1"
exit /b %FLOATING_RC%
REM ===========================================================================
:save_deps
@@ -1490,6 +1627,13 @@ for /f "usebackq delims=" %%P in (`powershell.exe -NoProfile -Command "[Environm
if not "%FORGE_INSTALL_STATUS%"=="0" echo One or more mirror tools could not be installed. See MISSING entries above.
exit /b %FORGE_INSTALL_STATUS%
REM ===========================================================================
:test_live
REM releases/live-tests/run.mjs runs every live test there (the GitLab one is
REM releases\live-tests\gitlab-write-back.mjs); each skips a service with no login.
node "%REPO%\releases\live-tests\run.mjs"
goto end
REM ===========================================================================
:mirror_forge
call "%REPO%\releases\mirror.bat"
@@ -1665,6 +1809,9 @@ REM Best-effort by image name; the per-port free below does the real work and is
REM narrow enough not to touch unrelated Node apps.
taskkill /F /IM meteor.exe /T >nul 2>&1
taskkill /F /IM mongod.exe /T >nul 2>&1
REM `rspack build --watch` listens on no port, so the per-port free below cannot
REM find it; stop it by its command line, as build.sh's pkill does.
powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'rspack build --watch' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }" >nul 2>&1
for %%p in (%DEV_SERVER_PORTS%) do call :free_port %%p
REM Wait for the ports to free (up to ~10s).
for /l %%i in (1,1,10) do (
@@ -1833,7 +1980,7 @@ netstat -ano | findstr /r /c:":3001 .*LISTENING" >nul 2>&1
if not errorlevel 1 (
echo ==^> Reusing the MongoDB already listening on :3001 ^(not started or stopped by this run^).
) else (
for %%i in ("%REPO%\..\mongodb-test-3001") do set "DBPATH=%%~fi"
for %%i in ("%REPO%\.tools\mongodb-test-3001") do set "DBPATH=%%~fi"
if not exist "!DBPATH!" md "!DBPATH!"
echo ==^> Starting MongoDB ^(Meteor's mongod^) on :3001, dbpath !DBPATH!.
start "WekanTestMongo" /MIN "%MONGOD_BIN%" --port 3001 --dbpath "!DBPATH!" --bind_ip 127.0.0.1 --logpath "%RUN_LOGDIR%\wekan-test-mongod.log"
@@ -1940,6 +2087,20 @@ if errorlevel 1 (
bash ./releases/db-conformance.sh
goto end
REM ===========================================================================
:test_db_speed
REM build.sh's "Database speed test": db-conformance.sh's databases, started the
REM same way, each loaded with the same dummy WeKan data and timed on the common
REM WeKan queries. See docs\Databases\FerretDB\1\SpeedTest\README.md.
where bash >nul 2>&1
if errorlevel 1 (
echo ERROR: bash was not found. It comes with Git for Windows ^(Git Bash^) and with WSL.
echo Install either, or run this from WSL: ./docs/Databases/FerretDB/1/SpeedTest/speedtest.sh
goto end
)
bash ./docs/Databases/FerretDB/1/SpeedTest/speedtest.sh
goto end
REM ===========================================================================
:test_ferretdb
REM All of FerretDB's own tests, one at a time: unit, vet, integration.
+143 -55
View File
@@ -103,6 +103,10 @@ WEKAN_TOOLS_DIR="$WEKAN_DIR/.tools"
export WEKAN_DIR WEKAN_TOOLS_DIR
export PATH="$WEKAN_TOOLS_DIR/bin:${GOBIN:+$GOBIN:}$PATH"
. "$WEKAN_DIR/releases/ensure-tools.sh"
# Everything this script and the releases/ scripts write - temporary files,
# downloads, Meteor, Node.js, the npm cache - stays in .tools/, on Linux, macOS
# and Windows alike (releases/tools-env.sh).
. "$WEKAN_DIR/releases/tools-env.sh"
# Prefer the repository-local toolchain installed under .tools. A fresh shell
# does not normally have .tools/.meteor on PATH, which made unattended test and
@@ -112,6 +116,8 @@ if [ -x "$WEKAN_TOOLS_DIR/.meteor/meteor" ]; then
PATH="$WEKAN_TOOLS_DIR/.meteor:$PATH"
fi
_wekan_node_version="$(sed -n 's/.*NODE_VERSION=v\([^ \\]*\).*/\1/p' "$WEKAN_DIR/Dockerfile" | head -1)"
# npm comes from the Dockerfile too (CLAUDE.md), never a number typed here.
_wekan_npm_version="$(sed -n 's/.*NPM_VERSION=\([^ \\]*\).*/\1/p' "$WEKAN_DIR/Dockerfile" | head -1)"
case "$(uname -m 2>/dev/null)" in
x86_64|amd64) _wekan_node_arch=x64 ;;
aarch64|arm64) _wekan_node_arch=arm64 ;;
@@ -462,10 +468,17 @@ function build_wekan(){
echo "===== wekan build started $(date '+%F %T') ====="
export npm_config_loglevel=verbose npm_config_foreground_scripts=true
export METEOR_PROFILE="${METEOR_PROFILE:-100}"
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
# Node module resolution, one line per require. OFF unless
# WEKAN_BUILD_TRACE_MODULES=1: a Meteor build writes it at gigabytes a
# minute - 1.1 GB in the first three minutes of a menu dev build, over
# 12 GB in the first half hour of EVERYTHING on macOS, written twice
# through tee - and slows the build it is tracing.
if [ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ]; then
export NODE_DEBUG="${NODE_DEBUG:+$NODE_DEBUG,}module"
fi
# NODE_OPTIONS parses quoted paths, including checkouts with spaces.
export NODE_OPTIONS="${NODE_OPTIONS:-} --require=\"$(pwd)/tools/build-command-output.cjs\""
echo "Build diagnostics: resolver command/output tracing, Node module resolution, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds)."
echo "Build diagnostics: resolver command/output tracing, npm verbose output, foreground install scripts, METEOR_PROFILE=$METEOR_PROFILE (milliseconds), Node module resolution $([ "${WEKAN_BUILD_TRACE_MODULES:-0}" = 1 ] && echo on || echo 'off (WEKAN_BUILD_TRACE_MODULES=1 turns it on)')."
build_stage "1/4 Remove dependencies and build caches" rm -rf node_modules node_modules/.cache .meteor/local .build _build || return $?
# Updating npm metadata has historically been best effort; installation is required.
build_stage "2/4 Compile app to resolve Meteor plugin npm dependencies" meteor update --npm || echo "WARNING: Meteor plugin npm dependency compilation failed; trying dependency installation."
@@ -639,8 +652,18 @@ function run_playwright_docker(){
local docker_base_url="${WEKAN_BASE_URL:-http://127.0.0.1:3000}"
local docker_mongo_url="${WEKAN_MONGO_URL:-mongodb://127.0.0.1:3001/meteor}"
local docker_host_proxy_port=""
local docker_network="host"
# Docker Desktop on macOS keeps the host outside the container's loopback.
if [ "$(uname -s)" = Darwin ]; then
# Its host network loses connections to host.docker.internal: from a
# container on that network about 1 connect in 75 never completed, so a
# test page waited 60 s for a document that never came (Firefox, 2026-11-01:
# 8 of 600 direct connects, against 0 of 1000 from the bridge network).
# Nothing here needs the host network on macOS - WeKan and Mongo are reached
# through host.docker.internal and SMTP through docker exec - so the
# container uses Docker's bridge network, where the proxy below still
# serves localhost.
docker_network="bridge"
# Meteor's dynamic imports use ROOT_URL (localhost in the test server).
# Keep the browser on that same origin and bridge its loopback to the host.
# Browsing host.docker.internal while ROOT_URL is localhost makes Firefox
@@ -664,35 +687,53 @@ function run_playwright_docker(){
mkdir -p "$filesroot"
echo "Running Playwright $browser in Docker ($image)."
echo "Expecting WeKan at $docker_base_url."
local smtp_proxy_pid="" smtp_proxy_ready="" container_name="wekan-playwright-${browser}-$$"
if [ "$(uname -s)" = Darwin ] && [ -n "${WEKAN_TEST_SMTP_PORT:-}" ]; then
smtp_proxy_ready="$(mktemp "$WEKAN_DIR/.tools/tmp/smtp-proxy.XXXXXX")"
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$WEKAN_TEST_SMTP_PORT" "$smtp_proxy_ready" &
smtp_proxy_pid=$!
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$smtp_proxy_ready" ] && break
kill -0 "$smtp_proxy_pid" 2>/dev/null || break
sleep 0.1
local container_name="wekan-playwright-${browser}-$$"
# Docker Desktop keeps the container's loopback from the host, so a server a
# spec starts in the container cannot be reached by WeKan outside it. Each
# port below is forwarded from the host into the container through docker
# exec (helpers/docker-smtp-proxy.cjs, which carries any TCP): the test SMTP
# server, and a small pool for the mock Jira/GitLab servers of the list-sync
# specs, which take a free port from WEKAN_TEST_REVERSE_PORTS
# (helpers/reachable-server.cjs). Elsewhere they listen on any port.
local forward_pids="" forward_ready="" reverse_ports=""
if [ "$(uname -s)" = Darwin ]; then
reverse_ports="${WEKAN_TEST_REVERSE_PORTS:-39101 39102 39103 39104}"
local fport fready fpid i
for fport in ${WEKAN_TEST_SMTP_PORT:-} $reverse_ports; do
fready="$(mktemp "$WEKAN_DIR/.tools/tmp/docker-port-proxy.XXXXXX")"
node "$pwdir/helpers/docker-smtp-proxy.cjs" "$container_name" "$fport" "$fready" &
fpid=$!
forward_pids="$forward_pids $fpid"; forward_ready="$forward_ready $fready"
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$fready" ] && break
kill -0 "$fpid" 2>/dev/null || break
sleep 0.1
done
if [ ! -s "$fready" ]; then
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
rm -f $forward_ready
echo "ERROR: local test proxy for port $fport did not start."
return 1
fi
done
if [ ! -s "$smtp_proxy_ready" ]; then
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
rm -f "$smtp_proxy_ready"
echo "ERROR: local SMTP test proxy did not start."
return 1
fi
fi
# Mount the whole repo so specs that reach the repo-root node_modules
# (e.g. @wekanteam/exceljs) and .tools resolve; run from tests/playwright.
# The repo and the files root are mounted at their OWN host paths, so a path
# names the same file for the specs in here and for the server outside:
# admin-continuous-backup gives the server a key file it wrote and reads the
# stream the server wrote, and with the repo at /repo neither side found the
# other's file.
# Run as the host user (--user) with a writable HOME so the container does
# not leave root-owned files under test-results/ (which would later make
# native Chromium/Firefox runs fail with "EACCES: permission denied, mkdir
# .../test-results/.playwright-artifacts-N").
docker_exec run --rm --init --ipc=host --network host \
docker_exec run --rm --init --ipc=host --network "$docker_network" \
--name "$container_name" \
--label org.wekan.test-run=everything \
--user "$(id -u):$(id -g)" \
-e HOME=/repo/.tools/tmp \
-e TMPDIR=/repo/.tools/tmp \
-e HOME="$reporoot/.tools/tmp" \
-e TMPDIR="$reporoot/.tools/tmp" \
-e WEKAN_BASE_URL="$docker_base_url" \
-e WEKAN_DOCKER_HOST_PROXY_PORT="$docker_host_proxy_port" \
-e WEKAN_MONGO_URL="$docker_mongo_url" \
@@ -704,16 +745,17 @@ function run_playwright_docker(){
-e WEKAN_TEST_SERIAL="${WEKAN_TEST_SERIAL:-0}" \
-e WEKAN_TEST_SERVER_MODE="${WEKAN_TEST_SERVER_MODE:-bundle}" \
-e WEKAN_TEST_SMTP_PORT="${WEKAN_TEST_SMTP_PORT:-}" \
-e WEKAN_TEST_REVERSE_PORTS="$reverse_ports" \
-e PLAYWRIGHT_HTML_OPEN=never \
-e WEKAN_FILES_PATH=/wekan-files \
-e WEKAN_FILES_PATH="$filesroot" \
-e PLAYWRIGHT_JSON_OUTPUT_NAME="${PLAYWRIGHT_JSON_OUTPUT_NAME:-}" \
-v "$filesroot":/wekan-files \
-v "$reporoot":/repo -w /repo/tests/playwright \
-v "$filesroot":"$filesroot" \
-v "$reporoot":"$reporoot" -w "$pwdir" \
"$image" \
sh -c '
export PATH=/repo/tests/playwright/node_modules/.bin:$PATH
export PATH="$PWD/node_modules/.bin:$PATH"
if [ -n "$WEKAN_DOCKER_HOST_PROXY_PORT" ]; then
node /repo/tests/playwright/helpers/docker-host-proxy.cjs &
node "$PWD/helpers/docker-host-proxy.cjs" &
proxy_pid=$!
trap '\''kill "$proxy_pid" 2>/dev/null || true; wait "$proxy_pid" 2>/dev/null || true'\'' EXIT
ready=0
@@ -726,9 +768,9 @@ function run_playwright_docker(){
npx playwright test --project="$0" "$@"
' "$browser" "$@"
local playwright_rc=$?
if [ -n "$smtp_proxy_pid" ]; then
kill "$smtp_proxy_pid" 2>/dev/null; wait "$smtp_proxy_pid" 2>/dev/null
rm -f "$smtp_proxy_ready"
if [ -n "$forward_pids" ]; then
kill $forward_pids 2>/dev/null; wait $forward_pids 2>/dev/null
rm -f $forward_ready
fi
return "$playwright_rc"
}
@@ -1214,7 +1256,7 @@ function run_all_tests(){
PW_FAILURES=""
TEST_SERVER_PID=""
MONGOD_PID=""
STATDIR="$(mktemp -d)"
STATDIR="$(mktemp -d "$TMPDIR/wekan-stat.XXXXXX")"
BPIDS=""
# STOP WHAT THIS RUN STARTED, on EVERY way out - not only the happy path.
@@ -1500,7 +1542,11 @@ function run_all_tests(){
echo "ERROR: nothing is listening on :$TEST_DB_PORT and no mongod was found (Meteor dev_bundle or PATH)."
echo " Meteor's bundled mongod is normally at <dev_bundle>/mongodb/bin/mongod."; rm -rf "$STATDIR"; return 1
fi
local DBPATH="../mongodb-test-$TEST_DB_PORT"
# In .tools/, where everything this script writes lives (CLAUDE.md;
# tests/toolsDirOnly.test.cjs). It was in the checkout's PARENT directory,
# which a sandboxed editor cannot write - the test database never
# started there.
local DBPATH="$WEKAN_DIR/.tools/mongodb-test-$TEST_DB_PORT"
mkdir -p "$DBPATH"
echo "==> Starting MongoDB (Meteor's mongod) on :$TEST_DB_PORT, dbpath $DBPATH."
{ echo "===== mongod :$TEST_DB_PORT - started $(date '+%Y-%m-%d %H:%M:%S %Z') ====="; exec "$MONGOD_BIN" --port "$TEST_DB_PORT" --dbpath "$DBPATH" --bind_ip 127.0.0.1 --nounixsocket; } > "$RUN_LOGDIR/wekan-test-mongod.log" 2>&1 &
@@ -1528,7 +1574,7 @@ function run_all_tests(){
export WEKAN_FILES_PATH_HOST="$WRITABLE_ABS/files"
# ...and the same path for a LOCAL playwright run, not only the Docker one.
# Only WEKAN_FILES_PATH_HOST was exported, which is read when the browsers run
# in a container (it is mounted there as /wekan-files). A local run fell back
# in a container (it is mounted there at the same path). A local run fell back
# to the spec's own default of .build/bundle/files, so a spec that writes a
# stored attachment wrote it OUTSIDE the root this server reads - and then
# tested what WeKan does with a file it does not own.
@@ -2387,8 +2433,8 @@ function git_fix_changelog_links(){
local script="$WEKAN_DIR/releases/fix-changelog-hashes.sh"
[ -f "$script" ] || return 1
bash "$script" || return 1
if [ -n "$(git status --porcelain -- CHANGELOG.md 2>/dev/null)" ]; then
git add CHANGELOG.md
if [ -n "$(git status --porcelain -- CHANGELOG-big.md CHANGELOG.md 2>/dev/null)" ]; then
git add CHANGELOG-big.md CHANGELOG.md
git commit -q -m "CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
@@ -2599,7 +2645,7 @@ choose() {
# snap/bundle, not on a maintainer's machine) and the superseded old-*.sh and
# translations/fill_translations.py.
# Release notes summarize translations as affected languages; other changelog
# details stay in CHANGELOG.md. Notes include only In short, Security,
# details stay in CHANGELOG-big.md. Notes include only In short, Security,
# Translations language list, thanks and the changelog link. Binary provenance remains
# a build artifact and must not be appended by any release entry point.
RELEASE_SCRIPTS=( "Release|Release All: audit, commit, push and build with Actions|releases/release-all.sh|||"
@@ -2651,6 +2697,7 @@ RELEASE_SCRIPTS=( "Release|Release All: audit, commit, push and build with Actio
"Bundles|Fetch the built bundle from the ppc64le build host|releases/up-o.sh|WeKan version, e.g. 10.50||"
"Bundles|Fetch the built bundle from the s390x build host|releases/up-s.sh|WeKan version, e.g. 10.50||"
"Bundles|Upload the Windows bundle to the download server|releases/up-w.sh|WeKan version, e.g. 10.50||"
"Docker images|Test Nextcloud 32-35 with native local services|tests/nextcloud/run.sh|Nextcloud majors (default 32 33 34 35)|linux|nextcloud-tests"
"Docker images|Build the WeKan Docker image|releases/docker-build.sh|||"
"Docker images|Create the multi-platform buildx builder|releases/docker-build-deps.sh|||"
"Docker images|Publish a variant image (wekan-gantt-gpl / wekan-ondra)|releases/docker-publish-variant.sh|Image and version, e.g. wekan-gantt-gpl 10.50||"
@@ -2913,7 +2960,9 @@ while [ -z "$opt" ]; do
"Floating-promises guard|Check floating promises guard (@typescript-eslint/no-floating-promises + auth await scan)" \
"Count tests by category|Count amount of tests by category" \
"Run all FerretDB tests - SEQUENTIAL|Run all FerretDB tests - SEQUENTIAL: unit, vet and the integration suite of the FerretDB subdirectory of this repo, one at a time, logs in log/<datetime>/" \
"All databases (sequential)|Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in log/<datetime>/)" ;;
"Live tests|Run live tests against real services with your own accounts: GitLab write-back with the mirror's glab login (only scratch items it creates and removes) and the API imports, read only (releases/live-tests/README.md); a service with no login here is skipped, log in .tools/log/<datetime>/" \
"All databases (sequential)|Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in log/<datetime>/)" \
"Database speed test|Time the common WeKan queries on every database that has a Docker image for this CPU, SEQUENTIALLY, with twice a production WeKan of dummy data; check they all answer the same and write docs/Databases/FerretDB/1/SpeedTest/Results.md" ;;
"Tools")
choose "Tools" \
"Save Meteor deps list|Save Meteor dependency chain to ../meteor-deps.txt" \
@@ -2967,12 +3016,35 @@ for _once in 1; do
;;
*) echo "Unsupported Linux distribution; install a C/C++ toolchain, git, curl, wget, 7zip, zip, unzip, file and npm." >&2; exit 1 ;;
esac
#sudo chown -R $(id -u):$(id -g) $HOME/.npm
sudo npm -g install n
sudo n "$_wekan_node_version"
sudo npm install -g npm@12.0.2
sudo npm -g install meteor --unsafe-perm
#sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
# Node.js into .tools/n with n's N_PREFIX, as this user: not into
# /usr/local with sudo. releases/tools-env.sh puts .tools/n/bin on
# PATH, and npm's global installs go to that same prefix.
N_PREFIX="$WEKAN_TOOLS_DIR/n" npx -y n "$_wekan_node_version"
export PATH="$WEKAN_TOOLS_DIR/n/bin:$PATH"
hash -r
npm install -g "npm@${_wekan_npm_version:-latest}"
hash -r
# Meteor the way https://docs.meteor.com/about/install.html installs
# it on Linux, macOS and Windows alike: `npx meteor`, as this user and
# never with sudo. The meteor npm package is only an installer; npx
# runs its cli.js, which installs into $HOME/.meteor and adds that to
# PATH in the shell rc files - so HOME is .tools for the installer
# only, and Meteor and those rc lines land in .tools/. `sudo npm -g install meteor` ran the
# same installer as an install script instead, which npm 12 refuses
# to run (and which also refused --unsafe-perm and the documented
# --foreground-script).
#
# The global meteor package such a setup left behind is only that
# installer, never Meteor itself (~/.meteor is not touched).
sudo npm -g uninstall meteor >/dev/null 2>&1 || true
# npx caches in npm_config_cache (.tools/npm-cache).
mkdir -p "$npm_config_cache"
if [ -x "$WEKAN_TOOLS_DIR/.meteor/meteor" ]; then
echo "Meteor is already installed in $WEKAN_TOOLS_DIR/.meteor"
else
HOME="$WEKAN_TOOLS_DIR" npx -y meteor
fi
. "$WEKAN_DIR/releases/tools-env.sh"
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "macOS"
# Node comes from nvm, not from Homebrew's node@26 keg.
@@ -2984,13 +3056,14 @@ for _once in 1; do
# puts that on PATH itself, so this does not go stale the way a
# pinned version does. npm comes with the Node it installs, so
# there is no `brew install npm` either.
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# NVM_DIR is .tools/nvm (releases/tools-env.sh).
mkdir -p "$NVM_DIR"
if [ ! -s "$NVM_DIR/nvm.sh" ]; then
echo "Installing nvm into $NVM_DIR ..."
# A PINNED tag, not master: this pipes a downloaded script into
# a shell, so it must be a revision somebody has looked at.
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.6/install.sh | bash
# PROFILE=/dev/null: the installer edits no shell rc file.
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.6/install.sh | PROFILE=/dev/null bash
fi
# nvm is a shell FUNCTION, not a binary, so it has to be sourced -
# `command -v nvm` finds nothing until this line has run.
@@ -3010,19 +3083,20 @@ for _once in 1; do
nvm install 26
nvm alias default 26
nvm use 26
npm install -g npm@12.0.2
npm install -g "npm@${_wekan_npm_version:-latest}"
echo "Node $(node --version), npm $(npm --version)"
# Let new shells find nvm too. Its installer appends these itself,
# but only to the rc file it detects and only when IT did the
# install - so a machine that already had nvm keeps working.
touch "$HOME/.zshrc"
for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do
[ -e "$rc" ] || continue
grep -qxF 'export NVM_DIR="$HOME/.nvm"' "$rc" || echo 'export NVM_DIR="$HOME/.nvm"' >> "$rc"
grep -qxF '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' "$rc" || echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> "$rc"
done
npx -y meteor
export PATH=~/.meteor:$PATH
# No shell rc file is edited: build.sh and the releases/ scripts put
# .tools/nvm's Node.js and .tools/.meteor on PATH themselves
# (releases/tools-env.sh).
# Meteor as https://docs.meteor.com/about/install.html installs it:
# `npx meteor`, as this user, never sudo, with HOME=.tools for the
# installer only (see the Linux branch).
if [ -x "$WEKAN_TOOLS_DIR/.meteor/meteor" ]; then
echo "Meteor is already installed in $WEKAN_TOOLS_DIR/.meteor"
else
HOME="$WEKAN_TOOLS_DIR" npx -y meteor
fi
. "$WEKAN_DIR/releases/tools-env.sh"
exit;
elif [[ "$OSTYPE" == "cygwin" ]]; then
# POSIX compatibility layer and Linux environment emulation for Windows
@@ -3303,6 +3377,14 @@ for _once in 1; do
break
;;
"Run live tests against real services with your own accounts: GitLab write-back with the mirror's glab login (only scratch items it creates and removes) and the API imports, read only (releases/live-tests/README.md); a service with no login here is skipped, log in .tools/log/<datetime>/")
# releases/live-tests/run.mjs: every live test there; each skips a
# service it has no login for. Human-run only - they write to the
# services (scratch items they remove), so no automatic run calls them.
node "$WEKAN_DIR/releases/live-tests/run.mjs"
break
;;
"Mirror GitHub to active mirrors: code, issues, PR conversations, releases and assets")
mirror_forge
break
@@ -3332,6 +3414,12 @@ for _once in 1; do
./releases/db-conformance.sh
;;
"Time the common WeKan queries on every database that has a Docker image for this CPU, SEQUENTIALLY, with twice a production WeKan of dummy data; check they all answer the same and write docs/Databases/FerretDB/1/SpeedTest/Results.md")
# docs/Databases/FerretDB/1/SpeedTest/README.md: db-conformance.sh's databases,
# the speed test's queries.
./docs/Databases/FerretDB/1/SpeedTest/speedtest.sh
;;
"Count amount of tests by category")
LOG="$(one_log test-counts)"
echo "Log: $LOG"
+26 -1
View File
@@ -62,7 +62,10 @@ try {
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
const swPath = new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname;
navigator.serviceWorker.register(swPath);
navigator.serviceWorker.register(swPath).catch(error => {
// No service worker (a private window, an old browser): the app works without the offline cache.
if (process.env.DEBUG === 'true') console.warn('service worker registration failed:', error);
});
});
}
@@ -111,6 +114,28 @@ Meteor.startup(() => {
}
});
// LOGOUT_IDLE_MINUTES: report the user's activity while signed in, if the
// limit is set (models/lib/idleLogout.js).
const { createIdleActivityReporter } = require('/client/lib/idleActivity');
const idleReporter = createIdleActivityReporter({
target: document,
touch: () => Meteor.call('touchActivity', () => {}),
});
let idleCheckedFor = null;
Tracker.autorun(() => {
const userId = Meteor.userId();
if (!userId) {
idleCheckedFor = null;
idleReporter.stop();
return;
}
if (idleCheckedFor === userId) return;
idleCheckedFor = userId;
Meteor.call('getIdleLogout', (error, result) => {
if (!error && result && result.enabled && Meteor.userId() === userId) idleReporter.start();
});
});
// Initialize mobile mode on startup for iOS devices
// This ensures mobile mode is applied correctly on page load
Tracker.afterFlush(() => {
+13 -5
View File
@@ -1,6 +1,11 @@
.new-comment {
position: relative;
margin: 0 0 20px 38px;
/* #6805: the room for the avatar is on the side the avatar is drawn - the
reading start (inset-inline-start below). A physical left margin kept it on
the left while a right-to-left page drew the avatar on the right, outside
the card. */
margin-block: 0 20px;
margin-inline: 38px 0;
}
.new-comment .member {
opacity: 0.7;
@@ -29,7 +34,8 @@
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
height: 36px;
margin: 4px 4px 6px 0;
margin-block: 4px 6px;
margin-inline: 0 4px;
padding: 9px 11px;
width: 100%;
}
@@ -47,9 +53,10 @@
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
height: 36px;
margin: 4px 4px 6px 0;
margin-block: 4px 6px;
margin-inline: 0 4px;
width: 92%;
}
.comment-item:hover {
@@ -161,7 +168,8 @@
background: #f4f4f4;
border-inline-start: 2px solid #ccc;
padding: 4px 6px;
margin: 0 4px 6px 0;
margin-block: 0 6px;
margin-inline: 0 4px;
font-size: 0.85em;
color: #666;
display: flex;
+9 -7
View File
@@ -15,7 +15,7 @@ template(name="commentForm")
a.js-cancel-reply(title="{{_ 'close' }}")
i.fa.fa-times-thin
form.js-new-comment-form
+editor(class="js-new-comment-input")
+editor(class="js-new-comment-input" labelKey="comment")
| {{getUnsavedValue 'cardComment' currentCard._id}}
.add-controls
button.primary.confirm.clear.js-add-comment(type="submit") {{_ 'comment'}}
@@ -40,7 +40,7 @@ template(name="comment")
= parentComment.text
+inlinedForm(classNames='js-edit-comment')
+editor(autofocus=true)
+editor(autofocus=true labelKey="comment")
= text
.edit-controls
button.primary(type="submit" title="{{_ 'edit'}}" aria-label="{{_ 'edit'}}")
@@ -66,6 +66,10 @@ template(name="comment")
if currentUser.isBoardMember
= ' - '
a.js-reply-comment {{_ "comment-reply"}}
//- Action items, Show on Card (docs/Features/Cards/Action-Items.md).
if actionItemsOf this
= ' - '
a.js-comment-action-items {{_ "action-items-create"}}
if canEditThisComment
+editOrDeleteComment
@@ -86,11 +90,9 @@ template(name="commentReactions")
span.reaction(class="{{#if isSelected reaction.userIds}}selected{{/if}}" data-codepoint="#{reaction.reactionCodepoint}" title="{{userNames reaction.userIds}}")
span.reaction-codepoint !{reaction.reactionCodepoint}
span.reaction-count #{reaction.userIds.length}
if (currentUser.isBoardMember)
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
i.fa.fa-smile-o(title="{{_ 'addReactionPopup-title' }}")
if canComment
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
i.fa.fa-smile-o(title="{{_ 'addReactionPopup-title' }}")
i.fa.fa-plus(title="{{_ 'add' }}")
template(name="addReactionPopup")
+2
View File
@@ -211,6 +211,8 @@ Template.comment.events({
'click .js-comment-permalink'() {
if (this._id) Session.set('revealCommentId', this._id);
},
// docs/Features/Cards/Action-Items.md
'click .js-comment-action-items': Popup.open('actionItems', { titleKey: 'action-items' }),
'click .js-copy-comment-link'(evt, tpl) {
evt.preventDefault();
const url = commentPermalinkFor(this);
@@ -7,7 +7,7 @@
//- the same `.sidebar-content` with a title and a back arrow above the view. So
//- it looks and behaves like the one on a board without pretending to be it.
//-
//- docs/Features/Page/Search.md, docs/Features/Page/Multi-Selection.md
//- docs/Features/Board-Header/Search.md, docs/Features/General/Multi-Selection.md
template(name="allBoardsSidebar")
//- The theme class goes on an ANCESTOR of `.sidebar`, never on it: every
@@ -42,7 +42,7 @@ template(name="allBoardsHomeSidebar")
what you are looking at, which belongs beside the page's name rather than
behind a panel you have to open. Search and Multi-Selection were in that bar
as well and are NOT repeated here: this view has offered both as rows since
it was written. docs/Features/Page/All-Boards.md
it was written. docs/Features/All-Boards/All-Boards.md
a.sidebar-btn.js-open-boards-sort(class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
i.fa.fa-sort
span {{_ 'sort-boards'}}
+6 -5
View File
@@ -25,10 +25,11 @@ import {
sidebarViewTemplate,
} from '/models/lib/allBoardsSidebar';
import { selectedStarTitleKey, selectedStarAction } from '/models/lib/selectedStars';
import { errorMessage } from '/client/lib/errorMessage';
// The All Boards right sidebar. Its shell mirrors the board sidebar's; what it
// shows is this page's own. docs/Features/Page/Search.md,
// docs/Features/Page/Multi-Selection.md
// shows is this page's own. docs/Features/Board-Header/Search.md,
// docs/Features/General/Multi-Selection.md
Template.allBoardsSidebar.helpers({
isSidebarOpen() {
@@ -99,7 +100,7 @@ Template.allBoardsHomeSidebar.events({
evt.preventDefault();
closeAllBoardsSidebar();
// The SECTION of this page, not the full-width page it replaced.
// docs/Features/Page/Archive.md
// docs/Features/All-Boards/Archive.md
FlowRouter.go(allBoardsPath(SECTION_ARCHIVE, []));
},
});
@@ -211,7 +212,7 @@ Template.allBoardsMultiSelectionSidebar.events({
if (!ids || !confirm(TAPi18n.__('delete-board-confirm-popup'))) return;
Meteor.call('permanentlyDeleteArchivedBoards', ids, (err) => {
if (err) {
alert(err.reason || err.message || 'Failed to permanently delete boards');
alert(errorMessage(err));
return;
}
BoardMultiSelection.reset();
@@ -243,7 +244,7 @@ Template.allBoardsMultiSelectionSidebar.events({
) {
selectedBoards.forEach((boardId) => {
Meteor.call('archiveBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
if (err) alert(errorMessage(err));
});
});
BoardMultiSelection.reset();
+1 -1
View File
@@ -3,7 +3,7 @@ import Boards from '/models/boards';
import { Utils } from '/client/lib/utils';
import { TABLE_PAGE_ROWS_PER_PAGE } from '/models/lib/tablePage';
// The app's one rows-per-page (docs/Features/Page/Table.md): the archive pages
// The app's one rows-per-page (docs/Features/General/Table.md): the archive pages
// like every other paginated page, whatever pager it draws.
const ARCHIVED_BOARDS_PER_PAGE = TABLE_PAGE_ROWS_PER_PAGE;
+12 -1
View File
@@ -12,6 +12,8 @@ template(name="board")
//-- XXX We need a better error message in case the board has been archived
+message(label="board-not-found")
//-- | {{goHome}}
else if boardLoadError
+message(label="board-load-failed")
else
+spinner
@@ -31,7 +33,8 @@ template(name="boardBody")
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive}}is-dragging-active{{/if}}"
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
class="{{#if isMiniScreen}}mobile-view{{/if}}")
class="{{#if isMiniScreen}}mobile-view{{/if}}"
class="{{#if currentBoard.hasSharedListHeaders}}shared-list-headers{{/if}}")
//- #6732: drawn when THIS user shows My or Board Dependencies.
if showAnyDependencies
+dependencyOverlay
@@ -111,6 +114,14 @@ template(name="boardBody")
+pulseView
else if isViewMap
+mapView
else if isViewEmail
+emailView
else if isViewChat
+chatView
else if isViewVersionControl
+versionControlView
else if isViewColumnSets
+columnSetsView
else
// Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
+67 -61
View File
@@ -1,3 +1,4 @@
import { markBoardVisited } from '/client/lib/boardVisits';
import { ReactiveCache } from '/imports/reactiveCache';
import { setSortableOptions } from '/client/lib/sortableOptions';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
@@ -16,6 +17,7 @@ import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { calendarDateDisplayOptions } from '/client/lib/dateDisplay';
import { toFullCalendarFirstDay } from '/client/lib/calendarFirstDay';
import { weekNumberByFirstDay } from '/models/lib/weekStart';
import { calendarBaseOptions } from '/models/lib/calendarBaseOptions';
import Swimlanes from '/models/swimlanes';
import Lists from '/models/lists';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
@@ -38,6 +40,10 @@ let BoardBody = null;
Template.board.onCreated(function () {
this.isBoardReady = new ReactiveVar(false);
// The board publication stopped with an error: say so instead of spinning
// forever (a board restored from an old WeKan whose data the publication
// could not read showed only the loading animation).
this.boardLoadError = new ReactiveVar(false);
// Kept for the template/jade which still references it; conversion is a no-op.
this.isConverting = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // boards where a default swimlane was ensured
@@ -231,6 +237,7 @@ Template.board.onCreated(function () {
if (this.subscriptionBoardId !== currentBoardId) {
this.subscriptionBoardId = currentBoardId;
this.isBoardReady.set(false);
this.boardLoadError.set(false);
}
if (!currentBoardId) return;
@@ -239,6 +246,14 @@ Template.board.onCreated(function () {
currentBoardId,
false,
subscriptionGeneration,
{
onStop: (error) => {
if (error && this.subscriptionBoardId === currentBoardId) {
console.error('[board] the board publication stopped:', error.reason || error.message || error.error);
this.boardLoadError.set(true);
}
},
},
);
// Learn this board's card-loading mode (lazy vs eager) so 'auto' can decide
// per board by size; the flag drives isLazyCards(boardId). #6480.
@@ -301,6 +316,9 @@ Template.board.helpers({
return Template.instance().isConverting.get();
},
boardLoadError() {
return Template.instance().boardLoadError.get();
},
isBoardReady() {
return Template.instance().isBoardReady.get();
},
@@ -323,6 +341,12 @@ Template.boardBody.onCreated(function () {
// Store global reference for external access (swimlanes, cardDetails, etc.)
BoardBody = this;
// Remember this visit, once per board per page load, for Board menu ->
// Since your last visit (client/lib/boardVisits.js).
this.autorun(() => {
markBoardVisited(Session.get('currentBoard'));
});
// Methods on the template instance for programmatic access
this.setIsDragging = (bool) => {
this.draggingActive.set(bool);
@@ -550,7 +574,7 @@ Template.boardBody.onRendered(function () {
outline: none;
}
/* Sidebar hamburger menu button in header. Renamed with the button when
it moved to the first top header bar: docs/Features/Page/Header.md */
it moved to the first top header bar: docs/Features/Header-Bar/Header.md */
.js-toggle-page-sidebar .fa-bars {
color: #fff !important;
}
@@ -943,6 +967,18 @@ Template.boardBody.helpers({
isViewMap() {
return Utils.boardView() === 'board-view-map';
},
isViewEmail() {
return Utils.boardView() === 'board-view-email';
},
isViewChat() {
return Utils.boardView() === 'board-view-chat';
},
isViewColumnSets() {
return Utils.boardView() === 'board-view-column-sets';
},
isViewVersionControl() {
return Utils.boardView() === 'board-view-version-control';
},
isViewPulse() {
return Utils.boardView() === 'board-view-pulse';
@@ -1203,82 +1239,34 @@ Template.calendarView.onRendered(function () {
Template.calendarView.helpers({
calendarOptions() {
const t = (key, fallback) => {
const translated = TAPi18n.__(key);
return translated && translated !== key ? translated : fallback;
};
// #5521: respect the user's "start day of week" setting instead of the
// FullCalendar locale default. getStartDayOfWeek() returns a zero-based day
// index (0=Sunday..6=Saturday), the same convention FullCalendar's firstDay
// uses; toFullCalendarFirstDay() validates it and falls back to Monday (1).
// #5521: getStartDayOfWeek() returns a zero-based day index
// (0=Sunday..6=Saturday), the convention FullCalendar's firstDay uses;
// toFullCalendarFirstDay() validates it and falls back to Monday (1).
const currentUser = ReactiveCache.getCurrentUser();
const firstDay = toFullCalendarFirstDay(
currentUser ? currentUser.getStartDayOfWeek() : 1,
);
const displayOptions = calendarDateDisplayOptions('calendar-view');
return {
id: 'calendar-view',
...displayOptions,
initialView: displayOptions.initialView || 'dayGridMonth',
firstDay,
...calendarBaseOptions({
id: 'calendar-view',
displayOptions: calendarDateDisplayOptions('calendar-view'),
firstDay,
weekNumberOf: weekNumberByFirstDay,
translate: key => TAPi18n.__(key),
locale: TAPi18n.getLanguage(),
rtl: TAPi18n.isRTL(),
}),
editable: true,
selectable: true,
weekNumbers: true,
// #4946: FullCalendar's default weekNumberCalculation is locale-based
// (Sunday-anchored for many locales), so the week-number column ignored
// the user's start-day-of-week setting even though `firstDay` moved the
// grid. Compute the number from the SAME firstDay so the label matches the
// way the week is laid out.
weekNumberCalculation: date => weekNumberByFirstDay(date, firstDay),
// Use non-localized AM/PM time format to avoid confusing notations like 上/下/中
// Use full 'am'/'pm' instead of single-letter 'a'/'p' for clarity
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
slotLabelFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
// #6686: Today/Previous/Next used to sit in their own group under the
// title, with the Day/Week/Month view toggles in a third, CENTER group
// that pushed everything onto a second row. All the buttons now sit
// together in ONE group on the right side of the title, which stays
// alone on the left - see calendarView.css for the title's vertical
// centering against that single row.
headerToolbar: {
left: 'title',
center: '',
right:
'today prev,next timeGridDay,listDay timeGridWeek,listWeek dayGridMonth,listMonth',
},
buttonIcons: false,
buttonText: {
prev: t('previous', 'Previous'),
next: t('next', 'Next'),
today: t('today', 'Today'),
day: t('day', 'Day'),
week: t('week', 'Week'),
month: t('month', 'Month'),
list: t('list', 'List'),
},
// height: 'parent', nope, doesn't work as the parent might be small
height: 'auto',
/* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
navLinks: true,
nowIndicator: true,
businessHours: {
// days of week. an array of zero-based day of week integers (0=Sunday)
daysOfWeek: [1, 2, 3, 4, 5], // Monday - Friday
start: '8:00',
end: '18:00',
},
locale: TAPi18n.getLanguage(),
direction: TAPi18n.isRTL() ? 'rtl' : 'ltr',
events(fetchInfo, callback) {
const currentBoard = Utils.getCurrentBoard();
// #5656: apply the active board Filter (member / assignee / due-date /
@@ -1354,6 +1342,24 @@ Template.calendarView.helpers({
'calendar-event-end',
);
});
// #1618: dated checklist items, as "card: item", opening their card.
// Their `ci:` ids never match a card, so dragging one is reverted.
const { datedChecklistItems } = require('/models/lib/checklistItemSchedule');
datedChecklistItems(
ReactiveCache.getChecklistItems({ boardId: currentBoard._id, dueAt: { $exists: true } }),
id => ReactiveCache.getCard(id), { from: fetchInfo.start, to: fetchInfo.end },
).forEach(item => {
events.push({
id: item.id,
title: item.title,
start: item.dueAt,
end: new Date(item.dueAt.getTime() + 36e5),
allDay: false,
editable: false,
url: FlowRouter.path('card', { boardId: currentBoard._id, slug: currentBoard.slug, cardId: item.cardId }),
className: `calendar-event-checklist-item${item.isFinished ? ' is-finished' : ''}`,
});
});
events.sort(function (first, second) {
return first.id > second.id ? 1 : -1;
});
File diff suppressed because it is too large. Load diff
-65
View File
@@ -642,71 +642,6 @@
transform: none !important;
}
/* Force mobile mode to override any media query styles */
@media screen and (min-width: 801px) {
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
}
.mobile-mode .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.mobile-mode .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-inline-start: none !important;
border-inline-end: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
inset-inline-start: auto !important;
inset-inline-end: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
}
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
.mobile-mode .board-header-btn i.fa + span {
display: none !important;
+29 -11
View File
@@ -11,7 +11,7 @@
//-
//- There is no desktop/mobile split any more either. The second bar wrote the
//- whole set out TWICE, once for each, because a labelled row does not fit on a
//- phone; icons do, so one set serves both. docs/Features/Page/Header.md
//- phone; icons do, so one set serves both. docs/Features/Header-Bar/Header.md
template(name="boardHeaderButtons")
if currentBoard
if currentUser
@@ -50,7 +50,7 @@ template(name="boardHeaderButtons")
//- The name beside the icon where the bar has room for it, dropped
when it does not - one rule in header.css hides every label of this
bar below the width they stop fitting at.
docs/Features/Page/Header.md
docs/Features/Header-Bar/Header.md
a.board-header-btn(title="{{#if isSortActive}}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}" class="js-sort-cards {{#if isSortActive }}emphasis{{/if}}")
i.fa.fa-sort
span.board-header-btn-label {{#if isSortActive}}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
@@ -126,6 +126,16 @@ template(name="boardVisibilityList")
template(name="boardChangeVisibilityPopup")
+boardVisibilityList
//- #4575: an open board can let every signed-in user edit it, where Admin
//- Panel / Settings / Visibility allows that.
if instanceEditingOffered
hr
ul.pop-over-list
li
a.flex.js-toggle-instance-editing(role="checkbox" aria-checked="{{#if currentBoardInstanceEditing}}true{{else}}false{{/if}}")
.materialCheckBox(class="{{#if currentBoardInstanceEditing}}is-checked{{/if}}")
span {{_ 'instance-editing'}}
p.quiet {{_ 'instance-editing-desc'}}
template(name="boardChangeWatchPopup")
ul.pop-over-list.described-list
@@ -161,7 +171,7 @@ template(name="boardChangeWatchPopup")
(models/lib/boardViewSettings.js boardViewMenuEntries), so the popup and
the menu can not drift apart. Each anchor keeps its per-view class
(js-open-<view>-view), which is what the click handlers in boardHeader.js
listen for. docs/Features/Board/Board-View-Settings.md
listen for. docs/Features/Board-Sidebar/Board-Settings/Board-View.md
template(name="boardChangeViewPopup")
ul.pop-over-list
each boardViewMenuEntries
@@ -261,13 +271,15 @@ template(name="boardChangeTitlePopup")
label
| {{_ 'description'}}
textarea.js-board-desc(dir="auto")= description
//- #2449/#2450: the board key card numbers show with (KEY-7).
label
| {{_ 'board-key'}}
input.js-board-key(type="text" value=key maxlength="10" autocapitalize="characters" spellcheck="false")
p.quiet {{_ 'board-key-desc'}}
if boardKeyError
p.error {{_ boardKeyError}}
input.primary.wide(type="submit" value="{{_ 'rename'}}")
template(name="boardCreateRulePopup")
p {{_ 'close-board-pop'}}
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
template(name="cardsSortPopup")
ul.pop-over-list
li
@@ -294,6 +306,12 @@ template(name="cardsSortPopup")
a.js-sort-votes
i.fa.fa-thumbs-o-up
| {{_ 'sort-by-votes'}}
hr
//- #2040: by the board's label order (edit a label to move it).
li
a.js-sort-label-order
i.fa.fa-tags
| {{_ 'sort-by-label-order'}}
//- The board's view menu, in the FIRST top header bar.
@@ -303,7 +321,7 @@ template(name="cardsSortPopup")
//- always on screen - the same place the All Boards Lists/Table menu is.
//-
//- `unless currentBoard.isTemplatesBoard`: there is nothing to look at six
//- ways on a templates board. docs/Features/Page/Header.md
//- ways on a templates board. docs/Features/Header-Bar/Header.md
//- Whether THIS board is starred, and the button that changes it. Its own
//- template because the first header bar places it beside the starred-boards
//- dropdown rather than among the board's other controls: the two are one pair -
@@ -311,7 +329,7 @@ template(name="cardsSortPopup")
//-
//- The outline star means not starred and the solid one means starred, so the
//- button shows its state rather than the action - the tooltip says the action.
//- docs/Features/Page/Header.md
//- docs/Features/Header-Bar/Header.md
template(name="boardStarButton")
if currentBoard
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
@@ -326,7 +344,7 @@ template(name="boardViewMenu")
icons are six glyphs to learn, and a tooltip is the one place a name
cannot be read without hovering - so the button says which view is on.
The bar wraps to a second row when it runs out of width, which is what
makes a word beside the icon affordable. docs/Features/Page/Header.md
makes a word beside the icon affordable. docs/Features/Header-Bar/Header.md
a.board-header-btn.js-toggle-board-view(title="{{boardViewName}}")
i.fa.fa-caret-down
if $eq boardView 'board-view-swimlanes'
+73 -8
View File
@@ -44,6 +44,16 @@ function setCardsSortBy(sortBy) {
} catch (e) {}
}
Template.boardChangeTitlePopup.onCreated(function () {
this.boardKeyError = new ReactiveVar('');
});
Template.boardChangeTitlePopup.helpers({
boardKeyError() {
return Template.instance().boardKeyError.get();
},
});
Template.boardChangeTitlePopup.events({
async submit(event, templateInstance) {
event.preventDefault();
@@ -58,7 +68,7 @@ Template.boardChangeTitlePopup.events({
if (newTitle) {
// The board this popup was opened FOR, when it was opened for one: the
// All Boards Table view opens this same popup from a row whose data
// context is that row's board (docs/Features/Page/All-Boards.md), and on
// context is that row's board (docs/Features/All-Boards/All-Boards.md), and on
// that page there is no "current board" at all. The board header opens it
// with the current board as its context, so that side is unchanged.
const context = Template.currentData();
@@ -67,6 +77,18 @@ Template.boardChangeTitlePopup.events({
? context
: Utils.getCurrentBoard();
if (board) {
// The key first: a key that is taken or not valid keeps the popup
// open with the reason, and nothing else is saved yet.
const keyInput = templateInstance.$('.js-board-key');
const newKey = keyInput.length ? String(keyInput.val() || '').trim().toUpperCase() : '';
if (newKey && newKey !== board.key) {
try {
await Meteor.callAsync('setBoardKey', board._id, newKey);
} catch (error) {
templateInstance.boardKeyError.set(error && error.error === 'board-key-taken' ? 'board-key-taken' : 'board-key-invalid');
return;
}
}
await board.rename(newTitle);
await board.setDescription(newDesc);
}
@@ -201,7 +223,7 @@ Template.boardHeaderButtons.events({
'click .js-watch-board': Popup.open('boardChangeWatch'),
// Boards in Archive is a SECTION of All Boards, not a page of its own: the
// row in its left menu. `/archive` is the full-width page that section
// replaced. docs/Features/Page/Archive.md
// replaced. docs/Features/All-Boards/Archive.md
'click .js-open-archived-board'() {
FlowRouter.go(allBoardsPath(SECTION_ARCHIVE, []));
},
@@ -266,7 +288,7 @@ Template.boardHeaderButtons.events({
},
});
// Board Settings / Board View (docs/Features/Board/Board-View-Settings.md):
// Board Settings / Board View (docs/Features/Board-Sidebar/Board-Settings/Board-View.md):
// a view the board admin un-ticked for this board's visibility (public or
// private) is not offered. Reads the board reactively, so switching the
// board Private <-> Public swaps the menu on the spot.
@@ -276,7 +298,7 @@ Template.boardChangeViewPopup.helpers({
boardViewMenuEntries() {
const board = Utils.getCurrentBoard();
// #6736: a view the instance disabled is not drawn at all.
return boardViewMenuEntries(board, Utils.boardView(), allowBoardView).map(entry => ({
return boardViewMenuEntries(board, Utils.boardView(), allowBoardView, Meteor.userId()).map(entry => ({
...entry,
icon: `fa ${entry.icon}`,
}));
@@ -434,6 +456,22 @@ Template.boardChangeViewPopup.events({
Utils.setBoardView('board-view-map');
Popup.back();
},
'click .js-open-email-view'() {
Utils.setBoardView('board-view-email');
Popup.back();
},
'click .js-open-chat-view'() {
Utils.setBoardView('board-view-chat');
Popup.back();
},
'click .js-open-version-control-view'() {
Utils.setBoardView('board-view-version-control');
Popup.back();
},
'click .js-open-column-sets-view'() {
Utils.setBoardView('board-view-column-sets');
Popup.back();
},
'click .js-open-pulse-view'() {
Utils.setBoardView('board-view-pulse');
Popup.back();
@@ -654,13 +692,16 @@ Template.boardVisibilityList.helpers({
// Settings / Visibility (Settings.customPrivateBoardDesc /
// customPublicBoardDesc); empty/unset falls back to the i18n default,
// byte-identical to before this setting existed.
// TAPi18n.__ reads `this` (its reactive language dependency), so it is
// passed bound: handed over bare it threw inside the helper and both
// descriptions rendered as an empty span in every language.
privateDesc() {
const setting = ReactiveCache.getCurrentSetting();
return visibilityDesc(setting?.customPrivateBoardDesc, TAPi18n.__, 'private-desc');
return visibilityDesc(setting?.customPrivateBoardDesc, key => TAPi18n.__(key), 'private-desc');
},
publicDesc() {
const setting = ReactiveCache.getCurrentSetting();
return visibilityDesc(setting?.customPublicBoardDesc, TAPi18n.__, 'public-desc');
return visibilityDesc(setting?.customPublicBoardDesc, key => TAPi18n.__(key), 'public-desc');
},
});
@@ -669,6 +710,15 @@ Template.boardChangeVisibilityPopup.onCreated(function () {
});
Template.boardChangeVisibilityPopup.helpers({
instanceEditingOffered() {
const board = Utils.getCurrentBoard();
return !!(board && ['instance', 'public'].includes(board.permission)
&& TableVisibilityModeSettings.findOne('tableVisibilityMode-allowInstanceEditing')?.booleanValue);
},
currentBoardInstanceEditing() {
const board = Utils.getCurrentBoard();
return !!(board && board.allowsInstanceEditing);
},
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
},
@@ -679,6 +729,11 @@ Template.boardChangeVisibilityPopup.helpers({
});
Template.boardChangeVisibilityPopup.events({
'click .js-toggle-instance-editing'(event) {
event.preventDefault();
const board = Utils.getCurrentBoard();
if (board) Boards.updateAsync(board._id, { $set: { allowsInstanceEditing: !board.allowsInstanceEditing } });
},
'click .js-select-visibility'() {
const currentBoard = Utils.getCurrentBoard();
const visibility = this;
@@ -850,6 +905,12 @@ Template.cardsSortPopup.events({
// so this is a marker only - client.components.lists.listBody's
// cardsWithLimit() recognizes it and re-sorts the rendered cards in JS
// instead of passing it through as a Mongo sort spec.
// #2040: a display order like votes - the cards' `sort` is not changed.
'click .js-sort-label-order'() {
setCardsSortBy({ labelOrder: 1 });
sortCardsBy.set(TAPi18n.__('sort-by-label-order'));
Popup.back();
},
'click .js-sort-votes'() {
const sortBy = {
votes: -1,
@@ -862,7 +923,7 @@ Template.cardsSortPopup.events({
// The board's view menu is its own template in the FIRST header bar now, so its
// handler is here rather than on the second bar that used to draw it.
// docs/Features/Page/Header.md
// docs/Features/Header-Bar/Header.md
Template.boardViewMenu.events({
'click .js-toggle-board-view': Popup.open('boardChangeView'),
});
@@ -872,7 +933,7 @@ Template.boardViewMenu.helpers({
return Utils.boardView();
},
// The tooltip: the name of the view that is ON. The button is icon only, so
// this is the only place the name appears. docs/Features/Page/Header.md
// this is the only place the name appears. docs/Features/Header-Bar/Header.md
boardViewName() {
const names = {
'board-view-swimlanes': 'swimlanes',
@@ -911,6 +972,10 @@ Template.boardViewMenu.helpers({
'board-view-process-behavior': 'board-view-process-behavior',
'board-view-size-cycle-time': 'board-view-size-cycle-time',
'board-view-map': 'board-view-map',
'board-view-email': 'board-view-email',
'board-view-chat': 'board-view-chat',
'board-view-version-control': 'board-view-version-control',
'board-view-column-sets': 'board-view-column-sets',
};
return TAPi18n.__(names[Utils.boardView()] || 'board-view');
},
+69 -23
View File
@@ -45,7 +45,7 @@
/* `auto`, not a fixed 260px: the menu carries its own width (below), so the
track is whatever the menu is - and folded, when the grid drops to a single
column, there is no second track left holding a gap open.
docs/Features/Page/Left-Menu.md */
docs/Features/All-Boards/Left-Menu.md */
grid-template-columns: auto 1fr;
/* Between the columns only. A `gap` is between tracks, not around them, so
this never insets the menu from the window edge - the padding that used to
@@ -64,12 +64,12 @@
min-height: calc(100vh - var(--wekan-header-height, 0px));
/* The width grip is positioned against the GRID: absolutely, so it is no grid
item and adds no track, and outside the menu, which scrolls its own
entries. docs/Features/Page/Left-Menu.md */
entries. docs/Features/All-Boards/Left-Menu.md */
position: relative;
}
/* The left menu is styled like the Admin Panel's (settingBody.css,
docs/Features/Page/Left-Menu.md): a PANEL with its own background, border and
docs/Features/All-Boards/Left-Menu.md): a PANEL with its own background, border and
rounded corners, not a column separated from the page by a hairline. WeKan
has one kind of left menu and it should look like one kind of left menu.
@@ -101,7 +101,7 @@
/* Flush on three sides, like the Admin Panel's: it starts at the window's
left edge, directly under the header, and runs to the bottom - so it has a
border and a corner only on the side that faces the page. A rounded corner
on a flush edge shows the grey behind it. docs/Features/Page/Left-Menu.md */
on a flush edge shows the grey behind it. docs/Features/All-Boards/Left-Menu.md */
border: 1px solid #f0f0f0;
border-inline-start: 0;
border-top: 0;
@@ -173,7 +173,7 @@
crossed it - which is the bug that had to be fixed in the Admin Panel's. */
.boards-left-menu .menu-item.active a,
.boards-left-menu .menu-item.active a:hover {
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
}
/* The Admin Panel puts the selected background on the LI, not the anchor, so
the fill reaches the row's full width. Here the anchor is what fills, so it
@@ -212,10 +212,10 @@
rather than being a grey box in a coloured page. `--theme-accent` is set by
the applied theme (boardColors.css); the fallback is the default header blue,
for the themes that predate the variable.
docs/Features/Board/Starred.md */
docs/Features/Pages/Starred.md */
.board-list .board-list-item-bookmark .board-list-item {
border: 4px solid #fff;
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
color: #fff;
/* Room at the end for the unstar button, which sits over the tile. */
padding-inline-end: 34px;
@@ -277,7 +277,7 @@
the board area so it cannot be missed, and turns red under the icon. Red at
REST would be a standing warning about a board nobody is touching; red on
hover is the answer to "what happens if I let go here".
docs/Features/Board/Home.md */
docs/Features/All-Boards/Home.md */
.home-remove-target {
display: flex;
align-items: center;
@@ -312,7 +312,7 @@
/* An empty Home section: the row exists before there is a board at it, so the
pane says what to drag rather than showing nothing at all. Sized and spaced
like a board tile so it sits where the first tile would.
docs/Features/Board/Home.md */
docs/Features/All-Boards/Home.md */
/* Home's "Drag only one board here to open it after login" placeholder.
It stands where a board tile will be, so it is the SIZE of a board tile:
114px like `.board-list .board-list-item` and `.js-add-board .label`, with
@@ -377,7 +377,7 @@
background: #888;
/* The rows' own inline margin, so the rule lines up with them instead of
running past their ends - the same `4px` the Admin Panel's separator uses.
docs/Features/Page/Left-Menu.md */
docs/Features/All-Boards/Left-Menu.md */
margin: 8px 4px;
}
@@ -417,7 +417,7 @@
whatever has been dragged into whatever.
`padding-inline-start`, so a right-to-left language indents from the right by
itself rather than needing a second rule kept in step.
docs/Features/Page/Workspaces.md */
docs/Features/All-Boards/Workspaces.md */
.workspace-tree {
list-style: none;
padding-inline-start: 10px;
@@ -466,7 +466,7 @@
drop-inside - the row lights up and the slot opens INDENTED under it, which
is where the workspace will appear: as its child.
docs/Features/Page/Workspaces.md */
docs/Features/All-Boards/Workspaces.md */
.workspace-node.drop-before > .workspace-node-content,
.workspace-node.drop-after > .workspace-node-content,
.workspace-node.drop-inside > .workspace-node-content {
@@ -481,7 +481,7 @@
content: '';
display: block;
height: 30px;
border: 2px dashed var(--theme-accent, #2980b9);
border: 2px dashed var(--theme-accent, #2573a7);
border-radius: 4px;
background: rgba(41, 128, 185, 0.08);
margin: 2px 0;
@@ -567,7 +567,7 @@
The cursor stays `pointer`, not `grab`: clicking this row is what opens the
workspace, and that is what a reader does with it nearly every time. Dragging
is the second gesture on the same element, exactly as it is on a board tile
with handles off. docs/Features/Page/All-Boards.md */
with handles off. docs/Features/All-Boards/All-Boards.md */
.workspace-node .js-select-space[draggable="true"] {
user-select: none;
-webkit-user-select: none;
@@ -603,7 +603,7 @@
`flex: 1`, and without this a long workspace name shrinks its two neighbours
instead of ellipsing itself, which pushes the count off the row that is
supposed to be showing it. The phone rules said this already; the desktop
ones had not. docs/Features/Page/All-Boards.md */
ones had not. docs/Features/All-Boards/All-Boards.md */
.workspace-node .workspace-count {
background: #ddd;
padding: 2px 6px;
@@ -694,7 +694,7 @@
as the pointer crossed it. */
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node.active > .workspace-node-content:hover .js-select-space {
background: var(--theme-accent, #2980b9);
background: var(--theme-accent, #2573a7);
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node.active > .workspace-node-content .js-select-space i {
@@ -714,7 +714,7 @@
which boards are in it.
So it keeps the same grey pill every other row has, and the rule that made it
white is gone rather than re-tinted: there is no accent behind it to contrast
with. docs/Features/Page/All-Boards.md */
with. docs/Features/All-Boards/All-Boards.md */
.boards-right-grid {
min-height: 200px;
@@ -735,7 +735,7 @@
out of the grid, but the TRACK it was placed in stays, so the boards would
have been left in the `auto` column with the 1fr one empty beside them. One
column, and the menu hidden by the same rule.
docs/Features/Page/Left-Menu.md */
docs/Features/All-Boards/Left-Menu.md */
.boards-layout.collapsed {
/* `!important` because the phone rules below set the two tracks with an
`!important` of their own (#6488/#6523, so a narrow screen keeps the menu
@@ -758,7 +758,7 @@
this the title would sit directly on top of the first row of board tiles.
The board tiles carry a 7px margin of their own, so 12px of margin here plus
that 7px is the same gap the Admin Panel's 12px gives above a flush pane.
docs/Features/Page/All-Boards.md */
docs/Features/All-Boards/All-Boards.md */
.boards-right-grid > .admin-pane-title {
margin-bottom: 12px;
/* The pager and the tiles below start at the tiles' own 7px inset, so the
@@ -1046,7 +1046,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
background-color: #767676; /* Grey with white text: 4.54, above the 4.5 of WCAG 1.4.3 (#999 gave 2.84) */
border-radius: 3px;
/* Same total vertical padding as .board-list-item (42px -> same tile height),
but shifted up (top 24->18, bottom 18->24) so the "+ Add Board" text sits
@@ -1661,7 +1661,7 @@
.js-board {
display: block;
background-color: #999; /* Default gray background if no color class is applied */
background-color: #767676; /* Default gray background if no color class is applied */
border-radius: 3px; /* Rounded corners for board items */
overflow: hidden; /* Ensure children respect rounded corners */
margin: 0; /* grid gap spaces the tiles now (#6523) */
@@ -1724,7 +1724,7 @@
chose, not a colour, and painting over it would be losing something rather
than calming it down. */
body.has-theme-board-tiles .board-list > li.js-board {
background: var(--theme-accent-fill, var(--theme-accent, #2980b9));
background: var(--theme-accent-fill, var(--theme-accent, #2573a7));
}
body.has-theme-board-tiles .board-list > li.js-board .board-list-item:not(.has-background-image) {
background-color: transparent;
@@ -2232,7 +2232,7 @@ body.has-theme-board-tiles .board-list > li.js-board .board-list-card-count {
}
/* All Boards: the Table view.
* Design: docs/Features/Page/All-Boards.md, which is docs/Features/Page/Table.md.
* Design: docs/Features/All-Boards/All-Boards.md, which is docs/Features/General/Table-Page.md.
* Only what is particular to these rows; the table itself is tablePage.css. */
.all-boards-row {
cursor: default;
@@ -2258,3 +2258,49 @@ body.has-theme-board-tiles .board-list > li.js-board .board-list-card-count {
white-space: normal;
word-break: break-word;
}
/* #4791: the All Boards facet bar. */
.board-facets {
display: flex;
flex-direction: column;
gap: 0.3em;
margin: 0.4em 0 0.6em;
}
.board-facet-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.3em;
}
.board-facet {
display: inline-flex;
align-items: center;
gap: 0.3em;
padding: 0.15em 0.6em;
border-radius: 1em;
background: rgba(0, 0, 0, 0.06);
cursor: pointer;
}
.board-facet.is-active {
background: #2b6cb0;
color: #fff;
}
.board-facet-count {
font-size: 0.8em;
opacity: 0.75;
}
.board-label-apply {
display: inline-flex;
flex-wrap: wrap;
gap: 0.3em;
}
.board-label-apply input {
width: auto;
margin: 0;
}
/* #802: an organization or team tab's members. */
.group-members-panel { margin: 0.4em 0 0.8em; padding: 0.5em; background: rgba(0, 0, 0, 0.04); border-radius: 4px; }
.group-members { display: flex; flex-wrap: wrap; gap: 0.3em 1em; list-style: none; padding: 0; }
.group-members-panel form { display: inline-flex; gap: 0.3em; }
.group-members-panel input { width: auto; margin: 0; }
+96 -14
View File
@@ -10,7 +10,7 @@ template(name="boardList")
a 260px-wide nothing beside them. The grid drops to one column and hides
the menu in the same rule. The Admin Panel marks its menu instead, because
a flex row with a hidden item simply closes up.
docs/Features/Page/Left-Menu.md
docs/Features/All-Boards/Left-Menu.md
.boards-layout(class="{{#if isLeftMenuCollapsed}}collapsed{{/if}}")
// Left menu
.boards-left-menu
@@ -18,7 +18,7 @@ template(name="boardList")
menus cannot end up with two different carets, and the same control a
list has on a board. Folded, the menu is not drawn at all and the
caret that brings it back rides on the pane title below.
docs/Features/Page/Left-Menu.md
docs/Features/All-Boards/Left-Menu.md
+leftMenuCollapse
ul.menu
//- ONE row, rendered four times, rather than four copies of the same
@@ -66,6 +66,17 @@ template(name="boardList")
// Workspaces tree
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
hr.boards-menu-divider
//- #802: a tab per organization and team the member is in: its boards,
and its members, managed there by its admins.
if myGroups.length
ul.menu.group-tabs
each myGroups
li(class="menu-item {{#if isSelectedGroup}}active{{/if}}")
a.js-select-group(data-kind=kind data-id=id)
span.menu-label
i.fa(class="{{#if $eq kind 'org'}}fa-sitemap{{else}}fa-users{{/if}}")
| {{name}}
hr.boards-menu-divider
// Existing filter by orgs/teams (kept)
ul.AllBoardTeamsOrgs
@@ -99,21 +110,21 @@ template(name="boardList")
child of the GRID and not of the menu: it is positioned absolutely, so it
is no grid item and adds no track, and it does not scroll away with a
long list of workspaces the way a handle inside the menu would.
docs/Features/Page/Left-Menu.md
docs/Features/All-Boards/Left-Menu.md
+leftMenuResize
// Right boards grid
//- There is NO bar above the boards, and no second header bar either.
Every control of this page - Sort, Search, Multi-Selection, the view
menu and the actions on a selection - is in the right sidebar, and the
page is named by the FIRST header bar. docs/Features/Page/All-Boards.md
page is named by the FIRST header bar. docs/Features/All-Boards/All-Boards.md
.boards-right-grid
//- The heading of the pane: which list of boards this is. It sits above
the view branch, so it is drawn ONCE whichever view is on - the board
icons and the Table are two ways of showing the same section, not two
sections.
The Admin Panel's own `paneTitle`, so the two pages have one heading
at one size and colour. docs/Features/Page/All-Boards.md
at one size and colour. docs/Features/All-Boards/All-Boards.md
+paneTitle(allBoardsPaneTitle)
if showsBoardSelectionControls
.board-multiselection-controls
@@ -124,7 +135,7 @@ template(name="boardList")
board ICONS every other section uses. It had a table of its own, which
meant the one place you go to bring a board back was the one place
multi-selection, dragging and the tile layout did not work.
docs/Features/Page/Archive.md
docs/Features/All-Boards/Archive.md
if isAllBoardsView 'table'
//- The create action is supplied to tablePage with its other controls,
rather than occupying an otherwise empty row of its own. Archive and
@@ -138,13 +149,77 @@ template(name="boardList")
span.boards-page-info {{boardsCurrentPage}} / {{boardsTotalPages}}
button.js-boards-next-page(class="{{#if hasBoardsNextPage}}{{else}}disabled{{/if}}")
| &gt;
//- #802: the selected organization or team - who is in it; its admins
add and remove members here. A directory-synced group is read-only.
if selectedGroup
.group-members-panel
h3
i.fa(class="{{#if $eq selectedGroup.kind 'org'}}fa-sitemap{{else}}fa-users{{/if}}")
| {{selectedGroup.name}}
ul.group-members
each groupMembers
li
| {{username}}
if fullname
span.quiet {{fullname}}
if isAdmin
span.quiet ({{_ 'admin'}})
if canManageGroup
a.js-remove-group-member(data-user-id=_id title="{{_ 'group-member-remove'}}")
i.fa.fa-times
if canManageGroup
form.js-add-group-member
input.js-group-member-username(type="text" placeholder="{{_ 'username'}}" aria-label="{{_ 'username'}}")
button.js-add-group-member-button(type="submit") {{_ 'group-member-add'}}
else
p.quiet {{_ 'group-members-read-only'}}
//- #4791: All Boards organized automatically - by first character,
organization, team and the member's own board labels, each with
its count; a click shows that group only.
.board-facets
if hasBoardFacets
.board-facet-row
each boardFacets.chars
a.board-facet.js-board-facet(data-type="char" data-value=key class="{{#if isFacet 'char'}}is-active{{/if}}")
| {{key}}
span.board-facet-count {{count}}
if boardFacets.orgs.length
.board-facet-row
i.fa.fa-sitemap
each boardFacets.orgs
a.board-facet.js-board-facet(data-type="org" data-value=key class="{{#if isFacet 'org'}}is-active{{/if}}")
| {{name}}
span.board-facet-count {{count}}
if boardFacets.teams.length
.board-facet-row
i.fa.fa-users
each boardFacets.teams
a.board-facet.js-board-facet(data-type="team" data-value=key class="{{#if isFacet 'team'}}is-active{{/if}}")
| {{name}}
span.board-facet-count {{count}}
.board-facet-row
i.fa.fa-tag
each boardFacets.labels
a.board-facet.js-board-facet(data-type="label" data-value=key class="{{#if isFacet 'label'}}is-active{{/if}}")
| {{key}}
span.board-facet-count {{count}}
span.js-remove-board-label(data-value=key title="{{_ 'board-label-remove'}}")
i.fa.fa-times
form.board-label-apply.js-apply-board-label
input.js-board-label-name(type="text" maxlength="40" placeholder="{{_ 'board-label-name'}}" aria-label="{{_ 'board-label-name'}}")
input.js-board-label-text(type="text" placeholder="{{_ 'board-label-name-includes'}}" aria-label="{{_ 'board-label-name-includes'}}")
button.js-apply-board-label-button(type="submit") {{_ 'board-label-apply'}}
if hasBoardFacet
a.js-board-facet-clear
i.fa.fa-times
| {{_ 'board-facet-clear'}}
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
//- The Remove bar, drawn only while a board from Home is actually
being dragged - the launcher's, which appears at the top of the
screen when an icon is picked up and is not there the rest of the
time. Dropping a board here takes it off Home; the board itself
stays where it lives, the way an app stays in the drawer.
docs/Features/Board/Home.md
docs/Features/All-Boards/Home.md
if showsHomeRemoveTarget
li.home-remove-target.js-home-remove
i.fa.fa-trash
@@ -183,7 +258,7 @@ template(name="boardList")
and the URL says what it is, which is what a bookmark bar shows.
Dragging one past another reorders them, and that order is the
order of the header dropdown - the two are views of one array.
docs/Features/Board/Starred.md
docs/Features/Pages/Starred.md
if isSelectedMenu 'starred'
each starredPages
li.js-bookmark.nodragscroll.board-list-item-bookmark(data-url="{{url}}", draggable="true")
@@ -326,6 +401,13 @@ template(name="boardList")
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
//- #4265: a board this user reaches only through a team,
organization or e-mail domain can be left: it is hidden
for them, and Member Settings / Hidden boards restores it.
if canHideBoard
a.js-hide-board(title="{{_ 'hide-board'}}")
span.emoji-icon
i.fa.fa-eye-slash
//- #2220 Home-board toggle is disabled: the icon and the
//- set-default-board action are intentionally not rendered.
@@ -425,7 +507,7 @@ template(name="workspaceTree")
NOT the whole row either: the row also holds the ⋯ menu and the board
count, and a drag started on those is a drag of something else.
`dragstart` bubbles, so the handler stays on the row in both cases.
docs/Features/Page/All-Boards.md
docs/Features/All-Boards/All-Boards.md
li.workspace-node(class="{{#if $eq id ../selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}")
.workspace-node-content
//- The caret that folds this workspace's own workspaces away, at
@@ -436,7 +518,7 @@ template(name="workspaceTree")
A workspace with no workspaces under it has nothing to fold, so it
gets a spacer of the same width instead: a row that shifted
sideways when it gained a child would be a moving target.
docs/Features/Page/Workspaces.md
docs/Features/All-Boards/Workspaces.md
if workspaceHasChildren
a.workspace-collapse-indicator.js-collapse-workspace(
title="{{workspaceCollapseLabel}}"
@@ -479,7 +561,7 @@ template(name="workspaceTree")
`..` counts block levels, and a second block between the `each` and
this inclusion is another level for `../selectedWorkspaceId` to
climb - which is how the highlight was lost once already.
docs/Features/Page/Workspaces.md
docs/Features/All-Boards/Workspaces.md
if workspaceShowsChildren
+workspaceTree(nodes=children selectedWorkspaceId=../selectedWorkspaceId)
@@ -520,7 +602,7 @@ template(name="workspaceActionsPopup")
//-
//- It names the CURRENT view, not itself: the board header says "Swimlanes" or
//- "Lists" rather than "Board View", and this matches it.
//- docs/Features/Page/Header.md
//- docs/Features/Header-Bar/Header.md
//- The page's controls, as icons, in the first top header bar - left of the
//- bell, exactly where a board's own controls are. They were rows inside the
//- right sidebar's home view, which meant two clicks and a panel covering the
@@ -530,7 +612,7 @@ template(name="workspaceActionsPopup")
//-
//- Each names itself beside its icon where the bar has room, and the one rule
//- in header.css drops every label below 1100px, leaving the tooltips.
//- docs/Features/Page/All-Boards.md
//- docs/Features/All-Boards/All-Boards.md
template(name="allBoardsHeaderButtons")
a.board-header-btn.js-open-boards-sort(title="{{_ 'sort-boards'}}" class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
i.fa.fa-sort
@@ -549,7 +631,7 @@ template(name="allBoardsHeaderButtons")
board's own Multi-Selection has. It looked identical whether or not a
selection was running, so the one control that changes what a click on a
board does gave no sign it had changed it, and the only way out was to
find the row inside the sidebar. docs/Features/Page/Multi-Selection.md
find the row inside the sidebar. docs/Features/General/Multi-Selection.md
a.board-header-btn.js-all-boards-sidebar-multiselection(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
+173 -33
View File
@@ -10,7 +10,7 @@ import getSlug from 'limax';
// The archived-at line on a tile in the Archive, in the reader's own format.
import { boardCreationAllowed } from '/client/lib/boardCreationAllowed';
// The All Boards URLs, and the slug path of a workspace in the tree.
// docs/Features/Page/All-Boards-URLs.md
// docs/Features/All-Boards/URLs.md
import {
ALL_BOARDS_SECTIONS,
defaultSection,
@@ -43,7 +43,7 @@ import { Utils } from '/client/lib/utils';
import '/client/lib/dragDropTouch'; // touch -> HTML5 DnD so board icons drag by finger
// What a drag does to the workspaces tree - move before, after, or INTO another
// workspace - as pure functions, so the rules are testable without a browser.
// docs/Features/Page/Workspaces.md
// docs/Features/All-Boards/Workspaces.md
const {
BEFORE: DROP_BEFORE,
AFTER: DROP_AFTER,
@@ -59,7 +59,7 @@ import {
} from '/models/lib/boardSortReorder';
// The sidebar the Search and Multi-Selection controls open. Its state is module
// scope, not a template instance: this bar and the sidebar are separate Blaze
// instances. docs/Features/Page/Search.md, docs/Features/Page/Multi-Selection.md
// instances. docs/Features/Board-Header/Search.md, docs/Features/General/Multi-Selection.md
import {
openAllBoardsSidebar,
closeAllBoardsSidebar,
@@ -70,6 +70,7 @@ import {
SIDEBAR_SEARCH,
SIDEBAR_MULTISELECTION,
} from '/models/lib/allBoardsSidebar';
import { errorMessage } from '/client/lib/errorMessage';
// SubsManager removed for Meteor 3 migration
@@ -228,7 +229,7 @@ function hasStarredBoards() {
// board has always been stored in (#2220), which is what "opened after login"
// reads on its way past. Home is a VIEW of that one field, not a second copy of
// it, so setting a Home board from the menu and setting it from Multi-Selection
// cannot disagree. docs/Features/Board/Home.md
// cannot disagree. docs/Features/All-Boards/Home.md
function homeBoardId() {
const user = ReactiveCache.getCurrentUser();
return (user && user.profile && user.profile.defaultBoardId) || null;
@@ -245,7 +246,7 @@ function homeBoardId() {
// dragover is where it matters. Dragging a board out of Home may only end at
// the Trash - the one gesture that takes it off Home - so every other target
// has to REFUSE the drop while it is still in the air, which means answering
// before the drop happens. docs/Features/Board/Home.md
// before the drop happens. docs/Features/All-Boards/Home.md
const DRAG_FROM_HOME = 'application/x-board-from-home';
const DRAG_FROM_REMAINING = 'application/x-board-from-remaining';
const DRAG_FROM_WORKSPACE = 'application/x-board-from-workspace';
@@ -254,7 +255,7 @@ const ARCHIVED_MULTI_BOARD_DRAG = 'application/x-archived-board-multi';
// Reordering a bookmark carries its own type, so a bookmark and a board cannot
// be dropped on each other. Readable in `dragover` for the same reason
// DRAG_FROM_HOME is - a target has to decide whether to accept the drop before
// the drop happens. docs/Features/Board/Starred.md
// the drop happens. docs/Features/Pages/Starred.md
const BOOKMARK_DRAG = 'application/x-wekan-bookmark';
function isBookmarkDrag(evt) {
@@ -380,7 +381,7 @@ Template.boardList.helpers({
},
// Whether to move the boards left, out from under the right sidebar. The
// sidebar is a separate Blaze instance, so the state is module scope - the
// page cannot read a ReactiveVar on it. docs/Features/Page/Search.md
// page cannot read a ReactiveVar on it. docs/Features/Board-Header/Search.md
isSidebarOpen() {
return isAllBoardsSidebarOpen();
},
@@ -401,11 +402,11 @@ Template.boardList.helpers({
// map catches events from the templates rendered inside it, so these fire for
// this bar's copy and the board header's map fires for its own. That is what
// lets one piece of markup mean "search cards" on a board and "search boards"
// here. docs/Features/Page/Search.md, docs/Features/Page/Multi-Selection.md
// here. docs/Features/Board-Header/Search.md, docs/Features/General/Multi-Selection.md
// Sort and the view menu, which were buttons in this page's second header bar
// and are rows of the sidebar's home view now. A Blaze event map only sees
// events inside its OWN template, so they had to move with their markup.
// docs/Features/Page/All-Boards.md
// docs/Features/All-Boards/All-Boards.md
Template.allBoardsHomeSidebar.events({
// Titled "Sort Boards", from the key the app already has for that phrase -
// the same reasoning as the starred-boards popup: a `boardsSortPopup-title`
@@ -433,7 +434,7 @@ Template.allBoardsHomeSidebar.helpers({
// A Blaze event map only sees events inside its OWN template, so these are
// their own map rather than shared with the sidebar's: the same
// `js-all-boards-sidebar-search` markup exists in both places and each map
// fires for its own copy. docs/Features/Page/All-Boards.md
// fires for its own copy. docs/Features/All-Boards/All-Boards.md
Template.allBoardsHeaderButtons.helpers({
// Every template registers the helpers IT uses. `BoardMultiSelection` is also
// a helper of boardList, but a Blaze template cannot see a sibling's helpers -
@@ -495,7 +496,7 @@ Template.boardList.events({});
// Put the selected menu entry in the address bar. A section is its own name; a
// workspace is the slugs of its names down the tree, so the URL says where you
// are rather than carrying a random id. `go`, not `replace`, so Back returns to
// the previous entry. docs/Features/Page/All-Boards-URLs.md
// the previous entry. docs/Features/All-Boards/URLs.md
function goToAllBoards(tpl, menuValue) {
const path = allBoardsPathForMenu(menuValue, tpl.workspacesTreeVar.get(), getSlug);
if (path && FlowRouter.current().path !== path) FlowRouter.go(path);
@@ -504,7 +505,7 @@ function goToAllBoards(tpl, menuValue) {
// The boards the page shows: the selected section, filtered by the search
// field, sorted and paged. Extracted from the `boards` helper so the Table
// view draws the SAME set - two copies of this would be two answers to
// "which boards am I looking at". docs/Features/Page/All-Boards.md
// "which boards am I looking at". docs/Features/All-Boards/All-Boards.md
// How many archived boards the section shows at once. The publication is
// paginated and this is the page size it is asked for; the Archive is a place
// you go to find one board, not a list to scroll for ever.
@@ -512,7 +513,7 @@ const ARCHIVED_BOARDS_LIMIT = 60;
function boardsForView(tpl) {
// The Archive shows ARCHIVED boards - the one section whose query is the
// opposite of every other one's. docs/Features/Page/Archive.md
// opposite of every other one's. docs/Features/All-Boards/Archive.md
const showsArchive = tpl.selectedMenu.get() === 'archive';
let query = {
$and: [
@@ -568,6 +569,13 @@ function boardsForView(tpl) {
if (membershipOrs.length) {
query.$and.splice(2, 0, { $or: membershipOrs });
}
// #4265: boards this user hid by leaving them stay out of All Boards.
const hidden = currUser?.profile?.hiddenBoards;
if (Array.isArray(hidden) && hidden.length) query.$and.push({ _id: { $nin: hidden } });
// #4791: only the chosen facet's boards (the drag-ordered view).
const { facetSelector } = require('/models/lib/allBoardsFacets');
const facetPart = facetSelector(tpl.boardFacet && tpl.boardFacet.get(), currUser?.profile?.boardLabels);
if (facetPart) query.$and.push(facetPart);
} else if (
allowPrivateVisibilityOnly !== undefined &&
!allowPrivateVisibilityOnly.booleanValue
@@ -718,7 +726,7 @@ Template.boardList.onCreated(function () {
// Shared with the right sidebar, which is a SEPARATE Blaze instance (it is
// rendered beside the page, not inside it) and carries this page's controls. Assigned onto the instance so every `tpl.selectedMenu` /
// `tpl.boardSearchVar` already written here keeps working unchanged.
// docs/Features/Page/All-Boards.md
// docs/Features/All-Boards/All-Boards.md
this.selectedMenu = allBoardsMenuVar;
// Whatever the address named, else the section this user should land on:
// Starred when anything is starred, Remaining when nothing is - opening on an
@@ -746,7 +754,7 @@ Template.boardList.onCreated(function () {
// It runs whenever either changes, so a link followed while the page is
// already open switches workspace, and a slug path that names nothing leaves
// the Workspaces section selected rather than an empty board list.
// docs/Features/Page/All-Boards-URLs.md
// docs/Features/All-Boards/URLs.md
this.autorun(() => {
const slugPath = Session.get('boardListWorkspacePath') || [];
const tree = this.workspacesTreeVar.get();
@@ -789,6 +797,24 @@ Template.boardList.onCreated(function () {
// #5799: server-side pagination state for the sorted (non-custom) modes.
this.boardsPageVar = new ReactiveVar(1);
this.pagedBoardsVar = new ReactiveVar({ ids: [], total: 0 });
// #4791: the facet bar - first characters, organizations, teams and the
// member's own board labels with their counts - and the chosen group.
this.boardFacet = new ReactiveVar(null);
this.boardFacets = new ReactiveVar({ chars: [], orgs: [], teams: [], labels: [] });
this.loadBoardFacets = () => Meteor.call('allBoardsFacets', (error, facets) => {
if (!error && facets) this.boardFacets.set(facets);
});
this.loadBoardFacets();
// #802: the organization or team tab picked, and its members.
this.selectedGroup = new ReactiveVar(null);
this.groupMembers = new ReactiveVar({ canManage: false, members: [] });
this.loadGroupMembers = () => {
const group = this.selectedGroup.get();
if (!group) return;
Meteor.call('groupMembers', group.kind, group.id, (error, result) => {
this.groupMembers.set(error ? { canManage: false, members: [] } : result);
});
};
// #5850: the user's orgs/teams that have the per-org/team Shared Templates
// flag set (plus email domains), fetched via a non-admin server method since
// the org/team publications are admin-only. Gates the drag-to-share targets.
@@ -817,7 +843,7 @@ Template.boardList.onCreated(function () {
// A workspace was dropped: before, after, or INTO another one. The tree it
// becomes is worked out by the pure module - the guards that keep a subtree
// attached to the root live there, with their own tests - and this only saves
// the answer. docs/Features/Page/Workspaces.md
// the answer. docs/Features/All-Boards/Workspaces.md
this.moveWorkspaceInTree = (draggedId, targetId, position) => {
const tree = this.workspacesTreeVar.get();
// A drop that puts a workspace back where it already is writes the same tree
@@ -893,9 +919,10 @@ Template.boardList.onCreated(function () {
const search = (this.boardSearchVar.get() || '').trim();
const menu = this.selectedMenu.get();
const page = this.boardsPageVar.get();
const facet = this.boardFacet.get();
Meteor.call(
'getAllBoardsPage',
{ search, sortBy, menu, page, perPage: BOARDS_PER_PAGE },
{ search, sortBy, menu, page, perPage: BOARDS_PER_PAGE, ...(facet ? { facet } : {}) },
(err, res) => {
if (err) {
console.error('getAllBoardsPage failed:', err);
@@ -967,6 +994,29 @@ Template.boardList.onRendered(function () {
});
Template.boardList.helpers({
// #4791: the facet bar.
boardFacets() { return Template.instance().boardFacets.get(); },
hasBoardFacets() {
const f = Template.instance().boardFacets.get();
return f.chars.length + f.orgs.length + f.teams.length + f.labels.length > 1;
},
isFacet(type) {
const f = Template.instance().boardFacet.get();
return !!(f && f.type === type && f.value === (this.key || ''));
},
hasBoardFacet() { return !!Template.instance().boardFacet.get(); },
// #802
myGroups() {
const { myGroups } = require('/models/lib/groupMembers');
return myGroups(ReactiveCache.getCurrentUser());
},
isSelectedGroup() {
const g = Template.instance().selectedGroup.get();
return !!(g && g.kind === this.kind && g.id === this.id);
},
selectedGroup() { return Template.instance().selectedGroup.get(); },
groupMembers() { return Template.instance().groupMembers.get().members; },
canManageGroup() { return Template.instance().groupMembers.get().canManage; },
userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
else return false;
@@ -1000,7 +1050,7 @@ Template.boardList.helpers({
// The Table view of the same boards the Lists view draws. Ten per page, the
// shared TABLE_PAGE_ROWS_PER_PAGE, and a rowTemplate because the Edit cell is a
// control and the row carries the board's colours.
// docs/Features/Page/All-Boards.md
// docs/Features/All-Boards/All-Boards.md
tablePageData() {
const tpl = Template.instance();
const all = boardsForView(tpl);
@@ -1077,6 +1127,14 @@ Template.boardList.helpers({
return user && user.hasStarred(this._id);
},
// #4265: not an active member - so it is here through a team, organization
// or e-mail domain - and not the archive.
canHideBoard() {
const userId = Meteor.userId();
if (!userId || this.archived) return false;
return !(this.members || []).some(m => m && m.userId === userId && m.isActive !== false);
},
// When this board was archived, in the reader's own date format. Shown only
// in the Archive, where it is what one old board is told from another by.
//
@@ -1157,7 +1215,7 @@ Template.boardList.helpers({
extraClass: 'js-open-archived-board' },
// The Home row is also a drop target of its own - dropping a board on it
// makes that board the one that opens after login - so it carries a
// second class like the Archive does. docs/Features/Board/Home.md
// second class like the Archive does. docs/Features/All-Boards/Home.md
home: { icon: 'fa-home', labelKey: 'home', extraClass: 'js-home-menu' },
};
return menuSectionOrder(hasStarredBoards()).map(type => ({
@@ -1175,7 +1233,7 @@ Template.boardList.helpers({
// colour rather than two that drift apart. Its words are the section's own
// title key - the same key the first header bar names the page with, and the
// same one the highlighted menu row carries - so all three say the same
// thing. docs/Features/Page/All-Boards.md
// thing. docs/Features/All-Boards/All-Boards.md
//
// `{ titleKey }` for a section, `{ label }` for a workspace: a workspace's
// name is what somebody typed, and a workspace called "starred" is not the
@@ -1196,14 +1254,14 @@ Template.boardList.helpers({
// The bookmarks, drawn as tiles beside the starred boards. Only in Starred:
// that is the list of places you keep, and a bookmark is one of them.
// docs/Features/Board/Starred.md
// docs/Features/Pages/Starred.md
starredPages() {
const user = ReactiveCache.getCurrentUser();
return user && user.starredPages ? user.starredPages() : [];
},
// Is the Android-launcher Remove bar showing? Only in Home, and only while a
// board from Home is actually in the air. docs/Features/Board/Home.md
// board from Home is actually in the air. docs/Features/All-Boards/Home.md
showsHomeRemoveTarget() {
return Template.instance().selectedMenu.get() === 'home'
&& draggingFromHome.get();
@@ -1325,7 +1383,7 @@ Template.boardList.helpers({
// it, or light the row up to say "into this one". The class is also what the
// drop reads back: the pointer's position is worked out once, while the row is
// showing it, rather than a second time from a drop event that may land a pixel
// away from where the slot was drawn. docs/Features/Page/Workspaces.md
// away from where the slot was drawn. docs/Features/All-Boards/Workspaces.md
const WORKSPACE_DROP_CLASSES = {
[DROP_BEFORE]: 'drop-before',
[DROP_INSIDE]: 'drop-inside',
@@ -1383,7 +1441,7 @@ Template.workspaceTree.helpers({
// The caret, and what it says. `Template.currentData()` is the NODE inside the
// `each`, which is what makes these read the row they are drawn on rather than
// needing the id passed to each of them.
// docs/Features/Page/Workspaces.md
// docs/Features/All-Boards/Workspaces.md
workspaceHasChildren() {
return hasChildren(Template.currentData());
},
@@ -1512,6 +1570,76 @@ function persistBoardOrderFromDom(evt, tpl) {
}
Template.boardList.events({
// #802: an organization or team tab: its boards, and its members.
'click .js-select-group'(evt, tpl) {
evt.preventDefault();
const { kind, id } = evt.currentTarget.dataset;
const current = tpl.selectedGroup.get();
if (current && current.kind === kind && current.id === id) {
tpl.selectedGroup.set(null);
tpl.boardFacet.set(null);
return;
}
const { myGroups } = require('/models/lib/groupMembers');
const group = myGroups(ReactiveCache.getCurrentUser()).find(g => g.kind === kind && g.id === id);
tpl.selectedGroup.set(group || { kind, id, name: '' });
tpl.boardFacet.set({ type: kind, value: id });
tpl.boardsPageVar.set(1);
tpl.loadGroupMembers();
},
async 'click .js-add-group-member-button, submit .js-add-group-member'(evt, tpl) {
evt.preventDefault();
const group = tpl.selectedGroup.get();
const username = String(tpl.$('.js-group-member-username').val() || '').trim();
if (!group || !username) return;
try {
await Meteor.callAsync('addGroupMember', group.kind, group.id, username);
tpl.$('.js-group-member-username').val('');
tpl.loadGroupMembers();
} catch (error) {
alert(errorMessage(error));
}
},
async 'click .js-remove-group-member'(evt, tpl) {
evt.preventDefault();
const group = tpl.selectedGroup.get();
if (!group) return;
await Meteor.callAsync('removeGroupMember', group.kind, group.id, evt.currentTarget.dataset.userId);
tpl.loadGroupMembers();
},
// #4791: pick a group of the facet bar, again to clear it.
'click .js-board-facet'(evt, tpl) {
evt.preventDefault();
const { type, value } = evt.currentTarget.dataset;
const current = tpl.boardFacet.get();
tpl.boardFacet.set(current && current.type === type && current.value === value ? null : { type, value });
tpl.boardsPageVar.set(1);
},
'click .js-board-facet-clear'(evt, tpl) {
evt.preventDefault();
tpl.boardFacet.set(null);
tpl.boardsPageVar.set(1);
},
async 'click .js-apply-board-label-button, submit .js-apply-board-label'(evt, tpl) {
evt.preventDefault();
const label = String(tpl.$('.js-board-label-name').val() || '');
const text = String(tpl.$('.js-board-label-text').val() || '');
try {
await Meteor.callAsync('applyBoardLabel', label, text);
tpl.$('.js-board-label-name').val('');
tpl.$('.js-board-label-text').val('');
tpl.loadBoardFacets();
} catch (error) {
alert(errorMessage(error));
}
},
async 'click .js-remove-board-label'(evt, tpl) {
evt.preventDefault();
evt.stopPropagation();
await Meteor.callAsync('removeBoardLabel', evt.currentTarget.dataset.value);
if (tpl.boardFacet.get()?.type === 'label') tpl.boardFacet.set(null);
tpl.loadBoardFacets();
},
// Boards in Archive no longer NAVIGATES anywhere. It is a section of this
// page, drawn beside the left menu by the generic `js-select-menu` click
// below - selecting it from a menu row and then losing the menu is a menu
@@ -1618,6 +1746,18 @@ Template.boardList.events({
tpl.boardsPageVar.set(adjacentPage(total, tpl.boardsPageVar.get(), 1,
BOARDS_PER_PAGE));
},
// #4265: leave a board that came through a team: it is hidden for this user.
'click .js-hide-board'(evt, tpl) {
evt.preventDefault();
evt.stopPropagation();
const boardId = this._id;
if (!boardId) return;
Meteor.call('quitBoard', boardId, (err) => {
if (err) return;
const paged = tpl.pagedBoardsVar.get();
tpl.pagedBoardsVar.set({ ids: (paged.ids || []).filter(id => id !== boardId), total: Math.max(0, (paged.total || 1) - 1) });
});
},
'click .js-star-board'(evt) {
evt.preventDefault();
evt.stopPropagation();
@@ -1869,7 +2009,7 @@ Template.boardList.events({
if (confirm(TAPi18n.__('archive-board-confirm'))) {
const boardId = this._id;
Meteor.call('archiveBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
if (err) alert(errorMessage(err));
});
evt.preventDefault();
}
@@ -2005,7 +2145,7 @@ Template.boardList.events({
// the drop handler below reads as "a board was dropped here", so the drop
// did nothing and the row snapped back. Handles ON started the drag from a
// span, which carries no text, which is why the same drop worked there.
// docs/Features/Page/Workspaces.md
// docs/Features/All-Boards/Workspaces.md
if (typeof dt.clearData === 'function') dt.clearData();
dt.setData('application/x-workspace-id', workspaceId);
@@ -2121,7 +2261,7 @@ Template.boardList.events({
// A board picked up in Home may only be dropped on the remove target. NOT
// calling preventDefault() is what REFUSES a drop in HTML5 drag and drop,
// so the cursor says no while the board is still in the air rather than the
// drop landing and quietly doing nothing. docs/Features/Board/Home.md
// drop landing and quietly doing nothing. docs/Features/All-Boards/Home.md
if (isDragFromHome(evt)) return;
const menuType = evt.currentTarget.getAttribute('data-type');
// Remaining accepts board drags generally. Starred additionally accepts a
@@ -2190,7 +2330,7 @@ Template.boardList.events({
boardIds.forEach((boardId) => {
Meteor.call('archiveBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
if (err) alert(errorMessage(err));
});
});
// The dragged boards are gone from this page, so a selection of them is
@@ -2311,7 +2451,7 @@ Template.boardList.events({
boardIds.forEach((boardId) => {
Meteor.call('clearDefaultBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to remove from Home');
if (err) alert(errorMessage(err));
});
});
if (isMultiBoard) BoardMultiSelection.reset();
@@ -2326,7 +2466,7 @@ Template.boardList.events({
// predictable: you drop a board on Home and that board is Home, whatever was
// there before. Taking a board off Home is the opposite gesture - dragging it
// out of the Home section - and clicking the row's Multi-Selection toggle
// still toggles. docs/Features/Board/Home.md
// still toggles. docs/Features/All-Boards/Home.md
'dragover .js-home-menu'(evt) {
if (isArchivedMultiBoardDrag(evt)) return;
evt.preventDefault();
@@ -2366,7 +2506,7 @@ Template.boardList.events({
// silently choose its first id; it has to be narrowed to one before this
// drop can change anything.
Meteor.call('setDefaultBoard', boardIds[0], (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to set Home board');
if (err) alert(errorMessage(err));
});
if (isMultiBoard) BoardMultiSelection.reset();
},
@@ -2423,7 +2563,7 @@ Template.boardList.events({
const board = ReactiveCache.getBoard(boardId);
if (board && board.archived) {
Meteor.call('restoreBoard', boardId, (err) => {
if (err) alert(err?.reason || err?.message || 'Failed to restore board');
if (err) alert(errorMessage(err));
});
}
// ...and out of whatever workspace it was in, which is the other half of
@@ -2580,7 +2720,7 @@ Template.workspaceActionsPopup.onRendered(function() {
// The All Boards view menu, in the FIRST header bar. Its handler and the helper
// that draws it follow it out of the sidebar. docs/Features/Page/Header.md
// that draws it follow it out of the sidebar. docs/Features/Header-Bar/Header.md
Template.allBoardsViewMenu.events({
// Titled, so it has a header with the close ✕ in it - the same popup the
// BOARD's view menu opens, and it is the same question: which view of this
@@ -1,4 +1,4 @@
//- The 10 board report charts (docs/Features/Reports/charts.tsv), each a thin
//- The 10 board report charts (docs/Features/Board-Header/Board-Views/Charts/charts.tsv), each a thin
// wrapper around the shared boardChartView (charts/boardCharts.jade/.js):
// export buttons, the chart's title, a small CSS-bar/table visualization and
// the underlying data table - all driven from the `boardChartData` server
@@ -24,7 +24,7 @@ function loadFlowRenderer() {
// ReactiveVar holds the answer, helpers read it. Drawn with Chart.js (MIT,
// canvas-based), loaded with a dynamic import() so its code only reaches the
// browser when a chart view actually mounts, following
// docs/Features/Reports/charts.tsv for what each chartKey means.
// docs/Features/Board-Header/Board-Views/Charts/charts.tsv for what each chartKey means.
Template.boardChartView.onCreated(function() {
this.chartData = new ReactiveVar(null);
this.options = new ReactiveVar({});
+347
View File
@@ -0,0 +1,347 @@
/* Board View / Chat: Slack / Rocket.Chat's three panes in the board's own
theme. No colour of its own: the accent is the theme's (--theme-accent,
falling back to WeKan blue as paginationControls.css does), surfaces are
translucent over the board background, text inherits.
The container is laid out LEFT TO RIGHT in every language and the pane
order is decided in chatView.js (paneOrder), so a right-to-left language
gets the mirror image exactly once; the pane CONTENTS follow the page's own
direction. */
.chat-view {
direction: ltr;
display: flex;
align-items: stretch;
height: calc(100vh - 110px);
min-height: 320px;
overflow: hidden;
}
.chat-view[data-dir="rtl"] .chat-view-pane {
direction: rtl;
}
.chat-view-pane {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.85);
color: #172b4d;
}
.chat-view-tree,
.chat-view-chat {
flex-grow: 0;
flex-shrink: 0;
}
.chat-view-card {
flex: 1 1 auto;
}
.chat-view-pane-folded {
flex: 0 0 36px;
align-items: center;
padding-top: 8px;
}
.chat-view-pane-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-weight: bold;
}
.chat-view-pane-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* A folded pane keeps its double caret, above the pane's icon. */
.chat-view-pane-folded button {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* Right to left the tree is on the right and the chat on the left, so every
double caret points the other way. */
.chat-view[data-dir="rtl"] .chat-view-fold-caret {
transform: scaleX(-1);
}
.chat-view-pane-header button,
.chat-view-pane-folded button {
margin: 0 !important;
padding: 4px 8px;
background: transparent;
color: inherit;
box-shadow: none;
}
/* The draggable borders. */
.chat-view-resizer {
flex: 0 0 6px;
cursor: col-resize;
background: rgba(0, 0, 0, 0.08);
touch-action: none;
}
.chat-view-resizer:hover,
.chat-view-resizer:focus {
background: var(--theme-accent, #01628c);
outline: none;
}
/* The tree. */
.chat-view-tree ul {
list-style: none;
margin: 0;
padding: 0;
}
.chat-view-workspaces {
overflow-y: auto;
padding: 6px 0;
}
.chat-view-node > ul,
.chat-view-collapse > ul {
padding-inline-start: 14px;
}
/* The caret before the icon: it points to where the text runs (right, or
left in a right-to-left language) when closed, and down when open. */
.chat-view-caret {
flex: 0 0 auto;
width: 0.8em;
text-align: center;
transition: transform 0.2s ease;
}
.chat-view-tree[dir="rtl"] .chat-view-caret {
transform: scaleX(-1);
}
.chat-view-node.is-open > .chat-view-row > .chat-view-caret {
transform: rotate(90deg);
}
.chat-view-tree[dir="rtl"] .chat-view-node.is-open > .chat-view-row > .chat-view-caret {
transform: rotate(90deg) scaleX(-1);
}
/* A card has no caret: its # holds the same place. */
.chat-view-channel > .chat-view-row > .chat-view-hash {
width: 0.8em;
text-align: center;
}
/* Opening and closing slide: the rows under a node grow from nothing to
their height (a 0fr to 1fr grid row) - and simply show or hide for a
member who asked the system for reduced motion. */
.chat-view-collapse {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
}
.chat-view-collapse > ul {
overflow: hidden;
min-height: 0;
visibility: hidden;
transition: visibility 0.2s;
}
.chat-view-node.is-open > .chat-view-collapse {
grid-template-rows: 1fr;
}
.chat-view-node.is-open > .chat-view-collapse > ul {
visibility: visible;
}
@media (prefers-reduced-motion: reduce) {
.chat-view-collapse,
.chat-view-collapse > ul,
.chat-view-caret {
transition: none;
}
}
/* Messages this member has not read, under that row. */
.chat-view-unread {
flex: 0 0 auto;
margin-inline-start: auto;
min-width: 1.6em;
padding: 0 6px;
border-radius: 9px;
background: var(--theme-accent, #01628c);
color: #fff;
font-size: 0.75em;
font-weight: bold;
line-height: 1.6;
text-align: center;
}
.chat-view-all-cards {
flex: 0 0 auto;
margin: 0;
padding: 0 4px;
border: 0;
background: none;
color: inherit;
opacity: 0.5;
cursor: pointer;
}
.chat-view-row:hover .chat-view-all-cards,
.chat-view-all-cards:focus-visible,
.chat-view-all-cards[aria-pressed="true"] {
opacity: 1;
}
.chat-view-row:focus-visible {
outline: 2px solid currentColor;
outline-offset: -2px;
}
.chat-view-row {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 12px;
border-radius: 4px;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.chat-view-row:hover {
background: rgba(0, 0, 0, 0.06);
}
.chat-view-row-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-view-node-heading {
margin: 10px 0 2px;
padding-inline-start: 12px;
font-size: 0.8em;
text-transform: uppercase;
opacity: 0.7;
}
.chat-view-board.is-current > .chat-view-row {
font-weight: bold;
}
.chat-view-hash {
opacity: 0.6;
}
.chat-view-channel.is-selected > .chat-view-row {
background: var(--theme-accent, #01628c);
color: #fff;
}
/* The card: the same card details as everywhere, held in the pane instead
of floating beside a minicard. */
.chat-view-card-body {
flex: 1 1 auto;
overflow-y: auto;
}
.chat-view-card-body .card-details {
position: static !important;
inset: auto !important;
width: auto !important;
max-width: none !important;
height: auto !important;
max-height: none !important;
transform: none !important;
visibility: visible !important;
box-shadow: none !important;
margin: 0 !important;
}
.chat-view-empty {
padding: 16px;
}
/* The chat. */
.chat-view-messages {
flex: 1 1 auto;
overflow-y: auto;
padding: 8px 12px;
}
.chat-view-message {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
}
.chat-view-message-body {
min-width: 0;
flex: 1 1 auto;
}
.chat-view-message-author {
font-weight: bold;
margin-inline-end: 8px;
}
.chat-view-message-time {
font-size: 0.8em;
}
.chat-view-message-text {
overflow-wrap: anywhere;
}
.chat-view-message.is-mine .chat-view-message-author {
color: var(--theme-accent, #01628c);
}
.chat-view-compose {
display: flex;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.chat-view-compose textarea {
flex: 1 1 auto;
min-height: 2.5em;
margin: 0;
resize: vertical;
}
.chat-view-compose button {
margin: 0 !important;
align-self: flex-end;
}
/* A dark theme: the panes darken with it. */
body.dark-mode .chat-view-pane,
.board-color-dark .chat-view-pane {
background: rgba(30, 30, 30, 0.88);
color: #eee;
}
/* Narrow windows: one pane at a time is all that fits; the tree and the chat
stack above and below the card. */
@media (max-width: 800px) {
.chat-view {
flex-direction: column;
height: auto;
}
.chat-view-pane {
flex-basis: auto !important;
}
.chat-view-resizer {
display: none;
}
}
+174
View File
@@ -0,0 +1,174 @@
//- Board View / Chat: the board as a Slack / Rocket.Chat workspace, in the
board's own theme. Design: docs/Features/Board-Header/Board-Views/Chat-View.md.
//-
//- tree Workspaces > boards > swimlanes > lists > cards. A card is a
//- channel; everything can be dragged to reorder it.
//- card the selected card, the same card details as everywhere else.
//- chat the card's comments as messages, newest at the bottom.
//-
//- The panes are laid out in screen order from paneOrder(): tree | card | chat
in a left-to-right language, mirrored in a right-to-left one. Each pane folds
to a thin bar, and the two borders between them are dragged to resize; both
are saved on the member's profile (setChatViewLayout).
template(name="chatView")
.chat-view.nodragscroll(data-dir="{{dir}}")
each panes
if $eq this 'tree'
+chatViewTreePane
else if $eq this 'card'
+chatViewCardPane
else
+chatViewChatPane
if resizerAfter this
.chat-view-resizer.js-chat-resize(data-pane="{{resizerAfter this}}" role="separator" aria-orientation="vertical" tabindex="0" title="{{_ 'chat-view-resize'}}")
template(name="chatViewTreePane")
if collapsed 'tree'
.chat-view-pane.chat-view-pane-folded
//- The double caret stays where it was, pointing the way the pane opens.
button.js-chat-unfold(data-pane="tree" title="{{_ 'chat-view-show-channels'}}" aria-label="{{_ 'chat-view-show-channels'}}")
i.fa.fa-angle-double-right.chat-view-fold-caret(aria-hidden="true")
i.fa.fa-sitemap(aria-hidden="true")
else
nav.chat-view-pane.chat-view-tree(style="flex-basis: {{width 'tree'}}px" aria-label="{{_ 'chat-view-channels'}}" dir="{{treeDir}}")
.chat-view-pane-header
span.chat-view-pane-title {{_ 'chat-view-channels'}}
button.js-chat-fold(data-pane="tree" title="{{_ 'chat-view-hide'}}")
i.fa.fa-angle-double-left.chat-view-fold-caret
//- Workspaces > boards > swimlanes > lists > cards that have a
conversation (server/chatView.js), each level folding open under its
caret; every board without one is under Other boards, until its first
message moves it up to its place.
with channelTree
ul.chat-view-workspaces(role="tree")
each workspaces
+chatViewWorkspaceNode
each unassigned
+chatViewBoardNode
if other.length
li.chat-view-node.chat-view-node-heading {{_ 'chat-view-other-boards'}}
each other
+chatViewBoardNode
//- A row: the caret before the icon (after it, mirrored, right to left),
the name as Markdown, and the messages this member has not read.
template(name="chatViewWorkspaceNode")
li.chat-view-node.chat-view-workspace.js-chat-draggable.js-chat-drop(draggable="true" data-kind="workspace" data-id="{{id}}" role="treeitem" aria-expanded="{{#if isOpen 'workspace' id}}true{{else}}false{{/if}}" class="{{#if isOpen 'workspace' id}}is-open{{/if}}")
.chat-view-row.js-chat-toggle(data-key="workspace:{{id}}" tabindex="0")
i.chat-view-caret.fa.fa-caret-right(aria-hidden="true")
i.fa(class="{{#if isOpen 'workspace' id}}fa-folder-open-o{{else}}fa-folder-o{{/if}}" aria-hidden="true")
span.chat-view-row-title
+inlineViewer
= name
+chatViewUnread(count=unread)
.chat-view-collapse
ul(role="group")
each children
+chatViewWorkspaceNode
each boards
+chatViewBoardNode
template(name="chatViewBoardNode")
li.chat-view-node.chat-view-board.js-chat-draggable(draggable="true" data-kind="board" data-id="{{_id}}" role="treeitem" aria-expanded="{{#if isOpen 'board' _id}}true{{else}}false{{/if}}" class="{{#if isOpen 'board' _id}}is-open{{/if}} {{#if isCurrentNode}}is-current{{/if}}")
//- Clicking a board folds or unfolds it here; it does not leave the Chat view.
.chat-view-row.js-chat-toggle(data-key="board:{{_id}}" data-board-id="{{_id}}" tabindex="0")
i.chat-view-caret.fa.fa-caret-right(aria-hidden="true")
i.fa.fa-columns(aria-hidden="true")
span.chat-view-row-title
+inlineViewer
= title
+chatViewUnread(count=unread)
if hasMessages
button.chat-view-all-cards.js-chat-all-cards(type="button" data-board-id="{{_id}}" title="{{#if full}}{{_ 'chat-view-message-cards'}}{{else}}{{_ 'chat-view-all-cards'}}{{/if}}" aria-pressed="{{#if full}}true{{else}}false{{/if}}")
i.fa(class="{{#if full}}fa-comments{{else}}fa-list-ul{{/if}}")
.chat-view-collapse
ul(role="group")
each swimlanes
li.chat-view-node.js-chat-drop(class="{{#if isCurrentNode}}js-chat-draggable{{/if}} {{#if isOpen 'swimlane' _id}}is-open{{/if}}" draggable="{{#if isCurrentNode}}true{{else}}false{{/if}}" data-kind="swimlane" data-id="{{_id}}" role="treeitem" aria-expanded="{{#if isOpen 'swimlane' _id}}true{{else}}false{{/if}}")
.chat-view-row.js-chat-toggle(data-key="swimlane:{{_id}}" tabindex="0")
i.chat-view-caret.fa.fa-caret-right(aria-hidden="true")
i.fa.fa-th-large(aria-hidden="true")
span.chat-view-row-title
+inlineViewer
= title
+chatViewUnread(count=unread)
.chat-view-collapse
ul(role="group")
each lists
li.chat-view-node.js-chat-drop(class="{{#if isCurrentNode}}js-chat-draggable{{/if}} {{#if isOpen 'list' _id}}is-open{{/if}}" draggable="{{#if isCurrentNode}}true{{else}}false{{/if}}" data-kind="list" data-id="{{_id}}" data-swimlane-id="{{swimlaneId}}" role="treeitem" aria-expanded="{{#if isOpen 'list' _id}}true{{else}}false{{/if}}")
.chat-view-row.js-chat-toggle(data-key="list:{{_id}}" tabindex="0")
i.chat-view-caret.fa.fa-caret-right(aria-hidden="true")
i.fa.fa-list(aria-hidden="true")
span.chat-view-row-title
+inlineViewer
= title
+chatViewUnread(count=unread)
.chat-view-collapse
ul(role="group")
each cards
li.chat-view-node.chat-view-channel.js-chat-drop(class="{{#if isCurrentNode}}js-chat-draggable{{/if}} {{#if isSelected _id}}is-selected{{/if}}" draggable="{{#if isCurrentNode}}true{{else}}false{{/if}}" data-kind="card" data-id="{{_id}}" role="treeitem")
.chat-view-row.js-chat-select-card(data-card-id="{{_id}}" data-board-id="{{boardId}}" tabindex="0")
span.chat-view-hash #
span.chat-view-row-title
+inlineViewer
= title
+chatViewUnread(count=unread)
template(name="chatViewUnread")
if count
span.chat-view-unread(title="{{_ 'chat-view-unread' count=count}}" aria-label="{{_ 'chat-view-unread' count=count}}") {{countText}}
template(name="chatViewCardPane")
if collapsed 'card'
.chat-view-pane.chat-view-pane-folded
button.js-chat-unfold(data-pane="card" title="{{_ 'chat-view-show-card'}}")
i.fa.fa-id-card-o
else
section.chat-view-pane.chat-view-card
.chat-view-pane-header
span.chat-view-pane-title {{_ 'card'}}
button.js-chat-fold(data-pane="card" title="{{_ 'chat-view-hide'}}")
i.fa.fa-compress
if selectedCard
.chat-view-card-body
+cardDetails(selectedCard)
else
p.quiet.chat-view-empty {{_ 'chat-view-pick-card'}}
template(name="chatViewChatPane")
if collapsed 'chat'
.chat-view-pane.chat-view-pane-folded
button.js-chat-unfold(data-pane="chat" title="{{_ 'chat-view-show-chat'}}" aria-label="{{_ 'chat-view-show-chat'}}")
i.fa.fa-angle-double-left.chat-view-fold-caret(aria-hidden="true")
i.fa.fa-comments(aria-hidden="true")
else
section.chat-view-pane.chat-view-chat(style="flex-basis: {{width 'chat'}}px")
.chat-view-pane-header
span.chat-view-pane-title
if selectedCard
| #
+inlineViewer
= selectedCard.title
else
| {{_ 'comments'}}
button.js-chat-fold(data-pane="chat" title="{{_ 'chat-view-hide'}}")
i.fa.fa-angle-double-right.chat-view-fold-caret
.chat-view-messages.js-chat-messages
each messages
.chat-view-message(class="{{#if mine}}is-mine{{/if}}")
+userAvatar(userId=userId noRemove=true)
.chat-view-message-body
.chat-view-message-meta
span.chat-view-message-author {{author}}
span.quiet.chat-view-message-time {{time}}
.chat-view-message-text
+viewer
= text
unless messages.length
if selectedCard
p.quiet.chat-view-empty {{_ 'chat-view-no-messages'}}
if canComment
form.chat-view-compose.js-chat-send
textarea.js-chat-input(rows="2" placeholder="{{_ 'chat-view-message-placeholder'}}" aria-label="{{_ 'chat-view-message-placeholder'}}")
button.primary(type="submit" title="{{_ 'chat-view-send'}}")
i.fa.fa-paper-plane
+443
View File
@@ -0,0 +1,443 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/reactiveCache';
import CardComments from '/models/cardComments';
import Cards from '/models/cards';
import Swimlanes from '/models/swimlanes';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
const {
DEFAULT_LAYOUT, cleanChatLayout, paneOrder, resizePane, buildChannelTree,
dropNeighbours, dropAllowed,
} = require('/models/lib/chatView');
const { dropPosition, moveWorkspace } = require('/models/lib/workspacesTree');
// Board View / Chat (chatView.jade). The layout is the member's
// (profile.chatViewLayout): read reactively from the user document, changed
// locally while a border is dragged, and saved once when the drag ends.
const selectedCardId = new ReactiveVar(null);
const dragLayout = new ReactiveVar(null);
// The channel tree (server/chatView.js): the summary of the member's
// conversations, the boards opened with all their cards, and which rows are
// open - kept in this browser, a per-viewer convenience.
const channelSummary = new ReactiveVar(null);
const fullBoards = new ReactiveVar({});
// The board the member is on shows all its cards unless they chose only
// the ones with messages; its cards come from the board's own subscription,
// so a card dragged to another list moves in the tree at once.
const messagesOnly = new ReactiveVar(new Set());
function currentBoardStructure() {
const boardId = Utils.getCurrentBoardId();
if (!boardId || messagesOnly.get().has(boardId)) return null;
const live = item => item && !item.archived;
return {
swimlanes: (ReactiveCache.getSwimlanes({ boardId, archived: false }) || []).filter(live)
.map(s => ({ _id: s._id, boardId, title: s.title || '', sort: s.sort })),
lists: (ReactiveCache.getLists({ boardId, archived: false }) || []).filter(live)
.map(l => ({ _id: l._id, boardId, title: l.title || '', sort: l.sort, swimlaneId: l.swimlaneId || '' })),
cards: (ReactiveCache.getCards({ boardId, archived: false }) || []).filter(live)
.map(c => ({ cardId: c._id, boardId, swimlaneId: c.swimlaneId, listId: c.listId, title: c.title || '', sort: c.sort })),
};
}
// { 'board:<id>': true, 'list:<id>': false, ... }: what the member opened
// or closed; every other row is as defaultOpen says.
const OPEN_KEY = 'wekan-chat-view-open';
const readOpen = () => {
try {
const stored = JSON.parse(window.localStorage.getItem(OPEN_KEY) || '{}');
return stored && typeof stored === 'object' && !Array.isArray(stored) ? stored : {};
} catch (e) { return {}; }
};
const openRows = new ReactiveVar(readOpen());
function setOpen(key, open) {
const next = { ...openRows.get(), [key]: !!open };
const keys = Object.keys(next);
for (const old of keys.slice(0, Math.max(0, keys.length - 500))) delete next[old];
openRows.set(next);
try { window.localStorage.setItem(OPEN_KEY, JSON.stringify(next)); } catch (e) { /* not kept */ }
}
let reloadTimer = null;
function loadChannels(delay = 0) {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(() => Meteor.call('chatViewChannels', (error, summary) => {
if (!error) channelSummary.set(summary);
}), delay);
}
function loadFullBoard(boardId) {
Meteor.call('chatViewBoardChannels', boardId, (error, full) => {
if (!error) fullBoards.set({ ...fullBoards.get(), [boardId]: full });
});
}
// The rows open before the member opened or closed any: the board they are
// on and the workspaces above it.
function defaultOpen(key) {
const boardId = Utils.getCurrentBoardId();
if (key === `board:${boardId}`) return true;
// Under a board that is open, its swimlanes and lists are too.
if (key.startsWith('swimlane:') || key.startsWith('list:')) return true;
if (!key.startsWith('workspace:')) return false;
const user = ReactiveCache.getCurrentUser();
const profile = (user && user.profile) || {};
const assigned = profile.boardWorkspaceAssignments && profile.boardWorkspaceAssignments[boardId];
if (!assigned) return false;
const find = (nodes, id) => {
for (const node of nodes || []) {
if (node.id === id) return node;
const inner = find(node.children, id);
if (inner) return inner;
}
return null;
};
const holds = node => !!node && (node.id === assigned || (node.children || []).some(holds));
return holds(find(profile.boardWorkspacesTree, key.slice('workspace:'.length)));
}
const isOpen = (kind, id) => {
const key = `${kind}:${id}`;
const chosen = openRows.get();
return Object.prototype.hasOwnProperty.call(chosen, key) ? chosen[key] === true : defaultOpen(key);
};
// The language's direction, reactively: the panes re-order the moment the
// member switches to or from a right-to-left language.
function isRtl() {
return !!(TAPi18n.getLanguageDirection && TAPi18n.getLanguageDirection() === 'rtl');
}
// Blaze hands over jQuery's event when jQuery is there, the DOM's otherwise.
const nativeEvent = event => event.originalEvent || event;
function layout() {
const live = dragLayout.get();
if (live) return live;
const user = ReactiveCache.getCurrentUser();
return cleanChatLayout(user && user.profile && user.profile.chatViewLayout);
}
function saveLayout(next) {
const clean = cleanChatLayout(next);
Meteor.call('setChatViewLayout', clean, () => dragLayout.set(null));
}
// The card opened in the middle, from any board in the tree: its own data
// (popupCardData) and conversation (chatViewCardComments) are subscribed.
function selectedCard() {
const id = selectedCardId.get();
return (id && ReactiveCache.getCard(id)) || null;
}
Template.chatView.onCreated(function () {
// The member's boards, for the Workspaces tree; the open board's own
// swimlanes, lists and cards are already subscribed by the board page.
this.subscribe('boards');
this.autorun(() => {
const id = selectedCardId.get();
if (id) {
this.subscribe('popupCardData', id);
this.subscribe('chatViewCardComments', id);
}
});
});
Template.chatView.helpers({
dir() { return isRtl() ? 'rtl' : 'ltr'; },
// The panes in SCREEN order: a flex row with dir=rtl would reverse them by
// itself, so the container is always laid out left to right (see the css)
// and the order is decided here, once.
panes() { return paneOrder(isRtl()); },
// The border to the end of a pane resizes the tree or the chat - the side
// panes. The card pane between them takes what is left.
resizerAfter(pane) {
const order = paneOrder(isRtl());
const at = order.indexOf(pane);
if (at === order.length - 1) return null;
const next = order[at + 1];
const side = pane === 'card' ? next : pane;
const l = layout();
if (side === 'tree' && l.treeCollapsed) return null;
if (side === 'chat' && l.chatCollapsed) return null;
return side;
},
});
const paneHelpers = {
collapsed(pane) { return layout()[`${pane}Collapsed`] === true; },
width(pane) { return layout()[`${pane}Width`] || DEFAULT_LAYOUT[`${pane}Width`]; },
selectedCard,
};
Template.chatViewTreePane.onCreated(function () {
loadChannels();
// A message written on this board - here or anywhere - is a reason to look
// again: a board's first message moves it out of Other boards.
this.autorun(() => {
const boardId = Utils.getCurrentBoardId();
if (boardId) CardComments.find({ boardId }).count();
loadChannels(500);
});
this.refresh = Meteor.setInterval(() => loadChannels(), 60000);
});
Template.chatViewTreePane.onDestroyed(function () {
Meteor.clearInterval(this.refresh);
});
Template.chatViewTreePane.helpers({
...paneHelpers,
treeDir() { return isRtl() ? 'rtl' : 'ltr'; },
channelTree() {
const user = ReactiveCache.getCurrentUser();
const profile = (user && user.profile) || {};
const full = { ...fullBoards.get() };
const current = currentBoardStructure();
if (current) full[Utils.getCurrentBoardId()] = current;
return buildChannelTree(channelSummary.get(), profile.boardWorkspacesTree, profile.boardWorkspaceAssignments, full);
},
});
const treeHelpers = {
isOpen,
// On the board the member is on: only there can rows be dragged, since a
// drop moves things on the open board.
isCurrentNode() { return this.boardId === Utils.getCurrentBoardId(); },
isSelected(id) { return selectedCardId.get() === id; },
};
Template.chatViewWorkspaceNode.helpers(treeHelpers);
Template.chatViewBoardNode.helpers(treeHelpers);
Template.chatViewUnread.helpers({
countText() { return this.count > 99 ? '99+' : String(this.count); },
});
Template.chatViewCardPane.helpers(paneHelpers);
Template.chatViewChatPane.helpers({
...paneHelpers,
messages() {
const card = selectedCard();
if (!card) return [];
const me = Meteor.userId();
return CardComments.find({ cardId: card._id }, { sort: { createdAt: 1 } }).fetch().map(c => {
const user = ReactiveCache.getUser(c.userId);
return {
_id: c._id,
userId: c.userId,
mine: c.userId === me,
author: user ? ((user.profile && user.profile.fullname) || user.username) : '',
time: c.createdAt ? new Date(c.createdAt).toLocaleString() : '',
text: c.text,
};
});
},
canComment() {
// The comment capability on the CARD's board, as the server decides
// (read-only roles too).
const card = selectedCard();
return !!(card && Utils.currentUserCan('comment', ReactiveCache.getBoard(card.boardId)));
},
});
// --- Drag and drop ------------------------------------------------------
let dragged = null;
async function dropOn(target, position) {
const boardId = Utils.getCurrentBoardId();
const { kind, id } = dragged;
const targetKind = target.dataset.kind;
const targetId = target.dataset.id;
if (!dropAllowed(kind, targetKind) || id === targetId) return;
if (kind === 'workspace') {
const user = ReactiveCache.getCurrentUser();
const tree = (user.profile && user.profile.boardWorkspacesTree) || [];
const next = moveWorkspace(tree, id, targetId, position);
if (next !== tree) Meteor.call('setWorkspacesTree', next);
return;
}
if (kind === 'board') {
Meteor.call('assignBoardToWorkspace', id, targetId);
return;
}
if (kind === 'swimlane') {
const siblings = ReactiveCache.getSwimlanes({ boardId, archived: false }) || [];
const { prev, next } = dropNeighbours(siblings, id, targetId, position === 'before' ? 'before' : 'after');
Swimlanes.update(id, { $set: { sort: Utils.calculateIndexData(prev, next, 1).base } });
return;
}
if (kind === 'list') {
const targetList = ReactiveCache.getList(targetId);
const siblings = (ReactiveCache.getLists({ boardId, archived: false }) || [])
.filter(l => !l.swimlaneId || l.swimlaneId === target.dataset.swimlaneId);
const { prev, next } = dropNeighbours(siblings, id, targetId, position === 'before' ? 'before' : 'after');
const update = { sort: Utils.calculateIndexData(prev, next, 1).base };
if (targetList && targetList.swimlaneId) update.swimlaneId = targetList.swimlaneId;
await Meteor.callAsync('updateListSort', id, boardId, update);
return;
}
// A card: before/after a card, or onto a list (its end). Where it lands is
// read from the TREE it was dropped on - the list and swimlane the member
// sees - not from a cached copy of the target card, which can still hold
// the list it was in before a move a moment ago.
const card = Cards.findOne(id);
if (!card) return;
const listId = targetKind === 'list' ? targetId : target.closest('[data-kind="list"]').dataset.id;
const swimlaneId = target.closest('[data-kind="swimlane"]').dataset.id;
const siblings = Cards.find({ boardId, listId, swimlaneId, archived: false }).fetch();
const neighbours = targetKind === 'list'
? dropNeighbours(siblings, id, null, 'after')
: dropNeighbours(siblings, id, targetId, position === 'before' ? 'before' : 'after');
await card.move(boardId, swimlaneId, listId, Utils.calculateIndexData(neighbours.prev, neighbours.next, 1).base);
}
Template.chatView.events({
'click .js-chat-fold'(event) {
event.preventDefault();
saveLayout({ ...layout(), [`${event.currentTarget.dataset.pane}Collapsed`]: true });
},
'click .js-chat-unfold'(event) {
event.preventDefault();
saveLayout({ ...layout(), [`${event.currentTarget.dataset.pane}Collapsed`]: false });
},
// A card is a channel: it opens in the middle, with its conversation
// beside it - from any board, without leaving the board the member is on.
'click .js-chat-select-card'(event) {
const link = event.target.closest && event.target.closest('a[href]');
if (link && event.currentTarget.contains(link)) return;
event.preventDefault();
selectedCardId.set(event.currentTarget.dataset.cardId);
},
// A workspace, board, swimlane or list folds or unfolds; a board without
// a conversation shows all its cards when opened, to write the first one.
'click .js-chat-toggle'(event) {
const link = event.target.closest && event.target.closest('a[href], button');
if (link && event.currentTarget.contains(link)) return;
event.preventDefault();
const key = event.currentTarget.dataset.key;
const [kind, id] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
const open = !isOpen(kind, id);
setOpen(key, open);
if (open && kind === 'board' && id !== Utils.getCurrentBoardId()) {
const summary = channelSummary.get();
const talking = summary && summary.channels.some(c => c.boardId === id);
if (!talking && !fullBoards.get()[id]) loadFullBoard(id);
}
},
'keydown .js-chat-toggle, keydown .js-chat-select-card'(event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.currentTarget.click();
},
'click .js-chat-all-cards'(event) {
event.preventDefault();
event.stopPropagation();
const boardId = event.currentTarget.dataset.boardId;
if (boardId === Utils.getCurrentBoardId()) {
const only = new Set(messagesOnly.get());
if (only.has(boardId)) only.delete(boardId); else only.add(boardId);
messagesOnly.set(only);
} else {
const full = { ...fullBoards.get() };
if (full[boardId]) { delete full[boardId]; fullBoards.set(full); } else loadFullBoard(boardId);
}
setOpen(`board:${boardId}`, true);
},
// Resize: the pointer is captured on the handle, the width follows it
// locally, and the layout is saved once on release.
'pointerdown .js-chat-resize'(event) {
const handle = event.currentTarget;
const pane = handle.dataset.pane;
const rtl = isRtl();
const start = event.clientX;
const base = layout();
handle.setPointerCapture(event.pointerId);
const move = e => dragLayout.set(resizePane(base, pane, e.clientX - start, rtl));
const up = e => {
handle.removeEventListener('pointermove', move);
handle.removeEventListener('pointerup', up);
saveLayout(resizePane(base, pane, e.clientX - start, rtl));
};
handle.addEventListener('pointermove', move);
handle.addEventListener('pointerup', up);
},
// The keyboard way to the same thing: arrows move the border 16px.
'keydown .js-chat-resize'(event) {
const step = { ArrowLeft: -16, ArrowRight: 16 }[event.key];
if (!step) return;
event.preventDefault();
saveLayout(resizePane(layout(), event.currentTarget.dataset.pane, step, isRtl()));
},
'dragstart .js-chat-draggable'(event) {
event.stopPropagation();
const node = event.currentTarget;
dragged = { kind: node.dataset.kind, id: node.dataset.id };
const dt = nativeEvent(event).dataTransfer;
if (dt) {
dt.effectAllowed = 'move';
dt.setData('text/plain', node.dataset.id);
}
},
'dragover .js-chat-drop'(event) {
if (!dragged || !dropAllowed(dragged.kind, event.currentTarget.dataset.kind)) return;
event.preventDefault();
event.stopPropagation();
},
async 'drop .js-chat-drop'(event) {
if (!dragged) return;
event.preventDefault();
event.stopPropagation();
const target = event.currentTarget;
const row = target.querySelector('.chat-view-row') || target;
const rect = row.getBoundingClientRect();
const position = dropPosition(nativeEvent(event).clientY - rect.top, rect.height);
try {
await dropOn(target, position);
} finally {
dragged = null;
}
},
'dragend .js-chat-draggable'() { dragged = null; },
'submit .js-chat-send'(event, tpl) {
event.preventDefault();
const input = tpl.$('.js-chat-input');
const text = String(input.val() || '').trim();
const card = selectedCard();
if (!text || !card) return;
CardComments.insert({ text, boardId: card.boardId, cardId: card._id, parentId: '' });
input.val('');
loadChannels(500);
},
// Enter sends, Shift+Enter is a new line - as in Slack and Rocket.Chat.
'keydown .js-chat-input'(event) {
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
event.preventDefault();
$(event.currentTarget).closest('form').trigger('submit');
}
},
});
// The open channel is read: a message arriving while it is open is seen, so
// its unread count does not grow.
Template.chatViewChatPane.onCreated(function () {
this.autorun(() => {
const id = selectedCardId.get();
if (!id) return;
CardComments.find({ cardId: id }).count();
Tracker.nonreactive(() => {
const user = ReactiveCache.getCurrentUser();
if (user && typeof user.setCardLastViewed === 'function') Promise.resolve(user.setCardLastViewed(id)).then(() => loadChannels(1000), () => {});
});
});
});
// Keep the newest message in view, as a chat does.
Template.chatViewChatPane.onRendered(function () {
this.autorun(() => {
Template.currentData();
selectedCardId.get();
CardComments.find({ cardId: selectedCardId.get() }).count();
Tracker.afterFlush(() => {
const box = this.find('.js-chat-messages');
if (box) box.scrollTop = box.scrollHeight;
});
});
});
@@ -0,0 +1,10 @@
/* #211: the Column sets view. */
.column-sets-view { padding: 0.5em; }
.column-sets-toolbar { display: flex; flex-wrap: wrap; gap: 0.5em; align-items: center; margin-bottom: 0.6em; }
.column-sets-toolbar form { display: inline-flex; gap: 0.3em; margin: 0; }
.column-sets-toolbar input, .column-sets-toolbar select { width: auto; margin: 0; }
.column-sets-columns { display: flex; gap: 0.6em; align-items: flex-start; overflow-x: auto; }
.column-sets-column { min-width: 14em; max-width: 18em; background: rgba(0, 0, 0, 0.05); border-radius: 4px; padding: 0.4em; }
.column-sets-column-header { font-weight: bold; margin-bottom: 0.4em; display: flex; gap: 0.4em; align-items: center; }
.column-sets-card { background: #fff; border-radius: 3px; padding: 0.35em 0.5em; margin-bottom: 0.35em; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.15); cursor: grab; }
.column-sets-card .card-label { font-size: 0.75em; margin-inline-end: 0.2em; }
@@ -0,0 +1,44 @@
//- #211: the same cards in several named column sets (models/lib/columnSets.js).
Pick a set; drag a card to another column to change its place in that set
only. Each card shows its column in the other sets.
template(name="columnSetsView")
.column-sets-view
.column-sets-toolbar
if columnSets.length
select.js-column-set-select(aria-label="{{_ 'column-sets'}}")
each columnSets
option(value=_id selected=isCurrentSet) {{name}}
if isBoardAdmin
form.js-add-column-set
input.js-column-set-name(type="text" maxlength="60" placeholder="{{_ 'column-set-new'}}" aria-label="{{_ 'column-set-new'}}")
button.js-add-column-set-button(type="submit") {{_ 'add'}}
if currentSet
form.js-add-column
input.js-column-title(type="text" maxlength="60" placeholder="{{_ 'column-new'}}" aria-label="{{_ 'column-new'}}")
select.js-column-color(aria-label="{{_ 'column-color'}}")
each colors
option(value=this) {{this}}
button.js-add-column-button(type="submit") {{_ 'add'}}
a.js-remove-column-set(title="{{_ 'column-set-remove'}}")
i.fa.fa-trash
if currentSet
.column-sets-columns
each columns
.column-sets-column.js-column-sets-drop(data-column-id=_id class="{{#if color}}card-label-{{color}}{{/if}}")
.column-sets-column-header
if title
| {{title}}
else
span.quiet {{_ 'column-not-placed'}}
span.quiet {{cards.length}}
if title
if isBoardAdmin
a.js-remove-column(data-column-id=_id title="{{_ 'column-remove'}}")
i.fa.fa-times
each cards
.column-sets-card.js-column-sets-card(draggable="{{#if canWrite}}true{{/if}}" data-card-id=_id)
a.js-column-sets-open-card(data-card-id=_id)= title
each otherBadges
span.card-label(class="card-label-{{color}}" title="{{setName}}") {{title}}
else
p.quiet {{_ 'column-sets-empty'}}
+106
View File
@@ -0,0 +1,106 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Random } from 'meteor/random';
import { ReactiveCache } from '/imports/reactiveCache';
import { Utils } from '/client/lib/utils';
import Boards from '/models/boards';
import Cards from '/models/cards';
const cs = require('/models/lib/columnSets');
// #211: the Column sets board view.
Template.columnSetsView.onCreated(function () {
this.setId = new ReactiveVar(null);
});
function currentSet(tpl) {
const board = Utils.getCurrentBoard();
const all = cs.sets(board);
return all.find(s => s._id === tpl.setId.get()) || all[0] || null;
}
function saveSets(next) {
const board = Utils.getCurrentBoard();
if (board && next) Boards.update(board._id, { $set: { columnSets: next } });
}
Template.columnSetsView.helpers({
columnSets() { return cs.sets(Utils.getCurrentBoard()); },
isCurrentSet() { const set = currentSet(Template.instance()); return !!(set && set._id === this._id); },
currentSet() { return currentSet(Template.instance()); },
colors() { return cs.COLORS; },
isBoardAdmin() { return !!ReactiveCache.getCurrentUser()?.isBoardAdmin(); },
canWrite() { const board = Utils.getCurrentBoard(); return !!board && Utils.currentUserCan('write', board); },
columns() {
const tpl = Template.instance();
const set = currentSet(tpl);
const board = Utils.getCurrentBoard();
if (!set || !board) return [];
const cards = ReactiveCache.getCards({ boardId: board._id, archived: false, type: 'cardType-card' }, { sort: { sort: 1 } });
const all = cs.sets(board);
return cs.columnsWithCards(set, cards).map(column => ({ ...column,
cards: column.cards.map(card => ({ _id: card._id, title: card.title, otherBadges: cs.otherSetBadges(card, all, set._id) })) }));
},
});
Template.columnSetsView.events({
'change .js-column-set-select'(event, tpl) {
tpl.setId.set(event.currentTarget.value);
},
'click .js-add-column-set-button, submit .js-add-column-set'(event, tpl) {
event.preventDefault();
const id = Random.id();
const next = cs.addColumnSet(cs.sets(Utils.getCurrentBoard()), tpl.$('.js-column-set-name').val(), id);
if (!next) return;
saveSets(next);
tpl.setId.set(id);
tpl.$('.js-column-set-name').val('');
},
'click .js-add-column-button, submit .js-add-column'(event, tpl) {
event.preventDefault();
const set = currentSet(tpl);
if (!set) return;
const next = cs.addColumn(cs.sets(Utils.getCurrentBoard()), set._id, tpl.$('.js-column-title').val(),
tpl.$('.js-column-color').val(), Random.id());
if (!next) return;
saveSets(next);
tpl.$('.js-column-title').val('');
},
'click .js-remove-column'(event, tpl) {
event.preventDefault();
const set = currentSet(tpl);
if (set) saveSets(cs.removeColumn(cs.sets(Utils.getCurrentBoard()), set._id, event.currentTarget.dataset.columnId));
},
'click .js-remove-column-set'(event, tpl) {
event.preventDefault();
const set = currentSet(tpl);
if (set && confirm(`${set.name}?`)) saveSets(cs.removeColumnSet(cs.sets(Utils.getCurrentBoard()), set._id));
tpl.setId.set(null);
},
'dragstart .js-column-sets-card'(event) {
const dt = event.originalEvent && event.originalEvent.dataTransfer;
if (dt) { dt.setData('text/plain', event.currentTarget.dataset.cardId); dt.effectAllowed = 'move'; }
},
'dragover .js-column-sets-drop'(event) {
event.preventDefault();
},
'drop .js-column-sets-drop'(event, tpl) {
event.preventDefault();
const dt = event.originalEvent && event.originalEvent.dataTransfer;
const cardId = dt && dt.getData('text/plain');
const set = currentSet(tpl);
const modifier = cs.placeModifier(set, event.currentTarget.dataset.columnId || null);
if (cardId && modifier) Cards.update(cardId, modifier);
},
'click .js-column-sets-open-card'(event) {
event.preventDefault();
const cardId = event.currentTarget.dataset.cardId;
const board = Utils.getCurrentBoard();
Meteor.subscribe('popupCardData', cardId, {
onReady() {
Session.set('popupCardId', cardId);
if (board) Session.set('popupCardBoardId', board._id);
if (!Popup.isOpen()) Popup.open('cardDetails')(event);
},
});
},
});
+10 -3
View File
@@ -1,6 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { Utils } from '/client/lib/utils';
import { ensureContrast } from '/models/lib/contrastColor';
import { surfacesFor } from '/models/lib/uiFonts';
import { activeThemeName } from '/client/lib/activeTheme';
import {
DEPENDENCY_ICON_CHOICES,
DEPENDENCY_TYPES,
@@ -138,13 +141,17 @@ Template.dependencyOverlay.onCreated(function () {
const y2 = dependent.y + dependent.h / 2;
const dx = Math.max(40, Math.abs(x2 - x1) / 2);
const path = `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
const markerId = markerIdForColor(dep.color);
// The colour is the member's free choice (a colour wheel), drawn over the
// board: made visible against the theme's surface - a line is a graphic
// that says something, 3:1 (WCAG 1.4.11). The stored colour is untouched.
const drawColor = ensureContrast(dep.color, surfacesFor(activeThemeName()), 3);
const markerId = markerIdForColor(drawColor);
if (meta.directed) {
markerColors.add(dep.color);
markerColors.add(drawColor);
}
lines.push({
path,
color: dep.color,
color: drawColor,
type: dep.type,
icon: dep.icon,
directed: meta.directed,
+180
View File
@@ -0,0 +1,180 @@
/* Board View / Email (#794): Gmail's layout - folders on the start side, the
message table beside them - in the board's own theme. No colour of its own:
the accent is the theme's (--theme-accent, falling back to WeKan blue, as
paginationControls.css does), and a label is the board label's own colour.
Logical properties, so a right-to-left language mirrors it. */
.email-view {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 12px;
min-height: 100%;
box-sizing: border-box;
}
.email-view-sidebar {
flex: 0 0 220px;
max-width: 220px;
position: sticky;
top: 0;
}
.email-view-compose {
display: inline-flex !important;
align-items: center;
margin: 0 0 12px 0 !important;
padding: 10px 20px;
border-radius: 16px;
background: var(--theme-accent, #01628c) !important;
color: #fff !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.email-view-folders {
list-style: none;
margin: 0;
padding: 0;
}
.email-view-folder a {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
border-start-end-radius: 16px;
border-end-end-radius: 16px;
color: inherit;
text-decoration: none;
}
.email-view-folder a:hover {
background: rgba(0, 0, 0, 0.06);
}
.email-view-folder.is-active a {
background: color-mix(in srgb, var(--theme-accent, #01628c) 18%, transparent);
font-weight: bold;
}
.email-view-folder-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.email-view-folder-heading {
margin: 12px 0 4px;
padding-inline-start: 12px;
font-size: 0.8em;
text-transform: uppercase;
opacity: 0.7;
}
.email-view-label-dot {
display: inline-block;
flex: 0 0 auto;
width: 12px;
height: 12px;
border-radius: 3px;
}
.email-view-main {
flex: 1 1 auto;
min-width: 0;
}
.email-view-hint {
margin: 0 0 8px;
}
.email-view-row {
cursor: pointer;
}
.email-view-row:hover {
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.15);
}
/* Received and not yet sent by us: bold, the way Gmail marks what is new to
you. Sent mail reads plainly. */
.email-view-row:not(.is-outgoing) .email-view-from,
.email-view-row:not(.is-outgoing) .email-view-subject-title {
font-weight: bold;
}
.email-view-snippet {
opacity: 0.7;
/* Two lines of the rendered message, then an ellipsis. */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
/* Rendered Markdown in a row reads as the row's text: a heading or a
paragraph keeps the row's size and flows on one line with the rest. */
.email-view-subject-title .viewer :is(h1, h2, h3, h4, h5, h6, p),
.email-view-snippet .viewer :is(h1, h2, h3, h4, h5, h6, p, ul, ol, li, blockquote) {
display: inline;
margin: 0;
padding: 0;
font-size: inherit;
line-height: inherit;
}
.email-view-snippet .viewer :is(h1, h2, h3, h4, h5, h6, p, li)::after {
content: ' ';
}
.email-view-subject-title .viewer img,
.email-view-snippet .viewer img {
max-height: 1.4em;
vertical-align: middle;
}
.email-view-row-label {
margin-inline-start: 6px;
padding: 0 6px;
font-size: 0.8em;
vertical-align: middle;
}
.email-view-reply {
margin-inline-start: 8px;
opacity: 0;
color: inherit;
}
.email-view-row:hover .email-view-reply,
.email-view-reply:focus {
opacity: 0.8;
}
.email-view-date {
white-space: nowrap !important;
text-align: end;
}
.email-compose label {
display: block;
}
.email-compose input,
.email-compose textarea {
width: 100%;
box-sizing: border-box;
}
/* Narrow windows: the folders above the messages, like Gmail's mobile menu. */
@media (max-width: 800px) {
.email-view {
flex-direction: column;
}
.email-view-sidebar {
position: static;
flex: 0 0 auto;
max-width: none;
width: 100%;
}
}
+89
View File
@@ -0,0 +1,89 @@
//- Board View / Email (#794): the board as a Gmail-like mailbox, in the
board's own theme. Design: docs/Features/Board-Header/Board-Views/Email-View.md.
//-
//- Left: Compose, the folders (Inbox = the list email to card puts new cards
in, Sent, All mail), the board's lists as folders and its labels as Gmail
labels. Right: the shared table page (docs/Features/General/Table.md) - one
page of messages, newest first. A row opens the card popup, as a message
opens in Gmail; its comments are the conversation.
template(name="emailView")
.email-view
nav.email-view-sidebar(aria-label="{{_ 'board-view-email'}}")
button.primary.email-view-compose.js-email-compose(type="button")
i.fa.fa-pencil
| &nbsp;{{_ 'email-view-compose'}}
ul.email-view-folders
each systemFolders
li.email-view-folder(class="{{#if active}}is-active{{/if}}")
a.js-email-folder(href="#" data-folder="{{id}}")
i.fa(class="{{icon}}")
span.email-view-folder-name {{_ labelKey}}
if listFolders.length
li.email-view-folder-heading {{_ 'lists'}}
each listFolders
li.email-view-folder(class="{{#if active}}is-active{{/if}}")
a.js-email-folder(href="#" data-folder="{{id}}")
i.fa.fa-folder-o
span.email-view-folder-name
+inlineViewer
= title
if labelFolders.length
li.email-view-folder-heading {{_ 'labels'}}
each labelFolders
li.email-view-folder(class="{{#if active}}is-active{{/if}}")
a.js-email-folder(href="#" data-folder="{{id}}")
span.email-view-label-dot(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
span.email-view-folder-name
+inlineViewer
= name
.email-view-main
unless receiving
p.quiet.email-view-hint {{_ 'email-view-not-receiving'}}
+tablePage(tablePageData)
//- One message. Cells match the header: From, Subject, Date.
template(name="emailViewRow")
tr.email-view-row.js-email-open(data-card-id="{{_id}}" class="{{#if outgoing}}is-outgoing{{/if}}")
td.email-view-from
if outgoing
span.quiet {{_ 'email-view-to'}}&nbsp;
| {{sender}}
//- Markdown, like every card title and description in WeKan: the viewer
honours the Admin Panel's security settings for links and code.
td.email-view-subject
.email-view-subject-title
+viewer
= title
if snippet
.email-view-snippet
+viewer
= snippet
each labels
span.card-label.email-view-row-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
+inlineViewer
= name
a.email-view-reply.js-email-reply(href="#" title="{{_ 'email-view-reply'}}" data-card-id="{{_id}}")
i.fa.fa-reply
td.email-view-date {{date}}
//- Compose, or reply on a card. Sent From the member's own board address, so
the answer comes back to this board (server/emailView.js sendBoardEmail).
template(name="emailComposePopup")
form.email-compose.js-email-compose-form
if replyTitle
p.quiet {{_ 'email-view-replying-to'}} {{replyTitle}}
label
| {{_ 'email-view-to'}}
input.js-email-to(type="text" value="{{to}}" placeholder="name@example.com" autocomplete="email" required)
label
| {{_ 'email-view-cc'}}
input.js-email-cc(type="text" autocomplete="email")
label
| {{_ 'email-view-subject'}}
input.js-email-subject(type="text" value="{{subject}}" required)
textarea.js-email-text(rows="10")
if error
p.warning {{error}}
button.primary(type="submit" disabled="{{#if sending}}disabled{{/if}}")
i.fa.fa-paper-plane
| &nbsp;{{_ 'email-view-send'}}
+236
View File
@@ -0,0 +1,236 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import { Utils } from '/client/lib/utils';
import { reportPageDocs } from '/client/lib/reportPages';
import { reportPageKey } from '/models/lib/reportPageIndex';
import { buildHeader, docsByIds, pageInfo } from '/models/lib/tablePage';
const { EMAIL_VIEW_PAGE_ID, emailViewPageArgs, parseFolder, previewMarkdown } = require('/models/lib/emailView');
// Board View / Email (emailView.jade, server/emailView.js). The folder, the
// page and the search are this view's state; the rows are the page the server
// named (the page index), never a re-slice of the board's cards in minimongo.
const COLUMNS = [
{ labelKey: 'email-view-from' },
{ labelKey: 'email-view-subject' },
{ labelKey: 'email-view-date', nowrap: true },
];
const SYSTEM_FOLDERS = [
{ id: 'inbox', icon: 'fa-inbox', labelKey: 'email-view-inbox' },
{ id: 'sent', icon: 'fa-paper-plane', labelKey: 'email-view-sent' },
{ id: 'all', icon: 'fa-envelope-o', labelKey: 'email-view-all' },
];
function formatDate(date) {
if (!date) return '';
const d = new Date(date);
const now = new Date();
// Today: the time; this year: day and month; older: the whole date.
if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString([], { day: 'numeric', month: 'short' });
return d.toLocaleDateString();
}
Template.emailView.onCreated(function () {
this.folder = new ReactiveVar('inbox');
this.page = new ReactiveVar(1);
this.searchTerm = new ReactiveVar('');
this.total = new ReactiveVar(0);
this.autorun(() => {
const boardId = Utils.getCurrentBoardId();
if (!boardId) return;
const options = { folder: this.folder.get(), page: this.page.get(), search: this.searchTerm.get() };
// New mail arrives as a new card on the board: the board's own
// subscription moves this count, and the page and total follow it.
Cards.find({ boardId }).count();
this.subscribe('emailViewPage', boardId, options);
Meteor.call('emailViewCount', boardId, options, (error, result) => {
if (!error && result) this.total.set(result.total);
});
});
});
function setFolder(tpl, folder) {
tpl.folder.set(folder);
tpl.page.set(1);
}
Template.emailView.helpers({
systemFolders() {
const current = Template.instance().folder.get();
return SYSTEM_FOLDERS.map(f => ({ ...f, active: f.id === current }));
},
listFolders() {
const board = Utils.getCurrentBoard();
const current = Template.instance().folder.get();
if (!board) return [];
return (ReactiveCache.getLists({ boardId: board._id, archived: false }, { sort: { sort: 1 } }) || [])
.map(l => ({ id: `list:${l._id}`, title: l.title, active: `list:${l._id}` === current }));
},
labelFolders() {
const board = Utils.getCurrentBoard();
const current = Template.instance().folder.get();
return ((board && board.labels) || [])
.map(l => ({ id: `label:${l._id}`, name: l.name || l.color, color: l.color, active: `label:${l._id}` === current }));
},
// Email to card off for this board: the Inbox only ever has what is put in
// the list by hand, and compose would be refused - say where to turn it on.
receiving() {
const board = Utils.getCurrentBoard();
return !!(board && board.emailToCard && board.emailToCard.enabled);
},
tablePageData() {
const tpl = Template.instance();
const info = pageInfo(tpl.total.get(), tpl.page.get());
const board = Utils.getCurrentBoard();
const labels = (board && board.labels) || [];
// The index of THIS request only: a previous search's index can still be
// in minimongo while its subscription stops.
const key = reportPageKey(EMAIL_VIEW_PAGE_ID, emailViewPageArgs(Utils.getCurrentBoardId(),
{ folder: tpl.folder.get(), page: tpl.page.get(), search: tpl.searchTerm.get() }));
const cards = reportPageDocs(Cards, key, docsByIds);
const docs = cards.map(card => {
const email = card.email || {};
const outgoing = email.direction === 'out';
const author = !email.from && card.userId ? ReactiveCache.getUser(card.userId) : null;
const sender = outgoing
? (email.to || []).join(', ')
: (email.from && (email.from.name || email.from.address))
|| (author && (author.profile && author.profile.fullname || author.username)) || '';
return {
_id: card._id,
title: card.title,
snippet: previewMarkdown(card.description),
outgoing,
sender,
date: formatDate(card.receivedAt || card.createdAt),
labels: (card.labelIds || []).map(id => labels.find(l => l._id === id)).filter(Boolean),
};
});
return {
header: buildHeader(COLUMNS),
rowTemplate: 'emailViewRow',
docs,
rowCount: docs.length,
total: tpl.total.get(),
searchTerm: tpl.searchTerm.get(),
page: info.page,
totalPages: info.totalPages,
hasPrev: info.hasPrev,
hasNext: info.hasNext,
emptyKey: 'email-view-empty',
};
},
});
Template.emailView.events({
'click .js-email-folder'(event, tpl) {
event.preventDefault();
setFolder(tpl, parseFolder(event.currentTarget.dataset.folder).kind === 'inbox'
? 'inbox' : event.currentTarget.dataset.folder);
},
'keydown .js-table-page-search'(event, tpl) {
if (event.key !== 'Enter') return;
event.preventDefault();
tpl.searchTerm.set(event.currentTarget.value || '');
tpl.page.set(1);
},
'click .js-table-page-prev'(event, tpl) {
event.preventDefault();
tpl.page.set(Math.max(1, tpl.page.get() - 1));
},
'click .js-table-page-next'(event, tpl) {
event.preventDefault();
const info = pageInfo(tpl.total.get(), tpl.page.get());
if (info.hasNext) tpl.page.set(tpl.page.get() + 1);
},
// A message opens as the card popup - its comments are the conversation.
'click .js-email-open'(event) {
if ($(event.target).closest('.js-email-reply').length) return;
const cardId = event.currentTarget.dataset.cardId;
const boardId = Utils.getCurrentBoardId();
if (!cardId) return;
event.preventDefault();
Meteor.subscribe('popupCardData', cardId, {
onReady() {
Session.set('popupCardId', cardId);
Session.set('popupCardBoardId', boardId);
if (!Popup.isOpen()) Popup.open('cardDetails')(event);
},
});
},
'click .js-email-compose'(event) {
Popup.open('emailCompose').call({}, event);
},
'click .js-email-reply'(event) {
event.preventDefault();
event.stopPropagation();
const card = ReactiveCache.getCard(event.currentTarget.dataset.cardId);
if (!card) return;
Popup.open('emailCompose').call({ currentData: () => ({ cardId: card._id }) }, event);
},
});
// --- Compose ---------------------------------------------------------
function replyCard() {
const data = Template.currentData() || {};
return data.cardId ? ReactiveCache.getCard(data.cardId) : null;
}
Template.emailComposePopup.onCreated(function () {
this.error = new ReactiveVar('');
this.sending = new ReactiveVar(false);
this.cardId = (Template.currentData() || {}).cardId || null;
});
Template.emailComposePopup.helpers({
error() { return Template.instance().error.get(); },
sending() { return Template.instance().sending.get(); },
replyTitle() {
const card = replyCard();
return card ? card.title : '';
},
// A reply goes back to whoever wrote, under "Re: ".
to() {
const card = replyCard();
const email = card && card.email;
if (!email) return '';
return email.direction === 'out' ? (email.to || []).join(', ') : ((email.from && email.from.address) || '');
},
subject() {
const card = replyCard();
if (!card) return '';
const subject = (card.email && card.email.subject) || card.title || '';
return /^re:/i.test(subject) ? subject : `Re: ${subject}`;
},
});
Template.emailComposePopup.events({
'submit .js-email-compose-form'(event, tpl) {
event.preventDefault();
const split = value => String(value || '').split(/[,;]/).map(s => s.trim()).filter(Boolean);
const input = {
to: split(tpl.$('.js-email-to').val()),
cc: split(tpl.$('.js-email-cc').val()),
subject: String(tpl.$('.js-email-subject').val() || ''),
text: String(tpl.$('.js-email-text').val() || ''),
};
if (tpl.cardId) input.cardId = tpl.cardId;
tpl.sending.set(true);
tpl.error.set('');
Meteor.call('sendBoardEmail', Utils.getCurrentBoardId(), input, error => {
tpl.sending.set(false);
if (error) {
tpl.error.set(TAPi18n.__(String(error.error || error.reason || 'email-view-send-failed')));
return;
}
Popup.back();
});
},
});
+9 -3
View File
@@ -17,6 +17,7 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { ReactiveCache } from '/imports/reactiveCache';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import { Accounts } from 'meteor/accounts-base';
import { BOARD_EXPORT_FIELDS } from '/models/lib/exportFields';
@@ -234,6 +235,9 @@ const EXPORT_FORMAT_GROUPS = [
path: 'exportZip',
ext: 'zip',
},
// Straight into Trello through its API (server/trelloApiExport.js), not
// a file: an action that opens its own popup.
{ key: 'trello-api', icon: 'fa-trello', label: 'Trello', action: 'js-export-to-trello', scopes: BOARD_ONLY },
{ key: 'kanboard', icon: 'fa-upload', label: 'Kanboard', path: 'export/kanboard', ext: 'json', scopes: BOARD_ONLY },
{ key: 'markdown', icon: 'fa-file-text-o', label: 'Markdown', path: 'export/markdown', ext: 'md', scopes: BOARD_ONLY },
{ key: 'leo', icon: 'fa-sitemap', label: 'Leo', path: 'export/leo', ext: 'leo', scopes: BOARD_ONLY },
@@ -320,10 +324,12 @@ function resolvedFormatGroups() {
// menus that offer the Import row have to ask the same question - four copies of
// four permission checks is how one of them ends up offering a row that then
// refuses to do anything.
//
// It asks the write capability, as the server does when the import runs. It used
// to test `!user.isWorker && ...` - the helper functions themselves, which are
// always truthy - so the Import row was offered to nobody.
function canImportIntoBoard() {
const user = ReactiveCache.getCurrentUser();
return Boolean(user && !user.isWorker && !user.isCommentOnly
&& !user.isReadOnly && !user.isReadAssignedOnly);
return Boolean(ReactiveCache.getCurrentUser() && Utils.currentUserCan('write'));
}
Template.registerHelper('canImportIntoBoard', canImportIntoBoard);
+1 -1
View File
@@ -42,4 +42,4 @@ template(name="mapImageUpload")
input.js-map-upload-input.hide(type="file" accept="image/png,image/jpeg,image/gif,image/webp")
button.primary.js-map-upload-button(type="button" disabled=uploading) {{_ 'map-view-upload'}}
if error
p.warning {{error}}
p.warning {{_ error}}
+7 -2
View File
@@ -130,12 +130,17 @@ Template.mapImageUpload.events({
});
uploader.on('end', err => {
tpl.uploading.set(false);
if (err) tpl.error.set(err.reason || err.message || 'upload-failed');
// The template translates the key; the reason is for the console.
if (err) {
if (process.env.DEBUG === 'true') console.warn('map image upload failed:', err);
tpl.error.set('upload-failed');
}
});
uploader.start();
} catch (error) {
tpl.uploading.set(false);
tpl.error.set((error && (error.reason || error.message)) || 'upload-failed');
if (process.env.DEBUG === 'true') console.warn('map image upload failed:', error);
tpl.error.set('upload-failed');
} finally {
input.value = '';
}
@@ -6,6 +6,7 @@ import { TAPi18n } from '/imports/i18n';
import { ReactiveVar } from 'meteor/reactive-var';
import { toFullCalendarFirstDay } from '/client/lib/calendarFirstDay';
import { weekNumberByFirstDay } from '/models/lib/weekStart';
import { calendarBaseOptions } from '/models/lib/calendarBaseOptions';
const { notHelperBoardTitle } = require('/models/lib/helperBoards');
// #2469: "Multi Board Calendar" - reuses #4223 Bigboard's exact
@@ -51,22 +52,21 @@ Template.multiboardCalendarView.helpers({
return !Template.instance().subsReady.get();
},
multiboardCalendarOptions() {
const t = (key, fallback) => {
const translated = TAPi18n.__(key);
return translated && translated !== key ? translated : fallback;
};
const currentUser = ReactiveCache.getCurrentUser();
const firstDay = toFullCalendarFirstDay(
currentUser ? currentUser.getStartDayOfWeek() : 1,
);
const displayOptions = calendarDateDisplayOptions('multiboard-calendar-view');
return {
id: 'multiboard-calendar-view',
...displayOptions,
initialView: displayOptions.initialView || 'dayGridMonth',
firstDay,
...calendarBaseOptions({
id: 'multiboard-calendar-view',
displayOptions: calendarDateDisplayOptions('multiboard-calendar-view'),
firstDay,
weekNumberOf: weekNumberByFirstDay,
translate: key => TAPi18n.__(key),
locale: TAPi18n.getLanguage(),
rtl: TAPi18n.isRTL(),
}),
// #2469 scope: cross-board drag-to-reschedule is out of scope for
// this pass (a card's board isn't necessarily the one currently
// open, and moving it needs more care than a single setDue call) -
@@ -74,39 +74,7 @@ Template.multiboardCalendarView.helpers({
// behaviour unchanged, and is unaffected by this view.
editable: false,
selectable: false,
weekNumbers: true,
weekNumberCalculation: date => weekNumberByFirstDay(date, firstDay),
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
slotLabelFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
headerToolbar: {
left: 'title',
center: '',
right:
'today prev,next timeGridDay,listDay timeGridWeek,listWeek dayGridMonth,listMonth',
},
buttonIcons: false,
buttonText: {
prev: t('previous', 'Previous'),
next: t('next', 'Next'),
today: t('today', 'Today'),
day: t('day', 'Day'),
week: t('week', 'Week'),
month: t('month', 'Month'),
list: t('list', 'List'),
},
height: 'auto',
navLinks: false,
nowIndicator: true,
locale: TAPi18n.getLanguage(),
direction: TAPi18n.isRTL() ? 'rtl' : 'ltr',
events(fetchInfo, callback) {
const filterSelector = Filter.isActive()
? Filter._getMongoSelector()
@@ -1,60 +0,0 @@
template(name="originalPositionsView")
.original-positions-view
.original-positions-header
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
i.fa.fa-history
if isShowingOriginalPositions
| Hide Original Positions
else
| Show Original Positions
if isShowingOriginalPositions
button.btn.btn-sm.btn-outline-primary.js-refresh-history
i.fa.fa-refresh
| Refresh
if isShowingOriginalPositions
.original-positions-content
if isLoading
.original-positions-loading
i.fa.fa-spinner.fa-spin
| Loading original positions...
else
.original-positions-filters
.btn-group.btn-group-sm(role="group")
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
| All
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
i.fa.fa-bars
| Swimlanes
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
i.fa.fa-columns
| Lists
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
i.fa.fa-sticky-note
| Cards
.original-positions-list
each getFilteredHistory
.original-position-item
.original-position-item-header
i.fa(class="{{getEntityTypeIcon entityType}}")
span.entity-type {{getEntityTypeLabel entityType}}
span.entity-name
+viewer
| {{getEntityDisplayName this}}
span.entity-id ({{entityId}})
.original-position-item-details
.original-position-description
| {{getEntityOriginalPositionDescription this}}
if originalTitle
.original-title
strong Original title:
+viewer
= originalTitle
.original-position-date
small.text-muted Created: {{formatDate createdAt}}
else
.no-original-positions
i.fa.fa-info-circle
| No original position data available for this board.
@@ -1,135 +0,0 @@
import { formatDateForDisplay } from '/client/lib/dateDisplay';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
/**
* Component to display original positions for all entities on a board
*/
Template.originalPositionsView.onCreated(function () {
this.showOriginalPositions = new ReactiveVar(false);
this.boardHistory = new ReactiveVar([]);
this.isLoading = new ReactiveVar(false);
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
const tpl = this;
this.loadBoardHistory = function () {
const boardId = Session.get('currentBoard');
if (!boardId) return;
tpl.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
tpl.isLoading.set(false);
if (error) {
console.error('Error loading board history:', error);
tpl.boardHistory.set([]);
} else {
tpl.boardHistory.set(result);
}
});
};
});
Template.originalPositionsView.onRendered(function () {
this.loadBoardHistory();
});
Template.originalPositionsView.helpers({
isShowingOriginalPositions() {
return Template.instance().showOriginalPositions.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
getBoardHistory() {
return Template.instance().boardHistory.get();
},
getFilteredHistory() {
const tpl = Template.instance();
const history = tpl.boardHistory.get();
const filterType = tpl.filterType.get();
if (filterType === 'all') {
return history;
}
return history.filter(item => item.entityType === filterType);
},
isFilterType(type) {
return Template.instance().filterType.get() === type;
},
getEntityDisplayName(entity) {
const position = entity.originalPosition || {};
return position.title || `Entity ${entity.entityId}`;
},
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
let description = `Position: ${position.sort || 0}`;
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
} else if (entity.entityType === 'card') {
if (entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
}
if (entity.originalListId) {
description += ` in list ${entity.originalListId}`;
}
}
return description;
},
getEntityTypeIcon(entityType) {
switch (entityType) {
case 'swimlane':
return 'fa-bars';
case 'list':
return 'fa-columns';
case 'card':
return 'fa-sticky-note';
default:
return 'fa-question';
}
},
getEntityTypeLabel(entityType) {
switch (entityType) {
case 'swimlane':
return 'Swimlane';
case 'list':
return 'List';
case 'card':
return 'Card';
default:
return 'Unknown';
}
},
formatDate(date) {
return formatDateForDisplay(date, true, value => value.toLocaleString());
},
});
Template.originalPositionsView.events({
'click .js-toggle-original-positions'(evt, tpl) {
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
},
'click .js-refresh-history'(evt, tpl) {
tpl.loadBoardHistory();
},
'click .js-filter-type'(evt, tpl) {
const type = evt.currentTarget.dataset.filterType;
tpl.filterType.set(type);
},
});
+3 -3
View File
@@ -1,5 +1,5 @@
/* /public — the Public Boards table.
* Design: docs/Features/Page/Public.md, which is docs/Features/Page/Table.md.
* Design: docs/Features/All-Boards/Public.md, which is docs/Features/General/Table-Page.md.
*
* ONLY the row's board colouring lives here. The table itself — its width, its
* columns, its cells, its controls row and its stacking below 800px — is
@@ -23,9 +23,9 @@
* so the row keeps the table's white background: with the light text below that
* is white on white, and only the row's emoji was visible. The All Boards tile
* has the same problem and solves it the same way — `.board-list-item` falls
* back to a grey (#999) so its white text always has something to sit on. */
* back to a grey (#767676) so its white text always has something to sit on. */
.public-board-row {
background-color: #999;
background-color: #767676;
background-size: cover;
background-position: center;
cursor: pointer;
+2 -2
View File
@@ -1,6 +1,6 @@
//- /public — the boards anybody may open.
//- Design: docs/Features/Page/Public.md, which is the Table page design
//- (docs/Features/Page/Table.md).
//- Design: docs/Features/All-Boards/Public.md, which is the Table page design
//- (docs/Features/General/Table.md).
//-
//- The whole page is the shared table. It used to render `boardList`, the All
//- Boards page, with its query swapped for `{ permission: 'public' }` — which
+7 -3
View File
@@ -2,6 +2,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Boards from '/models/boards';
import { ReportPages } from '/client/lib/reportPages';
import { reportPageKey } from '/models/lib/reportPageIndex';
import {
buildHeader,
docsByIds,
@@ -10,7 +11,7 @@ import {
} from '/models/lib/tablePage';
// /public — the boards anybody may open, as a table page.
// Design: docs/Features/Page/Public.md (which is docs/Features/Page/Table.md).
// Design: docs/Features/All-Boards/Public.md (which is docs/Features/General/Table.md).
//
// The page is ONLY the table. It used to render `boardList`, the All Boards page,
// with its query swapped for `{ permission: 'public' }` — which brought the
@@ -62,7 +63,10 @@ Template.publicBoards.helpers({
// a plain find(): every board this user has opened is also in minimongo, and
// reading the collection would draw those too — the pager would say "1 / 3"
// beside rows from all three pages.
const index = ReportPages.findOne(PAGE_ID);
// The index of THIS request (models/lib/reportPageIndex.js reportPageKey),
// built from the same arguments the autorun subscribed with.
const index = ReportPages.findOne(reportPageKey(PAGE_ID,
[tpl.searchTerm.get() || '', TABLE_PAGE_ROWS_PER_PAGE, (tpl.page.get() - 1) * TABLE_PAGE_ROWS_PER_PAGE]));
const ids = (index && index.ids) || [];
const boards = ids.length
? docsByIds(ids, Boards.find({ _id: { $in: ids } }).fetch())
@@ -88,7 +92,7 @@ Template.publicBoards.helpers({
// `boardListHeaderBar`, whose `h1` says "Public". The shared table page
// prints a title only when one is supplied, precisely so a page that
// already has a heading does not show two - the same reason the Admin
// Panel panes supply none (docs/Features/Page/Table.md).
// Panel panes supply none (docs/Features/General/Table.md).
header: buildHeader(COLUMNS),
// A row template, not `rows`: see COLUMNS above.
rowTemplate: 'publicBoardRow',
@@ -52,8 +52,14 @@ const boardFor = context => ReactiveCache.getBoard(context?.record?.boardId || S
// `field` (a definition suffix, 'Sprint') draws one field alone: on a card and
// a minicard each Scrum field sits where the board's Board Settings / Card
// order puts it (models/lib/cardFieldOrder.js SCRUM_FIELD_KEYS).
// Read through the board as that side sees it (models/lib/cardShownFields.js
// boardForSide): a Scrum section hidden whole on Show on Card or Show on
// Minicard in Card Settings hides these fields there too. The settings above
// (scrumFieldSettings) read the board itself.
function visibleFields(context) {
const board = boardFor(context);
const { boardForSide } = require('/models/lib/cardShownFields');
const side = context?.kind === 'card' || context?.kind === 'minicard' ? context.kind : null;
const board = side ? boardForSide(boardFor(context), side) : boardFor(context);
return (definitions[context?.kind] || []).filter(([suffix]) => (!context.field || context.field === suffix) &&
board?.scrum?.visibility?.[`${context.kind}${suffix}`] === true);
}
@@ -59,7 +59,7 @@ template(name="scrumView")
select(name="estimateSource")
each estimateSources
option(value=value selected=selected) {{label}}
label {{_ 'custom-field'}}
label {{_ 'custom-fields'}}
select(name="estimateCustomFieldId")
option(value="") —
each estimateFields
@@ -141,7 +141,7 @@ template(name="scrumView")
textarea(name="goal") {{selectedSprint.goal}}
label {{_ 'start'}}
input(type="date" name="plannedStart" value=sprintStart)
label {{_ 'end'}}
label {{_ 'card-end'}}
input(type="date" name="plannedEnd" value=sprintEnd)
label {{_ 'scrum-capacity'}}
input(type="number" name="capacity" min="0" step="any" value=selectedSprint.capacity)
@@ -174,7 +174,7 @@ template(name="scrumView")
textarea(name="goal") {{selectedRelease.goal}}
label {{_ 'start'}}
input(type="date" name="plannedStart" value=releaseStart)
label {{_ 'end'}}
label {{_ 'card-end'}}
input(type="date" name="plannedEnd" value=releaseEnd)
label {{_ 'status'}}
select(name="state")
@@ -241,7 +241,7 @@ template(name="scrumView")
button.primary(type="submit" disabled=busy) {{_ 'save'}}
else
tr
td(colspan="6") {{_ 'no-cards'}}
td(colspan="6") {{_ 'no-cards-found'}}
if hiddenCards
button.js-scrum-show-more(type="button") {{showMoreLabel}}
if isSprints
+17
View File
@@ -83,11 +83,28 @@
color: #333;
}
/* The table, not the board, scrolls sideways when its columns are wider than
the window. At 800px and below the table scrolls itself (display: block);
just above that - an iPad or an unfolded phone held upright, 834-928px - it
did not, the board canvas around it is overflow-x: hidden, and Members, Labels
and the date columns were cut off with no way to reach them.
tests/playwright/specs/viewport-matrix.e2e.js checks every column is reachable. */
.table-view-wrapper {
max-width: 100%;
overflow-x: auto;
}
.table-view-controls .js-table-view-toggle-card-title-wrap,
.table-view-controls .js-table-view-toggle-swimlane-groups {
box-sizing: border-box;
width: 34px;
height: 34px;
/* The global `button` rule (forms.css) pads every button 35px on each side
and paints its text white. On these white toggles that made the icon
white on white - two empty squares - and the padding stretched the 34px
box to 72px. tests/playwright/specs/viewport-matrix.e2e.js checks both. */
padding: 0;
color: #333;
line-height: 1;
border: 1px solid #bbb;
background: #fff;
+15 -3
View File
@@ -1,6 +1,6 @@
template(name="tableView")
if currentBoard
.wrapper
.wrapper.table-view-wrapper
.table-view-controls
.table-view-search-box
input.js-table-view-search(type="text" placeholder="{{_ 'search'}}" aria-label="{{_ 'search'}}")
@@ -57,11 +57,12 @@ template(name="tableView")
each row in rows
if row.isGroupHeader
tr.table-view-swimlane-group
th(colspan="11" scope="rowgroup")
th(colspan=columnCount scope="rowgroup")
+viewer
= row.swimlaneTitle
else
tr
//- #864: a row is dragged onto another to move its card there.
tr.js-table-view-row(draggable="{{#if canWrite}}true{{/if}}" data-card-id=row.card._id)
td.table-view-cell-edit
a.edit-card.js-table-view-edit-card(href=row.card.originRelativeUrl data-card-id=row.card._id title="{{_ 'edit'}}")
i.fa.fa-pencil-square-o
@@ -131,3 +132,14 @@ template(name="tableView")
if canModifyCard
a.card-label.add-label.js-end-date(title="{{_ 'card-end'}}")
i.fa.fa-plus
//- #864: add a card from the table: its title, list and swimlane.
if canWrite
form.table-view-add-card.js-table-view-add-card
input.js-table-view-add-card-title(type="text" placeholder="{{_ 'add-card'}}" aria-label="{{_ 'add-card'}}" required)
select.js-table-view-add-card-list(aria-label="{{_ 'list'}}")
each boardLists
option(value=_id) {{title}}
select.js-table-view-add-card-swimlane(aria-label="{{_ 'swimlane'}}")
each boardSwimlanes
option(value=_id) {{title}}
button.primary.js-table-view-add-card-button(type="submit") {{_ 'add-card'}}
+77
View File
@@ -3,6 +3,7 @@ import { Random } from 'meteor/random';
import { isLazyCards } from '/client/lib/lazyCards';
import { ReactiveCache } from '/imports/reactiveCache';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import { Filter } from '/client/lib/filter';
import { tableViewCardsSelector, tableCardWithDates } from '/models/lib/tableViewFilter';
import {
@@ -163,6 +164,20 @@ Template.tableView.helpers({
return Utils.getCurrentBoard();
},
// #864: dragging rows and adding cards need the right to write.
canWrite() {
const board = Utils.getCurrentBoard();
return !!board && Utils.currentUserCan('write', board);
},
boardLists() {
const board = Utils.getCurrentBoard();
return board ? ReactiveCache.getLists({ boardId: board._id, archived: false }, { sort: { sort: 1 } }) : [];
},
boardSwimlanes() {
const board = Utils.getCurrentBoard();
return board ? ReactiveCache.getSwimlanes({ boardId: board._id, archived: false }, { sort: { sort: 1 } }) : [];
},
rows() {
const tpl = Template.instance();
const all = tpl.filteredRows.get();
@@ -225,9 +240,71 @@ Template.tableView.helpers({
const board = Utils.getCurrentBoard();
return !!board && (board.allowsEndDate || board.allowsEndDateOnMinicard);
},
// A swimlane header row spans the whole table: Edit, Card, List and Swimlane,
// then - except on a small screen - Assignees, Members, Labels and each date
// column the board shows. A fixed 11 left it one short with all four dates
// and too wide on a phone.
columnCount() {
if (Utils.isMiniScreen()) return 4;
const board = Utils.getCurrentBoard();
const dates = !board ? 0 : ['Received', 'Start', 'Due', 'End']
.filter(kind => board[`allows${kind}Date`] || board[`allows${kind}DateOnMinicard`]).length;
return 7 + dates;
},
});
Template.tableView.events({
// #864: drag a row onto another: its card goes just before that card, in
// that card's list and swimlane (models/lib/tableViewDrop.js), through
// the same card.move the board uses.
'dragstart .js-table-view-row'(event) {
const dt = event.originalEvent && event.originalEvent.dataTransfer;
if (dt) {
dt.setData('text/plain', event.currentTarget.dataset.cardId);
dt.effectAllowed = 'move';
}
},
'dragover .js-table-view-row'(event) {
event.preventDefault();
},
async 'drop .js-table-view-row'(event) {
event.preventDefault();
const dt = event.originalEvent && event.originalEvent.dataTransfer;
const draggedId = dt && dt.getData('text/plain');
const dragged = draggedId && ReactiveCache.getCard(draggedId);
const target = ReactiveCache.getCard(event.currentTarget.dataset.cardId);
if (!dragged || !target) return;
const { tableViewDropTarget } = require('/models/lib/tableViewDrop');
const to = tableViewDropTarget(dragged, target, ReactiveCache.getCards({ listId: target.listId,
swimlaneId: target.swimlaneId, archived: false }));
if (!to) return;
try {
await dragged.move(to.boardId, to.swimlaneId, to.listId, to.sort);
} catch (error) {
console.error('Table view move failed:', error);
}
},
async 'submit .js-table-view-add-card'(event, tpl) {
event.preventDefault();
const board = Utils.getCurrentBoard();
const input = tpl.$('.js-table-view-add-card-title');
const title = String(input.val() || '').trim();
if (!board || !title) return;
try {
await Meteor.callAsync('tableViewAddCard', { boardId: board._id, title,
listId: String(tpl.$('.js-table-view-add-card-list').val() || ''),
swimlaneId: String(tpl.$('.js-table-view-add-card-swimlane').val() || '') });
input.val('');
} catch (error) {
// tableViewAddCard refuses with an error code; show it in the reader's
// language rather than the code or an English sentence.
const key = error?.error === 'not-found' ? 'error-list-doesNotExist' : 'error-notAllowed';
alert(TAPi18n.__(key));
}
},
'click .js-table-view-search-button'(event, tpl) {
event.preventDefault();
tpl.searchQuery.set(tpl.$('.js-table-view-search').val() || '');
+3 -2
View File
@@ -46,8 +46,9 @@ template(name="timelineView")
p.timeline-card-due
| {{_ 'due-date'}}: {{this.formattedDueAt}}
if selectedTimestamp
button.timeline-card-restore.js-restore-card-timeline.primary(type="button")
| {{_ 'board-view-timeline-restore'}}
if canRestoreCard this.cardId
button.timeline-card-restore.js-restore-card-timeline.primary(type="button")
| {{_ 'board-view-timeline-restore'}}
template(name="cardRestoreToTimelinePopup")
p {{_ 'board-view-timeline-restore-confirm'}}
+13 -1
View File
@@ -3,6 +3,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import { reconstructBoardStateAt } from '/models/lib/boardTimeline';
// Board View / Timeline (Step 2 of the maintainer's spec, Step 1 wiring):
@@ -91,6 +92,13 @@ function boardActivities() {
}
Template.timelineView.helpers({
// Restoring writes the card through its own setters, so it is offered only
// to those who may change that card now - the same rule as everywhere else
// (role, list exceptions, assigned-only).
canRestoreCard(cardId) {
const card = ReactiveCache.getCard(cardId);
return !!card && Utils.canModifyCard(card);
},
selectedTimestamp() {
return Template.instance().selectedTimestamp.get();
},
@@ -259,7 +267,11 @@ Template.timelineView.events({
'click .js-restore-card-timeline': Popup.afterConfirm('cardRestoreToTimeline', function () {
const cardContext = this;
if (cardContext && cardContext.cardId) {
restoreCardFromTimeline(cardContext.cardId, cardContext.fields);
// A refused write used to vanish: nothing awaited the restore.
restoreCardFromTimeline(cardContext.cardId, cardContext.fields).catch(error => {
if (process.env.DEBUG === 'true') console.warn('timeline restore failed:', error);
alert(TAPi18n.__('error-notAllowed'));
});
}
}),
});
@@ -0,0 +1,515 @@
/* Board View / Version Control: GitHub's and Gitea's repository layout - a
header, tabs, a bordered box of issues - in the board's own theme. No colour
of its own: the accent is the theme's (--theme-accent, falling back to
WeKan blue, as emailView.css does), a label is the board label's own colour,
and the boxes are drawn with currentColor, so a dark theme stays dark.
Logical properties, so a right-to-left language mirrors it. */
.vcs-view {
padding: 12px 16px;
min-height: 100%;
box-sizing: border-box;
max-width: 1280px;
}
.vcs-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.vcs-repo {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 1.3em;
}
.vcs-repo-name {
font-weight: bold;
color: var(--theme-accent, #01628c);
}
.vcs-badge {
font-size: 0.65em;
font-weight: normal;
padding: 1px 8px;
border: 1px solid currentColor;
border-radius: 2em;
opacity: 0.75;
}
.vcs-header-actions,
.vcs-import {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.vcs-import-link,
.vcs-button {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
border-radius: 6px;
color: inherit;
text-decoration: none;
}
.vcs-import-link:hover,
.vcs-button:hover {
background: color-mix(in srgb, currentColor 8%, transparent);
}
.vcs-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin: 12px 0 16px;
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.vcs-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-bottom: 2px solid transparent;
color: inherit;
text-decoration: none;
}
.vcs-tab.is-active {
border-bottom-color: var(--theme-accent, #01628c);
font-weight: bold;
}
.vcs-counter {
min-width: 1.5em;
padding: 0 6px;
border-radius: 2em;
font-size: 0.85em;
text-align: center;
background: color-mix(in srgb, currentColor 12%, transparent);
}
.vcs-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.vcs-search {
flex: 1 1 280px;
margin: 0 !important;
}
.vcs-new,
.vcs-view button.primary {
background: var(--theme-accent, #01628c) !important;
color: #fff !important;
}
.vcs-box {
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 6px;
overflow: hidden;
}
.vcs-box-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
padding: 10px 16px;
background: color-mix(in srgb, currentColor 5%, transparent);
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.vcs-state {
color: inherit;
text-decoration: none;
opacity: 0.7;
}
.vcs-state.is-active {
font-weight: bold;
opacity: 1;
}
.vcs-sort {
margin: 0 0 0 auto !important;
margin-inline-start: auto !important;
width: auto !important;
}
.vcs-issues,
.vcs-milestones,
.vcs-labels {
list-style: none;
margin: 0;
padding: 0;
}
.vcs-issue,
.vcs-milestone,
.vcs-label-row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 16px;
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
}
.vcs-issue:first-child,
.vcs-milestone:first-child,
.vcs-label-row:first-child {
border-top: 0;
}
.vcs-issue:hover {
background: color-mix(in srgb, currentColor 4%, transparent);
}
.vcs-issue-icon {
margin-top: 3px;
}
/* Open and closed are told apart by the icon as well as the colour. */
.vcs-issue-icon.is-open,
.vcs-state-badge.is-open {
color: #1a7f37;
}
.vcs-issue-icon.is-closed,
.vcs-state-badge.is-closed {
color: #8250df;
}
.vcs-issue-main,
.vcs-milestone-main {
flex: 1 1 auto;
min-width: 0;
}
.vcs-issue-title,
.vcs-milestone-title {
font-weight: bold;
color: inherit;
text-decoration: none;
margin-inline-end: 6px;
}
.vcs-issue-title:hover,
.vcs-milestone-title:hover {
color: var(--theme-accent, #01628c);
text-decoration: underline;
}
.vcs-label {
display: inline-block;
margin: 2px 4px 2px 0;
text-decoration: none;
}
.vcs-issue-meta {
font-size: 0.85em;
margin-top: 2px;
}
.vcs-issue-milestone {
margin-inline-start: 8px;
color: inherit;
}
.vcs-issue-side {
display: flex;
align-items: center;
gap: 6px;
flex: 0 0 auto;
}
.vcs-issue-side .member {
width: 22px;
height: 22px;
}
.vcs-empty {
padding: 24px 16px;
text-align: center;
}
.vcs-milestone-progress {
flex: 0 0 40%;
}
.vcs-progress {
height: 8px;
border-radius: 4px;
overflow: hidden;
background: color-mix(in srgb, currentColor 12%, transparent);
}
.vcs-progress-bar {
height: 100%;
background: #1a7f37;
}
.vcs-overdue {
color: #cf222e;
}
.vcs-files {
width: 100%;
border-collapse: collapse;
}
.vcs-files td {
padding: 8px 16px;
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
}
/* One issue */
.vcs-back {
color: inherit;
}
.vcs-issue-heading {
margin: 8px 0 4px;
font-size: 1.6em;
font-weight: normal;
}
.vcs-issue-status {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding-bottom: 12px;
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.vcs-state-badge {
padding: 4px 12px;
border: 1px solid currentColor;
border-radius: 2em;
font-weight: bold;
}
.vcs-issue-layout {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-top: 16px;
}
.vcs-timeline {
flex: 1 1 520px;
min-width: 0;
}
.vcs-sidebar {
flex: 0 1 240px;
}
.vcs-side-block {
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent);
}
.vcs-side-block h3 {
margin: 0 0 6px;
font-size: 0.9em;
opacity: 0.8;
}
.vcs-comment {
margin-bottom: 16px;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 6px;
}
.vcs-comment-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: color-mix(in srgb, currentColor 5%, transparent);
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.vcs-comment-header .member {
width: 22px;
height: 22px;
}
.vcs-comment-body {
padding: 8px 12px;
}
.vcs-comment-form textarea,
.vcs-new-issue textarea,
.vcs-new-issue input[type="text"] {
width: 100%;
box-sizing: border-box;
}
.vcs-comment-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.vcs-new-issue {
max-width: 900px;
}
.vcs-new-labels {
border: 0;
padding: 0;
margin: 8px 0;
}
.vcs-new-label {
display: inline-flex;
align-items: center;
gap: 4px;
margin-inline-end: 12px;
}
@media (max-width: 640px) {
.vcs-milestone {
flex-wrap: wrap;
}
.vcs-milestone-progress {
flex-basis: 100%;
}
.vcs-sidebar {
flex-basis: 100%;
}
}
.vcs-synced {
align-self: center;
margin-inline-end: auto;
}
/* Releases and the wiki, as GitHub draws them. */
.vcs-releases {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 16px;
}
.vcs-wiki {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
.vcs-wiki-main {
flex: 1 1 560px;
min-width: 0;
}
.vcs-wiki-side {
flex: 0 1 260px;
display: grid;
gap: 16px;
}
.vcs-wiki-tools {
display: flex;
gap: 16px;
}
.vcs-wiki-tools a {
color: inherit;
}
.vcs-wiki-footer {
margin-top: 24px;
padding-top: 12px;
border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.vcs-wiki-pages {
list-style: none;
margin: 0;
padding: 0;
}
.vcs-wiki-pages li a {
display: block;
padding: 6px 16px;
color: inherit;
text-decoration: none;
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
}
.vcs-wiki-pages li.is-active a {
font-weight: bold;
color: var(--theme-accent, #01628c);
}
.vcs-wiki-sidebar {
padding: 8px 16px;
}
/* Titles, labels and milestones that open or filter are spans with
role=link (a rendered title can hold a link of its own). */
.vcs-view [role="link"] {
cursor: pointer;
}
.vcs-view [role="link"]:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Attachments: the controls of an opened card's attachments. */
.vcs-file-view {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.vcs-file-view:hover,
.vcs-file-view:focus-visible {
text-decoration: underline;
}
.vcs-file-thumbnail {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 3px;
}
.vcs-file-actions {
white-space: nowrap;
}
.vcs-file-actions a {
margin-inline-end: 8px;
color: inherit;
cursor: pointer;
}
@@ -0,0 +1,381 @@
//- Board View / Version Control: the board as a GitHub / Gitea repository's
issue tracker, in the board's own theme. Design:
docs/Features/Board-Header/Board-Views/Version-Control-View.md. A card is an issue, the board's
labels are its labels, a label with a due date is a milestone, the comments
are the conversation, the attachments are the files and the board's change
history is the history, with its undo.
template(name="versionControlView")
.vcs-view
header.vcs-header
h2.vcs-repo
i.fa.fa-code-fork
span.vcs-repo-name
+inlineViewer
= boardTitle
if isPublic
span.vcs-badge {{_ 'public'}}
.vcs-header-actions
if canWrite
.vcs-import
span.quiet {{_ 'import'}}:
each importSources
a.vcs-import-link(href="{{pathFor 'import' source=key}}") {{name}}
if canWrite
each syncedLists
button.vcs-button.js-vcs-sync(type="button" data-list-id="{{_id}}" disabled="{{#if busy}}disabled{{/if}}" title="{{source}} {{project}}{{#if error}} - {{error}}{{/if}}")
i.fa.fa-refresh(class="{{#if busy}}fa-spin{{/if}}")
| &nbsp;{{_ 'vcs-sync-now' source}}:
+inlineViewer
= title
a.vcs-button.js-vcs-export(href="#")
i.fa.fa-download
| &nbsp;{{_ 'export-board'}}
nav.vcs-tabs(role="tablist" aria-label="{{_ 'board-view-version-control'}}")
each tabs
a.vcs-tab.js-vcs-tab(href="#" role="tab" data-tab="{{id}}" aria-selected="{{#if active}}true{{else}}false{{/if}}" class="{{#if active}}is-active{{/if}}")
i.fa(class="{{icon}}")
span {{_ labelKey}}
if count
span.vcs-counter {{count}}
if isTab 'issues'
if creating
+versionControlNewIssue
else if selectedIssue
+versionControlIssue(selectedIssue)
else
.vcs-toolbar
input.vcs-search.js-vcs-search(type="search" value="{{query}}" aria-label="{{_ 'search'}}" placeholder="is:open label:bug milestone:v1 author:@me")
a.vcs-button.js-vcs-tab(href="#" data-tab="labels")
i.fa.fa-tag
| &nbsp;{{_ 'labels'}}
a.vcs-button.js-vcs-tab(href="#" data-tab="milestones")
i.fa.fa-map-signs
| &nbsp;{{_ 'vcs-milestones'}}
if canWrite
button.primary.vcs-new.js-vcs-new(type="button") {{_ 'vcs-new-issue'}}
.vcs-box
.vcs-box-header
a.vcs-state.js-vcs-state(href="#" data-state="open" class="{{#if stateIs 'open'}}is-active{{/if}}")
i.fa.fa-dot-circle-o
| &nbsp;{{counts.open}} {{_ 'vcs-open'}}
a.vcs-state.js-vcs-state(href="#" data-state="closed" class="{{#if stateIs 'closed'}}is-active{{/if}}")
i.fa.fa-check
| &nbsp;{{counts.closed}} {{_ 'vcs-closed'}}
select.vcs-sort.js-vcs-sort(aria-label="{{_ 'sort'}}")
each sorts
option(value="{{id}}" selected="{{#if active}}selected{{/if}}") {{_ labelKey}}
ul.vcs-issues
each issues
li.vcs-issue
i.fa.vcs-issue-icon(class="{{#if closed}}fa-check-circle is-closed{{else}}fa-dot-circle-o is-open{{/if}}" aria-hidden="true")
.vcs-issue-main
//- Rendered titles and labels can hold links, and a link
inside a link is not one: the clickable ones are spans.
span.vcs-issue-title.js-vcs-open-issue(role="link" tabindex="0" data-card-id="{{_id}}")
+inlineViewer
= title
each labels
span.card-label.vcs-label.js-vcs-label(role="link" tabindex="0" data-name="{{name}}" class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
+inlineViewer
= name
.vcs-issue-meta.quiet
if number
| {{numberText}}&nbsp;
| {{_ 'vcs-opened-by' date author}}
each milestones
span.vcs-issue-milestone.js-vcs-milestone(role="link" tabindex="0" data-title="{{title}}")
i.fa.fa-map-signs
| &nbsp;
+inlineViewer
= title
.vcs-issue-side
each assignees
+userAvatar(userId=this noRemove=true)
if comments
span.vcs-issue-comments(title="{{_ 'comments'}}")
i.fa.fa-comment-o
| &nbsp;{{comments}}
else
li.vcs-empty.quiet {{_ 'vcs-no-issues'}}
if isTab 'milestones'
.vcs-box
ul.vcs-milestones
each milestones
li.vcs-milestone
.vcs-milestone-main
span.vcs-milestone-title.js-vcs-milestone(role="link" tabindex="0" data-title="{{title}}")
+inlineViewer
= title
.quiet(class="{{#if overdue}}vcs-overdue{{/if}}")
if dueAt
i.fa.fa-calendar
| &nbsp;{{_ 'vcs-due' due}}
else
| {{_ 'vcs-no-due-date'}}
.vcs-milestone-progress
.vcs-progress(role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{percent}}")
.vcs-progress-bar(style="width: {{percent}}%")
.quiet {{percent}}% · {{open}} {{_ 'vcs-open'}} · {{closed}} {{_ 'vcs-closed'}}
else
li.vcs-empty.quiet {{_ 'vcs-no-milestones'}}
if isTab 'labels'
.vcs-box
ul.vcs-labels
each labelRows
li.vcs-label-row
span.card-label.vcs-label.js-vcs-label(role="link" tabindex="0" data-name="{{name}}" class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
if name
+inlineViewer
= name
else
| {{color}}
span.quiet {{_ 'vcs-open-issues' open}}
else
li.vcs-empty.quiet {{_ 'vcs-no-labels'}}
if isTab 'releases'
ul.vcs-releases
each releases
li.vcs-box.vcs-release
.vcs-comment-header
i.fa.fa-tag
strong
+inlineViewer
= title
span.quiet {{date}}
.vcs-comment-body
+viewer
= body
if files.length
h4 {{_ 'attachments'}}
each files
p
i.fa.fa-file-archive-o
|
a(href="{{link}}" target="_blank" rel="noopener noreferrer") {{name}}
else
li.vcs-empty.quiet {{_ 'vcs-no-releases'}}
if isTab 'wiki'
if wikiPage
.vcs-wiki
.vcs-wiki-main
with wikiPage
h2.vcs-issue-heading
+inlineViewer
= title
p.vcs-wiki-tools
a.js-vcs-wiki-history(href="#")
i.fa.fa-history
| {{_ 'history'}}
if canWrite
a.js-vcs-wiki-edit(href="#")
i.fa.fa-pencil
| {{_ 'edit'}}
if wikiHistory
//- The page's history: each commit's edit, and Restore brings
an older text back - the card's own history table.
with wikiPage
+historyTable(scope="card" scopeId=_id)
else
.vcs-wiki-body
with wikiPage
+viewer
= text
if wikiFooter
.vcs-wiki-footer
+viewer
= wikiFooter
aside.vcs-wiki-side
.vcs-box
.vcs-box-header
strong {{_ 'vcs-wiki-pages'}}
ul.vcs-wiki-pages
each wikiPages
li(class="{{#if active}}is-active{{/if}}")
span.js-vcs-wiki-page(role="link" tabindex="0" data-card-id="{{_id}}")
+inlineViewer
= title
if wikiSidebar
.vcs-box.vcs-wiki-sidebar
.vcs-wiki-body
+viewer
= wikiSidebar
else
p.vcs-empty.quiet {{_ 'vcs-no-wiki'}}
if isTab 'files'
.vcs-box
table.vcs-files
tbody
each files
//- The controls of an opened card's attachments
(cards/attachments.jade attachmentGallery): a click on the
thumbnail or the name views it in the same viewer, walking the
tab's files; then download, rename, delete and the cover /
background menu, for the same members.
tr.vcs-file(data-attachment-id="{{_id}}")
td.vcs-file-name
if canPreview
button.vcs-file-view.js-vcs-file-view(type="button" data-attachment-id="{{_id}}" title="{{_ 'preview'}}" aria-label="{{_ 'preview'}}: {{cleanFilename name}}")
if isImage
img.vcs-file-thumbnail(src="{{attachmentPreviewUrl attachment}}" alt="" loading="lazy")
else
i.fa.fa-file-o
| &nbsp;{{cleanFilename name}}
else
i.fa.fa-file-o
| &nbsp;
a(href="{{link}}" target="_blank" rel="noopener noreferrer") {{name}}
td.quiet {{size}}
td.vcs-file-actions
if canPreview
a.js-vcs-file-view(data-attachment-id="{{_id}}" title="{{_ 'preview'}}")
i.fa.fa-search-plus
a.js-download(href="{{link}}?download=true" download="{{downloadFilename name}}" title="{{_ 'download'}}")
i.fa.fa-arrow-down
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-vcs-file-rename(data-attachment-id="{{_id}}" title="{{_ 'rename'}}")
i.fa.fa-pencil-square-o
a.js-vcs-file-delete(data-attachment-id="{{_id}}" title="{{_ 'delete'}}")
i.fa.fa-trash
a.js-vcs-file-menu(data-attachment-id="{{_id}}" title="{{_ 'attachmentActionsPopup-title'}}")
i.fa.fa-bars
td
span.js-vcs-open-issue(role="link" tabindex="0" data-card-id="{{cardId}}")
if number
| {{numberText}}&nbsp;
+inlineViewer
= issueTitle
td.quiet {{date}}
else
tr
td.vcs-empty.quiet {{_ 'vcs-no-files'}}
if isTab 'history'
//- The board's change history, as git log: who changed what, when -
and Restore undoes it, through the same history every board has.
+historyTable(scope="board" scopeId=boardId)
//- One issue: the conversation on the left, labels, people, milestone and
files on the right - GitHub's issue page.
template(name="versionControlIssue")
.vcs-issue-page
a.vcs-back.js-vcs-back(href="#")
i.fa.fa-arrow-left
| &nbsp;{{_ 'vcs-issues'}}
h2.vcs-issue-heading
+inlineViewer
= title
| &nbsp;
if number
span.quiet {{numberText}}
.vcs-issue-status
span.vcs-state-badge(class="{{#if closed}}is-closed{{else}}is-open{{/if}}")
i.fa(class="{{#if closed}}fa-check-circle{{else}}fa-dot-circle-o{{/if}}")
| &nbsp;{{#if closed}}{{_ 'vcs-closed'}}{{else}}{{_ 'vcs-open'}}{{/if}}
span.quiet {{_ 'vcs-opened-by' date author}} · {{_ 'vcs-comment-count' comments}}
.vcs-issue-layout
.vcs-timeline
.vcs-comment
.vcs-comment-header
strong {{author}}
span.quiet &nbsp;{{date}}
.vcs-comment-body
if body
+viewer
= body
else
p.quiet {{_ 'vcs-no-description'}}
each conversation
.vcs-comment
.vcs-comment-header
+userAvatar(userId=userId noRemove=true)
strong {{author}}
span.quiet &nbsp;{{time}}
.vcs-comment-body
+viewer
= text
if canComment
form.vcs-comment-form.js-vcs-comment
textarea.js-vcs-comment-text(rows="4" aria-label="{{_ 'vcs-leave-comment'}}" placeholder="{{_ 'vcs-leave-comment'}}")
.vcs-comment-actions
if synced
span.quiet.vcs-synced
i.fa.fa-refresh
| &nbsp;{{_ 'vcs-synced-from' syncSource}}
else if canWrite
if closed
button.js-vcs-set-state(type="button" data-state="open")
i.fa.fa-dot-circle-o
| &nbsp;{{_ 'vcs-reopen'}}
else
button.js-vcs-set-state(type="button" data-state="closed")
i.fa.fa-check-circle
| &nbsp;{{_ 'vcs-close'}}
button.primary(type="submit") {{_ 'comment'}}
aside.vcs-sidebar
.vcs-side-block
h3 {{_ 'assignee'}}
each assignees
+userAvatar(userId=this noRemove=true)
else
p.quiet {{_ 'vcs-none'}}
.vcs-side-block
h3 {{_ 'labels'}}
each labels
span.card-label.vcs-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
+inlineViewer
= name
else
p.quiet {{_ 'vcs-none'}}
.vcs-side-block
h3 {{_ 'vcs-milestones'}}
each milestones
p
i.fa.fa-map-signs
| &nbsp;
+inlineViewer
= title
else
p.quiet {{_ 'vcs-none'}}
.vcs-side-block
h3 {{_ 'attachments'}}
each issueFiles
p
a(href="{{link}}" target="_blank" rel="noopener noreferrer") {{name}}
else
p.quiet {{_ 'vcs-none'}}
//- Everything else - editing labels, people, dates, uploading files to
the Admin Panel's default storage - is the card's own popup.
button.js-vcs-open-card(type="button" data-card-id="{{_id}}")
i.fa.fa-id-card-o
| &nbsp;{{_ 'vcs-open-card'}}
template(name="versionControlNewIssue")
form.vcs-new-issue.js-vcs-create
h2 {{_ 'vcs-new-issue'}}
input.js-vcs-new-title(type="text" required aria-label="{{_ 'title'}}" placeholder="{{_ 'title'}}" autofocus)
textarea.js-vcs-new-body(rows="10" aria-label="{{_ 'description'}}" placeholder="{{_ 'description'}}")
if boardLabels.length
fieldset.vcs-new-labels
legend {{_ 'labels'}}
each boardLabels
label.vcs-new-label
input.js-vcs-new-label(type="checkbox" value="{{_id}}")
span.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
if name
+inlineViewer
= name
else
| {{color}}
if error
p.warning {{error}}
.vcs-comment-actions
button.js-vcs-cancel(type="button") {{_ 'cancel'}}
button.primary(type="submit") {{_ 'vcs-submit-issue'}}
@@ -0,0 +1,470 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import CardComments from '/models/cardComments';
import { Utils } from '/client/lib/utils';
import { liveAttachments } from '/models/lib/attachmentSoftDelete';
import { IMPORT_SOURCES } from '/models/lib/importSources';
import { attachmentKind } from '/models/lib/attachmentKind';
import { openAttachmentSlideshow } from '/client/components/cards/attachments';
const vcs = require('/models/lib/versionControlView');
// Board View / Version Control (versionControlView.jade, server/versionControlView.js,
// models/lib/versionControlView.js). Everything shown is the board's own data,
// already on the client; the two writes that need the server (New issue,
// Close / Reopen) are methods, and a comment is an ordinary card comment.
const TAB_ICONS = { issues: 'fa-dot-circle-o', milestones: 'fa-map-signs', labels: 'fa-tag', releases: 'fa-tags', wiki: 'fa-book', files: 'fa-file-o', history: 'fa-history' };
const TAB_LABELS = { issues: 'vcs-issues', milestones: 'vcs-milestones', labels: 'labels', releases: 'vcs-releases', wiki: 'vcs-wiki', files: 'attachments', history: 'history' };
const SOURCES = ['github', 'gitlab', 'gitea', 'forgejo'];
// A click on a link inside a rendered title or label (inlineViewer), not on
// the title itself.
const inRenderedLink = event => {
const link = event.target && event.target.closest && event.target.closest('a[href]');
return !!(link && event.currentTarget.contains(link) && link !== event.currentTarget);
};
const SOURCE_NAMES = { github: 'GitHub', gitlab: 'GitLab', gitea: 'Gitea', forgejo: 'Forgejo', jira: 'Jira' };
// The view's state. One board view is drawn at a time, so it lives here and is
// reset whenever the view is created.
const tab = new ReactiveVar('issues');
const query = new ReactiveVar('is:open');
const selectedId = new ReactiveVar(null);
const creating = new ReactiveVar(false);
const createError = new ReactiveVar('');
const syncing = new ReactiveVar(null);
const wikiPageId = new ReactiveVar(null);
const wikiHistory = new ReactiveVar(false);
function userName(userId) {
const user = userId && ReactiveCache.getUser(userId);
return user ? user.username || (user.profile && user.profile.fullname) || '' : '';
}
function shortDate(date) {
return date ? new Date(date).toLocaleDateString() : '';
}
function humanSize(bytes) {
const n = Number(bytes) || 0;
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
// Every card of the board as an issue row (models/lib/versionControlView.js).
function boardIssues() {
const board = Utils.getCurrentBoard();
if (!board) return [];
const labels = board.labels || [];
const labelById = new Map(labels.map(l => [l._id, l]));
const milestones = vcs.boardMilestones(labels);
const milestoneById = new Map(milestones.map(m => [m._id, m]));
const listTitle = new Map((ReactiveCache.getLists({ boardId: board._id }) || []).map(l => [l._id, l.title]));
const commentCount = new Map();
CardComments.find({ boardId: board._id }, { fields: { cardId: 1 } }).forEach((c) => {
commentCount.set(c.cardId, (commentCount.get(c.cardId) || 0) + 1);
});
// Releases and wiki pages have lists of their own; they are not issues.
const notIssues = new Set((ReactiveCache.getLists({ boardId: board._id }) || [])
.filter(l => [vcs.RELEASES_LIST_TITLE, vcs.WIKI_LIST_TITLE].includes(l.title)).map(l => l._id));
const cards = (ReactiveCache.getCards({ boardId: board._id, type: { $nin: ['template-card', 'cardType-linkedBoard'] } }) || [])
.filter(card => !notIssues.has(card.listId));
return cards.map((card) => {
const labelIds = card.labelIds || [];
const plain = labelIds.filter(id => labelById.has(id) && !milestoneById.has(id));
const ms = labelIds.filter(id => milestoneById.has(id));
const assignees = [...new Set([...(card.assignees || []), ...(card.members || [])])];
return {
_id: card._id,
number: vcs.issueNumber(card),
title: card.title || '',
body: vcs.issueBody(card.description),
state: vcs.issueState(card, listTitle.get(card.listId)),
labelIds,
labels: plain.map(id => labelById.get(id)),
labelNames: labelIds.map(id => (labelById.get(id) || {}).name).filter(Boolean),
milestoneIds: ms,
milestones: ms.map(id => milestoneById.get(id)),
milestoneTitles: ms.map(id => milestoneById.get(id).title),
author: card.requestedBy || userName(card.userId),
authorId: card.userId,
assignees,
assigneeNames: assignees.map(userName).filter(Boolean),
comments: commentCount.get(card._id) || 0,
createdAt: card.createdAt,
updatedAt: card.modifiedAt || card.dateLastActivity,
synced: vcs.isSyncedIssue(card),
syncSource: SOURCE_NAMES[card.syncSourceType] || card.syncSourceType || '',
};
});
}
// filterIssues compares `assignees` by name, the rows carry ids for avatars.
const searchable = issue => ({ ...issue, assignees: issue.assigneeNames });
function issueRow(issue) {
return {
...issue,
closed: issue.state === 'closed',
numberText: issue.number ? `#${issue.number}` : '',
date: shortDate(issue.createdAt),
};
}
// `author:@me` is the current user, as on GitHub.
function effectiveQuery() {
const me = userName(Meteor.userId());
return query.get().replace(/(author|assignee):@me\b/gi, (m, key) => `${key}:${me || '@me'}`);
}
function openCardPopup(event, cardId) {
const boardId = Utils.getCurrentBoardId();
Meteor.subscribe('popupCardData', cardId, {
onReady() {
Session.set('popupCardId', cardId);
Session.set('popupCardBoardId', boardId);
if (!Popup.isOpen()) Popup.open('cardDetails')(event);
},
});
}
const SPECIAL_WIKI = ['_Sidebar', '_Footer'];
function specialList(title) {
const boardId = Utils.getCurrentBoardId();
return boardId ? ReactiveCache.getList({ boardId, title, archived: false }) : null;
}
function wikiCards() {
const list = specialList(vcs.WIKI_LIST_TITLE);
return list ? (ReactiveCache.getCards({ boardId: list.boardId, listId: list._id, archived: false }, { sort: { sort: 1 } }) || []) : [];
}
// The page shown: the chosen one, else Home, else the first.
function selectedWikiPage() {
const cards = wikiCards().filter(c => !SPECIAL_WIKI.includes(c.title));
return cards.find(c => c._id === wikiPageId.get()) || cards.find(c => /^home$/i.test(c.title)) || cards[0] || null;
}
const fileRow = file => ({ name: file.name, link: typeof file.link === 'function' ? file.link() : '' });
function filesOf(cardIds) {
if (!cardIds.length) return [];
return ReactiveCache.getAttachments(liveAttachments({ 'meta.cardId': { $in: cardIds } })) || [];
}
Template.versionControlView.onCreated(() => {
tab.set('issues');
wikiPageId.set(null);
wikiHistory.set(false);
query.set('is:open');
selectedId.set(null);
creating.set(false);
});
Template.versionControlView.helpers({
boardId() { return Utils.getCurrentBoardId(); },
boardTitle() {
const board = Utils.getCurrentBoard();
return board ? board.title : '';
},
isPublic() {
const board = Utils.getCurrentBoard();
return !!(board && board.permission === 'public');
},
canWrite() { return Utils.canModifyBoard(); },
importSources() { return IMPORT_SOURCES.filter(s => SOURCES.includes(s.key)); },
// The lists of this board that sync from a tracker (List menu / Sync):
// "Sync now" brings the remaining issues, and with "Comments, labels, state
// and files" their conversation too.
syncedLists() {
const boardId = Utils.getCurrentBoardId();
return (ReactiveCache.getLists({ boardId, archived: false }) || [])
.filter(l => l.syncSource && SOURCES.includes(l.syncSource.type))
.map(l => ({ _id: l._id, title: l.title, source: SOURCE_NAMES[l.syncSource.type],
project: l.syncSource.projectKey, error: l.syncSource.lastSyncError || '',
busy: syncing.get() === l._id }));
},
tabs() {
const board = Utils.getCurrentBoard();
const issues = boardIssues();
const counts = {
issues: issues.filter(i => i.state === 'open').length,
milestones: vcs.boardMilestones((board && board.labels) || []).length,
labels: vcs.boardPlainLabels((board && board.labels) || []).length,
files: filesOf(issues.map(i => i._id)).length,
};
return vcs.TABS.map(id => ({ id, icon: TAB_ICONS[id], labelKey: TAB_LABELS[id], count: counts[id], active: tab.get() === id }));
},
isTab(id) { return tab.get() === id; },
creating() { return creating.get(); },
query() { return query.get(); },
stateIs(state) { return vcs.parseQuery(query.get()).state === state; },
counts() { return vcs.stateCounts(boardIssues().map(searchable), effectiveQuery()); },
sorts() {
const current = vcs.parseQuery(query.get()).sort;
return vcs.SORTS.map(id => ({ id, labelKey: `vcs-sort-${id}`, active: id === current }));
},
issues() {
const rows = boardIssues();
const byId = new Map(rows.map(r => [r._id, r]));
return vcs.filterIssues(rows.map(searchable), effectiveQuery()).map(r => issueRow(byId.get(r._id)));
},
selectedIssue() {
const id = selectedId.get();
const issue = id && boardIssues().find(i => i._id === id);
return issue ? issueRow(issue) : null;
},
milestones() {
const board = Utils.getCurrentBoard();
return vcs.milestoneProgress(vcs.boardMilestones((board && board.labels) || []), boardIssues())
.map(m => ({ ...m, due: shortDate(m.dueAt) }));
},
labelRows() {
const board = Utils.getCurrentBoard();
return vcs.labelUsage((board && board.labels) || [], boardIssues());
},
// Releases: the cards of the Releases list, newest first.
releases() {
const list = specialList(vcs.RELEASES_LIST_TITLE);
if (!list) return [];
return (ReactiveCache.getCards({ boardId: list.boardId, listId: list._id, archived: false }) || [])
.map(card => ({ _id: card._id, title: card.title, body: vcs.issueBody(card.description), date: shortDate(card.receivedAt || card.createdAt),
at: new Date(card.receivedAt || card.createdAt || 0).getTime(), files: filesOf([card._id]).map(fileRow) }))
.sort((a, b) => b.at - a.at);
},
// Wiki: the pages are the cards of the Wiki list (models/lib/githubWiki.js).
wikiPages() {
const current = selectedWikiPage();
return wikiCards().filter(c => !SPECIAL_WIKI.includes(c.title)).map(c => ({ _id: c._id, title: c.title, active: current && current._id === c._id }));
},
wikiPage() {
const page = selectedWikiPage();
return page && { _id: page._id, title: page.title, text: page.description || '' };
},
wikiSidebar() { const c = wikiCards().find(x => x.title === '_Sidebar'); return c ? c.description || '' : ''; },
wikiFooter() { const c = wikiCards().find(x => x.title === '_Footer'); return c ? c.description || '' : ''; },
wikiHistory() { return wikiHistory.get(); },
files() {
const issues = new Map(boardIssues().map(i => [i._id, i]));
return filesOf([...issues.keys()]).map((file) => {
const issue = issues.get(file.meta && file.meta.cardId) || {};
const kind = attachmentKind(file);
return {
_id: file._id,
attachment: file,
isImage: !!kind.isImage,
// What the attachment viewer opens (cards/attachments.js attachmentCanBeOpened).
canPreview: !!(kind.isImage || kind.isPDF || kind.isText || kind.isJSON
|| kind.isOffice || kind.isZIP || kind.isVideo || kind.isAudio),
name: file.name,
link: typeof file.link === 'function' ? file.link() : '',
size: humanSize(file.size),
cardId: issue._id,
number: issue.number,
numberText: issue.number ? `#${issue.number}` : '',
issueTitle: issue.title,
date: shortDate(file.uploadedAt || (file.meta && file.meta.uploadedAt)),
};
});
},
});
function setQuery(tpl, value) {
query.set(value);
selectedId.set(null);
tab.set('issues');
}
// The popups of an opened card's attachment work on the attachment itself.
const fileOf = event => ReactiveCache.getAttachment(event.currentTarget.dataset.attachmentId);
const deleteFile = Popup.afterConfirm('attachmentDelete', function () {
// A soft delete, as the card's gallery does (History.md §12.1).
if (this._id) Meteor.call('attachments.softDelete', this._id);
Popup.back();
});
Template.versionControlView.events({
// Viewed in the card's viewer; next and previous walk this tab's files.
'click .js-vcs-file-view'(event, tpl) {
event.preventDefault();
const ids = tpl.findAll('.vcs-file[data-attachment-id]').map(row => row.dataset.attachmentId);
openAttachmentSlideshow(event.currentTarget.dataset.attachmentId, ids);
},
// Not bubbled to FlowRouter, which would empty the page (#101).
'click .vcs-file-actions .js-download'(event) { event.stopPropagation(); },
'click .js-vcs-file-rename'(event, tpl) {
const file = fileOf(event);
if (file) Popup.open('attachmentRename').call(file, event, tpl);
},
'click .js-vcs-file-delete'(event, tpl) {
const file = fileOf(event);
if (file) deleteFile.call(file, event, tpl);
},
'click .js-vcs-file-menu'(event, tpl) {
const file = fileOf(event);
if (file) Popup.open('attachmentActions').call(file, event, tpl);
},
'click .js-vcs-tab'(event) {
event.preventDefault();
tab.set(event.currentTarget.dataset.tab);
selectedId.set(null);
creating.set(false);
},
'keydown .js-vcs-search'(event, tpl) {
if (event.key !== 'Enter') return;
event.preventDefault();
setQuery(tpl, String(event.currentTarget.value || ''));
},
'click .js-vcs-state'(event, tpl) {
event.preventDefault();
const rest = query.get().replace(/(^|\s)(is|state):(open|closed)\b/gi, ' ').trim();
setQuery(tpl, `is:${event.currentTarget.dataset.state} ${rest}`.trim());
},
'change .js-vcs-sort'(event, tpl) {
setQuery(tpl, vcs.withQualifier(query.get(), 'sort', event.currentTarget.value));
},
// The clickable titles and labels are spans with role=link: Enter and
// Space click them, as on a link or a button.
'keydown [role="link"]'(event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.currentTarget.click();
},
'click .js-vcs-label'(event, tpl) {
// A link inside a rendered title opens itself.
if (inRenderedLink(event)) return;
event.preventDefault();
setQuery(tpl, vcs.withQualifier(tab.get() === 'issues' ? query.get() : 'is:open', 'label', event.currentTarget.dataset.name));
},
'click .js-vcs-milestone'(event, tpl) {
// A link inside a rendered title opens itself.
if (inRenderedLink(event)) return;
event.preventDefault();
setQuery(tpl, vcs.withQualifier(tab.get() === 'issues' ? query.get() : 'is:open', 'milestone', event.currentTarget.dataset.title));
},
'click .js-vcs-open-issue'(event) {
// A link inside a rendered title opens itself.
if (inRenderedLink(event)) return;
event.preventDefault();
tab.set('issues');
creating.set(false);
selectedId.set(event.currentTarget.dataset.cardId);
},
'click .js-vcs-new'() {
createError.set('');
creating.set(true);
},
'click .js-vcs-sync'(event) {
event.preventDefault();
const listId = event.currentTarget.dataset.listId;
if (!listId || syncing.get()) return;
syncing.set(listId);
Meteor.call('syncListNow', listId, () => syncing.set(null));
},
'click .js-vcs-wiki-page'(event) {
// A link inside a rendered title opens itself.
if (inRenderedLink(event)) return;
event.preventDefault();
wikiPageId.set(event.currentTarget.dataset.cardId);
wikiHistory.set(false);
},
// A link between pages points at the other page's card: stay in the wiki.
'click .vcs-wiki-body a'(event) {
const href = event.currentTarget.getAttribute('href') || '';
const target = wikiCards().find(c => href.endsWith(`/${c._id}`));
if (!target) return;
event.preventDefault();
wikiPageId.set(target._id);
wikiHistory.set(false);
},
'click .js-vcs-wiki-history'(event) {
event.preventDefault();
wikiHistory.set(!wikiHistory.get());
},
'click .js-vcs-wiki-edit'(event) {
event.preventDefault();
const page = selectedWikiPage();
if (page) openCardPopup(event, page._id);
},
'click .js-vcs-export'(event) {
event.preventDefault();
Popup.open('exportBoard')(event);
},
});
Template.versionControlIssue.helpers({
canWrite() { return Utils.canModifyBoard(); },
canComment() { return Utils.currentUserCan('comment'); },
conversation() {
return CardComments.find({ cardId: this._id }, { sort: { createdAt: 1 } }).fetch().map(c => ({
_id: c._id,
userId: c.userId,
author: userName(c.userId),
time: c.createdAt ? new Date(c.createdAt).toLocaleString() : '',
text: c.text,
}));
},
issueFiles() {
return filesOf([this._id]).map(file => ({
name: file.name,
link: typeof file.link === 'function' ? file.link() : '',
}));
},
});
Template.versionControlIssue.events({
'click .js-vcs-back'(event) {
event.preventDefault();
selectedId.set(null);
},
'submit .js-vcs-comment'(event, tpl) {
event.preventDefault();
const input = tpl.$('.js-vcs-comment-text');
const text = String(input.val() || '').trim();
const card = ReactiveCache.getCard(this._id);
if (!text || !card) return;
CardComments.insert({ text, boardId: card.boardId, cardId: card._id, parentId: '' });
input.val('');
},
'click .js-vcs-set-state'(event) {
event.preventDefault();
Meteor.call('vcsSetIssueState', this._id, event.currentTarget.dataset.state);
},
'click .js-vcs-open-card'(event) {
event.preventDefault();
openCardPopup(event, this._id);
},
});
Template.versionControlNewIssue.helpers({
error() { return createError.get(); },
boardLabels() {
const board = Utils.getCurrentBoard();
return ((board && board.labels) || []).filter(l => l && l._id);
},
});
Template.versionControlNewIssue.events({
'click .js-vcs-cancel'() { creating.set(false); },
'submit .js-vcs-create'(event, tpl) {
event.preventDefault();
const input = {
title: String(tpl.$('.js-vcs-new-title').val() || ''),
body: String(tpl.$('.js-vcs-new-body').val() || ''),
labelIds: tpl.$('.js-vcs-new-label:checked').toArray().map(el => el.value),
};
createError.set('');
Meteor.call('vcsCreateIssue', Utils.getCurrentBoardId(), input, (error, result) => {
if (error) {
createError.set(TAPi18n.__(String(error.error || error.reason || 'error')));
return;
}
creating.set(false);
if (result && result.cardId) selectedId.set(result.cardId);
});
},
});
+42 -7
View File
@@ -124,15 +124,44 @@ div.attachment-thumbnail-container .attachment-thumbnail-text {
.attachment-actions a:first-child {
margin-inline-start: 0;
}
.add-attachment {
display: flex;
/* #6796: the add button is a small "+" like every other section's
(.card-label.add-label), not a full-width dashed box. */
.attachment-gallery .add-attachment {
align-self: flex-start;
margin-top: 4px;
cursor: pointer;
}
/* A file with nothing to preview, or every file when Attachments: Preview is
off: a small button with the file icon and its extension, which still opens
the preview, instead of a 144x96 box (models/lib/attachmentTile.cjs). */
.attachment-item.attachment-item-compact {
margin-top: 8px;
gap: 8px;
}
button.attachment-thumbnail-container.attachment-thumbnail-compact,
button.attachment-thumbnail-container.attachment-thumbnail-compact:hover,
button.attachment-thumbnail-container.attachment-thumbnail-compact:focus,
button.attachment-thumbnail-container.attachment-thumbnail-compact:active {
flex: 0 0 auto;
width: auto;
min-width: 2.6em;
height: 2.6em;
padding: 0 0.4em;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px dashed #555;
border-radius: 5px;
padding: 10px;
cursor: pointer;
margin-top: 16px;
gap: 0.3em;
line-height: 1;
}
.attachment-thumbnail-compact .attachment-thumbnail-ext {
font-size: 0.75em;
text-transform: lowercase;
}
/* The name first and on a line of its own, so a long one is read, not hidden
behind the buttons; who, when and the buttons follow on the next line. */
.attachment-details > div:first-child {
flex: 1 1 100%;
min-width: 0;
}
.icon {
font-size: 1.5em;
@@ -512,3 +541,9 @@ div.attachment-thumbnail-container .attachment-thumbnail-text {
flex: 0 0 auto;
cursor: pointer;
}
/* #6796: the upload rules, in the upload popup rather than above the list. */
.attachment-upload-help {
margin: 8px 12px 4px;
font-size: 0.85em;
}
+32 -10
View File
@@ -23,6 +23,19 @@ template(name="cardAttachmentsPopup")
//- #3257: a card attached to this card, as Trello's card attachments.
li
a.js-attach-card {{_ 'attach-card'}}
//- #6802: a new drawing, the whole window, saved as an SVG attachment.
li
a.js-attach-drawing
i.fa.fa-pencil
| {{_ 'cardDrawingPopup-title'}}
//- #6796: the upload rules, where the upload is chosen - they used to be
body text above the opened card's attachment list.
.attachment-upload-help.quiet
if Meteor.settings.public.attachmentsUploadMaxSize
div {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
if Meteor.settings.public.attachmentsUploadMimeTypes
div {{_ 'allowed-upload-filetypes'}} {{Meteor.settings.public.attachmentsUploadMimeTypes}}
div {{_ 'invalid-file'}}
//- #3257: attach a card - a pasted card link (Copy card link gives one), or one
of this board's cards found by its title.
@@ -59,7 +72,7 @@ template(name="attachmentViewer")
#viewer-container
button.fa.fa-caret-left#prev-attachment(type="button" aria-label="{{_ 'previous'}}")
#viewer-content
img#image-viewer.hidden
img#image-viewer.hidden(alt="{{_ 'preview'}}")
video#video-viewer.hidden(controls="true")
audio#audio-viewer.hidden(controls="true")
object#pdf-viewer.hidden(type="application/pdf")
@@ -73,15 +86,22 @@ template(name="attachmentGallery")
.attachment-gallery
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
//- #6796: a small "+" like every other section's, not a full-width box.
if canAddAttachment
a.card-label.add-label.add-attachment.js-add-attachment(title="{{_ 'add-attachment'}}" aria-label="{{_ 'add-attachment'}}")
i.fa.fa-plus
each attachments
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
button.attachment-thumbnail-container.open-preview(type="button" aria-label="{{_ 'preview'}}: {{cleanFilename name}}" data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
if link
//- #6796: Attachments: Preview, Show on Card (Board Settings / Card).
Off, the tile is a small file button that still opens the preview;
a file with nothing to preview (a .txt) always gets the small one.
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}} {{#if attachmentTileCompact this}}attachment-item-compact{{/if}}")
button.attachment-thumbnail-container.open-preview(type="button" aria-label="{{_ 'preview'}}: {{cleanFilename name}}" data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}" class="{{#if attachmentTileCompact this}}attachment-thumbnail-compact{{/if}}")
if attachmentTileCompact this
i.fa.fa-file-o(aria-hidden="true")
span.attachment-thumbnail-ext= extension
else if link
if(isImage)
img.attachment-thumbnail(src="{{attachmentPreviewUrl this}}" title="{{cleanFilename name}}" alt="{{cleanFilename name}}" loading="lazy")
else if($eq extension 'svg')
@@ -106,10 +126,12 @@ template(name="attachmentGallery")
div
b {{ cleanFilename name }}
span.file-size ({{fileSize size}})
if uploaderName
div.attachment-uploader {{uploaderName}}
if uploadedAt
div.attachment-uploaded-at {{uploadedAt}}
//- #6796: Attachments: Details, Show on Card - who and when.
unless $eq currentBoard.allowsAttachmentDetailsOnCard false
if uploaderName
div.attachment-uploader {{uploaderName}}
if uploadedAt
div.attachment-uploaded-at {{uploadedAt}}
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{downloadFilename name}}", title="{{_ 'download'}}")
i.fa.fa-arrow-down
+41 -5
View File
@@ -16,9 +16,12 @@ import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManag
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import { buildAttachmentUploadConfig } from '/client/lib/attachmentUploadConfig';
import { attachmentUploadsBlocked } from '/client/lib/attachmentUploadPolicy';
const { cleanFileName } = require('/imports/lib/fileNameDisplay');
const { attachmentTileCompact } = require('/models/lib/attachmentTile.cjs');
import { formatDateTime } from '/imports/lib/dateUtils';
import { EscapeActions } from '/client/lib/escapeActions';
import { openNewDrawing } from '/client/lib/newDrawing';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { ReactiveVar } from 'meteor/reactive-var';
@@ -159,6 +162,8 @@ function openAttachmentViewer(attachmentId) {
switch(true){
case (kind.isImage):
$("#image-viewer").attr("src", getAttachmentUrl(attachment));
// #337: the picture is named by its file for a screen reader.
if (attachment.name) $("#image-viewer").attr("alt", attachment.name);
$("#image-viewer").removeClass("hidden");
break;
case (kind.isPDF):
@@ -334,8 +339,8 @@ function openPrevAttachment() {
function processTouch(){
xDist = touchEndCoords.x - touchStartCoords.x;
yDist = touchEndCoords.y - touchStartCoords.y;
const xDist = touchEndCoords.x - touchStartCoords.x;
const yDist = touchEndCoords.y - touchStartCoords.y;
// Left swipe
if (Math.abs(xDist) > Math.abs(yDist) && xDist < 0) {
@@ -463,6 +468,20 @@ Template.attachmentGallery.onCreated(function () {
});
Template.attachmentGallery.helpers({
// #6870: the "+" button - a card the user may edit, unless uploads are blocked site-wide
canAddAttachment() {
return Utils.canModifyCard() && !attachmentUploadsBlocked();
},
// #6796: the small file button instead of the preview tile
// (models/lib/attachmentTile.cjs).
attachmentTileCompact(file) {
if (!file) return true;
const board = ReactiveCache.getBoard(file.meta?.boardId) || Utils.getCurrentBoard();
const link = typeof file.link === 'function' ? file.link() : file.link;
return attachmentTileCompact({
isImage: file.isImage, extension: file.extension, hasLink: !!link,
}, board?.allowsAttachmentPreviewsOnCard);
},
attachedCards() {
return Template.instance().attachedCards.get();
},
@@ -586,6 +605,17 @@ Template.cardAttachmentsPopup.events({
},
'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
'click .js-attach-card': Popup.open('attachCard'),
// #6802: draw a new file - the whole window - saved as an SVG attachment.
'click .js-attach-drawing'(event) {
event.preventDefault();
// This popup goes, and the drawing opens once the click is over: the
// document's click-outside handler would otherwise see a click on a popup
// that no longer exists and close the new one at once.
event.stopPropagation();
const cardId = this && this._id;
Popup.close();
setTimeout(() => openNewDrawing(cardId), 0);
},
});
// #3257: attach a card by a pasted card link, or one of this board's cards
@@ -647,6 +677,9 @@ export async function handleFileUpload(card, files) {
return [];
}
// #6870: the administrator blocked uploads for the whole site; the server refuses them too
if (attachmentUploadsBlocked()) return [];
// Check if board allows attachments
const board = card.board();
if (!board || !board.allowsAttachments) {
@@ -849,8 +882,9 @@ Template.attachmentActionsPopup.events({
Template.attachmentRenamePopup.helpers({
getNameWithoutExtension() {
const ret = this.name.replace(new RegExp("\\." + this.extension + "$"), "");
return ret;
// A literal suffix, never a RegExp built from the file's extension.
const suffix = `.${this.extension}`;
return this.name.endsWith(suffix) ? this.name.slice(0, -suffix.length) : this.name;
},
});
@@ -864,9 +898,11 @@ Template.attachmentRenamePopup.events({
'click button.js-submit-edit-attachment-name'(event, tpl) {
// save button pressed
event.preventDefault();
// A migrated attachment can lack extensionWithDot; its extension is enough.
const suffix = this.extensionWithDot || (this.extension ? `.${this.extension}` : '');
const name = tpl.$('.js-edit-attachment-name')[0]
.value
.trim() + this.extensionWithDot;
.trim() + suffix;
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this._id, name);
}
+285
View File
@@ -0,0 +1,285 @@
/* Card Settings (Board Settings / Card, docs/Features/Cards/Card-Settings.md):
"Show" and its two choices, then the sections drawn like checklists. */
.card-settings-inline { padding: 4px 0 8px; }
/* Two panes, Minicard first (the start: left in a left-to-right page, right in
a right-to-left one) and Card after it (sidebar.jade boardCardSettingsPopup).
STACKED by default - Minicard above Card - and the container scrolls as one;
SIDE BY SIDE once the popup is wide enough for two panes of about 20em,
and then each pane's body scrolls on its own while the container does not.
A collapsed pane is its header only, and the other pane takes the width.
The popup's content wrapper does not scroll here (popup.css), so the
container is the one scroller in the stacked layout. */
.card-settings-panes-container {
container-type: inline-size;
container-name: card-settings-panes;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
.card-settings-panes {
display: flex;
flex-direction: column;
flex: 0 0 auto;
gap: 8px;
}
.card-settings-pane { min-width: 0; }
.card-settings-pane-head {
display: flex;
align-items: center;
gap: 6px;
padding-block: 4px 6px;
border-block-end: 1px solid #ddd;
margin-block-end: 8px;
}
.card-settings-pane-collapse {
padding: 2px 4px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
cursor: pointer;
}
.card-settings-pane-collapse:hover,
.card-settings-pane-collapse:focus-visible { color: #2573a7; }
.card-settings-pane-title {
margin: 0;
font-size: calc(17px * var(--wekan-ui-font-scale, 1));
font-weight: bold;
}
.card-settings-pane-body[hidden] { display: none !important; }
/* The splitter: only side by side with neither pane collapsed (the container
query below shows it). A thin line with a wider grab area, the Admin Panel
left menu's grip (settingBody.css .left-menu-resize-handle) in look. */
.card-settings-splitter {
display: none;
flex: 0 0 10px;
align-self: stretch;
cursor: col-resize;
touch-action: none;
position: relative;
border-radius: 3px;
transition: background-color 0.2s ease;
}
.card-settings-splitter::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: 4px;
width: 2px;
background: #ddd;
}
.card-settings-splitter:hover,
.card-settings-splitter:focus-visible {
background: rgba(0, 123, 255, 0.4);
outline: none;
}
.card-settings-splitter:active { background: rgba(0, 123, 255, 0.6); }
body.card-settings-splitting-active {
user-select: none;
cursor: col-resize;
}
.card-settings-pane.is-pane-collapsed .card-settings-pane-head { margin-block-end: 0; }
@container card-settings-panes (min-width: 40em) {
.card-settings-panes {
flex-direction: row;
align-items: stretch;
flex: 1 1 auto;
min-height: 0;
gap: 0;
}
.card-settings-pane {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
}
/* The Minicard pane takes its share of the width (the splitter between
them, models/lib/cardSettingsSplit.js); the Card pane takes the rest. */
.card-settings-panes:not(.has-collapsed-pane) > .card-settings-pane-minicard {
flex: 0 0 calc((100% - 10px) * var(--card-settings-split, 0.5));
}
.card-settings-pane.is-pane-collapsed { flex: 0 0 auto; }
.card-settings-panes:not(.has-collapsed-pane) > .card-settings-splitter { display: block; }
.card-settings-pane-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-inline-end: 4px;
}
}
/* A section: the checklist's grey block, its title line and its items. */
.card-settings-inline-section {
/* The checklist's grey block; a dark theme has its own (#6863: white text on
this light grey was 1.07:1 in cleandark). */
background: var(--card-settings-section-bg, #f7f7f7);
border-radius: 3px;
margin-block-end: 8px;
padding: 4px 8px 6px;
}
.card-settings-inline-section.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.5);
cursor: grabbing;
}
.card-settings-inline-section-head {
display: flex;
align-items: center;
gap: 6px;
padding-block: 4px;
}
.card-settings-inline-section-title {
flex: 1 1 auto;
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: calc(16px * var(--wekan-ui-font-scale, 1));
min-width: 0;
overflow-wrap: anywhere;
}
.card-settings-inline-section-title.js-card-settings-section-handle { cursor: grab; }
.card-settings-inline-section-count { color: var(--wekan-quiet-text, #595959); white-space: nowrap; }
.card-settings-inline-collapse,
.card-settings-inline-section-move,
.card-settings-inline-menu {
padding: 2px 4px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
cursor: pointer;
}
.card-settings-inline-collapse:hover,
.card-settings-inline-collapse:focus-visible,
.card-settings-inline-menu:hover,
.card-settings-inline-menu:focus-visible { color: #2573a7; }
/* The section's own checkbox, left of its title: the rows' look. A section
hidden whole on this side keeps its rows' ticks, drawn faded. */
.card-settings-section-side { flex: 0 0 auto; }
.card-settings-section-side > a.flex { margin: 0; }
.card-settings-inline-section.is-section-hidden .card-settings-inline-items { opacity: 0.55; }
/* One row on this side: one line, no caret - its checkbox where the other
sections' checkboxes are. */
.card-settings-inline-section.is-single .card-settings-inline-section-head {
padding-inline-start: calc(0.5em + 8px);
}
/* "+" under Custom Fields' items: a new custom field. */
.card-settings-add-custom-field { padding: 2px 0 0 22px; }
.card-settings-add-custom-field-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
min-height: 28px;
border-radius: 3px;
color: var(--wekan-quiet-text, #595959);
text-decoration: none;
}
.card-settings-add-custom-field-button:hover,
.card-settings-add-custom-field-button:focus-visible { background: rgba(0, 0, 0, 0.06); color: #2573a7; }
/* Collapsed: the items hidden, not removed, so their drag stays bound. */
.card-settings-inline-section.is-collapsed .card-settings-inline-items,
.card-settings-inline-section.is-collapsed .card-settings-add-custom-field { display: none; }
/* An item: the checkbox, the name, and its hamburger at the end
(sidebar.css .card-settings-line is the line itself). */
.card-settings-inline .card-settings-line {
grid-template-columns: auto minmax(0, 1fr) auto;
/* A space's width between the checkbox and the name, as in text. */
column-gap: 0.3em;
border-bottom: 0;
padding-inline-start: 22px;
}
.card-settings-inline .card-settings-line.ui-sortable-helper { background: var(--card-settings-helper-bg, #fff); }
.card-settings-inline .materialCheckBox { transition: none; }
.card-settings-inline .card-field-order-toggle.is-animating .materialCheckBox { transition: 0.2s; }
/* The hamburgers' checkboxes, likewise: only the one just clicked animates. */
.card-settings-section-menu .materialCheckBox,
.card-settings-item-menu .materialCheckBox { transition: none; }
.card-settings-section-menu .is-animating .materialCheckBox,
.card-settings-item-menu .is-animating .materialCheckBox { transition: 0.2s; }
/* The hamburgers' popups. */
.card-settings-item-menu,
.card-settings-section-menu { min-width: 16em; }
/* The hamburgers' checkboxes: a space's width before their label, as on
the rows. */
.card-settings-item-menu .card-settings-board-option > a.flex,
.card-settings-section-menu .card-settings-board-option > a.flex {
align-items: center;
column-gap: 0.3em;
}
/* A menu's options start at its edge, like its table: not .check-div's
indent for a form's checkboxes. */
.card-settings-item-menu .card-settings-board-option,
.card-settings-section-menu .card-settings-board-option {
margin-inline-start: 0;
}
/* A tick is drawn rotated (forms.css #6786) and reaches about 3px past its
box toward the label, which ate the space: drawn 3px further left, it keeps
it - the box, and so the label, do not move when it is ticked. Right to
left the label is on the other side of the box and the tick leans away
from it, so it stays as it is. */
:root:not([dir="rtl"]) .card-settings-inline .materialCheckBox.is-checked,
:root:not([dir="rtl"]) .card-settings-item-menu .materialCheckBox.is-checked,
:root:not([dir="rtl"]) .card-settings-section-menu .materialCheckBox.is-checked {
left: -6px;
}
/* A hamburger's panel (sidebar.jade cardSettingsInlinePanel): what the
hamburger holds, opened UNDER its line inside the pane, pushing the rest
down - not a popup. The open hamburger is tinted, and the panel hangs from
it in the same tint, with a notch pointing up at the line it belongs to. */
.card-settings-inline-menu.is-open,
.card-settings-add-custom-field-button.is-open {
background: rgba(41, 128, 185, 0.15);
color: #2573a7;
border-radius: 3px;
}
.card-settings-inline-panel {
position: relative;
margin-block: 6px 8px;
padding: 10px 12px 8px;
border: 1px solid rgba(41, 128, 185, 0.45);
border-radius: 4px;
background: rgba(41, 128, 185, 0.06);
/* Its own block, the width of what it hangs from. */
flex: 0 0 100%;
box-sizing: border-box;
}
.card-settings-inline-panel::before {
content: '';
position: absolute;
inset-block-start: -7px;
inset-inline-end: 10px;
width: 12px;
height: 12px;
background: inherit;
border-inline-start: 1px solid rgba(41, 128, 185, 0.45);
border-block-start: 1px solid rgba(41, 128, 185, 0.45);
transform: rotate(45deg);
background-color: var(--card-settings-notch-bg, #eef4f9);
}
/* The + is at the start of its line: the notch is under it there. */
.card-settings-inline-panel-create::before {
inset-inline-end: auto;
inset-inline-start: 6px;
}
.card-settings-inline-panel-close {
/* Above the notch, which is drawn at the same end of the panel's top. */
z-index: 1;
position: absolute;
inset-block-start: 4px;
inset-inline-end: 6px;
color: var(--wekan-quiet-text, #595959);
padding: 2px 4px;
}
.card-settings-inline-panel-close:hover,
.card-settings-inline-panel-close:focus-visible { color: #2573a7; }
.card-settings-section-action-title {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 6px;
}
+35
View File
@@ -0,0 +1,35 @@
import './cardDetails.jade';
import { Utils } from '/client/lib/utils';
// docs/Features/Cards/Card-Fields.md: an opened card shows every field its
// board shows on cards, empty or not, less the Card Settings sections the
// board hides on Show on Card. The rules are models/lib/cardShownFields.js.
const f = require('/models/lib/cardShownFields');
// The board as its opened cards see it (cardShownFields.js boardForCard): the
// same for every card of the board, empty or not, so `data` (the card) does
// not change it - it is kept so the callers read as before.
export function cardBoardView(data) { // eslint-disable-line no-unused-vars
return f.boardForCard(Utils.getCurrentBoard());
}
// The opened card and each of its section templates read `currentBoard`; this
// template-level helper answers before the global one, so every
// `currentBoard.allowsX` in them asks the card's view - and a template not
// listed here (the board, Card Settings itself) is unchanged. The minicard has
// its own (client/components/cards/minicard.js MINICARD_FIELD_TEMPLATES).
export const CARD_FIELD_TEMPLATES = Object.freeze([
// One per Card Settings section the opened card draws (cardDetails.jade).
'cardDetails', 'cardFieldSectionLabels', 'cardFieldSectionDate', 'cardFieldSectionMembers',
'cardFieldSectionCard', 'cardFieldSectionTime', 'cardFieldSectionCustomFields',
'cardFieldSectionVote', 'cardFieldSectionPoker', 'cardFieldSectionDescription', 'cardFieldSectionScrum',
'cardFieldSectionChecklists', 'cardFieldSectionSubtasks', 'cardFieldSectionAttachments', 'cardCommentsSection',
]);
CARD_FIELD_TEMPLATES.forEach(name => {
Template[name].helpers({
currentBoard() {
const tpl = Template.instance();
return cardBoardView(tpl && tpl.data);
},
});
});
+14 -11
View File
@@ -19,17 +19,20 @@ template(name="cardCustomFieldsPopup")
= name
a.js-edit-custom-field(title="{{_ 'edit'}}" aria-label="{{_ 'edit'}}")
i.fa.fa-pencil-square-o(aria-hidden="true")
//- The rule separates the list above from "Add custom field" below - with
no custom field yet, there is nothing above it to separate from.
if customFields.length
hr
a.quiet-button.full.js-open-create-custom-field
i.fa.fa-plus
span {{_ 'createCustomField'}}
//- #5681: this card's fields linked, by name, to another card's.
a.quiet-button.full.js-open-custom-field-links
i.fa.fa-link
span {{_ 'custom-field-links'}}
//- In Card Settings' Custom Fields pane these two are forms of their own
under the list, so they are not repeated here (sidebar.css).
.card-custom-fields-more
//- The rule separates the list above from "Add custom field" below -
with no custom field yet, there is nothing above it to separate from.
if customFields.length
hr
a.quiet-button.full.js-open-create-custom-field
i.fa.fa-plus
span {{_ 'createCustomField'}}
//- #5681: this card's fields linked, by name, to another card's.
a.quiet-button.full.js-open-custom-field-links
i.fa.fa-link
span {{_ 'custom-field-links'}}
//- #5681: the cards this card's custom fields are linked to, and the form that
links one more - a pasted card link (Copy card link gives one) or one of this
-9
View File
@@ -33,9 +33,6 @@ template(name="dateBadgeBody")
b
| {{showWeek}}
template(name="dateBadge")
+dateBadgeBody(canModifyCard=canModifyCard baseClass="card-date" showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
template(name="cardReceivedDate")
+dateBadgeBody(canModifyCard=canModifyCard baseClass="card-date" showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
@@ -48,12 +45,6 @@ template(name="cardDueDate")
template(name="cardEndDate")
+dateBadgeBody(canModifyCard=canModifyCard baseClass="card-date" showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
template(name="dateCustomField")
+dateBadgeBody(showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
template(name="cardCustomFieldDate")
+dateBadgeBody(showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
template(name="voteEndDate")
+dateBadgeBody(canModifyCard=canModifyCard baseClass="card-date" showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
+137 -347
View File
@@ -187,360 +187,150 @@ function cardDateHelpers(extraHelpers) {
return Object.assign(base, extraHelpers);
}
// cardReceivedDate
Template.cardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getReceived()));
});
});
// The four card dates - Received, Start, Due, End - each drawn twice: in the
// opened card (card*Date) and on the minicard (minicard*Date). Both badges of a
// date read the same field, colour themselves the same way and open the same
// editor, so each date is described once here and both templates are built
// from it.
const CARD_DATE_KINDS = {
Received: {
read: card => card.getReceived(),
editor: 'editCardReceivedDate',
helpers: {
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
Template.cardReceivedDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
return classes;
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
},
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
Start: {
read: card => card.getStart(),
editor: 'editCardStartDate',
helpers: {
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
classes += 'current';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
},
},
}));
// Only the due date counts down (dueCountdownText), and only until the card
// has an end date.
Due: {
read: card => card.getDue(),
editor: 'editCardDueDate',
helpers: {
classes() {
const tpl = Template.instance();
const data = cardFromDateContext();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
Template.cardReceivedDate.events({
'click .js-edit-date': openDateEditor('editCardReceivedDate'),
});
// cardStartDate
Template.cardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getStart()));
});
});
Template.cardStartDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
classes += 'current';
}
return classes;
return `due-date ${dueDateClass(theDate, nowVal, endAt)}`;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
const title = `${TAPi18n.__('card-due-on')} ${formattedDate}`;
if (endAt) return title;
return `${title} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
showDate() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
if (endAt) return formattedDate;
return `${formattedDate} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
},
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
End: {
read: card => card.getEnd(),
editor: 'editCardEndDate',
helpers: {
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
classes += 'completed-late';
} else {
classes += 'completed-on-time';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${formatCardDateForDisplay(tpl.date.get(), true, date => format(date, 'LLLL'))}`;
},
},
},
}));
};
Template.cardStartDate.events({
'click .js-edit-date': openDateEditor('editCardStartDate'),
});
// cardDueDate
Template.cardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getDue()));
});
});
Template.cardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
const data = cardFromDateContext();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
return `due-date ${dueDateClass(theDate, nowVal, endAt)}`;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
const title = `${TAPi18n.__('card-due-on')} ${formattedDate}`;
if (endAt) return title;
return `${title} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
showDate() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
if (endAt) return formattedDate;
return `${formattedDate} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
}));
Template.cardDueDate.events({
'click .js-edit-date': openDateEditor('editCardDueDate'),
});
// cardEndDate
Template.cardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getEnd()));
});
});
Template.cardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
classes += 'completed-late';
} else {
classes += 'completed-on-time';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${formatCardDateForDisplay(tpl.date.get(), true, date => format(date, 'LLLL'))}`;
},
}));
Template.cardEndDate.events({
'click .js-edit-date': openDateEditor('editCardEndDate'),
});
// cardCustomFieldDate
Template.cardCustomFieldDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
Template.cardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const tpl = Template.instance();
return formatCardDateForDisplay(tpl.date.get(), true);
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${formattedDate}`;
},
classes() {
return 'customfield-date';
},
}));
// --- Minicard date templates ---
// minicardReceivedDate
Template.minicardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getReceived()));
});
});
Template.minicardReceivedDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
showDate() {
return formatCardDateForDisplay(Template.instance().date.get(), true);
},
}));
Template.minicardReceivedDate.events({
'click .js-edit-date': openDateEditor('editCardReceivedDate'),
});
// minicardStartDate
Template.minicardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getStart()));
});
});
Template.minicardStartDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
classes += 'current';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
showDate() {
return formatCardDateForDisplay(Template.instance().date.get(), true);
},
}));
Template.minicardStartDate.events({
'click .js-edit-date': openDateEditor('editCardStartDate'),
});
// minicardDueDate
Template.minicardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getDue()));
});
});
Template.minicardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
const data = cardFromDateContext();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
return `due-date ${dueDateClass(theDate, nowVal, endAt)}`;
},
showTitle() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
const title = `${TAPi18n.__('card-due-on')} ${formattedDate}`;
if (endAt) return title;
return `${title} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
showDate() {
const tpl = Template.instance();
const formattedDate = formatCardDateForDisplay(tpl.date.get(), true);
const data = cardFromDateContext();
const endAt = data.getEnd();
if (endAt) return formattedDate;
return `${formattedDate} (${dueCountdownText(tpl.date.get(), tpl.now.get())})`;
},
}));
Template.minicardDueDate.events({
'click .js-edit-date': openDateEditor('editCardDueDate'),
});
// minicardEndDate
Template.minicardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(cardFromDateContext().getEnd()));
});
});
Template.minicardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = cardFromDateContext();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
classes += 'completed-late';
} else {
classes += 'completed-on-time';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${formatCardDateForDisplay(tpl.date.get(), true, date => format(date, 'LLLL'))}`;
},
showDate() {
return formatCardDateForDisplay(Template.instance().date.get(), true);
},
}));
Template.minicardEndDate.events({
'click .js-edit-date': openDateEditor('editCardEndDate'),
});
for (const [kind, spec] of Object.entries(CARD_DATE_KINDS)) {
for (const name of [`card${kind}Date`, `minicard${kind}Date`]) {
Template[name].onCreated(function () {
cardDateOnCreated(this);
this.autorun(() => {
this.date.set(new Date(spec.read(cardFromDateContext())));
});
});
Template[name].helpers(cardDateHelpers(spec.helpers));
Template[name].events({
'click .js-edit-date': openDateEditor(spec.editor),
});
}
}
// minicardCustomFieldDate
Template.minicardCustomFieldDate.onCreated(function () {
+1 -1
View File
@@ -38,7 +38,7 @@
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
color: var(--wekan-quiet-text, #595959);
height: 36px;
margin: 4px 4px 6px 0;
width: 92%;
+1 -1
View File
@@ -3,5 +3,5 @@ template(name="descriptionForm")
.new-description.js-new-description(
class="{{#if descriptionFormIsOpen}}is-open{{/if}}")
form.js-new-description-form
+editor(class="js-new-description-input" autofocus="autofocus")
+editor(class="js-new-description-input" autofocus="autofocus" labelKey="description")
| {{getUnsavedValue 'cardDescription' _id getDescription}}
Loaded 100 of 3481 files, more files were not shown because too many files have changed in this diff. Show more