Compare commits

...
60 Commits
Author SHA1 Message Date
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
250 changed files with 8636 additions and 243 deletions

No files matched your search

+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:
+90 -11
View File
@@ -410,6 +410,17 @@ jobs:
# is actually on the registries. If `docker` fails, this job is skipped.
charts:
needs: docker
# `always()` so that a CANCELLATION somewhere else in the run cannot skip
# this job. It is not defensive dressing: in v10.80 the release itself, the
# image and every bundle were published, and then charts, ucs and nextcloud
# were all skipped the moment `docker` finished - because build-mac-x64 was
# queued on a retired runner label and had to be cancelled by hand, and a
# cancelled job cancels the RUN, which skips every job that has not started.
# A job whose `if` is `always()` still runs while a run is cancelling, so the
# chart is published for a release that IS on the registries. The guard is
# then explicit about the only condition that ever mattered: docker SUCCEEDED
# - the chart points at the image that job pushes, so it must exist first.
if: ${{ always() && needs.docker.result == 'success' }}
runs-on: ubuntu-latest
steps:
# First step on purpose: a missing secret is then ONE named line at the top of
@@ -475,6 +486,21 @@ jobs:
run: |
./wekan/releases/release-charts.sh "${{ inputs.new_version }}"
# What the index does NOT have, reported every release rather than
# discovered years later. A chart entry is written once, during the release
# it belongs to, so a release whose charts job did not run leaves a hole
# that nothing ever fills - v10.80's, when a cancelled build-mac-x64
# cancelled the run, is one of many. This is a PLAN ONLY: it reads the
# releases API, the live index and ghcr, writes nothing, and prints which
# releases have a chart, which are missing one that could be built, and
# which are omitted because they have no container image for a chart to
# point at. Filling them in is `backfill-charts.sh --apply --push`, run by
# hand: rewriting a published Helm index is not a side effect of a release.
- name: Report which releases the chart index is missing
continue-on-error: true
run: |
./wekan/releases/backfill-charts.sh | tee -a "$GITHUB_STEP_SUMMARY"
# Always, as the last step: the run summary then answers "what worked and what
# did not" for the whole release at a glance, instead of eighteen job logs. A
# step that failed has already printed an ::error:: saying why; this names the
@@ -2314,7 +2340,18 @@ jobs:
# failing red, and returns on its own if a Node.js for it appears again.
build-mac-x64:
needs: [prepare, build-amd64, release]
runs-on: macos-13 # macos-13 runners are Intel (x86_64); macos-14+ are arm64
# Intel (x86_64) macOS. It was `macos-13`, and GitHub 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: the job QUEUES FOREVER waiting for a runner that will never come, so
# every release since then had a build-mac-x64 sitting there for hours until
# it was cancelled by hand - and cancelling it cancelled the RUN, which is
# what skipped the charts, ucs and nextcloud jobs (they had not started yet).
# No release has ever carried a wekan-<version>-mac-x64.zip because of it.
# Intel macOS is still offered, under new labels: macos-15-intel and
# macos-26-intel (macos-14/15/26 without the suffix are Apple Silicon). This
# is the same label .tools/TSC already builds its Intel Mac on.
runs-on: macos-15-intel
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
@@ -2622,17 +2659,37 @@ jobs:
node_arch: x86
ferretdb_arch: i386
optional: true
# ARMv6: Raspberry Pi 1 and Zero. It runs in a linux/arm/v6 container,
# takes wekan/node-patches' node-armv6 (nobody else publishes an ARMv6
# Node - nodejs.org dropped it after Node 11), and FerretDB and the
# MongoDB tools build a GOARM=6 armv6 of their own. Their armel is
# GOARM=5 and WOULD run on these boards, but in software floating
# point, so it is not used here.
# ARMv6: Raspberry Pi 1 and Zero. It takes wekan/node-patches'
# node-armv6 (nobody else publishes an ARMv6 Node - nodejs.org dropped
# it after Node 11), and FerretDB and the MongoDB tools build a GOARM=6
# armv6 of their own. Their armel is GOARM=5 and WOULD run on these
# boards, but in software floating point, so it is not used here.
#
# The CONTAINER is linux/arm/v7, not linux/arm/v6, and that is not a
# typo. DEBIAN HAS NO ARMv6 PORT: debian:trixie publishes arm/v5 and
# arm/v7 only, and because containerd treats a lower ARM variant as
# compatible, asking it for linux/arm/v6 silently handed back arm/v5 -
# Debian armel, ARMv5 SOFT-float. node-armv6 is HARD-float, so it could
# not start there and the job died on
# qemu-arm: Could not open '/lib/ld-linux-armhf.so.3'
# after the whole apt install (v10.80). Debian's arm/v7 IS armhf: the
# same hard-float ABI and the same loader, so node-armv6 runs in it.
#
# Nothing ARMv7 reaches the bundle. What the container does is run
# `npm install`, and that install compiles NOTHING - it is npm-rebuild
# over pure-JavaScript packages ("rebuilt dependencies successfully",
# no node-gyp, no CXX). Every binary in the bundle is downloaded already
# built FOR ARMv6: node-armv6, ferretdb-armv6 and the armv6 MongoDB
# tools. The container only has to be a userland this arch's node can
# RUN in, and armhf is exactly that. The one thing that would have been
# the container's own CPU is the bundled qemu-user, and that is why
# SAME_ARCH_CONTAINER is false below.
- arch: armv6
platform: linux/arm/v6
platform: linux/arm/v7
image: debian:trixie
node_arch: armv6
ferretdb_arch: armv6
same_arch_container: false
optional: true
- arch: armhf
platform: linux/arm/v7
@@ -2778,6 +2835,7 @@ jobs:
-e NODE_SHA256="$NODE_SHA256" \
-e ARCH='${{ matrix.arch }}' \
-e NODE_ARCH='${{ matrix.node_arch }}' \
-e SAME_ARCH_CONTAINER='${{ matrix.same_arch_container }}' \
-v "$(pwd)/bundle:/bundle" \
-v "$(pwd)/releases:/releases:ro" \
-w /bundle/programs/server \
@@ -3988,7 +4046,18 @@ jobs:
# usually transient, so the three attempts above cover a one-off.
# (The large-history push that also broke these builds is fixed
# separately, by flattening the repo before the push.)
echo "::error::If the build ended as 'Stopped' with no artifact: the Launchpad project https://launchpad.net/xet7-craft-remote-build should be MIT / X/Expat (it is set now) - re-set it only if it shows 'None specified'. Otherwise Launchpad cancelled the build as it ran (often an OOM in the Meteor npm install, or a transient build-farm reset - the printed build log ends mid-step); re-run, and if it persists on this arch open a Launchpad question. Builds: https://launchpad.net/~xet7/+snaps" >&2
# THE BUILD LOG IS PRINTED ABOVE AND IT SAYS WHY. Read it before
# believing any guess in this message - the guess has been wrong. For
# four releases this line said "often an OOM in the Meteor npm install,
# or a transient build-farm reset", and every one of the four
# architectures was in fact failing on one deterministic line:
# Failed to copy '.../parts/mongo42/install/mongo42':
# no such file or directory
# a `stage:` filter naming a path that an amd64/arm64-only part does not
# produce anywhere else. Nothing transient about it, and "re-run" was
# the one thing that could not help. So these are named as the things to
# LOOK FOR in the log above, not as what probably happened.
echo "::error::snap-launchpad ${{ matrix.arch }}: the Launchpad build log is printed above - read it first; it names the part and the line that failed. Common causes, in the order they are worth checking: (1) a 'Failed to copy .../parts/<part>/install/<path>' - a stage:/prime: filter naming something the part did not produce on THIS architecture, which is deterministic and will not pass on a re-run; (2) an npm/network error inside a part's override-build, which is transient and is retried; (3) 'Stopped' with no artifact and no error - then check that https://launchpad.net/xet7-craft-remote-build still has a licence set (MIT / X/Expat), and otherwise Launchpad ended the build itself. Builds: https://launchpad.net/~xet7/+snaps" >&2
# A wrong or expired LP_CREDENTIALS makes Launchpad answer 401/403, which
# reads as an ordinary build failure unless the log is checked. But the
# pattern must be SPECIFIC: a bare `401`/`403` matches any three digits in
@@ -4361,8 +4430,13 @@ jobs:
ucs:
# ENABLED. `continue-on-error: true` below means a failure here can never
# fail the release or hold up another job - the point of running it is to
# SEE what it reports. Disable again with `if: ${{ false }}`.
# SEE what it reports. Disable again by replacing the `if` below with
# `if: ${{ false }}`.
needs: [prepare, release, docker]
# always(), for the same reason as the charts job above: a cancellation
# elsewhere in the run skips every job that has not started yet, and in
# v10.80 that silently dropped this one along with charts.
if: ${{ always() && needs.prepare.result == 'success' && needs.release.result == 'success' && needs.docker.result == 'success' }}
continue-on-error: true
timeout-minutes: 45
runs-on: ubuntu-latest
@@ -4456,8 +4530,13 @@ jobs:
nextcloud:
# ENABLED. `continue-on-error: true` below means a failure here can never
# fail the release or hold up another job - the point of running it is to
# SEE what it reports. Disable again with `if: ${{ false }}`.
# SEE what it reports. Disable again by replacing the `if` below with
# `if: ${{ false }}`.
needs: [prepare, release, docker]
# always(), for the same reason as the charts job above: a cancellation
# elsewhere in the run skips every job that has not started yet, and in
# v10.80 that silently dropped this one along with charts.
if: ${{ always() && needs.prepare.result == 'success' && needs.release.result == 'success' && needs.docker.result == 'success' }}
continue-on-error: true
timeout-minutes: 60
runs-on: ubuntu-latest
+1123 -3
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -20,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.80
ARG VERSION=10.85
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v10.80.0"
appVersion: "v10.85.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||"
+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;
}
+13 -1
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'}}
+41
View File
@@ -82,6 +82,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({
+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
+32
View File
@@ -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);
+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;
}
@@ -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;
+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();
},
+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
+51 -2
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
@@ -91,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:
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-10.80-win64.zip](https://github.com/wekan/wekan/releases/download/v10.80/wekan-10.80-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-10.85-win64.zip](https://github.com/wekan/wekan/releases/download/v10.85/wekan-10.85-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.80-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-10.85-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
+3
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
+3
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة غير صالحة. اكتب الأرقام الأربعة كاملة، مثل 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
+3
View File
@@ -710,6 +710,7 @@
"initials": "أولية",
"invalid-date": "تاريخ غير صالح",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سنة ماشي صالحة. كتب الأرقام الأربعة كاملين، بحال 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "انضمّ",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "انهيار",
"collapse-checklist": "طي قائمة التحقق",
"expand-checklist": "توسيع قائمة التحقق",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "إخفاء عناصر قائمة التحقق المحددة",
"hideAllChecklistItems": "إخفاء جميع عناصر قائمة التحقق",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Ініцыялы",
"invalid-date": "Няправільная дата",
"invalid-time": "Няправільны час",
"invalid-year": "Няправільны год. Увядзіце ўсе чатыры лічбы, напрыклад 2026.",
"invalid-user": "Няправільны карыстальнік",
"joined": "далучыўся",
"just-invited": "Вас толькі што запрасілі на гэтую дошку",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Пераўтварыць у markdown",
"import-board-zip": "Дадайце файл .zip, які мае файлы JSON дошкі і падкаталогі з назвамі дошак з укладаннямі",
"collapse": "Згарнуць",
"collapse-checklist": "Згарнуць кантрольны спіс",
"expand-checklist": "Разгарнуць кантрольны спіс",
"uncollapse": "Разгарнуць",
"hideCheckedChecklistItems": "Схаваць адзначаныя пункты кантрольнага спіса",
"hideAllChecklistItems": "Схаваць усе пункты кантрольнага спіса",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Инициали",
"invalid-date": "Невалидна дата",
"invalid-time": "Невалиден час",
"invalid-year": "Невалидна година. Въведете и четирите цифри, например 2026.",
"invalid-user": "Невалиден потребител",
"joined": "присъедини",
"just-invited": "Бяхте поканени в това табло",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Претвори у структуирани текст",
"import-board-zip": "Додај .zip архиву која има списе у json облику и фасцикле по имену списа где је предметна грађа",
"collapse": "Събери",
"collapse-checklist": "Свиване на чеклиста",
"expand-checklist": "Разгъване на чеклиста",
"uncollapse": "Рашири",
"hideCheckedChecklistItems": "Сакриј све обављене помоћне предметне радње",
"hideAllChecklistItems": "Сакриј све помоћне предметне радње",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "缩写",
"invalid-date": "无效日期",
"invalid-time": "无效时间",
"invalid-year": "年份无效。请输入完整的四位数字,例如 2026。",
"invalid-user": "无效用户",
"joined": "加入",
"just-invited": "您刚刚被邀请加入此论坛",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "转换为降价",
"import-board-zip": "添加包含板 JSON 文件和带有附件的板名称子目录的 .zip 文件",
"collapse": "坍塌",
"collapse-checklist": "折叠清单",
"expand-checklist": "展开清单",
"uncollapse": "展开",
"hideCheckedChecklistItems": "隐藏选中的清单项目",
"hideAllChecklistItems": "隐藏所有清单项目",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Αρχικά",
"invalid-date": "Λανθασμένη ημερομηνία",
"invalid-time": "Λανθασμένη ώρα",
"invalid-year": "Μη έγκυρο έτος. Πληκτρολογήστε και τα τέσσερα ψηφία, για παράδειγμα 2026.",
"invalid-user": "Λανθασμένος χρήστης",
"joined": "si è unito a",
"just-invited": "Μόλις προσκληθήκατε σε αυτόν τον πίνακα",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Αρχικά",
"invalid-date": "Λανθασμένη ημερομηνία",
"invalid-time": "Λανθασμένη ώρα",
"invalid-year": "Μη έγκυρο έτος. Πληκτρολογήστε και τα τέσσερα ψηφία, για παράδειγμα 2026.",
"invalid-user": "Λανθασμένος χρήστης",
"joined": "si è unito a",
"just-invited": "Μόλις προσκληθήκατε σε αυτόν τον πίνακα",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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": "Ha sido invitado a este tablero",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
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",
@@ -1846,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",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Initsiaalid",
"invalid-date": "Vale kuupäev",
"invalid-time": "Vale aeg",
"invalid-year": "Vigane aasta. Sisesta kõik neli numbrit, näiteks 2026.",
"invalid-user": "Invaliidne kasutaja",
"joined": "liitunud",
"just-invited": "Teid lihtsalt kutsutakse siia foorumisse",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Teisenda markdowniks",
"import-board-zip": "Lisa .zip fail, milles on tahvli JSON-failid ja tahvli nimega alamkataloogid manustega",
"collapse": "Kokkupõrge",
"collapse-checklist": "Ahenda kontrollnimekiri",
"expand-checklist": "Laienda kontrollnimekiri",
"uncollapse": "Laienda",
"hideCheckedChecklistItems": "Peida märgitud kontrollnimekirja objektid",
"hideAllChecklistItems": "Peida kõik kontrollnimekirja objektid",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Inizialak",
"invalid-date": "Baliogabeko data",
"invalid-time": "Baliogabeko denbora",
"invalid-year": "Urte baliogabea. Idatzi lau zifrak, adibidez 2026.",
"invalid-user": "Baliogabeko erabiltzailea",
"joined": "elkartu da",
"just-invited": "Arbel honetara gonbidatu berri zaituzte",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Bihurtu markdown-era",
"import-board-zip": "Gehitu arbeleko JSON fitxategiak dituen .zip fitxategia eta eranskinak dituzten arbelen izenen azpidirektorioak",
"collapse": "Tolestu",
"collapse-checklist": "Tolestu kontrol-zerrenda",
"expand-checklist": "Zabaldu kontrol-zerrenda",
"uncollapse": "Zabaldu",
"hideCheckedChecklistItems": "Ezkutatu egiaztatutako zerrendako elementuak",
"hideAllChecklistItems": "Ezkutatu kontrol-zerrendako elementu guztiak",
+3
View File
@@ -710,6 +710,7 @@
"initials": "تخصیصات اولیه",
"invalid-date": "تاریخ نامعتبر",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سال نامعتبر است. هر چهار رقم را وارد کنید، برای مثال 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "متصل",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "جمع کردن",
"collapse-checklist": "جمع کردن چک‌لیست",
"expand-checklist": "باز کردن چک‌لیست",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "پنهان کردن موارد انجام شده چک لیست",
"hideAllChecklistItems": "پنهان کردن همه موارد چک لیست",
+3
View File
@@ -710,6 +710,7 @@
"initials": "تخصیصات اولیه",
"invalid-date": "تاریخ نامعتبر",
"invalid-time": "زمان نامعتبر",
"invalid-year": "سال نامعتبر است. هر چهار رقم را وارد کنید، برای مثال 2026.",
"invalid-user": "کاربر نامعتبر",
"joined": "متصل",
"just-invited": "هم اکنون، شما به این برد دعوت شده‌اید.",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "تحويل إلى تخفيض السعر",
"import-board-zip": "أضف ملف .zip الذي يحتوي على ملفات JSON الخاصة باللوحة، ودلائل فرعية لأسماء اللوحة مع المرفقات",
"collapse": "جمع کردن",
"collapse-checklist": "جمع کردن چک‌لیست",
"expand-checklist": "باز کردن چک‌لیست",
"uncollapse": "إلغاء الانهيار",
"hideCheckedChecklistItems": "پنهان کردن موارد انجام شده چک لیست",
"hideAllChecklistItems": "پنهان کردن همه موارد چک لیست",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Nimikirjaimet",
"invalid-date": "Virheellinen päivämäärä",
"invalid-time": "Virheellinen aika",
"invalid-year": "Virheellinen vuosi. Kirjoita kaikki neljä numeroa, esimerkiksi 2026.",
"invalid-user": "Virheellinen käyttäjä",
"joined": "liittyi",
"just-invited": "Sinut on juuri kutsuttu tälle taululle",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Muuta markdowniksi",
"import-board-zip": "Lisää .zip tiedosto jossa on taulu JSON tiedostot, ja taulu nimi alihakemistot liitteineen",
"collapse": "Pienennä",
"collapse-checklist": "Pienennä tarkistuslista",
"expand-checklist": "Suurenna tarkistuslista",
"uncollapse": "Suurenna",
"hideCheckedChecklistItems": "Piilota ruksatut tarkistuslistan kohdat",
"hideAllChecklistItems": "Piilota kaikki tarkistuslistan kohdat",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Initiales",
"invalid-date": "Date invalide",
"invalid-time": "Heure invalide",
"invalid-year": "Année invalide. Saisissez les quatre chiffres, par exemple 2026.",
"invalid-user": "Utilisateur invalide",
"joined": "a rejoint",
"just-invited": "Vous venez d'être invité à ce tableau",
@@ -1846,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": "Réduire la check-list",
"expand-checklist": "Développer la check-list",
"uncollapse": "Développer",
"hideCheckedChecklistItems": "Cacher les éléments cochés de la check-list",
"hideAllChecklistItems": "Cacher tous les éléments de la check-list",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Initiales",
"invalid-date": "Date invalide",
"invalid-time": "Heure invalide",
"invalid-year": "Année invalide. Saisissez les quatre chiffres, par exemple 2026.",
"invalid-user": "Utilisateur invalide",
"joined": "a rejoint",
"just-invited": "Vous venez d'être invité à ce tableau",
@@ -1846,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": "Réduire la check-list",
"expand-checklist": "Développer la check-list",
"uncollapse": "Développer",
"hideCheckedChecklistItems": "Cacher les éléments cochés de la check-list",
"hideAllChecklistItems": "Cacher tous les éléments de la check-list",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Initiales",
"invalid-date": "Date invalide",
"invalid-time": "Heure invalide",
"invalid-year": "Année invalide. Saisissez les quatre chiffres, par exemple 2026.",
"invalid-user": "Utilisateur invalide",
"joined": "a rejoint",
"just-invited": "Vous venez d'être invité à ce tableau",
@@ -1846,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": "Réduire la check-list",
"expand-checklist": "Développer la check-list",
"uncollapse": "Développer",
"hideCheckedChecklistItems": "Cacher les éléments cochés de la check-list",
"hideAllChecklistItems": "Cacher tous les éléments de la check-list",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Inisjalen",
"invalid-date": "Unjildige datum",
"invalid-time": "Unjildige tiid",
"invalid-year": "Unjildich jier. Typ alle fjouwer sifers, bygelyks 2026.",
"invalid-user": "Unjildige brûker",
"joined": "meidien",
"just-invited": "Jo binne krekt útnoege foar dit bestjoer",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Konvertearje nei markdown",
"import-board-zip": "Add .zip triem dat hat board JSON triemmen, en board namme submappen mei taheaksels",
"collapse": "Ynstoarte",
"collapse-checklist": "Kontrôlelist ynklappe",
"expand-checklist": "Kontrôlelist útklappe",
"uncollapse": "Utklappe",
"hideCheckedChecklistItems": "Hide kontrolearre checklist items",
"hideAllChecklistItems": "Ferbergje alle checklist items",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Inisjalen",
"invalid-date": "Unjildige datum",
"invalid-time": "Unjildige tiid",
"invalid-year": "Unjildich jier. Typ alle fjouwer sifers, bygelyks 2026.",
"invalid-user": "Unjildige brûker",
"joined": "meidien",
"just-invited": "Jo binne krekt útnoege foar dit bestjoer",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Konvertearje nei markdown",
"import-board-zip": "Add .zip triem dat hat board JSON triemmen, en board namme submappen mei taheaksels",
"collapse": "Ynstoarte",
"collapse-checklist": "Kontrôlelist ynklappe",
"expand-checklist": "Kontrôlelist útklappe",
"uncollapse": "Utklappe",
"hideCheckedChecklistItems": "Hide kontrolearre checklist items",
"hideAllChecklistItems": "Ferbergje alle checklist items",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Iniciais",
"invalid-date": "A data é incorrecta",
"invalid-time": "Hora inválida",
"invalid-year": "Ano non válido. Escribe os catro díxitos, por exemplo 2026.",
"invalid-user": "Utilizador inválido",
"joined": "juntou-se",
"just-invited": "Acabou de ser convidado para este quadro",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Converter para markdown",
"import-board-zip": "Adicione o arquivo .zip que contém arquivos JSON do quadro e subdiretórios de nomes do quadro com anexos",
"collapse": "Colapsar",
"collapse-checklist": "Contraer a lista de verificación",
"expand-checklist": "Expandir a lista de verificación",
"uncollapse": "Compactar",
"hideCheckedChecklistItems": "Esconder itens concluídos da lista de verificação",
"hideAllChecklistItems": "Esconder todos os itens da lista de verificação",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Iniciais",
"invalid-date": "A data é incorrecta",
"invalid-time": "Hora inválida",
"invalid-year": "Ano non válido. Escribe os catro díxitos, por exemplo 2026.",
"invalid-user": "Utilizador inválido",
"joined": "juntou-se",
"just-invited": "Acabou de ser convidado para este quadro",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Converter para markdown",
"import-board-zip": "Adicione o arquivo .zip que contém arquivos JSON do quadro e subdiretórios de nomes do quadro com anexos",
"collapse": "Colapsar",
"collapse-checklist": "Contraer a lista de verificación",
"expand-checklist": "Expandir a lista de verificación",
"uncollapse": "Compactar",
"hideCheckedChecklistItems": "Esconder itens concluídos da lista de verificação",
"hideAllChecklistItems": "Esconder todos os itens da lista de verificação",
+3
View File
@@ -710,6 +710,7 @@
"initials": "આદ્યાક્ષરો",
"invalid-date": "અમાન્ય તારીખ",
"invalid-time": "અમાન્ય સમય",
"invalid-year": "અમાન્ય વર્ષ. ચારેય અંક લખો, ઉદાહરણ તરીકે 2026.",
"invalid-user": "અમાન્ય વપરાશકર્તા",
"joined": "જોડાયા",
"just-invited": "તમને આ બોર્ડમાં આમંત્રિત કર્યા છે",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "માર્કડાઉનમાં કન્વર્ટ કરો",
"import-board-zip": "બોર્ડ JSON ફાઇલો ધરાવતી .zip ફાઇલ અને જોડાણો સાથે બોર્ડ નામ સબડિરેક્ટરીઝ ઉમેરો",
"collapse": "સંકુચિત કરો",
"collapse-checklist": "ચેકલિસ્ટ સંકુચિત કરો",
"expand-checklist": "ચેકલિસ્ટ વિસ્તૃત કરો",
"uncollapse": "અનકલેપ્સ",
"hideCheckedChecklistItems": "ચેક કરેલ ચેકલિસ્ટ વસ્તુઓ છુપાવો",
"hideAllChecklistItems": "તમામ ચેકલિસ્ટ વસ્તુઓ છુપાવો",
+3
View File
@@ -710,6 +710,7 @@
"initials": "ראשי תיבות",
"invalid-date": "תאריך שגוי",
"invalid-time": "זמן שגוי",
"invalid-year": "שנה שגויה. יש להקליד את כל ארבע הספרות, למשל 2026.",
"invalid-user": "משתמש שגוי",
"joined": "הצטרפות",
"just-invited": "הוזמנת ללוח זה",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "המרה ל־Markdown",
"import-board-zip": "הוספת קובץ ‎.zip עם קובצי JSON של לוח, ותת־תיקיות בשם הלוח עם קבצים מצורפים",
"collapse": "צמצום",
"collapse-checklist": "צמצום רשימת המטלות",
"expand-checklist": "הרחבת רשימת המטלות",
"uncollapse": "ביטול צמצום",
"hideCheckedChecklistItems": "הסתרת פריטים שסומנו ברשימת הפריטים",
"hideAllChecklistItems": "הסתרת כל הפריטים ברשימת המטלות",
+3
View File
@@ -710,6 +710,7 @@
"initials": "ראשי תיבות",
"invalid-date": "תאריך שגוי",
"invalid-time": "זמן שגוי",
"invalid-year": "שנה שגויה. יש להקליד את כל ארבע הספרות, למשל 2026.",
"invalid-user": "משתמש שגוי",
"joined": "הצטרפות",
"just-invited": "הוזמנת ללוח זה",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "המרה ל־Markdown",
"import-board-zip": "הוספת קובץ ‎.zip עם קובצי JSON של לוח, ותת־תיקיות בשם הלוח עם קבצים מצורפים",
"collapse": "צמצום",
"collapse-checklist": "צמצום רשימת המטלות",
"expand-checklist": "הרחבת רשימת המטלות",
"uncollapse": "ביטול צמצום",
"hideCheckedChecklistItems": "הסתרת פריטים שסומנו ברשימת הפריטים",
"hideAllChecklistItems": "הסתרת כל הפריטים ברשימת המטלות",
+3
View File
@@ -710,6 +710,7 @@
"initials": "આદ્યાક્ષરો",
"invalid-date": "અમાન્ય તારીખ",
"invalid-time": "અમાન્ય સમય",
"invalid-year": "अमान्य वर्ष। चारों अंक टाइप करें, उदाहरण के लिए 2026।",
"invalid-user": "અમાન્ય વપરાશકર્તા",
"joined": "જોડાયા",
"just-invited": "You are just invited तक यह बोर्ड",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "मार्कडाउन में कनवर्ट करें",
"import-board-zip": ".zip फ़ाइल जोड़ें जिसमें बोर्ड JSON फ़ाइलें हैं, और अनुलग्नकों के साथ बोर्ड नाम उपनिर्देशिकाएँ हैं",
"collapse": "संक्षिप्त करें",
"collapse-checklist": "चेकलिस्ट संक्षिप्त करें",
"expand-checklist": "चेकलिस्ट विस्तृत करें",
"uncollapse": "असंकुचित करें",
"hideCheckedChecklistItems": "चेक किए गए चेकलिस्ट आइटम छुपाएं",
"hideAllChecklistItems": "सभी चेकलिस्ट आइटम छुपाएं",
+3
View File
@@ -710,6 +710,7 @@
"initials": "આદ્યાક્ષરો",
"invalid-date": "અમાન્ય તારીખ",
"invalid-time": "અમાન્ય સમય",
"invalid-year": "अमान्य वर्ष। चारों अंक टाइप करें, उदाहरण के लिए 2026।",
"invalid-user": "અમાન્ય વપરાશકર્તા",
"joined": "જોડાયા",
"just-invited": "You are just invited तक यह बोर्ड",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "मार्कडाउन में कन्वर्ट करें",
"import-board-zip": ".zip फ़ाइल जोड़ें जिसमें बोर्ड JSON फ़ाइलें और अनुलग्नकों वाले उपनिर्देशिका हों",
"collapse": "संक्षिप्त करें",
"collapse-checklist": "चेकलिस्ट संक्षिप्त करें",
"expand-checklist": "चेकलिस्ट विस्तृत करें",
"uncollapse": "असंकुचित करें",
"hideCheckedChecklistItems": "चेक की गई चेकलिस्ट आइटम छिपाएं",
"hideAllChecklistItems": "सभी चेकलिस्ट आइटम छिपाएं",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Inicijali",
"invalid-date": "Нетачан датум",
"invalid-time": "Нетачно време",
"invalid-year": "Nevaljana godina. Upišite sve četiri znamenke, na primjer 2026.",
"invalid-user": "Непознат корисник",
"joined": "придружен",
"just-invited": "Управо сте позвани да узмете учешће у раду на овим списима",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Претвори у структуирани текст",
"import-board-zip": "Додај .zip архиву која има списе у json облику и фасцикле по имену списа где је предметна грађа",
"collapse": "Скупи",
"collapse-checklist": "Sažmi kontrolnu listu",
"expand-checklist": "Proširi kontrolnu listu",
"uncollapse": "Рашири",
"hideCheckedChecklistItems": "Сакриј све обављене помоћне предметне радње",
"hideAllChecklistItems": "Сакриј све помоћне предметне радње",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Kezdőbetűk",
"invalid-date": "Érvénytelen dátum",
"invalid-time": "Érvénytelen idő",
"invalid-year": "Érvénytelen év. Írd be mind a négy számjegyet, például 2026.",
"invalid-user": "Érvénytelen felhasználó",
"joined": "csatlakozott",
"just-invited": "Éppen most hívták meg erre a táblára",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Konvertálás markdownra",
"import-board-zip": "Adj hozzá egy .zip fájlt, amely tábla JSON fájlokat és tábla nevű almappákat tartalmaz mellékletekkel",
"collapse": "Összecsukás",
"collapse-checklist": "Ellenőrzőlista összecsukása",
"expand-checklist": "Ellenőrzőlista kibontása",
"uncollapse": "Kibontás",
"hideCheckedChecklistItems": "Kipipált ellenőrzőlista-elemek elrejtése",
"hideAllChecklistItems": "Az összes ellenőrzőlista-elem elrejtése",
+3
View File
@@ -710,6 +710,7 @@
"initials": "սկզբնատառեր",
"invalid-date": "Անվավեր ամսաթիվ",
"invalid-time": "Անվավեր ժամանակ",
"invalid-year": "Անվավեր տարի: Մուտքագրեք բոլոր չորս թվանշանները, օրինակ՝ 2026:",
"invalid-user": "Անվավեր օգտվող",
"joined": "միացել է",
"just-invited": "Դուք հենց նոր հրավիրված եք այս խորհուրդը",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Փոխարկել նշագրման",
"import-board-zip": "Ավելացրեք .zip ֆայլ, որն ունի տախտակի JSON ֆայլեր և տախտակի անունների ենթագրքեր՝ հավելվածներով",
"collapse": "Փլուզվել",
"collapse-checklist": "Ծալել ստուգաթերթիկը",
"expand-checklist": "Բացել ստուգաթերթիկը",
"uncollapse": "Չփլուզվել",
"hideCheckedChecklistItems": "Թաքցնել ստուգված ստուգաթերթի տարրերը",
"hideAllChecklistItems": "Թաքցնել ստուգաթերթի բոլոր կետերը",
+3
View File
@@ -710,6 +710,7 @@
"initials": "Inisial",
"invalid-date": "Tanggal tidak sah",
"invalid-time": "Masa tidak sah",
"invalid-year": "Tahun tidak valid. Ketik keempat digitnya, misalnya 2026.",
"invalid-user": "Pengguna tidak sa",
"joined": "bergabung",
"just-invited": "Anda baru diundang di panel ini",
@@ -1846,6 +1847,8 @@
"convert-to-markdown": "Mengubah ke markdown",
"import-board-zip": "Tambahkan file .zip yang berisi file JSON papan, dan subdirektori nama papan dengan lampiran",
"collapse": "Ciutkan",
"collapse-checklist": "Ciutkan daftar periksa",
"expand-checklist": "Bentangkan daftar periksa",
"uncollapse": "Buka lipatan",
"hideCheckedChecklistItems": "Sembunyikan item checklist yang tercentang",
"hideAllChecklistItems": "Sembunyikan semua item checklist",
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff. Show more