Compare commits

...
140 Commits
Author SHA1 Message Date
github-actions[bot] a02959dde6 Bump versions for v10.87 2026-08-12 17:11:28 +00:00
Lauri Ojansivu 024ae3c1cd Prepare v10.87 release 2026-08-12 20:10:36 +03:00
Lauri Ojansivu e8843f51a1 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-08-12 20:08:32 +03:00
Lauri Ojansivu c1bed83464 CHANGELOG: point the npm-retry entry at its commit.
Thanks to xet7 !
2026-08-12 20:08:31 +03:00
Lauri Ojansivu 644e61f6a3 Retry the release's npm installs when the far end 5xxes.
The v10.86 run failed in build-amd64 with

  npm error code E503
  npm error 503 Service Unavailable - GET
  https://github.com/meteor/node-source-map-support/tarball/81bce1f9...

github.com was 503ing for a few minutes. npm's own fetch-retries is two quick
attempts and does not cover a git tarball fetch at all, so the install gave up;
every other Linux bundle is repacked from the amd64 one, so the whole release -
eleven architectures, the Docker images, the snap - was skipped and had to be
started again by hand. Every curl in that workflow has carried --retry 5 for
years; the npm installs carried nothing.

releases/npm-retry.sh runs an npm command with backoff (15s, 30s, 60s, 120s,
five attempts) and every npm install, npm pack and meteor npm install in
release-all.yml now goes through it, including the two inside docker run, which
get releases/ mounted the way the arch builds already do. Release All Missing
repacks through the same container script and inherits it.

A real npm error is NOT retried: ERESOLVE, a 404 for a package that does not
exist, a gyp compile error fail on the first attempt saying they are not a
network reason, because five attempts at an emulated arm64 install that was
never going to work is half an hour spent to print the same message. Only 5xx,
429 and the socket errors count as transient.

The Meteor installer is downloaded and then run instead of curl | sh, which
cannot be retried once sh is half way through the script.

tests/npmRetry.test.cjs pins both halves against a fake npm and reads the
workflow for a bare npm install.

Thanks to xet7 !
2026-08-12 20:08:30 +03:00
github-actions[bot] f1a569dae1 Bump versions for v10.86 2026-08-12 16:40:39 +00:00
Lauri Ojansivu 5ca9f20e03 Prepare v10.86 release 2026-08-12 19:39:48 +03:00
Lauri Ojansivu 52fe1822bf CHANGELOG: point the variant-images entry at the commit that survived the amend.
Thanks to xet7 !
2026-08-12 18:58:44 +03:00
Lauri Ojansivu 1d76e0d434 Publish the variant images on Docker Hub and Quay too, in the release build.
wekan-ondra and wekan-gantt-gpl are byte-identical to wekan/wekan apart from
the snap name:, so their images were retags of the released manifest, pushed
whenever somebody remembered to start docker-variant.yml. GHCR already got
them from the release build; Docker Hub and Quay now do as well - eighteen
tags out of the one docker buildx build --push, so a variant tag carries the
release's own digests for every architecture and cannot name a version it does
not contain.

docker-variant.yml stays for republishing one out of band, and is still
workflow_dispatch only.

A pushed tag is not a pullable tag: after the credentialed verification of all
nine images, a new step asks each one anonymously, the way a user does, and a
401/404 fails the job with the fix for that registry. Quay creates a new
repository private, so quay.io/wekan/wekan-ondra will pull for the pusher and
for nobody else until its visibility is changed - the same trap
ghcr.io/wekan/ferretdb fell into. An unreachable registry is a warning, not a
failure.

Thanks to xet7 !
2026-08-12 18:58:28 +03:00
Lauri Ojansivu b091a8ec57 CHANGELOG: the variant images on GHCR, and the chart's image going back there.
Two corrections and an addition to the Helm chart subsection: the default image
is ghcr.io/wekan/ferretdb again now that the package is public, the paragraph
about it tells the whole story (private package, org policy, what Artifact Hub
said and what a cluster would have said), and the release publishing
wekan-ondra and wekan-gantt-gpl on GHCR from its own build gets an entry of its
own - including that Release All Missing has no Docker part to change.
2026-08-12 18:45:13 +03:00
Lauri Ojansivu 4a519b6562 Release: publish the two variant images on GHCR from the release build itself.
wekan-ondra and wekan-gantt-gpl are the same WeKan as wekan - those repositories
are byte-identical to wekan/wekan apart from the snap `name:` in snapcraft.yaml,
which is why their Docker images have never been rebuilt but retagged from the
released manifest (docker-variant.yml, run by hand).

On GHCR they are now tagged in the release's own `docker buildx build --push`,
beside ghcr.io/wekan/wekan. Being in the SAME build is what makes it safe: the
variant tags carry the release's own digests for every architecture, there is no
second emulated build to go wrong, and there is no window in which a variant
image can differ from the release it names. The push verification loop asks the
registry about them like any other tag, so a variant tag that did not arrive
fails the job rather than being found months later by somebody on that CPU.

Docker Hub and Quay variants stay manual: docker-variant.yml exists so the
maintainer publishes those deliberately, and this does not change that.

Release All Missing has no Docker part to change - its jobs are plan,
extra-arches, appimage, flatpak, charts and done. It fills in artifacts for
versions whose image already exists; the charts job even refuses to publish a
chart for a version that has none.

The condition every bit of this rests on is written where the tags are, and in
docs/Design/Autoupdate/Snap-Ondra-Gantt.md: if a variant repository ever stops
being identical to wekan/wekan, these tags become a lie and that variant needs
its own build from its own Dockerfile.

The chart's default image also goes back to ghcr.io/wekan/ferretdb in the same
round, now that the package is public - the guard that briefly required it NOT to
be the GHCR one is replaced by the rule that outlives either state: the default
must be one of the three registries wekan/FerretDB pushes to, and the chart must
say which of them can be pulled.

Verified: the workflow parses, tests/chartsFerretdb.test.cjs passes (18 tests),
whole node suite 385 suites, 0 failed.

Thanks to xet7 !
2026-08-12 18:43:36 +03:00
Lauri Ojansivu d2a220a785 Pin @meteorjs/rspack to the version the Meteor rspack plugin asks for.
The build stopped before any test ran:

  => 🔧 Installing 1 dev dependency...
  => ❌ Failed to install Rspack
  For dev dependencies, run: meteor npm install -D @meteorjs/rspack@2.1.0

Nothing was missing. node_modules already had @meteorjs/rspack 2.1.0 and the
lockfile already resolved 2.1.0. What did not match was the DECLARATION:
package.json said `^2.1.0-beta.0`, and the plugin compares against the declared
string rather than the installed version, so it decided the dependency was not
there and tried to install it.

The trigger was mine: the dependency batch moved the Meteor package rspack from
1.2.0 to 1.2.1, and 1.2.1 asks for `@meteorjs/rspack@2.1.0` exactly. The
`^2.1.0-beta.0` range is older than that - it came in with "Updated dependencies."
on 2026-07-31 - and it was never really a prerelease in practice: npm resolves
that range up to the stable 2.1.0, which is what was installed all along. Only
the declaration still named a beta.

So it declares 2.1.0, which is both what the plugin wants and what is already on
disk. Nothing is downloaded and the resolved version does not change.

The package-lock.json diff also carries an unrelated npm reshuffle from the same
install - gcp-metadata re-nested under google-auth-library - which is left as npm
wrote it.

Verified by xet7: building and running WeKan works again.

Thanks to xet7 !
2026-08-12 18:40:33 +03:00
Lauri Ojansivu 67a3705cf5 Charts release: declare the images and the changes, and default to one that pulls.
From an Artifact Hub scan of the published chart:

  error scanning image ghcr.io/wekan/ferretdb:latest: image not found
  (package wekan:10.86.0)

Two separate things behind one line, and the second is the serious one.

The package 10.86.0 was a chart published out of band and already taken back off
gh-pages; the index there is clean again (230 entries, checked against the
registry and against the archives: every digest, inner chart version and
appVersion matches its entry, which is what Artifact Hub reads).

The image is the real fault, and it would have shipped in the next release.
ghcr.io/wekan/ferretdb cannot be pulled by anyone who is not logged in: a GHCR
package is private until it is made public, and that one has not been - an
anonymous manifest request answers 403 DENIED, while the same request for
ghcr.io/wekan/wekan answers 200. In a cluster that is ImagePullBackOff. The chart
now defaults to quay.io/wekan/ferretdb, which was verified to serve the full
multi-arch set (amd64, arm64, arm v5/v6/v7, 386, ppc64le, riscv64, s390x,
loong64), as does wekanteam/ferretdb on Docker Hub.

AND THE RELEASE NOW WRITES VERSION INFO INTO THE CHART, which is what a release
knows and a chart does not. releases/release-charts.sh adds two Artifact Hub
annotations to Chart.yaml alongside the version bump:

  artifacthub.io/changes   "WeKan updated to v<version>" with links to that
                           release's notes and the CHANGELOG. A chart version
                           with no changelog is one nobody can tell apart from
                           the version before it.
  artifacthub.io/images    the images this chart pulls, with exact tags, so the
                           scanner reads a list instead of inferring one - an
                           inferred list is what named an image the chart does
                           not use. The database image is READ out of values.yaml
                           as it is written, so the declaration cannot drift from
                           what is pulled.

Both are rewritten every release and were checked against the registries for the
current pair: ghcr.io/wekan/wekan:v10.85 and quay.io/wekan/ferretdb:latest exist.

The whole charts job was then run end to end against a throwaway clone of the
repository, and it does what the git history of both branches shows: main gets a
"<version>.0" commit with the bumped Chart.yaml, the archive is packaged and
carried to gh-pages, index.yaml is rebuilt from the packages, and gh-pages gets
its own "<version>.0" commit before returning to main. The archive has the same
structure and the same naming as every previous release - wekan/ with Chart.yaml,
values.yaml and templates/ - minus the Chart.lock and vendored MongoDB subchart,
which no longer exist, and plus the two FerretDB templates.

tests/chartsFerretdb.test.cjs gains three checks: the default image must be one
of the two registries that serve it publicly (never the GHCR one) with the reason
written beside it, the chart's declared images and changelog link must match its
own appVersion and its values.yaml, and the release script must build both from
the version being released rather than repeating them.

Verified by running that suite (18 tests) and the whole node suite (385 suites,
0 failed).

Thanks to xet7 !
2026-08-12 18:14:24 +03:00
Lauri Ojansivu ee7baf0b23 CHANGELOG: the Helm chart gets its own subsection, with the release path and the index.
The chart switch was sitting under "test coverage work", which is not what it is.
It now has a subsection of its own with the three entries that belong together:
the switch to FerretDB (published by this release's charts job, not before it),
the two release-path guards - a version bump cannot rewrite the database image,
and old releases are not backfilled onto a database they never ran - and the
Artifact Hub report about packages whose container image was deleted, which the
index already excluded and the backfill would have put back.

The first entry also carries what came after it: every FerretDB setting saying
what it would be on MongoDB, both compose files linked, the production notes
pointed at from three places, and the Docker Hub mongo:7 the FerretDB image
replaced named beside the three registries that carry it.
2026-08-12 18:02:17 +03:00
Lauri Ojansivu de8a293850 Backfill: rebuild the chart index with the tool that checks images.
From an Artifact Hub scan report of wekan/charts, 2026-08-11:

  error scanning image docker.io/bitnami/mongodb:7.0.14-debian-12-r3 (package wekan:7.94.0)
  error scanning image ghcr.io/wekan/wekan:v8.30: image not found (package wekan:8.30.0)
  ... v9.12, v9.14, v9.38, v9.39, v9.62

Checked against the live registry: every one of the index's 230 entries points at
an image that exists, so the index itself is already clean - releases/reindex-
charts.py asks the registry about every image a package pins and leaves out the
packages whose image is gone, which is how those seven left the index.

The hole was here. backfill-charts.sh rebuilt the index with `helm repo index .
--merge`, and helm indexes what it FINDS: 135 packages on that branch have an
image that no longer exists - six WeKan images that were never published, and 129
charts vendoring a Bitnami MongoDB image Bitnami has deleted - so one --apply run
would have put all 135 back and mailed the owner again. It now calls
reindex-charts.py, the same tool release-charts.sh uses, so "a package whose image
is gone stays out of the index" is one rule in one place rather than two tools
that disagree.

The duplicate-entry repair that followed the merge is gone with it: it existed
because `--merge` preserved the duplicates an older release-charts.sh prepended,
and reindex-charts.py writes ONE entry per package keyed on the version inside
it, so a duplicate cannot be written. The rule it implemented - keep the entry
whose digest matches the package on disk - is exercised where it now lives,
tests/reindexCharts.test.cjs, which already covers it.

tests/backfillCharts.test.cjs loses the extract-and-run of that block and gains
the two checks that matter now: no `helm repo index` in the script, and helm
still required for the one thing only helm does (packaging).
tests/chartsFerretdb.test.cjs pins the same rule from the chart's side, including
that a registry which cannot be reached is never read as "image gone" - that
would drop good entries on a network hiccup.

Six packages whose WeKan image no longer exists were deleted from the charts
gh-pages branch in the same round; the 129 Bitnami ones are left, since their
WeKan image is fine and only the old subchart's database image is missing.

Verified by running both suites and the whole node suite (385 suites, 0 failed).

Thanks to xet7 !
2026-08-12 17:44:32 +03:00
Lauri Ojansivu 2ad02cccc5 Release path: the next release publishes the FerretDB chart, and only for releases that had FerretDB.
Checking the Release All / Release All Missing charts jobs against the chart
change turned up three things worth fixing and one worth stating.

WHAT ALREADY WORKED, verified rather than assumed: a release changes exactly
three things in the chart - appVersion, the chart version, and the WeKan image
tag - and packages whatever else is on the charts repo's main branch. That branch
now holds the FerretDB chart, so the next release publishes it. Simulated with
the real substitutions for a hypothetical 10.86: appVersion 10.86, chart 10.86.0,
image ghcr.io/wekan/wekan:v10.86, no dependencies, ferretdb enabled with
ghcr.io/wekan/ferretdb:latest, both ferretdb templates in the package, and the
FerretDB MONGO_URL helper.

The tag substitution is `tag: v<digits>` and every other image in the chart is
`tag: latest` - FerretDB and the two busybox images - so a WeKan version bump
cannot rewrite the database image. The test asserts that against the pattern
itself rather than against a copy of it.

BACKFILL NEEDED A FLOOR. releases/backfill-charts.sh fills holes in the published
index by packaging TODAY'S chart with an old release's numbers on it. Today's
chart installs FerretDB, and WeKan did not default to FerretDB until v10.00
(commit 4dbf99010, released 2026-07-18) - so a backfilled chart for v6.09 would
pair that image with a database nobody ever ran it against, published under a
version number that says it is that release's chart. Versions below the floor are
now reported as "older" and left without a chart; CHART_FERRETDB_FLOOR overrides
it. Old ENTRIES are untouched either way: a version that already has a package is
"keep", and keep is never rebuilt.

The comments in release-charts.sh no longer describe a MongoDB dependency that is
gone; they say what the anchored substitutions protect now.

tests/chartsFerretdb.test.cjs gains the release-path half, which runs whether or
not the charts clone is present: three substitutions and no more, `tag: latest`
unmatched by the tag pattern, the image-existence check before publishing, and
the backfill floor.

Verified by running the suite (12 tests) and the whole node suite (385 suites,
0 failed).

Thanks to salcinad, ouvry-ems and xet7 !
2026-08-12 17:29:25 +03:00
Lauri Ojansivu 7a38213ab6 CHANGELOG: the Helm chart's FerretDB switch, guarded from here. 2026-08-12 17:15:06 +03:00
Lauri Ojansivu 5d1c9d0a3c Tests: pin the Helm chart's FerretDB switch from this side of the fence.
wekan/charts is a separate repository with no test runner, and the two things
that went wrong in it (charts#54, charts#55) are WeKan's own settings: the URL
parameter WeKan needs (#6582) and the environment WeKan's docker-compose.yml runs
FerretDB with. So the guard lives here, reads .tools/charts, and SKIPS with a
message when that clone is absent rather than passing quietly - the same shape as
the FerretDB checks in tests/fixedVulnerabilityClasses.test.cjs.

What it holds: no per-pod StatefulSet DNS may be assembled in _helpers.tpl again
(that was charts#54 - Bitnami's naming against groundhog2k's services, so WeKan
dialled a host that does not exist); the Service the URL names is the Service the
chart creates; directConnection=true is there and an external URL that already
sets it is left alone; the WeKan environment is docker-compose.yml's FerretDB one
(polling, sockjs, WRITABLE_PATH, WITH_API) with no MONGO_OPLOG_URL and no second
MONGO_URL; the image is ghcr.io/wekan/ferretdb with quay.io and Docker Hub named
as alternatives; there is one replica and the reason is written down; the data
survives a restart; and an existing MongoDB is still reachable after the upgrade.

One check is a sweep rather than an example: every setting docker-compose.yml
documents has to appear in values.yaml. It found NODE_OPTIONS missing, which the
chart now carries too - a Helm user gets the same reference a Docker user does.

Verified by running the suite against the current .tools/charts checkout (9 tests)
and the whole node suite (385 suites, 0 failed).

Thanks to salcinad, ouvry-ems and xet7 !
2026-08-12 17:13:36 +03:00
Lauri Ojansivu b07b599af8 CHANGELOG: repoint the Worker-role link after the commit was amended. 2026-08-12 16:06:41 +03:00
Lauri Ojansivu 2328b70aa9 CHANGELOG: the Worker role doing what it is for, and the card that lost its name. 2026-08-12 16:05:15 +03:00
Lauri Ojansivu f2e58945f0 Fix #3144: an archived card is still named in its own history.
"Activities for archived card displayed as undefined on board settings." Move a
card around to make some activities, archive it, open the board sidebar - and the
sentences that named that card name nothing. The reporter said what they wanted
instead: "{{ card title }} [archived]".

The feed asked `activity.card()` and rendered its title. An archived card is not
published to the client, so that lookup returns nothing and each sentence was
built around a hole: a card that still exists, still has a title, and whose
activities are right there on the page went nameless in its own history.

The activity already knew the title in most cases - createCard, moveCard and
moveCardBoard have stored `cardTitle` for years. So the answer is not to publish
archived cards to every sidebar, which would send data to clients that do not need
it; it is to read what the activity recorded. models/lib/activityCardLink.js
decides, in order: the card's CURRENT title when the card is here (a card renamed
since should read as it reads now), marked when that card says it is archived; the
title the activity recorded when the card is not here; and null when neither
exists, which the feed renders as "this card" rather than as a gap in the middle
of a sentence.

The link survives all of it. A card URL is /b/<boardId>/<slug>/<cardId>, and an
activity carries both ids, so the anchor is built without the card document
(models/lib/cardUrl.js). No card document also means no slug, and the fallback
'board' is a segment the route ignores - it does not invent "/b/undefined/...",
which is the bug that helper was written for (#6427).

A missing card is NOT called archived, and that is deliberate. It nearly always is
- a deleted card takes its activities with it (Cards.cardRemover) - but with lazy
card loading a card can simply be outside the window this client was sent, and
"archived" would then be a statement the feed cannot support. The mark is used
only where the card is here and says so itself.

The two activities that were NOT storing a title are the two about archiving:
archivedCard and restoredCard now record `cardTitle` like the others. They are
exactly the ones guaranteed to be about a card the client can no longer look up,
which is why this bug was reported about archiving and not about anything else.
Activities written before this have no title, and those are the ones that read
"this card".

Both strings are translated (`archived`, `this-card`, which already exist in every
language file), and a title out of the database is still sanitised before it
reaches the page.

tests/activityCardLink.test.cjs covers the reported case and the ones around it:
the live title winning over the recorded one, the board slug when the board
document is at hand, an activity with no cardId naming the card without inventing
a link, junk titles ('', '   ', null, numbers, objects) never becoming titles, and
a missing card never being called archived. Plus both ends of the wiring: the
archive activity recording the title, and no call site passing only the card.

Verified by running the suite and the whole node suite (384 suites, 0 failed). Not
verified in a browser: this sandbox has none.

Thanks to rptl and xet7 !

Fixes #3144.
2026-08-12 16:01:51 +03:00
Lauri Ojansivu 7b35cb5f03 Fix #3189: a Worker can move a card and assign themselves to it.
"User with Worker permission can't assignee card if it has been assigned to
someone else before." The card showed the reporter's name for a moment and then
showed the previous assignee again - an optimistic write the server threw away.
The previous assignee was a red herring: a Worker could not assign themselves at
all.

The board schema defines the role as "only allowed to move card, assign himself to
card and comment". Both of those are card UPDATES, and the capability table gives
Worker `write: false` - so the role defined by two specific writes was allowed
neither. The client had already built the UI for it: the assignee popup shows a
Worker exactly one name, their own. The server discarded what they clicked.

Widening `write` to true was never the fix - that hands a Worker every field of
every card, which is the opposite of what the role is. What was missing is the
field-level policy the "Known gaps" section of docs/Features/Members/Roles.md said
this needed, so that is what this is.

models/lib/workerCardWrite.js decides an update by WHAT IT WRITES:

  MOVE          $set of listId / swimlaneId / sort, plus the dateLastActivity and
                modifiedAt written with them
  SELF-ASSIGN   $addToSet or $pull of `assignees` with EXACTLY the caller's own id

and refuses everything else BY DEFAULT. Not "deny a list of dangerous fields" -
allow only when every operator and every field in the modifier is one of the
above. A modifier it does not recognise ($unset, $rename, $inc, a raw replacement
document, an operator MongoDB adds next year) is not a move and not a
self-assignment, so it is refused. A qualifier on the assignee write ($each, $in)
is refused rather than unpicked: a policy that starts unpicking list shapes is a
policy with a hole in it.

Deliberately not included: boardId, because "move card" here means within the
board and a cross-board move is a different act with its own guard
(GHSA-gm7v-pc38-53jr); `members`, because the role says assignee; and assigning or
unassigning anybody else, because the role says himself.

server/permissions/cards.js reads the modifier now (Meteor passes it to the allow
rule and it was being ignored) and consults the policy only for a member who has
no write access AND is a Worker on that board. Write access is still the first
question, and the vote/poker canaries still fire before either.

`moveCard` becomes its own capability in models/lib/boardRoleCapabilities.js -
true for every role that can write, plus Worker - so the client's canMoveCard()
asks for the same thing the server enforces. That is one answer added rather than
eight changed.

docs/Features/Members/Roles.md gains the column, the paragraph that used to
explain why moving a card could NOT be its own column now explains why it is, and
the Worker entry moves from "Known gaps" to "Fixed". The gaps section is empty and
says so.

tests/workerCardWrite.test.cjs covers both halves of the role and, more
importantly, the refusals: somebody else's name, a move with a passenger field, a
title, a label, members, an $unset, an $inc, a $rename, a whole-document
replacement, $each/$in qualifiers, and junk of every shape - none of which may
become permission. tests/boardRoles.test.cjs gains the column and flips its
gap test: the ⚠ that marked this role had to go with the fix.

Verified by running both suites and the whole node suite (383 suites, 0 failed).
Not verified in a browser: this sandbox has none, so the client half is verified
from the code and the popup template that has always shown a Worker their own name.

Thanks to rptl and xet7 !

Fixes #3189.
2026-08-12 15:58:17 +03:00
Lauri Ojansivu 49463d3dcb CHANGELOG: the snap reconciling its two copies, and the link that closed a card. 2026-08-12 15:40:15 +03:00
Lauri Ojansivu 75a23b76ae Fix #6588: a file:// link in a card no longer makes the card impossible to open.
"A card whose description or a comment contains a `file://` URL cannot be opened.
Clicking the minicard plays the open animation but the card details panel never
mounts. There is no visible error by default because Blaze swallows the render
exception." Captured, it was:

  TypeError: this.__schemas__[t.toLowerCase()].validate is not a function
      testSchemaAt / matchAtStart / markdown-it (tokenize -> parse -> render)

WeKan registers eight custom URL schemes so links like `thunderlink:` and
`file://` are recognised, and it registered them the linkify-it 4/5 way:

  Markdown.linkify.add(scheme + ':', 'http:')      // "behave like http:"

linkify-it 6 removed string aliases and builds the definition by SPREADING it:

  const def = { normalize: ..., ...definition };

Spreading the string 'http:' produces {0:'h',1:'t',2:'t',3:'p',4:':'} - an entry
with no `validate` - and testSchemaAt then calls `.validate(...)` on it. So every
one of those eight schemes was a landmine in any card's text, and the card holding
one could not be opened again. Reproduced here against the installed linkify-it,
not from the report: the test builds that same registration and watches it throw.

Each scheme now carries a validate of its own, which is all the alias was standing
in for: the URL after the scheme - an optional `//`, then everything up to
whitespace or a bracket - with trailing sentence punctuation handed back, so "see
file://server/share/doc.xlsm." does not swallow the full stop.

WHAT THAT DOES NOT DO, because the list promises more than it delivers.
Registering a scheme makes markdown-it RECOGNISE it; two filters then decide
whether a reader gets a clickable link, and both refuse these. markdown-it's own
validateLink blocks file: (with javascript:, vbscript: and data:), and the
viewer's DOMPurify allows only http/https/ftp/ftps/mailto/tel/callto/cid/xmpp
hrefs. So none of the eight has ever produced a clickable link - its only
observable effect was the crash. Making them clickable means relaxing both
filters for schemes that launch local applications, which is the security decision
#3218 asks for and not something to slip in while fixing a crash. It goes to TODO
Later; the tests pin today's answer so the day it changes is a decision.

And the guarantee behind the fix, which is worth more than the fix: nothing a card
contains may make that card impossible to open. The markdown render is wrapped, so
a throw from a plugin, a pathological formula or the next markdown-it upgrade
shows the text as written - escaped, sanitised, unformatted, and readable - instead
of a panel that never mounts.

tests/markdownCustomUrlSchemes.test.cjs runs the real linkify-it and markdown-it
from node_modules: the old registration throwing, every scheme having a working
validate, the reported strings rendering, punctuation staying in the sentence, a
bare `file:` and junk input returning zero rather than throwing, and the viewer's
fallback returning sanitised text rather than nothing.

Verified by running the suite and the whole node suite (382 suites, 0 failed). Not
verified in a browser: this sandbox has none, so the crash and the fix are
exercised against the same dependency the browser would load.

Thanks to rmb82 and xet7 !

Fixes #6588.
2026-08-12 15:36:53 +03:00
Lauri Ojansivu 40ff8151ea Snap: pick the database that holds the work, and merge the other one into its history.
An email report, on an instance being served the older of its two copies: "some
users are unable to log in (error: 'user not found'), and boards created after
mid-July appear to be missing." That is what serving a copy looks like from the
outside - the accounts and boards made after the copy was taken simply are not
there.

Until now the snap handled that by telling the admin to sort it out. #6583 taught
it not to switch on a guess, so when both MongoDB and the migrated FerretDB have
been written to since the migration it printed the two `snap set wekan
database=...` commands and stayed where it was. That message is in `snap logs`;
most people never see it, and their site meanwhile shows the wrong copy.

A file timestamp cannot answer "which copy holds the work" - an mtime says when a
file was touched, and starting a database touches its files. But both copies can
be READ, so ask them. bin/db-eval.mjs gains an `evidence` command (per-collection
counts, plus the newest timestamp any document carries) and bin/database-choose.mjs
decides from the two answers:

  one holds everything the other holds, and is not older -> serve it, nothing to
  merge;
  both hold work the other does not, and one is clearly newer -> serve the newer
  one and copy across what only exists in the other;
  too close to call, or nothing readable -> change NOTHING and keep the existing
  message, because that case is a decision about somebody's work.

"Clearly newer" is a margin, not a tie-break: six hours by default, so the writes
the migration itself makes on both sides cannot decide anything.

WHY THE MERGE IS SAFE, and it is WeKan's own design that makes it safe: the
history is APPEND-ONLY (docs/Features/Reports/History/History.md, which gains a
section 9a on exactly this). Activities, comments and the coming changeHistory
rows are never rewritten in place, so they can only be ADDED to. bin/database-
merge-missing.mjs therefore inserts every document whose _id is ABSENT from the
chosen database and does nothing else:

  * a document that IS there is never overwritten - a card edited on both sides
    keeps the version in the copy being served, which is the newer one;
  * nothing is deleted, on either side;
  * the copy that was not chosen stays on disk, so `snap set wekan database=<the
    other one>` is still the way back - now a choice rather than a repair;
  * the work done on the copy that is not being served becomes readable in the
    served copy's card History instead of sitting in a database nobody opens.

For the reported symptoms that means the missing users and boards are inserted
rather than left behind: a user who could not log in exists again, with their own
_id, and their boards with them.

A three-way merge - reconciling two edits of one field - is not attempted and will
not be from a script. Where the evidence cannot separate the copies, the snap
still says so and hands the decision to a person.

WEKAN_AUTOPICK=false turns the whole thing off, `database-autopick --dry-run`
shows what it would do, and AUTOPICK_MERGE_LIMIT bounds how much it copies.

tests/snapDatabaseAutopick.test.cjs covers the decision (both superset
directions, the both-diverged case, one side empty or unreadable, too-close, no
timestamps, and a BIGGER but older copy that must not win on size) and runs the
merge against an in-memory pair of databases: what is missing is inserted, what
exists is not overwritten, nothing is deleted on either side, --dry-run writes
nothing, a limit is not an error, and the merge script contains no update or
delete operation at all.

tests/ferretdbMigrationStale.test.cjs keeps its rule - no switch on a guess - with
the assertion updated to match what changed: this branch may not choose from the
timestamps it has, it delegates to the one thing that can choose, and when even
that cannot tell the copies apart the admin still gets both commands.

Verified by running both suites and the whole node suite (381 suites, 0 failed).
The snap itself cannot be built or refreshed in this sandbox, so the orchestration
that starts each database on a temporary port is verified from the code and from
the shapes migration-control already uses, not from a running snap.

Thanks to xet7 !
2026-08-12 15:30:25 +03:00
Lauri Ojansivu b7affb7a94 Update dependencies: the S3/GCS clients, bson, ws and the build toolchain.
Two Meteor packages and 63 npm ones, from a lockfile refresh - package.json's own
ranges are unchanged, so these are the versions those ranges already allowed.

The two that reach running WeKan rather than a build: bson 7.3.1 -> 7.3.2, the
codec the driver speaks to MongoDB and to FerretDB with, and ws 8.21.2 -> 8.21.3,
the WebSocket under Meteor's DDP connection. aldeed:collection2 4.2.1 -> 4.2.2 is
the schema validator - the one whose error formatter turned a list with no title
into "Cannot read properties of undefined (reading 'title')" in v10.80.

The AWS SDK (client-s3 and eighteen credential/signing/presigner packages, plus
the @smithy transport under them) and @google-cloud/storage are the optional
attachment backends: unused unless S3 or GCS is configured. The rest - rspack,
terser, browserslist and its tables, eslint and @typescript-eslint, puppeteer,
memfs and the @jsonjoy.com filesystem packages - are build- and test-time only
and ship in no WeKan bundle.

The CHANGELOG entry is the dependency batch of the upcoming release, in the
established form: what each one IS, so a reader can tell which of them could
affect their instance and which cannot.

Verified by running the whole node suite (380 suites, 0 failed) after the update.

Thanks to dependabot.
2026-08-12 15:16:52 +03:00
Lauri Ojansivu c495cb5d29 CHANGELOG: what the PDF fix could not reach, and one report with nothing to point at yet. 2026-08-12 15:13:36 +03:00
Lauri Ojansivu 35184e89bd CHANGELOG: the snap's four ways of serving a copy, and the reported UI bugs. 2026-08-12 15:11:49 +03:00
Lauri Ojansivu eb47e04657 Tests: ask whether a fixed mistake is anywhere ELSE, and close two gaps it found.
Every entry in https://wekan.fi/hall-of-fame/ has a suite pinning the place it was
found. None of them could answer the other half of the question: is the same
MISTAKE somewhere else - in WeKan, or in the FerretDB fork WeKan ships as its
default database? A per-site regression test knows one file, and the next
occurrence is written months later by somebody who never read the advisory.

tests/fixedVulnerabilityClasses.test.cjs asks it as a CLASS, over the whole tree
and over .tools/FerretDB when the clone is there (and saying it skipped those
checks when it is not, rather than passing quietly):

  ZipBleed      an archive entry naming its own destination path, without a
                containment check - and only for real archive readers, since a
                readdir entry name is one component and can escape nothing
  WhereBleed    a client-supplied selector carrying operators that execute
                JavaScript
  TransitBleed  rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED, and Go's
                InsecureSkipVerify in the FerretDB fork
  SourceBleed   eval() / new Function() on the server
  MimeBleed     an SVG served inline by a route that neither sanitizes it nor
                forces a download
  RandomBleed   a secret from Math.random(), and random bytes folded onto an
                alphabet with a bare % - CWE-1204 modulo bias

Comments and string literals are stripped before matching, or the advisory notes
explaining a fix would trip the check enforcing it. Allowed occurrences are listed
with the reason they are allowed. Each check was confirmed to FAIL on the mistake
by writing one into the tree and watching it fire; both probes were removed.

It found one straight away. models/lib/mongoSelectorSafety.js rejected `$where`
and nothing else - but `$where` stopped being the only way a FIND filter runs
JavaScript in MongoDB 4.4, which takes an aggregation expression inside a find
filter, and `$expr: { $function: { body, args, lang } }` there runs a JavaScript
body through the same client-supplied selector. `$accumulator` is the same again.
Both are rejected now, the helper is named for what it tests (hasServerSideJs,
with hasWhere as the alias every caller and the advisory use), and a cyclic
selector cannot spin it - an infinite recursion inside a security check is a
denial of service of its own. tests/mongoSelectorSafety.test.cjs gains those,
plus the negatives that keep it usable: a field literally called `function`, and
`$expr` doing ordinary comparison, are both fine.

randombleed moves from RECORDED to GUARDED in tests/securityRegressionCoverage.
test.cjs, and its gap count from 25 to 24: the rejection sampling CWE-1204 was
about is now held everywhere rather than in one file.

And one release entry had shipped with no test at all - the only one since v10.80
that is not a translation update. tests/databaseProblemsFixes.test.cjs covers it:
the default-swimlane self-heal calling the synchronous upsert Meteor 3 removed
(and the getter that starts the async one without awaiting it, which must handle
its own rejection or a self-heal failure takes the process with it), List.move()
inserting a list with no title and getting an error from the error formatter
rather than from the validator, and the two classifier rules that stop either
reading as "unknown / unclassified" in Admin Panel / Problems - on every database
WeKan runs on, and without swallowing an ordinary network error.

Coverage checked, not assumed: all 60 published vulnerabilities are named by a
suite, and every one of those suites carries negative assertions as well as
positive ones. New suites need no registration - build.sh's test run calls
tests/run-node-suites.cjs, which DISCOVERS every tests/*.test.cjs - so all three
files here are in the "Run WeKan's own tests" options already.

Verified by running the new suites and the whole node suite (380 suites, 0 failed).

Thanks to xet7 !
2026-08-12 15:06:45 +03:00
Lauri Ojansivu eb14168893 Fix #6586: the PDF export writes umlauts, not question marks.
"When using Board settings » Export board » export / PDF we get a pdf file but the
umlauts (german, ä, ü, ö,...) are corrupt", "all the text in this PDF file is
markdown formatted - this doesn't make sense in a pdf file, does it?", and "I can't
see in which swimlane a card is in that export, no tags".

The umlauts were removed on purpose, one line before anything was written:

  .replace(/[^\x20-\x7E\n]/g, '?')

so "Grüße" left the server as "Gr??e". That line was a workaround for the real
problem rather than a choice: text in a PDF is BYTES PLUS AN ENCODING, and the
exporter declared neither. A `(...)` string in a content stream is read through the
font's encoding; no /Encoding was given, so a viewer used the font's built-in one,
where the bytes above 127 are not the Latin-1 letters - and the file was assembled
as a UTF-8 string, so one "ü" would have arrived as two bytes and drawn as two
wrong glyphs. With no encoding to write into, erasing the letters was the only
thing left that did not corrupt the file.

So the font declares /WinAnsiEncoding - Windows-1252, which every viewer has for
the base-14 fonts - the text is encoded to those single bytes, and the file is
assembled as binary. The xref offsets are measured in bytes for the same reason:
measured as UTF-8, every offset after the first accented character is wrong, and
some viewers refuse the file outright.

What Windows-1252 does not have is transliterated rather than erased: a Latin
letter loses its mark (ā -> a, ź -> z, Ł -> L) and keeps its word. Only a script
the base-14 fonts cannot draw at all - Cyrillic, Greek, Hebrew, Arabic, CJK, emoji
- still falls back to '?', because the fix for those is an embedded Unicode font
and that is a font binary this repository does not carry. It is in TODO Later.

Markdown is flattened instead of printed. `**bold**` in a PDF is four asterisks,
and the board export was itself emitting markdown - "## List (3)", "- Card" - into
a file that has no markdown in it. The structure is drawn now: headings and titles
in Courier-Bold, cards as bullets, and a card's own markdown reduced to its words
(links keep their text and their URL). Emphasis underscores only count at a word
boundary, so `snake_case` and `file_name_here` are left alone - markdown treats
them the same way.

And the board export says what it was missing: swimlanes (walked swimlane by
swimlane when a board has more than the one every board is created with, and not
mentioned at all when it does not), labels, members, assignees and the dates.

The PDF itself - encoding, flattening, wrapping, pagination - moves to
models/lib/pdfDocument.js, which is pure, so tests/pdfExport.test.cjs reads the
BYTES that come out: that ü is 0xFC and ß is 0xDF, that the euro sign is 0x80,
which Latin-1 does not have at all, that every xref row points at the object it
claims, and that the bold font is selected only where a heading asks for it. Its
negative half is the part worth keeping: junk never throws, an empty board still
produces a file that opens, `2 * 3 * 4` and `a_b_c` are not emphasis, and a
Japanese title degrades to '?' without ever emitting a byte above 0xFF into the
stream.

Verified by running the suite and the whole node suite (378 suites, 0 failed). Not
verified against a PDF viewer: this sandbox has none, so the bytes are checked
against the PDF specification's own rules instead.

Thanks to Heart1010 and xet7 !

Fixes #6586.
2026-08-12 14:53:06 +03:00
Lauri Ojansivu 32b61f186f Checklists: "Show on minicard" is an override, so unchecking it hides the checklist.
Reported by email with a screenshot (Swedish): "Även om jag avmarkerar 'Visa på
minikort' (och vill dölja checklistan på minikortet)" - the switch is off and the
checklist is on the minicard anyway.

It was off for everybody. The minicard asked:

  board.allowsChecklistsOnMinicard || checklist.showChecklistAtMinicard

and the board flag defaults to TRUE. An OR cannot be argued with: while the board
setting is on, no value of the checklist's own field changes the answer. The popup
made it look like a working switch - it drew the state from the raw field, which
starts false, so it read OFF beside a checklist that was plainly showing, and
clicking it wrote true and changed nothing visible. Clicking again wrote false and
changed nothing either. Dead in both directions, which is exactly what the
screenshot shows.

The two settings are a DEFAULT and an OVERRIDE, not two ways of saying yes:

  showChecklistAtMinicard === true   -> shown, whatever the board says
  showChecklistAtMinicard === false  -> hidden, whatever the board says
  showChecklistAtMinicard undefined  -> follow the board's default

An override needs three states, so the field loses `defaultValue: false`: with
every checklist born false, "hidden" and "not chosen" are the same value and the
override cannot exist. models/lib/minicardChecklistVisibility.js is the one rule,
used by the minicard, the popup's switch and the toggle - and the toggle now flips
what is ON SCREEN rather than the raw field, because flipping the field is what
made the first click do nothing.

Checklists written under the old default carry a false that meant "follow the
board". Left alone under the new reading it would mean "hide this one" and every
checklist would vanish from every minicard - the mirror image of the bug. The
schema-upgrade step checklist-minicard-unset clears exactly those, and it is the
one step there that must run ONCE EVER rather than once per version: afterwards a
stored false is somebody's choice, and re-running would put their checklists back.
It keeps its own marker beside the schema-upgrade one for that.

tests/minicardChecklistVisibility.test.cjs covers the reported case, the opposite
one, that two clicks return to the board default, and that only true and false are
opinions - a string, a number or a null from an old REST client or a bad import
must not quietly mean "hide". tests/schemaUpgradeSteps.test.cjs gains the step,
including the negative: after it has run, a false set by a person survives the next
release.

Verified by running both suites and the whole node suite (377 suites, 0 failed).
Not verified in a browser: this sandbox has none.

Thanks to xet7 !
2026-08-12 14:45:07 +03:00
Lauri Ojansivu f7b0a4d1d2 Fix #6587: a board's type now follows Member Settings / Font size.
"The font size setting in the user menu (Settings -> Font Size) has no effect on
mini cards, while it correctly applies to other UI elements."

Two halves, and both had to be wrong for this to happen.

The preset was applied to <body>. A font-size on body is inherited by children
that ask for a relative size, and it leaves `rem` alone entirely - rem is measured
against the ROOT element. So anything sized in rem never heard about the setting.

And a board's type is sized in px: the list heading, the minicard title and the
"Add card" link were pinned to 16/14/13px in #6465, and the minicard's menu, drag
handle and mobile icons alongside them. px follows nothing at all - not the root
size, not body's - so the board was the part of the UI the setting could not reach
no matter which element carried it.

So the preset moves to <html> and the board's sizes become rem: 1rem, 0.875rem,
0.8125rem, which are the same 16/14/13px at the default root size. The type scale
#6465 set is unchanged and its guard still holds the order; what changes is that a
130% preset now scales it, and 80% shrinks it.

The size goes on ONE element only. With it on html AND body, a 130% preset
compounds to 169% for everything sized relative to body. Everything under body
still scales, because body inherits the root size - so nothing is lost by moving
it up, and uiFont.js sends only the font-size marker class to the root while the
font-family and colour rules keep matching on body, where their form-control
selectors live.

rem, not px and not vw: px cannot follow the setting, and vw is what made elements
resize with the browser window (fixed in earlier commits, deliberately). rem
follows the root font size and nothing else, which is exactly the preference the
user set.

The report also says minicards are "approximately 7-8pt at 100% browser zoom". At
the default root size a card title is 14px, and this commit does not change that
number - what it changes is that the setting can now raise it. If cards still come
out much smaller than the list heading above them, the board's own zoom control is
the other thing that scales them, and it is not affected here.

tests/listTypeScale.test.cjs reads rem as well as px (1rem = 16px at the default,
so the pinned ORDER reads the same either way) and gains two checks of its own:
that each of the three sizes is in a unit the preset can scale, and that the preset
lands on the root element and only there.

Verified by running that suite, tests/uiFonts.test.cjs and the whole node suite
(376 suites, 0 failed). Not verified in a browser: this sandbox has none.

Thanks to CCmesch and xet7 !

Fixes #6587.
2026-08-12 14:38:27 +03:00
Lauri Ojansivu 4400629066 Snap upgrade: copying the old common directory back is what destroys the restore.
From an upgrade report by email. The admin upgraded 6.09 -> 10.85 exactly by the
documented route - dump, move /var/snap/wekan/common aside, refresh, restore - and
it worked: the boards were back. Then, to get their attachments, they undid step 3
the way it reads:

  cp -pR /root/common/* /var/snap/wekan/common/

Their log, seconds later:

  mongodb-control: line 489: 351018 Aborted   ... $SNAP/bin/mongod --dbpath=...
  snap.wekan.mongodb.service: Main process exited, code=exited, status=134/n/a

"After that, Wekan was still running, but all the boards were missing."

Everything beside files/ in that directory is the raw database - *.wt, WiredTiger*,
_mdb_catalog.wt, sizeStorer.wt, storage.bson, mongod.lock, journal/ - and copying
it over a RUNNING mongod replaces the files it has open underneath it. mongod
aborts, and what comes back up is the 6.09 database it cannot read.

The documentation is where this is decided, because the mistake is made before any
code of ours runs. Step 6 already said to copy back only `files`; what it did not
say is what the obvious inversion of step 3 costs, so it says it now, in the step
itself: the exact wrong command, which files those are, what the failure looks like
in `snap logs wekan.mongodb` so somebody who has already done it recognises their
own log, and that the whole directory IS put back in one place - the "going back to
6.09" procedure - onto a STOPPED snap with the current contents removed first,
which is a different operation.

And mongodb-control now recognises the abort it produces. Exit 134 gets a case of
its own beside exit 132 (SIGILL/AVX): SIGABRT, the data directory changed underneath
a running mongod, which files may and may not be copied back, and the one way out,
`snap run wekan.database-restore <archive>`. Before this the whole story in the log
was the word "Aborted".

The same report also had a board with attachments that loads forever after a
restore. That one is not diagnosed here - the attached logs are mongod startup
only, with no errors in them - so it goes to TODO Later rather than being guessed
at.

tests/snapCopyBackDestroysDatabase.test.cjs pins both halves, including that the
warning sits in the step where the mistake is made and above the procedure it must
not be confused with.

Verified by running the new suite and the whole node suite (376 suites, 0 failed).

Thanks to xet7 !
2026-08-12 14:31:58 +03:00
Lauri Ojansivu c3811c5283 Snap: a MongoDB this snap has no reader for stops and says so, on every CPU.
"It turns out the mongodb version on my installation was even older. It was
running on mongodb 3.2, this is why your 4.2 check was not doing anything." Their
site never showed the explanatory page either: it waited for MongoDB forever,
which is the loop #6471 was opened about.

The migratemongo 3.2 tools are staged for amd64 ONLY - MongoDB published no 3.2
build for any other architecture (the migratemongo part in snapcraft.yaml). So on
arm64 and everything else, a MongoDB 3.x database has NO reader in this snap at
all, and that branch handed the case to fall_back_to_mongodb, which starts mongod
7 again. mongod 7 is the binary that has already refused these files, so it fails,
mongodb-control re-runs the migration, and the site sits on "MongoDB not ready
yet, retrying in 5 seconds..." with the reason only in `snap logs`.

A missing reader and an unreadable database are not the same thing, and only one
of them can usefully be retried. When mongod has already said "too recent to start
up on the existing data files", the tool that could have read them not being in
this snap is a final answer: stop, keep every byte, and serve the page. Data that
no mongod ever called too old still falls back to MongoDB as before, because that
case may yet come up serving. The log names the architecture, since that is the
part an admin cannot see from the outside.

The page also stops leaving the way out as an exercise. "Move the data across with
a MongoDB that can read it" was true and not enough - the reporter worked out the
steps themselves, and another admin who got this far lost their restored database
to the step that is easy to get wrong: they copied the old data files back into a
RUNNING data directory and mongod aborted with SIGABRT. So the page gives the four
steps in this snap's own paths: copy $SNAP_COMMON somewhere safe, dump it with a
MongoDB of that vintage, move the old database files OUT (naming them, and saying
that files/ - the attachments and avatars - stays), and restore with the snap's own
`snap run wekan.database-restore <archive>` rather than a mongorestore invocation
they have to assemble.

tests/snapOldMongoData.test.cjs gains the missing-reader branch and reads the
page's own HTML for the four steps. Its "newest first" test no longer checks the
order by "the first mongod_says_data_too_old comes after the 3.x probe": there are
now two ways the third reader produces no database - it is ABSENT, decided just
before the probe, or it RUNS and refuses, decided just after - and neither is
reachable until mongod 7 and mongod 4.2 have both failed, which is what that test
is really about.

Verified by running the suite and the whole node suite (375 suites, 0 failed). The
snap itself cannot be built or refreshed in this sandbox, so the arm64 path is
verified from the code and the packaging, not from a running snap.

Thanks to Philippe-Bentegeac, JDeepix and xet7 !

Fixes #6471.
2026-08-12 14:28:49 +03:00
Lauri Ojansivu 4b99fa701d Snap: the old MongoDB files are not WeKan's problem once it runs on FerretDB.
"It somehow tries to access Mongodb again instead of Ferretdb. I don't even have
an old version, but just this" - and the screenshot is the #6471 page, "Wekan
cannot open the existing database", on a snap whose data was in FerretDB and
perfectly readable.

The migration never deletes what it copied from, so a migrated snap keeps its old
MongoDB files in $SNAP_COMMON forever. Two places treated their presence as
something WeKan had to act on:

migration-pending called a migration "pending" for any $SNAP_COMMON holding
WiredTiger files without the .migration-to-ferretdb-done marker - the database
setting was never consulted. So an instance whose marker is gone (a forced
re-migration cleared it, or it was migrated by a snap old enough never to have
written one) was dragged back to those files on every start: mongodb-control
handed off to migration-control, which probed them with mongod 7, mongod 4.2 and
the 3.2 tools, and when none could open them wrote .mongodb-data-too-old.

wekan-control then served the explanatory page for that marker unconditionally.
That page is right when MongoDB is the database WeKan needs and cannot be read.
It is wrong when the snap runs on FerretDB: it replaced a working site with an
explanation of a database that site does not use.

Both now ask what the snap is actually running, and "already on FerretDB" means
the SETTING plus real data in files/db - never the setting alone, because
database=ferretdb with an empty files/db is precisely the case the migration
exists for and must still run.

The marker is left in place rather than deleted. It is true - those files really
are unreadable by this snap - and it belongs to MongoDB, so switching back with
`snap set wekan database=mongodb` brings the page back, correctly.

Nothing is switched automatically. #6583 is what choosing between two copies on
the snap's own initiative costs, and this page's own promise is that nothing
changes until an admin acts. What it does now is SAY that a FerretDB copy is
there when one is: a third way forward, the one command that serves it, the one
that undoes it, and the warning that a migrated copy is only as new as the
migration that made it. With no copy to serve it is word for word the page #6471
wrote.

tests/snapFerretdbNotBlockedByOldMongo.test.cjs RUNS migration-pending against
synthesised $SNAP/$SNAP_COMMON trees with a stub snapctl - already on FerretDB,
database=ferretdb with an empty files/db, database=mongodb with a copy beside it,
and the completed marker - and reads the page's own HTML for both shapes. It also
pins that the marker re-exec in the wait loop stays in the MongoDB branch: reached
while serving FerretDB, the new "start WeKan normally" branch would send it
straight back into the loop, exec after exec with no sleep between.

Verified by running the new suite, tests/snapOldMongoData.test.cjs (the #6471 page
it must not change) and the whole node suite (375 suites, 0 failed). The snap
itself cannot be built or refreshed in this sandbox.

Thanks to mueschel and xet7 !

Fixes #6585.
2026-08-12 14:22:24 +03:00
Lauri Ojansivu f5fee7dc01 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-08-12 14:05:35 +03:00
Lauri Ojansivu 894d7e87c0 CHANGELOG: the resume that copied a database three weeks out of date. 2026-08-12 14:05:34 +03:00
Lauri Ojansivu c783829823 Snap: a migration may not resume onto a database the source has outgrown.
"Our snap updated automatically from 10.81 yesterday to 10.85 this morning. Data
in the DB is from about 2 - 3 weeks ago. Many cards and work is lost."

Nothing was lost: the MongoDB data is in $SNAP_COMMON and untouched. What the
update completed was a migration that had been interrupted weeks earlier.

The MongoDB -> FerretDB migration is resumable, and has to be - it can run for
hours, and a snap refresh, a stop or a reboot part-way through is normal rather
than exceptional (migration-control's interrupted_keep_progress). So the importer
records every finished collection in $SNAP_COMMON/migration-progress.json and
skips those next time.

That checkpoint was only ever checked against the TARGET. migration-control
deletes it whenever it discards a partial SQLite (discard_partial_ferretdb), so it
can never name collections that are not there. Nothing checked it against the
SOURCE - and between an interrupted migration and its retry the snap hands WeKan
back to MongoDB and people go on using it (fail_3x_keep_progress,
resume_next_start). A migration interrupted in July and resumed in August
therefore skipped every collection it had finished in July, copied only the rest,
and reported success. finish_success then switched the snap onto that copy:
boards and cards as they stood in July, with three weeks of work still in MongoDB
and simply not being served.

A new snap revision is usually what sets the resume going again, because the
per-revision failure counter starts at zero (bin/stale-marker) - which is why
this reads as "the update lost my data". The update is when the weeks-old copy
finally got served. Same family as #6583, and the same answer: a copy made at one
time may not be used as if it were current.

Two checks, because they fail differently.

bin/migration-checkpoint-stale.mjs asks whether the checkpoint is too old to
resume from at all. mongod rewrites its WiredTiger files whenever it commits, so
the newest mtime among them is when MongoDB was last written to - the evidence
bin/ferretdb-migration-stale already uses for the finished-migration case. Later
than the checkpoint (by more than a margin, since the migration's own temporary
mongod writes on the way out) means the collections it lists are copies of an
older database, so the collection half is dropped and they are copied again. The
checkpoint's age is the OLDER of its updatedAt and the file's mtime: the two
disagreeing is a reason to doubt it, and a needless re-copy costs minutes while a
wrong skip costs weeks. The file half is kept - attachments are written once and
re-verified on disk at the recorded size, and re-extracting gigabytes is the
slowest part of a resume.

migration-control runs it as its first step, before it starts any mongod of its
own: starting one rewrites those files, and after that the question can no longer
be asked. It is the only way into a text migration - snap run wekan.migrate,
auto-migration and mongodb-control's fallback all arrive through it.

verifyCollectionCounts, in both importers, then asks the two databases themselves
once the copying is done, when both are quiet: a collection whose copy holds fewer
documents than the source is copied again from the source as it is now. It does
not care WHY a copy came up short, which is the point of a check standing behind
another one. Only a shortfall is acted on - a copy holding MORE is a resume
carrying documents deleted from MongoDB since, and deleting on that evidence is
the guess this whole family of bugs is made of - and a shortfall that survives the
second copy is reported rather than fatal, so one document FerretDB will not
accept cannot leave the snap in a migration it can never finish. It sits inside
the same "not repair mode" block as the copying it verifies, since a FILES_ONLY
repair (#6473) must never write text over a FerretDB people have been using.

tests/migrationCheckpointStale.test.cjs runs the detector against synthesised
$SNAP_COMMON directories: the reported case, an ordinary resume minutes later, the
margin that covers the migration stopping its own source mongod, a mongodb.log
that must not count as somebody using the database, and that migration-control
asks before it starts a mongod. tests/migrationCopyShortfall.test.cjs extracts
verifyCollectionCounts from BOTH importers and exercises it behaviourally.

Verified by running both suites and the whole node suite (374 suites, 0 failed).
The snap itself cannot be built or refreshed in this sandbox.

Thanks to Alishara and xet7 !

Fixes #6585.
2026-08-12 14:05:34 +03:00
github-actions[bot] c87da21587 Bump versions for v10.85 2026-08-12 04:19:57 +00:00
Lauri Ojansivu 6c8e4e88b3 Prepare v10.85 release 2026-08-12 07:18:38 +03:00
Lauri Ojansivu 2d3d6d6ca7 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-08-12 06:55:35 +03:00
Lauri Ojansivu 5b23954be8 CHANGELOG: the Font Awesome bounding boxes Firefox kept correcting. 2026-08-12 06:55:34 +03:00
Lauri Ojansivu f368c1a2ac Font Awesome: state the bounding box each glyph actually has.
Firefox logged one warning per glyph, on every page load, for every Font Awesome
file WeKan serves:

  downloadable font: glyf: Glyph bbox was incorrect; adjusting (glyph 19)
  (font-family: "Font Awesome 6 Free" style:normal weight:400 stretch:100
  src index:0) source: .../webfonts/fa-regular-400.woff2

819 of the 2163 glyphs in the four files were affected - 540 in fa-solid-900,
204 in fa-brands-400, 59 in fa-regular-400, 16 in fa-v4compatibility - so the
console filled with hundreds of lines and stopped being useful for spotting
anything else.

Every TrueType glyph stores its own bounding box in the `glyf` table. Font
Awesome ships boxes that are TIGHTER than the outline: they bound the on-curve
points only, while the box has to bound the control points too, because a
quadratic curve can bulge past its endpoints. Firefox's OpenType sanitiser
notices, corrects each box in memory and says so. Nothing rendered wrongly - the
warning was the whole of the damage - but the numbers in the file were wrong,
and they are wrong upstream, in Font Awesome's own build.

releases/fix-font-bboxes.py recomputes them: every glyph's box from its points,
the font-wide box in `head` from those, and the .ttf and .woff2 written from one
corrected font so the pair cannot drift. That last part also settles a smaller
oddity - fa-regular-400.woff2 does not round-trip through fontTools' woff2
reader at all, so it is rebuilt from the .ttf rather than read.

Only `head` and `hhea` differ from the files Font Awesome shipped, and both
follow from the boxes: `head` carries the font-wide box, and `hhea`'s
minRightSideBearing and xMaxExtent are derived from the glyph boxes. Every other
table - glyf outlines, cmap, hmtx, name, OS/2, post, GSUB, GPOS, loca, maxp -
compiles identically. The script verifies that itself, reading back what it
wrote and refusing to leave a font behind whose glyph order, character map,
advance widths, contour counts or point coordinates moved. The files also got
smaller (fa-solid-900.woff2 150024 -> 118388 bytes), which is fontTools'
compression rather than anything being dropped.

recalcTimestamp=False keeps Font Awesome's own head.modified, so two runs over
the same input agree byte for byte - verified - rather than churning a committed
binary with "now" on every run.

tests/fontGlyphBounds.test.cjs parses the .ttf files with its own reader, not
fontTools, so the check does not depend on the tool that wrote them. Verified it
FAILS on the original fa-regular-400.ttf with "59 of 212 glyphs state a wrong
bounding box" and passes on the corrected one.

The guard matters more than the fix here: these fonts are VENDORED, so the next
Font Awesome upgrade drops fresh upstream files straight back into the tree and
the warnings would return silently, months from anything that would explain
them. The failure message names the script to re-run.

Thanks to xet7 !
2026-08-12 06:55:33 +03:00
Lauri Ojansivu 4c02cf5585 CHANGELOG: v10.84 does rebuild its bundles, so it cannot say it does not. 2026-08-12 06:55:33 +03:00
github-actions[bot] 9a9e81f15a Bump versions for v10.84 2026-08-12 01:55:33 +00:00
Lauri Ojansivu aa76ae81b1 Prepare v10.84 release 2026-08-12 04:53:36 +03:00
Lauri Ojansivu 8d0de427ef CHANGELOG: v10.83's binaries table is v10.83's build, not v10.82's.
From the v10.83 build logs. The release itself is fine - every bundle built, the
GitHub release notes carry a correct generated table, and the only job failures
were three snap-launchpad uploads rejected by the Snap Store's own
upload-uniqueness check. What was wrong is the table in the CHANGELOG.

An Upcoming section that rebuilds nothing may say so and reuse the previous
release's table rather than invent one, and this one did: "The binaries below are
v10.82's: nothing here rebuilds them." Releasing invalidates both halves of that.
release-all.sh renamed the heading, every bundle WAS rebuilt, and the sentence
and the table came along unchanged - so v10.83 shipped claiming:

  FerretDB v1.48.0, when v1.49.0 was built and shipped;
  a loong64 row, when loong64 was SKIPPED this release - debian:trixie publishes
    no linux/loong64 image, so there is no loong64 userland to build in, and the
    job says exactly that and uploads no provenance;
  no armhf, armv6, armv7, i386 or win-arm64 rows, all of which were built.

The generated table at the top of the GitHub release notes was right all along -
releases/provenance-table.sh builds it from the provenance.tsv each build job
records, and it has 13 bundles and no loong64 - so the release notes carried two
tables that disagreed. The table here is now that same data: FerretDB v1.49.0
throughout, the five missing platforms added, loong64 gone, and s390x's FerretDB
marked *no checksum published*, which is what the build found rather than a
failed check. Full SHA256s recovered from the per-job build logs, which print
them in full where the release-notes table abbreviates.

This matters because the table is the answer to "which Node.js is in the arm64
bundle of 10.69, and was it checked" after the build log has expired. A table
carried forward unchanged answers that wrongly, which is worse than not
answering.

The Upcoming section keeps the same shape - nothing there rebuilds anything - but
its table is now v10.83's, so whatever is cut next starts from the current build
rather than one two releases back.

tests/changelogFormat.test.cjs gains a guard on the NEWEST release: it may not
contain "nothing here rebuilds them", because a release rebuilds every bundle.
Scoped to the newest deliberately. v10.73 through v10.81 carry the same sentence,
and whether each of those really rebuilt its bundles cannot be established now
that their logs have expired - rewriting eight historical tables on a guess would
put invented provenance where merely doubtful provenance is, so they are left as
they shipped.

Thanks to xet7 !
2026-08-12 04:36:00 +03:00
Lauri Ojansivu f3f6032160 CHANGELOG: the three new strings, in 133 languages. 2026-08-12 04:26:03 +03:00
Lauri Ojansivu 639214574c Translations: the three new Version-pane and checklist strings, in 133 languages.
"invalid-year":       "Invalid year. Please type all four digits, for example 2026."
  "collapse-checklist": "Collapse checklist"
  "expand-checklist":   "Expand checklist"

The keys shipped in en.i18n.json with the card-date fix and the collapsible
checklists; every other language file carried them as English placeholders.
Translated directly, as CLAUDE.md requires - no external translation service,
API or key - from each language's OWN existing strings, so the wording matches
what that file already says rather than being invented beside it.

Three anchors did most of the work. "checklist" and "collapse"/"uncollapse" give
each language its established terms, and "invalid-domain" is the same shape of
sentence as the new one - a rejection, then an instruction with an example - so
its phrasing, punctuation and politeness register carried over directly.

Where an anchor was itself wrong the correct term was used instead of copying the
mistake forward. Several files have terms that drifted in from another language:
Italian "Non collassare" in the Greek and Romanian files, Russian in the Georgian
and Mongolian ones, Vietnamese in the Thai one, Serbian in the Slovenian and
Bulgarian ones, German in the Klingon one. Others use a literal sense of
"collapse" that is not the UI one - Azerbaijani "Yıxılma", Estonian
"Kokkupõrge", Khmer "ដួលរលំ" and Chinese "崩溃" are structural collapse, a
building falling down. The new strings use the folding sense each language
actually uses for this control.

9 languages are deliberately left as English placeholders rather than guessed at:
tlh (Klingon), vo (Volapük), zgh (Tamazight), wa (Walloon), wo (Wolof), uz-AR
(Uzbek in Arabic script), and ve / ve-CC / ve-PP, whose files disagree with their
own locale tags - ve-CC reads as Venetian and ve-PP as Veps, so which language to
write is a question about the file, not about the string. A placeholder says
"nobody has translated this yet", which is true; a fabrication would say
something false in a shipped product. They are the documented
"untranslated everywhere" case and remain open to a translator.

Applied with fill-translations.mjs --apply, which writes ONLY into placeholders:
every language reported "filled 3, skipped 0 existing human translation(s)", so
no human translation was touched. Key order and the 2-space indent are preserved,
all 154 files still parse, and verify-human-preference.mjs passes 10/10.

Also carries two Traditional Chinese improvements that were already staged in the
tree from a Transifex pull - 系統運行時間 to 系統運作時間 in OS_Uptime, and the
same wording in database-migration-description. They are human translations and
the policy is that those always win.

Thanks to xet7 !
2026-08-12 04:24:23 +03:00
Lauri Ojansivu 7b84c74359 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-08-12 04:07:45 +03:00
Lauri Ojansivu 807fc5ea89 CHANGELOG: the staleness guard that called the live database stale. 2026-08-12 04:07:45 +03:00
Lauri Ojansivu 1993d7d3c8 Snap: a started mongod is not a used mongod, so stop calling the live copy stale.
The staleness guard added for #6583 had the opposite failure of the bug it fixed,
and this reporter ran into it:

  "A couple of weeks ago, I did a snap revert ... but then completed the
   migration successfully. Today, my database suddenly reverted to an old version
   from what looks like weeks ago. Upgrading to 10.83 did not fix the problem
   automatically."

Their FerretDB was the live database and had been for two weeks. MongoDB was the
frozen one. The guard decided otherwise because it compared exactly two things:
the newest mtime under the MongoDB data directory, and the migration marker.
STARTING mongod rewrites those files - recovery, and the checkpoint it writes on
startup - so one service start during a refresh put MongoDB's newest mtime at
today against a marker from two weeks ago. The guard called the live copy stale
and wekan-control set database=mongodb, which served data frozen at the migration
date: "reverted to an old version from weeks ago", exactly.

Upgrading could not fix it because the upgrade was the cause. wekan-control runs
this on every start, so every start re-applied it.

An mtime cannot tell "somebody used this database" from "this database was
started". Asking it of ONE copy therefore cannot answer the question. Asking it
of BOTH can, because the case the guard exists for has a shape the mistaken one
does not:

  migrated copy untouched since the migration, MongoDB moved on
    -> STALE (exit 0). Nothing has used FerretDB; MongoDB holds everything since.
  migrated copy moved on, MongoDB did not
    -> CURRENT (exit 1). The normal state after a successful switch.
  both moved on
    -> AMBIGUOUS (exit 2). Two databases have been written to since they were
       copies of each other. A timestamp says when a file was touched, not how
       much is in it, so there is no answer here - only a choice, and it is the
       admin's.

Only exit 0 may be acted on automatically. Both callers test with `if`, so that
falls out of the existing code, and the test pins it - most of all for the branch
in mongodb-control that DELETES files/db to migrate again, where ambiguity
reaching the rm -rf would destroy the very copy in doubt.

The ambiguous case switches nothing and says so, in mongodb-control and in
wekan-control, printing both databases' last-written times and the two commands
to look at each. Nothing is deleted by choosing either, and both live in
$SNAP_COMMON.

The message in wekan-control deliberately has NO database=ferretdb condition on
it. An instance the old guard already moved to database=mongodb is sitting on the
wrong copy right now and its setting persists, so a fix that only speaks up when
ferretdb is selected would leave it there silently. Whichever side is selected,
the admin hears that the other one holds writes of its own.

tests/ferretdbMigrationStale.test.cjs gains the reported regression - a
two-week-old migration, a FerretDB written to a minute ago, a mongod started an
hour ago - and pins that ambiguity can never reach the deletion. Its fixture now
stamps the SQLite at the migration time, which is what the migration itself
leaves behind; a later stamp is what "something has been using FerretDB" means.

Verified by running the detector against synthesised $SNAP_COMMON directories for
all four shapes. The snap itself cannot be built or refreshed in this sandbox.

Thanks to lukechao, markusst1982 and xet7 !

Fixes #6583,
2026-08-12 04:07:45 +03:00
github-actions[bot] ae46562308 Bump versions for v10.83 2026-08-11 16:23:54 +00:00
Lauri Ojansivu f48b2c03e2 Prepare v10.83 release 2026-08-11 19:22:45 +03:00
Lauri Ojansivu 3f9b81bcbc CHANGELOG: the public-board half of the avatar fix. 2026-08-11 19:21:54 +03:00
Lauri Ojansivu 484eae7362 Avatars: honour the ?boardId= the client has been sending all along.
Follow-up to the previous commit, found while checking why the Admin Panel
showed a picture that a board did not. The two URLs differ in one thing: the
avatarUrl helper in client/components/users/userAvatar.js appends
`?boardId=<id>`, and says why in its own comment - "so public viewers can access
avatars on public boards". The Admin Panel uses profile.avatarUrl raw.

/cdn/storage/avatars/:fileName never read that parameter. It required a signed-in
user and nothing else, so on a public board every visitor who was not logged in
got 401 and the missing-picture icon, which is the case the parameter was added
for. Fixing Meteor.userId() alone would have left that half broken.

avatarIsOnAPublicBoard() answers it: the named board must exist, be public, AND
have the avatar's owner as a member. The last part is not ceremony - without it,
naming any public board would unlock any avatar on the instance, and a public
board publishes its own members, not everybody.

The legacy redirect now keeps the query string. /cfs/files/avatars/<id> 301s to
/cdn/storage/avatars/<id>, and that is the path EVERY migrated 6.x avatar URL
takes, so dropping ?boardId= there would 401 exactly the installs the previous
commit set out to fix.

tests/requestUserAuth.test.cjs gains both: that the public path checks all three
conditions and is awaited (an unawaited Promise is truthy, which would make every
avatar public), and that the redirect carries the query and only when there is
one.

Thanks to markusst1982 and xet7 !

Fixes #6583,
2026-08-11 19:21:19 +03:00
Lauri Ojansivu 3f80161607 CHANGELOG: the avatar route that could never serve anybody. 2026-08-11 19:21:19 +03:00
Lauri Ojansivu bf047d53c8 Avatars: ask the request who it is, because Meteor.userId() cannot.
Following the same upgrade as #6583, profile pictures came back as broken
images:

  "I can see Profile Pics from the user in the Admin/User Pannel if i open a
   User. But on the boards i see only the 'missing Picture' Icon"
  "initials are OK, but there is broken avatar image for those that have image"

Nothing was lost, and the migration is not at fault. The avatar files migrate,
and the Meteor-Files record made from a CollectionFS filerecord even reuses its
_id (cfsRecordToMeteorFile), so a 6.x URL still names the right object. What
broke is the request for it.

A 6.x install stores profile.avatarUrl as /cfs/files/avatars/<id>. That route
serves the legacy CollectionFS bytes if they are still there, and otherwise
redirects to /cdn/storage/avatars/<id> - which asked who was asking like this:

  const userId = Meteor.userId();
  if (!userId) { res.writeHead(401); ... }

Meteor.userId() reads the current DDP invocation's environment. That exists
inside a method or a publication and NOT in a WebApp handler, where it does not
return "nobody" - it THROWS "Meteor.userId can only be invoked in method calls
or publications". The handler wraps its body in a try/catch that answers 500, so
the throw was swallowed into a broken image and no avatar served through that
route ever reached anybody, on any install. The upgrade did not cause it; it
moved every avatar URL onto the route where it already applied.

server/routes/legacyAttachments.js had the identical call, so legacy attachment
URLs failed the same way and for the same reason.

Initials were unaffected because they are rendered client-side from the username
and fetch nothing, which is why the report describes them as fine.

The other half of the report - the Admin Panel showing a picture that a board did
not - is NOT explained by this. Both templates end at the same route:
peopleBody.jade puts profile.avatarUrl into an img src, and a board goes through
+userAvatar, which appends ?boardId=. That parameter is the difference, and it
has a cause of its own; the commit that follows this one deals with it.

An HTTP request carries its identity in the request: a bearer token, an
X-Auth-Token header, an ?authToken= parameter, or the login cookie - and on
Sandstorm, a platform-injected user id and no Meteor token at all.
server/routes/universalFileServer.js has always resolved it that way and serves
attachments correctly today. server/lib/requestUser.js lifts that resolution out
so the two routes that were guessing share it rather than grow a third copy, and
so the next file route has something to call. It never throws: a caller deciding
whether to serve a file wants an answer, not an exception its own catch will turn
back into a 500.

universalFileServer keeps its own inline copy for now. Rewiring a working file
server is a separate change from fixing a broken one, and this commit should be
readable as the fix.

tests/requestUserAuth.test.cjs pins that neither route calls Meteor.userId(),
that both await the resolver (an unawaited Promise is truthy and would authorise
everybody), that all four token carriers and the Sandstorm path are handled, and
that the migration still reuses the id the old URL names.

Verified by reading and by the source guard; confirming the served image needs an
upgraded instance, which this sandbox has no Docker to run.

Thanks to markusst1982 and xet7 !

Fixes #6583,
2026-08-11 19:21:19 +03:00
Lauri Ojansivu c623718db1 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-08-11 19:02:58 +03:00
Lauri Ojansivu 4b89394087 CHANGELOG: PassBleed is the Upcoming release, not part of v10.82.
v10.82 was prepared and released while this was being written, so the PassBleed
entry had landed in a section for a release that had already gone out without the
fix - the same way the chart-index entry did after v10.81.

It moves to a new Upcoming section with its own summary and binaries table, and
v10.82 goes back to naming WhereBleed as its single CRITICAL issue, which is what
it actually shipped.

Thanks to TWPaMWang and xet7 !
2026-08-11 19:02:58 +03:00
Lauri Ojansivu 5269d0ba5c PassBleed: the Excel card export authorised one board and read a card from another.
GHSA-6p5m-f9p2-wqm5, CWE-639, CVSS 6.5.

    GET /api/boards/:boardId/lists/:listId/cards/:cardId/exportExcel

checked whether the caller could see the board in :boardId, then resolved the card
by :cardId alone. Nothing confirmed the card was on that board, so the two
identifiers came apart: one decided the authorisation, the other decided the data.
canExport() is given this._boardId and never sees this._cardId, so it can answer
"may this user see that board?" and cannot answer "may this user see that card?".

The pass is self-service. POST /api/boards takes `permission` straight from the
request body, so any authenticated user could mint their own PUBLIC board, name it
as :boardId, and pass the id of a card in somebody else's private board as
:cardId. :listId was never used in a query at all and could be any string.

What came back was the card: title, full description, members and assignees, every
comment with its author, checklists and checklist items, subtask titles,
attachment metadata - and, because image attachments are read through
getReadStream() and embedded with workbook.addImage, the attachment BYTES, which
is functionally an arbitrary private attachment download. One board could be
reused while :cardId was substituted, making it a scriptable bulk-collection
primitive rather than a single disclosure, and the REST API is on by default in
the shipped Docker configuration.

THE FIX WAS ALREADY IN THE CODEBASE, ONE FILE AWAY. The structurally identical
PDF route has always resolved getCard({ _id, boardId, listId }) and 404s a
cross-board id (ExporterCardPDF._getCardData). That control is what shows the
Excel exporter's omission was a defect rather than a decision, and it is exactly
what the Excel exporter does now.

Constraining the QUERY matters more than adding a check after it. The exporter
fans out on the same card id - checklists, checklist items, subtasks, comments,
attachments - and none of those carry a board constraint of their own. A card that
cannot resolve outside the authorised board makes every one of them safe by
construction rather than by a second guard that could be missed the same way.

The route binds the two identifiers as well, before either branch builds. That is
deliberate duplication: it is cheapest to state where both identifiers arrive
together, and it covers the public-board branch, which skips authentication
entirely and would otherwise rely on the exporter alone. A card that is not on the
named board is a 404 rather than a 403, so the difference does not tell an
unauthorised caller whether a given card id exists.

Audited the reporter's systemic point: the only two single-card export routes are
this one and the PDF one, and the attachment handlers already check
card.boardId !== boardId. tests/exportExcelCardContainment.test.cjs pins the
constrained lookup in both places, that the route checks before EITHER branch
builds, the 404, and that the PDF control it copies is still there.

Thanks to TWPaMWang and xet7 !
2026-08-11 19:02:58 +03:00
github-actions[bot] 3b6b601085 Bump versions for v10.82 2026-08-11 14:44:14 +00:00
Lauri Ojansivu fcc3951f40 Prepare v10.82 release 2026-08-11 17:43:11 +03:00
Lauri Ojansivu 418ea838bc CHANGELOG: the two developer-facing fixes, and the summary they were missing from.
Every commit since v10.81 now has an entry. The two that did not were both about
a build or a test run rather than about WeKan, so they go in a developer-facing
subsection at the end, which is where the format puts them:

  the database-conformance stage opening a debug port nothing in it uses, so an
  unrelated FerretDB on 8088 made every backend report a database problem that
  was not one;

  two more mongo42 give-up paths deleting the directory their own stage filter
  names, which on amd64 and arm64 would end the snap build the same way it ended
  all four Launchpad ones.

The subsections now read as one sentence in the order the format asks for -
CRITICAL security, then the new feature, then the bugs, then these - and the
summary paragraph accounts for them instead of stopping at the bug fixes.

Thanks to xet7 !
2026-08-11 17:42:43 +03:00
Lauri Ojansivu 09a525ae83 Conformance: do not open the debug port nothing in the run uses.
From the 2026-08-11_16-55-04 run, every backend failed before a single query was
compared:

    Failed to create debug handler ... listen tcp 127.0.0.1:8088:
      bind: address already in use
    ERROR sqlite      FerretDB did not start on this backend
    ERROR postgresql  FerretDB did not start on this backend

FerretDB opens a debug handler for metrics and profiling at 127.0.0.1:8088 by
default and EXITS when that address is taken. An unrelated FerretDB was running
on the machine, so the conformance stage reported a database problem that was
nothing of the sort - and would have reported it for any developer with anything
on 8088.

The script already takes exactly this seriously for the two ports it knows about:
it picks a free wire port, makes both overridable, and says in its own comment
that they are "chosen so this can run WHILE something else is running". The debug
port was simply never passed, so it always took the default. Nothing in the run
queries it, so the fix is not to find it a free port but not to open it:
`--debug-addr=-`, which is how FerretDB's main.go spells that
(`if addr := cli.DebugAddr; addr != "" && addr != "-"`). All four handler
launches pass it.

Verified against the built binary with 8088 deliberately held: without the flag
FerretDB does not come up and logs that error, with it it starts and logs
nothing.

tests/conformanceDebugPort.test.cjs pins it for all four backends, that the wire
port is still chosen to be free (the care that was already there), and that the
reason stays written down beside the other ports - the next person choosing one
needs to know there is a third.

Thanks to xet7 !
2026-08-11 17:35:48 +03:00
Lauri Ojansivu 68dbbd8b47 CHANGELOG: collapsible checklists and card sections.
A new-feature subsection between the CRITICAL security one and the bug fixes,
which is where the format puts features, and a line in the summary paragraph.

Thanks to czinkos, MikeRatcliffe, JannetGen and xet7 !
2026-08-11 16:43:19 +03:00
Lauri Ojansivu 1cef5ede9a Collapsible checklists and card sections, on the opened card and the minicard.
#1591, open since 2018: "It would be great to have collapsable checklists on
cards" - "Long checklists can make a card pretty cluttered, so being able to
collapse them and expand only when needed would keep the board much cleaner."

WeKan had something adjacent and it is NOT this. A checklist carries
`hideAllChecklistItems`, reachable through a toggle switch inside the checklist
actions popup - but that is a field ON THE CHECKLIST, so flipping it changes what
everyone on the board sees, and it is an edit to the card rather than a view
preference. Wrong thing, and buried where nobody folding a list would look. It is
untouched here; it has its own uses (showChecklist, isFinished).

Collapsing is per-user, which WeKan already says twice in its own models:
lists.js "collapsed state is per-user only, stored in user profile.collapsedLists"
and swimlanes.js the same for swimlanes. So this follows them:
profile.collapsedCardSections[cardId][sectionKey], beside those two.

ONE map for every foldable thing on a card, because they are the same question. A
feature group uses its own section name ('labels', 'members', 'due', ...); an
individual checklist uses 'checklist-<id>', which is why folding a checklist on
the opened card folds it on the minicard too - the same key, both places.

The control is a caret on the title, not a third entry in a menu: the whole point
is to fold at a glance while reading the card. It points down when open and right
when folded, carries aria-expanded, and answers Enter and Space, because it is an
<a> with no href and would otherwise be unreachable without a mouse. The click
stops propagation - on the opened card the title opens the inline rename form,
and a minicard is a link to the card.

For the sixteen feature groups on the opened card this is done ONCE, with a
delegated handler and CSS, rather than by editing sixteen templates - and it had
to be, because they are not uniform inside: all of them open with an
`h3.card-details-item-title` but only three wrap what follows in a
`.card-details-item-content`. So the title is the handle and folding hides every
sibling after it, which works whatever a section puts there. The caret is drawn
by CSS on the title for the same reason.

Two things this is careful about. A checklist's progress bar stays visible when
folded - it is the summary of what was folded away, and hiding it too would leave
a title that says nothing. And the fold survives reopening the card: the click
toggles the class and stores it, but a fresh render only has the stored state, so
cardDetails re-applies it in an autorun.

`float: inline-start` and logical margins on the caret, so it sits on the reading
-start side under dir=rtl - tests/rtl.test.js enforces that across component CSS
and caught a physical `float: left` here.

tests/checklistCollapse.test.cjs pins the per-user placement against its two
siblings, that the setter writes the USER and never the checklist, that "never
set" stays distinguishable from "expanded", the caret's state and keyboard
reachability, that the minicard uses the same key, and that a folded section hides
the siblings rather than a content element that most sections do not have.

Thanks to czinkos, MikeRatcliffe, JannetGen and xet7 !
2026-08-11 16:41:48 +03:00
Lauri Ojansivu 71992ef546 CHANGELOG: the queue - the snap markers, notifications, card click, dates and template containers.
Five entries and a rewritten summary paragraph, which now leads with WhereBleed
and names the snap upgrade that never retried.

Thanks to Philippe-Bentegeac, JDeepix, imlit, Nissulya, csonkaoszimt, Heart1010
and xet7 !
2026-08-11 16:31:33 +03:00
Lauri Ojansivu 80f5518724 Remove the Templates containers made for accounts that never used them.
From the cpu-monster email feedback: FerretDB at 190-350% CPU on an instance with
14490 boards, of which 13404 are template containers, for 9264 accounts of which
478 have ever logged in.

Before v10.00 every new account got a "Templates" container board at signup
whether or not the person ever saved a template. That was made lazy in v10.00
(#2339, #5850) - `ensureTemplatesBoard` creates one only when a template is
actually saved - so no NEW account creates one. But nothing ever removed the ones
already made, and they are not visible enough for anybody to delete by hand. On
that instance they are 13x the boards collection, and every query that touches
boards carries it; under polling reactivity that is every poll.

THIS DELETES BOARDS, so the rule for what may go is the whole of the risk and is
deliberately narrow: only a container nobody ever used. Anything at all having
been done to one keeps it - a template saved into it, a list, swimlane or card
added, a second member, a rename, a star, a manual archive. The rule is in
models/lib/unusedTemplateContainers.js on its own, so it can be read and tested
without the method around it, and every kept board comes back with the reason it
was kept.

A rename is judged only against the titles the app itself used, supplied by the
caller: that string is translated and has changed over time, so with no list to
compare against the title is not judged at all rather than a board being deleted
because its default name is in another language.

And it does nothing unless asked twice. The default is a dry run reporting what
WOULD go, with a sample rather than thirteen thousand rows; `apply: true` is what
deletes. For a collection this size that is the difference between a cleanup and
an accident. Admin only, checked before it reads anything. The
`profile.templatesBoardId` pointer is cleared with the board, or the next
ensureTemplatesBoard trips over a board that is not there, and one failed
deletion does not abandon the rest - the ones that failed are reported.

tests/unusedTemplateContainers.test.cjs is mostly about what must NOT be removed:
each of the four things that can be inside one, each of the four ways a person can
have claimed one, every non-container board type, junk input, and that the dry run
returns before anything is deleted.

Thanks to xet7 !
2026-08-11 16:28:38 +03:00
Lauri Ojansivu 097ad9160a Snap: a new revision is a new chance, so the 4.2 reader actually gets to run.
#6471, reopened against 10.81: "on 10.81 I still have the exact same issue,
MongoDB cannot start. The web interface is still unreachable, and I do not see
the messages you added in the last commits."

The messages were not printed because the code that prints them never ran. The
4.2 reader added for this issue was in their snap and was never reached, because
two markers in $SNAP_COMMON are PERMANENT:

  .mongodb-data-too-old   written when no reader in the snap could open the data.
                          mongodb-control then refuses to start mongod at all,
                          and wekan-control serves an explanatory page instead of
                          WeKan.
  .mongod-start-failures  a counter; past three, mongodb-control stops re-running
                          the MongoDB -> FerretDB migration, so a
                          migration<->mongod restart loop cannot form.

Both are right, and both record a conclusion about what THAT SNAP could do. An
instance that had already failed on 10.79 or 10.80 - before mongod 4.2 was
bundled - carried a marker saying "no reader can open this" and a counter far
past three. So on 10.81 mongod was never started, the migration was never
attempted, and nothing new was printed. Upgrading to the version with the fix
changed nothing, which is exactly what their log shows: the database-selection
line, then "Waiting for MongoDB replica set primary..." repeating forever.

A conclusion about an older revision is not an answer about this one. Each marker
now records the revision that wrote it (snap-src/bin/stale-marker), and a marker
from a different revision is ignored and cleared, so the snap tries again with
whatever it can do now. A marker with no revision recorded at all is stale by
definition - that is precisely what the affected instances are carrying.

Within one revision nothing changes: same revision, same refusal, same three-try
loop protection. Not knowing the revision (running outside a snap) is never
treated as evidence that it changed, so nothing is cleared on a guess.

Three places consult it. mongodb-control clears both the marker AND the counter
before deciding - an instance that gave up on an older revision has both, and
leaving one latched fixes nothing. wekan-control makes the same judgement because
it can start first and would otherwise keep serving "your data is too old for this
snap" on a snap that has not tried. migration-control stamps the marker when it
writes it. A mongod that does start still clears the counter and its stamp
together.

tests/snapStaleMarker.test.cjs runs the helper for the reported case, the same
revision, the no-stamp case, and the two ways of not knowing, then pins all three
call sites.

Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7 !
2026-08-11 16:24:52 +03:00
Lauri Ojansivu 6420819cfa Clicking an open card closes it, and a focused checkbox is not a diamond.
The two newest comments on #6465.

CLOSING BY CLICKING AGAIN was not a missing feature - it was an unreachable one.
clickOnMiniCard already ended with

    } else if (Session.equals('currentCard', card._id)) {
      Utils.goBoardId(Session.get('currentBoard'));

but the TITLE branch above it returns before that is ever reached, and a
minicard's title covers most of the minicard. So the second click almost always
landed in the title branch, which unconditionally re-opened the card that was
already open. The toggle worked only if you managed to click a part of the card
that was not its title, which is why two people asked for a feature that was
already written.

The title branch now asks the same question first, and closes the same way. A
click on a card that is NOT open still opens it, and still clears the phone popup
ids as it did. On a mini screen the card is a popup rather than a pane, so
"already open" is a different question - `popupCardId` and `Popup.isOpen()` - and
that path closes with `Popup.back()`. Multi-selection still short-circuits both,
so ticking cards is unaffected.

THE PROBLEMS PAGE GLITCH in the screenshot is the focus ring. The Admin Panel
draws its checkboxes as a material square that morphs into a tick, and the tick
IS a 40-degree rotation of the element. A browser draws its focus ring around the
element as it is - rotated - so a checkbox that is both checked and focused, which
is exactly what a checkbox you just clicked is, comes out as a blue DIAMOND where
the tick should be.

The ring is not removed: keyboard users need to see where they are. It moves to
the row that contains the checkbox, which is not rotated - every one of these sits
inside a label (problemsSummary.jade) - so it is a normal rectangle around the
whole row, which is easier to see than a 13px box anyway. `:focus-visible`, so a
mouse click does not leave a ring behind.

tests/cardClickToggle.test.cjs pins that the close decision comes before
re-opening (or it never happens), that opening a different card and the existing
non-title toggle both still work, that multi-selection still wins, that the tick
is still the rotation, and that the label nesting the ring relies on is really
there.

Thanks to csonkaoszimt, Heart1010 and xet7 !
2026-08-11 16:19:43 +03:00
Lauri Ojansivu 184e1713d3 Notifications: cap the tray array, which SQLite was being rewritten by.
#6533. A snap instance reports FerretDB at 737% CPU, logins taking over a minute,
boards not appearing, and logs full of:

    [db-retry] the database was busy: 0 write(s) retried, 0 succeeded on a retry,
      3 given up on.
    ValidationError: ... [collection.go:191 sqlite.(*collection).UpdateAll]
      database is locked (5) (SQLITE_BUSY)
      at _helpersConstructor.addNotification (models/users.js:2248)

The stack names the same write every time. addNotification is an `$addToSet` on
`profile.notifications`, which is an array INSIDE the user document: adding one
entry means reading the whole document, scanning the array for a duplicate, and
writing the whole document back. The cost is proportional to the array. FerretDB
on SQLite has a SINGLE writer, so those rewrites queue behind each other and
start failing outright - and login, which also writes to the user document,
queues behind them. That is the slow login, the boards that never arrive and the
pinned CPU, all from one array.

WHY IT GROWS WITHOUT LIMIT: the cleanup added in #5685 removes a notification only
when every entry for its activity has been READ and is past its removal age. A
user who does not clear their tray accumulates entries forever, and nothing else
in the product shrinks that array. The instance in the report has been running
since at least 2026-07-29 with contention warnings, which is what an array that
only ever grows looks like from the outside.

So the same cleanup pass now also caps it: the newest NOTIFICATION_TRAY_MAX_PER_USER
entries are kept (default 1000, a backstop rather than a retention policy) and the
rest dropped. Newest rather than oldest because `$addToSet` appends, so the tail
is the recent activity a tray is for; an unread notification from months ago is
not something anybody is going to act on.

Three things it is careful about. The cap is applied to what is LEFT AFTER the
expiry pull, not before, or it would trim entries that were about to be removed
anyway and keep fewer than the cap. A user needing neither change is not written
to at all - the whole problem here is writes to user documents, so the cleanup
must not add one per user per day for users with nothing to do. And it stays ONE
write per user, which is what #5685 collapsed K writes into; the capped case uses
`$set` because a `$pull` would have to list the entries it is removing, and there
being too many of them is the point.

This bounds the array. It does not make SQLite a multi-writer engine: an instance
this busy still wants the PostgreSQL backend, which is what the Problems pane
already tells the admin.

tests/notificationCap.test.cjs pins the decisions - keeping the newest, leaving an
array under the cap completely alone (so no write), refusing to act on junk or a
disabled cap, that unread entries are what this catches because the existing rule
provably cannot, and that it is still one write per user.

Thanks to Nissulya and xet7 !
2026-08-11 16:15:06 +03:00
Lauri Ojansivu 0c5cb99b7c Card dates: a typed two-digit year is refused, not stored as the year 26.
Email feedback: "If i write the expiration date with the keyboard it turns red,
if i choose it with the date picker it is yellow. Can you please tell me the
difference?"

It is a bug, and the colour was never it. `<input type="date">` reports its value
as YYYY-MM-DD, but a browser lets the YEAR sub-field be typed as two digits and
reports exactly that: entering 31-12-26 gives "0026-12-31", the year 26 AD. That
is a perfectly valid Date, so nothing refused it, and the card was saved with a
due date two thousand years in the past. Red is what an overdue date is supposed
to look like. The same day chosen from the calendar always fills four digits, so
it is 2026 and yellow, because it is only due soon.

The attached screenshot shows it rather than the colours: the yellow badges read
"31-12-2026 17:00" and the red ones "31-12-26 17:00" - two different years.

Saving now refuses a year outside 1000-9999 with "Invalid year. Please type all
four digits, for example 2026." and puts the cursor back in the date field. The
window is deliberately wide: WeKan holds real historical received dates and
long-dated deadlines, and this is only meant to catch a year that cannot have
been intended.

REFUSED, NOT CORRECTED. 0026 was almost certainly meant as 2026, but silently
rewriting a date somebody typed - one that other people's due-date reminders hang
off - is a guess in the wrong place. The message says which digits are missing,
which is the thing the reporter could not see.

tests/datePickerYear.test.cjs pins the reported value the browser really hands
over, the date that was meant, the width of the window either side, that the
refusal happens before storeDate, and that nothing rewrites the year.

Thanks to xet7 !
2026-08-11 16:07:47 +03:00
Lauri Ojansivu b6d0be0f45 Snap: the other two mongo42 give-up paths deleted their own staged directory.
The earlier fix for the stage filter covered one of three ways this part gives
up - the unsupported-architecture `exit 0`. The other two did `rm -rf "$dest"`
and exited 0, which leaves `stage: mongo42` naming a path that is not there, and
snapcraft ends the whole build on that:

    Failed to copy '/build/.../parts/mongo42/install/mongo42': no such file or directory

Those two are reached on amd64 and arm64, where the binary IS downloaded: OpenSSL
1.1 could not be obtained for the architecture, or the staged mongod 4.2 failed
the smoke test that checks it actually runs. Either would have ended the snap
build for the two architectures that matter most, in the same way it ended all
four Launchpad ones.

Both now remove the CONTENTS and keep the directory. An empty mongo42/ stages to
nothing, and migration-control guards every use on `-x "$M42/bin/mongod"` - the
binary, not the directory - so "no 4.2 reader here" is still exactly what an empty
one means. Checked: that is the only guard, so an empty directory cannot be
mistaken for a working reader.

tests/snapPartFilters.test.cjs now requires every give-up path to clear contents
only, rather than pinning the single shape the first failure happened to take.

Thanks to xet7 !
2026-08-11 15:59:29 +03:00
Lauri Ojansivu 3ebc38cb40 CHANGELOG: WhereBleed leads the release.
A CRITICAL SECURITY section comes first, so the bug subsections that follow it
become "and fixes ...". The summary paragraph leads with it too, in plain text -
the In short paragraph links nothing; the entry does.

Thanks to TungNGo02 and xet7 !
2026-08-11 15:57:11 +03:00
Lauri Ojansivu b4ebe48d7b WhereBleed: eight Admin Panel handlers ran the caller's selector unchecked.
GHSA-phm4-4v26-j2vq, CWE-943, CVSS 5.8. The Admin Panel's people, org, team and
translation publications and their companion count/page methods take a query
selector from the client and validate only its TYPE:

    check(query, Match.OneOf(Object, null));

which is not validation, because a MongoDB selector is executable data. `$where`
makes the database run the caller's JavaScript once per document scanned, so

    Meteor.subscribe('team', { $where: 'while(true){}' }, 25, 0)

pins a database worker for as long as the caller likes and is repeatable: denial
of service for every tenant on the instance, from one narrowly-scoped account.
The reporter demonstrated both halves on v10.81 against mongo:7 - `$where:
'sleep(2000) || true'` made the `people` subscription take 2.03s and return the
document, `$where: 'false'` returned nothing in 0.00s - which is the caller
deciding in JavaScript which documents come back.

It needs an authenticated admin session, so it is not reachable by a board member
or a visitor. It is still worth the fix at this severity because the people and
org surfaces are open to a PER-TENANT Global Admin, a role meant to be confined
to one Organization, and those two wrap the caller's selector as
`{ $and: [query, restriction] }` rather than stripping execution operators out of
it - so merging the tenant restriction never removed the `$where`, and a role
scoped to one tenant reached instance-wide impact.

WHAT MAKES THIS ONE PARTICULAR is that the defence was already here. classifySelector
(models/lib/injectionDetect.js) and hasWhere (models/lib/mongoSelectorSafety.js)
were written for exactly this class and were wired into the card-window
publication, with unit tests. Eight sibling handlers taking the identical shape of
client-supplied selector simply never called them. So this adds no new detection
logic: cardsWindow.js's own helper moves, unchanged, to
/server/lib/selectorGuard.js, and all nine call sites share the one copy. A second
copy of the check would be the same bug set up to happen again, which is why
cardsWindow.js now imports rather than keeps its own.

Each handler refuses with the "match nothing" selector that publication already
uses in production, `{ _id: { $in: [] } }` - frozen, so a caller cannot mutate the
shared object into one that matches - so a refused request returns an empty result
instead of throwing at an admin mid-page, and the canary still records who sent
what from where. Ordinary Admin Panel behaviour is untouched: none of the
searches, filters, regexes, $or/$and/$in/$elemMatch or date ranges those panes
send contains an execution operator, and the test pins fourteen real ones going
through unchanged.

FerretDB, WeKan's default database, rejects $where itself, so this degrades to a
rejected query there; the supported real-MongoDB path
(docker-compose-mongodb-v7.yml) is where it was reproduced. MongoDB 7 also happens
to reject $where inside the aggregation pipeline the four count methods use, but
that is an engine-level accident for one operator on one call path - $function and
$accumulator are legitimate aggregation operators - so the count methods are
guarded like the rest rather than left to it.

tests/selectorGuard.test.cjs pins the reported payloads (including nested inside
$and/$or, which is how the tenant merge would carry them), the other execution
operators, the fourteen ordinary queries, that all nine call sites route through
the guard, that none still passes the raw selector afterwards, and that exactly
one copy of the check exists. Three existing suites asserted the old shape -
canaryCoverage on where the canary trips, injectionDetect on the helper living in
cardsWindow.js, tenantWiring on the scope function receiving the raw query - and
each is updated to the new one, saying why.

Thanks to TungNGo02 and xet7 !
2026-08-11 15:50:31 +03:00
Lauri Ojansivu 09ce64ecf6 CHANGELOG: the snap database copy and the card drag scroll.
The two issues reported after upgrading from v6: a FerretDB migration that had
fallen weeks behind the MongoDB beside it and was being served in its place, and
a card drag that scrolled the board instead of the list because the vertical
auto-scroll never picked the list under the pointer.

Thanks to markusst1982 and xet7 !
2026-08-11 15:34:16 +03:00
Lauri Ojansivu 8456170d45 Dragging a card down scrolls the list, not the whole board.
"With v6 i was able to move a Card downwards by drag & Drop. Upwards is no
   problem, the Line scrolls automaticly up, but this does not work downwards.
   The whole Page/Site scrolls down and not ne Line"

The card sortable's `sort` handler auto-scrolls at the edges. HORIZONTALLY it
picks the lane under the pointer (#443). VERTICALLY it picked nothing: it always
scrolled `.board-canvas`, which is `overflow-y: auto` and holds the swimlanes,
rather than the `.list-body` under the pointer, which is `overflow-y: scroll` and
holds the cards. Scrolling the canvas moves the whole board - which is the report,
in the reporter's own words.

The asymmetry is what makes it reproducible rather than vague. Dragging UP, the
canvas is usually already at scrollTop 0, so computeEdgeScroll returned null, the
handler did nothing, and jQuery UI's own `scroll` option - which acts on the
placeholder's scrollParent, i.e. the list body - scrolled the list, which is why
"upwards is no problem". Dragging DOWN, the canvas nearly always has room left,
so the handler fired first and scrolled the board instead of the list.

The list body under the pointer is now scrolled first, found with the same
rectangle hit-test the lanes use, and the canvas only when the list cannot go
further that way - so a drag down a long list scrolls the list, and a drag past
the end of it moves on to the board, which is what dragging a card to another
swimlane needs. The horizontal lane scroll is unchanged, and the list branch sets
`scrolled` so the #6477 refreshPositions still runs after it: a manual scroll
invalidates jQuery UI's cached geometry whichever element was scrolled.

tests/cardDragVerticalScroll.test.cjs pins the order (list before canvas, canvas
only when the list did not move) and runs computeEdgeScroll with real numbers for
the three cases that decide it: a pointer in the bottom edge zone of a scrollable
list, the same at the end of its scroll range, and a list that does not overflow.

Thanks to markusst1982 and xet7 !

Fixes #6584,
2026-08-11 15:32:47 +03:00
Lauri Ojansivu b1e7b1e538 Snap: never serve a FerretDB copy that is older than the MongoDB beside it.
After upgrading from v6 the reporter saw "the state of the data from days ago"
and suspected a `snap revert` done four weeks earlier. They were right about the
cause, and nothing was lost.

The MongoDB -> FerretDB migration copies MongoDB into SQLite ONCE and writes
.migration-to-ferretdb-done. That copy is a snapshot; nothing keeps it in step.
Revert the snap to a revision that runs mongod and WeKan carries on writing to
MongoDB - for four weeks here - while the finished SQLite sits frozen at the date
it was made. Refresh forward again and mongodb-control's
switch_to_completed_ferretdb saw a marker plus a non-empty SQLite, called that a
completed migration, and switched the snap onto it. Every board and card written
during the revert was still on disk and simply not being served.

The data was never in danger: it lives in $SNAP_COMMON, which is shared across
revisions and is NOT rolled back by a revert (unlike $SNAP_DATA, which is
per-revision). What was wrong was WHICH of the two copies got served, and nothing
compared their ages.

snap-src/bin/ferretdb-migration-stale answers that one question: is the completed
migration older than the MongoDB data it was made from? mongod rewrites its
WiredTiger files whenever it commits, so the newest mtime among them is when
MongoDB was last written to; later than the marker means the copy is behind. It
counts DATA files only - a newer mongodb.log or .pid means the snap was started,
not that the database changed - and allows a margin, because the migration stops
its own temporary source mongod moments after writing the marker and that
shutdown touches the data directory. Anything it cannot tell (no marker, no
SQLite, no MongoDB data, unreadable mtimes) is reported as NOT stale: the callers
act on a yes, so being unsure has to leave the existing behaviour alone.

Three places act on it:

  mongodb-control's switch_to_completed_ferretdb refuses a stale migration and
  leaves MongoDB running, which has the newest data.

  mongodb-control's handle_mongod_start_failure - where mongod will not start at
  all, so keeping MongoDB is not on offer and the choice is between a fresh copy
  and a stale one - migrates AGAIN. The migration reads with its own temporary
  mongod and the 4.2/3.2 readers, none of which is the mongod that just failed,
  so it can still reach everything written since. It has to be restarted rather
  than resumed: migration-control exits immediately while the marker is there,
  and the checkpoint and old SQLite describe a finished copy of the old data, so
  the marker, the checkpoint and files/db are cleared first. The MongoDB data is
  never touched.

  wekan-control gains the second half of a guard it already had. It refused to
  start an EMPTY FerretDB while MongoDB had data; a FULL but out-of-date one looks
  worse, because WeKan comes up with boards and cards all present and only the
  last weeks missing, which reads as data loss rather than as a misconfiguration.
  It now stays on MongoDB and says why, including that nothing is lost.

tests/ferretdbMigrationStale.test.cjs runs the detector against synthesised
$SNAP_COMMON directories - the reported four-week case, a fresh migration, the
margin either side of it, a newer log file, and each "cannot tell" - and pins the
wiring at all three call sites.

Thanks to markusst1982 and xet7 !

Fixes #6583,
2026-08-11 15:32:32 +03:00
Lauri Ojansivu dac8d1da2f CHANGELOG: the two wekan-ldap links point into wekan/wekan.
github.com/wekan/wekan-ldap no longer exists - the API redirects and then answers
422 - so the two links in the v2.52 (2019) entry for "Added and then reverted
option to redirect OIDC OAuth2 login" were dead.

The HASHES do not carry over. The wekan-ldap code was copied into
packages/wekan-ldap on 2019-04-20 ("Include to Wekan packages directory
contents"), not history-merged, so 82a894ac and 36900cc exist in no repository
now: GitHub answers 422 for both under wekan/wekan, and neither object is in this
clone. Repointing the URL alone would have produced a link that still 404s.

What DOES exist is the pair of wekan/wekan commits that recorded those two steps
on the same day, and the first of them is certain rather than inferred - its
commit message IS the old link:

  part1  82a894ac  ->  188d42dcd  "[Add option to redirect OIDC OAuth2 login]
                                    (.../wekan-ldap/commit/82a894ac...)"
  part 2 36900cc   ->  d173844f9  "OAUTH2_LOGIN_STYLE popup or redirect,
                                    part 2 and 3."

So a reader following part1 still sees the original wekan-ldap hash, in the
message of the commit they land on. part3 is unchanged: 7919ae362 is in
wekan/wekan already and is the code change of that day (Dockerfile,
docker-compose.yml, server/authentication.js, the snap config).

Both targets checked against the GitHub API before use: HTTP 200.
releases/fix-changelog-hashes.sh --all-sections now reports all 3235 commit links
resolving.

Thanks to xet7 !
2026-08-11 15:18:26 +03:00
Lauri Ojansivu c452a2defd CHANGELOG: the canary-tokens entry links FerretDB, where that commit lives.
releases/fix-changelog-hashes.sh --all-sections checked all 3234 wekan/wekan
commit links and found exactly one that resolves to nothing:

  https://github.com/wekan/wekan/commit/a260a53e

It is not a rewritten hash, so there was nothing for the script to repoint - it
reported it and left it, which is what it should do. The hash is fine; the
REPOSITORY in the URL was wrong. The entry describes `internal/util/canary`,
which is a FerretDB path, and a260a53e is "Canary tokens: mark the operations the
client never issues." in wekan/FerretDB - confirmed both in .tools/FerretDB and
against the GitHub API, which serves it there and answers 422 for it under
wekan/wekan.

Thanks to xet7 !
2026-08-11 15:13:20 +03:00
Lauri Ojansivu c8393f3f53 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-08-11 11:36:19 +03:00
Lauri Ojansivu be558257cd CHANGELOG: the image-check work belongs to the next release, not to v10.81.
v10.81 was prepared, tagged and published (at "Bump versions for v10.81") while
this was being written, so the entry describing the chart-index image check was
added into a section for a release that had already gone out without it. A
release section has to describe what is IN that release; it is moved to a new
Upcoming section, where the change will actually ship.

What stays in v10.81 is what v10.81 really carries: the FerretDB
directConnection fix, All Boards on a phone, the retired Mac runner and the jobs
a cancellation skipped, the armv6 userland, the Launchpad snap part, and the two
chart commits that were pushed before the release was prepared.

Also: the new "is there an image for this version" check imports
releases/reindex-charts.py, and importing a module writes releases/__pycache__
beside it - untracked junk appearing in the repository on every release. It runs
with PYTHONDONTWRITEBYTECODE=1, verified to leave nothing behind.

Thanks to xet7 !
2026-08-11 11:36:18 +03:00
Lauri Ojansivu 7fc1f35bed CHANGELOG: link the image-check entry to its commit.
Thanks to xet7 !
2026-08-11 11:36:18 +03:00
Lauri Ojansivu a442959dda Charts: list only the ones whose container images still exist.
Backfilling the index taught this within the hour. Artifact Hub scans every entry
in a Helm index and mailed a list of errors:

    error scanning image ghcr.io/wekan/wekan:v9.62: image not found
    error scanning image docker.io/bitnami/mongodb:7.0.14-debian-12-r3: image not found

That was this side's doing, not Artifact Hub's. The rebuild listed every package
sitting on gh-pages, and a chart is a POINTER TO CONTAINER IMAGES: one whose
images have been deleted from their registries installs and then fails at the
pull, so listing it announces that the repository is broken when the repository
is fine and the images are gone.

135 of the 360 packages are in that state, from two unrelated causes. SIX have no
WeKan image - v8.30, v9.12, v9.14, v9.38, v9.39 and v9.62, releases whose own
docker job failed, and exactly the six ghcr.io errors that were reported. The
other 129 vendor the BITNAMI mongodb subchart and pin a tag Bitnami has since
deleted; charts from 8.41 on vendor groundhog2k's mongodb, which uses the
official `mongo` image and is unaffected.

The index now holds 225 entries: every one of the 216 that were listed before -
not one dropped - plus the 9 backfilled packages whose images all resolve. Twelve
listed charts were spot-checked against the registries afterwards and every image
pulled.

THE EXCLUSIONS ARE A FILE, unindexed.txt, beside the packages - not a check made
on every run. A rebuild during a release must not depend on reaching two
registries, where a blip would silently drop half the index; and a recorded
decision is reversible and reviewable, so an image that comes back is one deleted
line away from being listed again. The .tgz files all stay, so direct URLs keep
working. `--check-images` is what regenerates the file.

Two things this shook out, both about being careful with a negative result.

The registry handshake is now the REGISTRY'S OWN: ask for the manifest with no
credentials and let the 401 say which realm and scope to use. The first version
hard-coded a token URL per host and reported every quay.io image as missing -
including quay.io/wekan/wekan:latest, which plainly exists - because the wrong
endpoint returned a token quay would not accept and the manifest came back 404.
Four charts were nearly dropped on that. Related: a repository that already
begins with a host ("ghcr.io/wekan/wekan") must not be read as a docker.io path,
which is how a first pass came back "unknown" for every WeKan image.

And an image that cannot be CHECKED is never treated as missing - only a definite
404 excludes a chart. A registry that refuses anonymous auth or a request that
times out must not be able to silently unpublish anything.

release-charts.sh now refuses to publish a chart when
ghcr.io/wekan/wekan:v<version> does not exist, which is what created those six in
the first place. It checks before it touches the charts repository, and a
registry it cannot reach does not block a release.

Thanks to xet7 !
2026-08-11 11:36:18 +03:00
github-actions[bot] b6d003089c Bump versions for v10.81 2026-08-11 00:02:26 +00:00
Lauri Ojansivu fc1e089d38 Prepare v10.81 release 2026-08-11 03:01:27 +03:00
Lauri Ojansivu 35b44a1f8b CHANGELOG: link the chart-publishing entry to its commit.
Thanks to xet7 !
2026-08-11 02:54:55 +03:00
Lauri Ojansivu a434bb4bd5 Charts: both release workflows publish one, and the index is derived not edited.
The charts repository keeps the chart SOURCE on main and the published packages
plus index.yaml on gh-pages, and its own two scripts move between them: main's
release.sh commits the source, tars `wekan/` into wekan-<version>.0.tgz, checks
out gh-pages and drops the package there; gh-pages' release2.sh commits and
pushes it. release-charts.sh drives both and owns the index in between, and that
middle part is what changes here.

THE INDEX IS REBUILT FROM THE PACKAGES, not edited in place. It used to copy the
newest entry, substitute a few fields and prepend the result, and every one of
the index's four defects came from that: a re-run prepended a second entry for a
version instead of replacing it (9.36.0 ended with four, each a different digest,
so a helm client could pick one matching no file); appVersion was set to the
CHART version rather than the WeKan version on every entry; fields nobody
substituted never changed, so every entry claimed the mongodb subchart was 0.7.2
while the packages had moved to 0.7.6; and a release whose charts job did not run
left a hole nothing ever went back for. Deriving the index makes all four
impossible instead of repaired - one entry per package, its digest computed from
the file, its fields read out of the archive, and any package that was missed
picked up on the next run.

THE PACKAGE IS CHECKED AGAINST ITS OWN FILENAME FIRST. release.sh names the
tarball from its argument (`tar -cvzf wekan-$1.0.tgz wekan`) while the version
inside comes from the Chart.yaml this script sed-ed a moment earlier. When those
drift the repository gains a file called one version that declares another, and a
Helm index keys on the version INSIDE - so it can never be installed under the
name it was given. That is how wekan-1.2.7.tgz (containing 1.2.6) and
wekan-6.96.tgz (containing 6.9.6) came to exist; both are removed from the charts
repository, and a package like them now stops the release with a message naming
both versions rather than being published.

RELEASE ALL MISSING PUBLISHES A CHART TOO. It could rebuild any missing bundle
but not a missing chart - its header said the charts were out of scope - so a
release that never got a chart never would. It has a charts job now, and it is
the one part of that workflow which pushes to another repository, so it is
guarded twice: nothing happens without a WEKAN_REPO_TOKEN that can push to
wekan/charts (a named warning and a skip, not a failure inside git), and nothing
happens when the published index already lists that version, because
re-publishing would re-tar the package and change the digest of a chart people
have already pulled. Both workflows publish through the same
releases/release-charts.sh, so neither can drift from the other.

release-charts.sh also finds the checkout at .tools/charts, where CLAUDE.md keeps
companion repositories, before the older ../w/charts.

Verified against the real checkout: a package dropped in is picked up, sorted
first, with its appVersion and digest read from the archive - then removed and
the index restored. tests/releaseChartsIndex.test.cjs pins that the index is
derived rather than edited, that the filename check runs BEFORE indexing, that
both workflows publish through the same script, and that an already-published
version is not republished. It runs the script's own version-reading code against
a package named 1.2.7 that declares 1.2.6.

Thanks to xet7 !
2026-08-11 02:54:47 +03:00
Lauri Ojansivu 292ffb8906 CHANGELOG: the index.yaml rebuild entry.
Wrapped at 80 and repointed by releases/fix-changelog-hashes.sh after the amend
that rewrote its commit.

Thanks to xet7 !
2026-08-11 02:44:40 +03:00
Lauri Ojansivu 1a2ec06332 Charts: rebuild index.yaml from the packages the repository actually serves.
With wekan/charts cloned at .tools/charts, the index could be compared against
its own packages for the first time, and it had drifted from them in four ways at
once. All four come from the same habit: release-charts.sh writes an entry by
COPYING the previous entry and substituting a few fields, so the index was kept
as the record rather than as a description of the .tgz files beside it.

  146 PACKAGES HAD NO ENTRY. 362 packages, 220 entries. Those charts are
  downloadable by URL and invisible to `helm search`, to
  `helm install wekan --version 9.63.0` and to Artifact Hub. An entry is written
  once, during the release it belongs to, so a release whose charts job did not
  run left a hole and nothing ever went back for it.

  DUPLICATE ENTRIES WITH STALE DIGESTS. 9.36.0 had four entries and 10.30.0 two,
  each copy with a different digest and the same url, because a re-run of a
  release prepended a second entry instead of replacing the first. Only one
  digest per version can be the digest of the file served - the other four match
  no package in the repository - so a helm client that picked one of them failed
  the integrity check on a perfectly good file.

  A WRONG appVersion ON EVERY ENTRY. The index gave the CHART version there
  ("10.79.0") where the packaged Chart.yaml says the WeKan version ("10.79"),
  which is the one a reader is actually looking for.

  FROZEN DEPENDENCY VERSIONS. A field nobody substituted never changed: every
  entry claimed the mongodb subchart was 0.7.2 while the packages have been 0.7.6
  for some time.

releases/reindex-charts.py derives the index from the packages, reading each
one's Chart.yaml out of the archive - so an entry cannot describe a package
wrongly - and needs no helm, only python3.

Nothing is invented and nothing is lost. `created` is preserved exactly on every
entry whose digest matches its package; a package being added takes the date it
was committed to gh-pages, which is when it was really published (wekan-10.79.0
was committed at 2026-08-10T02:46:52Z and its entry already said created
2026-08-10T02:46:52.308983+00:00). Two 2023 packaging slips - wekan-1.2.7.tgz
containing chart 1.2.6, and wekan-6.96.tgz containing 6.9.6, each colliding with
the file whose NAME matches - are reported and left out rather than letting one
version mean two files; both files stay on the server, so a direct URL keeps
working.

Run against the real repository the index went from 220 entries to 360, and was
verified after writing: no repeated version, every digest equal to the sha256 of
its file, every file present, every url under wekan.github.io/charts, and no
version that was listed before missing.

backfill-charts.sh now looks in .tools/charts before ../w/charts, since that is
where CLAUDE.md keeps companion repositories, and both tools are Release menu
entries in build.sh and build.bat.

tests/reindexCharts.test.cjs runs the tool against a synthesised charts directory
- real gzipped chart packages, written without helm - and pins the rules that
have a decision in them: created is preserved rather than restamped, the
surviving duplicate is the one whose digest matches the file, a version claimed
by two packages is resolved by filename and REPORTED, no previously listed
version is dropped, and a report run does not touch the file.

Thanks to xet7 !
2026-08-11 02:43:19 +03:00
Lauri Ojansivu 2c7a016327 CHANGELOG: the Upcoming section for the six fixes.
Two bugs that stopped WeKan starting - the FerretDB replica-set handshake sending
the driver to 0.0.0.0, and All Boards on a phone - and four release-pipeline
failures: the retired macos-13 label that has never once built the Mac x64
bundle, the cancellation that silently skipped charts/ucs/nextcloud, the armv6
bundle assembled in a soft-float ARMv5 userland, and the optional MongoDB 4.2
part that ended every Launchpad snap. Plus the chart-index report.

The binaries table is v10.80's: nothing here rebuilds them.

Thanks to xet7 !
2026-08-11 02:19:44 +03:00
Lauri Ojansivu 18da0d2618 Snap: an optional part was ending every Launchpad build.
All four Launchpad architectures failed in v10.80 - armhf, s390x, ppc64el and
riscv64, three attempts each - and the job reported them as a Launchpad problem:
"often an OOM in the Meteor npm install, or a transient build-farm reset; re-run".
It was neither, and re-running was the one thing that could not help. The build
log, which the job prints, said the same deterministic thing every time:

    Staging mongo42
    Failed to copy '/build/.../parts/mongo42/install/mongo42':
      no such file or directory.
    Build failed

The mongo42 part downloads MongoDB 4.2 so a database from an old MongoDB snap can
still be migrated (#6471), and MongoDB publishes 4.2 for amd64 and arm64 only. On
every other architecture the part prints "nothing to migrate from there;
skipping" and exits 0 having created nothing - and its own comment calls that
"OPTIONAL BY DESIGN". It was not optional: the part also carries
`stage: [mongo42]` and `prime: [mongo42]`, and snapcraft does not skip a filter
whose path is missing, it ends the build. So a migration helper that no exotic
architecture has ever needed took the whole snap down on exactly the four
architectures that have no native runner and can only be built on Launchpad.

The directory is created before anything can decide to skip, so the filters
always have something to copy. The binary is still not downloaded anywhere but
amd64 and arm64, and migration-control still finds nothing there - "no MongoDB
4.2 to migrate from" is a missing binary again instead of a failed build.

riscv64 additionally lost its last attempt to the build farm's network:

    npm ERR! code ECONNRESET
    npm ERR! network request to .../string-width-4.2.3.tgz failed, reason: socket hang up
    'override-build' in part 'wekan' failed with code 1.

after half an hour of building. Every request there goes through Launchpad's
proxy and one tarball out of a couple of thousand can be cut mid-stream; npm's
own --fetch-retries covers the request, not a dropped socket. That npm install is
retried three times now, and still fails loudly if it is not the network.

The ::error:: no longer guesses. It sends the reader to the build log that is
already printed above it and lists what to look for in the order worth checking,
starting with a "Failed to copy .../parts/<part>/install/<path>", which is a
stage:/prime: filter naming something that architecture does not build and will
never pass on a re-run.

tests/snapPartFilters.test.cjs pins the ordering inside mongo42, that it is still
amd64/arm64 only, the npm retry, and the general rule the failure was one
instance of: a literal path in stage:/prime: must be something its part actually
produces.

Thanks to xet7 !
2026-08-11 02:16:30 +03:00
Lauri Ojansivu c0a5c2b7c9 All Boards on a phone: the scroll, the table view, the 4x tiles and the spacing.
Four things, reported together, in a issue that had been closed three times and
reopened against 10.10, 10.37, 10.38, 10.72 and 10.77.

THE SCROLL. Every fix so far removed viewport arithmetic from boardsList.css and
said so in its comments - "no viewport arithmetic here at all now". The last
piece of it was in layouts.css, where nobody looked:

    body.mobile-mode #content { height: calc(100dvh - 48px) }

48px is a guess at the height of the header. The header is not 48px and is not
any one number: Utils publishes --wekan-header-height from a ResizeObserver for
exactly this reason, and its own comment says the quick-access bar wraps to a
second and third row depending on language and window width and that "every fixed
number for it has been wrong for some of those". So on a phone whose bar wraps,
#content was TALLER than the room under the header, and its bottom sat below the
screen. body.mobile-mode is `position: fixed` and the phone rules make it
`overflow: hidden`, so that strip is unreachable, and the `height: 100%` chain
below inherited all of it: the board list scrolled, it just ended off screen, and
its last boards were where no gesture could reach them.

Nothing has to be computed. body is a flex column of exactly one viewport and
#content is its `flex: 1` item, so the space under the header IS that box, at
whatever height the header really is this paint. The calc was overriding a
correct answer with a wrong one. It is gone, and `min-height: 0` lets the chain
below actually shrink.

THE TABLE VIEW. The right column has two branches - the board icons and
`+tablePage` - and only the icons were ever given a scroller. In Table view the
table grew past the bottom of the column and `#content .wrapper` is
`overflow: hidden`, so everything past the fold was clipped with nothing to
scroll. Every section of All Boards can be shown as a table, so that was half the
page for all of them. `.table-page` now takes what the column has left and
scrolls in both directions, and the pane heading is `flex: 0 0 auto` so it keeps
its height instead of being squeezed while the list stays too tall.

THE 4x-TALL TILES. A grid defaults to `align-content: stretch` and
`align-items: stretch`, so a list SHORTER than its column has the leftover height
divided among its rows and each tile grown into it - and a board tile paints its
board colour over the whole cell, so one row of boards became one band several
times a normal icon's height. This was already fixed once, but only on
`.board-list.mobile-view`, the class the template sets from Utils.isMiniScreen().
The phone MEDIA QUERY builds a grid too, and a narrow window that is not a mini
screen - a desktop browser under 800px, a tablet in desktop mode - took that path
and never got it. It belongs on the rule that creates the grid, and is on both.

THE SPACING. The tiles had an 8px grid gap PLUS a `margin-bottom: 0.5rem` each,
so they were 16px apart vertically and 8px horizontally - which is what reads as
"too close to each other" sideways. One value now, 10px, in both directions, with
the margin removed on both the media-query and the mini-screen paths.

The mobile full-screen popup had the same class of bug as the first: `height:
100vh` on a `position: fixed` box (the viewport with the toolbars HIDDEN, so it
reaches under them) and a `.content-wrapper` of `calc(100vh - 48px)`, guessing at
its own title bar. It states dvh now, and the wrapper is left to the flex column
it is already part of.

Desktop is unaffected: above 800px none of these rules apply and the page scrolls
in #content as it always has.

tests/allBoardsPhoneScrollAndTiles.test.cjs pins all four, end to end - including
that no step between the body and the board list computes a height from the
viewport. tests/mobileViewportHeight.test.cjs REQUIRED the calc that was the bug,
so that assertion is replaced with its opposite and says why.

Thanks to mimZD and xet7 !

Fixes #6488,
2026-08-11 02:11:06 +03:00
Lauri Ojansivu 01f691ba3d Build menus: the chart-index report is a command a person runs.
build.sh and build.bat list every releases/*.sh that is not a workflow-only
helper, and backfill-charts.sh is not one of those - it is read by hand, on a
machine with a charts checkout. It is a Release menu entry in both, and
`./build.sh backfill-charts` / `build.bat backfill-charts` on the command line.

Thanks to xet7 !
2026-08-11 02:01:55 +03:00
Lauri Ojansivu 8248892e33 Charts: report the releases the index is missing, and repair it.
The charts job is fixed to RUN (previous commit); this is about what it left
behind. A chart entry is written once, by release-charts.sh, during the release
it belongs to - so a release whose charts job did not run leaves a hole that
nothing ever fills, because the next release adds ITS entry and never looks back.
v10.80 is one of those. Counted against the live index: 216 of the 690 WeKan
releases have a chart entry, and 474 do not.

releases/backfill-charts.sh answers "which WeKan releases should the index
list?" with: the ones that exist AND can be installed.

  keep   216  a chart is already published; never rebuilt, because repackaging
              changes a digest helm clients have already seen
  build  369  the release exists, its container image exists, and no chart is
              published - the hole to fill
  omit   162  the release exists but has NO container image on ghcr. A chart is
              a pointer to an image (tag: v<version>), so an entry for one of
              these is an install that fails at the pull. They stay OUT of the
              index rather than being listed and broken - most are releases whose
              own docker job failed, which is why they have no chart either.

The index is rebuilt with `helm repo index --merge`, from the packages actually
in the directory, so "omitted" needs no bookkeeping: a version with no .tgz gets
no entry, and no entry can point at a package that is not there. A ghcr request
that fails is "unknown", not "no image" - reading a network hiccup as absence
would omit a release that has one.

The run also found something already wrong. The published index has FOUR entries
for 9.36.0 and two for 10.30.0, every copy with a DIFFERENT digest and the SAME
url, because release-charts.sh PREPENDS an entry each time it runs and a re-run
duplicated the version instead of replacing it. At most one of those digests can
be the digest of the wekan-9.36.0.tgz that is really served; a helm client that
picks another fails the integrity check on a perfectly good file. So the rebuild
de-duplicates: for a version with more than one entry, keep the one whose digest
MATCHES the package on disk, and only when there is no package to compare
against fall back to the newest.

Nothing is written without --apply and nothing is pushed without --push, and the
charts job runs it in plan mode only, into the run summary - so the gap is
visible in the release it appears in, while rewriting a published Helm index
stays a command the maintainer runs deliberately.

tests/backfillCharts.test.cjs runs the dedup rule for real against a synthesised
index, including the case that separates the rule from its fallback: the matching
digest on the OLDEST copy, which must still win.

Thanks to xet7 !
2026-08-11 02:00:37 +03:00
Lauri Ojansivu 7b8c377099 Release: the retired Mac runner, the jobs a cancel skipped, and the armv6 userland.
Three failures from the v10.80 run, which reported four snap arches red, one
bundle red, and three jobs that never ran at all.

build-mac-x64 asked for macos-13, and GitHub has RETIRED that image:
actions/runner-images publishes macos-14, macos-15 and macos-26 only, and
macos-13 is gone from the hosted-runner table. A retired label does not fail -
that is the whole problem. The job QUEUES, waiting for a runner that is never
coming. It sat there for nearly two hours and had to be cancelled by hand, and
this has been happening every release: NO WeKan release has ever carried a
wekan-<version>-mac-x64.zip. Intel macOS did not go away, it was renamed, and
.tools/TSC already builds its Intel Mac on the new label: macos-15-intel.

Cancelling it is what skipped charts, ucs and nextcloud. A cancelled job cancels
the RUN, and a cancelling run skips every job that has not started yet - so all
three were skipped in the same second the docker job they wait on succeeded,
after the tag, the release, every bundle and the multi-arch image had shipped.
The Helm chart for a published WeKan was never pushed and nothing said why. A job
whose if is always() still runs while a run is cancelling, so the three now say
always() && <each need>.result == 'success': explicit about the one condition
that ever mattered, instead of relying on a default a cancellation overrides.

The armv6 bundle died on

    qemu-arm: Could not open '/lib/ld-linux-armhf.so.3': No such file or directory

400 lines in, after the whole apt install. DEBIAN HAS NO ARMv6 PORT.
debian:trixie publishes arm/v5 and arm/v7 and nothing between them, and because
containerd treats a lower ARM variant as compatible, --platform linux/arm/v6
quietly resolved to arm/v5: Debian armel, ARMv5 SOFT-float, which contains no
hard-float loader at all. node-armv6 is hard-float, so it could not start.

Two things were wrong. The preflight matched on "architecture": "arm" and ignored
the VARIANT, so "debian:trixie has no linux/arm/v6" was never the answer it gave;
it compares architecture and variant now, and an unrequested variant still
matches anything so linux/arm64 (recorded as arm64/v8) keeps working. And armv6
is built in Debian's arm/v7 container instead, which IS armhf - the same
hard-float ABI and the same loader, so node-armv6 runs in it.

Nothing ARMv7 reaches the bundle. The container's npm install compiles nothing:
it is npm-rebuild over pure-JavaScript packages, with no node-gyp and no CXX in
the log. node-armv6, ferretdb-armv6 and the armv6 MongoDB tools are all
downloaded already built for ARMv6. The single exception is the bundled qemu-user
(#6458), which is copied OUT of the container, and an ARMv7 qemu-armv7l in an
ARMv6 bundle is a binary a Raspberry Pi 1 cannot execute - under a name its
cpu-exec, which looks for qemu-armv6l, would never ask for. So the matrix marks
armv6 same_arch_container: false and install-node-for-arch.sh ships without one,
letting cpu-exec fall back, which is what it is written to do.

Three guards, because each of these was invisible until a release ran:
tests/workflowRunnerLabels.test.cjs checks every runs-on in every workflow
against the labels GitHub currently offers and names the replacement for a
retired one; tests/releaseCancelSafeJobs.test.cjs pins the always() guards and
that they still require every need to have SUCCEEDED;
tests/releaseArmv6Container.test.cjs pins the armv6 container and runs the
preflight's own platform match against the manifest shapes it meets, including
that debian:trixie answers NO to linux/arm/v6.

Thanks to xet7 !
2026-08-11 01:52:41 +03:00
Lauri Ojansivu e1c6221f22 FerretDB: connect with directConnection=true, so a fresh docker compose up starts.
A new install with nothing changed but the port and ROOT_URL could not reach the
database at all:

    MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017
    reason: TopologyDescription { type: 'ReplicaSetNoPrimary',
            servers: Map(1) { '0.0.0.0:27017' => [ServerDescription] },
            setName: 'rs0', ... }

0.0.0.0 is in no compose file. It is FerretDB's own listen address, and the
driver was handed it by the server.

The ferretdb service runs with --repl-set-name=rs0, added in #6480/#6481 so
Meteor can tail an OpLog instead of poll-and-diff. FerretDB therefore answers the
hello handshake as a one-member replica set, and fills hosts, me and primary with
its --listen-addr, which is the wildcard 0.0.0.0:27017. A driver that is not in
direct-connection mode reads that reply as an invitation to do replica-set
DISCOVERY: it adopts the advertised member list and drops the seed it was given,
because the server reports a name other than the one that was dialled. So
mongodb://ferretdb:27017 became 0.0.0.0:27017, which inside the wekan container is
the wekan container itself, where nothing listens.

Measured against FerretDB v1.49.0 - the version reported - with the mongodb
driver the bundle ships:

    mongodb://localhost:27017/wekan
      -> ReplicaSetWithPrimary, servers: 0.0.0.0:27017   (the seed discarded)
    mongodb://localhost:27017/wekan?directConnection=true
      -> Single, servers: localhost:27017

directConnection=true keeps the driver on exactly the host it was given. It costs
nothing else: the handshake still reports setName rs0, which is the only thing
Meteor checks before it will tail an OpLog (accounts for the check in the mongo
package's oplog_replica_set_error.js), so #6480/#6481 are unaffected and the
commented MONGO_OPLOG_URL examples keep replicaSet=rs0 beside it.

All five FerretDB v1 compose files are fixed, being the ones that start FerretDB
with --repl-set-name. The MongoDB compose files are deliberately left alone: those
are real replica sets whose members are reachable under the names they advertise,
and directConnection would pin Meteor to a single node of one.

tests/ferretdbDirectConnection.test.cjs pins both halves - that a compose file
running FerretDB as a replica set carries directConnection=true on every URL
pointing at it, and that the MongoDB ones do not.

Thanks to Dandrass and xet7 !

Fixes #6582,
2026-08-11 01:51:51 +03:00
github-actions[bot] 7f5408947b Bump versions for v10.80 2026-08-10 20:33:21 +00:00
Lauri Ojansivu 061593c5ee Prepare v10.80 release 2026-08-10 23:32:14 +03:00
Lauri Ojansivu 5fe2192962 CHANGELOG: the translations entry, and the Upcoming groups it exposed.
The entry: which three of the six strings needed translating and why the other
three did not, that each language's own OS_Type and OS_Platform decided the form
its translation takes, that Greek and Korean were NOT copied from because those
two files hold Italian and Japanese there, and that forty files keep the English
placeholder on purpose.

Adding it took the Upcoming section to five entries, and the grouping guard then
found what was already wrong: one group line - the snap one - stood above the
Filesystem integrity entry, which is not a snap change, so an unrelated entry sat
inside that group. Every entry now has a group naming its own area, and the snap
group holds only the two snap entries.

Thanks to xet7 !
2026-08-10 20:45:02 +03:00
Lauri Ojansivu 132a644848 Translations: the new Version-pane strings, in 113 languages.
The Version pane's five category labels and the packaging row were added in
English only, so every other language showed them in English. Three of the six
needed translating at all: Platform, package ("Package") and OS. Database was
already translated in 131 languages - the key existed before and this revived it
- and Meteor and Node are product names that stay as they are in every language,
which is also why the filler ignores a value equal to the English source.

Translated directly, per CLAUDE.md: no external service, and each language's own
existing strings as the reference. Its OS_Type and OS_Platform show which form
that language's translators use - "Typ des Betriebssystems", "Tipo SO", "Тип ОС",
"Käyttöjärjestelmän tyyppi" - so OS is "Betriebssystem" in German, "SO" in
Italian and Portuguese, "ОС" in Russian and "Käyttöjärjestelmä" in Finnish
rather than one spelling imposed on all of them.

Two files were NOT copied from: el's OS_Type and OS_Platform hold Italian, and
ko's hold Japanese. Propagating that would have spread somebody else's mistake
into three more strings, so those two got proper Greek and Korean instead.

Applied through releases/translations/fill-translations.mjs --apply, which
writes ONLY into a placeholder - so no human translation could be overwritten
even by accident. The diff shows it: 292 changed lines across 106 files, and
every one of them is Platform, package or OS. Key order and indentation are
unchanged, every file still parses, verify-human-preference.mjs passes 10/10,
and the 350 node suites pass.

40 language files are left with the English placeholder on purpose - ace, ary,
br, gu-IN, ig, km, mn, oc, or_IN, pa, tk_TM, tlh, ug, ve, vl-SS, vo, wa, wo, xh,
yi, yo, zgh, zu and the en-* variants, which are English by design. A placeholder
that says so is better than a translation I cannot stand behind, and Transifex
can still replace any of these with a human one: nothing here is pushed there.

The backlog is 10285 strings, from 10592.

Thanks to xet7 !
2026-08-10 20:40:53 +03:00
Lauri Ojansivu 5852351d30 Updated translations 2026-08-10 20:35:16 +03:00
Lauri Ojansivu a475afd3d3 Merge pull request #6581 from khuongsatou/codex/apple-glass-pastel-theme-clean
Add Apple Glass Pastel board theme
2026-08-10 18:13:23 +03:00
khuongsatou's projects 4763c94604 Add Apple Glass Pastel board theme
Register appleglasspastel as a fixed special theme, wire its accent and exporter colour, add glass/pastel board surfaces, and guard the picker/docs/runtime preview coverage.
2026-08-10 21:51:21 +07:00
Lauri Ojansivu 539c678199 CHANGELOG: the four v10.79 changes that shipped without an entry.
Every commit of a release belongs in that release's section, and four were not
there: the snap-store table correction, and the three that rewrote CLAUDE.md and
AGENTS.md. They are in v10.79 rather than Upcoming because that is the release
they are in - moving them would be tidier and untrue.

The documentation subsection gains a second entry, so it is grouped by area the
way the rules ask, and a new "and documents how to work on these repositories"
subsection carries the instruction-file entries: who maintains these
repositories and who commits, what is in .tools/ and which changelog format each
repository under it uses, and the correction that TSC is xet7's too.

Thanks to xet7 !
2026-08-10 15:26:36 +03:00
Lauri Ojansivu d0dc7dff98 CHANGELOG: the third MongoDB reader, under Upcoming.
Beside the entry it completes: which server can read which data and why that is
a property of the servers the snap carries, that mongod 4.2 reads only during a
migration, why it brings its own OpenSSL 1.1, that a failure to stage it never
fails a release, what was verified on this machine, and which vintages are still
answered by the page instead.

Thanks to xet7 !
2026-08-10 14:58:07 +03:00
Lauri Ojansivu 71ba0c2dcd Snap: a third MongoDB reader, so a 4.x database migrates instead of stopping.
#6471 again, the other half. The previous commit stopped the crash loop and
explained it; this one removes the reason for it in the case that was actually
reported.

A MongoDB server only starts on data whose featureCompatibilityVersion is at most
one major behind it, so what the snap can READ is decided by which servers it
carries: mongod 7 (FCV 6.0, 7.0) and the MongoDB 3.2 tools (3.2). Everything
between was unreadable, and the WeKan snap has shipped 3.6, 4.0, 4.2, 4.4 and 5.0
over the years. The reported error, twice, is mongod 7 naming the gap: "Try
MongoDB 4.2 or earlier" - FCV 4.0.

So the snap now bundles mongod 4.2 as a third reader, used ONLY to read the old
data during a migration and never as the running database. mongod 4.2 opens FCV
4.0 and 4.2, and the modern importer reads it with the bundled driver, which
supports servers from 4.2 up - the same importer that reads a 6/7 source, not a
second copy of it. The probes run newest-first: 7, then 4.2, then the 3.2 tools,
then the explanatory page.

amd64 and arm64 only. MongoDB publishes no 4.2 for the other architectures, and
they have been FerretDB from their first boot, so there is nothing there to
migrate from.

IT CARRIES ITS OWN OpenSSL 1.1. The 4.2 build links libssl.so.1.1 and
libcrypto.so.1.1, and core24 is Ubuntu 24.04, which ships OpenSSL 3 - without
them the binary does not even load. Both come from one Debian libssl1.1 package,
staged beside the binary and put on LD_LIBRARY_PATH exactly as the 3.2 tools
already are. The filename is resolved by listing the pool rather than pinned,
because point releases roll and a pinned name 404s the day they do; three
archives are tried in turn.

OPTIONAL BY DESIGN. Every failure in the part - download, checksum, OpenSSL,
or the binary not running - ends it with a message and no binary, and the
migration simply does not find one. A release is never failed over a migration
aid, and the snap then behaves as it did before: the unreadable-database page.

VERIFIED, as far as this machine allows: mongod 4.2.25 aarch64 was downloaded,
staged with the Debian bullseye libssl1.1, and RUN here - "db version v4.2.25,
OpenSSL version: OpenSSL 1.1.1w" - then started on a dbpath, forked, and
listened on a port. That is the whole mechanism the snap will use, on a 2026
system, outside a snap. The build itself repeats the --version check and
unstages the binary if it fails. What is not verified is a real snap build and a
real 4.x database; the first upgrade is that test.

Still unreadable, and still handled by the page rather than a migration: 3.4,
3.6, 4.4 and 5.0. Adding mongod 5.0 beside this one would close 4.4 and 5.0 the
same way, at the same cost in size.

Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7 !
2026-08-10 14:57:07 +03:00
Lauri Ojansivu 909496fb8b CHANGELOG: the snap's unreadable-database stop, under Upcoming.
The bug subsection has two entries now, so it is grouped by area and its header
is plural. The entry: what the loop was, why nothing in it could succeed, that
the snap stops and exits 0 rather than letting snapd restart it forever, what
the page tells an admin, and that nothing on that path is deleted or modified.

Thanks to xet7 !
2026-08-10 13:37:17 +03:00
Lauri Ojansivu 562fa02718 Snap: a database this snap cannot read stops and says so, instead of 502.
#6471. A snap upgraded onto a database left by a MongoDB 4.x or 5.x snap served
502 Bad Gateway forever, with the reason only in `snap logs`:

    This version of MongoDB is too recent to start up on the existing data
    files. Try MongoDB 4.2 or earlier.

The snap carries two readers - mongod 7, the server it runs, and the MongoDB 3.2
tools for a 6.09-era database - and nothing in between, so 4.x data opens in
neither. What the code then did is the one thing that cannot work:
migration-control found neither reader could open it and handed back to
mongodb-control, mongodb-control started mongod, mongod failed the same way,
mongodb-control re-ran the migration, three times by its own counter, and then
exited 1 for snapd to restart. Nothing in that loop can succeed - reading those
files needs a binary the snap does not have - and the site was down throughout.

It stops now, and says so where an admin will see it. migration-control tells
"no reader for this vintage" from "unreadable/corrupt" by mongod's own words,
writes $SNAP_COMMON/.mongodb-data-too-old carrying the version mongod named as
still able to read the data, pauses auto-migration and exits 0 - 0, because
snapd restarts a failing service forever and no restart can help. mongodb-control
refuses to start a mongod it knows cannot start. wekan-control serves the
maintenance page with a third reason, data-too-old, at the top of the script and
from inside the database wait loop, so a WeKan already waiting switches over
without a restart.

The page names the version that can still read the files and gives the two ways
forward - revert to the revision that worked, or dump with a MongoDB that can
read it and restore into this version - says that nothing was changed and that
attachments and avatars are files on disk, and drops the auto-refresh and the
spinner the other two maintenance pages have: this is a stop, not a wait, and
the page should not promise that something is happening.

NOTHING IS DELETED OR MODIFIED on this path. The source data is exactly as it
was; the marker is the only thing written, and removing it lets the snap try
again.

tests/snapOldMongoData.test.cjs covers the wiring in all three scripts and then
RUNS the page module - it is standalone Node with no dependencies - to check
what an admin actually sees: 503, the version, "untouched", both remedies, no
refresh, no spinner. The snap docs gain the section an admin searching for that
mongod line will find, with the commands.

Thanks to Philippe-Bentegeac, JDeepix, imlit and xet7 !

Fixes #6471,
2026-08-10 13:35:44 +03:00
Lauri Ojansivu fd67a510f8 CHANGELOG: a new Upcoming section for what came after the v10.79 release.
The v10.79 release renamed the Upcoming heading, and two changes landed after
it: the Filesystem integrity fix, whose entry was written "under Upcoming" when
Upcoming had already become v10.79, and the Version table restructure, which had
no entry at all. So the released section claimed a fix that is not in the
release, and the newest work was invisible.

Both are in a new Upcoming section now - the Filesystem integrity entry moved
out of v10.79 rather than copied, so it appears once - with the summary and the
binaries table the shape requires, and the table stated for what it is: v10.79's
binaries, which nothing here rebuilds.

Thanks to xet7 !
2026-08-10 12:57:57 +03:00
Lauri Ojansivu aed26c6771 Version: one table with combined category rows, over two 50% columns.
The pane was five tables, one per category, and five tables size their columns
independently: "WeKan ® Version" made the first one's label column wide and "OS
Type" made the next one's narrow, so the values started at a different x in
every group and the pane read as five unrelated things.

One table now. Each category is a ROW in it - a th with colspan=2, bold and
start-aligned - so it says what the rows under it are about instead of being a
label with an empty cell beside it. A colgroup of two 50% columns plus
table-layout:fixed puts every label and every value in the same place down the
whole pane, and settingBody.css's 240px header cap is undone for this table
only, since the width is now stated outright and the cap would fight it.

The category title takes the table's own font size deliberately: at the pane
title's size five of them would read as five pane titles and "Version" would be
lost among them.

Two of the repo's own guards caught mistakes on the way, which is what they are
for. tests/jadeCompiles.test.cjs refused `th(colspan=2)`: the Meteor jade
dialect wants the value quoted, and an unquoted one is a build failure rather
than a rendering difference. tests/rtl.test.js refused `text-align: left`: the
label column is on the RIGHT in Arabic and Hebrew, so it is `start`.

tests/versionPaneCategories.test.cjs follows the new shape - exactly one table,
five tr.info-category rows each spanning both columns, the colgroup and
table-layout:fixed that make the 50/50 real, and the category title bold with no
font-size of its own. The row-by-category and no-row-was-lost checks are
unchanged and still pass, so the regrouping moved nothing.

Thanks to xet7 !
2026-08-10 12:54:55 +03:00
Lauri Ojansivu ed0d7a2bad 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-08-10 12:51:42 +03:00
Lauri Ojansivu 5a947b6959 CHANGELOG: the blank Filesystem integrity pane, under Upcoming.
First group of the bug fixes, beside the Admin Panel work above it: what the
pane looked like, the one missing helper, and that Blaze reads an undefined
helper as false rather than complaining - which is why three-quarters of the
wiring being right showed as an empty page.

Thanks to xet7 !
2026-08-10 12:51:41 +03:00
Lauri Ojansivu 01c36852da Filesystem integrity showed a blank page: the helper was never added.
Admin Panel / Problems / Filesystem integrity drew nothing at all - the pane
title and then empty space - while Summary went on reporting "7 new problems"
for it.

Everything about the pane looked right, which is why it survived: the menu has a
report-integrity entry, clicking it is handled, the handler sets
tmpl.showIntegrity, the template has `else if showIntegrity.get` and an
+eventStreamReport(stream="integrity") under it. The one missing piece was the
HELPER. showIntegrity() was never added beside showDatabase() and the eight
others, and in Blaze an undefined helper is not an error - it is falsy. So the
branch never ran, the page was blank, and nothing anywhere said why.

The helper is there now, and tests/adminPaneHelpers.test.cjs guards the class
rather than this one pane: every `show*.get` branch in a settings template must
have a helper of that name in that template's own .js, and a ReactiveVar behind
it. The templates are FOUND, not listed, so a pane added later is covered
without editing the test. It also names Filesystem integrity's four wiring
points one by one, because that is the pane that was three-quarters wired.
Checked in the failing direction: removing the helper again fails the test with
the reason.

Thanks to xet7 !
2026-08-10 12:51:41 +03:00
github-actions[bot] afcb9353ee Bump versions for v10.79 2026-08-10 02:07:28 +00:00
Lauri Ojansivu 1d9222eb81 Prepare v10.79 release 2026-08-10 05:06:24 +03:00
Lauri Ojansivu 4588fb8b3d Updated translations. 2026-08-10 05:05:06 +03:00
Lauri Ojansivu fd3a381cd6 CHANGELOG: the packaging row's label is translated, its value is not.
Folded into the Version-pane entry rather than added as a second one: the
release ships one feature, and "we renamed the key afterwards" is not a thing a
reader of the release notes needs to follow.

Thanks to xet7 !
2026-08-10 03:52:34 +03:00
Lauri Ojansivu 5091c3883a Version pane: the packaging row is package, and its value is never translated.
The label was `Platform_packaging` / "Installed as". It is `package` / "Package"
now: a short key for a short column, in the vocabulary the rest of the pane uses.

The half that matters more is the VALUE. bundle.zip, Snap, Docker and Sandstorm
are the names of the things themselves - a package format, a store, a product -
not words describing them, so they are not translated anywhere and none of them
is an i18n key. What an admin reads in the pane is then the same string they can
put in an issue, search the docs for and grep a log with, in every language; a
translated "Ohjelmapaketti" in a Finnish screenshot would match nothing anybody
can look up.

The test carries both halves rather than the comment alone: the label must be
the translated key, the value must be printed straight from
statistics.platform.packaging with no {{_ }} around it, and none of the four
names may exist as an i18n key - which is how "translate it somewhere else
later" is caught. Checked in the failing direction too: wrapping the value in
the translation helper fails with the reason.

Thanks to xet7 !
2026-08-10 03:52:18 +03:00
Lauri Ojansivu de7770f211 CHANGELOG: the Version pane's five tables, under Upcoming.
Its own subsection between the new features and the bug fixes, where an Admin
Panel reorganisation goes: what the pane was, what the five categories are and
why reactivity and DDP moved to Meteor, the new "installed as" row and the order
its detection asks its questions in, and that the heading size is a test rather
than a promise.

Thanks to xet7 !
2026-08-10 03:42:10 +03:00
Lauri Ojansivu dcc6c8fe5d Version pane: five tables with a heading each, and what WeKan is installed as.
Admin Panel / Settings / Version was ONE table of 38 rows. The WeKan version,
the OS load average, the DDP transport and a V8 heap counter were the same kind
of thing in it: one flat list, read top to bottom, with no way to jump to the
part you came for.

Five tables now, each under a small heading, in the order somebody debugging
reads them - Platform (what this IS), OS (what it runs on), Meteor (how it talks
to the client: reactivity, reactivity order, DDP transport), Database (type,
versions, commits, storage engine, OpLog, sessions) and Node (version, heap,
memory). Reactivity and DDP moved to Meteor, where they belong: they are how the
client is fed, and they were the two rows most often read as database settings.
Whether an OpLog EXISTS stays with the database, because that is a property of
it. No row was dropped; a test lists all 38 by name and requires each to still
be shown.

NEW ROW: what WeKan is installed as - bundle.zip, Snap, Docker or Sandstorm. It
is the first thing a support answer turns on, and the pane never said it: the
same version keeps its data somewhere else, carries a different database and
gives the admin different reach in each of the four. models/lib/platformPackaging.js
detects it, most-specific-first: an explicit WEKAN_PACKAGING wins (a packaging
that knows what it is should say so, and it is how a future .deb or chart names
itself), then Sandstorm, then snapd's own SNAP/SNAP_NAME, then a container
runtime's marker file, then the bundle. Sandstorm and Snap are asked BEFORE the
container markers on purpose: a grain is a container, and a confined snap can
look like one, so the other order answers "Docker" for both. It claims nothing
it cannot know - there is no "source checkout" answer, because a `meteor run`
and an unpacked bundle are identical from inside the process.

The detection is a pure function taking the environment, the Sandstorm flag and
a file-exists callback, so all nine of its tests run here without a snap, a
container or a grain - including that an empty SNAP is not a snap, that an empty
WEKAN_PACKAGING falls through rather than blanking the field, and that an
unreadable root answers bundle.zip instead of throwing an admin's Version page
away.

The headings are SMALLER than the pane title above them, and a test compares the
two font sizes rather than trusting the CSS to stay that way: five headings at
1.25rem would read as five pane titles and "Version" would be lost among them,
and an unstyled h3 - the browser default here - is larger still. Six new English
strings; the other languages fall back to English until translated, as usual.

tests/clientBundleImports.test.cjs caught the new stylesheet before it could
matter: package.json sets meteor.mainModule, so a .css nobody imports is simply
not in the bundle. It is imported from client/features/settings.js now.

Thanks to xet7 !
2026-08-10 03:40:12 +03:00
Lauri Ojansivu 3170b70f51 CHANGELOG: the next base's six architectures, under Upcoming.
Its own group under the bug fixes, beside the snap builds group it belongs
with: what was missing from snapcraft-core26.yaml, why nothing could have
noticed, and why adding the platform without the t64 package names would have
been nominal.

Thanks to xet7 !
2026-08-10 03:04:32 +03:00
Lauri Ojansivu 664b8fde6f The next base builds all six architectures too, armhf included.
snapcraft.yaml declares six platforms - amd64, arm64, armhf, ppc64el, riscv64,
s390x - and every one of them is built: amd64 and arm64 natively, the other four
on Launchpad. snapcraft-core26.yaml declared FIVE. armhf was missing, and nothing
could notice: that file is not built by any release, it is the next base kept so
the move can be tested before it is made. The day core26 becomes the base, armhf
would simply stop being published and the only symptom would be a store column
going stale, exactly as wekan-ondra's armhf did at 0.22.

Adding the platform alone would have been nominal. The same file still carried
the PRE-t64 stage-package names - libssl3, libcurl4, libgoogle-perftools4 - and
those are precisely what failed armhf on core24 twice: Ubuntu's 64-bit time_t
transition renamed them, the 64-bit architectures kept a compatibility Provides
so the old spelling resolves there, and armhf did not. So armhf on core26 would
have failed on the first thing it tried, in the way this repository has already
debugged. It has the t64 names now. Its Caddy branch already mapped armhf to
Go's armv7 name; the comment saying that branch is unreachable is no longer true
and says so.

Two guards, both checked in the failing direction as well:
tests/releaseSnapArches.test.cjs now requires the two snapcraft files to declare
the SAME set of architectures - removing armhf from core26 fails it with the
reason - and the stage-package check runs over BOTH files rather than only the
one the release builds, so the next base cannot drift back to a name that breaks
on one architecture only.

Nothing here changes what the release builds today: core24 stays the built base,
and wekan's armhf snap is still waiting on the Caddy armhf fix in the upcoming
release.

Thanks to xet7 !
2026-08-10 03:04:11 +03:00
Lauri Ojansivu cf0c347b2e Snap CPU platforms: what the store actually holds at 10.78, and why.
The table said "as of 10.76" and marked every empty square the same way: "built
by snapcraft.yaml but that snap has no revision for it yet - uploading one is all
that is needed". For eight of the twelve squares that is not true, and it points
whoever reads it at the wrong job.

Three marks now, because there are three situations. wekan's armhf is BUILT by
the release and missing from the store: snapcraft.yaml declares it,
snap-launchpad builds it, and it has been failing - most recently on Caddy, which
publishes no linux_armhf archive because its asset names are Go's, fixed for the
next release. The exotic squares of wekan-ondra and wekan-gantt-gpl are not
pending uploads at all: the snap-variants job has four matrix entries, each
variant on amd64 and arm64 on GitHub runners, and the ppc64el/s390x/riscv64/armhf
builds come from snap-launchpad, which builds only the `wekan` name. Nothing will
ever upload those unless the variant job learns Launchpad remote builds under the
variant snap names. And wekan-ondra's armhf 0.22 stays a fossil, now said
explicitly: there is no newer revision to promote AND nothing currently builds
one.

riscv64 moved from missing to published since 10.76, so wekan is five of six.
Every published architecture is on all four channels, which is the earlier
release-to-all-channels work holding.

Thanks to xet7 !
2026-08-10 02:53:18 +03:00
Lauri Ojansivu bf1f7aea47 CLAUDE.md and AGENTS.md: TSC is xet7's too, under another organisation.
The previous commit read Secretchronicles/TSC's remote, saw it was not a wekan/
repository, and concluded it was somebody else's project xet7 contributes to. It
is his: the organisation is Secretchronicles rather than wekan, and that is the
only difference. His GitHub profile says it in three words - "WeKan and TSC
maintainer" - and the files now cite it, so the next reader checks a source
instead of inferring from a remote URL as this one did.

So maintainer mode covers it - commit directly to devel, no pull request, same
author, no AI attribution - and the files say which .tools/ repository actually
is somebody else's: sandstorm-io/sandstorm, cloned for reference.

What does NOT follow from maintaining it is WeKan's house style. TSC keeps its
own GNU ChangeLog file and its own release process, exactly as the CHANGELOG
section below already said, because a project is read on its own terms.

Thanks to xet7 !
2026-08-10 02:04:31 +03:00
Lauri Ojansivu d077948115 CLAUDE.md and AGENTS.md: what is in .tools/, and each repository's changelog format.
THE LAYOUT OF .tools/. It was one sentence naming three clones. It is now the two
kinds of thing that directory actually holds, because they are treated
differently. Companion git repositories - wekan/FerretDB on main-v1,
wekan/node-patches and wekan/mongo-tools-patches on main, Secretchronicles/TSC on
devel, sandstorm-io/sandstorm - each with their own history, branches, changelog
and release flow, cloned on demand by build.sh's ensure_tool_repo; a commit in
WeKan is never a commit in one of them. And unpacked toolchains that are
downloads rather than repositories: node-v<version>-linux-<arch>, go with its
gopath/gomodcache/gocache, .meteor when HOME points at .tools, the TSC AppImage.
Delete and re-fetch those freely.

TSC and sandstorm are NOT xet7's repositories, and the ownership paragraph at the
top no longer implies they are. The commit author is the same there; the branch,
the contribution process and the changelog format are that project's call.

NO CLAUDE.md OR AGENTS.md UNDER .tools/. node-patches and mongo-tools-patches
each had a pair, and they were removed: the rules are identical for all of these
repositories and a second copy drifts from the first. These two files are where
they live. A fact true of one repository only belongs in that repository's own
README.md or docs/ - and an instruction file found there is to be removed rather
than updated.

CHANGELOG FORMAT, PER REPOSITORY. A table of the five that come up, because they
are not all the WeKan format: wekan/wekan, node-patches and mongo-tools-patches
use it; wekan/FerretDB keeps UPSTREAM FerretDB's (## [v1.48.0](tag) (date) with
### New Features/Fixed/Other Changes bullets ending "by @xet7"); and TSC uses GNU
ChangeLog - a "YYYY-MM-DD  Name  <email>" header over tab-indented "* Fix: ..."
entries ending "(by Name)", in a file called CHANGELOG with no extension. The
reason is the reader: a FerretDB release is read beside upstream's, and a TSC
entry beside a decade of GNU ChangeLog entries. When this file and the file being
edited disagree, the file being edited wins.

AGENTS.md had drifted from CLAUDE.md - it still described a FerretDB subdirectory
at the repo root and told the release step to run from there. The two files are
now identical apart from their first and third lines, which name the tool that
reads them.

Thanks to xet7 !
2026-08-10 01:55:46 +03:00
Lauri Ojansivu 35dd10b89f CLAUDE.md and AGENTS.md: who maintains these repositories, and who commits.
The instruction files opened with an identity CHECK - "maintainer mode only when
the identity is exactly Lauri Ojansivu <x@xet7.org>" - which says what to do with
an identity but never says whose repositories these are. They are xet7's: WeKan,
the FerretDB v1 fork, node-patches and mongo-tools-patches. That is now the first
thing both files say, and the commit author follows from it rather than from a
conditional.

Two rules are spelled out under it. No commit, pull-request body or CHANGELOG
line ever attributes work to an AI - no Co-Authored-By, no "Generated with", no
assistant or model name - and CODE_OF_CONDUCT.md is cited as where that comes
from ("mention only those participants that are human"); a "Thanks to ... and
xet7 !" line credits people. And a missing or wrong git identity in one of these
checkouts is to be SET, not worked around: the .tools/ clones build.sh makes can
come up with no user.name/user.email at all - mongo-tools-patches did - which
would author a commit as whatever the machine's default is.

Contributor mode is unchanged in substance: a different person's identity, in
their own fork or clone, still means a branch and a pull request, and that pull
request carries no AI attribution either.

Thanks to xet7 !
2026-08-10 01:45:21 +03:00
Lauri Ojansivu cf000d000a CHANGELOG: the armv6 Docker platform, and the CPU-platform pages.
Two entries under Upcoming. The armv6 one goes in the "Raspberry Pi 1 and
Zero" group beside the bundle that already ships: what TARGETVARIANT changes,
why the base image is asked what it publishes at all, and that debian:trixie
has arm/v5 and arm/v7 and nothing between them.

The documentation subsection covers the new Docker CPU-platforms page, the
snap page's armv6 section and bundle table, and the FerretDB v1 page's
architecture list. The In short paragraph no longer claims armv6 ships a
Docker image today - it says the image is wired and gated.

Thanks to xet7 !
2026-08-10 01:36:08 +03:00
Lauri Ojansivu 5ec9f3ae5b Document the CPU platforms of the Docker image, beside the snap's.
"Which CPUs is this published for, and why not that one" was answerable for
the snap and nowhere else, so the same page now exists for the image:
docs/Platforms/FOSS/Container/Docker/CPU-platforms.md. It carries the platform
matrix with each platform's TARGETARCH, TARGETVARIANT and bundle, how the set
is decided on every release from what the base image publishes and which
bundles landed, the three names that differ between Docker, Debian and the
WeKan bundles, and one section per platform that is deliberately NOT an image:
loong64 (no base image exists at any tag), armv6 (gated on a base with
arm/v6), armv7 (the one 32-bit ARM slot must carry the armhf baseline) and the
Windows and macOS bundles. It ends with why the FerretDB image covers more
platforms than the WeKan one - FROM scratch around a static Go binary needs no
userland at all.

The snap's page gains the matching armv6 section - the Snap Store has no armv6
architecture, and its only 32-bit ARM is armhf, which an ARMv6 board cannot
run - and a table of all fifteen bundles against which six become snaps, so
"it is missing" and "it cannot be there" stop looking alike. The two pages
link each other, because the answer differs between them.

docs/Databases/FerretDB/1/README.md said the per-architecture FerretDB binary
was embedded in the bundles "for ppc64le, s390x, riscv64". Every bundle
carries one; those three are part of a longer list of platforms where it is
the DEFAULT because MongoDB publishes no server. It now lists all seventeen
built binaries and separates the three 32-bit ARM builds that are not variants
of each other: armhf is GOARM=7, armv6 is GOARM=6, armel is GOARM=5 software
floating point.

Thanks to xet7 !
2026-08-10 01:35:51 +03:00
Lauri Ojansivu f342d54d18 Docker: read TARGETVARIANT, and make linux/arm/v6 a candidate platform.
TARGETARCH does not identify a 32-bit ARM platform: linux/arm/v6 and
linux/arm/v7 both arrive as TARGETARCH=arm, and the generation is in
TARGETVARIANT. The Dockerfile's case mapped arm straight to armhf, so the
moment armv6 existed as a bundle an ARMv6 build would have been handed the
armhf zip - ARMv7-A instructions for a CPU that cannot run them. It now
branches on the variant: v6 takes armv6, v7 and an unset variant take armhf,
and anything else exits. v5 is armel, which Go builds and Node.js does not
exist for, so there is no bundle and refusing is the only honest answer.

linux/arm/v6 joins the docker job's optional platform list, and the release
now asks a question it never asked before: does the BASE IMAGE publish this
platform? For every other architecture that is uninteresting, because a base
that lacks one fails the build. For 32-bit ARM it does not fail - containerd
treats a LOWER arm variant as compatible, so a linux/arm/v6 request against a
base with arm/v5 and arm/v7 resolves to arm/v5, Debian armel, SOFT float, and
the image would be built on a userland whose loader cannot start the
hard-float node-armv6 in the bundle. A silent downgrade is worse than a
dropped platform.

So the job reads the base name out of the Dockerfile - no second copy of it -
asks docker buildx imagetools inspect what that base publishes, and drops a
candidate it lacks with a warning saying why. debian:trixie is 386, amd64,
arm64/v8, arm/v5, arm/v7, ppc64le, riscv64, s390x: no arm/v6, because Debian
has no ARMv6 port at all. The armv6 bundle zip is unaffected, and the image
platform turns itself on the day a base publishes the variant.

VERIFIED: debian:trixie's manifest list was read from the registry rather than
assumed, and the decide step was extracted and run against both that list
(armv6 drops, the other seven build) and a base that does publish arm/v6
(armv6 included, paired with the armv6 bundle). tests/releaseDockerPlatforms
pins the variant split, the refusal of v5, the platform-to-bundle pairing and
that both loops ask the base - the last so that deleting the check cannot
quietly become an image whose Node.js will not start.

Thanks to xet7 !
2026-08-10 01:35:38 +03:00
Lauri Ojansivu d9e4cd57cb Add the armv6 bundle: Raspberry Pi 1 and Zero.
Every piece of the chain now exists, and the missing one was Node.js.
nodejs.org dropped its ARMv6 binaries after Node 11 and unofficial-builds
has none, so wekan/node-patches gained an armv6 target (V8 still accepts
--arm-arch=armv6 and configure.py still carries is_arch_armv6() with
`vfp` among valid_arm_fpu); wekan/FerretDB and wekan/mongo-tools-patches
gained GOARM=6 targets beside their armhf ones.

Their armel is GOARM=5 and WOULD run on these boards, which is why it
looks like a substitute and is not one: GOARM=5 does floating point in
software and ARMv6 boards have VFPv2.

The bundle builds in a linux/arm/v6 container, takes node-armv6, and
releases/resolve-node-source.sh resolves it - naming linux-armv6l as the
upstream spelling even though nothing upstream will ever answer to it, so
the search it prints is honest about where it looked.

NO SNAP, and that is not an oversight: the Snap Store has no armv6
architecture at all. Its only 32-bit ARM is armhf, which is ARMv7-A
hard-float and will not run on an ARMv6 board. So armv6 ships as a
bundle zip and a linux/arm/v6 Docker image, and
models/lib/snapArchitectures.js records that reason in
NOT_SNAP_ARCHITECTURES beside i386's and armv7's.

Four tests found the four places a new platform has to be registered,
which is what they are for: releases/expected-assets.sh (or "Release all
missing" never notices it is absent), the resolver mapping table, the
non-native bundle list, and the snap-platform exemption. Each list was
updated rather than the guard loosened.

WHAT IS NOT VERIFIED: none of these binaries has been built. The Node.js
one is a multi-hour ARM cross compile and the first CI run is its test.
Everything checkable from source was checked - V8's accepted --arm-arch
values, configure.py's ARM handling, Go's GOARM semantics, and that
zlib's ARM SIMD is gated on arm_fpu=="neon" so an armv6 build selects
scalar code and needs none of the NEON patching armv7 does.

Thanks to xet7 !
2026-08-09 23:38:43 +03:00
Lauri Ojansivu 3d9ee1f9fc CHANGELOG: the win-arm64 bundle, and why armv5/armv6 cannot be added.
Thanks to xet7 !
2026-08-09 23:06:38 +03:00
Lauri Ojansivu 8bb7b9ba62 Add the win-arm64 bundle: every binary it needs was already published.
Windows on ARM is a bundle WeKan could always have shipped and simply
did not. All three suppliers publish it:

  Node.js       nodejs.org, node-v24.19.0-win-arm64.zip, officially
  FerretDB      wekan/FerretDB, ferretdb-win-arm64.exe
  mongo-tools   wekan/mongo-tools-patches, every tool as -win-arm64.exe

releases/resolve-node-source.sh gains the platform and resolves it live
to nodejs.org with a published SHA256, so it needs no node-patches build
at all - the asset name is recorded anyway, for the day it does.

The job is build-win64 with the architecture changed. It builds on the
x64 Windows runner, like win32 does: the Meteor bundle is JavaScript,
the native modules are installed with --ignore-scripts and are not
compiled for the runner's architecture either way, and the only
architecture-specific things in the zip are the three binaries above,
which are downloaded rather than built.

Registered where a new bundle has to be registered or it is invisible:
release-notes needs it, or the provenance rows are collected before the
job that writes them has finished; and releases/expected-assets.sh lists
it, or "Release all missing" never notices it is absent.

WHAT CANNOT BE ADDED, checked at the same time: armv5, armv6 and armel.
FerretDB and the MongoDB tools publish armel - they are Go, and Go still
targets it - but Node.js does not exist for any of the three. nodejs.org
publishes no 32-bit ARM at all for v24.19.0, unofficial-builds has no
armv6l, and wekan/node-patches builds node-armhf and node-armv7 only. No
Node.js means no bundle, so no Docker image and no snap either. The Snap
Store has no armv5 or armv6 architecture in any case; its only 32-bit
ARM is armhf, which WeKan already publishes.

tests/cpuExecWiring.test.cjs counted four non-Linux bundle jobs and
found five. Rather than change the 4 to a 5, it now derives the count
from the jobs and checks that EACH of them strips the Linux-only
cpu-exec and qemu-x86_64 - which is what the number was standing in for,
and what a bare count cannot tell you when it changes. Verified it fails
when build-win-arm64 does not strip its own copy.

Thanks to xet7 !
2026-08-09 23:05:00 +03:00
Lauri Ojansivu c892540f43 Caddy: the same armhf mapping in every snapcraft file that fetches it.
snapcraft.yaml was fixed; snapcraft-core26.yaml had the identical
passthrough default and was missed. core26 does not build armhf today,
so nothing failed there - which is exactly why it would have been
rediscovered by a release rather than by a test the day an armhf build
was added.

The guard now checks EVERY snapcraft file that fetches Caddy rather than
the first one it finds, which is the mistake that let this through: the
original test read `yaml.indexOf('case "${CRAFT_ARCH_BUILD_FOR}" in')`
and got the WEKAN_ARCH case above it, so it tested the wrong block while
looking like it worked. It is scoped to the block containing CADDY_ARCH
now. It deliberately does not require two such files - WeKan ships
core24 because that is what may publish to the stable channel, and
either file could be removed one day - so the invariant is about the
files that DO fetch Caddy, not how many there are.

Also pins what makes downloading the official binaries sufficient: the
bundled Caddyfile is `:8080 { reverse_proxy localhost:3000 }` and
nothing else, so every directive is in the standard distribution and no
xcaddy build is needed. A third-party module's directive would change
that and would not fail until Caddy refused the config at runtime, so
the test names the ones that would.

Thanks to xet7 !
2026-08-09 22:51:38 +03:00
Lauri Ojansivu 08f41037f3 CHANGELOG: the Caddy armhf mapping and the Snap Store retry.
Thanks to xet7 !
2026-08-09 22:44:56 +03:00
Lauri Ojansivu 4e9be22f95 Snap: Caddy has no armhf archive, and a store hiccup is not a bad snap.
Two failures in the v10.78 snap jobs, neither of them a problem with
WeKan.

ARMHF ASKED FOR A CADDY ARCHITECTURE THAT DOES NOT EXIST. With the
libcurl4t64 fix in, the armhf build got past the stage-packages and died
further along:

    :: Downloading Caddy 2.11.4 (linux/armhf) from GitHub releases...
    :: + curl -fsSL -o /tmp/caddy.tar.gz https://github.com/caddyserver/
       caddy/releases/download/v2.11.4/caddy_2.11.4_linux_armhf.tar.gz
    :: curl: (22) The requested URL returned error: 404
    'override-build' in part 'caddy' failed with code 1.

Caddy is built by Go and its release assets carry GO's architecture
names, not Debian's. There is no linux_armhf archive and there never has
been - the 32-bit ARM ones are armv5, armv6 and armv7 - and the case
statement had no armhf branch, so it fell through to a default that
passed the Debian name straight into the URL. The "fall back to the
pinned version" path then retried the SAME wrong name, so the failure
read as "Caddy stopped publishing this architecture" when it was this
file's mapping all along.

armhf maps to armv7, not armv6: Debian armhf's baseline is ARMv7-A with
VFPv3-D16 hard-float, and Go's armv7 build is GOARM=7, which is exactly
that. This is NOT the armhf/armv7 distinction that matters for Node.js
in wekan/node-patches - that one is about NEON, and GOARM=7 does not use
NEON. Checked against the actual release: all six mapped URLs answer,
and linux_armhf 404s.

The default branch now names the problem and stops, instead of guessing
a name and letting a 404 blame the wrong project.

THREE GOOD SNAPS WERE LOST TO A STORE HICCUP. riscv64, ppc64el and s390x
each built on Launchpad, downloaded, and were then refused:

    Status: error while processing
    Issues while processing snap:
    - binary_sha3_384: Error checking upload uniqueness.

That is the store failing its OWN duplicate check on a digest it had
just computed - a server-side error, not a bad snap - and the message
the job printed was about 'is not a valid file', credentials and ACLs,
none of which applied. The upload is retried three times with a backoff
now, and the give-up message says the snap is fine and nothing here
needs changing.

The retry stays narrow on purpose: a rejected file, unparseable
credentials or a missing ACL will be rejected identically three times,
and retrying those only buries the one message that says what to fix.
The classifier is tested against all four, not just the one that
happened.

New checks in tests/snapStagePackages.test.cjs (every snap architecture
maps to a Caddy asset that exists, every build-for has a mapping, and -
negatively - an unmapped one fails loudly instead of 404ing) and in
tests/provenanceTable.test.cjs (the processing failure is retried, the
four unretryable messages are not, and the give-up message does not send
somebody debugging a build that succeeded). Verified the Caddy guard
fails when the armhf branch is removed.

Thanks to xet7 !
2026-08-09 22:44:22 +03:00
Lauri Ojansivu 74dfacd76e CHANGELOG: a new Upcoming section for the amd64 provenance fix.
Its commit landed after "Bump versions for v10.78", so it did not ship
in v10.78. The entry had been written into that section anyway, because
the release was already cut by the time it was added; it moves out to
Upcoming, and the sentence in v10.78 that pointed at it says what the
other half was instead of pointing at an entry that is no longer there.

Also records #6580 (CHANGELOG.md is too big for GitHub to render or
blame) under TODO Later rather than leaving it looking unexamined.
Splitting the file by year was tried and abandoned: git records no move,
so a plain `git blame` on a per-year file credits every line to the
split and only `git blame -C` reaches the real history, which editors do
not pass by default. Trading working local blame on eleven years of
entries for a smaller file is the wrong way round, and reverting a split
does not undo it - the restored lines then blame to the revert - so the
note says what a real fix would have to do instead.

Thanks to mimZD and xet7 !
2026-08-09 22:28:22 +03:00
Lauri Ojansivu 058c74e9c8 CHANGELOG: the amd64 provenance script paths.
Thanks to xet7 !
2026-08-09 18:40:28 +03:00
Lauri Ojansivu e596805194 amd64: the provenance scripts were never found, only never complained.
The v10.78 amd64 build failed with exit 127 after the bundle was zipped
and checksummed:

    a95d331b…  wekan-10.78-amd64.zip
    bash: releases/record-provenance.sh: No such file or directory
    bash: releases/ferretdb-latest-tag.sh: No such file or directory

The step runs `cd .build` first, so nothing relative in it means what it
looks like it means. That is TWO bugs, and only the second one had ever
been visible:

The SCRIPT path. `bash releases/record-provenance.sh` resolves against
.build/, which has no releases/. It had printed "No such file or
directory" on every run since it was written, and the `|| true` on the
end swallowed it - so amd64 had never recorded any provenance at all,
and the only symptom was its absence from the release-notes table. The
missing-directory fix in the previous commit therefore fixed the half
that was visible and left the half that was not.

The OUTPUT path, fixed previously: `mkdir -p provenance` made
.build/provenance while upload-artifact looked at provenance/ from the
workspace root.

What turned it loud is that the FerretDB tag lookup added in that same
commit is an ASSIGNMENT. Under `set -e` - and every GitHub `run:` is
`bash -e` - `VAR="$(cmd)"` ends the step when cmd fails, so a line that
had been silently doing nothing for months became a hard 127 that failed
the whole amd64 build. Every path in that step is absolute now, and
every FERRET_TAG assignment across all seven sites ends `|| true`: the
bundle is the deliverable, and which version string reaches a markdown
cell is not worth failing a good build for.

Only amd64's step cds; the other six run from the workspace root, which
matches what the v10.77 run produced - they all uploaded provenance and
amd64 did not.

Two more checks in tests/provenanceTable.test.cjs, now 22. One walks
back from every provenance call to its step header and requires an
absolute path when a `cd` runs inside that step - which is the general
form of this bug and would have caught it. The other requires every
FERRET_TAG assignment to tolerate failure. Both verified to fail when
the v10.78 shapes are put back.

Thanks to xet7 !
2026-08-09 18:39:38 +03:00
341 changed files with 21319 additions and 1429 deletions

No files matched your search

+9 -4
View File
@@ -10,10 +10,15 @@
# half-hour emulated build, and no way for the variant image to drift from the
# release it claims to be.
#
# `workflow_dispatch` ONLY, on purpose. release-all.yml publishes wekanteam/wekan
# to Docker Hub, Quay and GHCR on every release; the variant images are published
# when the maintainer decides to, which is what this file exists for. Nothing
# here runs on a push, a tag or a schedule.
# A RELEASE NO LONGER NEEDS THIS. release-all.yml's docker job tags both variants
# on all three registries in its own `docker buildx build --push`, so every
# release publishes them with the release's own digests. What this file is for now
# is everything outside a release: publishing a variant for a version whose
# release predates that change, repairing a tag, or pointing a variant at a
# different version deliberately.
#
# `workflow_dispatch` ONLY, on purpose: nothing here runs on a push, a tag or a
# schedule.
#
# If a variant repository ever stops being identical to wekan/wekan, this
# retag becomes wrong: build that repository's own Dockerfile instead.
+106 -2
View File
@@ -10,7 +10,13 @@ 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, never touches Docker, snap, the website or the charts.
# tags, and never touches Docker, snap or the website.
#
# 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
# belongs to, so a release whose charts job did not run leaves a hole that the
# next release never goes back for - v10.80 is one of those, and it was not
# alone. See the charts job at the end.
#
# WHAT IT CAN BUILD, and why that is not everything. Only build-amd64 runs
# Meteor; every other bundle WeKan ships is that same bundle repacked. This job
@@ -279,9 +285,107 @@ jobs:
publish: true
only: ${{ needs.plan.outputs.flatpak }}
# ─── The Helm chart, for a release that never got one ───────────────────────
#
# Everything else here rebuilds a missing BUNDLE; this publishes a missing
# CHART, which goes missing the same way and for the same reason. A chart is
# written once, by release-charts.sh, during the release it belongs to, so a
# release whose charts job did not run has no chart and nothing ever goes back
# for it. That is not rare - 146 of the packages already on gh-pages had no
# index entry, and v10.80 has no package at all, because a cancelled
# build-mac-x64 cancelled its run before the charts job started.
#
# It is the one part of this workflow that PUSHES to another repository, so it
# is guarded twice: it does nothing unless a token that can push to
# wekan/charts is configured, and nothing if the chart for this version is
# already published. Re-publishing an existing version would re-tar the package
# and change the digest of a chart people have already pulled.
charts:
name: Helm chart
needs: plan
if: ${{ always() && needs.plan.outputs.version != '' }}
runs-on: ubuntu-latest
steps:
- name: Is a chart already published for this version?
id: check
env:
V: ${{ needs.plan.outputs.version }}
REPO_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
run: |
set -uo pipefail
if [ -z "${REPO_TOKEN:-}" ]; then
echo "::warning::No WEKAN_REPO_TOKEN, so the Helm chart cannot be published. Add a token that can push to wekan/charts to have this finish the chart too."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# The published index is the authority on what exists, not this
# checkout. An entry's own version is at EXACTLY four spaces; the
# mongodb subchart's version sits deeper and would match a loose grep.
curl -fsSL --retry 3 -o index.yaml https://wekan.github.io/charts/index.yaml || {
echo "::warning::Could not read the published chart index; not touching the charts repo."
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
# -F, a FIXED string: the dots in a version are regex wildcards
# otherwise, so "10.80" would also match a line reading 10x80. -x so it
# is the whole line, and the four-space indent is what makes it an
# entry's own version rather than the mongodb subchart's, which sits
# deeper.
if grep -qxF " version: ${V}.0" index.yaml; then
echo "::notice::v$V already has a published Helm chart (${V}.0); nothing to do."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "v$V has no published chart; publishing ${V}.0."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout wekan (for releases/release-charts.sh)
if: steps.check.outputs.skip != 'true'
uses: actions/checkout@v7
with:
path: wekan
- name: Checkout wekan/charts (main + gh-pages)
if: steps.check.outputs.skip != 'true'
uses: actions/checkout@v7
with:
repository: wekan/charts
token: ${{ secrets.WEKAN_REPO_TOKEN }}
fetch-depth: 0
path: charts
- name: Install Helm
if: steps.check.outputs.skip != 'true'
uses: azure/setup-helm@v5
- name: Publish the chart for this release
if: steps.check.outputs.skip != 'true'
env:
CHARTS_DIR: ${{ github.workspace }}/charts
run: |
set -euo pipefail
cd charts
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
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 }}"
- name: Job result
if: always()
run: |
case "${{ job.status }}" in
success) mark='OK ' ;;
failure) mark='FAILED ' ;;
cancelled) mark='CANCELLED' ;;
*) mark='${{ job.status }}' ;;
esac
echo "$mark charts" >> "$GITHUB_STEP_SUMMARY"
done:
name: what is still missing
needs: [plan, extra-arches, appimage, flatpak]
needs: [plan, extra-arches, appimage, flatpak, charts]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -2,7 +2,7 @@ accounts-base@3.3.0
accounts-express@1.0.0
accounts-oauth@1.4.7
accounts-password@3.3.1
aldeed:collection2@4.2.1
aldeed:collection2@4.2.2
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.0
@@ -87,7 +87,7 @@ reload@1.3.2
retry@1.1.1
reywood:publish-composite@1.9.0
routepolicy@1.1.2
rspack@1.2.0
rspack@1.2.1
service-configuration@1.3.5
session@1.2.3
sha@1.0.10
+110 -19
View File
@@ -2,7 +2,35 @@
Codex reads this file at the repo root before doing work here. Follow it.
## First: are you the maintainer or a contributor?
## First: who maintains this, and who is committing?
**WeKan, the `wekan/` repositories cloned under `.tools/`, and
[Secretchronicles/TSC](https://github.com/Secretchronicles/TSC) are all maintained by
Lauri Ojansivu (xet7) `<x@xet7.org>`** — [wekan/wekan](https://github.com/wekan/wekan),
[wekan/FerretDB](https://github.com/wekan/FerretDB),
[wekan/node-patches](https://github.com/wekan/node-patches),
[wekan/mongo-tools-patches](https://github.com/wekan/mongo-tools-patches) and TSC,
which is under the **Secretchronicles** organisation rather than **wekan** and is his
all the same — his GitHub profile, [xet7](https://github.com/xet7), says exactly that:
*"WeKan and TSC maintainer"*. Work done on the maintainer's behalf is committed as
**`Lauri Ojansivu <x@xet7.org>`** — that author, in every one of those repositories,
every time. Two rules follow from it and neither has an exception:
- **Never attribute a commit to an AI.** No `Co-Authored-By:` trailer, no "Generated
with", no assistant or model name — not in the commit message, not in a pull-request
body, not in the CHANGELOG. [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) is where this
comes from: *"For pull requests, mention only those participants that are
**human**."* A `Thanks to ... and xet7 !` line credits people — the issue reporter
and xet7 — never a tool.
- **If the git identity is missing or wrong in one of these checkouts, set it; do not
commit under something else.** The `.tools/` clones are made by `build.sh` inside
this checkout and can come up with no `user.name`/`user.email` of their own, which
would silently author a commit as whatever the machine's default is:
```
git -C .tools/<repo> config user.name 'Lauri Ojansivu'
git -C .tools/<repo> config user.email 'x@xet7.org'
```
Check the current git identity before committing or releasing:
@@ -10,16 +38,25 @@ Check the current git identity before committing or releasing:
git config user.name && git config user.email
```
- **Maintainer mode** — ONLY when the identity is exactly
`Lauri Ojansivu <x@xet7.org>` (name `Lauri Ojansivu`, email `x@xet7.org`). Then, and
only then: commit **directly to the current branch** as `Lauri Ojansivu <x@xet7.org>`
with no AI trailer and no pull request, and the **publishing / release steps** below
are available.
- **Contributor mode** — any other git identity. Then: do **not** commit directly to
the branch and do **not** run any release/publishing step. Make changes on a branch
and open a **pull request** for the maintainer to review. The "commit as Lauri
Ojansivu", "commit directly", and all release instructions below are **maintainer-only
and do not apply to you**.
- **Maintainer mode** — the identity is `Lauri Ojansivu <x@xet7.org>` (name
`Lauri Ojansivu`, email `x@xet7.org`), or it is unset in a checkout of one of the
repositories above, which means it is to be SET to that as above rather than worked
around. Then: commit **directly to the current branch** as
`Lauri Ojansivu <x@xet7.org>` with no AI trailer and no pull request, and the
**publishing / release steps** below are available.
- **Contributor mode** — the identity is somebody ELSE, in a fork or a clone of your
own. Then: do **not** commit directly to the branch and do **not** run any
release/publishing step. Make changes on a branch and open a **pull request** for
the maintainer to review, and keep that pull request free of AI attribution too.
The "commit as Lauri Ojansivu", "commit directly", and all release instructions
below are **maintainer-only and do not apply to you**.
Maintainer mode covers TSC as well: commit directly to its `devel` branch, no pull
request, same author and no AI attribution. What it does NOT bring along is WeKan's
house style — TSC keeps its own **GNU ChangeLog** `CHANGELOG` and its own release
process, because a project is read on its own terms (see the CHANGELOG section below).
The one repository under `.tools/` that is somebody else's is `sandstorm-io/sandstorm`,
cloned for reference only.
Everything below marked as maintainer-specific (committing directly, the exact commit
author, and the entire "Making a release" / publishing flow) applies only in maintainer
@@ -121,21 +158,75 @@ pushed to Transifex as if it were human.
"Co-Authored-By" or any other AI trailer, directly to the `main` branch of WeKan and
the `main-v1` branch of the FerretDB fork. **Do not make pull requests.** (Contributors
do the opposite: work on a branch and open a pull request — see the top section.)
- Lauri Ojansivu (xet7) maintains WeKan (https://wekan.fi) and the FerretDB v1 fork.
This is the same rule as the top section, restated where the release work is: one
author, `Lauri Ojansivu <x@xet7.org>`, and no AI attribution anywhere.
- Lauri Ojansivu (xet7) maintains WeKan (https://wekan.fi), the FerretDB v1 fork, and
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.
- `../w/wekan.fi` — the WeKan website.
- `FerretDB` subdirectory (when present) — https://github.com/wekan/FerretDB, its own
`CHANGELOG.md`. FerretDB `.go` files must contain **no** application-specific names
(say "the client" / "a Meteor 3 driver" / a bare `#NNNN`); its `CHANGELOG.md` may
use `wekan/wekan#NNNN`.
- `sandstorm` subdirectory (when present) — https://github.com/sandstorm-io/sandstorm.
- `.tools/` — everything that is NOT part of this repository but is needed to
build, test and release it, in ONE directory that `.gitignore` and
`.meteorignore` already exclude (each used to need its own ignore entry at
the repo root), so nothing in it can reach a commit or a Meteor rebuild. Two
different kinds of thing live there, and the difference matters:
**Companion git repositories.** Separate repositories with their own history,
branches, changelog and release flow — a commit here is never a commit there.
`build.sh`'s `ensure_tool_repo` clones one on demand (SSH first, HTTPS
second), so a fresh checkout needs no manual setup:
| Path | Repository | Branch | What it is |
| --- | --- | --- | --- |
| `.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/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 |
**Unpacked toolchains and caches.** Downloads, not repositories — put there by
the sandbox instructions in `docs/Security/Sandboxes/vscode/README.md`, deleted
and re-fetched freely, never committed anywhere:
`node-v<version>-linux-<arch>/` (the Node.js the test suites are run with),
`go/` with `gopath/`, `gomodcache/` and `gocache/` (FerretDB's Go builds),
`.meteor/` when `HOME` is pointed at `.tools`, and the `TSC*` AppImage.
- **Do NOT add a `CLAUDE.md` or an `AGENTS.md` to any repository under
`.tools/`.** node-patches and mongo-tools-patches each had a pair and they were
REMOVED on purpose: the rules are the same for every one of these repositories,
and a second copy of a rule drifts from the first. THIS file, and its `AGENTS.md`
twin beside it, are where they live for all of them. Something true of only one
of those repositories goes in that repository's own `README.md` or `docs/`,
never in a new instruction file — and if you find one there, remove it rather
than updating it.
- `.tools/FerretDB` specifics — FerretDB `.go` files must contain **no**
application-specific names (say "the client" / "a Meteor 3 driver" / a bare
`#NNNN`); its `CHANGELOG.md` may use `wekan/wekan#NNNN`.
### CHANGELOG
- **Every repository writes its changelog in the format that repository's own file
already uses.** Open its changelog, read the entries above the place you are
adding one, and match them — never import another project's shape into it. The
five that come up here:
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Platforms`, `# TODO Later`, 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.` |
| `Secretchronicles/TSC` | `CHANGELOG` (no extension) | **GNU ChangeLog** format: a `YYYY-MM-DD Name <email>` header line, then TAB-indented `* Version …` / `* Fix: …` / `* Misc: …` entries, wrapped and continued with further tabs, each ending `(by Name)` |
The reason is the reader, not consistency for its own sake: a FerretDB release is
read beside upstream FerretDB's releases, and a TSC entry beside a decade of GNU
ChangeLog entries. A WeKan-shaped `<details>` block in either would be the odd one
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.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`). Do **not** hand-edit
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
`package.json` or any other version reference — the release workflow bumps those.
- **The file's shape, top to bottom** — keep it exactly as it is now:
1. `# Platforms` — the line `Newest WeKan at these platforms:` and the Install /
@@ -342,7 +433,7 @@ All publishing / release steps below are maintainer-only. Contributors never run
version reference, then the release jobs tag `v<new>` and publish the GitHub Release).
An explicit `oldversion newversion` pair still overrides. Adding entries under Upcoming
is the only hand step.
- FerretDB: run `./build.sh release-ferretdb` from the `FerretDB` subdirectory (no
- FerretDB: run `./build.sh release-ferretdb` from `.tools/FerretDB` (no
version). It renames `## Upcoming FerretDB release` to the next version with the
correct git-tag link, commits + tags + pushes, then triggers `release-all.yml` (which
in turn triggers `docker.yml` for the multi-arch image). It refuses to re-release an
+2610 -12
View File
File diff suppressed because it is too large. Load diff
+109 -23
View File
@@ -2,7 +2,35 @@
Claude Code reads this file at the repo root before doing work here. Follow it.
## First: are you the maintainer or a contributor?
## First: who maintains this, and who is committing?
**WeKan, the `wekan/` repositories cloned under `.tools/`, and
[Secretchronicles/TSC](https://github.com/Secretchronicles/TSC) are all maintained by
Lauri Ojansivu (xet7) `<x@xet7.org>`** — [wekan/wekan](https://github.com/wekan/wekan),
[wekan/FerretDB](https://github.com/wekan/FerretDB),
[wekan/node-patches](https://github.com/wekan/node-patches),
[wekan/mongo-tools-patches](https://github.com/wekan/mongo-tools-patches) and TSC,
which is under the **Secretchronicles** organisation rather than **wekan** and is his
all the same — his GitHub profile, [xet7](https://github.com/xet7), says exactly that:
*"WeKan and TSC maintainer"*. Work done on the maintainer's behalf is committed as
**`Lauri Ojansivu <x@xet7.org>`** — that author, in every one of those repositories,
every time. Two rules follow from it and neither has an exception:
- **Never attribute a commit to an AI.** No `Co-Authored-By:` trailer, no "Generated
with", no assistant or model name — not in the commit message, not in a pull-request
body, not in the CHANGELOG. [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) is where this
comes from: *"For pull requests, mention only those participants that are
**human**."* A `Thanks to ... and xet7 !` line credits people — the issue reporter
and xet7 — never a tool.
- **If the git identity is missing or wrong in one of these checkouts, set it; do not
commit under something else.** The `.tools/` clones are made by `build.sh` inside
this checkout and can come up with no `user.name`/`user.email` of their own, which
would silently author a commit as whatever the machine's default is:
```
git -C .tools/<repo> config user.name 'Lauri Ojansivu'
git -C .tools/<repo> config user.email 'x@xet7.org'
```
Check the current git identity before committing or releasing:
@@ -10,16 +38,25 @@ Check the current git identity before committing or releasing:
git config user.name && git config user.email
```
- **Maintainer mode** — ONLY when the identity is exactly
`Lauri Ojansivu <x@xet7.org>` (name `Lauri Ojansivu`, email `x@xet7.org`). Then, and
only then: commit **directly to the current branch** as `Lauri Ojansivu <x@xet7.org>`
with no AI trailer and no pull request, and the **publishing / release steps** below
are available.
- **Contributor mode** — any other git identity. Then: do **not** commit directly to
the branch and do **not** run any release/publishing step. Make changes on a branch
and open a **pull request** for the maintainer to review. The "commit as Lauri
Ojansivu", "commit directly", and all release instructions below are **maintainer-only
and do not apply to you**.
- **Maintainer mode** — the identity is `Lauri Ojansivu <x@xet7.org>` (name
`Lauri Ojansivu`, email `x@xet7.org`), or it is unset in a checkout of one of the
repositories above, which means it is to be SET to that as above rather than worked
around. Then: commit **directly to the current branch** as
`Lauri Ojansivu <x@xet7.org>` with no AI trailer and no pull request, and the
**publishing / release steps** below are available.
- **Contributor mode** — the identity is somebody ELSE, in a fork or a clone of your
own. Then: do **not** commit directly to the branch and do **not** run any
release/publishing step. Make changes on a branch and open a **pull request** for
the maintainer to review, and keep that pull request free of AI attribution too.
The "commit as Lauri Ojansivu", "commit directly", and all release instructions
below are **maintainer-only and do not apply to you**.
Maintainer mode covers TSC as well: commit directly to its `devel` branch, no pull
request, same author and no AI attribution. What it does NOT bring along is WeKan's
house style — TSC keeps its own **GNU ChangeLog** `CHANGELOG` and its own release
process, because a project is read on its own terms (see the CHANGELOG section below).
The one repository under `.tools/` that is somebody else's is `sandstorm-io/sandstorm`,
cloned for reference only.
Everything below marked as maintainer-specific (committing directly, the exact commit
author, and the entire "Making a release" / publishing flow) applies only in maintainer
@@ -121,26 +158,75 @@ pushed to Transifex as if it were human.
"Co-Authored-By" or any other AI trailer, directly to the `main` branch of WeKan and
the `main-v1` branch of the FerretDB fork. **Do not make pull requests.** (Contributors
do the opposite: work on a branch and open a pull request — see the top section.)
- Lauri Ojansivu (xet7) maintains WeKan (https://wekan.fi) and the FerretDB v1 fork.
This is the same rule as the top section, restated where the release work is: one
author, `Lauri Ojansivu <x@xet7.org>`, and no AI attribution anywhere.
- Lauri Ojansivu (xet7) maintains WeKan (https://wekan.fi), the FerretDB v1 fork, and
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.
- `../w/wekan.fi` — the WeKan website.
- `.tools/` — companion repos cloned INSIDE this checkout, in one directory
that `.gitignore` and `.meteorignore` already exclude (they used to be one
ignored subdirectory each at the repo root). `build.sh`'s `ensure_tool_repo`
clones one on demand, so a fresh checkout needs no manual setup:
`.tools/FerretDB`, `.tools/node-patches`, `.tools/mongo-tools-patches`.
- `.tools/FerretDB` (cloned on demand) — https://github.com/wekan/FerretDB, its own
`CHANGELOG.md`. FerretDB `.go` files must contain **no** application-specific names
(say "the client" / "a Meteor 3 driver" / a bare `#NNNN`); its `CHANGELOG.md` may
use `wekan/wekan#NNNN`.
- `.tools/sandstorm` (when present) — https://github.com/sandstorm-io/sandstorm.
- `.tools/` — everything that is NOT part of this repository but is needed to
build, test and release it, in ONE directory that `.gitignore` and
`.meteorignore` already exclude (each used to need its own ignore entry at
the repo root), so nothing in it can reach a commit or a Meteor rebuild. Two
different kinds of thing live there, and the difference matters:
**Companion git repositories.** Separate repositories with their own history,
branches, changelog and release flow — a commit here is never a commit there.
`build.sh`'s `ensure_tool_repo` clones one on demand (SSH first, HTTPS
second), so a fresh checkout needs no manual setup:
| Path | Repository | Branch | What it is |
| --- | --- | --- | --- |
| `.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/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 |
**Unpacked toolchains and caches.** Downloads, not repositories — put there by
the sandbox instructions in `docs/Security/Sandboxes/vscode/README.md`, deleted
and re-fetched freely, never committed anywhere:
`node-v<version>-linux-<arch>/` (the Node.js the test suites are run with),
`go/` with `gopath/`, `gomodcache/` and `gocache/` (FerretDB's Go builds),
`.meteor/` when `HOME` is pointed at `.tools`, and the `TSC*` AppImage.
- **Do NOT add a `CLAUDE.md` or an `AGENTS.md` to any repository under
`.tools/`.** node-patches and mongo-tools-patches each had a pair and they were
REMOVED on purpose: the rules are the same for every one of these repositories,
and a second copy of a rule drifts from the first. THIS file, and its `AGENTS.md`
twin beside it, are where they live for all of them. Something true of only one
of those repositories goes in that repository's own `README.md` or `docs/`,
never in a new instruction file — and if you find one there, remove it rather
than updating it.
- `.tools/FerretDB` specifics — FerretDB `.go` files must contain **no**
application-specific names (say "the client" / "a Meteor 3 driver" / a bare
`#NNNN`); its `CHANGELOG.md` may use `wekan/wekan#NNNN`.
### CHANGELOG
- **Every repository writes its changelog in the format that repository's own file
already uses.** Open its changelog, read the entries above the place you are
adding one, and match them — never import another project's shape into it. The
five that come up here:
| Repository | File | Format |
| --- | --- | --- |
| `wekan/wekan` | `CHANGELOG.md` | the WeKan format this section describes: `# Platforms`, `# TODO Later`, 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.` |
| `Secretchronicles/TSC` | `CHANGELOG` (no extension) | **GNU ChangeLog** format: a `YYYY-MM-DD Name <email>` header line, then TAB-indented `* Version …` / `* Fix: …` / `* Misc: …` entries, wrapped and continued with further tabs, each ending `(by Name)` |
The reason is the reader, not consistency for its own sake: a FerretDB release is
read beside upstream FerretDB's releases, and a TSC entry beside a decade of GNU
ChangeLog entries. A WeKan-shaped `<details>` block in either would be the odd one
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.
- During development, add entries under a new `# Upcoming WeKan ® release` section above
the newest release (FerretDB uses `## Upcoming FerretDB release`). Do **not** hand-edit
the newest release (FerretDB uses `## Upcoming FerretDB release`; the patch repos use
`# Upcoming <repo> release`). Do **not** hand-edit
`package.json` or any other version reference — the release workflow bumps those.
- **The file's shape, top to bottom** — keep it exactly as it is now:
1. `# Platforms` — the line `Newest WeKan at these platforms:` and the Install /
+25 -2
View File
@@ -3,6 +3,14 @@
# arch this image targets - amd64, arm64, 386, arm/v7, ppc64le, riscv64, s390x -
# so ONE base covers them all, and it is the same base WeKan's per-arch .zip
# bundles are already built in (releases/... build-extra-arches).
#
# The ONE it does not ship is arm/v6: debian:trixie's manifest list has arm/v5
# and arm/v7 and nothing between them. That matters because containerd treats a
# LOWER ARM variant as compatible, so a linux/arm/v6 request does not fail - it
# quietly resolves to the arm/v5 (armel, SOFT-float) image, whose loader and
# 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.
FROM debian:trixie
LABEL maintainer="wekan"
LABEL org.opencontainers.image.ref.name="debian"
@@ -12,7 +20,7 @@ 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=10.78
ARG VERSION=10.87
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
@@ -210,7 +218,22 @@ case "${TARGETARCH}" in
"s390x") WEKAN_ARCH="s390x" ;;
"riscv64") WEKAN_ARCH="riscv64" ;;
"386") WEKAN_ARCH="i386" ;;
"arm") WEKAN_ARCH="armhf" ;;
# 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
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v10.78.0"
appVersion: "v10.87.0"
files:
userUploads:
- README.md
+6
View File
@@ -253,6 +253,8 @@ echo 19^) Delete a git tag, locally and on the remote
echo 20^) Move the 'stable' tag to HEAD
echo 21^) Release the wekan-ondra / wekan-gantt-gpl variants, part 1
echo 22^) Release the wekan-ondra / wekan-gantt-gpl variants, part 2
echo 23^) Report ^(or repair^) the Helm chart index for past releases
echo 24^) Rebuild the Helm index.yaml from the chart packages
set "choice="
set /p "choice=Choose: "
if "%choice%"=="1" call :rel_run "releases/release-all.sh" ""
@@ -277,6 +279,8 @@ if "%choice%"=="19" call :rel_run "releases/delete-tag.sh" "Version tag, e.g. v1
if "%choice%"=="20" call :rel_cmd "git tag --force stable HEAD && git push --tags --force && git push --follow-tags" ""
if "%choice%"=="21" call :rel_run "releases/release-ondra-1.sh" ""
if "%choice%"=="22" call :rel_run "releases/release-ondra-2.sh" ""
if "%choice%"=="23" call :rel_run "releases/backfill-charts.sh" ""
if "%choice%"=="24" call :rel_run "releases/reindex-charts.py" ""
if "%choice%"=="0" goto menu_releases
goto rel_release
@@ -628,6 +632,8 @@ if /I "%K%"=="release-ln" (set "CMD=bash releases/release-ln.sh" ^& goto cli_go)
if /I "%K%"=="release-x2" (set "CMD=bash releases/release-x2.sh" ^& goto cli_go)
if /I "%K%"=="release-cleanup" (set "CMD=bash releases/release-cleanup.sh" ^& goto cli_go)
if /I "%K%"=="release-charts" (set "CMD=bash releases/release-charts.sh" ^& goto cli_go)
if /I "%K%"=="backfill-charts" (set "CMD=bash releases/backfill-charts.sh" ^& goto cli_go)
if /I "%K%"=="reindex-charts" (set "CMD=python3 releases/reindex-charts.py" ^& goto cli_go)
if /I "%K%"=="release-website" (set "CMD=bash releases/release-website.sh" ^& goto cli_go)
if /I "%K%"=="npm-publish" (set "CMD=bash releases/npm-publish.sh" ^& goto cli_go)
if /I "%K%"=="test-download-urls" (set "CMD=bash releases/test-download-urls.sh" ^& goto cli_go)
+2
View File
@@ -1867,6 +1867,8 @@ RELEASE_SCRIPTS=( "Release|Release ALL platforms: push CHANGELOG, trigger releas
"Release|Move the 'stable' tag to HEAD|!git tag --force stable HEAD && git push --tags --force && git push --follow-tags|||stable-tag"
"Release|Release the wekan-ondra / wekan-gantt-gpl variants, part 1|releases/release-ondra-1.sh|||"
"Release|Release the wekan-ondra / wekan-gantt-gpl variants, part 2|releases/release-ondra-2.sh|||"
"Release|Report (or repair) the Helm chart index for past releases|releases/backfill-charts.sh|||"
"Release|Rebuild the Helm index.yaml from the chart packages|releases/reindex-charts.py|||"
"Snap|Build the snap from snapcraft.yaml|releases/snap-build.sh|||"
"Snap|Install the locally built .snap|releases/snap-install.sh|||"
"Snap|Push one .snap to the Snap Store|releases/snap-push-to-store.sh|Path to the .snap file||"
+37 -21
View File
@@ -6,6 +6,7 @@ import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import { TAPi18n } from '/imports/i18n';
import { Utils } from '/client/lib/utils';
import { getSidebarInstance } from '/client/features/sidebar/service';
import { activityCardLinkData } from '/models/lib/activityCardLink';
// #6480/#6481: 500 was far more than the sidebar/card activity feed ever shows
// at once, and with FerretDB (no oplog) every live cursor is re-run on a timer —
@@ -150,23 +151,23 @@ Template.activity.helpers({
return TAPi18n.__('this-card');
}
else if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
return createCardLink(this.activity, this.activity.card(), null);
}
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
return createCardLink(this.activity, this.activity.card(), this.activity.board().title);
}
return createCardLink(this.activity.card(), null);
return createCardLink(this.activity, this.activity.card(), null);
},
cardLink() {
const currentBoardId = Session.get('currentBoard');
if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
return createCardLink(this.activity, this.activity.card(), null);
}
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
return createCardLink(this.activity, this.activity.card(), this.activity.board().title);
}
return createCardLink(this.activity.card(), null);
return createCardLink(this.activity, this.activity.card(), null);
},
receivedDate() {
@@ -365,21 +366,36 @@ Template.commentReactions.helpers({
}
})
function createCardLink(card, board) {
if (!card) return '';
let text = card.title;
if (board) text = `${board} > ` + text;
return (
card &&
Blaze.toHTML(
HTML.A(
{
href: card.originRelativeUrl(),
class: 'action-card',
},
sanitizeHTML(text),
),
)
// #3144: the card is not always here. An ARCHIVED card is not published to the
// client, so `activity.card()` returned nothing and every sentence that named it
// was rendered around a hole - "Activities for archived card displayed as
// undefined on board settings".
//
// The activity itself recorded the title in most cases (`cardTitle`), and a card
// URL can be built from the ids it carries, so the link survives the card not
// being here. models/lib/activityCardLink.js decides what to show;
// this turns it into the anchor, and marks an archived card the way the reporter
// asked for: "{{ card title }} [archived]".
function createCardLink(activity, card, boardTitle) {
const link = activityCardLinkData(activity, card, card ? card.board && card.board() : null);
if (!link) {
// Nothing named the card - an old activity from before the titles were
// recorded, on a card this client cannot see. Say "this card" rather than
// leaving a gap in the middle of a sentence.
return sanitizeHTML(TAPi18n.__('this-card'));
}
let text = link.title;
if (link.archived) text = `${text} [${TAPi18n.__('archived')}]`;
if (boardTitle) text = `${boardTitle} > ` + text;
if (!link.url) return sanitizeHTML(text);
return Blaze.toHTML(
HTML.A(
{
href: link.url,
class: 'action-card',
},
sanitizeHTML(text),
),
);
}
+306
View File
@@ -1457,6 +1457,312 @@ THEME - Corteza
/* === END Corteza THEME === */
/* ===============
THEME - Apple Glass Pastel
=================*/
.board-color-appleglasspastel {
background:
radial-gradient(60% 50% at 85% 5%, rgba(255, 200, 220, 0.55), transparent 60%),
radial-gradient(55% 50% at 5% 25%, rgba(180, 215, 255, 0.55), transparent 65%),
radial-gradient(55% 50% at 95% 95%, rgba(200, 230, 255, 0.5), transparent 60%),
radial-gradient(70% 60% at 0% 100%, rgba(230, 215, 255, 0.5), transparent 65%),
linear-gradient(180deg, #f6f7fb 0%, #eef0f7 100%);
color: #111827;
}
.board-color-appleglasspastel#header,
.board-color-appleglasspastel#header-quick-access,
.board-color-appleglasspastel.sk-spinner div,
.board-color-appleglasspastel .setting-content .content-body .side-menu ul li.active,
.board-color-appleglasspastel .setting-content .content-body .side-menu ul li.active:hover {
background: #2563eb;
color: #fff;
}
.board-backgrounds-list .board-color-appleglasspastel.background-box,
.public-board-row.board-color-appleglasspastel,
.board-list .board-color-appleglasspastel a {
background:
radial-gradient(80% 70% at 80% 0%, rgba(255, 200, 220, 0.7), transparent 62%),
radial-gradient(70% 80% at 0% 15%, rgba(180, 215, 255, 0.75), transparent 66%),
radial-gradient(75% 70% at 100% 100%, rgba(230, 215, 255, 0.7), transparent 62%),
linear-gradient(180deg, #f6f7fb 0%, #eef0f7 100%);
color: #111827 !important;
}
.board-color-appleglasspastel#header-quick-access {
border-bottom: 1px solid rgba(255, 255, 255, 0.55);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.board-color-appleglasspastel#header #header-main-bar,
.board-color-appleglasspastel#header-quick-access,
.board-color-appleglasspastel .boards-left-menu .menu-item.active a,
.board-color-appleglasspastel .boards-left-menu .menu-item.active a:hover {
background: #2563eb;
}
.board-color-appleglasspastel#header ul li.current,
.board-color-appleglasspastel#header-quick-access ul li.current {
border-bottom: 4px solid #dbeafe;
}
.board-color-appleglasspastel#header-quick-access #header-new-board-icon,
.board-color-appleglasspastel#header-quick-access #header-user-bar,
.board-color-appleglasspastel#header-quick-access ul li,
.board-color-appleglasspastel#header-quick-access a,
.board-color-appleglasspastel#header .board-header-btn,
.board-color-appleglasspastel#header .board-header-btn i {
color: #fff;
}
.board-color-appleglasspastel#header ul li:hover,
.board-color-appleglasspastel#header-quick-access ul li:hover,
.board-color-appleglasspastel .board-header-btn:hover {
background: rgba(255, 255, 255, 0.16);
border-radius: 10px;
}
.board-color-appleglasspastel#header #header-main-bar .board-header-btn.emphasis {
background: #1e3a8a;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.12), 0 1px 2px rgba(0,0,0,0.04);
}
.board-color-appleglasspastel#header #header-main-bar .board-header-btn.emphasis:hover,
.board-color-appleglasspastel#header #header-main-bar .board-header-btn.emphasis .board-header-btn-close {
background: #1d4ed8;
}
.board-color-appleglasspastel#header #header-main-bar .board-header-btn.emphasis:hover .board-header-btn-close {
background: #1e3a8a;
}
.board-color-appleglasspastel.board-wrapper {
background:
radial-gradient(60% 50% at 85% 5%, rgba(255, 200, 220, 0.55), transparent 60%),
radial-gradient(55% 50% at 5% 25%, rgba(180, 215, 255, 0.55), transparent 65%),
radial-gradient(55% 50% at 95% 95%, rgba(200, 230, 255, 0.5), transparent 60%),
radial-gradient(70% 60% at 0% 100%, rgba(230, 215, 255, 0.5), transparent 65%),
linear-gradient(180deg, #f6f7fb 0%, #eef0f7 100%);
}
.board-color-appleglasspastel .board-canvas {
scrollbar-color: rgba(37, 99, 235, 0.35) rgba(255, 255, 255, 0);
scrollbar-width: thin;
}
.board-color-appleglasspastel .swimlane {
background: transparent;
}
.board-color-appleglasspastel .swimlane .swimlane-header-wrap {
background: rgba(255, 255, 255, 0.45);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border-bottom: 1px solid rgba(255, 255, 255, 0.55);
}
.board-color-appleglasspastel .swimlane-header {
color: #0f172a;
font-weight: 600;
}
.board-color-appleglasspastel .list {
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.55);
border-inline-start: none;
border-radius: 18px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.06);
color: #111827;
margin: 10px 0;
padding-bottom: 8px;
}
.board-color-appleglasspastel .list-header {
background: rgba(255, 255, 255, 0.45);
border-bottom: 1px solid rgba(229, 231, 235, 0.75);
border-radius: 18px 18px 0 0;
}
.board-color-appleglasspastel .list-header .list-header-name,
.board-color-appleglasspastel .list-header .viewer,
.board-color-appleglasspastel .minicard-title {
color: #0f172a;
}
.board-color-appleglasspastel .list-body {
margin-top: 8px;
padding: 11px;
scrollbar-color: rgba(37, 99, 235, 0.35) rgba(255, 255, 255, 0);
scrollbar-width: thin;
}
.board-color-appleglasspastel .minicard,
.board-color-appleglasspastel .card-details,
.board-color-appleglasspastel.pop-over,
.board-color-appleglasspastel .sidebar {
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.55);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
color: #111827;
}
.board-color-appleglasspastel .minicard {
border-radius: 12px;
padding: 10px 10px 4px 10px;
}
.board-color-appleglasspastel .minicard:hover:not(.minicard-composer),
.board-color-appleglasspastel .is-selected .minicard,
.board-color-appleglasspastel .draggable-hover-card .minicard {
background: rgba(239, 246, 255, 0.88);
}
.board-color-appleglasspastel .is-selected .minicard {
border-inline-start: 3px solid #2563eb;
}
.board-color-appleglasspastel .card-details {
border-radius: 18px;
}
.board-color-appleglasspastel .card-details-header {
background: rgba(239, 246, 255, 0.78);
border-bottom: 1px solid rgba(229, 231, 235, 0.75);
color: #0f172a;
}
.board-color-appleglasspastel .card-details-item-title,
.board-color-appleglasspastel .card-details-left .viewer p,
.board-color-appleglasspastel .activity-desc .activity-member {
color: #111827;
}
.board-color-appleglasspastel .quiet,
.board-color-appleglasspastel .quiet a,
.board-color-appleglasspastel .activity-desc,
.board-color-appleglasspastel .sidebar-list-item-description {
color: #6b7280;
}
.board-color-appleglasspastel .list.list-composer,
.board-color-appleglasspastel .list-body .open-minicard-composer,
.board-color-appleglasspastel .list.list-composer .open-list-composer {
background: rgba(255, 255, 255, 0.45);
border-radius: 12px;
color: #6b7280;
}
.board-color-appleglasspastel .list-body .open-minicard-composer:hover,
.board-color-appleglasspastel .list.list-composer .open-list-composer:hover,
.board-color-appleglasspastel.pop-over .pop-over-list li a:not(.disabled):hover,
.board-color-appleglasspastel .sidebar .sidebar-content .sidebar-btn:hover,
.board-color-appleglasspastel .sidebar-list li a:hover {
background: rgba(219, 234, 254, 0.78);
color: #1e3a8a;
}
.board-color-appleglasspastel button[type=submit].primary,
.board-color-appleglasspastel input[type=submit].primary,
.board-color-appleglasspastel button.primary,
.board-color-appleglasspastel input.primary,
.board-color-appleglasspastel .sidebar .sidebar-content .sidebar-btn {
background: #2563eb;
border: 0;
border-radius: 999px;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.12), 0 1px 2px rgba(0,0,0,0.04);
color: #fff;
}
.board-color-appleglasspastel button[type="submit"].primary:hover,
.board-color-appleglasspastel input[type="submit"].primary:hover,
.board-color-appleglasspastel button.primary:hover,
.board-color-appleglasspastel input.primary:hover {
background: #1d4ed8;
color: #fff;
}
.board-color-appleglasspastel input[type="email"],
.board-color-appleglasspastel input[type="password"],
.board-color-appleglasspastel input[type="text"],
.board-color-appleglasspastel textarea,
.board-color-appleglasspastel select {
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(229, 231, 235, 0.9);
border-radius: 12px;
color: #111827;
}
.board-color-appleglasspastel input:focus,
.board-color-appleglasspastel textarea:focus,
.board-color-appleglasspastel select:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.board-color-appleglasspastel .materialCheckBox.is-checked {
border-bottom: 2px solid #2563eb;
border-inline-end: 2px solid #2563eb;
}
.board-color-appleglasspastel .checklist-progress-bar {
background-color: #dbeafe !important;
}
.board-color-appleglasspastel .checklist-progress-bar .checklist-progress {
background-color: #2563eb !important;
}
.board-color-appleglasspastel .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #eff6ff;
}
.board-color-appleglasspastel .is-multiselection-active .multi-selection-checkbox:not(.is-checked) + .minicard:hover:not(.minicard-composer) {
background: rgba(255, 255, 255, 0.88);
}
.board-color-appleglasspastel .toggle-label:after {
background-color: #2563eb;
}
.board-color-appleglasspastel .toggle-switch:checked ~ .toggle-label {
background-color: #93c5fd;
}
.board-color-appleglasspastel .toggle-switch:checked ~ .toggle-label:after {
background-color: #2563eb;
}
.board-color-appleglasspastel .card-date.current,
.board-color-appleglasspastel .minicard .current {
background: #1f7d38;
color: #fff !important;
}
.board-color-appleglasspastel .card-date.due,
.board-color-appleglasspastel .minicard .due {
background: #dc3545;
color: #fff !important;
}
.board-color-appleglasspastel .sidebar .sidebar-content {
scrollbar-color: rgba(37, 99, 235, 0.35) rgba(255, 255, 255, 0);
}
@media screen and (max-width: 800px) {
.board-color-appleglasspastel.pop-over .header {
background: #2563eb;
color: #fff;
}
}
/* === END Apple Glass Pastel THEME === */
/* ===============
THEME - Clear Blue
=================*/
+59 -4
View File
@@ -1193,7 +1193,8 @@
(e.g. a laptop in mobile mode) gets more columns instead of two stretched ones. */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(46%, 200px), 1fr));
gap: 8px;
/* The same single spacing value as the phone grid above, in both directions. */
gap: 10px;
/* #6488: sized by the column it lives in (the flex chain in the phone block
below), not by the viewport minus a guess at the bars above it. */
flex: 1 1 auto;
@@ -1210,7 +1211,8 @@
width: auto; /* the grid controls the column width */
float: none;
display: block;
margin-bottom: 0.5rem;
/* The grid gap is the spacing; this added a second one, downwards only. */
margin-bottom: 0;
/* NO handle reserve here. The drag handle is positioned INSIDE the tile, and
the tile already keeps its text clear of it - so 32px here was a second,
invisible reserve on top of that one. On a phone the tile is only ~97px
@@ -1340,18 +1342,42 @@
.board-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
/* ONE spacing value, in both directions. It used to be an 8px gap PLUS a
`margin-bottom: 0.5rem` on each li, so the tiles were 16px apart vertically
and 8px apart horizontally - which reads as "the icons are too close
together" side to side even though there is space between the rows. The
margin is gone (below) and the gap is the whole answer. */
gap: 10px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 0 0.25rem;
margin: 0;
/* #6488. Without these a grid's rows default to `align-content: stretch` and
its items to `align-items: stretch`, so when the list is SHORTER than the
column it is given - one or two rows of boards - the leftover height is
divided among the rows and each tile is grown to fill it. A board tile
paints its board colour over the whole cell, so a single row of boards
became one band several times the height of a normal icon: "some board
icons are 4x normal height or more".
This was fixed once, but only on `.board-list.mobile-view` - the class the
template sets from Utils.isMiniScreen(). A narrow WINDOW that is not a mini
screen (a desktop browser resized under 800px, a tablet in desktop mode)
matches this media query, becomes a grid here, and never got the fix. It
belongs on the rule that creates the grid. */
align-content: start;
align-items: start;
}
.board-list li {
width: auto; /* the grid controls the column width */
float: none;
display: block;
margin-bottom: 0.5rem;
/* The grid gap spaces the tiles; a margin here spaced them a second time,
and only downwards. */
margin-bottom: 0;
/* ...and the tile is its own height, not the height of a stretched cell. */
align-self: start;
height: auto;
/* Same as `.board-list.mobile-view li` above: the tile keeps its own text
clear of the handle, so reserving the width a second time out here only
narrowed the title. */
@@ -1661,6 +1687,35 @@
flex: 0 0 auto;
}
/* ...and so does the pane's heading. Without this it is a flex item with the
default `flex-shrink: 1`, so in a column too short for its contents the
TITLE is what gets squeezed instead of the scroller below it growing a
scrollbar. */
.boards-right-grid > .admin-pane-title {
flex: 0 0 auto;
}
/* #6488. The right column has TWO branches, not one: the board icons, and -
when the view is Table - `+tablePage`, which is a `.table-page` (see
boardsList.jade). Only the icons were ever given a scroller, so in Table view
the table simply grew past the bottom of the column, and `#content .wrapper`
is `overflow: hidden`, so the rows past the fold were clipped with no way to
reach them. Every section of All Boards can be shown as a table - Starred,
Templates, Remaining, Workspaces, Archive - so that was the whole page in
one of its two views.
The same shape as the board list: take what the column has left, be allowed
to be shorter than the content, and scroll inside. The table is also WIDER
than a phone, so this one scrolls in both directions. */
.boards-right-grid > .table-page {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
}
/* The left menu (Starred / Templates / Remaining / Workspaces / Add board) is its
own vertical scroll area, so when it is taller than the screen you can drag-scroll
down to its lower items — the board icons already scroll in their own column on the
+30
View File
@@ -1146,3 +1146,33 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
user-select: none;
margin: 8px 0;
}
/* #1591: foldable feature groups on the opened card.
The title is the handle. Everything after it inside the section is what folds
away - a sibling rule rather than "hide .card-details-item-content", because
the sections are not uniform inside: all sixteen start with an
h3.card-details-item-title, but only three wrap what follows in a content
element. Hiding the siblings works whatever a section puts there. */
.card-details-item > .card-details-item-title {
cursor: pointer;
user-select: none;
}
/* The caret, drawn on the title rather than added to sixteen templates. It
points down when open and right when folded, like the checklist one. */
.card-details-item > .card-details-item-title::after {
content: "\f0d7"; /* fa-caret-down */
font-family: FontAwesome;
font-size: 12px;
color: #808080;
margin-inline-start: 6px;
vertical-align: middle;
}
.card-details-item.is-collapsed > .card-details-item-title::after {
content: "\f0da"; /* fa-caret-right */
}
.card-details-item.is-collapsed > *:not(.card-details-item-title) {
display: none;
}
+62
View File
@@ -513,6 +513,31 @@ Template.cardDetails.onRendered(function () {
// below throws "Can't select in removed DomRange", so bail out early.
if (this.view && this.view.isDestroyed) return;
// #1591: re-apply the sections this user folded. The click handler toggles the
// class as well as storing it, but on a fresh render - reopening the card, a
// reload - only the stored state exists, and the title is plain markup that
// nothing re-renders from the profile.
this.autorun(() => {
const user = ReactiveCache.getCurrentUser();
const card = Template.currentData();
if (!user || !card || !card._id) return;
// Read inside the autorun so a fold made in another tab arrives here too.
const stored = (user.profile && user.profile.collapsedCardSections) || {};
const forCard = stored[card._id] || {};
Meteor.defer(() => {
if (this.view && this.view.isDestroyed) return;
this.$('.card-details-item').each((index, item) => {
const named = Array.from(item.classList).find(
c => c.startsWith('card-details-item-') &&
c !== 'card-details-item-title' &&
c !== 'card-details-item-content');
if (!named) return;
const key = named.replace('card-details-item-', '');
item.classList.toggle('is-collapsed', forCard[key] === true);
});
});
});
// #6465: place the window beside its minicard (X only) once it has been laid
// out, so the width measured here is the one the stylesheet gave it.
const $cardDetails = this.$('.card-details').first();
@@ -828,7 +853,44 @@ Template.cardDetails.helpers({
},
});
// #1591: fold any feature group on the opened card - Labels, Members, Dates,
// Custom Fields, and the rest.
//
// Done here, once, rather than by editing sixteen sections in cardDetails.jade,
// because the sections are NOT uniform inside: they all begin with an
// `h3.card-details-item-title`, but only three of them wrap what follows in a
// `.card-details-item-content`. So the title is the handle, and "collapsed"
// hides every sibling after it (the CSS rule) - which works whatever the section
// puts there.
//
// The state is the same per-user map the checklists use
// (profile.collapsedCardSections), keyed by the section's own name, so it
// survives a reload and belongs to the reader rather than to the card.
function cardSectionKeyOf(element) {
const item = element.closest('.card-details-item');
if (!item) return null;
// 'card-details-item-labels' -> 'labels'. The class that is not the generic
// one IS the section's name; there is no separate id to invent.
const named = Array.from(item.classList).find(
c => c.startsWith('card-details-item-') &&
c !== 'card-details-item-title' &&
c !== 'card-details-item-content');
return named ? named.replace('card-details-item-', '') : null;
}
Template.cardDetails.events({
'click .card-details-item > .card-details-item-title'(event) {
const key = cardSectionKeyOf(event.currentTarget);
const user = ReactiveCache.getCurrentUser();
const card = Template.currentData();
if (!key || !user || !card || !card._id) return;
const collapsed = user.getCollapsedCardSection(card._id, key) === true;
user.setCollapsedCardSection(card._id, key, !collapsed);
// Applied straight away as well as stored: the title is plain markup, so
// nothing re-renders on its own when the profile changes.
event.currentTarget.closest('.card-details-item')
.classList.toggle('is-collapsed', !collapsed);
},
[`${CSSEvents.transitionend} .js-card-details`](event, tpl) {
tpl.isLoaded.set(true);
},
+23
View File
@@ -191,3 +191,26 @@ textarea.js-edit-checklist-item {
.edit-controls label.toggle-label {
margin-inline-start: 2px;
}
/* #1591: the fold caret on a checklist title. Sized and coloured like the
actions menu beside it so the two read as one row of controls, and given a
real hit area - it is a single glyph, and a caret you have to aim at is worse
than no caret. */
.checklist-title .checklist-collapse {
/* Logical, so the caret sits on the reading-start side under dir=rtl too
(tests/rtl.test.js enforces this across component CSS). */
float: inline-start;
margin-block-start: 2px;
margin-inline-end: 4px;
padding: 2px 4px;
color: #808080;
font-size: 14px;
line-height: 14px;
cursor: pointer;
text-decoration: none;
}
.checklist-title .checklist-collapse:hover,
.checklist-title .checklist-collapse:focus-visible {
color: #2980b9;
}
+18 -2
View File
@@ -38,6 +38,15 @@ template(name="checklistDetail")
else
.checklist-title
span
//- #1591: fold a long checklist out of the way. A caret rather than
another entry in the actions menu, because the whole point is to do it
at a glance while reading the card - and it is a view preference of
this user, so it does not touch the card.
a.fa.checklist-collapse.js-collapse-checklist(
class="{{#if checklistCollapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}"
title="{{#if checklistCollapsed}}{{_ 'expand-checklist'}}{{else}}{{_ 'collapse-checklist'}}{{/if}}"
role="button" tabindex="0"
aria-expanded="{{#if checklistCollapsed}}false{{else}}true{{/if}}")
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
@@ -52,12 +61,15 @@ template(name="checklistDetail")
+viewer
= checklist.title
//- The progress bar stays visible when folded: it is the summary of what was
folded away, so hiding it too would leave a title that says nothing.
if $gt finishedPercent 0
.checklist-progress-bar-container
.checklist-progress-text {{finishedPercent}}%
.checklist-progress-bar
.checklist-progress(style="width:{{finishedPercent}}%")
+checklistItems(checklist = checklist card = card)
unless checklistCollapsed
+checklistItems(checklist = checklist card = card)
template(name="checklistDeletePopup")
p {{_ 'confirm-checklist-delete-popup'}}
@@ -188,7 +200,11 @@ template(name="checklistActionsPopup")
i.fa.fa-th-list
| {{_ "show-on-minicard"}} ...
.material-toggle-switch(title="{{_ 'show-on-minicard'}}")
if checklist.showChecklistAtMinicard
//- The switch shows what is ON THE MINICARD, not the raw field. Drawn
from the field it read OFF beside a checklist that was plainly showing,
because the board default (allowsChecklistsOnMinicard, on out of the
box) put it there - reported by email.
if shownAtMinicard
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
+66 -1
View File
@@ -8,6 +8,7 @@ import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlane
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
import autosize from 'autosize';
import { isChecklistShownAtMinicard } from '/models/lib/minicardChecklistVisibility';
// SubsManager removed for Meteor 3 migration
const { calculateIndexData } = Utils;
@@ -82,6 +83,47 @@ Template.checklistDetail.helpers({
const ret = this.checklist.finishedPercent();
return ret;
},
/** #1591: is this checklist folded for THIS user?
*
* Per-user, keyed by card and checklist, exactly like collapsed lists and
* swimlanes - not the checklist's own hideAllChecklistItems, which is a field
* on the checklist and so changes what everyone on the board sees. `null` from
* the getter means "never set", which is what makes the default (expanded)
* distinguishable from somebody having deliberately expanded it. */
checklistCollapsed() {
const checklist = this.checklist;
if (!checklist || !checklist._id) return false;
const user = ReactiveCache.getCurrentUser();
if (!user) return false;
const cardId = (this.card && this.card._id) || checklist.cardId;
const stored = user.getCollapsedCardSection(
cardId, user.checklistSectionKey(checklist._id));
return stored === true;
},
});
Template.checklistDetail.events({
'click .js-collapse-checklist'(event) {
// The caret sits inside the title, which opens the inline rename form when
// clicked - so this must not reach it.
event.preventDefault();
event.stopPropagation();
const checklist = this.checklist;
const user = ReactiveCache.getCurrentUser();
if (!checklist || !checklist._id || !user) return;
const cardId = (this.card && this.card._id) || checklist.cardId;
const key = user.checklistSectionKey(checklist._id);
const collapsed = user.getCollapsedCardSection(cardId, key) === true;
user.setCollapsedCardSection(cardId, key, !collapsed);
},
'keydown .js-collapse-checklist'(event) {
// It is a link acting as a button, so it has to answer the keys a button
// answers or it is unreachable without a mouse.
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).trigger('click');
},
});
Template.checklists.helpers({
@@ -256,6 +298,24 @@ Template.addChecklistItemForm.events({
},
});
// The board default this checklist's own setting overrides. Read here rather than
// in the model helper, because the model runs on the server too, where looking a
// board up is asynchronous - and a Blaze helper has to answer now.
function boardAllowsChecklistsOnMinicard(checklist) {
if (!checklist) return false;
const board = ReactiveCache.getBoard(checklist.boardId);
return !!(board && board.allowsChecklistsOnMinicard);
}
Template.checklistActionsPopup.helpers({
// What the "Show on minicard" switch draws: whether the checklist is on the
// minicard right now, board default included.
shownAtMinicard() {
const checklist = this.checklist;
return isChecklistShownAtMinicard(checklist, boardAllowsChecklistsOnMinicard(checklist));
},
});
Template.checklistActionsPopup.events({
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
@@ -279,7 +339,12 @@ Template.checklistActionsPopup.events({
},
'click .js-show-checklist-at-minicard'(event) {
event.preventDefault();
Template.currentData().checklist.toggleShowChecklistAtMinicard();
const checklist = Template.currentData().checklist;
// The board's setting is the default this one overrides, so the toggle has to
// flip what is ON SCREEN, not the raw field. Flipping the field is what made
// the first click do nothing while the board default was on (false -> true,
// still shown) - reported by email.
checklist.toggleShowChecklistAtMinicard(boardAllowsChecklistsOnMinicard(checklist));
Popup.back();
},
});
+5 -5
View File
@@ -58,13 +58,13 @@
.minicard-details-menu-with-handle {
float: inline-end;
padding-inline-start: 10px;
font-size: 14px;
font-size: 0.875rem;
padding: 0;
z-index: 1;
}
.minicard-details-menu {
float: inline-end;
font-size: 14px;
font-size: 0.875rem;
padding-inline-start: 10px;
}
@media print {
@@ -237,12 +237,12 @@
}
body.mobile-mode .minicard .handle .drag-handle,
body.mobile-mode .minicard .handle .fa {
font-size: 22px;
font-size: 1.375rem;
}
}
.minicard .handle .drag-handle {
font-size: 16px;
font-size: 1rem;
color: #ccc;
display: inline-block;
width: 1.4em;
@@ -253,7 +253,7 @@
"Add card" affordance below (13px). It used to inherit the 16px document
size, which made a card title exactly as loud as the heading of the column
holding it. See the note on .list-header-name in lists/list.css. */
font-size: 14px;
font-size: 0.875rem;
margin-inline-end: 21px;
/* #6444: isolate each title's bidirectional context. The title is a
dir="auto" .viewer; under a global dir="rtl" document, non-isolated
+19 -11
View File
@@ -265,6 +265,13 @@ template(name="editCardSortOrderPopup")
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
//- #1591: the same caret as on the opened card, and the same stored key,
so folding a checklist folds it in both places.
a.fa.checklist-collapse.js-collapse-checklist(
class="{{#if checklistCollapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}"
title="{{#if checklistCollapsed}}{{_ 'expand-checklist'}}{{else}}{{_ 'collapse-checklist'}}{{/if}}"
role="button" tabindex="0"
aria-expanded="{{#if checklistCollapsed}}false{{else}}true{{/if}}")
if canModifyCard
+inlinedForm(classNames="js-edit-checklist-title" checklist=checklist)
+editChecklistForm(checklist=checklist)
@@ -275,16 +282,17 @@ template(name="minicardChecklist")
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
i.fa.fa-bars
.checklist-items.js-checklist-items
each visibleItems
+inlinedForm(classNames="js-edit-checklist-item" item=. checklist=checklist)
+editChecklistItemForm(type='item' item=. checklist=checklist)
unless checklistCollapsed
.checklist-items.js-checklist-items
each visibleItems
+inlinedForm(classNames="js-edit-checklist-item" item=. checklist=checklist)
+editChecklistItemForm(type='item' item=. checklist=checklist)
else
+checklistItemDetail(item=. checklist=checklist card=card)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist=checklist)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
+checklistItemDetail(item=. checklist=checklist card=card)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist=checklist)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
+40 -8
View File
@@ -7,6 +7,7 @@ import { Utils } from '/client/lib/utils';
import ChecklistItems from '/models/checklistItems';
import Cards from '/models/cards';
import { resolveCoverId } from '/models/lib/linkedCardCover';
import { isChecklistShownAtMinicard } from '/models/lib/minicardChecklistVisibility';
import {
parseChecklistItemTitles,
buildChecklistItemPayload,
@@ -231,14 +232,13 @@ Template.minicard.helpers({
const visibleChecklists = [];
checklists.forEach(checklist => {
// Show checklist if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific checklist has the setting enabled
// #5565: use the same board field the sidebar toggle writes
// (`allowsChecklistsOnMinicard`); this previously checked a different,
// UI-less field (`allowsChecklistAtMinicard`), so the board toggle to show
// checklists on minicards had no effect.
if (currentBoard.allowsChecklistsOnMinicard || checklist.showChecklistAtMinicard) {
// The board setting is the DEFAULT and the checklist's own setting is an
// OVERRIDE. This was an OR - board || checklist - and the board setting
// (`allowsChecklistsOnMinicard`, #5565) defaults to TRUE, so unchecking
// "Show on minicard" on a checklist could never hide it: reported by email
// with a screenshot of a checklist still on the minicard after the toggle
// was switched off. See models/lib/minicardChecklistVisibility.js.
if (isChecklistShownAtMinicard(checklist, currentBoard.allowsChecklistsOnMinicard)) {
visibleChecklists.push(checklist);
}
});
@@ -375,6 +375,18 @@ Template.minicard.events({
});
Template.minicardChecklist.helpers({
/** #1591: folded for THIS user, under the same key the opened card uses, so
* the two agree. `this.checklist || this` because this template is called both
* with an explicit checklist and with one as the data context. */
checklistCollapsed() {
const checklist = this.checklist || this;
if (!checklist || !checklist._id) return false;
const user = ReactiveCache.getCurrentUser();
if (!user) return false;
const cardId = (this.card && this.card._id) || checklist.cardId;
return user.getCollapsedCardSection(
cardId, user.checklistSectionKey(checklist._id)) === true;
},
visibleItems() {
const checklist = this.checklist || this;
const items = checklist.items();
@@ -416,6 +428,26 @@ function addMinicardChecklistItems(textarea, checklist) {
}
Template.minicardChecklist.events({
'click .js-collapse-checklist'(event) {
// A minicard is a link to the card, and the title opens the rename form -
// neither must happen because somebody folded a checklist.
event.preventDefault();
event.stopPropagation();
const data = Template.currentData();
const checklist = (data && data.checklist) || data || this;
const user = ReactiveCache.getCurrentUser();
if (!checklist || !checklist._id || !user) return;
const cardId = (data && data.card && data.card._id) || checklist.cardId;
const key = user.checklistSectionKey(checklist._id);
const collapsed = user.getCollapsedCardSection(cardId, key) === true;
user.setCollapsedCardSection(cardId, key, !collapsed);
},
'keydown .js-collapse-checklist'(event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).trigger('click');
},
'click .js-convert-checklist-item-to-card'(event) {
event.stopPropagation();
const formData = Blaze.getData(event.currentTarget);
+7 -3
View File
@@ -476,8 +476,12 @@ form.inlined-form .list-composer {
the cards. The scale is now heading 16 > card title 14 >= add-card link 13,
kept in that order by tests/listTypeScale.test.cjs. Mobile keeps its own
larger sizes (the !important rules further down) - a phone is read at arm's
length. */
font-size: 16px;
length.
#6587: written in rem, not px. The three sizes are the same as before at the
default root size (1rem = 16px), but a px size cannot follow Member Settings
/ Font size, which sets the root font size - so a board's type was the one
part of the UI that ignored the setting entirely. */
font-size: 1rem;
line-height: 1.2;
margin: 0;
font-weight: bold;
@@ -837,7 +841,7 @@ form.inlined-form .list-composer {
/* Smaller than a card title, because it is not a card: it is the affordance
for making one. At the inherited 16px it was the largest text in the list
body, level with the board's real content. */
font-size: 13px;
font-size: 0.8125rem;
padding: 7px 10px;
position: relative;
text-decoration: none;
+40 -2
View File
@@ -423,8 +423,46 @@ Template.list.onRendered(function () {
scrolled = true;
}
}
// vertical auto-scroll stays on the board canvas
const canvas = document.querySelector('.board-canvas');
// #6584: VERTICALLY, the thing to scroll is the LIST the pointer is over,
// not the board canvas. `.list-body` is `overflow-y: scroll` (list.css) and
// is what holds the cards; `.board-canvas` is `overflow-y: auto` and holds
// the swimlanes. Scrolling the canvas moves the whole board, which is what
// the report describes: "The whole Page/Site scrolls down and not the Line".
//
// It only showed up DOWNWARDS, and the asymmetry is the tell. Dragging up,
// the canvas is usually already at scrollTop 0, so computeEdgeScroll
// returned null, nothing here fired, and jQuery UI's own `scroll` option -
// which acts on the placeholder's scrollParent, i.e. the list body - scrolled
// the list, exactly as expected. Dragging down, the canvas nearly always HAS
// room, so this fired first and scrolled the board instead of the list.
//
// The canvas is still scrolled, but only once the list under the pointer
// cannot go further that way - so a drag down a long list scrolls the list,
// and a drag past the end of it moves on to the board. Same shape as the
// horizontal case above, which already picks the lane under the pointer.
let scrolledList = false;
const listBodies = document.querySelectorAll('.list-body');
const listRects = Array.prototype.map.call(listBodies, el =>
el.getBoundingClientRect());
// findLaneUnderPointer is a plain rectangle hit-test; the name is about
// where it was first used, not what it can be given.
const listIndex = findLaneUnderPointer(listRects, event.clientX, event.clientY);
if (listIndex !== -1) {
const nextListTop = computeEdgeScroll({
pointer: event.clientY,
lowEdge: listRects[listIndex].top,
highEdge: listRects[listIndex].bottom,
scrollPos: listBodies[listIndex].scrollTop,
scrollSize: listBodies[listIndex].scrollHeight,
clientSize: listBodies[listIndex].clientHeight,
});
if (nextListTop !== null) {
listBodies[listIndex].scrollTop = nextListTop;
scrolled = true;
scrolledList = true;
}
}
const canvas = scrolledList ? null : document.querySelector('.board-canvas');
if (canvas) {
const crect = canvas.getBoundingClientRect();
const nextTop = computeEdgeScroll({
+16
View File
@@ -256,6 +256,16 @@ Template.listBody.onCreated(function () {
if (clickedTitle && !clickedLinkedReference) {
evt.stopImmediatePropagation();
evt.preventDefault();
// #6465: clicking the OPEN card again closes it. The toggle below has
// always been here, but this branch returned before ever reaching it - and
// the title covers most of the minicard, so in practice the second click
// almost always lands here and just re-opened the card that was already
// open. That is why it read as a missing feature rather than a dead
// branch. Same call as below, so both click targets behave alike.
if (Session.equals('currentCard', card._id)) {
Utils.goBoardId(Session.get('currentBoard'));
return;
}
Session.delete('popupCardId');
Session.delete('popupCardBoardId');
Session.set('currentCard', card._id);
@@ -265,6 +275,12 @@ Template.listBody.onCreated(function () {
if (Utils.isMiniScreen()) {
evt.preventDefault();
// ...and on a phone, where the card is a popup rather than a pane, the
// second click closes that popup for the same reason.
if (Session.equals('popupCardId', card._id) && Popup.isOpen()) {
Popup.back();
return;
}
Session.set('popupCardId', card._id);
this.cardDetailsPopup(evt);
} else if (Session.equals('currentCard', card._id)) {
+24 -2
View File
@@ -243,8 +243,30 @@ body.mobile-mode #content {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
height: calc(100vh - 48px);
height: calc(100dvh - 48px);
/* #6488, and the reason that issue kept coming back after being closed three
times: THE LAST PIECE OF VIEWPORT ARITHMETIC. This said
`height: calc(100dvh - 48px)`, and 48px is a guess at how tall the header is.
The header is not 48px and is not any one number - Utils publishes
--wekan-header-height from a ResizeObserver for exactly this reason, because
the quick-access bar WRAPS to a second and third row depending on the
language and the window width, and its own comment says "every fixed number
for it has been wrong for some of those".
On a phone, where the bar does wrap, #content was therefore taller than the
room under the header by however much the header exceeded 48px, so its
bottom sat BELOW the bottom of the screen. body.mobile-mode is
`position: fixed` and the phone rules make it `overflow: hidden`, so that
strip is unreachable: everything below it inherits the error through the
`height: 100%` chain (boardsList.css), and the last rows of the board list
end up where no gesture can reach them. That is "in mobile board menu you
cant scroll boards" - the list scrolls, it just ends off screen.
Nothing has to be computed here. body is a flex COLUMN of exactly one
viewport and this is its `flex: 1` item (below), so the space left under the
header - whatever the header turns out to be this paint - IS this box. The
arithmetic was overriding a correct answer with a wrong one.
`min-height: 0` so it can be SHORTER than its content: a flex item will not
shrink below its content otherwise, and an inner scroller whose ancestor
refuses to shrink never scrolls. */
min-height: 0;
overscroll-behavior: contain;
}
#content {
+18 -3
View File
@@ -893,14 +893,29 @@ body.mobile-mode .pop-over {
inset-inline-end: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
/* #6488. `100vh` is the viewport with the browser's toolbars HIDDEN, so a
`position: fixed` box sized that way reaches under them: on a phone the
bottom of this popup is off screen, and being fixed there is no gesture that
brings it back. `100dvh` is the viewport as it is right now. The `vh` pair
stays first as the fallback where `dvh` is unknown - the same fix
body.mobile-mode already carries in layouts.css. */
height: 100vh !important;
max-height: 100vh !important;
height: 100dvh !important;
max-height: 100dvh !important;
}
body.mobile-mode .pop-over .content-wrapper {
width: 100% !important;
height: calc(100vh - 48px) !important;
max-height: calc(100vh - 48px) !important;
/* #6488. This was `calc(100vh - 48px)`, which is two guesses at once: the
viewport measured with the toolbars hidden, minus a fixed 48px for the
popup's own title bar. When the title wraps - a long board name, a
translated label - the bar is taller than 48px and the list inside this
wrapper ends below the screen, so its last rows cannot be scrolled to.
Nothing needs measuring: `.pop-over` above is a flex column of exactly one
viewport, `.pop-over .header` is `flex-shrink: 0`, and this wrapper is
already `flex: 1 1 auto; min-height: 0` - so it is whatever the title bar
leaves, at whatever height the title bar really is. */
overflow-y: auto !important;
overflow-x: hidden !important;
}
+9 -2
View File
@@ -14,8 +14,15 @@ body.has-ui-font .wekan-form-control {
/* Font size preset (#4759): scale the root font size. Elements using relative units
* (em/rem/%) scale with it; the variable is a percentage set by uiFont.js only for a
* known preset. 'default'/unset removes it. */
body.has-ui-font-size {
* known preset. 'default'/unset removes it.
*
* #6587: this is on <html>, and it used to be on <body>. `rem` is measured against
* the ROOT element, so a size on body never reached it — and a board's type is
* written in rem (px would not follow this setting at all, and vw would change with
* the window), which is why minicards were the part of the UI that ignored the
* preset. Everything relative to body still scales, because body inherits from the
* root; putting the size on BOTH would compound the two. */
html.has-ui-font-size {
font-size: var(--wekan-ui-font-size) !important;
}
+16 -2
View File
@@ -9,14 +9,28 @@ import { fontFamilyValue, fontSizeValue, colorValue } from '/models/lib/uiFonts'
// saved values are also validated server-side. Unset => nothing applied (defaults).
Meteor.startup(() => {
// #6587: the font-size preset has to land on the ROOT element, not on <body>.
// A size on <body> is inherited only by children that ask for a relative size,
// and it leaves `rem` — which is measured against the root — untouched. So a
// board's type, which states its sizes in rem so it does not change with the
// window, ignored the setting completely: "The font size setting in the user
// menu (Settings -> Font Size) has no effect on mini cards, while it correctly
// applies to other UI elements."
//
// Only ONE element may carry it. With the size on both html and body, a 130%
// preset would compound to 169% for anything relative to body, so this marks
// the root for font-size and <body> for everything else (the font-family and
// colour rules match on body, and form controls are listed there explicitly).
const ROOT_CLASSES = new Set(['has-ui-font-size']);
function toggle(varName, className, value) {
try {
const el = ROOT_CLASSES.has(className) ? document.documentElement : document.body;
if (value) {
document.documentElement.style.setProperty(varName, value);
document.body.classList.add(className);
el.classList.add(className);
} else {
document.documentElement.style.removeProperty(varName);
document.body.classList.remove(className);
el.classList.remove(className);
}
} catch (_) {
// document not ready in exotic embeddings; ignore.
@@ -365,6 +365,13 @@ Template.adminReports.helpers({
showDatabase() {
return Template.instance().showDatabase;
},
// Filesystem integrity. Without this helper the pane rendered NOTHING: the
// menu entry set tmpl.showIntegrity, the template asked for `showIntegrity.get`,
// and an undefined helper is simply falsy - so the branch never ran, the page
// was blank, and Summary went on counting the problems it could not show.
showIntegrity() {
return Template.instance().showIntegrity;
},
showCpu() {
return Template.instance().showCpu;
},
@@ -0,0 +1,60 @@
/* ── Admin Panel / Settings / Version ────────────────────────────────────────
ONE table, in five categories - Platform, OS, Meteor, Database, Node - where
each category is a row of its own that spans both columns.
WHY ONE TABLE. It was five, one per category, and five tables size their
columns independently: "WeKan ® Version" made the first one's label column
wide and "OS Type" made the next one's narrow, so the values started at a
different x in every group and the pane read as five unrelated things. One
table puts every label and every value in the same place. */
/* Two EQUAL columns, and they stay equal whatever the longest label happens to
be. `table-layout: fixed` makes the browser take the widths from the colgroup
instead of measuring the content, and the two `col` rules below are that
colgroup - written here rather than as inline width attributes so a theme can
override them in one place.
The `max-width: none` undoes settingBody.css's 240px cap on admin table
headers, which exists to stop a long label blowing a column up; here the
column width is stated outright, so the cap has nothing to protect and would
only fight the 50%. Both rules are scoped through .info-table, so no other
admin table is touched. */
.setting-content .content-body .main-body table.info-table {
table-layout: fixed;
}
.setting-content .content-body .main-body table.info-table > colgroup > col {
width: 50%;
}
.setting-content .content-body .main-body table.info-table th,
.setting-content .content-body .main-body table.info-table td {
max-width: none;
}
/* The category row. It spans both columns, so it is not a label with a value
beside it - it is a line that says what the rows under it are about.
BOLD and start-aligned, at the table's own font size: it names a group of rows
inside the pane, and the pane title above it (`.admin-pane-title`, 1.25rem)
names the pane. Growing these to that size would read as five pane titles and
"Version" would be lost among them. `color: inherit` for the same reason it is
on the pane title: the panel is themeable and a fixed colour goes unreadable
on a dark one. */
.setting-content .content-body .main-body table.info-table tr.info-category > th {
/* start, not left: a th centres by default, and this must sit over the label
column - which is the RIGHT edge in Arabic and Hebrew (tests/rtl.test.js). */
text-align: start;
font-weight: 700;
color: inherit;
/* Space above the group, none below it: the separation belongs BETWEEN
groups, and a gap under the title would push it away from the rows it
introduces. */
padding-top: 18px;
}
/* ...except the first one, which sits directly under the pane title and needs no
second gap on top of the 12px that title already brings. */
.setting-content .content-body .main-body table.info-table tr.info-category:first-child > th {
padding-top: 0;
}
+89 -40
View File
@@ -5,49 +5,45 @@
//- templates are global) inside the Settings page, which brings the left menu, the
//- pane title and the admin check with it. The /information URL redirects to
//- Settings, the same way the old /translation URL does.
//-
//- ONE table, in FIVE categories. This was a single flat list of 40 rows in which
//- the WeKan version, the OS load average, the DDP transport and a V8 heap counter
//- were the same kind of thing, read top to bottom, with no way to jump to the part
//- you came for. It is grouped now - what this IS (version and packaging), what it
//- runs ON, how Meteor is talking to the client, what the database is, and finally
//- Node's own memory - and each group is introduced by a row of its own that SPANS
//- BOTH COLUMNS, in bold.
//-
//- One table rather than five, because five tables size their columns
//- independently: "WeKan ® Version" made the first one's label column wide and
//- "OS Type" made the next one's narrow, so the values started at a different x
//- in every group and the pane read as five unrelated things. One table with a
//- colgroup of two 50% columns puts every label and every value in the same place
//- (.info-table in informationBody.css).
template(name='statistics')
table
table.info-table
//- Two equal columns, stated once here rather than left to the widest cell:
with table-layout fixed these are what the browser uses, so the labels and
the values line up down the whole pane no matter which group is longest.
colgroup
col
col
tbody
tr.info-category
th(colspan="2") {{_ 'Platform'}}
tr
th WeKan ® {{_ 'info'}}
td {{statistics.version}}
//- The LABEL is translated; the VALUE never is. bundle.zip, Snap, Docker
and Sandstorm are the names of the things themselves - a package
format, a store, a product - not words describing them, and a reader
reporting an issue, searching the docs or grepping a log needs the same
four strings in every language. See models/lib/platformPackaging.js.
tr
th {{_ 'Meteor_version'}}
td {{statistics.meteor.meteorVersion}}
tr
th {{_ 'Node_version'}}
td {{statistics.process.nodeVersion}}
tr
th {{_ 'Database_type'}}
td {{statistics.mongo.databaseType}}
tr
th {{_ 'MongoDB_version'}}
td {{statistics.mongo.mongoVersion}}
tr
th {{_ 'Database_commit'}}
td {{statistics.mongo.databaseCommit}}
if statistics.mongo.ferretdbVersion
tr
th {{_ 'FerretDB_version'}}
td {{statistics.mongo.ferretdbVersion}}
tr
th {{_ 'FerretDB_commit'}}
td {{statistics.mongo.ferretdbCommit}}
tr
th {{_ 'MongoDB_storage_engine'}}
td {{statistics.mongo.mongoStorageEngine}}
tr
th {{_ 'MongoDB_Oplog_enabled'}}
td {{formatBoolean statistics.mongo.mongoOplogEnabled}}
tr
th {{_ 'Reactivity_mode'}}
td {{statistics.mongo.reactivity}}
tr
th {{_ 'Reactivity_order'}}
td {{statistics.mongo.reactivityOrder}}
tr
th {{_ 'DDP_transport'}}
td {{statistics.mongo.ddpTransport}}
th {{_ 'package'}}
td {{statistics.platform.packaging}}
tr.info-category
th(colspan="2") {{_ 'OS'}}
tr
th {{_ 'OS_Type'}}
td {{statistics.os.type}}
@@ -75,6 +71,62 @@ template(name='statistics')
tr
th {{_ 'OS_Cpus'}}
td {{statistics.os.cpus.length}}
tr.info-category
th(colspan="2") {{_ 'Meteor'}}
tr
th {{_ 'Meteor_version'}}
td {{statistics.meteor.meteorVersion}}
//- Reactivity and DDP are Meteor's, not the database's: what is LIVE
(reactivity), what was ASKED for (reactivity order), and how the client
is connected (DDP transport). Whether an OpLog exists to tail is a
property of the database and stays in that table.
tr
th {{_ 'Reactivity_mode'}}
td {{statistics.mongo.reactivity}}
tr
th {{_ 'Reactivity_order'}}
td {{statistics.mongo.reactivityOrder}}
tr
th {{_ 'DDP_transport'}}
td {{statistics.mongo.ddpTransport}}
tr.info-category
th(colspan="2") {{_ 'Database'}}
tr
th {{_ 'Database_type'}}
td {{statistics.mongo.databaseType}}
tr
th {{_ 'MongoDB_version'}}
td {{statistics.mongo.mongoVersion}}
tr
th {{_ 'Database_commit'}}
td {{statistics.mongo.databaseCommit}}
//- Only when FerretDB is what answered: on MongoDB these two rows have
nothing to say, and an empty row reads as a missing value.
if statistics.mongo.ferretdbVersion
tr
th {{_ 'FerretDB_version'}}
td {{statistics.mongo.ferretdbVersion}}
tr
th {{_ 'FerretDB_commit'}}
td {{statistics.mongo.ferretdbCommit}}
tr
th {{_ 'MongoDB_storage_engine'}}
td {{statistics.mongo.mongoStorageEngine}}
tr
th {{_ 'MongoDB_Oplog_enabled'}}
td {{formatBoolean statistics.mongo.mongoOplogEnabled}}
unless isSandstorm
tr
th {{_ 'Mongo_sessions_count'}}
td {{statistics.session.sessionsCount}}
tr.info-category
th(colspan="2") {{_ 'Node'}}
tr
th {{_ 'Node_version'}}
td {{statistics.process.nodeVersion}}
//- A Sandstorm grain has no V8 heap figures collected for it (see
server/statistics.js), so the version above is all this table can say
there.
unless isSandstorm
tr
th {{_ 'Node_heap_total_heap_size'}}
@@ -121,6 +173,3 @@ template(name='statistics')
tr
th {{_ 'Node_memory_usage_external'}}
td {{fileSize statistics.nodeMemoryUsage.external}}
tr
th {{_ 'Mongo_sessions_count'}}
td {{statistics.session.sessionsCount}}
@@ -456,6 +456,34 @@ body.left-menu-resizing-active {
/* …and exactly .materialCheckBox.is-checked: the square becomes the tick, which is
what the transition above animates. */
/* #6465: THE FOCUS RING MUST NOT BE DRAWN ON THE ROTATED BOX.
The checked state below turns the square into a tick by making two borders
transparent and rotating the element 40deg. A browser draws its focus ring
around the element as it is - rotated - so a checkbox that is both checked and
focused (which is exactly what a checkbox you just clicked is) comes out as a
blue DIAMOND sitting where the tick should be. That is the glitch reported on
the Problems page.
The ring is not simply removed: keyboard users need to see where they are. It
moves to the row that CONTAINS the checkbox, which is not rotated - every one
of these lives inside a label (problemsSummary.jade: `label.admin-problem-item
> input.js-problem-check`) - so the indicator is a normal rectangle around the
whole row, which is easier to see than a 13px box anyway.
`:focus-visible` rather than `:focus`, so clicking with a mouse does not leave
a ring behind; `:focus-within` is the fallback where :has() is not supported,
and lands on the same element. */
.setting-content input[type="checkbox"]:focus,
.setting-content input[type="checkbox"]:focus-visible {
outline: none;
}
.setting-content label:has(> input[type="checkbox"]:focus-visible) {
outline: 2px solid #2980b9;
outline-offset: 2px;
border-radius: 3px;
}
.setting-content input[type="checkbox"]:checked {
top: -4px;
inset-inline-start: -3px;
+1
View File
@@ -40,6 +40,7 @@ import '/client/components/settings/translationBody.js';
// forms.css and rendered with a black/grey background instead of the theme.
import '/client/components/settings/adminReports.css';
import '/client/components/settings/attachments.css';
import '/client/components/settings/informationBody.css';
import '/client/components/settings/lockedUsersBody.css';
import '/client/components/settings/migrationProgress.css';
import '/client/components/settings/peopleBody.css';
+27
View File
@@ -3,6 +3,13 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/reactiveCache';
import { getCurrentCardFromContext } from '/client/lib/currentCard';
import { normalizeDigits } from '/imports/lib/dateUtils';
// The window a typed year has to fall in. Wide on purpose: WeKan holds real
// historical received dates and long-dated deadlines, so this is only meant to
// catch a year that cannot have been intended - the two-digit one a browser
// reports verbatim (0026), and the five-digit slip in the other direction.
const MIN_PLAUSIBLE_YEAR = 1000;
const MAX_PLAUSIBLE_YEAR = 9999;
import {
isValidDate,
formatTime,
@@ -165,6 +172,26 @@ export function datePickerEvents({ storeDate, deleteDate }) {
return;
}
// A TWO-DIGIT YEAR is a real date that is not the one anybody meant.
// `<input type="date">` reports YYYY-MM-DD, but the browser lets the year
// sub-field be typed as two digits and reports that literally: typing
// 31-12-26 gives "0026-12-31", the year 26 AD. Nothing above rejects it -
// it is a perfectly valid Date - so the card got a due date two thousand
// years in the past, which is why the reporter saw a date they had TYPED
// come out red (overdue) while the same date picked from the calendar,
// which always fills four digits, came out yellow (due soon). The colour
// was right; the year was wrong.
//
// Refused rather than corrected: 0026 could be meant as 2026, but guessing
// silently rewrites what somebody typed, and this is a date other people's
// reminders hang off. The error names the year so the fix is obvious.
const year = newCompleteDate.getFullYear();
if (year < MIN_PLAUSIBLE_YEAR || year > MAX_PLAUSIBLE_YEAR) {
tpl.datePicker.error.set('invalid-year');
evt.target.date.focus();
return;
}
storeDate.call(tpl, newCompleteDate);
Popup.back();
},
+5 -4
View File
@@ -238,11 +238,12 @@ export const Utils = {
canModifyCard() {
return Utils.currentUserCan('write');
},
// A move IS a card update on the server, so it is the same capability. They are
// kept as two names because the call sites mean different things, not because
// the answer differs.
// A move is a card update on the server, but it is NOT the same capability: a
// Worker may move a card and assign themselves to it while writing nothing else
// (#3189, models/lib/workerCardWrite.js enforces exactly that field by field).
// Every role that can `write` can also move, so this only ever ADDS the Worker.
canMoveCard() {
return Utils.currentUserCan('write');
return Utils.currentUserCan('moveCard');
},
canModifyBoard() {
return Utils.currentUserCan('write');
+1
View File
@@ -11,6 +11,7 @@ export const ALLOWED_BOARD_COLORS = [
'dark',
'relax',
'corteza',
'appleglasspastel',
'clearblue',
'cleargreen',
'clearorange',
+14 -3
View File
@@ -218,8 +218,18 @@ services:
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
- MONGO_URL=mongodb://ferretdb:27017/wekan
# directConnection=true is REQUIRED (wekan/wekan#6582). The ferretdb service
# above runs with --repl-set-name=rs0, so its handshake advertises a replica
# set whose only member is its own LISTEN address, 0.0.0.0:27017. Without
# directConnection the MongoDB driver does replica-set discovery: it drops the
# host written here and dials the advertised 0.0.0.0:27017 instead, which
# inside the wekan container is the wekan container itself — that is the
# "MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017" a fresh
# `docker compose up` died with. directConnection=true keeps the driver on
# exactly this host. OpLog tailing is unaffected: Meteor only needs setName in
# the handshake, which is still there.
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan?directConnection=true
- MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
@@ -274,7 +284,8 @@ services:
# MONGO_OPLOG_URL makes Meteor start an OpLog tail regardless of the
# reactivity order (#6498), so it is commented out. To try OpLog tailing,
# uncomment the next line AND set METEOR_REACTIVITY_ORDER=oplog,polling above.
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0
# directConnection=true for the same reason as MONGO_URL above (#6582).
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0&directConnection=true
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/edemaine/kadira-compose
+14 -3
View File
@@ -220,8 +220,18 @@ services:
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
- MONGO_URL=mongodb://ferretdb:27017/wekan
# directConnection=true is REQUIRED (wekan/wekan#6582). The ferretdb service
# above runs with --repl-set-name=rs0, so its handshake advertises a replica
# set whose only member is its own LISTEN address, 0.0.0.0:27017. Without
# directConnection the MongoDB driver does replica-set discovery: it drops the
# host written here and dials the advertised 0.0.0.0:27017 instead, which
# inside the wekan container is the wekan container itself — that is the
# "MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017" a fresh
# `docker compose up` died with. directConnection=true keeps the driver on
# exactly this host. OpLog tailing is unaffected: Meteor only needs setName in
# the handshake, which is still there.
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan?directConnection=true
- MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
@@ -276,7 +286,8 @@ services:
# MONGO_OPLOG_URL makes Meteor start an OpLog tail regardless of the
# reactivity order (#6498), so it is commented out. To try OpLog tailing,
# uncomment the next line AND set METEOR_REACTIVITY_ORDER=oplog,polling above.
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0
# directConnection=true for the same reason as MONGO_URL above (#6582).
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0&directConnection=true
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/edemaine/kadira-compose
+14 -3
View File
@@ -207,8 +207,18 @@ services:
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
- MONGO_URL=mongodb://ferretdb:27017/wekan
# directConnection=true is REQUIRED (wekan/wekan#6582). The ferretdb service
# above runs with --repl-set-name=rs0, so its handshake advertises a replica
# set whose only member is its own LISTEN address, 0.0.0.0:27017. Without
# directConnection the MongoDB driver does replica-set discovery: it drops the
# host written here and dials the advertised 0.0.0.0:27017 instead, which
# inside the wekan container is the wekan container itself — that is the
# "MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017" a fresh
# `docker compose up` died with. directConnection=true keeps the driver on
# exactly this host. OpLog tailing is unaffected: Meteor only needs setName in
# the handshake, which is still there.
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan?directConnection=true
- MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
@@ -263,7 +273,8 @@ services:
# MONGO_OPLOG_URL makes Meteor start an OpLog tail regardless of the
# reactivity order (#6498), so it is commented out. To try OpLog tailing,
# uncomment the next line AND set METEOR_REACTIVITY_ORDER=oplog,polling above.
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0
# directConnection=true for the same reason as MONGO_URL above (#6582).
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0&directConnection=true
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/edemaine/kadira-compose
+14 -3
View File
@@ -224,8 +224,18 @@ services:
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
- MONGO_URL=mongodb://ferretdb:27017/wekan
# directConnection=true is REQUIRED (wekan/wekan#6582). The ferretdb service
# above runs with --repl-set-name=rs0, so its handshake advertises a replica
# set whose only member is its own LISTEN address, 0.0.0.0:27017. Without
# directConnection the MongoDB driver does replica-set discovery: it drops the
# host written here and dials the advertised 0.0.0.0:27017 instead, which
# inside the wekan container is the wekan container itself — that is the
# "MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017" a fresh
# `docker compose up` died with. directConnection=true keeps the driver on
# exactly this host. OpLog tailing is unaffected: Meteor only needs setName in
# the handshake, which is still there.
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan?directConnection=true
- MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
@@ -280,7 +290,8 @@ services:
# MONGO_OPLOG_URL makes Meteor start an OpLog tail regardless of the
# reactivity order (#6498), so it is commented out. To try OpLog tailing,
# uncomment the next line AND set METEOR_REACTIVITY_ORDER=oplog,polling above.
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0
# directConnection=true for the same reason as MONGO_URL above (#6582).
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0&directConnection=true
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/edemaine/kadira-compose
+14 -3
View File
@@ -178,8 +178,18 @@ services:
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
- MONGO_URL=mongodb://ferretdb:27017/wekan
# directConnection=true is REQUIRED (wekan/wekan#6582). The ferretdb service
# above runs with --repl-set-name=rs0, so its handshake advertises a replica
# set whose only member is its own LISTEN address, 0.0.0.0:27017. Without
# directConnection the MongoDB driver does replica-set discovery: it drops the
# host written here and dials the advertised 0.0.0.0:27017 instead, which
# inside the wekan container is the wekan container itself — that is the
# "MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017" a fresh
# `docker compose up` died with. directConnection=true keeps the driver on
# exactly this host. OpLog tailing is unaffected: Meteor only needs setName in
# the handshake, which is still there.
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan?directConnection=true
- MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
@@ -234,7 +244,8 @@ services:
# MONGO_OPLOG_URL makes Meteor start an OpLog tail regardless of the
# reactivity order (#6498), so it is commented out. To try OpLog tailing,
# uncomment the next line AND set METEOR_REACTIVITY_ORDER=oplog,polling above.
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0
# directConnection=true for the same reason as MONGO_URL above (#6582).
#- MONGO_OPLOG_URL=mongodb://ferretdb:27017/local?replicaSet=rs0&directConnection=true
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/edemaine/kadira-compose
+18 -1
View File
@@ -106,7 +106,24 @@ mv /var/snap/wekan/common/* /root/common/
sudo snap refresh wekan --channel=latest/candidate --amend
```
5. [Restore Snap](#restore-wekan-snap)
6. Copy back files directory, if it is there: `sudo cp -pR /root/common/files /var/snap/wekan/common/`
6. Copy back **only the `files` directory**, if it is there — that is the attachments
and avatars, which live on disk and are not in the database:
`sudo cp -pR /root/common/files /var/snap/wekan/common/`
**Do NOT copy the rest of `/root/common` back.** Step 3 moved the whole directory
aside, so `cp -pR /root/common/* /var/snap/wekan/common/` looks like the obvious
way to undo it — and it is the one command here that destroys the database you
have just restored. Everything beside `files` is the OLD raw database
(`*.wt`, `WiredTiger*`, `_mdb_catalog.wt`, `sizeStorer.wt`, `storage.bson`,
`mongod.lock`, `journal/`), and dropping those on top of a RUNNING MongoDB
replaces the files it has open underneath it: mongod aborts (`SIGABRT`,
`status=134/n/a` in `snap logs wekan.mongodb`), and when the snap restarts, the
boards restored in step 5 are gone. Nothing is recoverable from that except your
backup, which is why step 2 exists.
The old database directory is only ever put back as a WHOLE, onto a STOPPED snap,
with the current contents removed first — that is the "going back to 6.09"
procedure below, and it is a different operation from this step.
7. If you use [Caddy](../Webserver/Caddy.md), that is included in WeKan, edit /var/snap/wekan/Caddyfile to new syntax:
```
wekan.yourcompany.com {
+69 -4
View File
@@ -7,8 +7,10 @@ the **default database of WeKan**: `docker-compose.yml` runs FerretDB v1 on its
embedded SQLite, so there is no separate database server to install at all.
Whichever backend is used, WeKan talks only to FerretDB
(`MONGO_URL=mongodb://ferretdb:27017/wekan`) — nothing in WeKan knows what is
behind it.
(`MONGO_URL=mongodb://ferretdb:27017/wekan?directConnection=true`) — nothing in
WeKan knows what is behind it. See
[directConnection](#why-the-url-says-directconnectiontrue) for why that parameter
is not optional.
## Which one to use
@@ -55,8 +57,24 @@ The compose files do not use a FerretDB image: a small Debian container download
the `ferretdb-<arch>` binary for its own architecture from the newest
[wekan/FerretDB release](https://github.com/wekan/FerretDB/releases), caches it on
the volume and runs it. Pin a version with `FERRETDB_RELEASE=download/v1.24.2`.
The same per-arch binaries are embedded in the WeKan bundles for the platforms
MongoDB has no server build for — ppc64le, s390x, riscv64.
**Seventeen of those binaries are built**, all cross-compiled from one checkout
with CGO disabled: ten Linux (`amd64`, `arm64`, `armhf`, `armv6`, `armel`,
`i386`, `ppc64le`, `s390x`, `riscv64`, `loong64`), three Windows (`win64`,
`win-arm64`, `win32`), two macOS (`mac-amd64`, `mac-arm64`) and two FreeBSD
(`freebsd-amd64`, `freebsd-arm64`). The 32-bit ARM ones are three different
builds, not three names for one: `armhf` is `GOARM=7`, `armv6` is `GOARM=6` for
the Raspberry Pi 1 and Zero, and `armel` is `GOARM=5` software floating point for
genuine ARMv5.
A `ferretdb-<arch>` binary is embedded in **every** WeKan bundle, and it is the
default database on the platforms MongoDB publishes no server for — ppc64le,
s390x, riscv64, i386, armv6 and armhf — where nothing else can be. There is also
a multi-arch image (`wekanteam/ferretdb`, `quay.io/wekan/ferretdb`,
`ghcr.io/wekan/ferretdb`) built `FROM scratch` around these same binaries, which
is why it covers every Linux one of them, including `linux/arm/v6` and
`linux/loong64` that the WeKan image cannot reach — see
[Docker CPU platforms](../../../Platforms/FOSS/Container/Docker/CPU-platforms.md).
The `hana` handler is behind the `ferretdb_hana` build tag; the released binaries
are built with it, so `--handler=hana` exists. A FerretDB built elsewhere without
@@ -75,6 +93,53 @@ merely setting `MONGO_OPLOG_URL` starts a tail regardless of the reactivity orde
FerretDB has no MongoDB change streams at all, in either version.
## Why the URL says `directConnection=true`
Every FerretDB v1 compose file connects with
`mongodb://ferretdb:27017/wekan?directConnection=true`, and dropping that
parameter breaks the stack on the first start
([#6582](https://github.com/wekan/wekan/issues/6582)):
```
MongoServerSelectionError: connect ECONNREFUSED 0.0.0.0:27017
reason: TopologyDescription {
type: 'ReplicaSetNoPrimary',
servers: Map(1) { '0.0.0.0:27017' => [ServerDescription] },
setName: 'rs0', ... }
```
`0.0.0.0` appears in no compose file. It is FerretDB's own **listen** address,
and the driver was handed it by the server.
Because the `ferretdb` service runs with `--repl-set-name=rs0` (see
[The OpLog](#the-oplog) above), FerretDB answers the `hello` handshake as a
one-member replica set, and fills the `hosts`, `me` and `primary` fields with its
`--listen-addr` — the wildcard `0.0.0.0:27017`. A MongoDB driver that is not in
direct-connection mode treats such a reply as an invitation to do replica-set
**discovery**: it adopts the member list the server advertised, and drops the
seed it was given, because the server reports a name other than the one that was
dialled. So `mongodb://ferretdb:27017` turns into `0.0.0.0:27017`, which inside
the `wekan-app` container means that container itself — where nothing is
listening. Hence `ECONNREFUSED`, on a compose file that was never edited.
`directConnection=true` tells the driver to stay on exactly the host it was
given and skip discovery entirely. Measured against FerretDB v1.49.0 with the
driver the bundle ships:
| MONGO\_URL | Topology the driver ends up with |
| --- | --- |
| `mongodb://localhost:27017/wekan` | `ReplicaSetWithPrimary`, servers: `0.0.0.0:27017` — the seed was discarded |
| `mongodb://localhost:27017/wekan?directConnection=true` | `Single`, servers: `localhost:27017` |
It costs nothing else. The handshake still reports `setName: 'rs0'`, and that is
the only thing Meteor checks before it will tail an OpLog, so
`MONGO_OPLOG_URL=...?replicaSet=rs0&directConnection=true` keeps working exactly
as [#6480/#6481](https://github.com/wekan/wekan/issues/6480) left it.
This applies only to FerretDB. The MongoDB compose files talk to a real replica
set whose members are configured with names that do resolve, so they keep plain
`replicaSet=rs0` and must **not** gain `directConnection`.
## Do they all answer the same?
[Conformance.md](Conformance.md) — `./build.sh` → Tests → **All databases
+6 -1
View File
@@ -2,7 +2,12 @@
FerretDB speaks the MongoDB wire protocol and stores the data in something else,
so WeKan runs unchanged on top of it — WeKan only ever connects to
`mongodb://ferretdb:27017/wekan` and knows nothing about what is behind it.
`mongodb://ferretdb:27017/wekan?directConnection=true` and knows nothing about
what is behind it. The `directConnection=true` is required, not decoration:
without it the driver follows FerretDB's replica-set handshake to the wildcard
address FerretDB listens on and the stack cannot start
([#6582](https://github.com/wekan/wekan/issues/6582), explained in
[1/README.md](1/README.md#why-the-url-says-directconnectiontrue)).
There are two versions, and they are different products:
+29 -5
View File
@@ -38,11 +38,35 @@ before it is uploaded — a `wekan_*.snap` published from here would overwrite t
default snap — and the two repositories were synced by hand on 2026-07-29, so they
are the newest WeKan with the snap name changed and nothing else.
The Docker side is deliberately **manual**: `wekanteam/wekan-gantt-gpl` and
`wekanteam/wekan-ondra` are published by
[`.github/workflows/docker-variant.yml`](../../../.github/workflows/docker-variant.yml)
("Publish variant Docker image (manual)") or `releases/docker-publish-variant.sh`,
which retag the released `wekanteam/wekan` manifest rather than rebuilding it.
The Docker side is automatic on **all three registries**. `release-all.yml`'s
`docker` job tags `wekan-ondra` and `wekan-gantt-gpl` — on Docker Hub, Quay and
GHCR — in the SAME `docker buildx build --push` as `wekan` itself, so the variant
tags carry the release's own digests for every architecture. No second build, and
no window in which a variant image can differ from the release it names.
Two guards ride with it. The push verification asks each registry what it
actually has, and a second step asks the way a STRANGER would — an anonymous pull
token and a manifest request — because a private image passes every check made
with the release's own credentials. That is how `ghcr.io/wekan/ferretdb` shipped
private until Artifact Hub mailed about it.
That second guard matters most the first time: `wekanteam/wekan-ondra` and
`quay.io/wekan/wekan-ondra` do not exist yet, and a repository created by a first
push takes the registry's default visibility — Docker Hub public, **Quay
private**. So the first release after this change is expected to fail that step
for the Quay one, with the page to open and the setting to change; the image is
already pushed, so making it public and re-running the job is all it takes.
[`docker-variant.yml`](../../../.github/workflows/docker-variant.yml) and
`releases/docker-publish-variant.sh` remain for everything outside a release:
publishing a variant for an older version, repairing a tag, or pointing a variant
somewhere deliberately.
Both rest on the same fact, and it is the thing to check before touching either:
the variant repositories are byte-identical to `wekan/wekan` apart from the snap
`name:` in `snapcraft.yaml`. If that ever stops being true, the GHCR tags in
`release-all.yml` and the retag in `docker-variant.yml` both become a lie, and the
variant needs its own build from its own Dockerfile.
## Remaining steps
+1 -1
View File
@@ -43,7 +43,7 @@ Files in this directory are served by meteor as-is to the client. It hosts some
* **[popup.tpl.jade](../../client/components/main/popup.tpl.jade)**: tpl files only define a single template so there's no need to wrap content in a template tag; the name of the template is the base name of the file (`popup` in this case);
* **[spinner.tpl.jade](https://github.com/wekan/wekan/tree/main/client/components/main/spinner.tpl.jade)**: is the template for a "waiting" dialog;
* **[settings](../../client/components/settings)**:
* **[informationBody.jade](../../client/components/settings/informationBody.jade)**: `information`, `statistics` templates;
* **[informationBody.jade](../../client/components/settings/informationBody.jade)**: the `statistics` template — the Version pane of Admin Panel / Settings, five tables (Platform, OS, Meteor, Database, Node). There is no `information` template any more: it is a pane rendered by `settingBody.jade`, not a page;
* **[invitationCode.jade](../../client/components/settings/invitationCode.jade)**: `invitationCode` template;
* **[peopleBody.jade](../../client/components/settings/peopleBody.jade)**: `people`, `peopleGeneral`, `peopleRow`, `editUserPopup`;
* **[settingBody.jade](../../client/components/settings/settingBody.jade)**: `setting`, `general`, `email`, `accountSettings`, `announcementSettings`
+42 -30
View File
@@ -19,25 +19,29 @@ Everything below is what the **server** allows. The server is the authority: the
UI can only hide buttons, and where the two disagree it is listed under
[Where the UI is more generous than the server](#where-the-ui-is-more-generous-than-the-server).
| Role | Member flag | Which cards they see | Comment | Create / edit cards, lists, swimlanes, checklists — and move a card | Board settings, members, roles |
| Role | Member flag | Which cards they see | Comment | Create / edit cards, lists, swimlanes, checklists | Board settings, members, roles | Move a card, assign yourself |
|---|---|---|---|---|---|
| **Board admin** | `isAdmin` | all | yes | yes | **yes** |
| **Normal** | *(none set)* | all | yes | yes | no |
| **Normal, assigned only** | `isNormalAssignedOnly` | **only assigned to them** | yes | yes | no |
| **No comments** | `isNoComments` | all | **no** | yes | no |
| **Comment only** | `isCommentOnly` | all | yes | no | no |
| **Comment only, assigned** | `isCommentAssignedOnly` | **only assigned to them** | yes | no | no |
| **Worker** | `isWorker` | all | yes | no ⚠ | no |
| **Read only** | `isReadOnly` | all | no | no | no |
| **Read only, assigned** | `isReadAssignedOnly` | **only assigned to them** | no | no | no |
| **Board admin** | `isAdmin` | all | yes | yes | **yes** | yes |
| **Normal** | *(none set)* | all | yes | yes | no | yes |
| **Normal, assigned only** | `isNormalAssignedOnly` | **only assigned to them** | yes | yes | no | yes |
| **No comments** | `isNoComments` | all | **no** | yes | no | yes |
| **Comment only** | `isCommentOnly` | all | yes | no | no | no |
| **Comment only, assigned** | `isCommentAssignedOnly` | **only assigned to them** | yes | no | no | no |
| **Worker** | `isWorker` | all | yes | no | no | **yes** |
| **Read only** | `isReadOnly` | all | no | no | no | no |
| **Read only, assigned** | `isReadAssignedOnly` | **only assigned to them** | no | no | no | no |
⚠ = the code does not do what the role's name says — see
[Known gaps](#known-gaps). Nothing here is written to make the table say
something nicer than the code does.
**Why moving a card is not its own column.** A move is a card *update*, so it goes
through the same rule as editing one. There is no way for a role to move a card
but not edit it, which is exactly the Worker problem below.
**Why moving a card IS its own column** (#3189). A move is a card *update*, so for
a long time it went through the same rule as editing one — and the Worker role,
whose whole purpose is moving cards and assigning itself, could do neither.
Moving and self-assigning are their own capability now, `moveCard`, enforced on
the server field by field (`models/lib/workerCardWrite.js`). Every role that can
create and edit can also move, so the column adds one answer rather than changing
eight.
The same table is in the product: **Admin Panel → People → Roles**, under the Save
button, as the read-only *Roles Status* pane. It is rendered from
@@ -73,9 +77,15 @@ comments, attachments, checklists and checklist items follow the same scope.
**Board settings, members, roles** is `isBoardAdmin()`.
**Move cards** is `Utils.canMoveCard()` in the UI; on the server a move is a card
update, so it goes through the write-access rule above — which is why Worker,
whose whole purpose is moving cards, is marked "yes" here but "no" for editing.
**Move a card, assign yourself** is the `moveCard` capability:
`Utils.canMoveCard()` in the UI, and on the server `canUpdateCard()` in
`server/permissions/cards.js`, which falls through to
`models/lib/workerCardWrite.js` for a member who has `moveCard` but not `write`.
That policy reads the update itself and allows only a move (`listId`,
`swimlaneId`, `sort`, and the `dateLastActivity` / `modifiedAt` written with them)
or an `$addToSet` / `$pull` of the caller's OWN id on `assignees`. Anything else —
a title, a label, somebody else's name, a whole-document replacement, an operator
it does not recognise — is refused.
## Fixed
@@ -96,7 +106,20 @@ different places, and the three lists had drifted. They now read one table, and
the edit buttons for. It blocks commenting only now, which is what the name and
the board schema both say it is for.
3. **The write rule did not exempt board admins.** Every `has*()` helper on the
3. **A Worker could not move a card or assign itself** (#3189) — the two things
the role exists for. The board schema calls it "only allowed to move card,
assign himself to card and comment"; both are card *updates*, so both went
through the write rule, which excludes Worker. The reporter saw it as a card
that showed their own name for a moment and then showed the previous assignee
again: an optimistic write the server threw away.
The fix is the field-level policy the old "Known gaps" entry said this needed,
and not a widening of `write`: `moveCard` is its own capability, and
`models/lib/workerCardWrite.js` decides each update by what it writes. A
Worker may move a card and add or remove their own id in `assignees`;
everything else, including assigning somebody else, is refused by default.
4. **The write rule did not exempt board admins.** Every `has*()` helper on the
board ignores a flag on an admin; the write rule read the raw flags, so an
admin who also carried `isNoComments` silently lost write access. Not reachable
from the Web UI, which writes all eight flags at once, but reachable over the
@@ -110,19 +133,8 @@ somebody whose write the server then refused. All three now ask the same table.
## Known gaps
4. **A Worker cannot move a card**, which is the one thing the role is for. The
board schema calls it "only allowed to move card, assign himself to card and
comment", and moving a card is a card *update*, so it goes through the write
rule — which excludes Worker. The UI used to offer the drag anyway and the
server refused it; now the UI does not offer it either, so the role is at least
honest, but it is still a role that does nothing except comment.
Not fixed here because the fix is not a table edit: it means letting a role
write SOME fields of a card (`listId`, `swimlaneId`, `sort`, and `boardId`
under the existing cross-board guard) and not others, and "assign himself"
means validating that a member/assignee change only ever adds the caller. That
is a field-level policy on the path every card update takes, and it wants
deciding on purpose rather than as a side effect of documenting the table.
None recorded. A new one belongs here, with the reason it is not fixed — not in
the table as a softer word.
## Setting a role
+56
View File
@@ -293,6 +293,62 @@ each knows how to re-apply its own `previousContent`.
- No client-exposed update/delete on `cardGroupHistory` (append-only invariant).
- Retention cap per card/board via a server cron (reuse the `userPositionHistory.cleanup` pattern).
## 9a. Append-only is what makes two copies of a database mergeable
This section is here because the snap now depends on it (#6583, #6585). It is a
consequence of section 9's invariant, not a new rule.
**The situation.** A WeKan snap can end up holding TWO copies of its data that
have both been written to since they were copies of each other: the MongoDB to
FerretDB migration is a snapshot and nothing keeps it in step, `snap revert` does
not roll back `$SNAP_COMMON`, and `snap set wekan database=...` can be switched
back and forth. Which copy gets served then decides what a user sees, and getting
it wrong looks exactly like data loss — that is what both of those issues were.
**Why history decides it.** File timestamps cannot answer "which copy holds the
work": an mtime says when a file was touched, and merely starting a database
touches its files. The DATA can answer it, and history is the part of the data
that answers it best — every change a user makes writes a row, so the newest
history row is the newest moment somebody was actually working, on either side.
`snap-src/bin/db-eval.mjs evidence` reads exactly that (per-collection counts plus
the newest timestamp any document carries) and `snap-src/bin/database-choose.mjs`
compares the two.
**Why the other copy is not lost.** Because history is APPEND-ONLY — never
rewritten in place, never updated, only added to — rows from one copy can be
inserted into the other without contradicting anything already there. So the snap
serves the copy holding the newer work and copies into it every document whose
`_id` is ABSENT from it (`snap-src/bin/database-merge-missing.mjs`):
- nothing that exists in the served copy is overwritten, so the newer version of
a card that was edited on both sides stands;
- nothing is deleted, on either side, and the copy that was not chosen stays on
disk — switching back is still one `snap set` away;
- the activities, comments and (once this design ships) `changeHistory` rows
written on the other copy become part of the served copy's history, so the work
done there is READABLE IN THE CARD'S HISTORY rather than stranded in a database
nobody opens.
**What is deliberately not attempted.** Reconciling two edits of the same field —
a three-way merge — is a decision about somebody's work and is not made by a
script. When the two copies cannot be told apart (their newest moments are within
hours of each other, or neither carries a timestamp), the snap changes nothing and
says so, which is the behaviour #6583 arrived at the hard way.
**What this design owes the snap**, when it ships:
1. Every `changeHistory` row keeps a stable, content-derived or random `_id` that
is never reused, so "absent by `_id`" is a safe test for "this row is not here".
2. Rows stay immutable after insert (section 9 already requires this); a row that
could be updated in place would make two copies of it disagree, and the merge
would then have to choose between them.
3. The retention cron prunes by age, not by rewriting rows, so a pruned copy and a
full copy merge to the full one rather than to a contradiction.
4. `changeHistory` is listed in the merge's collection list
(`MERGE_COLLECTIONS` in `snap-src/bin/database-choose.mjs`) the moment the
collection exists — the list is the only place the snap learns which
collections carry history.
## 10. Phasing (each phase verified live before the next)
1. **`changeHistory` model + write helper + pure helpers (paging/search/selection/pick undo-redo) +
@@ -0,0 +1,210 @@
# WeKan Docker image — CPU platforms
This page documents, **for the WeKan Docker image**, which CPU platforms the
multi-arch manifest carries, how that set is decided on every release, which
database each platform starts by default, and which bundle platforms are
deliberately not images.
The snap has the same page of its own:
[Snap CPU platforms](../Snap/CPU-platforms.md).
## Summary
- **The image is assembled from the release bundles, not compiled per platform.**
`Dockerfile` downloads `wekan-<version>-<arch>.zip` from the GitHub Release for
the platform it is building, installs a Node.js resolved by
[`releases/resolve-node-source.sh`](../../../../../releases/resolve-node-source.sh)
— the same script the bundles and the snap use, so one architecture's image and
bundle always carry the same Node.js from the same source — and runs the
bundle. There is no per-platform compile in the image at all.
- **At most eight Linux platforms.** `linux/amd64` and `linux/arm64` are
**required**; `linux/ppc64le`, `linux/s390x`, `linux/riscv64`, `linux/386`,
`linux/arm/v6` and `linux/arm/v7` are **optional** and included only when their
bundle landed on the release **and** the base image publishes the platform.
- **A missing optional bundle drops only itself.** One failed extra-arch build
removes that platform from the manifest with a warning; it does not fail or
skip the Docker job, and the platform comes back on the next release that
builds it.
- **`linux/arm/v6` is a candidate that drops itself today**, because
`debian:trixie` publishes `arm/v5` and `arm/v7` and nothing between them. See
[armv6](#armv6-wired-end-to-end-waiting-on-a-base-image) — this is the one
platform whose absence is not about WeKan at all.
- **Three registries, two tags each**: `wekanteam/wekan`, `quay.io/wekan/wekan`
and `ghcr.io/wekan/wekan`, at `:v<version>` and `:latest`, built with
`--provenance=mode=max` and verified after the push by asking each registry
what it actually has.
- **Not image platforms:** loong64 (no base image exists), armv6, armv7, and
every Windows and macOS bundle. Each has a reason below, and none of them is an
oversight.
## Per-platform matrix
| Docker platform | `TARGETARCH` | `TARGETVARIANT` | Bundle | Required | Database when nothing is set |
|-----------------|--------------|-----------------|--------|:--------:|------------------------------|
| `linux/amd64` | `amd64` | — | `amd64` | ✅ | external MongoDB (`MONGO_URL`) |
| `linux/arm64` | `arm64` | (`v8`, implied) | `arm64` | ✅ | external MongoDB (`MONGO_URL`) |
| `linux/ppc64le` | `ppc64le` | — | `ppc64le` | ❌ | bundled FerretDB v1 (SQLite) |
| `linux/s390x` | `s390x` | — | `s390x` | ❌ | bundled FerretDB v1 (SQLite) |
| `linux/riscv64` | `riscv64` | — | `riscv64` | ❌ | bundled FerretDB v1 (SQLite) |
| `linux/386` | `386` | — | `i386` | ❌ | bundled FerretDB v1 (SQLite) |
| `linux/arm/v6` | `arm` | `v6` | `armv6` | ❌ | bundled FerretDB v1 (SQLite) |
| `linux/arm/v7` | `arm` | `v7` or unset | `armhf` | ❌ | bundled FerretDB v1 (SQLite) |
**`TARGETARCH` alone does not identify a platform.** ARMv6 and ARMv7 both arrive
as `arm`; only `TARGETVARIANT` tells them apart, which is why the `Dockerfile`'s
architecture `case` branches on the pair. Mapping `arm` straight to `armhf`, as
it did before, would hand a Raspberry Pi 1 the ARMv7-A bundle. `v5` (armel) is
refused outright rather than falling through: Go builds ARMv5, Node.js does not
exist for it, so there is no bundle to put in an image.
**Required** means the Docker job fails if that bundle is not on the release:
without amd64 and arm64 there is no image worth pushing, and a release that got
that far would already have failed.
**The database column is a default, not a limit.** Every bundle carries a
`ferretdb` binary, so FerretDB v1 runs on every platform; the default is decided
by [`releases/ferretdb/wekan-entrypoint.sh`](../../../../../releases/ferretdb/wekan-entrypoint.sh)
from a `/build/.ferretdb-default` marker that every extra-architecture bundle
carries and the amd64/arm64 bundles have removed. `WEKAN_DB=ferretdb` or
`WEKAN_DB=mongodb` overrides it on any platform, and setting `MONGO_URL` on a
FerretDB-default platform points WeKan at that database instead.
## How the platform set is decided, on every release
The `docker` job in
[`.github/workflows/release-all.yml`](../../../../../.github/workflows/release-all.yml)
does this before it builds anything:
1. It holds two lists — `req="linux/amd64:amd64 linux/arm64:arm64"` and
`opt="linux/ppc64le:ppc64le linux/s390x:s390x linux/riscv64:riscv64
linux/386:i386 linux/arm/v6:armv6 linux/arm/v7:armhf"` — where each entry is
*docker platform : the bundle arch the Dockerfile downloads for it*.
2. It asks the **base image** which platforms it publishes
(`docker buildx imagetools inspect`, with the base name read out of the
`Dockerfile` so there is no second copy of it), and drops any candidate the
base lacks.
3. It asks the **release**, with an HTTP `HEAD`, whether
`wekan-<version>-<arch>.zip` is there.
A missing **required** base platform or bundle is an error; a missing
**optional** one is a warning and that platform is dropped. The chosen set then
drives all three of the buildx `--platform` list, the wait-for-the-assets loop,
and the post-push verification — so the three can never disagree about what the
image was supposed to contain.
**Why the base is asked at all**, when a missing base would surely just fail the
build: for 32-bit ARM it does not fail. containerd treats a **lower** ARM variant
as compatible, so a `linux/arm/v6` request against a base that has `arm/v5` and
`arm/v7` resolves to `arm/v5` — Debian **armel**, soft-float — and an image built
on that would carry a hard-float `node-armv6` its own loader cannot start. A
silent downgrade is worse than a dropped platform, so it is dropped, loudly,
with the reason in the warning.
`tests/releaseDockerPlatforms.test.cjs` pins that shape: which platforms are
required, which are optional, that `linux/loong64` is never among them, that
every platform resolves to the bundle the Decide step paired it with, that the
`arm` branch reads `TARGETVARIANT` and refuses `v5`, and that both loops check
the base.
## The names, and the ones that do not match
An architecture is spelled one way by Docker, another by Debian, and a third by
the WeKan bundles. Three of them differ here:
| Docker | `TARGETARCH` | WeKan bundle | Note |
|--------|--------------|--------------|------|
| `linux/386` | `386` | `i386` | everyone but Docker and Node.js says i386 |
| `linux/arm/v7` | `arm` | `armhf` | Debian's 32-bit ARM port: ARMv7-A, VFPv3-D16 |
| `linux/arm64` | `arm64` | `arm64` | buildx reports it as `linux/arm64/v8`; the verify step strips the implied `/v8` |
The `Dockerfile` maps `TARGETARCH` to the bundle name in one `case` and **exits
on anything it does not know**, rather than guessing — an unrecognised platform
would otherwise download another CPU's `.zip` and produce an image that starts
and then dies.
## What is not an image platform, and why
### loong64 — there is no base image to build on
`node-loong64` and `ferretdb-loong64` both exist and the bundle is built, but no
Docker base image publishes loong64 at any tag — not debian, not ubuntu — so
there is no loong64 userland to install into, and the registries do not agree on
what the platform is even called. `tests/releaseDockerPlatforms.test.cjs`
asserts `linux/loong64` is not in the platform list, so this stays a decision
rather than something that quietly reappears.
### armv6 — wired end to end, waiting on a base image
ARMv6 (Raspberry Pi 1 and Zero) is a **candidate platform**: `linux/arm/v6` is in
the optional list, the `Dockerfile` resolves it to the `armv6` bundle through
`TARGETVARIANT`, and the guard test pins both. Everything the image needs exists —
`node-armv6` from [wekan/node-patches](https://github.com/wekan/node-patches)
(nobody else publishes an ARMv6 Node.js) and `GOARM=6` binaries from
[wekan/FerretDB](https://github.com/wekan/FerretDB) and
[wekan/mongo-tools-patches](https://github.com/wekan/mongo-tools-patches).
**What is missing is a base image.** `debian:trixie`'s manifest list is `386`,
`amd64`, `arm64/v8`, `arm/v5`, `arm/v7`, `ppc64le`, `riscv64`, `s390x` — there is
no `arm/v6`. Debian has no ARMv6 port: its 32-bit ARM ports are armel (ARMv5,
soft-float) and armhf (ARMv7-A). ARMv6 hard-float is Raspberry Pi OS territory,
not Debian's. So the release drops the platform with a warning and the image
ships without it, while the **armv6 bundle zip is unaffected** — that is how an
ARMv6 board runs WeKan today.
The day the base publishes `arm/v6`, or the base for that one platform becomes an
ARMv6 userland, the platform is included automatically: nothing else has to
change. Until then, do **not** "fix" this by removing the base check — that is
what makes the drop a warning instead of an image whose Node.js cannot start.
`models/lib/snapArchitectures.js` describes armv6 as shipping "a bundle zip and a
`linux/arm/v6` Docker image"; the bundle is real today, the image is prepared and
gated.
### armv7 — one 32-bit ARM slot, and it carries the baseline
There is one `linux/arm/v7` platform, and it must run on any ARMv7-A board, so it
carries the **armhf** bundle: the `armv7` bundle assumes NEON, which is an
illegal instruction on an ARMv7-A board without it. This is the same reasoning
that gives armv7 no snap — see
[the snap page](../Snap/CPU-platforms.md#armhf-and-armv7-are-the-same-cpu-family-built-to-different-baselines).
### win64, win32, win-arm64, mac-x64, mac-arm64
Bundle platforms that are not Linux. They are not container images and are not
expected to be.
## Variant images
[`docker-variant.yml`](../../../../../.github/workflows/docker-variant.yml)
publishes the variant repositories, **manually and never as part of a release**.
It rebuilds nothing: `docker buildx imagetools create` copies the *manifest*, so
a variant tag points at the very digests the release built and verified, and can
never drift from `wekanteam/wekan`. Before and after the retag it checks that
`linux/amd64`, `linux/arm64`, `linux/ppc64le`, `linux/s390x` and `linux/riscv64`
are all present, so a variant cannot silently cover fewer architectures than
`wekan` does. `releases/docker-publish-variant.sh` is the same thing locally.
## The FerretDB image covers more platforms than the WeKan image
[wekan/FerretDB](https://github.com/wekan/FerretDB)'s own `docker.yml` builds
`FROM scratch` around the **prebuilt** per-architecture Go binaries, with no
QEMU and no base image, so a platform costs it nothing but a binary. Its manifest
is computed from whichever `ferretdb-<arch>` assets downloaded, mapping
`amd64`→`linux/amd64`, `arm64`→`linux/arm64`, `armhf`→`linux/arm/v7`,
`armv6`→`linux/arm/v6`, `armel`→`linux/arm/v5`, `i386`→`linux/386`,
`ppc64le`→`linux/ppc64le`, `s390x`→`linux/s390x`, `riscv64`→`linux/riscv64` and
`loong64`→`linux/loong64`. It is published as `wekanteam/ferretdb`,
`quay.io/wekan/ferretdb` and `ghcr.io/wekan/ferretdb`.
That is why the two lists differ: the WeKan image needs a Debian userland and a
Node.js for a platform, and the FerretDB image needs only one static binary.
## Related
- [Snap CPU platforms](../Snap/CPU-platforms.md) — the same page for the snap
- [Which compose file, which database](README.md)
- [FerretDB v1 and its backends](../../../../Databases/FerretDB/1/)
- The platform list in code: the `docker` job of
[`release-all.yml`](../../../../../.github/workflows/release-all.yml), guarded by
`tests/releaseDockerPlatforms.test.cjs`
- Node.js per platform: [wekan/node-patches](https://github.com/wekan/node-patches)
@@ -1,3 +1,6 @@
Which CPU platforms the image is published for, and why some bundle platforms are
not images: [CPU-platforms.md](CPU-platforms.md).
## WeKan Docker compose files (which database?)
Since the default database changed, the compose files in https://github.com/wekan/wekan are:
@@ -10,8 +10,8 @@ and how the one-time MongoDB 3 → FerretDB v1 migration works.
FerretDB v1 is the wekan/FerretDB fork with an embedded pure-Go SQLite backend
that speaks the MongoDB wire protocol, so WeKan talks to it exactly like MongoDB.
- **Six architectures are built as snaps**: amd64, arm64, armhf, ppc64el, riscv64
and s390x — exactly `snapcraft.yaml`'s `build-for:` list. i386 and armv7 are
release bundles but **not** snaps, for reasons that are not oversights; see
and s390x — exactly `snapcraft.yaml`'s `build-for:` list. i386, armv6 and armv7
are release bundles but **not** snaps, for reasons that are not oversights; see
[CPU architecture names](#cpu-architecture-names-and-what-they-mean).
- **MongoDB 7 *server* binaries (mongod) are included for amd64 and arm64 only.**
The other architectures (armhf, ppc64el, riscv64, s390x) have no MongoDB server
@@ -91,6 +91,32 @@ the `armhf` bundle, and **`armv7` has no snap** — it ships as a bundle only. A
separate snap name or track would be the only way to offer the NEON build, which
is not worth a second listing for one board family.
### armv6 has no snap, because the store has no such architecture
ARMv6 is the Raspberry Pi 1 and the Pi Zero: 32-bit ARM with **VFPv2** hardware
floating point and **no NEON**. The Snap Store's only 32-bit ARM architecture is
`armhf`, which is the **ARMv7-A** hard-float baseline — an ARMv6 board cannot run
an armhf snap, and there is no armv6 architecture to publish one as. So there is
nothing to fix here and nothing to add: **armv6 ships as a release bundle zip**,
and nothing else in the Snap Store. (It is a candidate platform of the Docker
image too, gated on a base image that publishes `linux/arm/v6` — see
[Docker CPU platforms](../Docker/CPU-platforms.md#armv6-wired-end-to-end-waiting-on-a-base-image).)
It is a separate build from armhf all the way down, not a re-tag of it:
[wekan/node-patches](https://github.com/wekan/node-patches) builds `node-armv6`
(`--with-arm-fpu=vfp`, `-march=armv6+fp`) because nobody publishes an ARMv6
Node.js any more — nodejs.org dropped it after Node 11 —
and [wekan/FerretDB](https://github.com/wekan/FerretDB) and
[wekan/mongo-tools-patches](https://github.com/wekan/mongo-tools-patches) build
`GOARM=6` binaries beside their armhf ones. Their `armel` (`GOARM=5`) would run
on an ARMv6 board, which is what makes it look like a substitute: it does
floating point in **software**, so it is not one.
`models/lib/snapArchitectures.js` records this reason in
`NOT_SNAP_ARCHITECTURES`, beside i386's and armv7's, and
`tests/releaseSnapArches.test.cjs` asserts armv6 is neither a snap platform nor a
Launchpad build.
### i386 has no snap, and cannot have a new one
core24 is Ubuntu 24.04, which has **no i386 port**, so there is no i386 core24
@@ -107,11 +133,24 @@ version `0.X-ci`) because the store keeps whatever was ever uploaded. It is year
stale, nothing new can be built for it, and re-releasing it would only re-publish
`0.X-ci`. **i386 users are served by the .deb and the AppImage.**
### loong64, win64, win32, mac-x64, mac-arm64
### loong64, win64, win32, win-arm64, mac-x64, mac-arm64
Bundle platforms with no Snap Store architecture at all. They are not snaps and
are not expected to be.
### The whole bundle list, and which of it is a snap
The release builds fifteen bundles; six of them become snaps. The rest are
bundles (and, for some, Docker images) only:
| Bundle | Snap? | Why not |
|--------|-------|---------|
| `amd64`, `arm64`, `armhf`, `ppc64le` (→ `ppc64el`), `riscv64`, `s390x` | ✅ | — |
| `armv6` | ❌ | the store has no armv6 architecture |
| `armv7` | ❌ | the store's one 32-bit ARM slot must carry the armhf baseline |
| `i386` | ❌ | core24 has no i386 port |
| `loong64`, `win64`, `win32`, `win-arm64`, `mac-x64`, `mac-arm64` | ❌ | no Snap Store architecture at all |
## New installs
On a fresh install, every architecture starts on **FerretDB v1 (SQLite)** with
@@ -183,6 +222,73 @@ After a successful migration the snap switches to `database=ferretdb`.
Architectures other than amd64 ship no MongoDB server and no MongoDB 3 client, so
there is nothing to migrate from — they are FerretDB v1 from the first boot.
### Which MongoDB databases the snap can migrate from
**Three readers, on amd64 and arm64.** A MongoDB server only starts on data whose
`featureCompatibilityVersion` is at most one major behind it, so which versions
can be read is a property of which servers the snap carries:
| Reader | Opens data from | Used for |
|--------|-----------------|----------|
| mongod 7 (the server it runs) | MongoDB 6.0, 7.0 | the running database, and reading a 6/7 one to migrate |
| **mongod 4.2** (migration only) | MongoDB 4.0, 4.2 | reading a 4.x database to migrate |
| the MongoDB 3.2 tools (`migratemongo`) | MongoDB 3.2 | reading a 6.09-era database to migrate |
mongod 4.2 is bundled for **amd64 and arm64 only** — MongoDB publishes no 4.2 for
the others, and they have been FerretDB from their first boot, so there is nothing
there to migrate from. It carries its own OpenSSL 1.1, because core24 ships
OpenSSL 3 and the 4.2 build will not load without 1.1. It is used **only to read**
during a migration; the running database is never mongod 4.2.
What is still unreadable, therefore: **3.4, 3.6, 4.4 and 5.0**. A database from
one of those gets the page below rather than a migration.
### A database from a MongoDB none of the three can read
A database left by a **MongoDB 4.4 or 5.0** snap (or a 3.4/3.6 one) opens in none
of them — mongod 7 refuses it with
```
This version of MongoDB is too recent to start up on the existing data files.
Try MongoDB 4.2 or earlier.
```
and neither mongod 4.2 nor the 3.2 tools can open them either. Reading those
files needs a server the snap does not carry, so **no retry can succeed**, and
there is nothing the snap can migrate from.
What it does instead of trying: it **stops**, writes
`$SNAP_COMMON/.mongodb-data-too-old` with the version mongod itself named as
still able to read the data, and serves an explanatory page on the web port
instead of 502. MongoDB is not started (a start that cannot work would be
restarted by snapd forever, and used to bounce between mongod and the migration
three times before giving up), auto-migration is paused, and **nothing is
changed**: the database files, the attachments and the avatars are exactly as
they were.
Two ways forward, both keeping the data:
1. **Go back to the revision that worked** and stay there for now:
```
sudo snap revert wekan
sudo snap refresh --hold=forever wekan
```
2. **Move the data across** with a MongoDB that can read it (the version the page
names): start that MongoDB on the data directory, `mongodump` from it, restore
the dump into this version, and let the MongoDB → FerretDB migration above run.
Attachments and avatars are **files on disk**, not database rows, so they are
unaffected either way — see `$SNAP_COMMON/files`.
To let the snap try again — after moving the data, say — remove the marker:
```
sudo rm /var/snap/wekan/common/.mongodb-data-too-old
sudo snap restart wekan
```
## Where the FerretDB v1 binary comes from
FerretDB v1 is built and released separately in
@@ -263,22 +369,35 @@ publishes edge builds to stable users. Pass the version, or `--dry-run` first.
### What each snap currently has, and what is still to add
As of WeKan 10.76 the store holds:
As of WeKan **10.78** the store holds:
| Snap | amd64 | arm64 | armhf | ppc64el | riscv64 | s390x |
|------|:-----:|:-----:|:-----:|:-------:|:-------:|:-----:|
| `wekan` | ✅ | ✅ | — | ✅ | — | ✅ |
| `wekan-ondra` | ✅ | ✅ | ⚠️ | — | — | — |
| `wekan-gantt-gpl` | ✅ | ✅ | — | — | — | — |
| `wekan` | ✅ 10.78 | ✅ 10.78 | ❌ | ✅ 10.78 | ✅ 10.78 | ✅ 10.78 |
| `wekan-ondra` | ✅ 10.78 | ✅ 10.78 | ⚠️ 0.22 | 🚫 | 🚫 | 🚫 |
| `wekan-gantt-gpl` | ✅ 10.78 | ✅ 10.78 | 🚫 | 🚫 | 🚫 | 🚫 |
- **—** the architecture is built by `snapcraft.yaml` but that snap has no
revision for it yet. Uploading one is all that is needed; the release script
then puts it on all four channels.
- **⚠️** `wekan-ondra`'s armhf is stuck at version **0.22**, and its i386 column at
**0.X-ci**, on all four channels — fossils the store kept from years ago.
Promoting channels cannot fix these: there is no newer revision to promote.
armhf needs a build uploaded for that snap; i386 cannot be built at all (see
[i386](#i386-has-no-snap-and-cannot-have-a-new-one)).
`wekan-ondra` also shows an **i386** column at `0.X-ci`; nothing builds it and
nothing can (see [i386](#i386-has-no-snap-and-cannot-have-a-new-one)).
The three marks mean three different things, and the difference is the whole
point of the table:
- **❌ built by the release, not in the store.** `wekan`'s armhf is the only one:
`snapcraft.yaml` declares it, the `snap-launchpad` job builds it, and it has
been failing — most recently on Caddy, which publishes no `linux_armhf` archive
because its asset names are Go's. That is fixed for the next release. Nothing
to do here but release.
- **🚫 the release does not build it for that snap at all.** The `snap-variants`
job that publishes `wekan-ondra` and `wekan-gantt-gpl` has FOUR matrix entries —
each variant on amd64 and on arm64, on GitHub runners. The exotic architectures
come from `snap-launchpad`, which builds only the `wekan` name. So no upload is
pending for these squares: they need the variant job extended to Launchpad
remote builds under the variant snap names first.
- **⚠️ a fossil.** `wekan-ondra`'s armhf is stuck at version **0.22** on all four
channels — a revision the store kept from years ago. Promoting channels cannot
fix it: there is no newer revision to promote, and per the row above nothing
currently builds one.
Publishing the **stable** channel is safe for these because the snap performs its
**automatic migrations** on start — the schema upgrades and the one-time
@@ -288,6 +407,8 @@ manual step.
## Related
- [Docker CPU platforms](../Docker/CPU-platforms.md) — the same page for the
Docker image, which has a different platform list and different reasons
- FerretDB v1 fork: https://github.com/wekan/FerretDB
- Snap install: [Install.md](Install.md)
- Snap settings keys: [Supported-settings-keys.md](Supported-settings-keys.md)
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-10.78-win64.zip](https://github.com/wekan/wekan/releases/download/v10.78/wekan-10.78-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-10.87-win64.zip](https://github.com/wekan/wekan/releases/download/v10.87/wekan-10.87-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
@@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-10.78-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-10.87-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+7 -7
View File
@@ -4,11 +4,11 @@ Status: **Core implemented · picker UI design** · Owner: xet7 · Related: #577
per-user theme), Board Settings / Change Color, Member Settings / Change Color, #5514
(the native color-wheel `<input type="color">` already used for list colors).
WeKan ships ~19 board theme colors. Many are near-duplicates that differ only by **one or
two accent colors**; a few (`clearblue`) are **two-color "color slides"** (gradients). This
document defines how the **Select Color** picker — used in **both** Board Settings and
Member Settings (the global override) — is reorganized into **categories** with **two-level
dropdowns** and **custom colors** where it makes sense.
WeKan ships named board theme colors. Many are near-duplicates that differ only by **one
or two accent colors**; the `clear*` themes are **two-color "color slides"** (gradients).
This document defines how the **Select Color** picker — used in **both** Board Settings
and Member Settings (the global override) — is reorganized into **categories** with
**two-level dropdowns** and **custom colors** where it makes sense.
## 1. Categories
@@ -19,9 +19,9 @@ The colors partition into four categories (single source of truth:
| Category | Colors | Custom colors |
|----------|--------|---------------|
| **flat** | belize, nephritis, pomegranate, pumpkin, wisteria, moderatepink, strongcyan, limegreen, natural | **1** (single accent) |
| **clear** | clearblue | **2** (color slide / gradient) |
| **clear** | clearblue, cleargreen, clearorange, clearpink, clearpurple, clearred | **2** (color slide / gradient) |
| **dark** | midnight, dark, moderndark, exodark, cleandark | **none** (fixed) |
| **special** | relax, corteza, modern, cleanlight | **none** (fixed) |
| **special** | relax, corteza, appleglasspastel, modern, cleanlight | **none** (fixed) |
Rationale: **flat** designs are one accent color over a flat surface, so a single custom color
fully re-skins them. **clear** designs are a two-color gradient ("slide"), so they take two
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Inisial",
"invalid-date": "Tanggal tidak sah",
"invalid-time": "Masa tidak sah",
"invalid-year": "Thon hana sah. Neutuléh mandum peuet angka, contoh jih 2026.",
"invalid-user": "Pengguna tidak sa",
"joined": "bergabung",
"just-invited": "Anda baru diundang di panel ini",
@@ -949,6 +950,12 @@
"global-webhook": "Cangkuk Web Global",
"new-outgoing-webhook": "Cangkuk Web Keluar Baru",
"no-name": "(Tidak Diketahui)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Pangkalan data",
"Node": "Node",
"Node_version": "Versi Node",
"Meteor_version": "Versi Meteor",
"Database_type": "Tipe database",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Mengubah ke markdown",
"import-board-zip": "Tamah file .zip nyang na file JSON papan, dan subdirektori nan papan ngon lampiran",
"collapse": "Ciutkan",
"collapse-checklist": "Ciutkan dapeuta peumeriksa",
"expand-checklist": "Peubuka dapeuta peumeriksa",
"uncollapse": "Peubuka",
"hideCheckedChecklistItems": "Sok item checklist nyang ka geucek",
"hideAllChecklistItems": "Sok mandum item checklist",
@@ -2339,5 +2348,8 @@
"event-severity": "Keuparah",
"event-action": "Tindakan",
"event-source": "Sumber",
"event-detail": "Rincian"
}
"event-detail": "Rincian",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Voorletters",
"invalid-date": "Ongeldige datum",
"invalid-time": "Ongeldige tyd",
"invalid-year": "Ongeldige jaar. Tik al vier syfers in, byvoorbeeld 2026.",
"invalid-user": "Ongeldige gebruiker",
"joined": "aangesluit het",
"just-invited": "Jy word net uitgenooi na hierdie bord",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Nuwe Uitgaande Webhook",
"no-name": "(Onbekend)",
"Platform": "Platform",
"package": "Pakket",
"OS": "Bedryfstelsel",
"Meteor": "Meteor",
"Database": "Databasis",
"Node": "Node",
"Node_version": "Node weergawe",
"Meteor_version": "Meteoor weergawe",
"Database_type": "Databasistipe",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Skakel om na markdown",
"import-board-zip": "Voeg .zip-lêer by wat bord-JSON-lêers het, en bordnaam-subgidse met aanhegsels",
"collapse": "Ineenstort",
"collapse-checklist": "Vou kontrolelys toe",
"expand-checklist": "Ontvou kontrolelys",
"uncollapse": "Ontvou",
"hideCheckedChecklistItems": "Versteek gemerkte kontrolelysitems",
"hideAllChecklistItems": "Versteek alle kontrolelys-items",
@@ -2339,5 +2348,8 @@
"event-severity": "Erns",
"event-action": "Aksie",
"event-source": "Bron",
"event-detail": "Besonderhede"
}
"event-detail": "Besonderhede",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Voorletters",
"invalid-date": "Ongeldige datum",
"invalid-time": "Ongeldige tyd",
"invalid-year": "Ongeldige jaar. Tik al vier syfers in, byvoorbeeld 2026.",
"invalid-user": "Ongeldige gebruiker",
"joined": "aangesluit het",
"just-invited": "Jy word net uitgenooi na hierdie bord",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Nuwe Uitgaande Webhook",
"no-name": "(Onbekend)",
"Platform": "Platform",
"package": "Pakket",
"OS": "Bedryfstelsel",
"Meteor": "Meteor",
"Database": "Databasis",
"Node": "Node",
"Node_version": "Node weergawe",
"Meteor_version": "Meteoor weergawe",
"Database_type": "Databasistipe",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Skakel om na markdown",
"import-board-zip": "Voeg .zip-lêer by wat bord-JSON-lêers het, en bordnaam-subgidse met aanhegsels",
"collapse": "Ineenstort",
"collapse-checklist": "Vou kontrolelys toe",
"expand-checklist": "Ontvou kontrolelys",
"uncollapse": "Ontvou",
"hideCheckedChecklistItems": "Versteek gemerkte kontrolelysitems",
"hideAllChecklistItems": "Versteek alle kontrolelys-items",
@@ -2339,5 +2348,8 @@
"event-severity": "Erns",
"event-action": "Aksie",
"event-source": "Bron",
"event-detail": "Besonderhede"
}
"event-detail": "Besonderhede",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -949,6 +950,12 @@
"global-webhook": "وب‌هوک‌های سراسری",
"new-outgoing-webhook": "ويبهوك جديدة",
"no-name": "(غير معروف)",
"Platform": "المنصة",
"package": "الحزمة",
"OS": "نظام التشغيل",
"Meteor": "Meteor",
"Database": "قاعدة البيانات",
"Node": "Node",
"Node_version": "إصدار النود",
"Meteor_version": "نسخه متئور",
"Database_type": "نوع قاعدة البيانات",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
@@ -2339,5 +2348,8 @@
"event-severity": "الخطورة",
"event-action": "الإجراء",
"event-source": "المصدر",
"event-detail": "التفاصيل"
}
"event-detail": "التفاصيل",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -949,6 +950,12 @@
"global-webhook": "وب‌هوک‌های سراسری",
"new-outgoing-webhook": "ويبهوك جديدة",
"no-name": "(غير معروف)",
"Platform": "المنصة",
"package": "الحزمة",
"OS": "نظام التشغيل",
"Meteor": "Meteor",
"Database": "قاعدة البيانات",
"Node": "Node",
"Node_version": "إصدار النود",
"Meteor_version": "نسخه متئور",
"Database_type": "نوع قاعدة البيانات",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
@@ -2339,5 +2348,8 @@
"event-severity": "الخطورة",
"event-action": "الإجراء",
"event-source": "المصدر",
"event-detail": "التفاصيل"
}
"event-detail": "التفاصيل",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -949,6 +950,12 @@
"global-webhook": "وب‌هوک‌های سراسری",
"new-outgoing-webhook": "ويبهوك جديدة",
"no-name": "(غير معروف)",
"Platform": "المنصة",
"package": "الحزمة",
"OS": "نظام التشغيل",
"Meteor": "Meteor",
"Database": "قاعدة البيانات",
"Node": "Node",
"Node_version": "إصدار النود",
"Meteor_version": "نسخه متئور",
"Database_type": "نوع قاعدة البيانات",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
@@ -2339,5 +2348,8 @@
"event-severity": "الخطورة",
"event-action": "الإجراء",
"event-source": "المصدر",
"event-detail": "التفاصيل"
}
"event-detail": "التفاصيل",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة ماشي صالحة. كتب الأرقام الأربعة كاملين، بحال 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -949,6 +950,12 @@
"global-webhook": "وب‌هوک‌های سراسری",
"new-outgoing-webhook": "ويبهوك جديدة",
"no-name": "(غير معروف)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "قاعدة البيانات",
"Node": "Node",
"Node_version": "إصدار النود",
"Meteor_version": "نسخه متئور",
"Database_type": "نوع قاعدة البيانات",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
@@ -2339,5 +2348,8 @@
"event-severity": "الخطورة",
"event-action": "الإجراء",
"event-source": "المصدر",
"event-detail": "التفاصيل"
}
"event-detail": "التفاصيل",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha no válida",
"invalid-time": "Tiempo no válido",
"invalid-year": "Añu inválidu. Escribi les cuatro cifres, por exemplu 2026.",
"invalid-user": "Usuario no válido",
"joined": "se ha unido",
"just-invited": "Has sido invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo webhook saliente",
"no-name": "(Desconocido)",
"Platform": "Plataforma",
"package": "Paquete",
"OS": "Sistema operativu",
"Meteor": "Meteor",
"Database": "Base de datos",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Triba de base de datos",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Contraer la llista de comprobación",
"expand-checklist": "Espandir la llista de comprobación",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravedá",
"event-action": "Aición",
"event-source": "Fonte",
"event-detail": "Detalle"
}
"event-detail": "Detalle",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Baş hərflər",
"invalid-date": "Yanlış tarix",
"invalid-time": "Yanlış vaxt",
"invalid-year": "Yanlış il. Dörd rəqəmin hamısını yazın, məsələn 2026.",
"invalid-user": "Yanlış istifadəçi",
"joined": "qoşuldu",
"just-invited": "Siz sadəcə bu şuraya dəvətlisiniz",
@@ -949,6 +950,12 @@
"global-webhook": "Qlobal Webhooks",
"new-outgoing-webhook": "Yeni Gedən Webhook",
"no-name": "(Naməlum)",
"Platform": "Platforma",
"package": "Paket",
"OS": "Əməliyyat sistemi",
"Meteor": "Meteor",
"Database": "Verilənlər bazası",
"Node": "Node",
"Node_version": "Node versiyası",
"Meteor_version": "Meteor versiyası",
"Database_type": "Verilənlər bazası növü",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Markdown-a çevirin",
"import-board-zip": "Lövhə JSON faylları olan .zip faylı və əlavələri olan board adı alt kataloqları əlavə edin",
"collapse": "Yıxılma",
"collapse-checklist": "Yoxlama siyahısını yığ",
"expand-checklist": "Yoxlama siyahısını aç",
"uncollapse": "Açmaq",
"hideCheckedChecklistItems": "Yoxlanmış yoxlama siyahısı elementlərini gizlədin",
"hideAllChecklistItems": "Bütün yoxlama siyahısı elementlərini gizlədin",
@@ -2339,5 +2348,8 @@
"event-severity": "Ciddilik",
"event-action": "Əməliyyat",
"event-source": "Mənbə",
"event-detail": "Təfərrüat"
}
"event-detail": "Təfərrüat",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Baş hərflər",
"invalid-date": "Yanlış tarix",
"invalid-time": "Yanlış vaxt",
"invalid-year": "Yanlış il. Dörd rəqəmin hamısını yazın, məsələn 2026.",
"invalid-user": "Yanlış istifadəçi",
"joined": "qoşuldu",
"just-invited": "Siz sadəcə bu şuraya dəvətlisiniz",
@@ -949,6 +950,12 @@
"global-webhook": "Qlobal Webhooks",
"new-outgoing-webhook": "Yeni Gedən Webhook",
"no-name": "(Naməlum)",
"Platform": "Platforma",
"package": "Paket",
"OS": "Əməliyyat sistemi",
"Meteor": "Meteor",
"Database": "Verilənlər bazası",
"Node": "Node",
"Node_version": "Node versiyası",
"Meteor_version": "Meteor versiyası",
"Database_type": "Verilənlər bazası növü",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Markdown-a çevirin",
"import-board-zip": "Lövhə JSON faylları olan .zip faylı və əlavələri olan board adı alt kataloqları əlavə edin",
"collapse": "Yıxılma",
"collapse-checklist": "Yoxlama siyahısını yığ",
"expand-checklist": "Yoxlama siyahısını aç",
"uncollapse": "Açmaq",
"hideCheckedChecklistItems": "Yoxlanmış yoxlama siyahısı elementlərini gizlədin",
"hideAllChecklistItems": "Bütün yoxlama siyahısı elementlərini gizlədin",
@@ -2339,5 +2348,8 @@
"event-severity": "Ciddilik",
"event-action": "Əməliyyat",
"event-source": "Mənbə",
"event-detail": "Təfərrüat"
}
"event-detail": "Təfərrüat",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Baş hərflər",
"invalid-date": "Yanlış tarix",
"invalid-time": "Yanlış vaxt",
"invalid-year": "Yanlış il. Dörd rəqəmin hamısını yazın, məsələn 2026.",
"invalid-user": "Yanlış istifadəçi",
"joined": "qoşuldu",
"just-invited": "Siz sadəcə bu şuraya dəvətlisiniz",
@@ -949,6 +950,12 @@
"global-webhook": "Qlobal Webhooks",
"new-outgoing-webhook": "Yeni Gedən Webhook",
"no-name": "(Naməlum)",
"Platform": "Platforma",
"package": "Paket",
"OS": "Əməliyyat sistemi",
"Meteor": "Meteor",
"Database": "Verilənlər bazası",
"Node": "Node",
"Node_version": "Node versiyası",
"Meteor_version": "Meteor versiyası",
"Database_type": "Verilənlər bazası növü",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Markdown-a çevirin",
"import-board-zip": "Lövhə JSON faylları olan .zip faylı və əlavələri olan board adı alt kataloqları əlavə edin",
"collapse": "Yıxılma",
"collapse-checklist": "Yoxlama siyahısını yığ",
"expand-checklist": "Yoxlama siyahısını aç",
"uncollapse": "Açmaq",
"hideCheckedChecklistItems": "Yoxlanmış yoxlama siyahısı elementlərini gizlədin",
"hideAllChecklistItems": "Bütün yoxlama siyahısı elementlərini gizlədin",
@@ -2339,5 +2348,8 @@
"event-severity": "Ciddilik",
"event-action": "Əməliyyat",
"event-source": "Mənbə",
"event-detail": "Təfərrüat"
}
"event-detail": "Təfərrüat",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Ініцыялы",
"invalid-date": "Няправільная дата",
"invalid-time": "Няправільны час",
"invalid-year": "Няправільны год. Увядзіце ўсе чатыры лічбы, напрыклад 2026.",
"invalid-user": "Няправільны карыстальнік",
"joined": "далучыўся",
"just-invited": "Вас толькі што запрасілі на гэтую дошку",
@@ -949,6 +950,12 @@
"global-webhook": "Глабальныя вэб-хукі",
"new-outgoing-webhook": "Новы выходны вэб-хук",
"no-name": "(Невядома)",
"Platform": "Платформа",
"package": "Пакет",
"OS": "АС",
"Meteor": "Meteor",
"Database": "База даных",
"Node": "Node",
"Node_version": "Версія Node",
"Meteor_version": "Версія Meteor",
"Database_type": "Тып базы дадзеных",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Пераўтварыць у markdown",
"import-board-zip": "Дадайце файл .zip, які мае файлы JSON дошкі і падкаталогі з назвамі дошак з укладаннямі",
"collapse": "Згарнуць",
"collapse-checklist": "Згарнуць кантрольны спіс",
"expand-checklist": "Разгарнуць кантрольны спіс",
"uncollapse": "Разгарнуць",
"hideCheckedChecklistItems": "Схаваць адзначаныя пункты кантрольнага спіса",
"hideAllChecklistItems": "Схаваць усе пункты кантрольнага спіса",
@@ -2339,5 +2348,8 @@
"event-severity": "Сур'ёзнасць",
"event-action": "Дзеянне",
"event-source": "Крыніца",
"event-detail": "Дэталі"
}
"event-detail": "Дэталі",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Инициали",
"invalid-date": "Невалидна дата",
"invalid-time": "Невалиден час",
"invalid-year": "Невалидна година. Въведете и четирите цифри, например 2026.",
"invalid-user": "Невалиден потребител",
"joined": "присъедини",
"just-invited": "Бяхте поканени в това табло",
@@ -949,6 +950,12 @@
"global-webhook": "Глобални Webhooks",
"new-outgoing-webhook": "Нов Изходящ Webhook",
"no-name": "(Непознат)",
"Platform": "Платформа",
"package": "Пакет",
"OS": "ОС",
"Meteor": "Meteor",
"Database": "База данни",
"Node": "Node",
"Node_version": "Версия на Node",
"Meteor_version": "Версия на Meteor",
"Database_type": "Тип на базата данни",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Претвори у структуирани текст",
"import-board-zip": "Додај .zip архиву која има списе у json облику и фасцикле по имену списа где је предметна грађа",
"collapse": "Събери",
"collapse-checklist": "Свиване на чеклиста",
"expand-checklist": "Разгъване на чеклиста",
"uncollapse": "Рашири",
"hideCheckedChecklistItems": "Сакриј све обављене помоћне предметне радње",
"hideAllChecklistItems": "Сакриј све помоћне предметне радње",
@@ -2339,5 +2348,8 @@
"event-severity": "Сериозност",
"event-action": "Действие",
"event-source": "Източник",
"event-detail": "Детайл"
}
"event-detail": "Детайл",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initiales",
"invalid-date": "Date invalide",
"invalid-time": "Heure invalide",
"invalid-year": "Bloavezh direizh. Skrivit ar pevar sifr, da skouer 2026.",
"invalid-user": "Utilisateur invalide",
"joined": "a rejoint",
"just-invited": "Vous venez d'être invité à ce tableau",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globaux",
"new-outgoing-webhook": "Nouveau webhook sortant",
"no-name": "(Inconnu)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Diaz roadennoù",
"Node": "Node",
"Node_version": "Version de Node",
"Meteor_version": "Version de Meteor",
"Database_type": "Rizh ar bank-roadennoù",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en markdown",
"import-board-zip": "Ajouter un fichier .zip qui contient les fichiers JSON du tableau et les noms de sous-répertoires avec les pièces jointes",
"collapse": "Réduire",
"collapse-checklist": "Pleg ar roll gwiriañ",
"expand-checklist": "Astenn ar roll gwiriañ",
"uncollapse": "Développer",
"hideCheckedChecklistItems": "Cacher les éléments cochés de la check-list",
"hideAllChecklistItems": "Cacher tous les éléments de la check-list",
@@ -2339,5 +2348,8 @@
"event-severity": "Garvder",
"event-action": "Ober",
"event-source": "Mammenn",
"event-detail": "Munud"
}
"event-detail": "Munud",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Inicials",
"invalid-date": "Dades invàlides",
"invalid-time": "Temps Invàlid",
"invalid-year": "Any no vàlid. Escriviu les quatre xifres, per exemple 2026.",
"invalid-user": "Usuari invàlid",
"joined": "s'ha unitat",
"just-invited": "Has estat convidat a aquest tauler",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globals",
"new-outgoing-webhook": "Nou Webbook sortt",
"no-name": "Importa tauler des de Wekan",
"Platform": "Plataforma",
"package": "Paquet",
"OS": "Sistema operatiu",
"Meteor": "Meteor",
"Database": "Base de dades",
"Node": "Node",
"Node_version": "Versió Node",
"Meteor_version": "Versió Meteor",
"Database_type": "Tipus de base de dades",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Converteix a markdown",
"import-board-zip": "Afegeix un fitxer .zip que tingui els fitxers JSON del tauler, i subdirectoris amb el nom del tauler amb els adjunts",
"collapse": "Contraure",
"collapse-checklist": "Contreu la llista de verificació",
"expand-checklist": "Desplegueu la llista de verificació",
"uncollapse": "Desplega",
"hideCheckedChecklistItems": "Amaga els elements marcats de la checklist",
"hideAllChecklistItems": "Amaga tots els elements de la checklist",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravetat",
"event-action": "Acció",
"event-source": "Origen",
"event-detail": "Detall"
}
"event-detail": "Detall",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Inicials",
"invalid-date": "Dades invàlides",
"invalid-time": "Temps Invàlid",
"invalid-year": "Any no vàlid. Escriviu les quatre xifres, per exemple 2026.",
"invalid-user": "Usuari invàlid",
"joined": "s'ha unitat",
"just-invited": "Has estat convidat a aquest tauler",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globals",
"new-outgoing-webhook": "Nou Webbook sortt",
"no-name": "Importa tauler des de Wekan",
"Platform": "Plataforma",
"package": "Paquet",
"OS": "Sistema operatiu",
"Meteor": "Meteor",
"Database": "Base de dades",
"Node": "Node",
"Node_version": "Versió Node",
"Meteor_version": "Versió Meteor",
"Database_type": "Tipus de base de dades",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraure",
"collapse-checklist": "Contraure la llista de verificació",
"expand-checklist": "Expandir la llista de verificació",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravetat",
"event-action": "Acció",
"event-source": "Origen",
"event-detail": "Detall"
}
"event-detail": "Detall",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Inicials",
"invalid-date": "Dades invàlides",
"invalid-time": "Temps Invàlid",
"invalid-year": "Any no vàlid. Escriviu les quatre xifres, per exemple 2026.",
"invalid-user": "Usuari invàlid",
"joined": "s'ha unitat",
"just-invited": "Has estat convidat a aquest tauler",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globals",
"new-outgoing-webhook": "Nou Webbook sortt",
"no-name": "Importa tauler des de Wekan",
"Platform": "Plataforma",
"package": "Paquet",
"OS": "Sistema operatiu",
"Meteor": "Meteor",
"Database": "Base de dades",
"Node": "Node",
"Node_version": "Versió Node",
"Meteor_version": "Versió Meteor",
"Database_type": "Tipus de base de dades",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Converteix a markdown",
"import-board-zip": "Afegeix un fitxer .zip que tingui els fitxers JSON del tauler, i subdirectoris amb el nom del tauler amb els adjunts",
"collapse": "Contraure",
"collapse-checklist": "Contreu la llista de verificació",
"expand-checklist": "Desplegueu la llista de verificació",
"uncollapse": "Desplega",
"hideCheckedChecklistItems": "Amaga els elements marcats de la checklist",
"hideAllChecklistItems": "Amaga tots els elements de la checklist",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravetat",
"event-action": "Acció",
"event-source": "Origen",
"event-detail": "Detall"
}
"event-detail": "Detall",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "缩写",
"invalid-date": "无效日期",
"invalid-time": "无效时间",
"invalid-year": "年份无效。请输入完整的四位数字,例如 2026。",
"invalid-user": "无效用户",
"joined": "加入",
"just-invited": "您刚刚被邀请加入此论坛",
@@ -949,6 +950,12 @@
"global-webhook": "全局 Webhooks",
"new-outgoing-webhook": "新的传出 Webhook",
"no-name": "(未知)",
"Platform": "平台",
"package": "软件包",
"OS": "操作系统",
"Meteor": "Meteor",
"Database": "数据库",
"Node": "Node",
"Node_version": "节点版本",
"Meteor_version": "流星版",
"Database_type": "数据库类型",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "转换为降价",
"import-board-zip": "添加包含板 JSON 文件和带有附件的板名称子目录的 .zip 文件",
"collapse": "坍塌",
"collapse-checklist": "折叠清单",
"expand-checklist": "展开清单",
"uncollapse": "展开",
"hideCheckedChecklistItems": "隐藏选中的清单项目",
"hideAllChecklistItems": "隐藏所有清单项目",
@@ -2339,5 +2348,8 @@
"event-severity": "严重程度",
"event-action": "操作",
"event-source": "来源",
"event-detail": "详情"
}
"event-detail": "详情",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciály",
"invalid-date": "Neplatné datum",
"invalid-time": "Neplatný čas",
"invalid-year": "Neplatný rok. Zadejte všechny čtyři číslice, například 2026.",
"invalid-user": "Neplatný uživatel",
"joined": "spojeno",
"just-invited": "Právě jsi byl pozván(a) do tohoto tabla",
@@ -949,6 +950,12 @@
"global-webhook": "Globální webhooks",
"new-outgoing-webhook": "Nové odchozí Webhooky",
"no-name": "(Neznámé)",
"Platform": "Platforma",
"package": "Balíček",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Databáze",
"Node": "Node",
"Node_version": "Node verze",
"Meteor_version": "Verze Meteor",
"Database_type": "Typ databáze",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Převést na markdown",
"import-board-zip": "Přidejte soubor .zip, který obsahuje soubory JSON tabla a podadresáře s názvem tabla obsahující přílohy",
"collapse": "Sbalit",
"collapse-checklist": "Sbalit kontrolní seznam",
"expand-checklist": "Rozbalit kontrolní seznam",
"uncollapse": "Rozbalit",
"hideCheckedChecklistItems": "Skrýt zaškrtnuté položky checklistu",
"hideAllChecklistItems": "Skrýt všechny položky checklistu",
@@ -2339,5 +2348,8 @@
"event-severity": "Závažnost",
"event-action": "Akce",
"event-source": "Zdroj",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciály",
"invalid-date": "Neplatné datum",
"invalid-time": "Neplatný čas",
"invalid-year": "Neplatný rok. Zadejte všechny čtyři číslice, například 2026.",
"invalid-user": "Neplatný uživatel",
"joined": "spojeno",
"just-invited": "Právě jsi byl pozván(a) do tohoto tabla",
@@ -949,6 +950,12 @@
"global-webhook": "Globální webhooks",
"new-outgoing-webhook": "Nové odchozí Webhooky",
"no-name": "(Neznámé)",
"Platform": "Platforma",
"package": "Balíček",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Databáze",
"Node": "Node",
"Node_version": "Node verze",
"Meteor_version": "Verze Meteor",
"Database_type": "Typ databáze",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Převést na markdown",
"import-board-zip": "Přidejte soubor .zip, který obsahuje soubory JSON tabla a podadresáře s názvem tabla obsahující přílohy",
"collapse": "Sbalit",
"collapse-checklist": "Sbalit kontrolní seznam",
"expand-checklist": "Rozbalit kontrolní seznam",
"uncollapse": "Rozbalit",
"hideCheckedChecklistItems": "Skrýt zaškrtnuté položky checklistu",
"hideAllChecklistItems": "Skrýt všechny položky checklistu",
@@ -2339,5 +2348,8 @@
"event-severity": "Závažnost",
"event-action": "Akce",
"event-source": "Zdroj",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Llythrennau blaen",
"invalid-date": "Dyddiad annilys",
"invalid-time": "Amser annilys",
"invalid-year": "Blwyddyn annilys. Teipiwch y pedwar digid, er enghraifft 2026.",
"invalid-user": "Defnyddiwr annilys",
"joined": "ymunodd",
"just-invited": "Fe'ch gwahoddir i'r bwrdd hwn",
@@ -949,6 +950,12 @@
"global-webhook": "Bachau Gwe Byd-eang",
"new-outgoing-webhook": "Llyfr Gwe Allan Newydd",
"no-name": "(Anhysbys)",
"Platform": "Platfform",
"package": "Pecyn",
"OS": "System gweithredu",
"Meteor": "Meteor",
"Database": "Cronfa ddata",
"Node": "Node",
"Node_version": "Fersiwn nod",
"Meteor_version": "Fersiwn Meteor",
"Database_type": "Math o gronfa ddata",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Trosi i markdown",
"import-board-zip": "Ychwanegu ffeil .zip sydd â ffeiliau bwrdd JSON, ac is-gyfeiriaduron enw bwrdd gydag atodiadau",
"collapse": "Cwymp",
"collapse-checklist": "Crebachu'r rhestr wirio",
"expand-checklist": "Ehangu'r rhestr wirio",
"uncollapse": "Dad-gwymp",
"hideCheckedChecklistItems": "Cuddio eitemau rhestr wirio wedi'u gwirio",
"hideAllChecklistItems": "Cuddio holl eitemau'r rhestr wirio",
@@ -2339,5 +2348,8 @@
"event-severity": "Difrifoldeb",
"event-action": "Gweithred",
"event-source": "Ffynhonnell",
"event-detail": "Manylion"
}
"event-detail": "Manylion",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Llythrennau blaen",
"invalid-date": "Dyddiad annilys",
"invalid-time": "Amser annilys",
"invalid-year": "Blwyddyn annilys. Teipiwch y pedwar digid, er enghraifft 2026.",
"invalid-user": "Defnyddiwr annilys",
"joined": "ymunodd",
"just-invited": "Fe'ch gwahoddir i'r bwrdd hwn",
@@ -949,6 +950,12 @@
"global-webhook": "Bachau Gwe Byd-eang",
"new-outgoing-webhook": "Llyfr Gwe Allan Newydd",
"no-name": "(Anhysbys)",
"Platform": "Platfform",
"package": "Pecyn",
"OS": "System gweithredu",
"Meteor": "Meteor",
"Database": "Cronfa ddata",
"Node": "Node",
"Node_version": "Fersiwn nod",
"Meteor_version": "Fersiwn Meteor",
"Database_type": "Math o gronfa ddata",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Trosi i markdown",
"import-board-zip": "Ychwanegu ffeil .zip sydd â ffeiliau bwrdd JSON, ac is-gyfeiriaduron enw bwrdd gydag atodiadau",
"collapse": "Cwymp",
"collapse-checklist": "Crebachu'r rhestr wirio",
"expand-checklist": "Ehangu'r rhestr wirio",
"uncollapse": "Dad-gwymp",
"hideCheckedChecklistItems": "Cuddio eitemau rhestr wirio wedi'u gwirio",
"hideAllChecklistItems": "Cuddio holl eitemau'r rhestr wirio",
@@ -2339,5 +2348,8 @@
"event-severity": "Difrifoldeb",
"event-action": "Gweithred",
"event-source": "Ffynhonnell",
"event-detail": "Manylion"
}
"event-detail": "Manylion",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initialer",
"invalid-date": "Ugyldig dato",
"invalid-time": "Ugyldig tidsangivelse",
"invalid-year": "Ugyldigt år. Skriv alle fire cifre, for eksempel 2026.",
"invalid-user": "Ugyldig bruger",
"joined": "sammenlagt",
"just-invited": "Du er netop blevet inviteret til denne tavle",
@@ -949,6 +950,12 @@
"global-webhook": "Globale webhooks",
"new-outgoing-webhook": "Ny udgående webhook",
"no-name": "(Ukendt)",
"Platform": "Platform",
"package": "Pakke",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node-version",
"Meteor_version": "Meteor-version",
"Database_type": "Databasetype",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Konvertér til markdown",
"import-board-zip": "Lägga till .zip-filen till tavlans JSON filer, och tavla",
"collapse": "Sammenfold",
"collapse-checklist": "Sammenfold tjekliste",
"expand-checklist": "Udvid tjekliste",
"uncollapse": "Expandera",
"hideCheckedChecklistItems": "Dölj markerade objekt i checklistan",
"hideAllChecklistItems": "Dölj alla objekt i checklistan",
@@ -2339,5 +2348,8 @@
"event-severity": "Alvorsgrad",
"event-action": "Handling",
"event-source": "Kilde",
"event-detail": "Detalje"
}
"event-detail": "Detalje",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initialen",
"invalid-date": "Ungültiges Datum",
"invalid-time": "Ungültige Zeitangabe",
"invalid-year": "Ungültiges Jahr. Bitte alle vier Ziffern eingeben, zum Beispiel 2026.",
"invalid-user": "Ungültiger Benutzer",
"joined": "beigetreten",
"just-invited": "Sie wurden soeben zu diesem Board eingeladen",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Neuer ausgehender Webhook",
"no-name": "(Unbekannt)",
"Platform": "Plattform",
"package": "Paket",
"OS": "Betriebssystem",
"Meteor": "Meteor",
"Database": "Datenbank",
"Node": "Node",
"Node_version": "Node-Version",
"Meteor_version": "Meteor-Version",
"Database_type": "Datenbanktyp",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Als Markdown konvertieren",
"import-board-zip": "Erstelle .zip Datei, die Board JSON Dateien und Board Unterverzeichnisse mit Boardname inklusive Anhängen enthält",
"collapse": "Einklappen",
"collapse-checklist": "Checkliste einklappen",
"expand-checklist": "Checkliste aufklappen",
"uncollapse": "Aufklappen",
"hideCheckedChecklistItems": "Erledigte Checklisteneinträge ausblenden",
"hideAllChecklistItems": "Verberge alle Checklisteneinträge",
@@ -2339,5 +2348,8 @@
"event-severity": "Schweregrad",
"event-action": "Aktion",
"event-source": "Quelle",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initialen",
"invalid-date": "Ungültiges Datum",
"invalid-time": "Ungültige Zeitangabe",
"invalid-year": "Ungültiges Jahr. Bitte alle vier Ziffern eingeben, zum Beispiel 2026.",
"invalid-user": "Ungültiger Benutzer",
"joined": "beigetreten",
"just-invited": "Sie wurden soeben zu diesem Board eingeladen",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Neuer ausgehender Webhook",
"no-name": "(Unbekannt)",
"Platform": "Plattform",
"package": "Paket",
"OS": "Betriebssystem",
"Meteor": "Meteor",
"Database": "Datenbank",
"Node": "Node",
"Node_version": "Node-Version",
"Meteor_version": "Meteor-Version",
"Database_type": "Datenbanktyp",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Als Markdown konvertieren",
"import-board-zip": "Erstelle .zip Datei, die Board JSON Dateien und Board Unterverzeichnisse mit Boardname inklusive Anhängen enthält",
"collapse": "Einklappen",
"collapse-checklist": "Checkliste einklappen",
"expand-checklist": "Checkliste aufklappen",
"uncollapse": "Aufklappen",
"hideCheckedChecklistItems": "Erledigte Checklisteneinträge ausblenden",
"hideAllChecklistItems": "Verberge alle Checklisteneinträge",
@@ -2339,5 +2348,8 @@
"event-severity": "Schweregrad",
"event-action": "Aktion",
"event-source": "Quelle",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initialen",
"invalid-date": "Ungültiges Datum",
"invalid-time": "Ungültige Zeitangabe",
"invalid-year": "Ungültiges Jahr. Bitte alle vier Ziffern eingeben, zum Beispiel 2026.",
"invalid-user": "Ungültiger Benutzer",
"joined": "beigetreten",
"just-invited": "Sie wurden soeben zu diesem Board eingeladen",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Neuer ausgehender Webhook",
"no-name": "(Unbekannt)",
"Platform": "Plattform",
"package": "Paket",
"OS": "Betriebssystem",
"Meteor": "Meteor",
"Database": "Datenbank",
"Node": "Node",
"Node_version": "Node-Version",
"Meteor_version": "Meteor-Version",
"Database_type": "Datenbanktyp",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Als Markdown konvertieren",
"import-board-zip": "Erstelle .zip Datei, die Board JSON Dateien und Board Unterverzeichnisse mit Boardname inklusive Anhängen enthält",
"collapse": "Einklappen",
"collapse-checklist": "Checkliste einklappen",
"expand-checklist": "Checkliste aufklappen",
"uncollapse": "Aufklappen",
"hideCheckedChecklistItems": "Erledigte Checklisteneinträge ausblenden",
"hideAllChecklistItems": "Verberge alle Checklisteneinträge",
@@ -2339,5 +2348,8 @@
"event-severity": "Schweregrad",
"event-action": "Aktion",
"event-source": "Quelle",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initialen",
"invalid-date": "Ungültiges Datum",
"invalid-time": "Ungültige Zeitangabe",
"invalid-year": "Ungültiges Jahr. Bitte alle vier Ziffern eingeben, zum Beispiel 2026.",
"invalid-user": "Ungültiger Benutzer",
"joined": "beigetreten",
"just-invited": "Sie wurden soeben zu diesem Board eingeladen",
@@ -949,6 +950,12 @@
"global-webhook": "Globale Webhooks",
"new-outgoing-webhook": "Neuer ausgehender Webhook",
"no-name": "(Unbekannt)",
"Platform": "Plattform",
"package": "Paket",
"OS": "Betriebssystem",
"Meteor": "Meteor",
"Database": "Datenbank",
"Node": "Node",
"Node_version": "Node-Version",
"Meteor_version": "Meteor-Version",
"Database_type": "Datenbanktyp",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Als Markdown konvertieren",
"import-board-zip": "Erstelle .zip Datei mit JSON Board Dateien und die Unterverzeichnisse des Boardnamen mit Anhängen",
"collapse": "Einklappen",
"collapse-checklist": "Checkliste einklappen",
"expand-checklist": "Checkliste aufklappen",
"uncollapse": "Aufklappen",
"hideCheckedChecklistItems": "Verberge erledigte Checklisteneinträge",
"hideAllChecklistItems": "Verberge alle Checklisteneinträge",
@@ -2339,5 +2348,8 @@
"event-severity": "Schweregrad",
"event-action": "Aktion",
"event-source": "Quelle",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Αρχικά",
"invalid-date": "Λανθασμένη ημερομηνία",
"invalid-time": "Λανθασμένη ώρα",
"invalid-year": "Μη έγκυρο έτος. Πληκτρολογήστε και τα τέσσερα ψηφία, για παράδειγμα 2026.",
"invalid-user": "Λανθασμένος χρήστης",
"joined": "si è unito a",
"just-invited": "Μόλις προσκληθήκατε σε αυτόν τον πίνακα",
@@ -949,6 +950,12 @@
"global-webhook": "Webhook globali",
"new-outgoing-webhook": "Νέο Εξερχόμενο Webhook",
"no-name": "(Άγνωστο)",
"Platform": "Πλατφόρμα",
"package": "Πακέτο",
"OS": "Λειτουργικό σύστημα",
"Meteor": "Meteor",
"Database": "Βάση δεδομένων",
"Node": "Node",
"Node_version": "Έκδοση Node",
"Meteor_version": "Έκδοση Meteor",
"Database_type": "Τύπος βάσης δεδομένων",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Converti in markdown",
"import-board-zip": "Aggiungi il file .zip contenente i file JSON della bacheca, i nomi bacheca delle sottodirectory con gli allegati",
"collapse": "Σύμπτυξη",
"collapse-checklist": "Σύμπτυξη λίστας ελέγχου",
"expand-checklist": "Ανάπτυξη λίστας ελέγχου",
"uncollapse": "Non collassare",
"hideCheckedChecklistItems": "Nascondi gli elementi della lista di controllo selezionati",
"hideAllChecklistItems": "Nascondi gli elementi della lista di controllo",
@@ -2339,5 +2348,8 @@
"event-severity": "Σοβαρότητα",
"event-action": "Ενέργεια",
"event-source": "Πηγή",
"event-detail": "Λεπτομέρεια"
}
"event-detail": "Λεπτομέρεια",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Αρχικά",
"invalid-date": "Λανθασμένη ημερομηνία",
"invalid-time": "Λανθασμένη ώρα",
"invalid-year": "Μη έγκυρο έτος. Πληκτρολογήστε και τα τέσσερα ψηφία, για παράδειγμα 2026.",
"invalid-user": "Λανθασμένος χρήστης",
"joined": "si è unito a",
"just-invited": "Μόλις προσκληθήκατε σε αυτόν τον πίνακα",
@@ -949,6 +950,12 @@
"global-webhook": "Webhook globali",
"new-outgoing-webhook": "Νέο Εξερχόμενο Webhook",
"no-name": "(Άγνωστο)",
"Platform": "Πλατφόρμα",
"package": "Πακέτο",
"OS": "Λειτουργικό σύστημα",
"Meteor": "Meteor",
"Database": "Βάση δεδομένων",
"Node": "Node",
"Node_version": "Έκδοση Node",
"Meteor_version": "Έκδοση Meteor",
"Database_type": "Τύπος βάσης δεδομένων",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Converti in markdown",
"import-board-zip": "Aggiungi il file .zip contenente i file JSON della bacheca, i nomi bacheca delle sottodirectory con gli allegati",
"collapse": "Σύμπτυξη",
"collapse-checklist": "Σύμπτυξη λίστας ελέγχου",
"expand-checklist": "Ανάπτυξη λίστας ελέγχου",
"uncollapse": "Non collassare",
"hideCheckedChecklistItems": "Nascondi gli elementi della lista di controllo selezionati",
"hideAllChecklistItems": "Nascondi gli elementi della lista di controllo",
@@ -2339,5 +2348,8 @@
"event-severity": "Σοβαρότητα",
"event-action": "Ενέργεια",
"event-source": "Πηγή",
"event-detail": "Λεπτομέρεια"
}
"event-detail": "Λεπτομέρεια",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+9
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Initials",
"invalid-date": "Invalid date",
"invalid-time": "Invalid time",
"invalid-year": "Invalid year. Please type all four digits, for example 2026.",
"invalid-user": "Invalid user",
"joined": "joined",
"just-invited": "You are just invited to this board",
@@ -949,6 +950,12 @@
"global-webhook": "Global Webhooks",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(Unknown)",
"Platform": "Platform",
"package": "Package",
"OS": "OS",
"Meteor": "Meteor",
"Database": "Database",
"Node": "Node",
"Node_version": "Node version",
"Meteor_version": "Meteor version",
"Database_type": "Database type",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convert to markdown",
"import-board-zip": "Add .zip file that has board JSON files, and board name subdirectories with attachments",
"collapse": "Collapse",
"collapse-checklist": "Collapse checklist",
"expand-checklist": "Expand checklist",
"uncollapse": "Uncollapse",
"hideCheckedChecklistItems": "Hide checked checklist items",
"hideAllChecklistItems": "Hide all checklist items",
@@ -2339,5 +2348,8 @@
"event-severity": "Severity",
"event-action": "Action",
"event-source": "Source",
"event-detail": "Detail"
}
"event-detail": "Detail",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha no válida",
"invalid-time": "Tiempo no válido",
"invalid-year": "Nevalida jaro. Tajpu ĉiujn kvar ciferojn, ekzemple 2026.",
"invalid-user": "Usuario no válido",
"joined": "se ha unido",
"just-invited": "Has sido invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo webhook saliente",
"no-name": "(Desconocido)",
"Platform": "Platformo",
"package": "Pakaĵo",
"OS": "Mastruma sistemo",
"Meteor": "Meteor",
"Database": "Datumbazo",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Datumbaza tipo",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Faldi kontrolliston",
"expand-checklist": "Malfaldi kontrolliston",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Graveco",
"event-action": "Ago",
"event-source": "Fonto",
"event-detail": "Detalo"
}
"event-detail": "Detalo",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha inválida",
"invalid-time": "Tiempo inválido",
"invalid-year": "Año no válido. Escribe los cuatro dígitos, por ejemplo 2026.",
"invalid-user": "Usuario inválido",
"joined": "unido",
"just-invited": "Fuiste invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo Gancho Web",
"no-name": "(desconocido)",
"Platform": "Plataforma",
"package": "Paquete",
"OS": "Sistema operativo",
"Meteor": "Meteor",
"Database": "Base de datos",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Tipo de base de datos",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Contraer la lista de tareas",
"expand-checklist": "Expandir la lista de tareas",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravedad",
"event-action": "Acción",
"event-source": "Origen",
"event-detail": "Detalle"
}
"event-detail": "Detalle",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha no válida",
"invalid-time": "Tiempo no válido",
"invalid-year": "Año no válido. Escribe los cuatro dígitos, por ejemplo 2026.",
"invalid-user": "Usuario no válido",
"joined": "se ha unido",
"just-invited": "Has sido invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo webhook saliente",
"no-name": "(Desconocido)",
"Platform": "Plataforma",
"package": "Paquete",
"OS": "Sistema operativo",
"Meteor": "Meteor",
"Database": "Base de datos",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Tipo de base de datos",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Contraer la lista de tareas",
"expand-checklist": "Expandir la lista de tareas",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravedad",
"event-action": "Acción",
"event-source": "Origen",
"event-detail": "Detalle"
}
"event-detail": "Detalle",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha no válida",
"invalid-time": "Tiempo no válido",
"invalid-year": "Año no válido. Escribe los cuatro dígitos, por ejemplo 2026.",
"invalid-user": "Usuario no válido",
"joined": "se ha unido",
"just-invited": "Has sido invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo webhook saliente",
"no-name": "(Desconocido)",
"Platform": "Plataforma",
"package": "Paquete",
"OS": "Sistema operativo",
"Meteor": "Meteor",
"Database": "Base de datos",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Tipo de base de datos",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Contraer la lista de tareas",
"expand-checklist": "Expandir la lista de tareas",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravedad",
"event-action": "Acción",
"event-source": "Origen",
"event-detail": "Detalle"
}
"event-detail": "Detalle",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
+14 -2
View File
@@ -710,6 +710,7 @@
"initials": "Iniciales",
"invalid-date": "Fecha no válida",
"invalid-time": "Tiempo no válido",
"invalid-year": "Año no válido. Escribe los cuatro dígitos, por ejemplo 2026.",
"invalid-user": "Usuario no válido",
"joined": "se ha unido",
"just-invited": "Has sido invitado a este tablero",
@@ -949,6 +950,12 @@
"global-webhook": "Webhooks globales",
"new-outgoing-webhook": "Nuevo webhook saliente",
"no-name": "(Desconocido)",
"Platform": "Plataforma",
"package": "Paquete",
"OS": "Sistema operativo",
"Meteor": "Meteor",
"Database": "Base de datos",
"Node": "Node",
"Node_version": "Versión de Node",
"Meteor_version": "Versión de Meteor",
"Database_type": "Tipo de base de datos",
@@ -1840,6 +1847,8 @@
"convert-to-markdown": "Convertir en Markdown",
"import-board-zip": "Añadir archivo .zip que contenga los archivos JSON del tablero y los nombres de las subcarpetas con adjuntos del tablero",
"collapse": "Contraer",
"collapse-checklist": "Contraer la lista de tareas",
"expand-checklist": "Expandir la lista de tareas",
"uncollapse": "Expandir",
"hideCheckedChecklistItems": "Ocultar elementos marcados de la lista de verificación",
"hideAllChecklistItems": "Ocultar todos los elementos de la lista de verificación",
@@ -2339,5 +2348,8 @@
"event-severity": "Gravedad",
"event-action": "Acción",
"event-source": "Origen",
"event-detail": "Detalle"
}
"event-detail": "Detalle",
"event-ip": "IP address",
"event-attempts": "Attempts",
"integrityReportTitle": "Filesystem integrity"
}
Loaded 100 of 341 files, more files were not shown because too many files have changed in this diff. Show more