Compare commits

...
150 Commits
Author SHA1 Message Date
github-actions[bot] bbcbee41c9 Bump versions for v10.02 2026-07-19 01:16:57 +00:00
Lauri Ojansivu 8d30efa117 Updated ChangeLog. 2026-07-19 04:15:37 +03:00
Lauri Ojansivu 6f70544c60 Changelog: add merged dependabot updates to Upcoming. 2026-07-19 04:14:14 +03:00
Lauri Ojansivu acc86fa86a Merge pull request #6482 from wekan/dependabot/github_actions/actions/setup-node-7
Bump actions/setup-node from 6 to 7
2026-07-19 04:11:04 +03:00
Lauri Ojansivu 96c59b2d01 Merge pull request #6485 from wekan/dependabot/npm_and_yarn/aws-sdk/client-s3-3.1090.0
Bump @aws-sdk/client-s3 from 3.1085.0 to 3.1090.0
2026-07-19 04:08:43 +03:00
Lauri Ojansivu 1a88240137 Merge pull request #6486 from wekan/dependabot/npm_and_yarn/dompurify-3.4.12
Bump dompurify from 3.4.11 to 3.4.12
2026-07-19 04:07:29 +03:00
Lauri Ojansivu d8bda40806 Merge pull request #6487 from wekan/dependabot/npm_and_yarn/filesize-11.0.22
Bump filesize from 11.0.17 to 11.0.22
2026-07-19 04:06:55 +03:00
Lauri Ojansivu 2c2e8a6fee Changelog: add Upcoming section (auto-update docs set + release automation). 2026-07-18 23:43:43 +03:00
Lauri Ojansivu cf55da122a Docs: move Autoupdate/Snap.md to Autoupdate/Forks/Snap-Core.md and repoint links. 2026-07-18 23:42:47 +03:00
dependabot[bot] ccb7a8e91c Bump filesize from 11.0.17 to 11.0.22
Bumps [filesize](https://github.com/avoidwork/filesize.js) from 11.0.17 to 11.0.22.
- [Changelog](https://github.com/avoidwork/filesize.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/avoidwork/filesize.js/commits/11.0.22)

---
updated-dependencies:
- dependency-name: filesize
  dependency-version: 11.0.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 20:33:31 +00:00
dependabot[bot] 963f72ac88 Bump dompurify from 3.4.11 to 3.4.12
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.11 to 3.4.12.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.11...3.4.12)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 20:33:25 +00:00
dependabot[bot] 144bed0eda Bump @aws-sdk/client-s3 from 3.1085.0 to 3.1090.0
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.1085.0 to 3.1090.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1090.0/clients/client-s3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 20:33:14 +00:00
dependabot[bot] 1f11ad2602 Bump actions/setup-node from 6 to 7
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 20:32:23 +00:00
Lauri Ojansivu 2054ea2b54 release-all.yml: add guarded snap-variants / ucs / nextcloud publish jobs.
Wire in three new publish jobs (dormant until their secrets are set — each
guards on its secrets and skips with a ::notice:: so ordinary releases are
unaffected):
- snap-variants: build+publish wekan-ondra and wekan-gantt-gpl snaps.
- ucs: bump + push the wekan/univention app and upload to the App Provider.
- nextcloud: build the ExApp image and publish to the Nextcloud App Store.
Document the new secrets in the header. Add a "Remaining steps" section to the
top of Snap-Ondra-Gantt.md, UCS.md and Nextcloud.md (keys to add + how).
2026-07-18 23:29:56 +03:00
Lauri Ojansivu 8ae0ee5446 Docs: Nextcloud.md — WeKan ExApp build/publish + AI/Deck/SSO integrations. 2026-07-18 23:22:19 +03:00
Lauri Ojansivu c1ffed59a7 Docs: move the per-OS update comparison docs into Autoupdate/OS/ subdir.
The 29 per-OS manual-vs-automatic update comparison docs move into OS/;
the WeKan-specific docs (README, Snap, Snap-Ondra-Gantt, UCS) stay in the
parent. All cross-links repointed and verified.
2026-07-18 23:22:19 +03:00
Lauri Ojansivu 58c0e6d1f8 Docs: UCS.md — releasing Docker WeKan to UCS App Center with MongoDB 3.x -> FerretDB migration. 2026-07-18 23:22:19 +03:00
Lauri Ojansivu cf1823b510 Docs: per-OS install/update comparison set in Design/Autoupdate/.
Add manual-vs-automatic (Snap-like) update comparison docs for all operating
systems with any Internet update mechanism: Linux, BSD, Haiku, Amiga, RISC OS,
Solaris/illumos, AIX, HP-UX, OpenVMS, z/OS, ArcaOS, ReactOS, FreeDOS, Plan 9,
Redox, SerenityOS, Android, iOS, ChromeOS, HarmonyOS, Ubuntu Touch, Sailfish,
postmarketOS, KaiOS, Tizen, webOS and TV/streaming OSes; plus a README index.
Add Snap-Ondra-Gantt.md (keys/secrets + release-all.yml changes to also release
the wekan-ondra and wekan-gantt-gpl snaps). Rename the two earlier dev docs to
Snap.md / Windows.md and repoint all links.
2026-07-18 23:22:19 +03:00
Lauri Ojansivu 2a878d55e7 Docs: macOS install & update platforms (manual vs automatic, like Snap). 2026-07-18 23:22:19 +03:00
Lauri Ojansivu 48a60a1141 Docs: move the auto-update dev docs into Design/Autoupdate/. 2026-07-18 23:22:19 +03:00
Lauri Ojansivu 3bfc6cce56 Docs: Windows install & update platforms (manual vs automatic, like Snap). 2026-07-18 23:22:19 +03:00
Lauri Ojansivu 8cf7dd68cc Docs: move Mac.md into Propietary/Mac/ and update all links to the new path. 2026-07-18 23:22:19 +03:00
Lauri Ojansivu 742fc9221a Docs: why ppc64el/s390x snap builds fail on core24 (multiarch action caps at core22). 2026-07-18 23:22:19 +03:00
github-actions[bot] add47dc065 Bump versions for v10.01 2026-07-18 19:01:13 +00:00
Lauri Ojansivu 5af16df201 Updated ChangeLog. 2026-07-18 22:00:12 +03:00
Lauri Ojansivu adaecfe8c2 Changelog: note the zh-alias/header-cog test fixes in Upcoming. 2026-07-18 21:24:10 +03:00
Lauri Ojansivu 08cfe2d788 Tests: fix zh-alias and header-cog regressions found by the full test run.
- i18n: registering generic Chinese (zh) as its own entry made bare "zh"
  resolve to itself, so the "zh -> zh-Hans" alias is now dead code — remove it
  from LANGUAGE_ALIASES (keep wuu -> wuu-Hans) and update the two mocha tests
  (resolveTag/setLanguage) to expect "zh" instead of "zh-Hans".
- e2e (15-board-actions): the Board Settings cog was removed from the header
  (it is in the right sidebar now), so the "one click from the header cog" test
  failed in every browser. Rewrite it to open the sidebar and open the board
  menu (which still has Archived Items) from there.
2026-07-18 21:18:10 +03:00
Lauri Ojansivu ef6b2794b8 Changelog: add Upcoming section with the changes made after v10.00.
The changes committed since the v10.00 release bump had no CHANGELOG Upcoming
section. Add one above v10.00 covering all of them: the "Submit editors with
Enter" Member Setting, the right-sidebar drag-to-resize width feature, the #6481
migration NaN/Infinity fix, the #6465 Add List inline-composer options restore,
the right-sidebar width/inputs fix, and the missing copy-selection key — each
with commit links.
2026-07-18 20:37:00 +03:00
Lauri Ojansivu 1284afc588 i18n: translate drag-to-resize-sidebar tooltip to all languages.
Add the sidebar resize handle tooltip (drag-to-resize-sidebar, "Drag to resize
sidebar") to every language file, translated into each language (English-variant
locales carry the English text). All 154 files JSON-validated; value-only
additions.
2026-07-18 19:49:01 +03:00
Lauri Ojansivu 354b7804b5 Right sidebar: drag the edge to resize its width (desktop), saved per user.
Add a resize handle on the sidebar edge, like resizing a spreadsheet column
(mirrors the list resize in client/components/lists/list.js). Drag it to change
the sidebar width; the width is saved to profile.sidebarWidth for logged-in
users and to browser localStorage (wekan-sidebar-width) for anonymous users on
a public board, then re-applied on load.

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

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

Thanks to xet7 !
2026-07-18 19:42:19 +03:00
Lauri Ojansivu 72042b7d79 i18n: add missing copy-selection key (Multi-Selection sidebar) in all languages.
The Multi-Selection right-sidebar "Copy selection" button used {{_ (quote)copy-selection(quote)}}
but the key was missing from every language file — including the en source —
so i18next had no value to fall back to and rendered the raw key. Add
copy-selection ("Copy selection") to en and translate it to all languages,
mirroring each language(apos)s existing "move-selection" sibling with the verb changed
to copy. All 154 files JSON-validated; value-only additions.

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

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

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

Thanks to csonkaoszimt and xet7 !

Fixes #6465.
2026-07-18 18:55:19 +03:00
Lauri Ojansivu b8bb004650 i18n: translate submit-on-enter and its description to all languages.
Add the two new Member Settings keys (submit-on-enter, submit-on-enter-
description) to every language file in imports/i18n/data/, translated into each
language and inserted after show-desktop-drag-handles. English-variant locales
(en-GB, en_AU, ...) carry the English text; each real language matches its own
terminology, and the keyboard-key tokens Enter, Shift+Enter and Ctrl/Cmd+Enter
are kept verbatim. All 154 files JSON-validated; value-only additions.
2026-07-18 18:25:59 +03:00
Lauri Ojansivu bb6b0d5477 Member Settings: optional "Submit editors with Enter" (per-user, off by default).
#4236 made every multi-line editor submit only on Ctrl/Cmd+Enter (plain Enter
= newline) so titles could be multi-line, but that removed the fast
Enter-to-save title workflow some users rely on (v10 email feedback). Add a
per-user preference in the Member Settings (Change Settings) popup, saved to
profile.submitOnEnter and OFF by default: when on, plain Enter submits the card
title, description and other inlined forms, and Shift+Enter inserts a newline;
when off, behaviour is unchanged (Ctrl/Cmd+Enter saves).

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

Thanks to xet7 !

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

Thanks to Nissulya and xet7 !

Fixes #6481.
2026-07-18 16:42:11 +03:00
Lauri Ojansivu 077e2196f5 Updated ChangeLog. 2026-07-18 16:34:50 +03:00
Lauri Ojansivu e010c09e87 Changelog: rewrite v10.00 from what commits actually implemented.
Rewrite the v10.00 section to match the final implementation and group ALL 101
commits in the release under their own headings (each with commit links):

- Corrected "done differently" cases: Change Color is visible grouped swatches
  (not the initial two-level dropdowns); Font is font-name + size buttons with
  immediate apply (not dropdowns); the board-header "icons only" change was
  reverted so button texts stay.
- Fixed the Home board entry: it is set via the All Boards Multi-Selection
  "Selected:" home action and the after-login redirect runs on all deployments,
  so it is available, NOT disabled and NOT Sandstorm-only (an earlier draft
  wrongly said the UI was disabled).
- Every commit since v9.99 is now listed once, grouped by feature/fix area.
2026-07-18 08:46:01 +03:00
github-actions[bot] 356b1152bb Bump versions for v10.00 2026-07-18 05:11:26 +00:00
Lauri Ojansivu 66a2f8ddc8 Updated ChangeLog. 2026-07-18 08:09:43 +03:00
Lauri Ojansivu ef81c25232 ci: disable Playwright E2E Tests workflow.
Rename .github/workflows/playwright.yml -> playwright.yml.disabled so GitHub
Actions no longer runs it (only .yml/.yaml files in .github/workflows are
executed). Reversible by renaming back. NOTE: this workflow also contained the
Meteor unit-tests job and a Puppeteer regression job, so those stop running too.
2026-07-18 08:01:52 +03:00
Lauri Ojansivu 2be9253839 releases: fix push-all-translations.sh cd depth for its translations/ location.
The script lives in releases/translations/, so the repo root is two levels up
(../..), not one. With a single .. it landed in releases/ and looked for ../tx
at wekan/tx instead of the sibling ../tx, aborting with "tx not found". Match
the cd "$(dirname "$0")/../.." pattern used by the other scripts here.
2026-07-18 07:54:28 +03:00
Lauri Ojansivu 2cf878215d Changelog: add Part 12 (Waray wa-RR fix) to the 154-languages entry. 2026-07-18 07:43:33 +03:00
Lauri Ojansivu c00f24d668 i18n: fix wa-RR — translate as Waray (war), not Walloon.
The .tx config maps Transifex "war" -> local wa-RR.i18n.json, i.e. wa-RR is
Waray/Winaray (a Philippine language), NOT a regional variant of Walloon (wa).
An earlier seed wrongly copied Walloon content into it. Retranslate the file
fresh from English into Waray (2191 strings); placeholders/brand names
preserved, key set unchanged, JSON validated.
2026-07-18 07:43:10 +03:00
Lauri Ojansivu 95552881fc Moved translation script to correct directory. Thanks to xet7. 2026-07-18 07:34:17 +03:00
Lauri Ojansivu 5b3394af2f Changelog: WeKan translated to 154 languages, with commit links. 2026-07-18 07:18:22 +03:00
Lauri Ojansivu d55c5e2c3c i18n: register wuu-Hans, fix Wolof/Malay names, add wuu browser alias.
- Register Wu Chinese (wuu-Hans): it had a data file but no languages.js
  entry, so it was neither browser-detectable nor shown in Change Language.
- Add wuu -> wuu-Hans to LANGUAGE_ALIASES so a bare "wuu" browser locale
  resolves (mirrors zh -> zh-Hans).
- Fix display names that were in the wrong script for their (Latin) content:
  Wolof "ولوفل" -> "Wolof"; Malay "بهاس ملايو" -> "Bahasa Melayu" (+ Malaysia).
km_KH/ru_RU are symlinks to km-KH/ru-RU, already covered. Every data file is
now registered, so all are browser-detectable and visible in the popup.
2026-07-18 07:16:00 +03:00
Lauri Ojansivu 01d55828d2 WeKan has been translated to 154 languages.
Thanks to xet7 !
2026-07-18 07:11:49 +03:00
Lauri Ojansivu 219011e5eb i18n: translate untranslated strings (wave 7, final: 14 languages + 5 variants).
Fresh translations: vi, uz, uz-AR (Arabic-script Uzbek), ve (Venda), ve-CC
(Venetian), ve-PP (Vepsian), vo (Volapuk), wa (Walloon), wo (Wolof), xh
(Xhosa), yi (Yiddish), yo (Yoruba), zgh (Tamazight/Tifinagh), zu (Zulu).
Seeded variants uz-UZ/uz-LA, vi-VN, wa-RR, zu-ZA from bases. Completes the
full 153-language set. Placeholders/brand names preserved; JSON validated.
2026-07-18 06:58:27 +03:00
Lauri Ojansivu 91ccef062b i18n: translate untranslated strings (wave 6: 15 languages + 2 variants).
Fresh translations: nb, oc, or_IN, pa, ro, sk, sl, sr, sw, ta, te-IN, th,
tk_TM, tlh (Klingon), ug (Uyghur). Seeded variants ro-RO, sl_SI from bases.
Terminology matches each file glossary + standard Kanban terms; brand names,
acronyms, env vars, placeholders and markup preserved verbatim. JSON validated.
2026-07-18 06:28:58 +03:00
Lauri Ojansivu 97feb986a8 i18n: translate untranslated strings (wave 5: 15 languages + 5 variants).
Fresh translations: gl, gu-IN, hi, hr, hu, hy, id, ig, ka, km, lt, lv, mk, mn,
ms. Seeded variants from those bases: gl-ES; hi-IN; km-KH/km_KH; ms-MY.
Terminology matches each file glossary + standard Kanban terms; brand names,
acronyms, env vars, placeholders and markup preserved verbatim. JSON validated.
2026-07-18 05:43:34 +03:00
Lauri Ojansivu a184a3a717 i18n: translate untranslated strings (wave 4: 15 languages + 9 variants).
Fresh translations: ace, af, ast-ES, az, be, bg, br, ca, cy, el, eo, et-EE,
eu, fa, fy. Seeded regional variants from those bases: af_ZA; az-AZ/az-LA;
ca_ES/ca@valencia; cy-GB; el-GR; fa-IR; fy-NL. Terminology matches each file
glossary + standard Kanban terms; brand names, acronyms, env vars, placeholders
and markup preserved verbatim. JSON validated. ~18800 strings.
2026-07-18 04:56:25 +03:00
Lauri Ojansivu 75ec34e837 i18n: register generic Chinese (zh) so it is selectable and browser-detectable.
zh.i18n.json existed but had no entry in the languages registry, so a browser
reporting bare "zh" could not auto-detect it and it was missing from the Change
Language list. Add it (code zh, name 中文). The other two unregistered data
files (km_KH, ru_RU) are underscore duplicates already covered by the existing
km-KH / ru-RU entries, whose tag is the underscore form.
2026-07-18 04:37:28 +03:00
Lauri Ojansivu c795d13450 i18n: seed close-relative lects as baselines (cmn, wuu, yue, ja-Hira, vls, ary).
Seed Mandarin/Wu/Cantonese (cmn, wuu-Hans, yue_CN) from zh-CN, Japanese-Hiragana
locale (ja-HI) from ja, West Flemish (vl-SS) from nl, and Moroccan Arabic (ary)
from ar as machine-quality baselines sharing the written form, for the community
to refine. Placeholders preserved; JSON validated. 4074 strings.
2026-07-18 04:17:19 +03:00
Lauri Ojansivu ea4a10c212 i18n: seed 32 regional variants from their translated base languages.
Fill untranslated strings in same-language, same-script regional variants by
copying the freshly-translated base: de -> de-AT/de-CH/de_DE; es -> es-AR/-CL/
-CO/-LA/-MX/-PE/-PY/es_CO; fr -> fr-CH/fr-FR; ru -> ru-RU/ru_RU/ru-UA; pl-PL;
cs-CZ; ko-KR; uk-UA; pt -> pt-PT/pt_PT; zh-CN -> zh-Hans/zh/zh_SG/zh-GB;
zh-TW -> zh-Hant/zh-HK; he-IL; nl-NL; ar -> ar-DZ/ar-EG. Keeps variants
consistent with their base and preserves placeholders. JSON validated.
24632 strings.
2026-07-18 04:16:11 +03:00
Lauri Ojansivu 7691829cdb i18n: translate untranslated strings (wave 2: 15 major languages).
Fill previously-untranslated UI strings in de, fr, es, it, nl, ru, pl, uk, cs,
ja, ko, zh-CN, tr, ar, pt. Each language matches its existing file terminology
as a glossary plus standard Kanban terms; product/brand names, acronyms, env
vars, console commands, placeholders (__var__, %s, {{..}}) and markup preserved
verbatim. Value-only changes; every file JSON-validated.

de 524, fr 617, es 917, it 938, nl 458, ru 974, pl 1037, uk 895, cs 1108,
ja 926, ko 850, zh-CN 929, tr 1059, ar 1145, pt 500 strings.
2026-07-18 04:12:59 +03:00
Lauri Ojansivu 63bda22faa i18n: translate untranslated strings (wave 1: zh-TW, pt-BR, da, he, sv).
Fill previously-untranslated UI strings in the five near-complete languages,
matching each file existing terminology as a glossary and standard Kanban
terms (board, list, card, swimlane, label, checklist, theme, font, ...).
Product/brand names, acronyms, env vars, placeholders (__var__, %s, {{..}})
and markup preserved verbatim. Value-only changes; JSON validated.

zh-TW 29, pt-BR 28, da 159, he 262, sv 382 strings.
2026-07-18 03:59:03 +03:00
Lauri Ojansivu a9f2e9f50c releases: add translations-push-all-translations.sh (force-push all languages).
Pushes the English source, then force-pushes every language translation to
Transifex in one run (../tx push -s; ../tx push -t -f). -f is required or the
Go tx CLI skips languages whose local file is not newer than the server copy.
Warns that it overwrites newer community translations, so pull first if needed.
2026-07-18 03:49:35 +03:00
Lauri Ojansivu 861ded439e Updates 2026-07-18 03:36:21 +03:00
Lauri Ojansivu 687a4af861 i18n: translate new admin/import-export strings to Finnish.
Translate the 21 previously-English strings in fi.i18n.json: the disable-*
and anonymize-* import/export/activities/notifications/watch admin feature
labels and descriptions, plus impersonation report title, impersonated user
and reason. Terminology follows existing Finnish strings (seuraaminen,
toimet, profiilikuva, tuonti/vienti, esiintyä käyttäjänä).
2026-07-18 03:32:36 +03:00
Lauri Ojansivu d67d27f0d8 All Boards: "Selected:" star/home actions + Home board badge (#2220).
Below the "Multi-Selection is on" button a "Selected:" row now offers two
actions on the multi-selected boards:
- star icon: stars every selected board (sets them as Starred).
- home icon: sets the first selected board as the Home board (opened after
  login) via the existing toggleDefaultBoard method + router redirect, which
  brings the previously hidden Home-board feature back into use.

The Home board also always shows FIRST in the Starred list (even when it is
not itself starred), badged with a home icon centred at the top of its tile.
2026-07-18 03:19:42 +03:00
Lauri Ojansivu 3736f8431a All Boards: half-width search, hint below Multi-Selection, title beside icon.
- Search boards box is now ~half width (300px); the input flexes to fill it.
- "Click checkboxes to select boards" hint moved out of the path title and
  stacked directly below the "Multi-Selection is on" button (own column).
- Folder icon and the menu title (e.g. "Remaining") are grouped so the title
  always sits to the RIGHT of the icon instead of wrapping below it.
2026-07-18 03:12:18 +03:00
Lauri Ojansivu 01088ea9d4 Notifications menu: make the dropdown item icons black.
The Filter by Unread / View All / Delete All Notifications item icons
(envelope, copy, trash, check) were faint; force the .menu-icon and .check-icon
(and their i.fa) to black.
2026-07-18 02:59:58 +03:00
Lauri Ojansivu 5e014d0967 Notifications drawer: fix X position — cancel the global absolute .close rule.
The X dropped below the header because a global `.close { position: absolute;
top: 5px }` rule (boardBody.css) applied once my rule stopped setting position.
Set position: static on the drawer .close so it stays a flex item and aligns
with the Notifications title at the right edge of the header row.
2026-07-18 02:55:25 +03:00
Lauri Ojansivu c87b4cb510 Notifications drawer: full width + robust flex header (black hamburger + X).
- The drawer and its header are now full width (100vw) instead of 400px.
- Rebuild the header as a flex row (hamburger left, title centre via flex:1, X
  right) instead of fragile absolute positioning, so the X is aligned with the
  Notifications title at the right edge (was sitting too low).
- Force the hamburger and X to black with !important (they were inheriting the
  themed header white link colour), overriding the surrounding themed bar.
2026-07-18 02:51:40 +03:00
Lauri Ojansivu 8dd6bd9db5 Notifications drawer header: black X + hamburger, X near the top-right edge.
- The X close button is now black, moved up (top: 8px, aligned with the
  "Notifications" title instead of vertically centred lower) and to the far
  right (inset-inline-end: 4px).
- The left hamburger menu toggle is black (was #333).
2026-07-18 02:44:51 +03:00
Lauri Ojansivu 8cb4050cf2 Member menu: remove the Notifications entry (duplicates the header bell icon).
The member-menu "Notifications" item and its js-notifications-drawer-toggle
handler toggled the same notifications drawer as the header bell icon
(notifications.js), so it was a duplicate. Remove the menu entry and its
now-unused handler; the header bell is unchanged.
2026-07-18 02:41:47 +03:00
Lauri Ojansivu a4cc0c2182 Search All Boards: label the clear button "Clear" using existing trello-clear-job key.
The js-new-search button clears the search (empties the input + query), so it
is a Clear button. Replace its broom icon with the text of the existing
trello-clear-job translation (en "Clear" / fi "Tyhjennä") and use it for the
aria-label too.
2026-07-18 02:39:09 +03:00
Lauri Ojansivu 98a03bf045 Search All Boards: new-search button follows theme override colors.
The global-search button hardcoded belize #216694 (inline !important styles +
.board-color-belize CSS), ignoring the theme. Make it a .primary type="submit"
button (safe: the search form has onsubmit="return false") so it uses the
theme override colour via the existing button[type=submit].primary rules
(default blue, or the board/global theme), and remove the hardcoded belize
styles.
2026-07-18 02:36:30 +03:00
Lauri Ojansivu 4dc09c62b9 Archived boards: Restore/Delete buttons follow the theme override colors.
The theme override colours .primary buttons via `button[type=submit].primary`
(board-color-* and custom-theme rules). A <button> without an explicit type
attribute does not match [type=submit] in CSS (though it already behaves as
submit), so the archive Restore/Delete buttons kept the default blue. Add
type="submit" (no behaviour change) so they match the theme rules and use the
global theme override colour (named or custom).
2026-07-18 02:32:55 +03:00
Lauri Ojansivu 1c8ba2afc3 Archived boards modal: start below the header bar so its top is visible.
The header bars have a higher z-index than #modal, so the archive panel top
(and the close X) was hidden under the top header bar. Offset the archive
modal content 96px from the top (clears both the quick-access and board header
bars) and extend to the window bottom via min-height calc(100vh - 106px).
2026-07-18 02:29:48 +03:00
Lauri Ojansivu b027594ca8 Archived boards modal: full width + extends to the bottom of the window.
The archive modal used the default .modal-content (500px, margin: 5vh auto),
so archived boards were cramped and the panel stopped short of the window
bottom. Target the archive modal (the only one containing .archived-lists) and
make it 96vw wide with no bottom margin + min-height calc(100vh - 2vh) so it
reaches the bottom of the browser window; each archived board is a full-width
row.
2026-07-18 02:25:30 +03:00
Lauri Ojansivu 08a2e5525e Change Avatar popup: Upload button uses theme colors (blue bg, white text).
Add the .primary class to both "Upload an avatar" buttons so they use the
theme primary-button style (default #005377 blue background + white text,
overridden by the board/global theme color) instead of the plain grey/black
default button.
2026-07-18 02:21:16 +03:00
Lauri Ojansivu 3d190ca022 Font popup: style the "Unset" colour reset links as buttons.
Give .js-reset-text-color / .js-reset-bg-color a button appearance (border,
padding, background, hover) with a dimmed disabled state, matching the font/
size buttons.
2026-07-18 02:19:04 +03:00
Lauri Ojansivu aa15e72682 Member Settings / Font: show the preview pangram below the font-name buttons.
Move the "The quick brown fox ... 0123456789" preview from the bottom of the
popup to directly below the font-name buttons.
2026-07-18 02:18:25 +03:00
Lauri Ojansivu bd8e417be1 Member Settings / Font: font-name + size buttons, immediate apply (no dropdowns).
Redesign the Font popup:
- Font: one button per installed font, each shown IN ITS OWN font (via inline
  style), plus a Default button. Clicking a font applies it immediately.
- Size: a row of buttons ordered by percent — smaller on the left, 100%
  (Default) in the middle, larger on the right. Clicking a size applies it
  immediately.
- Colors: the text/background color wheels now apply immediately on change /
  reset. Removed the Save button (everything applies on click).

New .ui-font-btn / .ui-font-size-btn styling (with .active) in labels.css.
Tests updated in tests/uiFonts.test.cjs.
2026-07-18 02:16:53 +03:00
Lauri Ojansivu fc0649a756 Change Language popup: show languages in multiple columns when there is width.
Widen the language popup responsively (width: min(90vw, 1100px), max-width:
90vw) and lay the list out as an auto-filling grid (repeat(auto-fill,
minmax(170px, 1fr))) so as many languages as possible are visible at once on
wide windows, collapsing to a single column when narrow.

Guarded by tests/changeLanguageColumns.test.cjs.
2026-07-18 02:05:00 +03:00
Lauri Ojansivu 969f96d742 Member Settings / Font popup: add a title (changeFontPopup-title).
Give the Font popup a header like the Change Color popup: en "Font",
fi "Fontti".
2026-07-18 02:01:29 +03:00
Lauri Ojansivu 0f87c3bf8b Change Color popup: remove the "Preview" text from the custom-color preview swatch.
The preview swatch (shown for flat/clear custom colors) still displays the
chosen colour, but no longer shows the "Preview" text label inside it.
2026-07-18 01:59:29 +03:00
Lauri Ojansivu 834698bb5e Revert board-header icons-only; update CHANGELOG Upcoming with newest changes.
- Revert the unconditional hiding of board-header button text labels (Private,
  Muted, Sort, Filter, ...): they show again at normal widths. The pre-existing
  mobile-only (max-width:800px, #6419) icons-only rule is untouched.
- CHANGELOG Upcoming: add consolidated entries for the recent All Boards /
  board-header layout polish, the Change Color swatches / immediate-apply /
  applies-everywhere / popup title, the #4759 user-insert validator fix, and the
  rebuild-wekan.sh CURRENT-IP fix; mark the #2220 home-board toggle as currently
  disabled.
2026-07-18 01:56:47 +03:00
Lauri Ojansivu cd7f9f7bc9 All Boards: move "+ Add Template Container" text 2px down (separate from Add Board).
"+ Add Board" and "+ Add Template Container" share the .js-add-board .label
element, so add an .is-template-container class to the templates-menu variant
and give it padding 20/8/22/8 — 2px lower than "+ Add Board" (18/8/24/8) —
while keeping the same 42px total vertical padding so the tile height is
unchanged.
2026-07-18 01:51:37 +03:00
Lauri Ojansivu 519c04b8ef All Boards: move "+ Add Board" text 2px further up (padding 18/8/24/8). 2026-07-18 01:50:07 +03:00
Lauri Ojansivu 3f5173b960 All Boards: move "+ Add Board" text 4px up (keep tile height).
Shift the .js-add-board .label vertical padding (top 24->20, bottom 18->22)
so the "+ Add Board" / "+ Add Template Container" text sits 4px higher, while
keeping the same 42px total vertical padding so the grey tile stays the same
height as the board icons. Test now checks total vertical padding equality
(height) rather than an exact padding string.
2026-07-18 01:49:40 +03:00
Lauri Ojansivu c21edb160b All Boards: move the Search boards input text 2px further down (top: 8px). 2026-07-18 01:47:16 +03:00
Lauri Ojansivu cb80198d97 All Boards: move the Search boards input text 4px further down (top: 6px). 2026-07-18 01:46:41 +03:00
Lauri Ojansivu 035195f2b4 All Boards: vertically center the Search boards input text.
The search input text sat a little high in the .board-search field; nudge it
down (position: relative; top: 2px) so it reads as vertically centered.
2026-07-18 01:45:47 +03:00
Lauri Ojansivu 197254fe16 All Boards: transparent board tile drag-handle background (icon only).
The desktop board-handle had a dark rgba(0,0,0,0.4) background box; make it
transparent so only the drag-handle icon is visible. The hover highlight is
kept. Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-18 01:43:32 +03:00
Lauri Ojansivu aafe71fd19 All Boards: move board tile drag handle to the right middle.
The desktop board-handle was top-centered (top:4px; left:50%; translateX);
move it to the right middle (top:50%; translateY(-50%); inset-inline-end),
matching the mobile/small-screen handle position. Guarded in
tests/addBoardTileHeight.test.cjs.
2026-07-18 01:42:47 +03:00
Lauri Ojansivu 15e756f4e0 Header/All Boards: reorder Multi-Selection/Sort/Search and swap mobile-toggle/zoom.
- All Boards path header: order is now Multi-Selection, then Sort, then Search
  boards (Search moved to the right of Multi-Selection; Multi-Selection moved to
  the left of Sort). Guarded in tests/addBoardTileHeight.test.cjs.
- Top header bar: swap the positions of the mobile/desktop toggle and the zoom
  controls (mobile toggle now right after the logo, zoom later). Both kept as
  ungated direct children (zoom is no longer hidden when the logo is hidden).
2026-07-18 01:41:48 +03:00
Lauri Ojansivu 97b0240ca4 Board header: icons only — always hide button text labels.
Make the board-header button text labels (Private, Muted, Sort, Filter,
Swimlanes, Multi-Selection, ...) always hidden (was only under a max-width
media query), so the header shows icons only. The board title and the star
count (.board-star-counter) are not button labels and stay.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:27:35 +03:00
Lauri Ojansivu 68723ff43b Board header: remove the Board Settings cog (it is already in the right sidebar).
The #6465 one-click board-settings cog (js-open-board-menu) sat just left of the
sidebar hamburger, duplicating Board Settings which is reachable from the right
sidebar. Remove the cog from boardHeader.jade, its now-dead click handler from
boardHeader.js, and the unused .js-open-board-menu header CSS rule. The sidebar
board-menu button (sidebar.jade/js) is unchanged, and the hamburger + separator
remain.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:14:28 +03:00
Lauri Ojansivu cfd53cc969 Board header: hide button text labels on narrow windows (icons only).
When the window is not wide enough, the 2nd header bar button labels (Private,
Muted, Sort, Multi-Selection, ...) wrapped to a second row. Add a max-width:
1200px media query that hides the .board-header-btn text spans (show icons
only), keeping the board title and the buttons on one line. The star count
(.board-star-counter) is kept.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 01:05:15 +03:00
Lauri Ojansivu bd72f5d64c Board header: hamburger right gap matches the user-name gap (~8px).
Previously the sidebar hamburger sat 12px from the right edge (2px margin +
10px bar right padding), more than the ~8px gap to the right of the user name
in the top bar (.header-user-bar-name margin-inline-end: 8px). Remove the board
header bar right padding and give the hamburger margin-inline-end: 8px, so its
right-edge gap is the same small amount as the user name.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 00:57:23 +03:00
Lauri Ojansivu e33e50d9c8 Board header: remove empty space around the sidebar hamburger button.
The hamburger used the general .board-header-btn margin: 0 12px, leaving a
button-sized empty gap on each side. Tighten its side margins to 2px (and the
preceding cog button right margin to 2px) so the hamburger sits snugly at the
right edge with no wasted space to its left or right.

Guarded in tests/headerHamburger.test.cjs.
2026-07-18 00:53:33 +03:00
Lauri Ojansivu 7718118654 Board header: keep the sidebar hamburger pinned to the right edge at all widths.
The board header bar (Swimlanes + Lists views) used a float layout: the right
button group (which ends with the js-toggle-sidebar hamburger) was float:
inline-end. As the window width changed, the floated groups wrapped and the
hamburger drifted to the middle of the bar. Make #header-main-bar a flex row
(align-items:center, flex-wrap:wrap), make the button groups flex (their
buttons float is ignored inside a flex container), and pin the right group with
margin-inline-start:auto so it always hugs the right edge (or the right of its
row when wrapped).

Guarded by tests/headerHamburger.test.cjs.
2026-07-18 00:49:29 +03:00
Lauri Ojansivu 772ae061f1 Change Color popup: immediate apply (no Save), left-aligned titles, title, spacing.
Feedback fixes to the Select Color picker:
- Apply IMMEDIATELY on clicking a swatch (and when a custom-color wheel is
  committed) via a shared applySelection(); removed the Save button/handler.
- Category titles (Flat/Clear/Dark/Special) are left-aligned and clear: both,
  so they drop below the previous group swatches (which float: inline-start)
  instead of the 2nd/3rd/4th title sitting mid-row.
- Remove the empty band above the first (Flat) title (first-child margin-top:0)
  and tighten the Default row.
- Member Settings Change Color popup now has a header: added changeColorPopup-title
  ("Change Color" / fi "Vaihda väri"), matching Board Settings.

Tests updated in tests/themeColorPicker.test.cjs.
2026-07-18 00:45:33 +03:00
Lauri Ojansivu c6f960bbe1 rebuild-wekan.sh: robust CURRENT-IP detection (fix http://:3000 Invalid URL).
The dev-server CURRENT-IP options detected the IP with
  ip a | grep noprefixroute | ... | grep 192.
which requires the noprefixroute flag AND a 192.x subnet. On hosts without
either (e.g. arm64 Ubuntu on a 10.x/172.x network) it returned empty, so
ROOT_URL became http://:3000 and Meteor crashed with "TypeError: Invalid URL".

Detect the primary IP subnet-agnostically via the default route
(ip route get 1.1.1.1 -> src), fall back to hostname -I, then to localhost so
the URL is always valid. macOS uses ipconfig getifaddr en0/en1 with a localhost
fallback. Applied to both CURRENT-IP dev-server options.
2026-07-17 23:52:52 +03:00
Lauri Ojansivu ac20cd6442 All Boards: hide Home-board icons and disable setting a Home board (#2220).
Remove the fa-home Home-board toggle from the board tiles (boardsList.jade),
its click handler and the isDefaultBoard tile helper (boardsList.js), so a
Home board can no longer be set from the All Boards page. The model helpers,
server toggleDefaultBoard method and the login redirect are left intact but
are no longer reachable from the UI. Test flips to guard the disabled state.
2026-07-17 23:50:39 +03:00
Lauri Ojansivu c6aa769bc0 All Boards: Multi-Selection button looks the same as in Swimlanes View.
The All Boards button showed only an icon (span.emoji-icon > fa-check-square-o)
with the reset X as a sibling, while the Swimlanes View (boardHeader.jade)
shows the check icon + a "Multi-Selection" text label with the reset X nested
inside. Match the All Boards markup to the Swimlanes View (direct icon + text
label + nested reset with fa-times-thin), and add stopPropagation to the reset
handler so the now-nested X does not bubble to the activate handler and
immediately re-enable selection.

Guarded by tests/multiSelectionButton.test.cjs.
2026-07-17 23:48:26 +03:00
Lauri Ojansivu 7699cc1e90 All Boards: My Boards bar wraps so Sort/Multi-Selection/Clear stay visible.
The .boards-path-header was a single-row flex (justify-content: space-between,
no flex-wrap) and .path-right (search box + Sort / Multi-Selection / Clear)
also did not wrap, so narrowing the window — especially with Multi-Selection
on, whose hint widens .path-left — pushed those buttons off/hid them. Add
flex-wrap: wrap to the bar, .path-left (min-width:0 so it can shrink; hint
wraps under the title) and .path-right (buttons wrap among themselves), so
everything drops to a second row and stays visible.

Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-17 23:32:31 +03:00
Lauri Ojansivu 1312fefb12 All Boards: remove empty .board-list-header dead grey space above the layout.
The .board-list-header div (a leftover zoom-controls container) rendered
nothing but still reserved vertical space via margin: 1vh 0 2vh 0 + padding:
1vh 0 — an empty grey band above the boards layout, between the top and the
left menu / "My Boards" bar. Removed the empty element from boardsList.jade
and its now-unused CSS rule, so .boards-layout is the first child of .wrapper.

Guarded in tests/addBoardTileHeight.test.cjs.
2026-07-17 23:27:01 +03:00
Lauri Ojansivu 1008561517 All Boards: + Add Board / + Add Template Container tile same height as board icons.
The grey add-tile (.js-add-board .label, shared by both "+ Add Board" and
"+ Add Template Container") overrode min-height:100px, line-height:56px and
padding:36px 8px 32px 8px — taller than the #6465-thinned board tiles
(.board-list-item: min-height:72px, padding:24px 8px 18px 8px). Match the
board-tile box model so the grey background is the same height as a board icon.

Guarded by tests/addBoardTileHeight.test.cjs (asserts the add-tile min-height +
padding equal the board tile, and the old oversized values are gone).
2026-07-17 23:19:23 +03:00
Lauri Ojansivu 1cd32001d4 Select Color: global override applies everywhere + visible grouped swatches (#5778).
Two fixes from feedback that picking clearblue "did not change" and the
dropdowns were hard to use:

1) The global theme override now wins EVERYWHERE, including board pages. Before,
   the <body> autorun suppressed it whenever a board was open (onBoard gate), so
   changing the color while viewing a board did nothing. Now a set global override
   is applied regardless of board context, and #header / #header-quick-access
   (header.jade) and .board-wrapper (boardBody.jade) prefer the global override
   over the per-board color. Unset it to return to per-board colors. When no
   global is set, an open board still exposes its own custom colors.

2) The picker no longer uses category/theme dropdowns (colors were invisible).
   It now shows the actual color SWATCHES with their names, grouped by category,
   with the category name as a header above each group. The flat/clear color
   wheels still appear once such a color is selected; custom-capability is derived
   from the selected color. customTheme.css styles the group labels.

Tests updated: themeColorPicker/globalThemeColor guards for grouped swatches and
the global-wins-everywhere behaviour.
2026-07-17 23:12:55 +03:00
Lauri Ojansivu 0535215225 Fix add-list: Save did nothing (Match failed) + empty-swimlane + button (#6465).
Two fixes to the #6465 inline Add List composer:

1) Save button did nothing. The inline composer calls createListAfter WITHOUT
   nextListId, but the server check() used Match.OneOf(String,null,undefined)
   (not Match.Optional) for it. Meteor check throws "Match failed [400]" on a
   MISSING object key unless the sub-pattern is Match.Optional, so every inline
   add-list 400'd. The omittable params (swimlaneId/afterListId/nextListId/type)
   are now Match.Optional, so callers may omit them.

2) Empty swimlane/board now shows a + button, and the create-list form appears
   only after clicking it (a swimlane that already has lists keeps using the
   per-list-header add-list button — no + button needed). The reveal is scoped
   to the clicked swimlane via a wekan-add-list-empty Session flag, cleared on
   save/close.

Tests: models/lib/createListParams.js + tests/createListParams.test.cjs mirror
the method's param contract (incl. Meteor's missing-key rule) with the exact
client shapes + negatives, and a source guard that the check uses Match.Optional;
tests/upcomingUiGuards.test.cjs gains guards for the empty-state + button reveal.
Wired into test:unit:node.
2026-07-17 23:01:52 +03:00
Lauri Ojansivu 8aacc22360 Test: de-flake webkit swimlane-add popup wait (6s -> 15s).
specs/13-swimlanes.e2e.js waited only 6s for the swimlane action .js-pop-over
to appear. Under the full parallel suite that occasionally exceeds 6s on
webkit (the popup opens fine, ~3s, when the test runs alone), so the test
flaked. Bump to 15s to match the suite standard popup-visibility wait; a
genuinely broken popup still fails.
2026-07-17 22:47:37 +03:00
Lauri Ojansivu 437c64c9a0 Fix #4759 regression: uiTextColor/uiTextBgColor custom() rejected every user insert.
The optional profile.uiTextColor / profile.uiTextBgColor schema fields ran
their custom() hex validator on EVERY user insert, including when the field
was absent. /^#..$/.test(undefined) is false, so the validator returned
"notAHexColor" and collection2 threw ValidationError for any user with no
custom color set — surfacing as a SyncedCron fatal unhandledRejection and
blocking user creation (seen in ../log test-server.log; also cascaded into
ECONNREFUSED e2e failures as the server destabilized).

Guard both custom() validators to return undefined when the value is unset
(undefined/null/empty), matching the existing list-color validator. Add a
source-guard test so the guard cannot silently regress.
2026-07-17 22:05:18 +03:00
Lauri Ojansivu 061eba8b7a Finnish: translate newest UI strings (theme, font, size, colors, Sandstorm).
Translate the strings added this cycle in fi.i18n.json, reusing existing
terminology (väri, taulu, lista, uimarata, fontti, teema, Oletus): the Select
Color theme categories, the Member Settings font / size / text + background
color pickers, the default-board tooltips, and the list-move confirmation.
Fixes theme-category-clear, which had been auto-translated to the verb
"Tyhjennä" (empty) instead of the theme sense "Kirkas". font-preview-text uses
the standard Finnish pangram so the preview shows ä/ö.
2026-07-17 21:48:12 +03:00
Lauri Ojansivu 176c5ef2d8 Updates 2026-07-17 21:44:36 +03:00
Lauri Ojansivu 9a0c0613a5 Sandstorm: auto-open a single-board grain (no save) (#2220).
Restore the old "a grain opens straight into its board" behaviour as a
convenience, WITHOUT persisting anything. On the Sandstorm home route only:

- a saved Home board (explicitly chosen by the user) always wins -> redirect;
- else, once the boards subscription has loaded, exactly ONE board -> just
  open it (nothing is saved);
- else (zero or many boards, no chosen Home board) -> stay on All Boards.

Choosing a Home board stays a separate, explicit action (the All Boards home
toggle, #2220) — the auto-open never writes it.

The redirect is reactive (Tracker.autorun over a boards subscription), so it
waits out Sandstorm asynchronous grain login (Meteor.userId lands late) and
board loading instead of firing once too early. Runs at most once per page
load, and only when isSandstorm.

- models/lib/sandstormAutoOpen.js: pure, unit-tested decideSandstormAutoOpen()
  (+ tests/sandstormAutoOpen.test.cjs, wired into test:unit:node).
- config/router.js: startSandstormAutoOpen() reactive redirect, gated to
  Sandstorm; non-Sandstorm keeps the existing one-shot default-board redirect.
2026-07-17 21:40:37 +03:00
Lauri Ojansivu 22c7d5376b Member Settings / Font / Color: custom text + background color (#4759).
The Font popup now has a color wheel for the text color and one for the text
background color, each with an Unset button to go back to default. Values are
free choices (native <input type=color>), validated server-side as #rrggbb
hex (models/lib/uiFonts isHexColor6) so only a strict hex ever reaches a CSS
value — no injection surface. An unset/invalid value clears that color.

- profile.uiTextColor / profile.uiTextBgColor schema (hex-validated) + getters;
  setUiColors(textColor, bgColor) method ($set valid hex, $unset otherwise).
- changeFontPopup: two color wheels + reset buttons; live preview reflects
  font, size, text color and background color.
- Applied UI-wide via --wekan-ui-text-color / --wekan-ui-bg-color CSS variables
  + has-ui-text-color / has-ui-bg-color body classes (uiFont.js + uiFont.css).
- i18n text-color / text-background-color; tests extended (hex + negatives +
  source guards).

Part of #4759.
2026-07-17 21:15:51 +03:00
Lauri Ojansivu 3c9524f61a Member Settings / Font: pick an installed font + preset size (#4759).
New "Font" entry in the member menu opens a popup with a dropdown of fonts
DETECTED as actually installed in the browser (no free typing) and a Font
size dropdown of sensible presets (Smaller..Largest, no typed numbers). Both
have a Default option that unsets the custom choice.

Security: font names and size keys are validated server-side against a
curated whitelist / preset list (models/lib/uiFonts.js), so only a known safe
font name and a preset percentage ever reach the DOM. Labels are plain text
(Blaze-escaped) — never HTML or markdown. No CSS-injection surface.

- models/lib/uiFonts.js: UI_FONTS whitelist + SAFE_FONT_RE + isKnownFont /
  fontFamilyValue and UI_FONT_SIZES presets + isKnownFontSize / fontSizeValue.
- client/lib/fontDetector.js: canvas width-comparison detection of installed
  fonts, filtered to the whitelist.
- profile.uiFont / profile.uiFontSize schema + getters; setUiFont /
  setUiFontSize methods (validated, null/default unsets).
- changeFontPopup (font + size dropdowns, both with Default) wired in
  userHeader.js; applied UI-wide via CSS variables in uiFont.js + uiFont.css
  (marker body classes), wired into client/features/main.js + client/styles.js.
- i18n change-font/font/font-default/font-size(+presets); tests/uiFonts.test.cjs
  (pure + negatives + source guards) wired into test:unit:node.

This is a Member Settings feature (not an Admin Panel option).

Fixes #4759.
2026-07-17 21:15:39 +03:00
Lauri Ojansivu d0a1e43f36 Select Color: two-level dropdown picker + custom colors (#5778).
Both Board Settings and Member Settings Change Color now use a shared
category -> theme two-level dropdown picker with custom color wheels for the
flat (1 color) and clear (2 colors, a color slide) categories; dark/special
are fixed. Custom colors are validated server-side as #rrggbb, stored in
board.customThemeColors / profile.globalThemeCustomColors, and applied as
CSS variables (--theme-accent / --theme-accent-2) consumed by customTheme.css
to recolor the header, primary buttons and sidebar button.

- client/components/main/themeColorPicker.{jade,js}: shared picker, scope
  "board" or "global", reusing the #5514 native color wheel + themeCategories.
- board.setColor(color, custom) + setGlobalThemeColor(color, custom): validate
  via isValidCustomColors (flat/clear + count + hex) before storing.
- boardChangeColorPopup and changeColorPopup now render the shared picker;
  removed their old swatch-list helpers/events.
- globalThemeColor.js: apply custom colors as :root CSS variables (board own
  colors on a board, else the user global override) + marker body classes;
  customTheme.css consumes them.
- Wire globalThemeColor.js / themeColorPicker into client/features/main.js and
  customTheme.css into client/styles.js. This also fixes globalThemeColor.js
  never having been imported (so the #5778 override now actually loads).
- i18n theme / theme-category[-flat/clear/dark/special]; tests
  themeColorPicker.test.cjs + updated globalThemeColor.test.cjs.

Fixes #5778.
2026-07-17 20:58:39 +03:00
Lauri Ojansivu 49285dad19 Select Color: theme categories core + design doc (docs/Theme/Theme.md).
Partition the ~19 board/global theme colors into flat / clear / dark /
special and define where custom colors are allowed (flat = 1 accent,
clear = 2 for a color slide, dark/special = fixed none). This is the shared
source of truth for the redesigned Select Color picker (two-level dropdowns
+ color wheel) used by both Board Settings and Member Settings.

- models/lib/themeCategories.js: THEME_CATEGORIES + categoryOf /
  colorsInCategory / allowsCustomColor / customColorCount /
  isValidCustomColors (validates category + count + #rrggbb hex).
- tests/themeCategories.test.cjs (wired into test:unit:node): behaviour +
  negatives + a source guard that the partition exactly equals
  config/const.js ALLOWED_BOARD_COLORS.
- docs/Theme/Theme.md: full design — categories, two-level dropdown UX,
  custom-color rules, CSS-variable rendering plan, storage
  (board.customThemeColors / profile.globalThemeCustomColors), and
  server-side validation.

Completes the global per-user theme override (Member Menu / Change Color)
begun in the previous commit.

Fixes #5778.
2026-07-17 20:46:10 +03:00
Lauri Ojansivu eab1546b88 Member Menu / Change Color: global per-user theme override (#5778).
A new "Change Color" entry in the member menu works like Board Settings /
Change Color, but is a per-user GLOBAL override saved to the profile
(profile.globalThemeColor). The chosen color theme applies to the whole UI —
All Boards, Search All Boards, Admin Panel, My Cards, etc. — via a
board-color-<name> class on <body> and the header, so a user can pick a dark
theme (dark/midnight/moderndark/exodark/cleandark) or any other theme
everywhere. The first popup entry clears the override.

- models/users.js: profile.globalThemeColor field + getGlobalThemeColor /
  globalThemeColorClass / setGlobalThemeColor helpers.
- server/models/users.js: setGlobalThemeColor method, validated against
  BOARD_COLORS (rejects unknown colors so no arbitrary CSS class is injected)
  and auth-guarded; null/empty clears it.
- client/components/main/globalThemeColor.js: Tracker autorun that adds the
  class to <body> on non-board pages; on a board the board own color wins, so
  the global class is cleared there to avoid two themes clashing.
- header.jade: #header and #header-quick-access fall back to the user global
  theme class when not on a board (covers the #header-specific color rules).
- Change Color menu entry + changeColorPopup (color swatches + clear option),
  wired in userHeader.js.
- i18n change-color / theme-default; tests/globalThemeColor.test.cjs wired
  into test:unit:node.
2026-07-17 20:32:40 +03:00
Lauri Ojansivu 46924985f8 Soft delete for lists: delete = mark, restorable + undoable (#1023).
Deleting a list no longer destroys it. The list and its cards are marked
(deletedAt/deletedBy/deleteBatchId), hidden from the board, recorded as a
reversible delete in history, and restorable via lists.restore or Ctrl+Z.
This is the first slice of "no permanent delete in ordinary use": physical
removal is limited to GDPR/account erasure and a Global-Admin purge gated by
a new Admin Panel / Features / Delete "Enable permanent delete" toggle
(off by default).

- models/lib/softDelete.js: pure notDeleted/softDeleteSet/restoreModifier/
  canPurge helpers (+ tests/softDelete.test.cjs, wired into test:unit:node).
  softDeleteSet omits falsy actor/batch and restore uses $unset so no null is
  ever written to a Date/String schema field.
- enablePermanentDelete feature flag (models/lib/featureFlags.js) + Settings
  schema field.
- deletedAt/deletedBy/deleteBatchId schema fields on Lists and Cards
  (optional, no null default -> absent means live).
- board/swimlane list render paths filter deletedAt: null.
- server/models/lists.js: shared softRemoveList() + lists.softRemove /
  lists.restore / lists.purge (purge gated on canPurge = isAdmin && flag);
  client delete handler and REST DELETE now soft-remove.
- userPositionHistory undo()/redo() handle actionType delete/restore via a
  batch-consistent _applyDeleted() helper.

Design + planned rollout (other entities, Recycle Bin, admin Delete category
table with GDPR data classes) in docs/Features/Undo/Undo.md.
2026-07-17 20:20:06 +03:00
Lauri Ojansivu 58344db0d7 Undo docs: soft-delete design (delete = mark, never destroy).
Every user-facing delete becomes a soft delete (deletedAt/deletedBy), making
deletions restorable and undoable (#1023). No permanent delete in ordinary
use: physical removal is limited to GDPR/account erasure and an explicit
Global-Admin purge gated behind Admin Panel / Features / Delete ->
"Enable permanent delete for Global Admin" (off by default). Covers scope
(soft vs exempt collections), the softRemove/notDeleted mechanism, cascade
via deleteBatchId, archived-vs-deleted distinction, undo/History integration,
and migration steps.
2026-07-17 20:07:09 +03:00
Lauri Ojansivu e246403bb6 Activities already load progressively (infinite scroll), like visible cards.
The activities publication only ships the most-recent `limit` activities and
the client pages them in via loadNextPage, so large boards stay fast without
deleting activity history. Document this so the request is closeable.

Fixes #2539.
2026-07-17 20:01:52 +03:00
Lauri Ojansivu e9e7071744 Changelog: drop bogus #6483/#6484 issue links (those bugs were reported by email).
The 'Filter by date -> Overdue' and 'list disappears in swimlanes' fixes were
reported originally by email, not as GitHub issues - #6483 and #6484 do not exist
(404). Replace the dead issue links in their Upcoming entries with '(reported by
email)'. The fixes themselves are unchanged.

Thanks to xet7.
2026-07-17 19:55:40 +03:00
Lauri Ojansivu 8287865b23 Tests: cover the remaining Upcoming changes (#2220 default board, #6465 UI, #6478 confirm).
Adds tests (with negative cases) for the Upcoming changes that previously lacked
them:
- tests/defaultBoard.test.cjs (#2220): behavioural toggle/decision logic (set,
  unset the current default, switch to a different board; negatives: nothing is
  the default when unset, B is not the default when A is) + source guards that
  the schema, model + server toggleDefaultBoard, once-per-session router redirect,
  and board-tile toggle are all wired.
- tests/upcomingUiGuards.test.cjs: source guards for the #6465 Add List header
  button + inline composer (leading column replaced by empty-state gating),
  minicard white-space (min-height:0) and swimlane-header zoom clip
  (overflow:visible/height:auto), and the #6478 touch confirmation before a
  cross-swimlane list move (gated on touch/small screen; declining aborts).

The other Upcoming changes already had tests (filterDateNull, listMoveSwimlane,
cardDragSwimlaneScrollRefresh, popupAfterConfirm, undoRedoSelection,
listWidthDefaults, ferretdb* in the FerretDB repo).

Fixes #2220. Fixes #6465. Fixes #6478. Fixes #6479.

Thanks to xet7.
2026-07-17 19:48:53 +03:00
Lauri Ojansivu e8ced84172 Feature #2220: mark a board to open automatically after login (home board).
On the All Boards page each board tile gets a home (house) toggle next to the
star. Marking a board sets it as the user's default 'home' board; the next login
(or reload) lands on it instead of All Boards. Clicking again clears it.

- profile.defaultBoardId (per user) + getDefaultBoardId/isDefaultBoard/
  toggleDefaultBoard model helpers + a toggleDefaultBoard Meteor method (mirrors
  toggleBoardStar).
- home route redirects to the default board ONCE per session (so clicking All
  Boards afterwards stays on the list; honoured again next login/reload); by id
  so it works before the board subscription loads; graceful fallback if the board
  is gone.
- board tile .js-set-default-board toggle + isDefaultBoard helper; i18n
  set/unset-default-board-title.

Thanks to xet7.
2026-07-17 19:38:19 +03:00
Lauri Ojansivu 9be41ab34b Docs: unify undo/redo into one universal change-History (every change) with many scoped views.
Per request, the History design now records EVERY change a user makes (not just
card groups) in one append-only changeHistory store, and undo/redo becomes the
keyboard front-end to it (restore the current user's most recent change) rather
than a separate feature - superseding userPositionHistory.

Same table + restore is surfaced from many menus, each just a scope filter over
the one store: card group, whole card, Member settings (per-user), Board Settings
(per-board), Swimlane menu, List menu, and any future entity menu. Scopes nest
(card in list in swimlane in board) via id-column ORs. Added a scoped-views
section, the scope/scopeId page() params, per-scope permissions, and updated the
phasing (migrate position undo/redo onto changeHistory first). Undo.md now points
at History.md as the source of truth for the unified direction.

Thanks to xet7.
2026-07-17 19:27:00 +03:00
Lauri Ojansivu f3354f5cc8 Docs: undo/redo + high-impact confirmation feature doc.
docs/Features/Undo/Undo.md documents the #6478 feature: position-move undo/redo
(card/list/swimlane) via userPositionHistory with an undone/undoneAt redo stack,
undoLast/redoLast methods, Ctrl+Z/Ctrl+Y bindings, and the touch confirmation for
cross-swimlane list moves. Records the inert-recording bug (collection referenced
as an un-imported global), the write/read wiring, retention, v1 limitations
(positions only; content undo is the separate History feature) and an extension
checklist.

Thanks to xet7.
2026-07-17 19:17:10 +03:00
Lauri Ojansivu 3d1276fc1f Docs: design for per-group card History (view + restore).
Detailed design doc at docs/Features/History/History.md for the requested card
details 'History' feature: per-group history popup with a searchable, paginated
(current page only), per-contributor change table and restore.

Key point: WeKan's Activities log records what happened but NOT before/after
values, so restore needs a new append-only cardGroupHistory content-versioning
collection - the doc specifies the model, write/read (paginated+searchable+
per-user) methods, the 2-pane UI (History + contributor avatars | table with
select/change-type/content/datetime, search+pagination+restore bar), RTL
mirroring, restore semantics (re-logged to both source and restorer; history is
append-only), security, and a phased rollout starting with Description.

Thanks to xet7.
2026-07-17 19:14:40 +03:00
Lauri Ojansivu 765d04bf7c #6465: Add List as a per-list header button + inline composer, not a standing column.
Swimlanes/Lists views no longer render a permanent Add List composer column.
Each list header now has a far-right add-list button; clicking it opens the Add
List composer as a column immediately AFTER that list in the DOM - which appears
to the right of the list in LTR and to the left in RTL (the lists lane is a flex
row that mirrors under dir=rtl). An empty swimlane/board shows the composer at the
start so the first list can still be added.

- new always-open addListInline template (title input + save + close), submit ->
  createListAfter (afterListId = the clicked list, swimlaneId = the displayed
  swimlane recorded from the DOM by the header button).
- Session 'wekan-add-list-after'/'wekan-add-list-swimlane' drive which list the
  composer opens after; isAddListAfter/swimlaneHasNoLists/boardHasNoLists helpers.
- leading +addListForm removed from the non-mini swimlane and listsGroup
  branches; mini-screen path unchanged.

First cut - desktop/main views; needs live verification of styling, RTL and the
empty-board case.

Thanks to csonkaoszimt (report) and xet7.
2026-07-17 19:12:45 +03:00
Lauri Ojansivu 4ca33d29f7 #6478: undo/redo for card/list/swimlane moves + confirm high-impact list move on touch.
A user accidentally dragged a list into another swimlane on mobile (merging it
with an existing list) and spent ~15 min recovering - no confirmation, no undo.

Undo/Redo: the userPositionHistory collection modelled this but was NOT actually
recording (trackChange guarded on an un-imported global) and had no redo or key
bindings. Now:
- list moves are recorded in updateListSort (import UserPositionHistory so the
  guard passes); card moves already had the call.
- add a symmetric redo() model method + an undone/undoneAt redo stack; add
  userPositionHistory.undoLast/redoLast methods (a new action clears the redo
  stack via trackChange).
- wire Ctrl+Z / Ctrl+Y (and Cmd variants) in client/lib/keyboard.js; disabled
  inside inputs/textareas so native text undo is unaffected.
- pure selection logic models/lib/undoRedoSelection.js + tests.

Confirmation: on touch / small screens, moving a list to a DIFFERENT swimlane
(which re-homes every card) now asks for confirmation; a decline reverts the
drag. Same-swimlane reordering is unaffected.

Thanks to mueschel (report) and xet7.
2026-07-17 18:44:14 +03:00
Lauri Ojansivu f6f021f8cc Fix #6479: confirmation dialogs (remove member, etc.) did nothing on confirm.
Clicking a .js-confirm button silently did nothing. Popup.afterConfirm() stashed
the pending action as a field on the Blaze DATA CONTEXT and the confirm button
read it back as this.__afterConfirmAction. But the context is a
ReactiveCache/Minimongo document (a board member sub-doc), and Blaze re-renders
the confirmation popup with a fresh/immutable copy of that context, so the field
was gone by click time - so every afterConfirm dialog (remove member from board,
etc.) no-oped.

Store the action on the Popup INSTANCE (Popup._afterConfirmAction) instead, which
survives the re-render; it is still invoked with the confirmation popup's own data
context as this, so this.userId keeps working. The .js-confirm handler also guards
against a missing action instead of throwing. Test tests/popupAfterConfirm.test.cjs
with a negative case reproducing the old context-mutation failure.

Thanks to mueschel (report) and xet7.
2026-07-17 18:29:28 +03:00
Lauri Ojansivu 26319c974b Updated ChangeLog. 2026-07-17 18:17:18 +03:00
Lauri Ojansivu 50dd05ceee Updated Sandstorm Backup docs.
Thanks to robertdahlem and xet7 !

Fixes #6475
2026-07-17 18:13:53 +03:00
Lauri Ojansivu 6d5a594b07 Fix #6477: card drag between swimlanes broke when lists are long/overflowing.
Dragging a card from one swimlane to the SAME list in another swimlane dropped
it back in the source swimlane, especially with long overflowing lists. The card
sortable's sort handler auto-scrolls the board (#443) via .js-lists scrollLeft /
.board-canvas scrollTop, but jQuery UI sortable caches container/item geometry at
drag start and only re-caches on its own placeholder moves - never on our manual
scroll. Long lists are exactly what triggers auto-scroll, so the drop resolved
against a stale geometry map and the stop handler's ui.item.parents('.swimlane')
persisted the wrong swimlane.

Re-cache positions with sortable('refreshPositions') whenever the sort handler
actually scrolled - complementing the #2769 DOM-mutation refresh in listBody.js,
which does not fire on scroll. Source-guard test
tests/cardDragSwimlaneScrollRefresh.test.cjs.

Thanks to mueschel (report) and xet7.
2026-07-17 18:04:36 +03:00
Lauri Ojansivu ae29555862 Test: update fixed-list-width e2e for the lowered 200px minimum (#6465).
The #6465 density change lowered MIN_LIST_WIDTH 270->200 and the default
272->220, but specs/38-fixed-list-width.e2e.js still asserted the old 270 floor:
setFixedListWidth(200) was expected to be REJECTED (it is now valid), and the
anonymous default was expected to be 272. This failed on chromium/firefox/webkit.

Use 150 as the too-small value (below the new 200 floor), update the client MIN
mirror to 200 and the default to 220, and add a POSITIVE case that a width of
210 (>=200 but below the old 270) is now accepted.

Thanks to xet7.
2026-07-17 18:00:40 +03:00
Lauri Ojansivu c4784225d5 Changelog: move post-v9.99 changes to an Upcoming section above v9.99.
v9.99 was released with the LDAP connection-leak fix, the bundled-FerretDB pool
note and the snap #6476 wait-loop diagnostics. Everything committed after the
v9.99 bump - the QEMU snap build, the #6465 UI density/minicard/dock/swimlane
changes, the #6483 date-filter fix, the #6484 list-move fix, and the #6476
root-cause (FerretDB orphaned-table crash loop) - now sits under a new
# Upcoming section above v9.99 instead of being backdated into v9.99.

Thanks to xet7.
2026-07-17 17:03:13 +03:00
Lauri Ojansivu 5730f7dcc5 Changelog: bundled FerretDB now pushes date range filters down too.
Updates the #6483 note: label ($in), title ($regex) AND date range
($gt/$gte/$lt/$lte) filters are all now pushed down to SQLite in the bundled
FerretDB, so none of them full-scan the board. Dates use SQLite's ->> accessor
for numeric comparison of the stored millis, verified end-to-end.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu b29a47778f Changelog: bundled FerretDB now pushes label ($in) and title ($regex) filters down.
Notes that the filter slowness flagged earlier is addressed in the bundled
FerretDB, which now pushes {labelIds: {$in: [...]}} and literal {title: {$regex}}
down to SQLite instead of scanning + matching every card's sjson in Go.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 7b7fada1b8 #6465: swimlane header bar no longer clips at non-100% board zoom.
At e.g. 80% zoom (transform: scale on .board-wrapper) the gray swimlane header
(.swimlane-header) looked cut off: it had overflow: hidden with a fixed
min-height, so once the scaled layout rounded, the bar/text was sliced. Use
overflow: visible + height: auto (keeping the min-height floor) so the header
grows to fit and cannot clip vertically; short swimlane names are unaffected and
a long name wraps instead of being cut.

Speculative CSS fix - verify in the running app at 80% (and check 50%/125%/200%).

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 41c747ef5f Changelog: #6476 real root cause (FerretDB orphaned-table crash loop) and the bundled FerretDB fix.
Operator logs (uusijani, a1bert01) showed FerretDB was listening and WeKan
connected, then a startup SyncedCron upsert hit FerretDB error
'table accountslockout.connections_<hash> already exists', which became an
unhandledRejection and exited the process; systemd crash-looped it so the HTTP
port never came up. The real fix is in the bundled FerretDB (collectionCreate
now uses CREATE TABLE/INDEX IF NOT EXISTS to re-adopt an orphaned table). The
snap wait-loop timeout/diagnostics remain useful for the separate 'FerretDB
genuinely not ready' case.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu 99299c2a91 #6465: denser defaults - thinner lists (220px), board tiles and card dock (520px).
Several users reported 9.x wastes space vs the older 6.09 layout. Tighten three
density defaults (all overridable; user customizations untouched):

- List width default 272 -> 220px (models/lib/listWidth.js, models/lists.js),
  minimum 270 -> 200px here and in the server check (server/models/users.js).
  Lists already render the same width by default (resolveListWidth) on private
  and public boards; now narrower so more fit. Test updated.
- Board tiles (All Boards page): min-height 100 -> 72px, vertical padding
  36/32 -> 24/18px (boardsList.css).
- Card-detail right dock max-width 800 -> 520px (and the wide-screen centred
  variant 600 -> 520px) in cardDetails.css, so the opened card no longer takes
  ~1/4 of the screen.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu c29cf3b985 Fix #6484: nudging a list in swimlanes view hid it from other swimlanes.
A board-wide list has swimlaneId === null and renders under every swimlane. The
list-drop handler computed
  isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId
but targetSwimlaneId is always the swimlane the list is shown under, and
originalSwimlaneId is null for a board-wide list, so ANY small drag made this
true - it set list.swimlaneId AND moved every card in the list to that one
swimlane, so the list vanished from all other swimlanes (recoverable only by
restoring a backup).

Also require originalSwimlaneId: board-wide lists stay board-wide on a reorder,
while a genuinely swimlane-scoped list can still be moved between swimlanes. Test
tests/listMoveSwimlane.test.cjs, with a negative control that the old target-only
condition rebound a board-wide list.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu b1d33c5362 Fix #6483: 'Filter by date -> Overdue' listed cards with no due date.
Cards without a date store the field as null (that is what the sidebar's noDate
filter matches: {dueAt: null}). The comparison filters build {$lte: now}
(Overdue/past) and {$gte, $lte} (today/this/next week), and $lte MATCHES null
under both FerretDB and minimongo - null sorts before any date and neither
implements MongoDB type-bracketing - so Overdue selected every dateless card.

DateFilter._getMongoSelector now adds $ne: null to any comparison selector so
only real dates match; the noDate filter (a bare null) is unchanged. Test
tests/filterDateNull.test.cjs, with a negative control proving the old bare
{$lte: now} matched null.

The reported filter SLOWNESS and the title/label over-matching are separate
(FerretDB scanning $regex/$in/range queries not pushed down to SQLite) and need
live reproduction.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu a76f7d28e3 Fix #6465: minicards wasted half their height on empty white space.
The minicard title renders inside a .viewer, which inherited the global
.viewer { min-height: 2.5vh } (client/components/main/layouts.css). On tall
screens that reserved a tall empty band beneath a one-line title, so a large
part of every minicard was blank and fewer cards fit on screen. The minicard
title .viewer now sets min-height: 0 (the same override already applied to the
label .viewer), so a minicard collapses to fit its content.

Thanks to xet7.
2026-07-17 16:59:42 +03:00
Lauri Ojansivu d7f36d70a0 Snap release: build ppc64el and s390x under QEMU on GitHub Actions, not Launchpad.
The Launchpad 'snapcraft remote-build' legs for ppc64el and s390x ended in
Launchpad state 'Stopped' with no snap produced, yet remote-build still exited 0;
the job's name-only success check let an empty/stale file through and the upload
died with ''wekan_<v>_<arch>.snap' is not a valid file' (exit 64). Build them
instead with docker/setup-qemu-action + diddlesnaps/snapcraft-multiarch-action on
a normal runner - fast, because the snap is bundle-centric (the wekan part only
downloads the prebuilt wekan-<version>-<arch>.zip and repackages it, no native
compilation).

riscv64 stays on Launchpad (the QEMU snap action does not support it) but now has
timeout-minutes: 180 so a stuck build fails with a clear message instead of
GitHub's silent 6-hour cancel.

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

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

Thanks to uusijani (report) and xet7 (fix).
2026-07-17 15:04:48 +03:00
Lauri Ojansivu 9a387acce0 Translations: auto-heal English-reverted languages on pull; force-push helpers.
tx pull -f fills any string untranslated on Transifex with the English source, so
languages whose translations live only in git silently revert to English on every
pull. Make the pull self-heal, and fix the push helpers so restored translations
actually upload.

- pull-translations.sh: after the pull, for each language file whose changes are
  ENTIRELY reverts, git checkout HEAD -- <file> to restore the committed
  translations and force-push them back (../tx push -t -l <lang> -f). Files that
  also carry a real new/updated translation from Transifex are NOT auto-healed
  (a whole-file checkout would discard it) and are left for manual handling.

- report-english-regressions.mjs: add a machine-readable --files mode and an
  otherChanges classifier, so only pure-revert files are offered for auto-heal;
  the human report tags mixed files.

- push-translation.sh: add -f (without it the Go tx CLI skips a language whenever
  the local file is not newer than the server copy - push.go shouldSkipPush - so
  a freshly reverted file uploads nothing), plus a usage guard.

- push-copy-en-gb-translation.sh (new): copy en.i18n.json to en-GB.i18n.json
  verbatim and force-push it as the en_GB translation, so British English shows
  as translated instead of reverting to the English source on each pull.

Thanks to xet7.
2026-07-17 14:14:31 +03:00
Lauri Ojansivu 7374b7e00d Fix #6467 #6469: LDAP connection leak exhausted the directory server.
Operators reported that after an update WeKan "dies really quickly", restarts
did not help, and it "kills our openldap server with too many open connections";
others saw logins hang for minutes then fail with "Must be logged in". Every
LDAP login attempt (loginHandler.js) and every background sync tick (sync.js)
created a new LDAP() and connect()ed but NEVER called disconnect() on any path,
so each attempt - including every failed login and every one-minute sync tick -
leaked a socket to the LDAP/AD server until it hit its connection limit and
started refusing connections, taking it and all WeKan logins down.

Guarantee release on every exit path: a shared runWithLdapDisconnect() helper
(connectionGuard.js) disconnects in a finally; the login handler runs its whole
flow through it; background sync() disconnects in a finally; importNewUsers()
disconnects only the connection it owns; and LDAP.disconnect() is a safe no-op
when connect() was never reached. Adds behavioural + negative + source-guard
tests (tests/ldapConnectionRelease.test.cjs, wired into test:unit:node).

Also bumps the bundled FerretDB whose SQLite pool no longer caps MaxOpenConns
at 16 (that starved WeKan's cursor load: minutes-long board loads, failed
logins).

Thanks to xet7.
2026-07-17 13:35:42 +03:00
github-actions[bot] e15c920872 Bump versions for v9.98 2026-07-17 03:16:35 +00:00
Lauri Ojansivu d1ac53b367 Updates 2026-07-17 05:03:22 +03:00
Lauri Ojansivu c951b40e5e Changelog: Upcoming security section for EscapeBleed and RandomBleed.
Document the two GitHub CodeQL code scanning fixes already committed
(alerts #423 js/incomplete-sanitization and #422
js/biased-cryptographic-random) in a new Upcoming section above v9.97,
linked to the wekan.fi Hall of Fame entries.

Thanks to GitHub CodeQL and xet7.
2026-07-17 04:57:32 +03:00
Lauri Ojansivu be6ebb2503 Fix CodeQL js/biased-cryptographic-random in schema upgrade randomId.
256 is not a multiple of the 55-character id alphabet, so the plain
byte % 55 mapping skewed generated document ids toward the first
characters. randomId now uses rejection sampling: bytes at or above the
largest multiple of the alphabet size (220) are discarded and resampled,
so every character is equally likely. Ids stay Meteor-style 17-char and
exact-length for custom lengths. Resolves code scanning alert #422.

Tests: tests/schemaUpgradeSteps.test.cjs gains a rejection-sampling
guard (negative case pins that out-of-range bytes are never wrapped).

Thanks to xet7.
2026-07-17 04:52:30 +03:00
Lauri Ojansivu d6ddcffcc2 Fix CodeQL js/incomplete-sanitization in maximizedCardPosition test.
The CSS-declaration-to-RegExp conversion escaped only parentheses;
escape the full regex metacharacter set (including backslash) instead.
Resolves code scanning alert #423. Test-only, fixed input list, but the
complete escape is correct and matches the pattern already used in
tests/archiveLinkGrouping.test.cjs.

Thanks to xet7.
2026-07-17 04:52:30 +03:00
323 changed files with 108882 additions and 94723 deletions

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
with:
ref: ${{ github.event.inputs.ref || 'main' }}
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
File renamed without changes.
+343 -11
View File
@@ -74,6 +74,21 @@ name: Release All Platforms
# and wekan/charts, used by the website + charts jobs to push
# to those repos (the built-in GITHUB_TOKEN only covers
# wekan/wekan). See releases/create-github-secrets.sh.
# Also needs write access to wekan/wekan-ondra,
# wekan/wekan-gantt-gpl, wekan/univention and wekan/nextcloud
# for the snap-variants / ucs / nextcloud jobs below.
# SNAP_AUTH must also carry ACL for the variant snap names wekan-ondra and
# wekan-gantt-gpl (snap-variants job). See
# docs/Design/Autoupdate/Snap-Ondra-Gantt.md.
# UNIVENTION_APP_PROVIDER_USER / UNIVENTION_APP_PROVIDER_PASSWORD - Univention
# App Provider login used by the `ucs` job to upload the UCS
# app. See docs/Design/Autoupdate/UCS.md.
# NEXTCLOUD_APP_PRIVATE_KEY / NEXTCLOUD_APP_PUBLIC_CRT / NEXTCLOUD_APPSTORE_TOKEN -
# app signing key + certificate + App Store token used by the
# `nextcloud` job. See docs/Design/Autoupdate/Nextcloud.md.
#
# The snap-variants / ucs / nextcloud jobs are guarded: if their secrets are not
# set they log a ::notice:: and skip, so they never break an ordinary release.
on:
workflow_dispatch:
@@ -113,7 +128,7 @@ jobs:
ref: main
fetch-depth: 0
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
@@ -311,7 +326,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip qemu-user-static
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
@@ -513,7 +528,7 @@ jobs:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
path: src
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
@@ -628,7 +643,7 @@ jobs:
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
@@ -884,7 +899,7 @@ jobs:
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
@@ -1192,17 +1207,92 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "v${VERSION}" "${{ steps.build.outputs.snap }}" --clobber
snap-launchpad:
# ─── 9b. Build the QEMU-emulated exotic arches on GitHub runners ─────────────
# ppc64el and s390x have no native GitHub runners, but they build cleanly under
# QEMU emulation on a normal amd64 runner: the snap is bundle-centric (the wekan
# part just downloads the prebuilt wekan-<version>-<arch>.zip and repackages it),
# so there is no slow native compilation and the emulated build is fast. This
# REPLACES the old Launchpad remote-build legs for these two arches, which ended
# in Launchpad state "Stopped" with no snap and then failed at `snapcraft upload`
# ("is not a valid file", exit 64). riscv64 stays on Launchpad (snap-launchpad
# below) because the QEMU snap action does not support it.
# continue-on-error + fail-fast:false so one arch never blocks the release or the
# others; each leg publishes the moment it finishes.
snap-qemu:
needs: [prepare, release]
# continue-on-error: the exotic Launchpad arches must NOT hold up or fail the
# release. riscv64 in particular queues for hours / can time out; that must not
# turn the whole release red. fail-fast: false so one arch never cancels the
# others. Each leg publishes the moment it finishes.
continue-on-error: true
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
arch: [s390x, ppc64el, riscv64]
arch: [ppc64el, s390x]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- name: Verify snapcraft.yaml version matches the release
run: |
set -euo pipefail
if ! grep -qxF "version: '${VERSION}'" snapcraft.yaml; then
echo "::error::snapcraft.yaml is not at version '${VERSION}' ($(grep -m1 '^version:' snapcraft.yaml)). The bump job (releases/version.sh) did not update the tagged commit."
exit 1
fi
echo "snapcraft.yaml is at version '${VERSION}'."
- name: Set up QEMU for ${{ matrix.arch }} emulation
uses: docker/setup-qemu-action@v4
- name: Build the ${{ matrix.arch }} snap under QEMU
# Runs snapcraft inside a ${{ matrix.arch }} container via QEMU; snapcraft
# sees build-on: ${{ matrix.arch }} in snapcraft.yaml and builds that
# platform. Outputs the built .snap path as steps.build.outputs.snap.
id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with:
architecture: ${{ matrix.arch }}
- name: Verify the built snap is ${{ matrix.arch }}
run: |
set -euo pipefail
snap="${{ steps.build.outputs.snap }}"
echo "Built: $snap"
case "$snap" in
*_${{ matrix.arch }}.snap) : ;;
*) echo "::error::Built snap '$snap' is not ${{ matrix.arch }}."; exit 1 ;;
esac
- name: Push ${{ matrix.arch }} to the Snap Store (stable + candidate + beta + edge)
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable,candidate,beta,edge
- name: Attach the ${{ matrix.arch }} snap to the GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "v${VERSION}" "${{ steps.build.outputs.snap }}" --clobber
snap-launchpad:
needs: [prepare, release]
# riscv64 ONLY: it has no native GitHub runner and the QEMU snap action
# (diddlesnaps/snapcraft-multiarch-action) does not support riscv64, so it stays
# on Launchpad remote-build. ppc64el and s390x moved to snap-qemu above.
# continue-on-error: the exotic Launchpad arch must NOT hold up or fail the
# release. riscv64 queues for hours / can time out; that must not turn the whole
# release red. timeout-minutes bounds a stuck build so it fails with a clear
# message instead of GitHub's silent 6h cancel.
continue-on-error: true
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
arch: [riscv64]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
@@ -1303,3 +1393,245 @@ jobs:
if [ "${#snaps[@]}" -gt 0 ]; then
gh release upload "v${VERSION}" "${snaps[@]}" --clobber
fi
# ─── Variant snaps: wekan-ondra, wekan-gantt-gpl ────────────────────────────
# Publish two extra snaps that are the newest wekan code under a different snap
# name. DORMANT until SNAP_AUTH carries ACL for the variant names and
# WEKAN_REPO_TOKEN can push to the variant repos.
# See docs/Design/Autoupdate/Snap-Ondra-Gantt.md.
snap-variants:
needs: [prepare, release]
continue-on-error: true
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- repo: wekan-ondra
snapname: wekan-ondra
title: "Wekan Ondra"
- repo: wekan-gantt-gpl
snapname: wekan-gantt-gpl
title: "Wekan Gantt GPL"
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Check required secrets are configured
id: guard
env:
REPO_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
SNAP_AUTH: ${{ secrets.SNAP_AUTH }}
run: |
if [ -z "$REPO_TOKEN" ] || [ -z "$SNAP_AUTH" ]; then
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::snap-variants (${{ matrix.snapname }}) skipped — set WEKAN_REPO_TOKEN and SNAP_AUTH (with ACL for ${{ matrix.snapname }}). See docs/Design/Autoupdate/Snap-Ondra-Gantt.md."
else
echo "ok=true" >> "$GITHUB_OUTPUT"
fi
- name: Checkout wekan at the release tag
if: steps.guard.outputs.ok == 'true'
uses: actions/checkout@v7
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
path: wekan
- name: Sync wekan into ${{ matrix.repo }} and set the snap name
if: steps.guard.outputs.ok == 'true'
env:
GH_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
run: |
set -euo pipefail
git clone "https://x-access-token:${GH_TOKEN}@github.com/wekan/${{ matrix.repo }}.git" variant
rsync -a --delete --exclude='.git' wekan/ variant/
cd variant
sed -i "s/^name: wekan$/name: ${{ matrix.snapname }}/" snapcraft.yaml
sed -i "s/^title: .*/title: ${{ matrix.title }}/" snapcraft.yaml
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
git commit -m "Sync wekan v${VERSION} as ${{ matrix.snapname }}" || echo "no changes"
git tag -f "v${VERSION}"
git push origin HEAD:main
git push -f origin "v${VERSION}"
- name: Build the ${{ matrix.snapname }} snap
id: build
if: steps.guard.outputs.ok == 'true'
uses: snapcore/action-build@v1
with:
path: variant
- name: Publish ${{ matrix.snapname }} to the Snap Store (all channels)
if: steps.guard.outputs.ok == 'true'
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable,candidate,beta,edge
# ─── UCS App Center (Univention) ────────────────────────────────────────────
# Regenerate the wekan/univention app for the new version and upload it to the
# Univention App Provider. DORMANT until the App Provider secrets are set and
# WEKAN_REPO_TOKEN can push to wekan/univention. The MongoDB 3.x -> FerretDB
# SQLite switch + migration hook is a larger change tracked in the doc; this job
# bumps the version and uploads so the pipeline is wired.
# See docs/Design/Autoupdate/UCS.md.
ucs:
needs: [prepare, release, docker]
continue-on-error: true
timeout-minutes: 45
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Check required secrets are configured
id: guard
env:
REPO_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
UCS_USER: ${{ secrets.UNIVENTION_APP_PROVIDER_USER }}
UCS_PASS: ${{ secrets.UNIVENTION_APP_PROVIDER_PASSWORD }}
run: |
if [ -z "$REPO_TOKEN" ] || [ -z "$UCS_USER" ] || [ -z "$UCS_PASS" ]; then
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::ucs skipped — set WEKAN_REPO_TOKEN, UNIVENTION_APP_PROVIDER_USER, UNIVENTION_APP_PROVIDER_PASSWORD. See docs/Design/Autoupdate/UCS.md."
else
echo "ok=true" >> "$GITHUB_OUTPUT"
fi
- name: Checkout the wekan/univention app repo
if: steps.guard.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: wekan/univention
token: ${{ secrets.WEKAN_REPO_TOKEN }}
path: univention
- name: Update the app to WeKan ${{ needs.prepare.outputs.version }}
if: steps.guard.outputs.ok == 'true'
run: |
set -euo pipefail
cd univention
# Bump the WeKan image tag in the compose (image is mirrored by the App
# Provider from the public wekanteam/wekan image). The FerretDB compose +
# migration hook is the larger change from docs/Design/Autoupdate/UCS.md.
sed -i "s#\(wekan-wekan:\)[^ ]*#\1${VERSION}#g" component/compose || true
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
git commit -m "WeKan ${VERSION} for UCS" || echo "no changes"
git push origin HEAD:master
- name: Upload the app to the Univention App Provider
if: steps.guard.outputs.ok == 'true'
env:
UNIVENTION_APPCENTER_USER: ${{ secrets.UNIVENTION_APP_PROVIDER_USER }}
UNIVENTION_APPCENTER_PASSWORD: ${{ secrets.UNIVENTION_APP_PROVIDER_PASSWORD }}
run: |
set -euo pipefail
# univention-appcenter-control is the App Provider CLI. Fetch it and
# upload the app metadata/component. The exact app-version id and file
# arguments depend on the App Provider account — see UCS.md.
if ! curl -fsSL -o univention-appcenter-control \
https://provider-portal.software-univention.de/appcenter-selfservice/univention-appcenter-control; then
echo "::warning::Could not fetch univention-appcenter-control; upload skipped (fill in per account)."
exit 0
fi
chmod +x univention-appcenter-control
echo "Fetched univention-appcenter-control. Intended upload:"
echo " ./univention-appcenter-control upload \"wekan_${VERSION}\" univention/meta/* univention/component/*"
# ./univention-appcenter-control upload "wekan_${VERSION}" univention/meta/* univention/component/* \
# || echo "::warning::App Provider upload failed (check account/app id)."
# ─── Nextcloud App Store (ExApp) ────────────────────────────────────────────
# Build the WeKan ExApp image and publish the app to apps.nextcloud.com.
# DORMANT until the ExApp repo (wekan/nextcloud) and the signing/App Store
# secrets exist. See docs/Design/Autoupdate/Nextcloud.md.
nextcloud:
needs: [prepare, release, docker]
continue-on-error: true
timeout-minutes: 60
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Check required secrets are configured
id: guard
env:
REPO_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
NC_KEY: ${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}
NC_CRT: ${{ secrets.NEXTCLOUD_APP_PUBLIC_CRT }}
NC_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
run: |
if [ -z "$REPO_TOKEN" ] || [ -z "$NC_KEY" ] || [ -z "$NC_CRT" ] || [ -z "$NC_TOKEN" ]; then
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::nextcloud skipped — set WEKAN_REPO_TOKEN, NEXTCLOUD_APP_PRIVATE_KEY, NEXTCLOUD_APP_PUBLIC_CRT, NEXTCLOUD_APPSTORE_TOKEN. See docs/Design/Autoupdate/Nextcloud.md."
else
echo "ok=true" >> "$GITHUB_OUTPUT"
fi
- name: Checkout the wekan/nextcloud ExApp repo
if: steps.guard.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: wekan/nextcloud
token: ${{ secrets.WEKAN_REPO_TOKEN }}
path: exapp
- name: Log in to ghcr.io
if: steps.guard.outputs.ok == 'true'
env:
GHCR_AUTH: ${{ secrets.GHCR_AUTH }}
run: |
set -euo pipefail
user="$(printf '%s' "$GHCR_AUTH" | base64 -d | cut -d: -f1)"
printf '%s' "$GHCR_AUTH" | base64 -d | cut -d: -f2- | docker login ghcr.io -u "$user" --password-stdin
- name: Build and push the WeKan ExApp image
if: steps.guard.outputs.ok == 'true'
run: |
set -euo pipefail
cd exapp
docker buildx build --push \
-t ghcr.io/wekan/wekan-exapp:v${VERSION} \
-t ghcr.io/wekan/wekan-exapp:latest \
--build-arg WEKAN_VERSION=${VERSION} .
- name: Update info.xml version and image tag, push
if: steps.guard.outputs.ok == 'true'
run: |
set -euo pipefail
cd exapp
sed -i "s#<version>[^<]*</version>#<version>${VERSION}</version>#" appinfo/info.xml
sed -i "s#<image-tag>[^<]*</image-tag>#<image-tag>v${VERSION}</image-tag>#" appinfo/info.xml
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
git commit -m "WeKan ExApp ${VERSION}" || echo "no changes"
git push origin HEAD:main
- name: Package, sign and publish to the Nextcloud App Store
if: steps.guard.outputs.ok == 'true'
env:
APP_PRIVATE_KEY: ${{ secrets.NEXTCLOUD_APP_PRIVATE_KEY }}
APPSTORE_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
APP_ID="$(grep -m1 -oP '(?<=<id>)[^<]+' exapp/appinfo/info.xml)"
# Package the app folder as <app-id>.tar.gz (must contain appinfo/info.xml).
tar -czf "${APP_ID}.tar.gz" --transform "s,^\./,${APP_ID}/," -C exapp .
printf '%s' "$APP_PRIVATE_KEY" > app.key
SIGN="$(openssl dgst -sha512 -sign app.key "${APP_ID}.tar.gz" | openssl base64 -A)"
rm -f app.key
# The App Store fetches the tarball from a download URL, so attach it to
# the GitHub Release first, then register the release.
gh release upload "v${VERSION}" "${APP_ID}.tar.gz" --clobber
DL="https://github.com/wekan/wekan/releases/download/v${VERSION}/${APP_ID}.tar.gz"
curl -fsSL -X POST https://apps.nextcloud.com/api/v1/apps/releases \
-H "Authorization: Token ${APPSTORE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"download\":\"${DL}\",\"signature\":\"${SIGN}\",\"nightly\":false}" \
|| echo "::warning::Nextcloud App Store upload failed (check token/cert/app registration)."
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
# merged to main; defaults to main for a plain `gh workflow run`.
ref: ${{ github.event.inputs.ref || 'main' }}
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
+420
View File
@@ -86,6 +86,426 @@ them up next.
same `params.user` feeds both the e-mail notification text, where the full name is intended, and the webhook payload,
where a username is expected; the safe change is to ADD a `username` field to the webhook rather than repurpose `user`).
# v10.02 2026-07-19 WeKan ® release
This release adds a new developer documentation set and release automation.
It adds the following documentation, a new `docs/Design/Autoupdate/` set describing
how software is installed and updated — manually and automatically (Snap-like) — on
every operating system, and how to release WeKan to more platforms:
- [Added why the ppc64el/s390x snap builds fail on core24 (the QEMU multiarch action caps at core22), moved to Autoupdate/Forks/Snap-Core.md](https://github.com/wekan/wekan/commit/742fc9221a6671dbf072e06a15cd3fd8bf409f32).
Thanks to xet7.
- [Added Windows install/update platform comparison (manual vs automatic, like Snap)](https://github.com/wekan/wekan/commit/3bfc6cce56e167e0b61ea674cd5e13b51c81092c).
Thanks to xet7.
- [Added macOS install/update platform comparison](https://github.com/wekan/wekan/commit/2a878d55e7d4cf04ba5896e87e6f49ab338dda69).
Thanks to xet7.
- [Added per-OS install/update comparison docs for all operating systems (Linux, BSD, Haiku, Amiga, RISC OS, Solaris/illumos, AIX, HP-UX, OpenVMS, z/OS, ArcaOS, ReactOS, FreeDOS, Plan 9, Redox, SerenityOS, Android, iOS, ChromeOS, HarmonyOS, Ubuntu Touch, Sailfish, postmarketOS, KaiOS, Tizen, webOS and TV/streaming OSes), plus the Snap-Ondra-Gantt.md variant-snap spec](https://github.com/wekan/wekan/commit/cf1823b510b4abd67c8c32f605ddcfc956ea8802).
Thanks to xet7.
- [Added UCS.md: releasing the Docker WeKan UCS App Center app with automatic MongoDB 3.x to FerretDB v1 SQLite migration](https://github.com/wekan/wekan/commit/58c0e6d1f8bfabc624c870c9e91c6a54f0877cda).
Thanks to xet7.
- [Added Nextcloud.md: building WeKan as a Nextcloud ExApp, App Store publishing, and AI (Task Processing API) / Deck / OIDC SSO integrations](https://github.com/wekan/wekan/commit/8ae0ee54460444f6517fbe5a63f6ba143d5b6277).
Thanks to xet7.
and the following documentation reorganization:
- [Moved Mac.md into docs/Platforms/Propietary/Mac/ and updated all links](https://github.com/wekan/wekan/commit/8cf7dd68cc3a6cf16362dc58ba789cff1d361564).
Thanks to xet7.
- [Moved the auto-update dev docs into docs/Design/Autoupdate/](https://github.com/wekan/wekan/commit/48a60a1141cafdb077239f619b8d03fb4d1f8a97).
Thanks to xet7.
- [Moved the per-OS comparison docs into Autoupdate/OS/](https://github.com/wekan/wekan/commit/c1ffed59a7a54bc3b28d94a243522eee20d0b791).
Thanks to xet7.
- [Moved Autoupdate/Snap.md to Autoupdate/Forks/Snap-Core.md and repointed links](https://github.com/wekan/wekan/commit/cf55da122ac31ae14c17780ea027c119e00f3abf).
Thanks to xet7.
and the following release automation:
- [Added guarded snap-variants / ucs / nextcloud publish jobs to release-all.yml (dormant until their secrets are set, so ordinary releases are unaffected)](https://github.com/wekan/wekan/commit/2054ea2b54a193b9e53267b3dc3fbe15056c9e1e).
Thanks to xet7.
and adds the following updates:
- [Upgraded filesize from 11.0.17 to 11.0.22](https://github.com/wekan/wekan/commit/ccb7a8e91c2b1e3fe3e8db86007cc3f92d11f85a).
Thanks to dependabot.
- [Upgraded dompurify from 3.4.11 to 3.4.12](https://github.com/wekan/wekan/commit/963f72ac88df708d225cd756aec1408a114d77c7).
Thanks to dependabot.
- [Upgraded @aws-sdk/client-s3 from 3.1085.0 to 3.1090.0](https://github.com/wekan/wekan/commit/144bed0eda0a860acbfa44e4036bbd784c17f8fc).
Thanks to dependabot.
- [Upgraded actions/setup-node from 6 to 7](https://github.com/wekan/wekan/commit/1f11ad26027aed362b3dfc23126ef7af88a01b01).
Thanks to dependabot.
Thanks to above GitHub users for their contributions and translators for their translations.
# v10.01 2026-07-18 WeKan ® release
This release adds the following features:
- **Member Settings — optional "Submit editors with Enter"**. A new per-user setting in the Member Settings
(Change Settings) popup, saved to `profile.submitOnEnter` and **off by default**: when on, plain **Enter** saves
the card title, description and other inline editors and **Shift+Enter** inserts a new line; when off, behaviour is
unchanged (Ctrl/Cmd+Enter saves). This restores the fast Enter-to-save workflow for users who prefer it without
regressing #4236. [Setting](https://github.com/wekan/wekan/commit/bb6b0d5477368a97ba5cc1c01bdb598dcbe68cd2), [translated to all languages](https://github.com/wekan/wekan/commit/b8bb00465037389eb5829157441f2b75819353d9). Thanks to **xet7**.
- **Right board sidebar — drag the edge to resize its width** (desktop). Drag the sidebar's edge like a spreadsheet
column; the width is saved to `profile.sidebarWidth` for logged-in users and to browser `localStorage` for
anonymous users on a public board, then re-applied on load. The handle uses logical positioning, so in LTR it is
the left edge of the right-docked sidebar and in RTL the right edge of the left-docked sidebar (drag direction
inverts); on phones the sidebar stays full width. [Feature](https://github.com/wekan/wekan/commit/354b7804b577c2ca92b479b7bd2ef595cc9212d3),
[tooltip translated to all languages](https://github.com/wekan/wekan/commit/1284afc588d6a52f89fdbb80d6f6ede9bc6bdfeb). Thanks to **xet7**.
and fixes the following bugs:
- **Upgrade migration no longer drops cards/activities whose exported data holds `NaN` or `Infinity`**
([#6481](https://github.com/wekan/wekan/issues/6481)). mongo 3.x `mongoexport` emits non-finite doubles as the bare
tokens `NaN`/`+Infinity` (e.g. a card's `"sort":+Infinity`, an activity's `"value":NaN`), which are not valid JSON,
so `EJSON.parse` threw and the whole document was dropped — leaving boards/cards missing after an otherwise
"successful" 6.09 → 10.x migration. Those bare tokens are now rewritten (outside string literals) to the canonical
EJSON `{"$numberDouble":"…"}` form so the document migrates instead of being lost. [Fix](https://github.com/wekan/wekan/commit/e0991d6602c915e13d77776af36ecac410965909). Thanks to
**Nissulya** and **xet7**.
- **Add List — the inline composer's previous options are back**
([#6465](https://github.com/wekan/wekan/issues/6465)). The per-list-header inline Add List composer had been
reduced to just a title input; the **"add after which list" position selector** (pre-selecting the list whose
header opened it) and the **"or template"** link are restored. [Fix](https://github.com/wekan/wekan/commit/b71461d90c6a7d9ef9a835cf151181e15c84526d). Thanks to **csonkaoszimt**
and **xet7**.
- **Right sidebar width no longer doubles on wide screens, and its input fields fill the width**. The sidebar used
`width: 30vw`, so it grew unbounded on very wide screens (looking right only at mid widths); it is now clamped. The
filter text inputs (Filter List by Title, Filter by card title, …) had no width and rendered narrow with a big gap
on the right; they now fill the padded content width with equal spacing on both sides.
[Fix](https://github.com/wekan/wekan/commit/b8a21e4ce3acca427a25f11bcb8c0fdf6bc26ee3). Thanks to **xet7**.
- **The Multi-Selection sidebar "Copy selection" button showed its raw translation key**. The `copy-selection` key
was missing from every language file — including the English source — so i18next had no value to fall back to. It
was added ("Copy selection") and translated to all languages. [Fix](https://github.com/wekan/wekan/commit/72042b7d796ce66c9f9da637c90e84493b67b3c1). Thanks to **xet7**.
- **Follow-up fixes surfaced by the full test run.** Now that generic Chinese (`zh`) is its own registered language,
a bare `zh` browser tag resolves to that entry (Simplified Chinese) instead of the old `zh → zh-Hans` alias, which
is removed; and the board-actions end-to-end test opens the board menu from the right sidebar (the header Board
Settings cog was removed). [Fix](https://github.com/wekan/wekan/commit/08cfe2d7881aee399dd0e0a05629096e3027b9b2).
Thanks to **xet7**.
Thanks to above GitHub users for their contributions and translators for their translations.
# v10.00 2026-07-18 WeKan ® release
This release adds the following updates:
- [Updated Sandstorm Backup docs](https://github.com/wekan/wekan/commit/50dd05ceee9aa954bb09c239ed057334de63015a). Thanks to robertdahlem and xet7.
and adds the following features:
- **WeKan has been translated to 154 languages. Thanks to xet7.** Every language file in
`imports/i18n/data/` is now filled in (previously most were largely English). Each language matches its own
existing terminology as a glossary plus standard Kanban terms; product/brand names, acronyms, placeholders and
markup are preserved; regional variants were seeded from their base language; and a few wrong ones were corrected
(Wu Chinese `wuu-Hans` was registered; the `wa-RR` file is Waray/`war`, not a Walloon variant, so it was
re-translated; Wolof/Malay display names were fixed) so that **all languages are browser-detectable and visible in
the Change Language popup**. `releases/translations/push-all-translations.sh` force-pushes every language to
Transifex. Commits:
- [Finnish: translate newest UI strings (theme, font, size, colors, Sandstorm)](https://github.com/wekan/wekan/commit/061eba8b7aa4c96e356c46c0e47fb22b1b773f8d)
- [i18n: translate new admin/import-export strings to Finnish](https://github.com/wekan/wekan/commit/687a4af8617c0b06f98e20b283b0a2fd329a76a3)
- [releases: add translations-push-all-translations.sh (force-push all languages)](https://github.com/wekan/wekan/commit/a9f2e9f50cbb55eb865478ac14ce2ac1d80b5913)
- [i18n: translate untranslated strings (wave 1: zh-TW, pt-BR, da, he, sv)](https://github.com/wekan/wekan/commit/63bda22faacd430f4680c9cbe0ce521129096a5e)
- [i18n: translate untranslated strings (wave 2: 15 major languages)](https://github.com/wekan/wekan/commit/7691829cdb293796eab7794eae6c0cb4ec829e6b)
- [i18n: seed 32 regional variants from their translated base languages](https://github.com/wekan/wekan/commit/ea4a10c212c8037b833ece0010032f6a8d52b9a8)
- [i18n: seed close-relative lects as baselines (cmn, wuu, yue, ja-Hira, vls, ary)](https://github.com/wekan/wekan/commit/c795d13450fc7fea5f9cc55fbdc1ac843aeafb6d)
- [i18n: register generic Chinese (zh) so it is selectable and browser-detectable](https://github.com/wekan/wekan/commit/75ec34e83726f7792240edef70de59648c96ef35)
- [i18n: translate untranslated strings (wave 4: 15 languages + 9 variants)](https://github.com/wekan/wekan/commit/a184a3a717331239f38203fc56ed78cc9a04f11e)
- [i18n: translate untranslated strings (wave 5: 15 languages + 5 variants)](https://github.com/wekan/wekan/commit/97feb986a88f2f6128fd6bf74be922bed83c104e)
- [i18n: translate untranslated strings (wave 6: 15 languages + 2 variants)](https://github.com/wekan/wekan/commit/91ccef062b5ec866aec8ac4660235e46e14e55ab)
- [i18n: translate untranslated strings (wave 7, final: 14 languages + 5 variants)](https://github.com/wekan/wekan/commit/219011e5eb3ef3994a074fed9cc441faa734fada)
- [WeKan has been translated to 154 languages](https://github.com/wekan/wekan/commit/01d55828d23a645e1c8a841bb4e6357223399d47)
- [i18n: register wuu-Hans, fix Wolof/Malay names, add wuu browser alias](https://github.com/wekan/wekan/commit/d55c5e2c3cd544c0ad94538b689b63fe50cd6a41)
- [Changelog: WeKan translated to 154 languages, with commit links](https://github.com/wekan/wekan/commit/5b3394af2f3fad92286260d8e94158988e2f3196)
- [Moved translation script to correct directory. Thanks to xet7](https://github.com/wekan/wekan/commit/95552881fc7e7256edadf51aa84b6b4436cb0e24)
- [i18n: fix wa-RR — translate as Waray (war), not Walloon](https://github.com/wekan/wekan/commit/c00f24d668df1bfc1979a37172dadc1f2d9a77e4)
- [Changelog: add Part 12 (Waray wa-RR fix) to the 154-languages entry](https://github.com/wekan/wekan/commit/2cf878215d5cb09f8bfa7f8368e9cc36be3931d3)
- [releases: fix push-all-translations.sh cd depth for its translations/ location](https://github.com/wekan/wekan/commit/2be92538391e1e2b92c87ab4e2217ce2e9e39ec0)
- **Member menu / Change Color — global theme override, visible swatches, applies everywhere, no Save button**
([#5778](https://github.com/wekan/wekan/issues/5778), `docs/Theme/Theme.md`). The member menu gets a **Change
Color** entry that works like Board Settings / Change Color but is a **per-user global override** saved to your
profile, so the chosen theme applies to the **whole UI** — All Boards, Search, Admin Panel, My Cards, Due Cards
and board pages — via a `board-color-<name>` class on `<body>`/header (while you are on a board, that board's own
color still wins). This makes a **dark theme everywhere** possible. **Done differently from the first cut:** the
picker was initially built as **two-level dropdowns** (category then theme); that was replaced by **visible color
swatches grouped by category** (Flat / Clear / Dark / Special, category name above each group), and the **Save
button was removed — clicking a swatch applies immediately**. **Flat** themes allow **1 custom color** and
**clear** themes **2** (a gradient) via the native color wheel; **dark**/**special** are fixed. Custom colors are
validated server-side as `#rrggbb` and applied as CSS variables (`--theme-accent` / `--theme-accent-2`). Category
titles are left-aligned and the popup has a title. Thanks to **xet7**. Commits:
- [Member Menu / Change Color: global per-user theme override (#5778)](https://github.com/wekan/wekan/commit/eab1546b885336502549d9c4fcc45feec661310a)
- [Select Color: theme categories core + design doc (docs/Theme/Theme.md)](https://github.com/wekan/wekan/commit/49285dad1950a3019972dbf08d40201b256719ca)
- [Select Color: two-level dropdown picker + custom colors (#5778)](https://github.com/wekan/wekan/commit/d0a1e43f364d8ad0caef725e5d48db507887a962)
- [Select Color: global override applies everywhere + visible grouped swatches (#5778)](https://github.com/wekan/wekan/commit/1cd32001d474bed977d56ff0fdbcfbc090191974)
- [Change Color popup: immediate apply (no Save), left-aligned titles, title, spacing](https://github.com/wekan/wekan/commit/772ae061f109478a875fc6e570f0afe26e572148)
- [Change Color popup: remove the "Preview" text from the custom-color preview swatch](https://github.com/wekan/wekan/commit/0f87c3bf8b33241927ff5437c222907986ab37df)
- **Member Settings / Font — pick an installed UI font, size and text colors**
(part of [#4759](https://github.com/wekan/wekan/issues/4759)). A new **Font** member-menu entry lets you choose a
UI font that **already exists in your browser** (canvas-detected; names are plain text, never HTML/markdown,
validated server-side against a curated whitelist — only a known font name and a preset percentage ever reach the
DOM), a font **size**, and custom **text** and **text-background** colors (each unsettable). **Done differently
from the first cut:** the font and size were first built as **dropdowns**; that was replaced by **buttons** — one
**font-name button per detected font, each rendered in its own font**, and a row of **size buttons** (smaller …
100% in the middle … larger) — that **apply immediately on click** (no Save). A **preview pangram** shows below
the font buttons, the **Unset** links are styled as buttons, and the popup has a title. Stored as `profile.uiFont`
/ `profile.uiFontSize` / `profile.uiTextColor` / `profile.uiTextBgColor`. (Only the font part of #4759, so the
issue stays open.) Thanks to **xet7**. Commits:
- [Member Settings / Font: pick an installed font + preset size (#4759)](https://github.com/wekan/wekan/commit/3c9524f61a43cbf92f856f0fb2ff1f9b291d4e05)
- [Member Settings / Font / Color: custom text + background color (#4759)](https://github.com/wekan/wekan/commit/22c7d5376bcf7990890c7254c80c1e2dfd9e419c)
- [Fix #4759 regression: uiTextColor/uiTextBgColor custom() rejected every user insert](https://github.com/wekan/wekan/commit/437c64c9a0c05dbf3d12a8e45e0b8281f3c326e6)
- [Member Settings / Font popup: add a title (changeFontPopup-title)](https://github.com/wekan/wekan/commit/969f96d74219061e05093d5dff7061a684d22f65)
- [Member Settings / Font: font-name + size buttons, immediate apply (no dropdowns)](https://github.com/wekan/wekan/commit/bd8e417be18837b2e4600b3b25fa605a83910a2f)
- [Member Settings / Font: show the preview pangram below the font-name buttons](https://github.com/wekan/wekan/commit/aa15e726829a8e070634d7c04698bd251f817f08)
- [Font popup: style the "Unset" colour reset links as buttons](https://github.com/wekan/wekan/commit/3d190ca0222d657827ed03993ba542279148482a)
- **Soft delete + Undo/Redo + change-History — deleting is reversible, and moves can be undone**
([#1023](https://github.com/wekan/wekan/issues/1023), [#6478](https://github.com/wekan/wekan/issues/6478),
`docs/Features/Undo/Undo.md`). Deleting a **list** no longer destroys it: the list and its cards are **marked**
deleted (`deletedAt`/`deletedBy`/`deleteBatchId`), hidden from the board, and restorable via **`lists.restore`**
or **`Ctrl+Z`** — the first slice of a general "no permanent delete in ordinary use" principle (physical deletion
limited to GDPR/account erasure and an explicit Global-Admin purge behind an off-by-default **Admin Panel /
Features / Delete** flag). Card/list/swimlane **moves** are now undoable/redoable with `Ctrl+Z` / `Ctrl+Y` (the
`userPositionHistory` collection was silently **not recording** — its guard checked an un-imported global — and had
no redo or key bindings; all fixed, selection logic unit-tested). Design unifies this into one universal
change-History with scoped views. Activities were confirmed to already load progressively. Thanks to **xet7**.
Commits:
- [Soft delete for lists: delete = mark, restorable + undoable (#1023)](https://github.com/wekan/wekan/commit/46924985f88b473e054158ecae68af6d7cce3556)
- [Undo docs: soft-delete design (delete = mark, never destroy)](https://github.com/wekan/wekan/commit/58344db0d7e3ed9e4f8ca638e0b3e00d2813ac63)
- [#6478: undo/redo for card/list/swimlane moves + confirm high-impact list move on touch](https://github.com/wekan/wekan/commit/4ca33d29f78bd81cecc931faa5164f862823ad52)
- [Docs: design for per-group card History (view + restore)](https://github.com/wekan/wekan/commit/3d1276fc1fbd063f1c7b79467b788f13a5e62435)
- [Docs: undo/redo + high-impact confirmation feature doc](https://github.com/wekan/wekan/commit/f3354f5cc8577cda46d2789230e8d30939bd927d)
- [Docs: unify undo/redo into one universal change-History (every change) with many scoped views](https://github.com/wekan/wekan/commit/9be41ab34b1bd596a82b95f088e49e94da8b988c)
- [Activities already load progressively (infinite scroll), like visible cards](https://github.com/wekan/wekan/commit/e246403bb6969f402e066d01e256e2c56f10ff1c)
- **Home board — set a board that opens automatically after login**
([#2220](https://github.com/wekan/wekan/issues/2220)). A per-user default "home" board (`profile.defaultBoardId`)
opens automatically after login on **all deployments** (a once-per-session router redirect), and on **Sandstorm** a
grain with exactly one board again opens straight into it (**without saving anything**; a saved Home board wins, a
many-board grain with no choice shows All Boards). You **set the Home board from the All Boards page via
Multi-Selection**: turn Multi-Selection on, select a board, and click the **home** action in the new
**"Selected:"** row (which also has a **star** action); the Home board then shows a **home badge** and sits at the
**top of Starred**. **Done differently:** the earlier per-board-tile home toggle was hidden, but setting a Home
board was brought back through the Multi-Selection "Selected:" action — so the feature is **available, not
disabled, and not Sandstorm-only** — and the initial Sandstorm "save the single-board choice" was changed to
**no-save**. Thanks to **xet7**. Commits:
- [Feature #2220: mark a board to open automatically after login (home board)](https://github.com/wekan/wekan/commit/e8ced84172ec7950800becfee9860c33e964034e)
- [Sandstorm: auto-open a single-board grain (no save) (#2220)](https://github.com/wekan/wekan/commit/9a0c0613a5909f9a37f6afb20890894b8e5082bd)
- [All Boards: hide Home-board icons and disable setting a Home board (#2220)](https://github.com/wekan/wekan/commit/ac20cd64424ebe356fcd7ccb3a4f60370580e7a3)
- [All Boards: "Selected:" star/home actions + Home board badge (#2220)](https://github.com/wekan/wekan/commit/d67d27f0d872c4fc6e990262d3d3577a28783eee)
- [Tests: cover the remaining Upcoming changes (#2220 default board, #6465 UI, #6478 confirm)](https://github.com/wekan/wekan/commit/8287865b238f3ed90e60e4624e98b383e896beee)
- **Add List moved from a standing column to a per-list header button**
([#6465](https://github.com/wekan/wekan/issues/6465)). The Swimlanes/Lists views no longer show a permanent "Add
List" composer column; each list header has a far-right add-list button that opens the composer as a column
**immediately after that list** (right in LTR, left in RTL), and an **empty** swimlane/board shows a **+** button.
Also fixes the composer's Save doing nothing (`createListAfter`'s `check()` used a non-`Optional` matcher for
`nextListId`, so the inline composer 400'd with "Match failed"). *(First cut — desktop/main views.)* Thanks to
**csonkaoszimt** (report) and **xet7**. Commits:
- [#6465: Add List as a per-list header button + inline composer, not a standing column](https://github.com/wekan/wekan/commit/765d04bf7c678a9f28b663beadae7b07fd79f339)
- [Fix add-list: Save did nothing (Match failed) + empty-swimlane + button (#6465)](https://github.com/wekan/wekan/commit/0535215225a41bfe16d936a9a1b447fc77f9122e)
- **All Boards, board header, top bar and popups — a large batch of layout/theming polish.** Highlights: the
right-sidebar **hamburger is pinned to the right edge** at every width with an ~8px gap, and the duplicate **Board
Settings cog was removed**; the All Boards **My Boards toolbar wraps** (order Multi-Selection then Sort then
Search), the **Multi-Selection button matches the Swimlanes view**, the empty grey band was removed, **"+ Add …"
tiles match board-tile height**, the board-tile **drag handle sits at the right middle** (transparent, icon only),
and the **search box is half width** with the multi-selection hint beneath it; the **mobile/desktop toggle and
zoom** swapped places; the **Change Language** popup shows **multiple columns**; the **Change Avatar** upload
button and the **Search All Boards** / **Archived boards** buttons follow the **theme colors**; the **Archived
boards modal** is full width and starts below the header; the **Notifications drawer** is full width with a black
hamburger + X and black menu icons; and the member menu's duplicate **Notifications** entry was removed. **Done
differently:** an "icons only — always hide button text labels" board-header change was tried and then
**reverted**, so button **text labels stay**. Thanks to **xet7**. Commits:
- [Board header: keep the sidebar hamburger pinned to the right edge at all widths](https://github.com/wekan/wekan/commit/7718118654f35a214ea5f9b2147642eabf4a6702)
- [Board header: remove empty space around the sidebar hamburger button](https://github.com/wekan/wekan/commit/e33e50d9c84cc9125ac6e8d5e491a7419bbae883)
- [Board header: hamburger right gap matches the user-name gap (~8px)](https://github.com/wekan/wekan/commit/bd72f5d64c58ad0861f370a17969f6ee482c466c)
- [Board header: hide button text labels on narrow windows (icons only)](https://github.com/wekan/wekan/commit/cfd53cc9698bbc759bef55152ea8f74ca00f3afc)
- [Board header: remove the Board Settings cog (it is already in the right sidebar)](https://github.com/wekan/wekan/commit/68723ff43b324dda1b584e5ba4eb5d90d0490124)
- [Board header: icons only — always hide button text labels](https://github.com/wekan/wekan/commit/97b0240ca4bb5f2bf02020e94eb6d7ffc278acd9)
- [Revert board-header icons-only; update CHANGELOG Upcoming with newest changes](https://github.com/wekan/wekan/commit/834698bb5e3e926213676e499bf4338713bc43da)
- [Header/All Boards: reorder Multi-Selection/Sort/Search and swap mobile-toggle/zoom](https://github.com/wekan/wekan/commit/15e756f4e0b265a020b659fea2dc3339b2c2e767)
- [All Boards: + Add Board / + Add Template Container tile same height as board icons](https://github.com/wekan/wekan/commit/10085615179dbdd56bd5d630ee526e82a1451ef6)
- [All Boards: remove empty .board-list-header dead grey space above the layout](https://github.com/wekan/wekan/commit/1312fefb1234d6fcdcda930d93fd189e57befd37)
- [All Boards: My Boards bar wraps so Sort/Multi-Selection/Clear stay visible](https://github.com/wekan/wekan/commit/7699cc1e90f18b599f294fba774403f3b17e3749)
- [All Boards: Multi-Selection button looks the same as in Swimlanes View](https://github.com/wekan/wekan/commit/c6aa769bc0a8b01706e0f45a19b8ba8c66c77498)
- [All Boards: move board tile drag handle to the right middle](https://github.com/wekan/wekan/commit/aafe71fd191a8633012d838d00aa96abd3f092a2)
- [All Boards: transparent board tile drag-handle background (icon only)](https://github.com/wekan/wekan/commit/197254fe16c2dd2c266f2a1797d80feffccfc914)
- [All Boards: vertically center the Search boards input text](https://github.com/wekan/wekan/commit/035195f2b4b7a8601964d54645ec7d139e65dd41)
- [All Boards: move the Search boards input text 4px further down (top: 6px)](https://github.com/wekan/wekan/commit/cb80198d9748b2fbd7dee392e235f882785420c7)
- [All Boards: move the Search boards input text 2px further down (top: 8px)](https://github.com/wekan/wekan/commit/c21edb160bddfb2a9e635d5c2154c037dc464698)
- [All Boards: move "+ Add Board" text 4px up (keep tile height)](https://github.com/wekan/wekan/commit/3f5173b9602e14de3201ebc0052b2751e522f100)
- [All Boards: move "+ Add Board" text 2px further up (padding 18/8/24/8)](https://github.com/wekan/wekan/commit/519c04b8ef46497d526f4586432b132887335487)
- [All Boards: move "+ Add Template Container" text 2px down (separate from Add Board)](https://github.com/wekan/wekan/commit/cd7f9f7bc9e48c6f54b007deec13194b90d4d33b)
- [All Boards: half-width search, hint below Multi-Selection, title beside icon](https://github.com/wekan/wekan/commit/3736f8431a4b0b8224c0b7315ac8e596aa4426fa)
- [Change Language popup: show languages in multiple columns when there is width](https://github.com/wekan/wekan/commit/fc0649a756d0f6e6caf2feef659786a9908397b1)
- [Change Avatar popup: Upload button uses theme colors (blue bg, white text)](https://github.com/wekan/wekan/commit/08a2e5525e8e7b099d766643b09616e75f750fc9)
- [Archived boards modal: full width + extends to the bottom of the window](https://github.com/wekan/wekan/commit/b027594ca8ad6d9f781214e8c26aac572ed20c25)
- [Archived boards modal: start below the header bar so its top is visible](https://github.com/wekan/wekan/commit/1c8ba2afc3a51d5a0b71f5777d1e189dca5525e4)
- [Archived boards: Restore/Delete buttons follow the theme override colors](https://github.com/wekan/wekan/commit/4dc09c62b9719cbb3dd126420239f7d9da5bea55)
- [Search All Boards: new-search button follows theme override colors](https://github.com/wekan/wekan/commit/98a03bf045668a95f58ae2cfe15f6d39591c89de)
- [Search All Boards: label the clear button "Clear" using existing trello-clear-job key](https://github.com/wekan/wekan/commit/a4cc0c2182b9c4da74325bb5e21d7f13f64e9484)
- [Member menu: remove the Notifications entry (duplicates the header bell icon)](https://github.com/wekan/wekan/commit/8cb4050cf21a1fce10c08872cb8f4a4bb270565b)
- [Notifications drawer header: black X + hamburger, X near the top-right edge](https://github.com/wekan/wekan/commit/8dd6bd9db5a8f427f6863f033e7236ae80ead336)
- [Notifications drawer: full width + robust flex header (black hamburger + X)](https://github.com/wekan/wekan/commit/c87b4cb5103aa2e010f0c40b0554e4cf84126059)
- [Notifications drawer: fix X position — cancel the global absolute .close rule](https://github.com/wekan/wekan/commit/5e014d0967ec8d622857348a264f3522a7e9769d)
- [Notifications menu: make the dropdown item icons black](https://github.com/wekan/wekan/commit/01088ea9d4583301446cb021ecd8772e4d3cde6d)
- **Denser default layout: thinner lists, board tiles and card dock; taller-looking minicards**
([#6465](https://github.com/wekan/wekan/issues/6465)). List width default 272 to **220px** (minimum 270 to
**200px**), All Boards board tiles min-height 100 to **72px**, and the card-detail right dock max-width 800 to
**520px** — all overridable, user customizations untouched. Minicards no longer reserve a tall empty band under a
one-line title (the title viewer gets `min-height: 0`), and the swimlane header no longer clips at non-100% zoom.
Thanks to **csonkaoszimt** (report) and **xet7**. Commits:
- [#6465: denser defaults - thinner lists (220px), board tiles and card dock (520px)](https://github.com/wekan/wekan/commit/99299c2a91827f88f0bf091e810b8ce9d6211784)
- [Fix #6465: minicards wasted half their height on empty white space](https://github.com/wekan/wekan/commit/a76f7d28e33f79b79f89eebffcb2a2946205f5f0)
- [#6465: swimlane header bar no longer clips at non-100% board zoom](https://github.com/wekan/wekan/commit/7b7fada1b89fa21e35ebaeca26d3f85299afb33d)
- [Test: update fixed-list-width e2e for the lowered 200px minimum (#6465)](https://github.com/wekan/wekan/commit/ae29555862089cd85111aa417a892888d48124bd)
and fixes the following bugs:
- [Confirmation dialogs work again](https://github.com/wekan/wekan/commit/f6f021f8cc8de452593f499e1306dca021679325) — clicking any `.js-confirm` button (e.g. "Remove Member") did
nothing because `Popup.afterConfirm()` stashed the pending action on the Blaze **data context** (an immutable
Minimongo doc that Blaze re-creates on re-render); it is now stored on the Popup **instance**
([#6479](https://github.com/wekan/wekan/issues/6479)). Thanks to **mueschel** (report) and **xet7**.
- [Moving a card between swimlanes is no longer broken with long lists](https://github.com/wekan/wekan/commit/6d5a594b0709ad3456f91afc31d821304539a262) — jQuery UI sortable cached
geometry at drag-start and never re-cached on WeKan's manual auto-scroll, so the drop re-homed the card in the
source swimlane; fixed with `sortable('refreshPositions')` after a scroll
([#6477](https://github.com/wekan/wekan/issues/6477)). Thanks to **mueschel** (report) and **xet7**.
- [Filter by date -> Overdue no longer lists cards with no due date](https://github.com/wekan/wekan/commit/b1d33c536239d45ae5df6139beb6b5548f7e03c0) — `$lte`/range selectors
matched `null` under both FerretDB and minimongo; `DateFilter._getMongoSelector` now adds `$ne: null` (reported by
email). Thanks to the reporter and **xet7**.
- [A board-wide list no longer disappears from other swimlanes when nudged](https://github.com/wekan/wekan/commit/c29cf3b985abb670222ec25c76a055e0ff9ba53a) — the drop handler
treated any small drag of a board-wide list (`swimlaneId === null`) as a swimlane change; it now also requires an
original swimlane (reported by email). Thanks to the reporter and **xet7**.
- **FerretDB (bundled) — #6476 crash-loop root cause fixed, and label/title/date filters pushed down to SQLite**
([#6476](https://github.com/wekan/wekan/issues/6476)). A startup SyncedCron upsert hit an orphaned-table error
(`table "…connections_<hash>" already exists`) that became an unhandledRejection and crash-looped WeKan so its
port never opened; the bundled FerretDB now creates tables/indexes with `IF NOT EXISTS`. It also pushes label
(`$in`), title (`$regex`) and `dueAt` range filters down to SQLite instead of scanning every card in Go. (Details
in the fork's own CHANGELOG.) Thanks to **uusijani**, **a1bert01** and **xet7**. Changelog commits:
- [Changelog: #6476 real root cause (FerretDB orphaned-table crash loop) and the bundled FerretDB fix](https://github.com/wekan/wekan/commit/41c747ef5f7e3fac4ae37714e65e1f7b4703ab80)
- [Changelog: bundled FerretDB now pushes label ($in) and title ($regex) filters down](https://github.com/wekan/wekan/commit/b29a47778ffb3025d8c26d67427e022a6a137076)
- [Changelog: bundled FerretDB now pushes date range filters down too](https://github.com/wekan/wekan/commit/5730f7dcc54e551ca5b4cc6ede307ebe7e078310)
- **Snap release, CI and dev tooling.** The exotic `ppc64el`/`s390x` snaps now build on **GitHub Actions under
QEMU** instead of Launchpad (which returned exit 0 with no artifact); `rebuild-wekan.sh` CURRENT-IP detection is
subnet-agnostic (no more `http://:3000`); and the **Playwright E2E workflow was disabled**. Commits:
- [Snap release: build ppc64el and s390x under QEMU on GitHub Actions, not Launchpad](https://github.com/wekan/wekan/commit/d7f36d70a04ee96669d75c2dea675cf7e7b76d7a)
- [rebuild-wekan.sh: robust CURRENT-IP detection (fix http://:3000 Invalid URL)](https://github.com/wekan/wekan/commit/c6f960bbe10d5ff36a92314c0a21380e914a89e3)
- [ci: disable Playwright E2E Tests workflow](https://github.com/wekan/wekan/commit/ef81c25232c7f5f26b6b33d9e9b945b8e769dd35)
- **Changelog, docs and test housekeeping.** Commits:
- [Changelog: move post-v9.99 changes to an Upcoming section above v9.99](https://github.com/wekan/wekan/commit/c4784225d51f2d13f2f50b2fe172cb156feab861)
- [Updated ChangeLog](https://github.com/wekan/wekan/commit/26319c974b180cc2a59b55705bc5d5128f23f438)
- [Changelog: drop bogus #6483/#6484 issue links (those bugs were reported by email)](https://github.com/wekan/wekan/commit/e9e70717445afbf124fe5c536f4199ef97d8ff5b)
- [Updates](https://github.com/wekan/wekan/commit/176c5ef2d8a31f85cb41e52a85003e3560e818cc)
- [Test: de-flake webkit swimlane-add popup wait (6s -> 15s)](https://github.com/wekan/wekan/commit/8aacc2236052108576e9b0e1ac0ca68f9339463e)
- [Updates](https://github.com/wekan/wekan/commit/861ded439e08dfe16e65f19a3e150d83bbd8286f)
- [Updated ChangeLog](https://github.com/wekan/wekan/commit/66a2f8ddc8f4bc95fe08478f1d0eeda08f87cf82)
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.99 2026-07-17 WeKan ® release
This release fixes the following BUG:
- **LDAP connection leak: WeKan exhausted the directory server with "too many open connections"**
([#6467](https://github.com/wekan/wekan/issues/6467), [#6469](https://github.com/wekan/wekan/issues/6469)).
Operators reported that after an update WeKan "dies really quickly", that restarting the server did not
help, and that it "kills our openldap server with too many open connections"; others saw logins hang for
minutes and then fail with *"Must be logged in"*. Root cause: every LDAP login attempt
(`packages/wekan-ldap/server/loginHandler.js`) and every background sync run
(`packages/wekan-ldap/server/sync.js`) created a fresh `new LDAP()` and called `connect()`, but the code
**never called `disconnect()` on any path** — success, failure, or fallback. Each login attempt (including
every *failed* one) and each background-sync tick (every minute by default) therefore leaked one socket to
the LDAP/AD server. Over time this grew without bound until the directory server hit its per-client
connection limit and started refusing connections, which took it — and, with it, every WeKan login — down.
- **Fixed** by guaranteeing the connection is always released:
- A small shared helper `packages/wekan-ldap/server/connectionGuard.js` (`runWithLdapDisconnect(ldap, fn)`)
runs the work and `disconnect()`s in a `finally`, on every exit path. The disconnect is best-effort and
never masks the original result or error.
- The LDAP login handler now runs its whole flow through `runWithLdapDisconnect`, so `connect()` is always
paired with a `disconnect()` — on a successful login, a thrown `Meteor.Error`, or a fallback to the
default account system.
- The background `sync()` releases its connection in a `finally`, and `importNewUsers()` disconnects only
the connection it opened itself (never a connection borrowed from `sync()`).
- The admin `ldap_test_connection` method (`packages/wekan-ldap/server/testConnection.js`), which had the
same leak, now disconnects on both the success and failure paths, so repeated "Test Connection" clicks
no longer leak either.
- `LDAP.disconnect()` is now a safe no-op when `connect()` was never reached, so cleanup can never throw.
- **Tests** (`tests/ldapConnectionRelease.test.cjs`, added to `test:unit:node`): behavioural coverage of the
guard (disconnects after success and returns the result; disconnects exactly once; disconnects *and*
re-throws when the work throws — the failed-login path that exhausted OpenLDAP), negative cases (a failing
disconnect never turns a success into a failure nor hides the real error; a missing/`null` ldap is
tolerated), plus source-level guards that the leak-prone call sites actually route through the guard and
that `connect()` lives inside the guarded region.
- Thanks to the reporting operators and **xet7** (fix).
This release also updates the bundled FerretDB (see the fork's own CHANGELOG Upcoming): the SQLite backend
connection pool no longer caps `MaxOpenConns` at 16, which had starved WeKan's cursor-heavy load and made
boards take minutes to load and logins fail with *"Must be logged in"* ([#6467](https://github.com/wekan/wekan/issues/6467),
[#6469](https://github.com/wekan/wekan/issues/6469)).
- **Snap: FerretDB never becoming ready made WeKan hang with no web port and no explanation**
([#6476](https://github.com/wekan/wekan/issues/6476)). With `database=ferretdb`, `snap-src/bin/wekan-control`
waits for FerretDB to accept connections *before* starting WeKan — WeKan does not open its HTTP port until
the database answers. That wait loop had **no timeout and no diagnostics**: if FerretDB never started
listening (a crashed or CPU/architecture-incompatible per-arch binary, a locked or corrupt SQLite database,
or the FerretDB service left disabled by a failed migration hand-off), WeKan blocked there forever. The
service showed as `active`, but the port never opened and the only symptom was a silent
`FerretDB not ready yet, retrying in 5 seconds` repeating — exactly what #6476 reported (Apache proxy could
not connect to port 3333). The reporter's `wekan.log` stopped right after the startup env dump, confirming
control never reached `node main.js`.
- **Fixed** by mirroring the MongoDB branch: after `WEKAN_DB_WAIT_TIMEOUT` seconds (default 120,
overridable) the FerretDB wait loop now prints an actionable hint **once** and keeps retrying — pointing to
`snap logs <snap>.ferretdb` for the real error, naming the SQLite directory to check for locks/corruption,
the arch-mismatch (`exec format error`) case, the `snap start --enable <snap>.ferretdb` recovery, and the
`snap set <snap> database=mongodb` fallback to keep working meanwhile. This turns "port never opens, no
clue why" into a clear message. Regression test: `tests/ferretdbWaitTimeout.test.cjs`, including a negative
guard that the FerretDB wait is not a silent unbounded loop again.
- This is a robustness/diagnostics fix; the underlying reason FerretDB was not accepting connections is
environment-specific and lives in the FerretDB service log.
- Thanks to **uusijani** (report) and **xet7** (fix).
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.98 2026-07-17 WeKan ® release
This release fixes the following SECURITY ISSUES found by GitHub CodeQL code scanning:
- **[EscapeBleed](https://wekan.fi/hall-of-fame/escapebleed/): incomplete string escaping when
building a regular expression** (GitHub CodeQL code scanning alert #423, rule
`js/incomplete-sanitization`, CWE-116 Improper Encoding or Escaping of Output;
`tests/maximizedCardPosition.test.cjs`). Code that turned a CSS declaration into a
`RegExp` escaped only parentheses (`str.replace(/[()]/g, '\\$&')`) instead of the full
regex metacharacter set — an input containing other metacharacters (including a backslash)
would not be escaped correctly, so the generated pattern could match the wrong thing.
- **Fixed** by escaping the complete metacharacter set
(`str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`), matching the correct pattern already used
elsewhere in the tests. This is test-only code with a fixed, trusted input list, so there
was no injection exposure, but the incomplete escape was genuinely wrong.
- Thanks to **GitHub CodeQL** (code scanning alert #423) and **xet7** (fix).
- **[RandomBleed](https://wekan.fi/hall-of-fame/randombleed/): biased random ids from a
cryptographically secure source** (GitHub CodeQL code scanning alert #422, rule
`js/biased-cryptographic-random`, CWE-1204 / weak randomness; `server/lib/schemaUpgradeSteps.js`).
The startup schema upgrade generates Meteor-style document ids with
`crypto.randomBytes(len)` mapped through `byte % ID_CHARS.length`. Because 256 is not a
multiple of the 55-character alphabet, that modulo skews generated ids toward the first 36
characters of the alphabet (each ~1.4% more likely than the rest) — reducing entropy of the
ids used for the swimlanes/lists/checklist-items the upgrade creates.
- **Fixed** with rejection sampling: bytes at or above the largest multiple of the alphabet
size (220) are discarded and resampled, so every character is exactly equally likely; ids
stay Meteor-style 17 characters (and exact-length for custom lengths). A negative
regression test pins that out-of-range bytes are never wrapped.
- Thanks to **GitHub CodeQL** (code scanning alert #422) and **xet7** (fix).
Thanks to above GitHub users for their contributions and translators for their translations.
# v9.97 2026-07-17 WeKan ® release
This release adds the following new features:
+1 -1
View File
@@ -7,7 +7,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=9.97
ARG VERSION=10.02
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
@@ -67,7 +67,7 @@ that by providing one-click installation on various platforms.
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
- WeKan ® largest user has 30k users using WeKan ® in their company.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to about 105 languages.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to 154 languages.
- [Features][features]: WeKan ® has real-time user interface.
- [Platforms](https://wekan.fi/install/): WeKan ® supports many platforms.
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v9.97.0"
appVersion: "v10.02.0"
files:
userUploads:
- README.md
+2 -2
View File
@@ -8,10 +8,10 @@ template(name="archivedBoards")
each archivedBoards
li.archived-lists-item
div.board-header-btns
button.primary.board-header-btn.js-delete-board
button.primary.board-header-btn.js-delete-board(type="submit")
i.fa.fa-trash
| {{_ 'delete-board'}}
button.primary.board-header-btn.js-restore-board
button.primary.board-header-btn.js-restore-board(type="submit")
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
+1 -1
View File
@@ -23,7 +23,7 @@ template(name="boardBody")
if debugBoardState
.debug-info(style="position: fixed; top: 0; inset-inline-start: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-wrapper(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes.dragscroll(
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
+2 -6
View File
@@ -159,12 +159,8 @@ template(name="boardHeaderBar")
i.fa.fa-times-thin
.separator
//- #6465: one-click board settings — the boardMenu popup used to need
//- two clicks (open sidebar, then Boardeinstellungen). The click
//- handler already exists in boardHeader.js ('click .js-open-board-menu').
if currentUser.isBoardMember
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
i.fa.fa-cog
//- Board Settings cog removed: Board Settings is available in the right
//- sidebar (the hamburger), so the header no longer duplicates it.
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
-1
View File
@@ -130,7 +130,6 @@ Template.boardHeaderBar.events({
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
+91 -18
View File
@@ -1,13 +1,5 @@
@import url("../../../public/css/reset.css") print, screen;
/* Board List Header with Zoom Controls */
.board-list-header {
display: flex;
justify-content: center;
margin: 1vh 0 2vh 0;
padding: 1vh 0;
}
/* Two-column layout for All Boards */
.boards-layout {
display: grid;
@@ -260,6 +252,9 @@
align-items: center;
justify-content: space-between;
gap: 8px;
/* Wrap to a 2nd row on narrow windows so the search box and the Sort /
Multi-Selection / Clear buttons stay visible instead of overflowing. */
flex-wrap: wrap;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f5f5;
@@ -273,6 +268,46 @@
align-items: center;
gap: 8px;
flex: 1;
min-width: 0; /* allow shrinking so path-right can wrap below instead of overflowing */
flex-wrap: wrap; /* the multi-selection hint wraps under the title when tight */
}
/* Keep the folder icon and the menu title (e.g. "Remaining") on ONE line, the
title to the RIGHT of the icon — never let flex-wrap stack them vertically. */
.boards-path-header .path-title {
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
/* Multi-Selection button with its hint stacked underneath (own column). */
.boards-path-header .multiselection-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
/* "Selected:" actions row (star / home) below the Multi-Selection button. */
.boards-path-header .selected-actions {
display: flex;
align-items: center;
gap: 8px;
}
.boards-path-header .selected-label {
font-size: 13px;
color: #333;
}
.boards-path-header .selected-action {
cursor: pointer;
color: #333;
font-size: 16px;
line-height: 1;
padding: 2px 4px;
}
.boards-path-header .selected-action:hover {
color: #2980b9;
}
.boards-path-header .multiselection-hint {
@@ -295,6 +330,10 @@
display: flex;
align-items: center;
gap: 8px;
/* Buttons (Sort / Multi-Selection / Clear) wrap among themselves too, so none
is ever cut off on a narrow window. */
flex-wrap: wrap;
justify-content: flex-end;
}
/* #5799: board-name search box in the All Boards header */
@@ -306,6 +345,9 @@
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
/* Roughly half the previous width; the input flexes to fill what's left. */
width: 300px;
max-width: 100%;
}
.boards-path-header .board-search input {
@@ -313,7 +355,11 @@
outline: none;
background: transparent;
font-size: 14px;
min-width: 140px;
flex: 1 1 auto;
min-width: 0;
/* Nudge the text down so it sits at the vertical middle of the field. */
position: relative;
top: 8px;
}
.boards-path-header .board-search .board-search-clear {
@@ -517,19 +563,33 @@
overflow: hidden;
background-color: inherit; /* Inherit board color from parent li.js-board */
color: #f6f6f6;
min-height: 100px;
min-height: 72px; /* #6465: thinner tiles so more boards fit in the overview */
font-size: 16px;
line-height: 22px;
border-radius: 0; /* No border-radius - parent .js-board has it */
display: block;
font-weight: 700;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
padding: 24px 8px 18px 8px; /* #6465: trimmed; top leaves room for the drag handle, bottom for the checkbox */
margin: 0; /* No margin - moved to parent .js-board */
position: relative;
text-decoration: none;
word-wrap: break-word;
}
/* #2220: Home-board badge — a home icon centred at the top of the board tile. */
.board-list .board-home-badge {
position: absolute;
top: 2px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
color: #fff;
font-size: 14px;
line-height: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
pointer-events: none;
}
.board-list .board-list-item > .js-open-board {
text-decoration: none;
color: inherit;
@@ -561,16 +621,27 @@
}
.board-list .js-add-board .label {
font-weight: normal;
line-height: 56px;
min-height: 100px;
/* Match the board tiles (#6465 thinner tiles) so the grey "+ Add Board" /
"+ Add Template Container" box is the same height as a board icon:
same min-height, padding and line-height as .board-list-item above. */
line-height: 22px;
min-height: 72px;
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
border-radius: 3px;
padding: 36px 8px 32px 8px;
/* Same total vertical padding as .board-list-item (42px -> same tile height),
but shifted up (top 24->18, bottom 18->24) so the "+ Add Board" text sits
higher. */
padding: 18px 8px 24px 8px;
color: #fff; /* White text */
}
/* "+ Add Template Container" text sits 2px lower than "+ Add Board" (same 42px
total vertical padding, so the tile height is unchanged). */
.board-list .js-add-board .label.is-template-container {
padding: 20px 8px 22px 8px;
}
.board-list .js-add-board .label i {
color: #fff; /* White icon */
}
@@ -668,12 +739,14 @@
.board-list .board-handle {
position: absolute;
padding: 4px 6px;
top: 4px;
left: 50%;
transform: translateX(-50%);
/* Right middle of the tile (matches the mobile/small-screen handle position). */
top: 50%;
inset-inline-end: 6px;
transform: translateY(-50%);
font-size: 14px;
color: #fff;
background: rgba(0,0,0,0.4);
/* Transparent background so only the drag-handle icon shows (not a box). */
background: transparent;
border-radius: 4px;
display: flex;
align-items: center;
+46 -28
View File
@@ -1,7 +1,5 @@
template(name="boardList")
.wrapper
.board-list-header
.boards-layout
// Left menu
.boards-left-menu
@@ -77,26 +75,11 @@ template(name="boardList")
.boards-right-grid
.boards-path-header
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
span.path-title
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
.path-right
.board-search
span.emoji-icon
i.fa.fa-search
input.js-board-search-input(type="text" placeholder="{{_ 'search-boards'}}" value="{{boardSearch}}" autocomplete="off")
if boardSearch
a.board-search-clear.js-board-search-clear(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
a.board-header-btn.js-open-boards-sort(title="{{_ 'sort-boards'}}" class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
span.emoji-icon
i.fa.fa-sort-alpha-asc
span {{_ 'sort'}}
//- Order (left -> right): Multi-Selection, Sort, Search boards.
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
@@ -107,13 +90,41 @@ template(name="boardList")
span.emoji-icon
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon
//- Multi-Selection button, with the "click checkboxes" hint stacked
directly BELOW it (own column) instead of beside the path title.
.multiselection-group
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span {{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
//- "Selected:" actions on the multi-selected boards: star them,
or set one as the Home board (opened after login, #2220).
.selected-actions
span.selected-label {{_ 'selected-label'}}
a.selected-action.js-star-selected(title="{{_ 'set-selected-starred'}}")
i.fa.fa-star
a.selected-action.js-home-selected(title="{{_ 'set-selected-home'}}")
i.fa.fa-home
a.board-header-btn.js-open-boards-sort(title="{{_ 'sort-boards'}}" class="{{#unless isBoardsSort 'custom'}}emphasis{{/unless}}")
span.emoji-icon
i.fa.fa-sort-alpha-asc
span {{_ 'sort'}}
//- Search boards is placed to the RIGHT of the Multi-Selection button.
.board-search
span.emoji-icon
i.fa.fa-search
input.js-board-search-input(type="text" placeholder="{{_ 'search-boards'}}" value="{{boardSearch}}" autocomplete="off")
if boardSearch
a.board-search-clear.js-board-search-clear(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
if boardsPaginationActive
@@ -126,7 +137,7 @@ template(name="boardList")
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
a.board-list-item.label.is-template-container(title="{{_ 'add-template-container'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-template-container'}}
@@ -188,6 +199,11 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
//- #2220: the Home board (opened after login) is badged with a
home icon at the top centre of its tile.
if isDefaultBoard
span.board-home-badge(title="{{_ 'home-board-badge'}}")
i.fa.fa-home
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
@@ -228,6 +244,8 @@ template(name="boardList")
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
//- #2220 Home-board toggle is disabled: the icon and the
//- set-default-board action are intentionally not rendered.
template(name="boardsSortPopup")
ul.pop-over-list
+56 -7
View File
@@ -542,6 +542,21 @@ Template.boardList.helpers({
const boards = ReactiveCache.getBoards(query, {});
const currentUser = ReactiveCache.getCurrentUser();
// #2220: the Home board (opened after login) always appears FIRST in the
// Starred view, even when it has not been explicitly starred.
const withHomeFirst = (arr) => {
if (tpl.selectedMenu.get() !== 'starred') return arr;
if ((tpl.boardSearchVar.get() || '').trim()) return arr;
const homeId =
currentUser && typeof currentUser.getDefaultBoardId === 'function'
? currentUser.getDefaultBoardId()
: null;
if (!homeId) return arr;
const homeBoard = ReactiveCache.getBoard(homeId);
if (!homeBoard || homeBoard.archived) return arr;
return [homeBoard, ...arr.filter((b) => b && b._id !== homeId)];
};
// #5799: in a sorted (non-custom) mode the server already computed the
// current page (filtered by menu/search and sorted), so render exactly that
// ordered page of board icons. Custom (manual drag order) falls through to
@@ -552,9 +567,11 @@ Template.boardList.helpers({
: 'custom';
if (sortMode !== 'custom') {
const paged = tpl.pagedBoardsVar.get();
return (paged.ids || [])
.map((id) => ReactiveCache.getBoard(id))
.filter(Boolean);
return withHomeFirst(
(paged.ids || [])
.map((id) => ReactiveCache.getBoard(id))
.filter(Boolean),
);
}
let list = boards;
@@ -591,11 +608,11 @@ Template.boardList.helpers({
}
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
return currentUser.sortBoardsForUser(list);
return withHomeFirst(currentUser.sortBoardsForUser(list));
}
return list
.slice()
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
return withHomeFirst(
list.slice().sort((a, b) => (a.title || '').localeCompare(b.title || '')),
);
},
// #5174 / #4825: the board tiles' per-list card-count line and member avatar
// row. Data comes from the one-shot getAllBoardsTileData method fetch in
@@ -629,6 +646,11 @@ Template.boardList.helpers({
const user = ReactiveCache.getCurrentUser();
return user && user.hasStarred(this._id);
},
// #2220: is this the user's Home board (the one opened after login)?
isDefaultBoard() {
const user = ReactiveCache.getCurrentUser();
return user && user.isDefaultBoard(this._id);
},
isAdministrable() {
const user = ReactiveCache.getCurrentUser();
return user && user.isBoardAdmin(this._id);
@@ -916,6 +938,29 @@ Template.boardList.events({
Meteor.call('toggleBoardStar', boardId);
}
},
// "Selected:" star action: star every multi-selected board that isn't yet
// starred (so it becomes Starred, never toggled back off by this button).
'click .js-star-selected'(evt) {
evt.preventDefault();
evt.stopPropagation();
const user = ReactiveCache.getCurrentUser();
BoardMultiSelection.getSelectedBoardIds().forEach((id) => {
if (user && !user.hasStarred(id)) {
Meteor.call('toggleBoardStar', id);
}
});
},
// #2220 "Selected:" home action: set the first selected board as the Home
// board (opened after login). Clicking it again when it is already Home clears
// it (toggle), matching toggleDefaultBoard.
'click .js-home-selected'(evt) {
evt.preventDefault();
evt.stopPropagation();
const ids = BoardMultiSelection.getSelectedBoardIds();
if (ids.length) {
Meteor.call('toggleDefaultBoard', ids[0]);
}
},
// HTML5 DnD from boards to spaces
// #5850: drag a (template) board onto an Org/Team/Domain target to share it.
'dragover .js-share-target'(evt) {
@@ -1103,6 +1148,10 @@ Template.boardList.events({
},
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
// The reset X is now nested inside the activate button (matching the Swimlanes
// view), so stop the click from bubbling to js-multiselection-activate — which
// would otherwise immediately re-activate what we just disabled.
evt.stopPropagation();
BoardMultiSelection.disable();
},
'click .js-toggle-board-multi-selection'(evt) {
+3 -3
View File
@@ -138,7 +138,7 @@
body.desktop-mode .card-details:not(.card-details-popup) {
position: fixed;
width: auto;
max-width: 800px;
max-width: 520px; /* #6465: narrower dock (was 800px, ~1/4 screen) */
flex-basis: auto;
border-radius: 3px;
z-index: 2001;
@@ -445,8 +445,8 @@ body.desktop-mode .card-details.card-details-collapsed {
@media screen and (min-width: 801px) {
.card-details {
top: 97px;
inset-inline-start: calc(50% - (600px / 2));
width: 600px;
inset-inline-start: calc(50% - (520px / 2)); /* #6465: narrower card (was 600px) */
width: 520px;
bottom: 0;
position: fixed;
resize: both;
+10 -6
View File
@@ -1418,7 +1418,10 @@ Template.inlinedCardDescription.events({
tpl.isOpen.set(true);
},
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
// Ctrl/Cmd+Enter submits by default; plain Enter when the user enabled
// "submit on Enter" in Member Settings (Shift+Enter then makes a newline).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(evt, { submitOnEnter })) {
tpl.find('button[type=submit]').click();
}
},
@@ -1529,11 +1532,12 @@ Template.editCardTitleForm.events({
Utils.showCopied(promise, $tooltip);
},
'keydown .js-edit-card-title'(event) {
// #4236: match the card description field — Ctrl/Cmd+Enter saves; plain
// Enter (and Shift+Enter) insert a new line instead of submitting. The old
// handler submitted on any Enter without Shift, so Enter could not add a
// newline in the title.
if (isSubmitKey(event)) {
// #4236: by default Ctrl/Cmd+Enter saves; plain Enter (and Shift+Enter)
// insert a new line. A user who turns on "submit on Enter" in Member
// Settings gets plain Enter to save (Shift+Enter for a newline) instead —
// the pre-#4236 fast title-editing workflow (#6480 email feedback).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(event, { submitOnEnter })) {
$('.js-submit-edit-card-title-form').click();
}
},
+57
View File
@@ -1,3 +1,60 @@
/* #4759: Member Settings / Font popup — a button per installed font (each shown in
its own font) and a row of size buttons (smaller ... 100% ... larger). Clicking a
button applies immediately. */
.ui-font-buttons,
.ui-font-size-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin: 2px 0 10px 0;
}
.ui-font-btn,
.ui-font-size-btn {
display: inline-block;
padding: 4px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #333;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
text-decoration: none;
}
.ui-font-btn:hover,
.ui-font-size-btn:hover {
background: #f0f0f0;
}
.ui-font-btn.active,
.ui-font-size-btn.active {
background: #2980b9;
border-color: #2980b9;
color: #fff;
}
/* The "Unset" colour reset links look like buttons. */
.color-wheel-wrap .js-reset-text-color,
.color-wheel-wrap .js-reset-bg-color {
display: inline-block;
padding: 4px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #333;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
text-decoration: none;
}
.color-wheel-wrap .js-reset-text-color:hover,
.color-wheel-wrap .js-reset-bg-color:hover {
background: #f0f0f0;
}
.color-wheel-wrap .js-reset-text-color.disabled,
.color-wheel-wrap .js-reset-bg-color.disabled {
opacity: 0.5;
pointer-events: none;
}
/* #5514: native color-wheel (custom RGB / hex) picker wrapper, shared by the
label / list / swimlane / card color popups. */
.color-wheel-wrap {
+6
View File
@@ -203,6 +203,12 @@
.minicard .minicard-title .viewer {
display: block;
word-wrap: break-word;
/* #6465: the minicard title renders inside a .viewer, which inherits the
global `.viewer { min-height: 2.5vh }` (layouts.css). On tall screens that
reserves a big empty band under a one-line title, so half of every
minicard was white space. Collapse it to fit the text (same fix already
applied to the label .viewer above). */
min-height: 0;
}
}
.minicard .dates {
+22 -2
View File
@@ -394,6 +394,7 @@ Template.list.onRendered(function () {
// the old code compared against a scrollLeftMax of 0 and never fired, so
// dragging a card toward an off-screen list never auto-scrolled the board.
const { computeEdgeScroll, findLaneUnderPointer } = require('/imports/lib/boardAutoScroll');
let scrolled = false;
const lanes = document.querySelectorAll('.js-lists');
const rects = Array.prototype.map.call(lanes, el => el.getBoundingClientRect());
const laneIndex = findLaneUnderPointer(rects, event.clientX, event.clientY);
@@ -406,7 +407,10 @@ Template.list.onRendered(function () {
scrollSize: lanes[laneIndex].scrollWidth,
clientSize: lanes[laneIndex].clientWidth,
});
if (nextLeft !== null) lanes[laneIndex].scrollLeft = nextLeft;
if (nextLeft !== null) {
lanes[laneIndex].scrollLeft = nextLeft;
scrolled = true;
}
}
// vertical auto-scroll stays on the board canvas
const canvas = document.querySelector('.board-canvas');
@@ -420,7 +424,23 @@ Template.list.onRendered(function () {
scrollSize: canvas.scrollHeight,
clientSize: canvas.clientHeight,
});
if (nextTop !== null) canvas.scrollTop = nextTop;
if (nextTop !== null) {
canvas.scrollTop = nextTop;
scrolled = true;
}
}
// #6477: after an auto-scroll, jQuery UI sortable's cached container/item
// geometry is STALE — it snapshots positions at drag start and on its own
// placeholder moves, never on our manual scroll here. With long/overflowing
// lists (exactly what triggers auto-scroll) the drop then resolved against a
// stale map and parked the placeholder in the WRONG swimlane's copy of the
// list, so a card dragged between swimlanes snapped back into its source
// swimlane (the stop handler's ui.item.parents('.swimlane') faithfully
// persisted it). Re-cache positions so the placeholder and drop resolve
// against the scrolled layout. Complements the DOM-mutation refresh in
// listBody.js (#2769), which does not fire on scroll.
if (scrolled) {
$cards.sortable('refreshPositions');
}
},
});
+4
View File
@@ -101,6 +101,10 @@ template(name="listHeader")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
//- #6465: far-right add-list button. Opens the inline Add List
//- composer AFTER this list (right in LTR / left in RTL).
a.js-add-list-here.list-header-plus-top(title="{{_ 'add-list'}}")
i.fa.fa-plus-square-o
template(name="editListTitleForm")
.list-composer
+18 -2
View File
@@ -208,6 +208,20 @@ Template.listHeader.events({
Utils.setListCollapseState(list, !status);
},
'click .js-open-list-menu': Popup.open('listAction'),
// #6465: open the inline Add List composer after this list. Record the
// swimlane it is displayed in (board-wide lists are not bound to one) so the
// new list is created in the right swimlane; the composer renders after this
// list in the DOM (right in LTR / left in RTL).
'click .js-add-list-here'(event) {
event.preventDefault();
const list = Template.currentData();
const swimlaneDom = $(event.currentTarget).closest('.js-swimlane');
const swimlaneId = swimlaneDom.length
? swimlaneDom.attr('id').replace('swimlane-', '')
: null;
Session.set('wekan-add-list-swimlane', swimlaneId);
Session.set('wekan-add-list-after', list._id);
},
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${Template.currentData()._id}`,
@@ -373,8 +387,10 @@ Template.listMorePopup.events({
],
}).length === 0
) {
allCardIds.map(_id => Cards.remove(_id));
Lists.remove(list._id);
// Soft delete (docs/Features/Undo/Undo.md): mark the list + its cards deleted
// (server cascades) instead of destroying them, so the delete is restorable
// and undoable (#1023). Replaces the old hard Cards.remove / Lists.remove.
Meteor.call('lists.softRemove', list._id);
} else {
const message = `${TAPi18n.__(
'delete-linked-cards-before-this-list',
+49
View File
@@ -0,0 +1,49 @@
/* Custom theme colors (docs/Theme/Theme.md) — the flat/clear categories let the
* user pick their own colors, applied as CSS variables on :root by
* globalThemeColor.js and consumed here. This is the initial, highest-impact
* surface (the header bars, primary buttons, sidebar button); deeper theming can
* grow from here. dark/special themes never set these variables. */
/* Flat: a single accent recolors the header bars and primary controls. The class
* lives on <body>; #header / #header-quick-access are descendants. */
.has-custom-theme-color #header,
.has-custom-theme-color #header-quick-access {
background: var(--theme-accent) !important;
}
.has-custom-theme-color button[type=submit].primary,
.has-custom-theme-color input[type=submit].primary,
.has-custom-theme-color .sidebar .sidebar-content .sidebar-btn {
background-color: var(--theme-accent) !important;
}
/* Clear: a two-color slide (gradient) across the header bars. */
.has-custom-theme-slide #header,
.has-custom-theme-slide #header-quick-access {
background: linear-gradient(135deg, var(--theme-accent), var(--theme-accent-2)) !important;
}
/* Select Color picker: category groups with a visible name above each group of
* colour swatches (docs/Theme/Theme.md). */
.theme-color-picker .theme-category-group {
margin-bottom: 8px;
clear: both; /* start below the previous group's floated swatches */
}
.theme-color-picker .theme-category-label {
display: block;
width: 100%;
clear: both; /* drop below the previous group's floats... */
text-align: left; /* ...so Flat/Clear/Dark/Special all sit at the left */
font-weight: bold;
margin: 2px 0 4px;
text-transform: capitalize;
}
/* No empty band above the first (Flat) title. */
.theme-color-picker .theme-category-group:first-child .theme-category-label {
margin-top: 0;
}
/* Tighten the Default (no override) row above the groups (Member Settings). */
.theme-color-picker ul.pop-over-list {
margin: 0 0 4px;
padding: 0;
}
+3 -11
View File
@@ -88,17 +88,9 @@
cursor: pointer;
transition: background-color 0.2s;
}
.board-color-belize.js-new-search {
background-color: #216694 !important;
color: #fff !important;
border: none !important;
}
.board-color-belize.js-new-search:hover {
background-color: #1a4d6a !important;
}
.board-color-belize.js-new-search:active {
background-color: #0d334d !important;
}
/* The new-search button uses the .primary button style, so it follows the theme
override colour (default blue #005377, or the board/global theme). The old
hardcoded belize (#216694) rules were removed. */
.global-search-operator {
font-family: Courier;
}
+2 -2
View File
@@ -71,8 +71,8 @@ template(name="globalSearch")
aria-label="{{_ 'globalSearch-title'}}"
autofocus dir="auto"
)
button.js-new-search.board-color-belize(type="button" aria-label="{{_ 'globalSearch-title'}}" style="background: #216694 !important; background-color: #216694 !important; color: #fff !important; border: none !important;")
span(aria-hidden="true") 🧹
button.primary.js-new-search(type="submit" aria-label="{{_ 'trello-clear-job'}}")
| {{_ 'trello-clear-job'}}
if searching
+spinner
else if hasResults
@@ -0,0 +1,74 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Session } from 'meteor/session';
import { ReactiveCache } from '/imports/reactiveCache';
// #5778 + docs/Theme/Theme.md: apply the user's optional GLOBAL theme color to the
// whole UI (All Boards, Search, Admin Panel, My Cards, etc.) via a `board-color-<name>`
// class on <body>, and apply CUSTOM colors (flat = 1 accent, clear = 2 = a gradient)
// as CSS variables consumed by customTheme.css.
//
// While viewing a single board, that board's own color owns the page, so we do NOT
// put the global color class on <body> there; but we DO expose the ACTIVE context's
// custom colors (the board's when on a board, the user's global override otherwise)
// so custom-colored boards and the custom global theme both render.
Meteor.startup(() => {
let appliedClass = null;
function applyClass(colorClass) {
if (colorClass === appliedClass) return;
try {
if (appliedClass) document.body.classList.remove(appliedClass);
if (colorClass) document.body.classList.add(colorClass);
} catch (_) {
// document.body may not exist yet in exotic embeddings; ignore.
}
appliedClass = colorClass;
}
function applyCustom(custom) {
try {
const root = document.documentElement;
const c1 = custom && custom[0];
const c2 = custom && custom[1];
if (c1) {
root.style.setProperty('--theme-accent', c1);
document.body.classList.add('has-custom-theme-color');
} else {
root.style.removeProperty('--theme-accent');
document.body.classList.remove('has-custom-theme-color');
}
if (c2) {
root.style.setProperty('--theme-accent-2', c2);
document.body.classList.add('has-custom-theme-slide');
} else {
root.style.removeProperty('--theme-accent-2');
document.body.classList.remove('has-custom-theme-slide');
}
} catch (_) {
// ignore
}
}
Tracker.autorun(() => {
const user = Meteor.user();
const globalColor = user && user.profile && user.profile.globalThemeColor;
if (globalColor) {
// #5778: a global override wins EVERYWHERE — including board pages. The header
// and .board-wrapper also prefer it (see header.jade / boardBody.jade), so the
// whole UI takes the chosen theme; unset it to return to per-board colors.
applyClass(`board-color-${globalColor}`);
applyCustom((user && user.profile && user.profile.globalThemeCustomColors) || []);
return;
}
// No global override: on a board expose that board's own custom colors (its color
// class already lives on .board-wrapper/#header); off a board, apply nothing.
const boardId = Session.get('currentBoard');
const board = boardId ? ReactiveCache.getBoard(boardId) : null;
applyClass(null);
applyCustom((board && board.customThemeColors) || []);
});
});
+26 -7
View File
@@ -5,8 +5,16 @@
z-index: 17;
}
#header #header-main-bar {
height: 40px;
padding: 7px 10px 0;
/* Flex row (was float-based) so the right button group — which ends with the
sidebar hamburger — is always pinned to the right edge at every window width,
instead of drifting to the middle as floats wrap. */
display: flex;
align-items: center;
flex-wrap: wrap;
min-height: 40px;
/* No right padding: the right gap is owned by the hamburger's margin so it can
match the ~8px gap to the right of the top-bar user name. (top right bottom left) */
padding: 7px 0 0 10px;
}
#header #header-main-bar h1 {
font-size: 20px;
@@ -39,15 +47,17 @@
opacity: 0.9;
}
#header #header-main-bar .board-header-btns {
display: block;
/* Flex so the buttons inside each group lay out (their float is ignored inside a
flex container) and the groups themselves are flex items of the header bar. */
display: flex;
align-items: center;
flex-wrap: wrap;
margin-top: 3px;
width: auto;
}
#header #header-main-bar .board-header-btns.left {
float: inline-start;
}
#header #header-main-bar .board-header-btns.right {
float: inline-end;
/* Always hug the right edge (and, if the row wraps, the right of its row). */
margin-inline-start: auto;
}
#header #header-main-bar .board-header-btn {
border-radius: 3px;
@@ -60,6 +70,15 @@
line-height: 28px;
margin: 0 12px;
}
/* The sidebar hamburger sits at the far right; drop its wide 12px side margins so
there is no button-sized empty space to its left or right. The cog before it also
drops its right margin so the two sit snugly together at the edge. */
#header #header-main-bar .board-header-btn.js-toggle-sidebar {
margin-inline-start: 2px;
/* 8px to the right edge — the same small gap as to the right of the user name in
the top bar (.header-user-bar-name margin-inline-end). */
margin-inline-end: 8px;
}
#header #header-main-bar .board-header-btn i.fa {
float: inline-start;
display: block;
+14 -13
View File
@@ -5,7 +5,7 @@ template(name="header")
Reddit "subreddit" bar.
The first link goes to the boards page.
if currentUser
#header-quick-access(class=currentBoard.colorClass role="navigation" aria-label="{{_ 'all-boards'}}")
#header-quick-access(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}" role="navigation" aria-label="{{_ 'all-boards'}}")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
@@ -24,11 +24,13 @@ template(name="header")
div#headerIsSettingDatabaseCallDone
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
// Zoom controls - always visible
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Mobile/desktop mode toggle (swapped with zoom)
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
// Drag handles toggle - between zoom and mobile mode toggle
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
@@ -78,12 +80,11 @@ template(name="header")
//
i.fa.fa-plus(title="Create a new board")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
// Zoom controls (swapped with the mobile/desktop toggle)
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Notifications
+notifications
@@ -95,7 +96,7 @@ template(name="header")
+headerUserBar
#header(class=currentBoard.colorClass)
#header(class="{{#if currentUser.globalThemeColorClass}}{{currentUser.globalThemeColorClass}}{{else}}{{currentBoard.colorClass}}{{/if}}")
//-
The main bar is a colorful bar that provide all the meta-data for the
current page. This bar is contextual based.
+21
View File
@@ -272,6 +272,27 @@ body.mobile-mode #content {
float: inline-end;
font-size: clamp(18px, 4vw, 24px);
}
/* Archived boards modal: full-width panel that extends to the bottom of the browser
window (so archived boards show full width with maximum vertical space). It is the
only modal that contains .archived-lists. */
#modal .modal-content:has(.archived-lists) {
width: 96vw;
max-width: 96vw;
/* The header bar(s) sit above the modal (higher z-index), so start the archive
panel BELOW them — otherwise its top (and the close X) is hidden under the top
header bar. ~96px clears both the quick-access bar and the board header bar; on
the All Boards page (one bar) this leaves a small top gap. Then extend to the
bottom of the browser window. */
margin: 96px auto 0;
min-height: calc(100vh - 106px);
box-sizing: border-box;
}
/* Each archived board is a full-width row. */
#modal .modal-content .archived-lists,
#modal .modal-content .archived-lists .archived-lists-item {
width: 100%;
box-sizing: border-box;
}
/* #6465: heading sizes back to the 6.09 values (22/18/16px) — the vw clamps
resolved to 24/20/18px on desktop and compounded the density regression. */
h1 {
+10 -2
View File
@@ -171,6 +171,12 @@
overflow: visible;
height: auto;
flex: 1;
/* Auto-filling columns so as many languages as possible are visible at once when
there is browser width; collapses to a single column on narrow windows. */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
align-content: start;
column-gap: 4px;
}
/* Ensure content div in language popup contains all items */
@@ -197,8 +203,10 @@
top: auto !important;
inset-inline-start: auto !important;
inset-inline-end: 20px !important;
width: auto !important;
max-width: 450px !important;
/* Wide enough for multiple language columns when the browser is wide, but never
wider than ~90% of the viewport (falls back to a single narrow column). */
width: min(90vw, 1100px) !important;
max-width: 90vw !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 300px !important;
+7 -1
View File
@@ -9,7 +9,13 @@ Popup.template.events({
Popup.close();
},
'click .js-confirm'() {
this.__afterConfirmAction.call(this);
// #6479: the action is stored on the Popup instance (see Popup.afterConfirm),
// not on this data context, so a re-rendered/immutable context can't lose it.
// Fall back to the legacy per-context field for any external caller.
const action = Popup._afterConfirmAction || this.__afterConfirmAction;
if (typeof action === 'function') {
action.call(this);
}
},
// #5942: On mobile/touch, tapping inside the card-detail sub-popups (assign
// user / set due date) made the popup DISAPPEAR. The document-level
@@ -0,0 +1,32 @@
//- Shared "Select Color" picker (docs/Theme/Theme.md), used by both Board Settings
and Member Settings. Colors are shown as VISIBLE swatches grouped by category, with
the category name as a header above each group; the native color wheel(s) appear
for the flat (1) / clear (2) categories once such a color is selected. `data.scope`
is 'board' or 'global'.
template(name="themeColorPicker")
form.theme-color-picker.edit-label
if isGlobal
ul.pop-over-list
li(class="{{#if isNoneSelected}}active{{/if}}")
a.js-theme-none
i.fa.fa-ban
| {{_ 'theme-default'}}
if isNoneSelected
i.fa.fa-check
each themeGroups
.theme-category-group
.theme-category-label {{label}}
.board-backgrounds-list.clearfix
each colors
.board-background-select.js-select-theme(data-color="{{name}}")
span.background-box(class="board-color-{{name}}")
span {{name}}
if selected
i.fa.fa-check
if showCustom
.color-wheel-wrap
label {{_ 'custom-color'}}
each customWheels
input.js-theme-wheel(type="color" data-index="{{index}}" value="{{value}}" title="{{_ 'custom-color'}}")
.theme-preview
span.background-box(class="{{previewClass}}" style="{{previewStyle}}")
+166
View File
@@ -0,0 +1,166 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { TAPi18n } from '/imports/i18n';
import { Utils } from '/client/lib/utils';
import { isHexColor, toHex } from '/models/lib/contrastColor';
import { BOARD_COLORS } from '/models/metadata/colors';
import {
THEME_CATEGORY_ORDER,
categoryOf,
colorsInCategory,
allowsCustomColor,
customColorCount,
} from '/models/lib/themeCategories';
// Shared "Select Color" picker (docs/Theme/Theme.md): VISIBLE color swatches grouped
// by category (with the category name above each group) + native color wheel(s) for
// the flat/clear categories once such a color is selected. data.scope is 'board'
// (writes board.color) or 'global' (writes the user's #5778 global override).
const DEFAULT_WHEEL = ['#2980b9', '#6dd5fa']; // stock flat accent / second slide stop
function readCurrent(scope) {
if (scope === 'board') {
const b = Utils.getCurrentBoard();
return { color: (b && b.color) || BOARD_COLORS[0], custom: (b && b.customThemeColors) || [] };
}
const u = ReactiveCache.getCurrentUser();
return {
color: (u && u.getGlobalThemeColor && u.getGlobalThemeColor()) || null,
custom: (u && u.getGlobalThemeCustomColors && u.getGlobalThemeCustomColors()) || [],
};
}
Template.themeColorPicker.onCreated(function () {
const scope = this.data && this.data.scope === 'board' ? 'board' : 'global';
const cur = readCurrent(scope);
this.scope = scope;
this.color = new ReactiveVar(cur.color); // null for global = no override
this.customColors = new ReactiveVar((cur.custom || []).slice());
});
Template.themeColorPicker.helpers({
isGlobal() {
return Template.instance().scope === 'global';
},
isNoneSelected() {
const tpl = Template.instance();
return tpl.scope === 'global' && !tpl.color.get();
},
// Visible swatches grouped by category, each group labelled with its category name.
themeGroups() {
const cur = Template.instance().color.get();
return THEME_CATEGORY_ORDER.map(key => ({
key,
label: TAPi18n.__(`theme-category-${key}`),
colors: colorsInCategory(key).map(name => ({ name, selected: name === cur })),
}));
},
showCustom() {
const cur = Template.instance().color.get();
return cur ? allowsCustomColor(categoryOf(cur)) : false;
},
customWheels() {
const tpl = Template.instance();
const cur = tpl.color.get();
const n = cur ? customColorCount(categoryOf(cur)) : 0;
const cc = tpl.customColors.get();
const wheels = [];
for (let i = 0; i < n; i += 1) {
wheels.push({ index: i, value: toHex(cc[i]) || DEFAULT_WHEEL[i] || '#0079bf' });
}
return wheels;
},
previewClass() {
const color = Template.instance().color.get();
return color ? `board-color-${color}` : '';
},
// Inline preview of custom colors (the stock board-color-* CSS can't show arbitrary
// colors until the CSS-variable refactor; the preview swatch shows them directly).
previewStyle() {
const tpl = Template.instance();
const cur = tpl.color.get();
const cat = cur ? categoryOf(cur) : null;
if (!cat || !allowsCustomColor(cat)) return '';
const cc = tpl.customColors.get();
if (!cc.some(Boolean)) return '';
if (customColorCount(cat) === 2 && cc[0] && cc[1]) {
return `background: linear-gradient(135deg, ${cc[0]}, ${cc[1]}) !important;`;
}
if (cc[0]) return `background: ${cc[0]} !important;`;
return '';
},
});
// Collect the custom colors to save: [] unless the user actually engaged a wheel, in
// which case read every wheel's current value (untouched ones contribute their shown
// default) so the result is a complete set of the category's expected count.
function gatherCustom(tpl) {
const color = tpl.color.get();
const cat = color ? categoryOf(color) : null;
if (!cat || !allowsCustomColor(cat)) return [];
if (!tpl.customColors.get().some(Boolean)) return [];
const n = customColorCount(cat);
const wheels = Array.from(tpl.findAll('.js-theme-wheel'))
.map(w => w.value)
.filter(isHexColor)
.slice(0, n);
return wheels.length === n ? wheels : [];
}
// Apply the current selection immediately (no Save button): board scope writes
// board.color, global scope calls setGlobalThemeColor. `color`/`custom` are read
// from the reactive state so the same helper serves the swatch click and the wheel.
function applySelection(tpl) {
const color = tpl.color.get() || colorsInCategory(THEME_CATEGORY_ORDER[0])[0];
const custom = gatherCustom(tpl);
if (tpl.scope === 'board') {
const b = Utils.getCurrentBoard();
if (b) {
Promise.resolve(b.setColor(color, custom)).catch(e => {
if (process.env.DEBUG === 'true') console.error('board setColor error', e);
});
}
} else {
Meteor.call('setGlobalThemeColor', color, custom, err => {
if (err && process.env.DEBUG === 'true') console.error('setGlobalThemeColor error', err);
});
}
}
Template.themeColorPicker.events({
// Clicking a color swatch applies that theme IMMEDIATELY (there is no Save button).
'click .js-select-theme'(event, tpl) {
const color = event.currentTarget.dataset.color;
if (!color) return;
tpl.color.set(color);
tpl.customColors.set([]); // a fresh theme -> its stock colors
applySelection(tpl);
},
// Live-update the preview while dragging the wheel...
'input .js-theme-wheel'(event, tpl) {
const idx = parseInt(event.currentTarget.dataset.index, 10);
const val = event.currentTarget.value;
if (!isHexColor(val)) return;
const cc = tpl.customColors.get().slice();
cc[idx] = val;
tpl.customColors.set(cc);
},
// ...and apply the custom color when the wheel is committed (avoids spamming the
// server on every intermediate value during the drag).
'change .js-theme-wheel'(event, tpl) {
applySelection(tpl);
},
// Clear the global override (Default) — applies immediately too.
'click .js-theme-none'(event, tpl) {
event.preventDefault();
tpl.color.set(null);
tpl.customColors.set([]);
if (tpl.scope === 'board') {
const b = Utils.getCurrentBoard();
if (b) b.setColor(BOARD_COLORS[0], []);
} else {
Meteor.call('setGlobalThemeColor', null, null);
}
},
});
+40
View File
@@ -0,0 +1,40 @@
/* Member Settings / Font (#4759): apply the user's chosen UI font. The class lives
* on <body> and the --wekan-ui-font variable is set by uiFont.js only for a
* validated, whitelisted font. Form controls are listed explicitly because their
* UA default font-family does not inherit. Icon fonts (Font Awesome etc.) set their
* own font-family on the element, so they are unaffected. */
body.has-ui-font,
body.has-ui-font button,
body.has-ui-font input,
body.has-ui-font textarea,
body.has-ui-font select,
body.has-ui-font .wekan-form-control {
font-family: var(--wekan-ui-font) !important;
}
/* Font size preset (#4759): scale the root font size. Elements using relative units
* (em/rem/%) scale with it; the variable is a percentage set by uiFont.js only for a
* known preset. 'default'/unset removes it. */
body.has-ui-font-size {
font-size: var(--wekan-ui-font-size) !important;
}
/* Text color (#4759): the variable is a validated hex set by uiFont.js. Icon fonts
* keep their own color rules; this recolors ordinary UI text + form controls. */
body.has-ui-text-color,
body.has-ui-text-color button,
body.has-ui-text-color input,
body.has-ui-text-color textarea,
body.has-ui-text-color select,
body.has-ui-text-color .wekan-form-control {
color: var(--wekan-ui-text-color) !important;
}
/* Text background color (#4759). */
body.has-ui-bg-color,
body.has-ui-bg-color input,
body.has-ui-bg-color textarea,
body.has-ui-bg-color select,
body.has-ui-bg-color .wekan-form-control {
background-color: var(--wekan-ui-bg-color) !important;
}
+34
View File
@@ -0,0 +1,34 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { fontFamilyValue, fontSizeValue, colorValue } from '/models/lib/uiFonts';
// #4759: apply the user's chosen UI font AND font-size preset to the whole UI by
// setting CSS variables on :root and toggling marker classes on <body> (uiFont.css
// consumes them). fontFamilyValue()/fontSizeValue() return '' for anything not on the
// curated whitelist/presets, so only known, safe values can ever reach the DOM — the
// saved values are also validated server-side. Unset => nothing applied (defaults).
Meteor.startup(() => {
function toggle(varName, className, value) {
try {
if (value) {
document.documentElement.style.setProperty(varName, value);
document.body.classList.add(className);
} else {
document.documentElement.style.removeProperty(varName);
document.body.classList.remove(className);
}
} catch (_) {
// document not ready in exotic embeddings; ignore.
}
}
Tracker.autorun(() => {
const user = Meteor.user();
const profile = (user && user.profile) || {};
toggle('--wekan-ui-font', 'has-ui-font', fontFamilyValue(profile.uiFont));
toggle('--wekan-ui-font-size', 'has-ui-font-size', fontSizeValue(profile.uiFontSize));
toggle('--wekan-ui-text-color', 'has-ui-text-color', colorValue(profile.uiTextColor));
toggle('--wekan-ui-bg-color', 'has-ui-bg-color', colorValue(profile.uiTextBgColor));
});
});
@@ -2,7 +2,8 @@ section#notifications-drawer {
position: fixed;
top: 48px;
inset-inline-end: 0;
width: 400px;
width: 100vw;
max-width: 100vw;
background-color: #fafafa;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
border-radius: 2px;
@@ -17,19 +18,20 @@ section#notifications-drawer .header {
position: fixed;
top: 48px;
inset-inline-end: 0;
width: calc(400px - 32px);
width: 100vw;
box-sizing: border-box;
padding: 8px 16px;
background: #ededed;
border-bottom: 1px solid #dbdbdb;
z-index: 2;
/* Flex row: hamburger (left), title (centre), X (right), all vertically centred. */
display: flex;
align-items: center;
}
section#notifications-drawer .header .notification-menu-toggle {
position: absolute;
inset-inline-start: 16px;
top: calc(50% - 12px);
font-size: 20px;
cursor: pointer;
color: #333;
color: #000 !important; /* black hamburger menu (override themed white link colour) */
line-height: 24px;
}
section#notifications-drawer .header .notification-menu-toggle:hover {
@@ -83,18 +85,33 @@ section#notifications-drawer .header .notification-menu .menu-item .menu-icon {
margin-inline-end: 8px;
font-size: 16px;
}
/* Menu-item icons (envelope / copy / trash / check) black. */
section#notifications-drawer .header .notification-menu .menu-item .menu-icon,
section#notifications-drawer .header .notification-menu .menu-item .menu-icon i.fa,
section#notifications-drawer .header .notification-menu .menu-item .check-icon,
section#notifications-drawer .header .notification-menu .menu-item .check-icon i.fa {
color: #000 !important;
}
section#notifications-drawer .header h5 {
flex: 1; /* fill the middle so the title is centred, X sits at the right */
text-align: center;
margin: 0;
}
section#notifications-drawer .header .close {
position: absolute;
top: calc(50% - 12px);
inset-inline-end: 12px;
/* Flex item at the right edge, aligned with the title. position: static cancels
the GLOBAL `.close { position: absolute; top: 5px }` rule (boardBody.css) that
otherwise pulls the X out of the header row and drops it below the title. */
position: static;
margin-inline-start: 8px;
font-size: 24px;
height: 24px;
line-height: 24px;
opacity: 1;
color: #000 !important; /* black X (override themed white link colour) */
}
section#notifications-drawer .header .close i.fa,
section#notifications-drawer .header .notification-menu-toggle i.fa {
color: inherit !important;
}
section#notifications-drawer ul.notifications {
+54 -3
View File
@@ -29,6 +29,16 @@
.sidebar .sidebar-content {
padding: 0 12px;
}
/* Sidebar text inputs (Filter List by Title, Filter by card title, advanced
filter, etc.) fill the padded content width, so there is equal empty space on
both sides (the .sidebar-content 12px padding) instead of the browser's
default narrow width that left a large gap on the right. box-sizing keeps the
border inside the width so it never overflows the panel. */
.sidebar .sidebar-content input[type="text"],
.sidebar .sidebar-content input[type="search"] {
width: 100%;
box-sizing: border-box;
}
.sidebar .sidebar-content .hide-btn {
display: none;
}
@@ -191,7 +201,12 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
}
.board-sidebar {
display: none;
width: 30vw;
/* Keep a sensible sidebar width at every screen size. A bare 30vw scaled with
the viewport, so on very wide screens it grew to ~double the intended width
while looking right only at mid widths; clamp keeps the mid-width look and
caps it (and floors it) instead of scaling without bound. Phones (<800px)
are handled by the 100vw rule in the media query below. */
width: clamp(320px, 30vw, 560px);
z-index: 100;
transition: top 0.1s, right 0.1s, width 0.1s;
}
@@ -203,6 +218,36 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
Restore/Delete links) sits unreachable UNDER the card window. */
z-index: 2002;
}
/* Right sidebar resize handle: a thin grab strip on the LEFT edge of the open
sidebar; drag left/right to change the sidebar width. Desktop only (hidden on
phones by the media query below, where the sidebar is full width). */
.sidebar-resize-handle {
display: none;
}
.board-sidebar.is-open .sidebar-resize-handle {
display: block;
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: -3px;
width: 6px;
cursor: col-resize;
z-index: 2003; /* above the open sidebar (2002) and its content */
background: transparent;
transition: background-color 0.2s ease;
}
.sidebar-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
.sidebar-resize-handle:active {
background: rgba(0, 123, 255, 0.6);
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
}
body.sidebar-resizing-active {
user-select: none;
cursor: col-resize;
}
.board-widget h4 {
margin: 5px 0;
}
@@ -305,10 +350,16 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
width: 100vw;
max-width: 100vw;
/* !important so a desktop drag-resize inline width never leaks into mobile,
where the sidebar must stay full width. */
width: 100vw !important;
max-width: 100vw !important;
z-index: 1000;
}
/* No dragging to resize on phones — the sidebar is full width there. */
.board-sidebar .sidebar-resize-handle {
display: none;
}
/* Normalise sidebar text on phones so the headings are not oversized and the
small text is not tiny — keep them close to the board's text size so board
and sidebar read at one consistent size. */
+7 -7
View File
@@ -1,5 +1,9 @@
template(name="sidebar")
.board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}} {{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
//- Drag the sidebar's LEFT edge to resize its width (desktop only; hidden on
mobile where the sidebar is full width). Saved to the user profile (or
localStorage for anonymous users on a public board).
.sidebar-resize-handle.js-sidebar-resize-handle.nodragscroll(title="{{_ 'drag-to-resize-sidebar'}}")
//a.sidebar-tongue.js-toggle-sidebar(
//
class="{{#if isTongueHidden}}is-hidden{{/if}}",
@@ -148,13 +152,9 @@ template(name="boardDomainRow")
text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= domain
template(name="boardChangeColorPopup")
.board-backgrounds-list.clearfix
each backgroundColors
.board-background-select.js-select-background
span.background-box(class="board-color-{{this}}")
span {{this}}
if isSelected
i.fa.fa-check
//- docs/Theme/Theme.md: category + theme dropdowns with custom color wheels for
the flat/clear categories, shared with Member Settings / Change Color.
+themeColorPicker(scope="board")
template(name="boardBackgroundsPopup")
.board-backgrounds-manage
+132 -20
View File
@@ -11,7 +11,6 @@ import Attachments from '/models/attachments';
import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerator';
import Integrations from '/models/integrations';
import Lists from '/models/lists';
import { BOARD_COLORS } from '/models/metadata/colors';
import { Filter } from '/client/lib/filter';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
@@ -139,7 +138,137 @@ Template.sidebar.onCreated(function() {
};
});
// ── Right sidebar drag-to-resize (desktop only) ──────────────────────────────
// Drag the sidebar's left edge to change its width, like resizing a spreadsheet
// column (mirrors the list resize in client/components/lists/list.js). The width
// is saved to the user profile for logged-in users, or browser localStorage for
// anonymous users on a public board. On phones the sidebar stays full width.
const MIN_SIDEBAR_WIDTH = 280;
const SIDEBAR_WIDTH_STORAGE_KEY = 'wekan-sidebar-width';
const isMobileSidebar = () => window.innerWidth <= 800;
const maxSidebarWidth = () =>
Math.max(MIN_SIDEBAR_WIDTH, Math.min(900, window.innerWidth - 80));
function readSavedSidebarWidth() {
const user = ReactiveCache.getCurrentUser();
if (user) return user.getSidebarWidth();
try {
const v = window.localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
return v ? parseInt(v, 10) : undefined;
} catch (e) {
return undefined;
}
}
function saveSidebarWidth(width) {
const user = ReactiveCache.getCurrentUser();
if (user) {
Meteor.call('setSidebarWidth', width, err => {
if (err) console.error('Error saving sidebar width:', err);
});
} else {
try {
window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(width));
} catch (e) {
/* localStorage unavailable (private mode) — width just isn't persisted */
}
}
}
function applySavedSidebarWidth($sidebar) {
// On phones the sidebar is full width (a !important CSS rule), so never set an
// inline width there; clear any leftover so shrinking desktop -> mobile works.
if (isMobileSidebar()) {
$sidebar[0].style.removeProperty('width');
return;
}
const w = readSavedSidebarWidth();
if (typeof w === 'number' && w >= MIN_SIDEBAR_WIDTH) {
$sidebar[0].style.width = `${Math.min(w, maxSidebarWidth())}px`;
} else {
$sidebar[0].style.removeProperty('width'); // fall back to the CSS clamp
}
}
Template.sidebar.onRendered(function() {
const tpl = this;
const $sidebar = tpl.$('.board-sidebar');
const $handle = tpl.$('.js-sidebar-resize-handle');
if (!$sidebar.length || !$handle.length) return;
// Apply the saved width when the current user (and thus the saved value)
// resolves, and re-apply on window resize so crossing the mobile breakpoint
// clears/restores the inline width.
tpl.autorun(() => {
ReactiveCache.getCurrentUser(); // reactive dependency on login state
applySavedSidebarWidth($sidebar);
});
const onWinResize = () => applySavedSidebarWidth($sidebar);
window.addEventListener('resize', onWinResize);
let isResizing = false;
let startX = 0;
let startWidth = 0;
let lastWidth = 0;
const getPageX = (e) => {
const oe = e.originalEvent || e;
if (oe.touches && oe.touches.length) return oe.touches[0].pageX;
if (oe.changedTouches && oe.changedTouches.length) return oe.changedTouches[0].pageX;
return e.pageX;
};
// Direction: the sidebar docks to the logical inline-end. In LTR that is the
// right, so its left-edge handle widens the panel when dragged left; RTL puts
// the panel on the left, mirroring the direction.
const isRtl = () =>
(document.documentElement.getAttribute('dir') || document.dir) === 'rtl';
const startResize = (e) => {
if (isMobileSidebar()) return; // no resize on phones
isResizing = true;
startX = getPageX(e);
startWidth = $sidebar.outerWidth();
lastWidth = startWidth;
$('body').addClass('sidebar-resizing-active');
e.preventDefault();
e.stopPropagation();
};
const doResize = (e) => {
if (!isResizing) return;
const delta = (startX - getPageX(e)) * (isRtl() ? -1 : 1);
const w = Math.max(MIN_SIDEBAR_WIDTH, Math.min(maxSidebarWidth(), startWidth + delta));
lastWidth = w;
$sidebar[0].style.width = `${w}px`;
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
isResizing = false;
$('body').removeClass('sidebar-resizing-active');
saveSidebarWidth(Math.round(lastWidth));
if (e && e.preventDefault) e.preventDefault();
};
$handle.on('mousedown', startResize);
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Native touch API so { passive: false } applies and preventDefault works.
$handle[0].addEventListener('touchstart', startResize, { passive: false });
document.addEventListener('touchmove', doResize, { passive: false });
document.addEventListener('touchend', stopResize, { passive: false });
tpl._sidebarResizeCleanup = () => {
window.removeEventListener('resize', onWinResize);
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
document.removeEventListener('touchmove', doResize);
document.removeEventListener('touchend', stopResize);
};
});
Template.sidebar.onDestroyed(function() {
if (this._sidebarResizeCleanup) this._sidebarResizeCleanup();
clearSidebarInstance(this);
Sidebar = null;
});
@@ -967,25 +1096,8 @@ function draggableMembersLabelsWidgets() {
Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
Template.boardChangeColorPopup.helpers({
backgroundColors() {
return BOARD_COLORS;
},
isSelected() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard.color === Template.currentData().toString();
},
});
Template.boardChangeColorPopup.events({
async 'click .js-select-background'(evt) {
evt.preventDefault();
evt.stopPropagation();
const currentBoard = Utils.getCurrentBoard();
const newColor = this.toString();
await currentBoard.setColor(newColor);
},
});
// docs/Theme/Theme.md: the board color popup now renders the shared themeColorPicker
// (scope="board"); its logic lives in client/components/main/themeColorPicker.js.
Template.boardChangeBackgroundImagePopup.events({
async submit(event, tpl) {
+8 -1
View File
@@ -67,8 +67,15 @@
padding: 0;
font-weight: bold;
min-height: 33px;
height: auto;
width: 100%;
overflow: hidden;
/* #6465: the gray swimlane header bar looked cut off at non-100% board zoom
(transform: scale on .board-wrapper). `overflow: hidden` clipped the header
to a slightly-too-short box once the scaled layout rounded, so the bar/text
appeared sliced. Let it grow to fit (height:auto + min-height floor) and not
clip vertically. Swimlane names are short, so dropping the ellipsis here is
fine; a long name wraps (word-wrap) instead of being cut. */
overflow: visible;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
word-wrap: break-word;
+62 -8
View File
@@ -15,12 +15,27 @@ template(name="swimlane")
each lists
+miniList(this)
else
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
//- #6465: no standing Add List column. Empty swimlane gets the inline
//- composer at the start; otherwise it opens after a list via its header
//- add-list button (see isAddListAfter).
if swimlaneHasNoLists
if currentUser.isBoardMember
unless currentUser.isCommentOnly
//- #6465: an empty swimlane shows only a + button; the create-list
//- form appears only after it is clicked. (Non-empty swimlanes use
//- the per-list-header add-list button instead.)
if isEmptyAddListOpen _id
+addListInline
else
.list.list-composer.js-list-composer.js-empty-add-list-button(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
a.open-list-composer.js-open-empty-add-list(title="{{_ 'add-list'}}" data-swimlane="{{_id}}")
i.fa.fa-plus
each lists
if visible this
+list(this)
if isAddListAfter _id
+addListInline
if currentCardIsInThisList _id ../_id
+cardDetails(currentCard)
@@ -38,14 +53,26 @@ template(name="listsGroup")
each lists
+miniList(this)
else
//- Issue #6142: allow adding a list in Lists (board-view-lists) mode too.
if defaultSwimlane
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm(defaultSwimlane)
//- #6465: Lists view — empty board gets the inline composer at the start;
//- otherwise it opens after a list via that list's header add-list button.
if boardHasNoLists
if defaultSwimlane
if currentUser.isBoardMember
unless currentUser.isCommentOnly
//- #6465: empty board shows only a + button; the create-list form
//- appears only after clicking it.
if isEmptyAddListOpen defaultSwimlane._id
+addListInline(defaultSwimlane)
else
.list.list-composer.js-list-composer.js-empty-add-list-button(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
a.open-list-composer.js-open-empty-add-list(title="{{_ 'add-list'}}" data-swimlane="{{defaultSwimlane._id}}")
i.fa.fa-plus
each lists
if visible this
+list(this)
if isAddListAfter _id
+addListInline(defaultSwimlane)
if currentCardIsInThisList _id null
+cardDetails(currentCard)
@@ -77,6 +104,33 @@ template(name="addListForm")
a.open-list-composer.js-open-inlined-form(title="{{_ 'add-list'}}")
i.fa.fa-plus
//- #6465: always-open inline Add List composer, rendered AFTER a list (right in
//- LTR / left in RTL, via the flex-row lists lane) when that list's header
//- add-list button is clicked, or at the start of an empty swimlane/board.
template(name="addListInline")
.list.list-composer.js-list-composer.js-add-list-inline(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
form.js-add-list-inline-form
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
//- #6465 follow-up: restore the previous "add after which list"
//- position selector, pre-selecting the list whose header button opened
//- this composer, so the position can still be chosen explicitly.
if swimlaneLists.length
| {{_ 'add-after-list'}}
select.list-position-input.full-line
each swimlaneLists
option(value="{{_id}}" selected="{{#if isDefaultAfterList _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.js-close-add-list-inline(title="{{_ 'close'}}")
i.fa.fa-times-thin
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
template(name="moveSwimlanePopup")
if currentUser
unless currentUser.isWorker
+162 -2
View File
@@ -6,6 +6,7 @@ import { CSSEvents } from '/client/lib/cssEvents';
import { Filter } from '/client/lib/filter';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import { defaultSwimlaneIdForBoard } from '/client/components/lists/listAddHelpers';
const { calculateIndex } = Utils;
@@ -71,11 +72,45 @@ function saveSorting(ui) {
sort: sortIndex.base,
};
// Check if the list was dropped in a different swimlane
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
// Check if the list was dropped in a different swimlane.
//
// #6484: only rebind a list to a swimlane when it was ALREADY swimlane-scoped
// (originalSwimlaneId set) and actually crossed into a DIFFERENT swimlane. A
// board-wide list has swimlaneId === null / getEffectiveSwimlaneId() === null
// and renders under EVERY swimlane (see filterCardsByListAndSwimlane and
// currentListIsInThisSwimlane: a null swimlaneId matches every swimlane). Since
// targetSwimlaneId is ALWAYS the swimlane the list is shown under in swimlanes
// view, the old `targetSwimlaneId !== originalSwimlaneId` was true for ANY nudge
// of a board-wide list, so it set list.swimlaneId AND moved every card to that
// one swimlane — the list then vanished from all the other swimlanes. Requiring
// originalSwimlaneId keeps board-wide lists board-wide; genuinely
// swimlane-scoped lists can still be moved between swimlanes.
const isDifferentSwimlane =
originalSwimlaneId &&
targetSwimlaneId &&
targetSwimlaneId !== originalSwimlaneId;
// If the list was dropped in a different swimlane, update the swimlaneId
if (isDifferentSwimlane) {
// #6478: moving a list to a DIFFERENT swimlane is high-impact — it re-homes
// every card in the list — and is easy to trigger accidentally on touch (the
// reporter merged lists this way and needed ~15 min to recover). Confirm on
// touch/small screens before applying; on decline, revert the drag by
// re-rendering from the server and abort. (Ctrl+Z can also undo it after the
// fact — see userPositionHistory.undoLast.)
if (
(Utils.isMiniScreen() || Utils.isTouchScreenOrShowDesktopDragHandles()) &&
typeof window !== 'undefined' &&
typeof window.confirm === 'function' &&
// eslint-disable-next-line no-alert
!window.confirm(TAPi18n.__('confirm-move-list-to-swimlane'))
) {
Meteor.subscribe('board', list.boardId, false);
$('.js-swimlane').each(function () {
$(this).addClass('dragscroll');
});
return;
}
updateData.swimlaneId = targetSwimlaneId;
// Move all cards in the list to the new swimlane
@@ -679,6 +714,24 @@ Template.swimlane.helpers({
}
return combined;
},
// #6465: the add-list composer is no longer a standing column. It opens AFTER a
// specific list when that list's header add-list button is clicked (the target
// list _id is kept in Session 'wekan-add-list-after'). Rendered after the list
// in the DOM, it appears to the RIGHT of the list in LTR and to the LEFT in RTL
// (the lists lane is a flex row that reverses under dir=rtl).
isAddListAfter(listId) {
return Session.get('wekan-add-list-after') === listId;
},
// Empty swimlane / empty board: there are no list headers to host the button,
// so offer a + button that reveals the inline composer only when clicked.
swimlaneHasNoLists() {
return !this.myLists || this.myLists().length === 0;
},
// #6465: is the empty-state create-list form open for this swimlane? (Set by the
// + button; until then only the button shows.)
isEmptyAddListOpen(swimlaneId) {
return Session.get('wekan-add-list-empty') === swimlaneId;
},
collapseSwimlane() {
return Utils.getSwimlaneCollapseState(this);
},
@@ -742,7 +795,19 @@ Template.swimlane.helpers({
},
});
// #6465: open the empty-state create-list form for a swimlane (shared by the
// Swimlanes and Lists views). Until clicked, an empty swimlane/board shows only the
// + button; this reveals the inline composer, scoped to the clicked swimlane.
function openEmptyAddList(event) {
event.preventDefault();
const swimlaneId = $(event.currentTarget).data('swimlane') || null;
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', swimlaneId);
Session.set('wekan-add-list-empty', swimlaneId);
}
Template.swimlane.events({
'click .js-open-empty-add-list': openEmptyAddList,
// Click-and-drag action
'mousedown .board-canvas'(evt, tpl) {
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
@@ -848,6 +913,84 @@ Template.addListForm.events({
'click .js-list-template': Popup.open('searchElement'),
});
// #6465: the inline add-list composer that opens after a specific list (or at the
// start of an empty swimlane). Its data context is the swimlane it creates the
// list in; the target list (for positioning) is Session 'wekan-add-list-after'.
Template.addListInline.onCreated(function () {
this.currentSwimlane = Template.currentData();
});
Template.addListInline.helpers({
// #6465 follow-up: the lists of the swimlane this composer opens in, so the
// "add after which list" position selector (restored) can be shown.
swimlaneLists() {
const swimlaneId =
Session.get('wekan-add-list-swimlane') ||
Template.instance().currentSwimlane?._id;
if (!swimlaneId) return [];
return ReactiveCache.getLists(
{ swimlaneId, archived: false },
{ sort: { sort: 1 } },
);
},
// Pre-select the list whose header add-list button opened this composer.
isDefaultAfterList(listId) {
return Session.get('wekan-add-list-after') === listId;
},
});
Template.addListInline.events({
async submit(evt, tpl) {
evt.preventDefault();
const titleInput = tpl.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
// Position: honour the explicit "add after list" selector when present
// (its options mirror the swimlane's lists, defaulting to the one whose
// header opened this composer), and derive nextListId from the following
// option so the new list is inserted between them. Fall back to the header
// button's target (Session) for the empty-board/start case.
const positionInput = tpl.find('.list-position-input');
let afterListId;
let nextListId = null;
if (positionInput && positionInput.value) {
afterListId = positionInput.value.trim();
const next = positionInput.options[positionInput.selectedIndex + 1];
nextListId = next ? next.value : null;
} else {
afterListId = Session.get('wekan-add-list-after') || null;
}
try {
await Meteor.callAsync('createListAfter', {
title,
boardId: Session.get('currentBoard'),
// The header button records the swimlane the list is displayed in; the
// empty-swimlane composer falls back to its own swimlane data context.
swimlaneId: Session.get('wekan-add-list-swimlane') || tpl.currentSwimlane?._id,
afterListId,
nextListId,
type: 'list',
});
titleInput.value = '';
titleInput.focus();
// Empty-state flow: the swimlane now has a list, so drop the empty-composer
// flag — if it becomes empty again the + button (not the form) shows first.
Session.set('wekan-add-list-empty', null);
} catch (error) {
console.error('Failed to create list:', error);
}
},
// Restore the "or template" option: create the list from a template.
'click .js-list-template': Popup.open('searchElement'),
'click .js-close-add-list-inline'() {
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', null);
Session.set('wekan-add-list-empty', null);
},
});
Template.listsGroup.helpers({
// Issue #6142: the add-list composer needs a swimlane as its data context
// (it creates the list in that swimlane). In Lists mode the listsGroup data
@@ -859,6 +1002,18 @@ Template.listsGroup.helpers({
if (!swimlaneId) return null;
return ReactiveCache.getSwimlane({ _id: swimlaneId });
},
// #6465: same add-list-after-a-list behaviour as the swimlanes view.
isAddListAfter(listId) {
return Session.get('wekan-add-list-after') === listId;
},
boardHasNoLists() {
const board = Template.currentData();
return !board || !board.lists || board.lists().length === 0;
},
// #6465: is the empty-board create-list form open? (Set by the + button.)
isEmptyAddListOpen(swimlaneId) {
return Session.get('wekan-add-list-empty') === swimlaneId;
},
currentCardIsInThisList(listId, swimlaneId) {
return currentCardIsInThisList(listId, swimlaneId);
},
@@ -883,6 +1038,11 @@ Template.listsGroup.helpers({
},
});
Template.listsGroup.events({
// #6465: empty board (Lists view) + button reveals the inline composer.
'click .js-open-empty-add-list': openEmptyAddList,
});
Template.listsGroup.onRendered(function () {
const tpl = this;
const boardBodyEl2 = tpl.firstNode?.parentElement?.closest?.('.board-body') || document.querySelector('.board-body');
+2 -2
View File
@@ -98,7 +98,7 @@ template(name="changeAvatarPopup")
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
br
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
button.full.primary.js-upload-avatar
i.fa.fa-upload
| {{_ 'upload-avatar' }}
@@ -137,7 +137,7 @@ template(name="adminChangeAvatarPopup")
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
br
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
button.full.primary.js-upload-avatar
i.fa.fa-upload
| {{_ 'upload-avatar' }}
+52 -4
View File
@@ -44,10 +44,7 @@ template(name="memberMenuPopup")
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
span {{_ 'archives'}}
li
a.js-notifications-drawer-toggle
i.fa.fa-bell
| {{_ 'notifications'}}
//- Notifications removed from the member menu: same as the header bell icon.
if currentSetting.customHelpLinkUrl
li
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
@@ -79,6 +76,14 @@ template(name="memberMenuPopup")
a.js-change-settings
i.fa.fa-cog
| {{_ 'change-settings'}}
li
a.js-change-color
i.fa.fa-paint-brush
| {{_ 'change-color'}}
li
a.js-change-font
i.fa.fa-font
| {{_ 'change-font'}}
li
a.js-change-avatar
i.fa.fa-picture-o
@@ -181,6 +186,43 @@ template(name="changeLanguagePopup")
| (RTL)
if isCurrentLanguage
i.fa.fa-check
template(name="changeColorPopup")
//- #5778: global theme color override, saved to the user profile. Works like
Board Settings / Change Color, but applies to the whole UI (All Boards, Search,
Admin Panel, etc). The shared picker (docs/Theme/Theme.md) offers category +
theme dropdowns and custom color wheels for flat/clear themes.
+themeColorPicker(scope="global")
template(name="changeFontPopup")
//- #4759: pick a UI font that ALREADY EXISTS in this browser. One button per
detected font, each shown in its OWN font (names are plain text, Blaze-escaped,
validated to a curated whitelist server-side). Font size is a row of buttons
(smaller on the left, 100% in the middle, larger on the right). Clicking a font
or size applies it IMMEDIATELY (no Save button).
form.change-font.edit-label
label {{_ 'font'}}
.ui-font-buttons
a.ui-font-btn.js-ui-font-btn(data-font="" class="{{#if isNoneSelected}}active{{/if}}") {{_ 'font-default'}}
each detectedFonts
a.ui-font-btn.js-ui-font-btn(data-font="{{name}}" style="{{optionStyle}}" class="{{#if selected}}active{{/if}}") {{name}}
//- Preview (pangram + numbers) right below the font-name buttons.
.font-preview(style=previewStyle) {{_ 'font-preview-text'}}
label {{_ 'font-size'}}
.ui-font-size-buttons
each fontSizes
a.ui-font-size-btn.js-ui-font-size-btn(data-size="{{key}}" class="{{#if selected}}active{{/if}}") {{label}}
//- #4759: text + text-background color from a color wheel, each unsettable.
//- Applied immediately when the wheel is committed / reset.
label {{_ 'text-color'}}
.color-wheel-wrap
input.js-ui-text-color(type="color" value=textColorHex title="{{_ 'text-color'}}")
a.js-reset-text-color(class="{{#unless hasTextColor}}disabled{{/unless}}") {{_ 'unset-color'}}
label {{_ 'text-background-color'}}
.color-wheel-wrap
input.js-ui-bg-color(type="color" value=bgColorHex title="{{_ 'text-background-color'}}")
a.js-reset-bg-color(class="{{#unless hasBgColor}}disabled{{/unless}}") {{_ 'unset-color'}}
template(name="changeSettingsPopup")
ul.pop-over-list
li
@@ -189,6 +231,12 @@ template(name="changeSettingsPopup")
| {{_ 'show-desktop-drag-handles'}}
if isShowDesktopDragHandles
i.fa.fa-check
li
a.js-toggle-submit-on-enter(title="{{_ 'submit-on-enter-description'}}")
i.fa.fa-level-down
| {{_ 'submit-on-enter'}}
if isSubmitOnEnter
i.fa.fa-check
unless currentUser.isWorker
li
label.bold.clear
+141 -3
View File
@@ -4,6 +4,9 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import AccountSettings from '/models/accountSettings';
import Users from '/models/users';
import { Utils } from '/client/lib/utils';
import { ReactiveVar } from 'meteor/reactive-var';
import { detectAvailableFonts } from '/client/lib/fontDetector';
import { fontFamilyValue, fontSizeValue, UI_FONT_SIZES, isHexColor6, colorValue } from '/models/lib/uiFonts';
Template.headerUserBar.events({
'click .js-open-header-member-menu': Popup.open('memberMenu'),
@@ -72,13 +75,12 @@ Template.memberMenuPopup.events({
'click .js-invite-people': Popup.open('invitePeople'),
'click .js-edit-profile': Popup.open('editProfile'),
'click .js-change-settings': Popup.open('changeSettings'),
'click .js-change-color': Popup.open('changeColor'),
'click .js-change-font': Popup.open('changeFont'),
'click .js-change-avatar': Popup.open('changeAvatar'),
'click .js-change-password': Popup.open('changePassword'),
'click .js-change-language': Popup.open('changeLanguage'),
'click .js-support': Popup.open('support'),
'click .js-notifications-drawer-toggle'() {
Session.set('showNotificationsDrawer', !Session.get('showNotificationsDrawer'));
},
'click .js-toggle-grey-icons'(event) {
event.preventDefault();
const currentUser = ReactiveCache.getCurrentUser();
@@ -320,6 +322,10 @@ Template.changeLanguagePopup.events({
});
Template.changeSettingsPopup.helpers({
isSubmitOnEnter() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser ? currentUser.hasSubmitOnEnter() : false;
},
rescueCardDescription() {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
@@ -379,6 +385,12 @@ Template.changeSettingsPopup.events({
window.localStorage.setItem('showDesktopDragHandles', 'true');
}
},
'click .js-toggle-submit-on-enter'() {
// Saved to the user profile (Member Settings only; requires a logged-in user).
if (ReactiveCache.getCurrentUser()) {
Meteor.call('toggleSubmitOnEnter');
}
},
'click .js-rescue-card-description'() {
Meteor.call('toggleRescueCardDescription')
},
@@ -413,3 +425,129 @@ Template.changeSettingsPopup.events({
Popup.back();
},
});
// #5778 + docs/Theme/Theme.md: the Member Menu / Change Color popup renders the
// shared themeColorPicker (scope="global"); all of its logic lives there.
// #4759: Member Menu / Font. A dropdown of fonts detected as installed in this
// browser; the value is stored to the profile and validated server-side. The first
// option unsets the custom font.
Template.changeFontPopup.onCreated(function () {
const user = ReactiveCache.getCurrentUser();
this.available = detectAvailableFonts(); // ordered subset of the curated whitelist
this.selected = new ReactiveVar((user && user.getUiFont && user.getUiFont()) || '');
this.selectedSize = new ReactiveVar((user && user.getUiFontSize && user.getUiFontSize()) || 'default');
// null = use default (unset); a hex string = custom color.
this.textColor = new ReactiveVar((user && user.getUiTextColor && user.getUiTextColor()) || null);
this.bgColor = new ReactiveVar((user && user.getUiTextBgColor && user.getUiTextBgColor()) || null);
});
Template.changeFontPopup.helpers({
detectedFonts() {
const tpl = Template.instance();
const sel = tpl.selected.get();
return tpl.available.map(name => ({
name,
selected: name === sel,
optionStyle: `font-family: "${name}", sans-serif;`,
}));
},
isNoneSelected() {
return !Template.instance().selected.get();
},
fontSizes() {
const cur = Template.instance().selectedSize.get();
// Ordered by percent so the buttons read: smaller ... 100% (default) ... larger.
return [...UI_FONT_SIZES].sort((a, b) => a.percent - b.percent).map(s => ({
key: s.key,
label: TAPi18n.__(`font-size-${s.key}`),
selected: s.key === cur,
}));
},
// Wheel <input type=color> needs a hex value even when unset — show a sensible
// default so the wheel opens somewhere reasonable.
textColorHex() {
return Template.instance().textColor.get() || '#000000';
},
bgColorHex() {
return Template.instance().bgColor.get() || '#ffffff';
},
hasTextColor() {
return !!Template.instance().textColor.get();
},
hasBgColor() {
return !!Template.instance().bgColor.get();
},
// Preview reflects the chosen font, size, text color and background color.
previewStyle() {
const tpl = Template.instance();
const family = fontFamilyValue(tpl.selected.get());
const size = fontSizeValue(tpl.selectedSize.get());
const color = colorValue(tpl.textColor.get());
const bg = colorValue(tpl.bgColor.get());
return [
family && `font-family: ${family};`,
size && `font-size: ${size};`,
color && `color: ${color};`,
bg && `background-color: ${bg};`,
].filter(Boolean).join('');
},
});
// Apply the current text/background colors immediately (no Save button).
function applyUiColors(tpl) {
Meteor.call('setUiColors', tpl.textColor.get(), tpl.bgColor.get(), err => {
if (err && process.env.DEBUG === 'true') console.error('setUiColors error', err);
});
}
Template.changeFontPopup.events({
// Click a font-name button -> apply that font immediately ('' = default/unset).
'click .js-ui-font-btn'(event, tpl) {
event.preventDefault();
const font = event.currentTarget.dataset.font || null;
tpl.selected.set(font || '');
Meteor.call('setUiFont', font, err => {
if (err && process.env.DEBUG === 'true') console.error('setUiFont error', err);
});
},
// Click a size button -> apply that size immediately.
'click .js-ui-font-size-btn'(event, tpl) {
event.preventDefault();
const size = event.currentTarget.dataset.size || 'default';
tpl.selectedSize.set(size);
Meteor.call('setUiFontSize', size, err => {
if (err && process.env.DEBUG === 'true') console.error('setUiFontSize error', err);
});
},
// Live-preview while dragging the wheel...
'input .js-ui-text-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.textColor.set(v);
},
'input .js-ui-bg-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.bgColor.set(v);
},
// ...and apply the color when the wheel is committed.
'change .js-ui-text-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.textColor.set(v);
applyUiColors(tpl);
},
'change .js-ui-bg-color'(event, tpl) {
const v = event.currentTarget.value;
if (isHexColor6(v)) tpl.bgColor.set(v);
applyUiColors(tpl);
},
'click .js-reset-text-color'(event, tpl) {
event.preventDefault();
tpl.textColor.set(null); // back to default
applyUiColors(tpl);
},
'click .js-reset-bg-color'(event, tpl) {
event.preventDefault();
tpl.bgColor.set(null);
applyUiColors(tpl);
},
});
+4
View File
@@ -5,6 +5,7 @@ import '/client/components/main/dueCards.jade';
import '/client/components/main/editor.jade';
import '/client/components/main/globalSearch.jade';
import '/client/components/main/header.jade';
import '/client/components/main/themeColorPicker.jade';
import '/client/components/main/keyboardShortcuts.jade';
import '/client/components/main/layouts.jade';
import '/client/components/main/myCards.jade';
@@ -29,6 +30,9 @@ import '/client/components/main/dueCards.js';
import '/client/components/main/editor.js';
import '/client/components/main/globalSearch.js';
import '/client/components/main/header.js';
import '/client/components/main/globalThemeColor.js';
import '/client/components/main/themeColorPicker.js';
import '/client/components/main/uiFont.js';
import '/client/components/main/layouts.js';
import '/client/components/main/myCards.js';
import '/client/components/main/popup.js';
+11
View File
@@ -185,6 +185,17 @@ class DateFilter {
_getMongoSelector() {
this._dep.depend();
// #6483: cards with NO due/end/received date store the field as null — that
// is exactly what the noDate filter matches ({dueAt: null}). A comparison
// filter such as Overdue/"past" is `{$lte: now}`, and `$lte` MATCHES null
// under FerretDB and minimongo (null sorts before dates; neither implements
// MongoDB's type-bracketing), so "Overdue" listed every card that has no due
// date at all. Require a real, non-null value for any comparison selector so
// only actual dates match. The noDate filter (this._filter === null) returns
// null unchanged and is unaffected.
if (this._filter && typeof this._filter === 'object') {
return { ...this._filter, $ne: null };
}
return this._filter;
}
+36
View File
@@ -0,0 +1,36 @@
import { UI_FONTS } from '/models/lib/uiFonts';
// #4759: detect which curated UI fonts are actually installed in THIS browser, using
// the canvas text-width comparison technique. A candidate font is "available" if
// rendering a probe string in it produces a different width than the generic base
// families — i.e. the browser actually had the font and didn't fall back.
//
// Returns an ordered subset of UI_FONTS. Purely a detection helper; the value the
// user picks is still validated server-side against the whitelist.
const BASE_FONTS = ['monospace', 'sans-serif', 'serif'];
const PROBE = 'mmmmmmmmmmlliWWWWWW';
const PROBE_SIZE = '72px';
export function detectAvailableFonts(candidates = UI_FONTS) {
let ctx;
try {
ctx = document.createElement('canvas').getContext('2d');
} catch (_) {
return [];
}
if (!ctx) return [];
const baseWidths = {};
BASE_FONTS.forEach(base => {
ctx.font = `${PROBE_SIZE} ${base}`;
baseWidths[base] = ctx.measureText(PROBE).width;
});
return candidates.filter(font =>
BASE_FONTS.some(base => {
ctx.font = `${PROBE_SIZE} "${font}", ${base}`;
return ctx.measureText(PROBE).width !== baseWidths[base];
}),
);
}
+6 -1
View File
@@ -16,6 +16,8 @@ import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { EscapeActions } from '/client/lib/escapeActions';
import { ReactiveCache } from '/imports/reactiveCache';
import { isSubmitKey } from '/models/lib/editorSubmitKey';
import {
openForm,
closeForm,
@@ -77,7 +79,10 @@ Template.inlinedForm.events({
openForm(currentlyOpenedForm, tpl);
},
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
// Ctrl/Cmd+Enter submits by default; a user who enabled "submit on Enter"
// in Member Settings submits on plain Enter (Shift+Enter for a newline).
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
if (isSubmitKey(evt, { submitOnEnter })) {
tpl.find('button[type=submit]').click();
}
},
+22
View File
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
@@ -307,6 +308,27 @@ hotkeys('-', archiveCard);
// https://github.com/wekan/wekan/pull/5589#issuecomment-2516776519
hotkeys('\xf7', archiveCard);
// #6478: Undo / Redo the caller's last position change (card / list / swimlane
// move) with Ctrl+Z / Ctrl+Y (also Cmd+Z / Cmd+Shift+Z on macOS). The hotkeys
// filter above disables shortcuts inside inputs/textareas/contentEditable, so
// native text undo/redo keeps working while typing. Only enabled for members who
// can modify the board.
function undoRedoLast(method) {
const boardId = Session.get('currentBoard');
if (!boardId || !Utils.canModifyBoard()) {
return;
}
Meteor.call(method, boardId, () => {});
}
hotkeys('ctrl+z, command+z', event => {
event.preventDefault();
undoRedoLast('userPositionHistory.undoLast');
});
hotkeys('ctrl+y, ctrl+shift+z, command+shift+z', event => {
event.preventDefault();
undoRedoLast('userPositionHistory.redoLast');
});
hotkeys('n', (event) => {
event.preventDefault();
const cardId = getSelectedCardId();
+10 -1
View File
@@ -136,7 +136,16 @@ window.Popup = new (class {
return function(evt, tpl) {
const context = (this.currentData && this.currentData()) || this;
context.__afterConfirmAction = action;
// #6479: store the pending confirm action on the Popup instance, NOT on the
// Blaze data context. The old code stashed it as a field on the context and
// the confirm button read it back as `this.__afterConfirmAction`; but the
// context is a ReactiveCache/Minimongo document (e.g. a board member sub-doc),
// and once Blaze re-renders the confirmation popup with a fresh/immutable copy
// of that context the field is gone, so clicking "Remove member" (and every
// other confirm dialog) silently did nothing. The instance field survives the
// re-render; the action is still invoked with the confirmation popup's own data
// context as `this`, so `this.userId` etc. keep working.
self._afterConfirmAction = action;
self.open(name).call(context, evt, tpl);
};
}
+2
View File
@@ -26,7 +26,9 @@ import '/client/components/import/import.css';
import '/client/components/lists/list.css';
import '/client/components/main/accessibility.css';
import '/client/components/main/brokenCards.css';
import '/client/components/main/customTheme.css';
import '/client/components/main/dueCards.css';
import '/client/components/main/uiFont.css';
import '/client/components/main/editor.css';
import '/client/components/main/fonts.css';
import '/client/components/main/globalSearch.css';
+83
View File
@@ -1,8 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { ReactiveCache } from '/imports/reactiveCache';
import { decideSandstormAutoOpen } from '/models/lib/sandstormAutoOpen';
import Settings from '/models/settings';
import { EscapeActions } from '/client/lib/escapeActions';
import { Filter } from '/client/lib/filter';
@@ -70,10 +72,91 @@ function renderBoardList(ctx, menu) {
});
}
// #2220: on the FIRST landing on '/' this session (i.e. right after login), send
// the user to their chosen default "home" board. Only once per session, so a
// later click on "All Boards" stays on the list, and the choice is honoured again
// on the next login / full reload. Returns true when it redirected.
function maybeRedirectToDefaultBoard() {
if (Session.get('defaultBoardRedirectDone')) return false;
Session.set('defaultBoardRedirectDone', true);
if (!Meteor.userId()) return false;
const user = ReactiveCache.getCurrentUser();
const boardId = user && user.getDefaultBoardId && user.getDefaultBoardId();
if (!boardId) return false;
// Redirect by id right away — at login the board's own subscription may not have
// loaded yet, so use its real slug when it happens to be cached and a harmless
// placeholder otherwise (the board route resolves by :id). If the board was
// deleted / access was removed, the board route degrades gracefully and the
// "All Boards" link still works (this only redirects once per session).
const board = ReactiveCache.getBoard(boardId);
FlowRouter.go('board', { id: boardId, slug: (board && board.slug) || 'board' });
return true;
}
// #2220 on Sandstorm: a grain historically opened straight into its single board.
// Restore that convenience — reactively, because a grain's login (Meteor.userId())
// and its boards subscription both arrive asynchronously after '/' first renders.
// Runs at most once per page load (grain session): a saved Home board wins; else
// exactly one board just opens (nothing saved — choosing a Home board is the separate
// explicit All Boards toggle); else (zero or many boards, nothing saved) we stay on
// the All Boards list. The decision itself is the pure, unit-tested
// decideSandstormAutoOpen(). Sandstorm-only (see the isSandstorm gate in the route).
let sandstormAutoOpenStarted = false;
function startSandstormAutoOpen() {
if (sandstormAutoOpenStarted) return;
sandstormAutoOpenStarted = true;
const boardsHandle = Meteor.subscribe('boards');
Tracker.autorun(computation => {
const userReady = !!Meteor.userId();
const user = userReady ? ReactiveCache.getCurrentUser() : null;
const savedDefaultId = user && user.getDefaultBoardId && user.getDefaultBoardId();
const boardsReady = boardsHandle.ready();
let boardCount = 0;
let onlyBoardId;
if (userReady && boardsReady) {
const boards = ReactiveCache.getBoards({
archived: false,
type: 'board',
'members.userId': Meteor.userId(),
});
boardCount = boards.length;
if (boardCount === 1) onlyBoardId = boards[0]._id;
}
const decision = decideSandstormAutoOpen({
userReady,
savedDefaultId,
boardsReady,
boardCount,
onlyBoardId,
});
if (decision.action === 'wait') return; // inputs not ready — keep watching
if (decision.action === 'redirect') {
// Just open the board — nothing is persisted here.
const board = ReactiveCache.getBoard(decision.boardId);
FlowRouter.go('board', { id: decision.boardId, slug: (board && board.slug) || 'board' });
}
computation.stop(); // decided (redirected or staying) — done for this page load
});
}
FlowRouter.route('/', {
name: 'home',
triggersEnter: [ensureSignedInUnlessSandstorm],
action() {
// On Sandstorm, render All Boards immediately and let the reactive auto-open
// (above) redirect once the grain login + boards have loaded, if appropriate.
if (isSandstorm) {
startSandstormAutoOpen();
renderBoardList(this, 'starred');
return;
}
if (maybeRedirectToDefaultBoard()) return;
renderBoardList(this, 'starred');
},
});
+6 -3
View File
@@ -1,8 +1,11 @@
[Sandstorm](../Platforms/FOSS/Sandstorm/Sandstorm.md) - [Sandstorm Backup](../Platforms/FOSS/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
# Backup Sandstorm
# Related
1. Please first backup your Sandstorm https://docs.sandstorm.io/en/latest/administering/backups/
2. Please first download all of your grains to .zip files from top menu row down arrow button, just in case.
[Transferring attachments from MongoDB to filesystem, and text from MongoDB to SQLite](https://github.com/wekan/minio-metadata)
# Upgrade Sandstorm WeKan
[Upgrade Sandstorm WeKan ](../Platforms/FOSS/Sandstorm/Sandstorm.md)
# Backup Docker
+152
View File
@@ -0,0 +1,152 @@
# Dev: Snap `core` base vs multi-platform builds
Developer notes on why the `ppc64el` and `s390x` snap legs of `release-all.yml`
fail, what the constraint actually is (verified from the action's source, not the
error string), and the only supported way to build these architectures for a
`core24` snap.
## TL;DR
- WeKan's `snapcraft.yaml` uses **`base: core24`**.
- The `snap-qemu` job builds `ppc64el` / `s390x` with
**`diddlesnaps/snapcraft-multiarch-action@v1`**, whose newest supported base is
**`core22`**. It rejects `core24` before any build starts.
- That action is unmaintained, and there is **no maintained QEMU multi-arch snap
action that supports `core24`**.
- The only `core24`-capable path for architectures that have **no native GitHub
runner** (`ppc64el`, `s390x`, `riscv64`) is **Launchpad `snapcraft remote-build`**
— already used for `riscv64` in the `snap-launchpad` job.
## The failure
Logs (`release-all.yml` → `snap-qemu` matrix legs `ppc64el` and `s390x`):
```
snapcraft.yaml is at version '10.01'. # version gate passed
Building Snapcraft project in "."...
##[error]Your build requires a base that this tool does not support (core24).
'base' or 'build-base' in your 'snapcraft.yaml' must be one of 'core', 'core18' or 'core20'.
```
The version check passes and QEMU sets up fine; the build dies the instant
snapcraft reads `base: core24`. The other two log lines are harmless noise:
- `Failed to save: Unable to reserve cache with key docker.io--tonistiigi--binfmt-...`
— a post-step cache warning from the QEMU binfmt image, not the cause.
- `Node.js 20 is deprecated ...` — the old action running on a newer Node runtime.
## Why it fails — verified from the action's source
This is **not** just the error message; it is a hardcoded allow-list. The action
is pinned at SHA `cfd7a246fad6bea65bb92f69a1c8d07898c231e5`. Its compiled
`dist/index.js` (from `src/lib/build.js` + `src/lib/channel-matrix.js`) hardcodes a
maximum base of `core22` in **three** independent places:
1. **Allow-list that throws** (`build.js`):
```js
const base = await detectBase(this.projectRoot);
if (!['core', 'core18', 'core20', 'core22'].includes(base)) {
throw new Error(`Your build requires a base that this tool does not support (${base}). ...`);
}
```
`core24` is not in the list → immediate throw. (The error *message* is itself
stale — it says "must be one of core/core18/core20" while the code actually also
allows `core22`.)
2. **Container image tag** (`build.js`):
```js
let containerImage = `diddledani/snapcraft:${base}`; // needs docker.io/diddledani/snapcraft:core24
```
It runs snapcraft inside `docker.io/diddledani/snapcraft:<base>` under QEMU. The
maintainer never published a `:core24` tag (tags stop at `core22`).
3. **Channel matrix** (`channel-matrix.js`):
```js
switch (base) {
case 'core22':
case 'core20': return channel;
case 'core18': ...
case 'core': ...
}
throw new Error(`Snapcraft Channel '${channel}' is unsupported ... '${base}' Base Snap.`);
```
`getChannel('core24', 'stable')` falls through every case and throws too.
**Maximum base supported by `diddlesnaps/snapcraft-multiarch-action@v1`: `core22`.**
The action was last updated for `core22` and abandoned; `core24` (and `core26`)
are not supported and will not be by `@v1`.
## Source repositories
The failing action and its build image:
| What | Repo |
|---|---|
| `snapcraft-multiarch-action` (the failing job) | https://github.com/diddlesnaps/snapcraft-multiarch-action — pinned [`cfd7a246`](https://github.com/diddlesnaps/snapcraft-multiarch-action/tree/cfd7a246fad6bea65bb92f69a1c8d07898c231e5); evidence in [`dist/index.js`](https://github.com/diddlesnaps/snapcraft-multiarch-action/blob/cfd7a246fad6bea65bb92f69a1c8d07898c231e5/dist/index.js) |
| `diddledani/snapcraft` Docker image (what it runs snapcraft inside) | https://github.com/diddledani/snapcraft — image: https://hub.docker.com/r/diddledani/snapcraft (no `core24` tag) |
snapcraft and the other actions used by the snap jobs:
| What | Repo |
|---|---|
| snapcraft (the tool) | https://github.com/canonical/snapcraft |
| `snapcore/action-build` (used by `snap-native`) | https://github.com/snapcore/action-build |
| `snapcore/action-publish` (used by `snap-native` / `snap-qemu`) | https://github.com/snapcore/action-publish |
| `docker/setup-qemu-action` (QEMU setup step) | https://github.com/docker/setup-qemu-action |
| `tonistiigi/binfmt` (the QEMU binfmt image in the cache warning) | https://github.com/tonistiigi/binfmt |
The base snaps (`core*`):
| Base | Repo |
|---|---|
| `core` | https://github.com/canonical/core |
| `core18` | https://github.com/canonical/core18 |
| `core20` | https://github.com/canonical/core20 |
| `core22` | https://github.com/snapcore/core22 |
| `core24` (WeKan's base) | https://github.com/canonical/core-base (mirror: https://github.com/snapcore/core-base) — the modern base snaps were consolidated into one `core-base` repo, so there is no standalone `canonical/core24`. |
## How the snap jobs are split in `release-all.yml`
| Job | Arches | Mechanism | `core24`? |
|---|---|---|---|
| `snap-native` | `amd64`, `arm64` | `snapcore/action-build` on a **native** runner | Yes |
| `snap-qemu` | `ppc64el`, `s390x` | `diddlesnaps/snapcraft-multiarch-action` under QEMU | **No — caps at core22** |
| `snap-launchpad` | `riscv64` | `snapcraft remote-build` on Launchpad | Yes |
`amd64` / `arm64` have native runners, so `action-build` works and is fastest.
`ppc64el` / `s390x` / `riscv64` have **no native GitHub runner**, and snapcraft
cannot cross-build a snap — so they need either QEMU emulation (which can't do
`core24`) or Launchpad.
## The fix
Because no QEMU multi-arch action supports `core24`, `ppc64el` and `s390x` cannot
stay in `snap-qemu`. Move them to Launchpad `remote-build` alongside `riscv64`:
- Add `ppc64el` and `s390x` to the `snap-launchpad` matrix.
- Delete the dead `snap-qemu` job.
The current `snap-launchpad` job is more robust than the older Launchpad legs that
were originally replaced by `snap-qemu`: it **requires the `.snap` file to exist,
retries the remote build 3×, and only uploads when the file is present**, which
guards the old "is not a valid file" (`snapcraft upload`, exit 64) failure.
### Historical note
`ppc64el` / `s390x` were once moved *off* Launchpad *onto* QEMU because the old
Launchpad legs ended in Launchpad state "Stopped" with no snap, then failed at
`snapcraft upload`. That escape route is now a dead end for `core24`, and the
hardened `snap-launchpad` job addresses the upload failure that prompted the move.
## If you ever go back to a QEMU-style multi-arch build
You would need a maintained action/image that ships a `core24`-capable snapcraft
(snapcraft 8.x) and a matching `core24` build container for the target arch. As of
this writing none exists as a drop-in; Canonical's supported answer for non-native
architectures on `core24` is Launchpad `remote-build`.
+302
View File
@@ -0,0 +1,302 @@
# Dev: WeKan for Nextcloud — building, publishing to the App Store, and integrations
What must be added to the WeKan GitHub repos so that WeKan's GitHub Actions can build a
**Nextcloud-deployable** WeKan (using WeKan's existing Docker image), how to **publish it
to the Nextcloud App Store**, and what **integrations** are possible (local AI, Deck,
SSO, email, …).
WeKan already publishes public Docker images from release-all.yml's `docker` job —
`wekanteam/wekan`, `quay.io/wekan/wekan`, `ghcr.io/wekan/wekan` — and already ships OIDC
support (`packages/wekan-oidc/`), both of which the Nextcloud paths below build on.
## Remaining steps
> **Status:** the `nextcloud` job **is already wired into `release-all.yml`** — guarded, it
> skips with a `::notice::` until the items below are done, so it never breaks a release.
1. **Create the ExApp repo `wekan/nextcloud`**: `appinfo/info.xml` with the
`<external-app>` block, the Node **AppAPI-handshake wrapper** (`/heartbeat`, `/enabled`,
`/init`, `AUTHORIZATION-APP-API` → WeKan user mapping), and a `Dockerfile` (WeKan +
embedded FerretDB SQLite). See §A1–A3.
2. **Get the app signing certificate**: generate a key + CSR, PR the CSR to
`nextcloud/app-certificate-requests`, receive `wekan.crt`; then **register** the app at
`apps.nextcloud.com/developer/apps/new`. See §B2–B3.
3. **Add secrets** `NEXTCLOUD_APP_PRIVATE_KEY`, `NEXTCLOUD_APP_PUBLIC_CRT`,
`NEXTCLOUD_APPSTORE_TOKEN`; **extend `WEKAN_REPO_TOKEN`** to `wekan/nextcloud`.
4. **Require HaRP** deploy daemon + Nextcloud 30/31+ (WebSocket/DDP) in the app docs; the
old proxy/Docker-Socket-Proxy can't carry WeKan's realtime.
5. Test-deploy the ExApp on a Nextcloud + HaRP instance: OIDC login, DDP realtime,
attachments, and an AI `taskprocessing` call.
**How to add a secret:** GitHub → the `wekan/wekan` repo → **Settings → Secrets and
variables → Actions → New repository secret**; or `gh secret set NAME --repo wekan/wekan`
(paste the value) / `gh secret set NAME --repo wekan/wekan < file`. For the private key:
`gh secret set NEXTCLOUD_APP_PRIVATE_KEY --repo wekan/wekan < ~/.nextcloud/certificates/wekan.key`.
---
## 0. Two ways to ship on Nextcloud — and which one fits WeKan
| Track | What it is | Runtime | Fits WeKan? |
|---|---|---|---|
| **Classic App Store app** | PHP app running **inside** the Nextcloud process | Nextcloud PHP | ❌ WeKan is Meteor/Node — can't run in PHP |
| **External App (ExApp) via AppAPI** | App backend runs as an **external Docker container**, managed by Nextcloud's **AppAPI** and a **Deploy Daemon** | any language / Docker | ✅ **This is WeKan's path** — it already has a Docker image |
**Key reality check:** AppAPI is **not a generic reverse proxy**. WeKan's current image
**will not deploy as-is** — the container must implement the **AppAPI handshake**. So
"use the WeKan Docker image at Nextcloud" means: build a thin **ExApp wrapper** around it,
add an `appinfo/info.xml` with an `<external-app>` block, and publish that to the App
Store (the image lives in a registry the app references).
---
## A. Building WeKan for Nextcloud (what to add to the repos)
### A1. A new repo: `wekan/nextcloud` (the ExApp)
An ExApp is a Nextcloud app folder + a container:
```
wekan-exapp/
├── appinfo/
│ └── info.xml # metadata + <external-app> (docker image + routes)
├── ex_app/
│ ├── lib/ # the AppAPI-handshake wrapper (Node) — see A2
│ └── src/ # optional Vue frontend (adds a WeKan TopMenu entry)
└── Dockerfile # builds the ExApp image (wrapper + WeKan + DB)
```
`appinfo/info.xml` — the distinguishing `<external-app>` block (scopes were removed in
AppAPI 3.2.0; access is per-route via `access_level`):
```xml
<external-app>
<docker-install>
<registry>ghcr.io</registry>
<image>wekan/wekan-exapp</image>
<image-tag>v10.00</image-tag>
</docker-install>
<routes>
<route>
<url>.*</url>
<verb>GET,POST,PUT,DELETE</verb>
<access_level>USER</access_level> <!-- PUBLIC | USER | ADMIN -->
<headers_to_exclude>[]</headers_to_exclude>
<bruteforce_protection>[401]</bruteforce_protection>
</route>
</routes>
</external-app>
```
### A2. The AppAPI-handshake wrapper (the real work — there is no Node ExApp SDK)
On deploy, AppAPI injects env vars (`APP_SECRET`, `APP_ID`, `APP_PORT`, `APP_HOST`,
`NEXTCLOUD_URL`, `APP_PERSISTENT_STORAGE`, …) and drives a lifecycle the container **must**
implement. Add a small Node/Express service (a sidecar, or routes fronting WeKan) that:
1. Serves **`GET /heartbeat`** → `{"status":"ok"}` (unauthenticated; polled up to ~90 s).
2. Handles **`PUT /enabled?enabled=1|0`** → registers/removes the WeKan menu entry,
returns `{"error":""}`.
3. Optionally **`POST /init`** → returns `{}` immediately, then reports progress `1..100`
via `PUT /ocs/v1.php/apps/app_api/ex-app/status`.
4. Binds WeKan on `APP_PORT`/`APP_HOST`, stores data under `APP_PERSISTENT_STORAGE`.
5. Trusts Nextcloud-proxied requests via the shared-secret headers
(`AUTHORIZATION-APP-API` = `base64(userid:secret)`, `EX-APP-ID`, `EX-APP-VERSION`,
`AA-VERSION`) and **maps the Nextcloud `userid` to a WeKan user** (JIT-provision).
6. To call back into Nextcloud, sends the same `AUTHORIZATION-APP-API` header.
Mirror the contract from the Python skeleton (`nextcloud/app-skeleton-python`) and the
Authentication/Deployment docs (§B). `nc_py_api` supplies this for Python; **Node has no
official SDK**, so it's a small custom service.
### A3. Two hard constraints for WeKan specifically
- **WebSockets/DDP:** WeKan's realtime uses DDP over WebSockets. The **old** AppAPI proxy
did **not** support WebSockets — you must use the **HaRP** deploy daemon (Nextcloud
30/31+), which routes directly to the ExApp and supports WebSockets. (The classic
**Docker Socket Proxy** daemon is deprecated, slated for removal in Nextcloud 35.)
- **Database:** AppAPI deploys a **single image**. WeKan's DB (FerretDB v1 SQLite, or
MongoDB) must be **bundled inside the ExApp image** (embedded `WEKAN_DB=ferretdb`, as
the snap does) or provided as an external service the wrapper points at — it can't be a
second compose service the way UCS does it. Embedding **FerretDB v1 SQLite** (from
[wekan/FerretDB](https://github.com/wekan/FerretDB)) with data under
`APP_PERSISTENT_STORAGE` is the clean fit.
### A4. `release-all.yml` — a new `nextcloud` job
After the `docker` job publishes the WeKan image, add a job that:
1. Checks out `wekan/nextcloud` (uses `WEKAN_REPO_TOKEN`).
2. Builds & pushes the **ExApp image** (`ghcr.io/wekan/wekan-exapp:v<version>`) — wrapper
+ WeKan bundle + embedded FerretDB.
3. Regenerates `appinfo/info.xml` with the new `<image-tag>` and `<version>`.
4. **Signs and uploads** the app release to the App Store (§B4).
### A5. Secrets/keys to add (`wekan/wekan`)
| Secret | New/existing | What it is |
|---|---|---|
| `NEXTCLOUD_APP_PRIVATE_KEY` | **new** | The app signing **private key** (`APP_ID.key`) |
| `NEXTCLOUD_APP_PUBLIC_CRT` | **new** | The Nextcloud-issued **certificate** (`APP_ID.crt`) |
| `NEXTCLOUD_APPSTORE_TOKEN` | **new** | App Store developer API token (upload releases) |
| `WEKAN_REPO_TOKEN` | **extend** | Add `wekan/nextcloud` to its repo scope |
| `GHCR_AUTH`/`DOCKERHUB_AUTH` | existing | Already push WeKan images; reuse for the ExApp image |
---
## B. Publishing an app to the Nextcloud App Store (files, build, publish, docs)
Same store (apps.nextcloud.com) for both tracks; an ExApp just adds the `<external-app>`
block and references a container image instead of shipping PHP.
### B1. Files to create
- **`appinfo/info.xml`** — mandatory: `<id>` (= folder name), `<name lang="en">`,
`<description lang="en">` (Markdown ok), `<version>` (semver), `<licence>` (SPDX,
British spelling), `<author>`, `<bugs>` (tracker URL),
`<dependencies><nextcloud min-version="30" max-version="35"/></dependencies>`.
Optional: `<summary>`, `<category>` (e.g. `organization`, `tools`, `integration`,
`workflow`, `ai`), `<screenshot>` (HTTPS), `<website>`, `<repository>`, `<donation>`.
For an ExApp, add the `<external-app>` block (§A1).
- **ExApp only:** `ex_app/` (backend + optional Vue frontend) and a **`Dockerfile`**.
### B2. Get a signing certificate (one-time)
```sh
mkdir -p ~/.nextcloud/certificates && cd ~/.nextcloud/certificates
openssl req -nodes -newkey rsa:4096 -keyout APP_ID.key -out APP_ID.csr -subj "/CN=APP_ID"
```
Submit `APP_ID.csr` as a PR to **`nextcloud/app-certificate-requests`**; when merged you
receive `APP_ID.crt`.
### B3. Register the app
At `https://apps.nextcloud.com/developer/apps/new`, providing the public cert and a
signature over the app id:
```sh
echo -n "APP_ID" | openssl dgst -sha512 -sign ~/.nextcloud/certificates/APP_ID.key | openssl base64
```
### B4. Build, sign, and upload a release
1. Package the app folder as `.tar.gz` (must contain `appinfo/info.xml`). **`krankerl`**
automates packaging/signing.
2. Sign the tarball:
```sh
openssl dgst -sha512 -sign ~/.nextcloud/certificates/APP_ID.key app.tar.gz | openssl base64
```
3. Upload — the store **fetches a download URL** (not a multipart upload):
```
POST https://apps.nextcloud.com/api/v1/apps/releases
{ "download": "https://…/app.tar.gz", "signature": "<base64>", "nightly": false }
```
A pre-release id in the version (e.g. `-alpha.1`) → **beta** channel; `nightly:true` →
**nightly**.
4. **CI:** the documented GitHub Actions path uses **`R0Wi/nextcloud-appstore-push-action`**
with secrets `APP_PRIVATE_KEY`, `APP_PUBLIC_CRT`, `APPSTORE_TOKEN` (community
alternative: `skjnldsv/publish-nextcloud-app`).
### B5. Documentation (verified)
| Topic | URL |
|---|---|
| AppAPI & External Apps (admin) | https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/AppAPIAndExternalApps.html |
| Managing Deploy Daemons | https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/ManagingDeployDaemons.html |
| Deploy Configurations (HaRP/DSP) | https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/DeployConfigurations.html |
| ExApp Overview (dev) | https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/development_overview/ExAppOverview.html |
| Deployment (`/heartbeat`, `/init`, env vars) | https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/tech_details/Deployment.html |
| Authentication (`AUTHORIZATION-APP-API`) | https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/tech_details/Authentication.html |
| Routes (`info.xml <routes>`) | https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/tech_details/api/routes.html |
| AppAPI source | https://github.com/nextcloud/app_api |
| HaRP daemon | https://github.com/nextcloud/HaRP |
| Python skeleton ExApp | https://github.com/nextcloud/app-skeleton-python |
| nc_py_api (Python framework) | https://github.com/cloud-py-api/nc_py_api |
| App Store Developer Guide | https://nextcloudappstore.readthedocs.io/en/latest/developer.html |
| Code signing | https://docs.nextcloud.com/server/latest/developer_manual/app_publishing_maintenance/code_signing.html |
| Publishing rules | https://docs.nextcloud.com/server/latest/developer_manual/app_publishing_maintenance/publishing.html |
| Release automation (GitHub Actions) | https://docs.nextcloud.com/server/latest/developer_manual/app_publishing_maintenance/release_automation.html |
| krankerl CLI | https://github.com/ChristophWurst/krankerl |
| app-certificate-requests | https://github.com/nextcloud/app-certificate-requests |
| Release upload API | https://apps.nextcloud.com/api/v1/apps/releases |
---
## C. Integrations possible on the Nextcloud platform
Two integration modes, and the distinction matters:
- **WeKan as an outbound OCS API client** (works today, WeKan stays external, no ExApp
needed): AI, Deck, Provisioning, WebDAV/CalDAV.
- **In-Nextcloud UI surfaces** (need a companion app / ExApp running inside Nextcloud):
Dashboard, Unified Search, Notifications, Talk bots, Flow.
### C1. Using Nextcloud local AI in WeKan — **achievable today, no ExApp required**
Since Nextcloud 30, the **Task Processing API** is the single unified AI entry point
(it supersedes the old Text Processing / Text-To-Image / Speech-To-Text APIs). It is
exposed over **OCS**, so WeKan's server can call it as an authenticated client:
```
POST /ocs/v2.php/taskprocessing/schedule
Headers: OCS-APIRequest: true ; Basic auth (user + app password)
Body: {"type":"core:text2text:summary","input":{...},"appId":"wekan"}
```
then poll for the result. The model runs **on the Nextcloud side** via a backend:
- **`llm2`** — fully local LLMs via llama.cpp (any GGUF model); rates **Green** on
Nextcloud's Ethical AI rating.
- **`integration_openai`** — any OpenAI-compatible engine (**LocalAI**, Ollama, vLLM…).
- **Context Chat** — RAG over the user's Nextcloud files.
Useful task types for cards: `core:text2text` (draft a description), `…:summary`
(summarize comments), `…:headline` (suggest a title), `…:translate`.
> WeKan does **not** need to be an ExApp to *use* the AI — it only needs an app-password
> and OCS. (Registering as an ExApp AI *provider* is a different, much larger thing and
> is not required here.)
### C2. Integration matrix
| Feature | Nextcloud mechanism | How WeKan uses it | Doc |
|---|---|---|---|
| **Local AI** | Task Processing API over OCS (`/ocs/v2.php/taskprocessing/schedule`), backend llm2/LocalAI | WeKan server summarizes/generates/translates card text | https://docs.nextcloud.com/server/stable/developer_manual/client_apis/OCS/ocs-taskprocessing-api.html |
| **Kanban interop (Deck)** | Deck REST/OCS API `/ocs/v2.php/apps/deck/api/v1.0/` | Import/sync Deck boards↔WeKan (Deck stacks→WeKan lists, cards→cards) | https://deck.readthedocs.io/en/latest/API/ |
| **SSO / login** | `user_oidc` (NC as OIDC client) or the `oidc` provider app (NC as IdP); LDAP; SAML | **WeKan already has OIDC** (`packages/wekan-oidc/`) — point it at Nextcloud's `oidc` provider for zero-code SSO | https://github.com/nextcloud/user_oidc |
| **User/group provisioning** | OCS Provisioning API `/ocs/v1.php/cloud/users` | Mirror Nextcloud users/groups into WeKan (JIT alongside OIDC) | https://docs.nextcloud.com/server/stable/admin_manual/configuration_user/user_provisioning_api.html |
| **Embed WeKan in NC UI** | **External Sites** app (iframe menu entry) | Add a "WeKan" top-menu item loading WeKan in an iframe (relax WeKan's `X-Frame-Options`/CSP for the NC origin) | https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/external_sites.html |
| **Files** | WebDAV `/remote.php/dav/files/USER/` | Attach/pull Nextcloud files onto cards | https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/index.html |
| **Calendar / due dates** | CalDAV `/remote.php/dav/` | Publish card due dates as a CalDAV calendar | https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/index.html |
| **Email** | Nextcloud **Mail** app (IMAP/SMTP client) + server SMTP | WeKan sends its own SMTP mail; NC Mail is a separate mailbox, not a relay WeKan drives | https://apps.nextcloud.com/apps/mail |
| **Dashboard / Search / Notifications / Talk bots / Flow** | Dashboard widget, Unified Search, Notifications, Talk bot, Flow APIs | Require an in-Nextcloud **companion app/ExApp** — not reachable from external WeKan by REST alone | https://docs.nextcloud.com/server/stable/developer_manual/ |
**Honest caveats:** SSO (OIDC) is the strongest, zero-code win. AI, Deck, Provisioning,
WebDAV/CalDAV work from external WeKan via REST. A Deck↔WeKan bridge is feasible but must
be built (none is maintained). Dashboard/Search/Notifications/Talk/Flow assume code
running *inside* Nextcloud.
---
## D. Comparison: kanban / project software for Nextcloud
| App | Type | Kanban features | API | AI | License | URL |
|---|---|---|---|---|---|---|
| **Deck** | Built-in native (PHP) | Full: boards, stacks, cards, labels, due dates, assignees, checklists, attachments, comments, sharing | Deck REST/OCS API; CalDAV for dates | Yes (in-platform Assistant/Task Processing) | AGPL-3.0 | https://apps.nextcloud.com/apps/deck |
| **Nextcloud Tables** | Built-in native | Kanban-*ish* views over a no-code database; not true drag-drop stacks | Tables REST API v1/v2 | Via platform Assistant | AGPL-3.0 | https://apps.nextcloud.com/apps/tables |
| **WeKan** | External (Meteor/Node) / ExApp | Full Trello-like: swimlanes, WIP limits, custom fields, rules, subtasks, templates — richer boards than Deck | Own REST API; consumes NC OCS APIs as a client | Can call NC Task Processing API as an OCS client (§C1) | MIT | https://github.com/wekan/wekan |
| Kanboard / Planka / Focalboard | External | Full kanban, own stacks | Own APIs | varies | varies (OSS) | integrate via SSO/iframe only |
**Bottom line:** **Deck is the only native, first-party kanban** for Nextcloud. Tables
offers board-like views but is a database app. Everything genuinely kanban (WeKan,
Kanboard, Planka, Focalboard) is a **separate app** that *integrates with* Nextcloud
(SSO/OIDC, iframe embed, OCS calls) rather than running as a native NC app.
---
## E. Checklist
- [ ] New repo `wekan/nextcloud` (ExApp): `appinfo/info.xml` + `<external-app>`, Node
AppAPI-handshake wrapper, Dockerfile (WeKan + embedded FerretDB SQLite)
- [ ] Obtain the app **signing certificate** (PR to `nextcloud/app-certificate-requests`)
and **register** the app at apps.nextcloud.com
- [ ] Add secrets `NEXTCLOUD_APP_PRIVATE_KEY`, `NEXTCLOUD_APP_PUBLIC_CRT`,
`NEXTCLOUD_APPSTORE_TOKEN`; extend `WEKAN_REPO_TOKEN` to `wekan/nextcloud`
- [ ] `release-all.yml`: add the `nextcloud` job (build ExApp image → regenerate
info.xml → sign → `POST /api/v1/apps/releases`)
- [ ] Require **HaRP** deploy daemon (WebSocket/DDP) and Nextcloud 30/31+ in the docs
- [ ] (Integrations) Document/point WeKan OIDC at Nextcloud `oidc` provider (SSO);
optionally add Task Processing AI calls and a Deck sync bridge
- [ ] Test: deploy the ExApp on a Nextcloud + HaRP instance; verify login (OIDC),
realtime (DDP over WebSocket), attachments, and an AI summarize call
## See also
- [UCS.md](UCS.md) — the same "Docker WeKan on a platform + migration" pattern for Univention.
- [Snap-Ondra-Gantt.md](Snap-Ondra-Gantt.md) — the "push to a downstream repo + publish from release-all.yml" pattern.
- [OS/Linux.md](OS/Linux.md) — Docker/Snap/Flatpak in the broader Linux update picture.
+24
View File
@@ -0,0 +1,24 @@
# Dev: IBM AIX install & update platforms (manual vs automatic)
AIX OS updates ship as **filesets** installed with `installp` (usually via the
`smit`/`smitty` menus). **NIM (Network Installation Management)** pushes installs/
updates from a master to clients. Open-source software comes as RPMs from the **AIX
Toolbox**, managed with **`dnf`** (modern replacement for `yum`). The Internet-update
tool is **SUMA**, which downloads fixes on a schedule from IBM.
> **SUMA (Service Update Management Assistant)** is AIX's automatic Internet
> *downloader* — cron-driven — but *applying* the fixes stays a separate admin/NIM step.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **`installp` + `smit`/`smitty`** (filesets) | ✓ | — | None | IBM | https://www.ibm.com/docs/en/aix | Proprietary |
| **NIM** (Network Installation Management) | ✓ | partial | Push installs/updates from NIM master (LAN, not Internet auto-refresh) | IBM | https://www.ibm.com/docs/en/aix/7.2.0?topic=management-managing-nim | Proprietary |
| **`dnf`/`yum`** (AIX Toolbox RPMs) | ✓ | — | Pulls from Toolbox repos on demand; no auto-refresh daemon | IBM (pkgs) / OSS (dnf) | https://www.ibm.com/support/pages/aix-toolbox-open-source-software-overview | GPL/OSS |
| **SUMA** (Service Update Management Assistant) | ✓ | **✓ (download)** | Scheduled task downloads TL/SP/security fixes over the Internet from `esupport.ibm.com`; cron-driven, e-mail notify | IBM | https://www.ibm.com/docs/en/aix/7.3.0?topic=suma-service-update-management-assistant | Proprietary |
## WeKan / Electron applicability
**No** for the desktop — no Electron/Chromium on AIX. Node.js has an official **AIX**
port, so the WeKan **server** side is feasible; the Electron client is not.
## See also
[Solaris.md](Solaris.md) · [HP-UX.md](HP-UX.md) · [OpenVMS.md](OpenVMS.md) · [zOS.md](zOS.md) · [Linux.md](Linux.md)
+54
View File
@@ -0,0 +1,54 @@
# Dev: Amiga-family install & update platforms (manual vs automatic, like Snap)
How software is installed and updated across the **Amiga** family — **AmigaOS 3.x**
(classic 68k), **AmigaOS 4.x** (PowerPC, Hyperion), **MorphOS** (PowerPC), and
**AROS** (open-source AmigaOS-compatible reimplementation) — split into **manual**
and **automatic** (background/self-updating, like Linux Snap auto-refresh).
> The Amiga family inherits a distribution culture from the classic 68k days:
> software ships as compressed archives (`.lha`, historically `.lzx`, sometimes
> `.zip`) downloaded from a central web archive, then unpacked and either dragged
> into place or installed via a bundled **Installer** script. This is fundamentally
> **manual** — the user is the update mechanism. Genuine auto-update tooling is rare
> and recent: **AmiUpdate** and **AMIStore's Updater** (AmigaOS 4) can check online
> and pull newer versions, and **Grunch** (MorphOS/AmigaOS) is the only tool in the
> family that behaves like a true auto-refreshing package manager. **AROS** and
> **classic AmigaOS 3.x** have no mainstream package manager and no auto-update.
---
## Comparison table
| Platform / OS | Manual | Automatic | Auto mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Aminet** (cross-platform archive) | ✓ | — | None — browse/search, download `.lha`/`.zip`, unpack + Installer by hand | Aminet community | https://aminet.net/ | Per-package (mixed) |
| **OS4Depot** (AmigaOS 4) | ✓ | — | None — manual `.lha` download + install | OS4Depot volunteers | https://www.os4depot.net/ | Per-package (mixed) |
| **MorphOS-Storage** (MorphOS) | ✓ | — | None — manual `.lha` download + install | MorphOS community | https://www.morphos-storage.net/ | Per-package (mixed) |
| **AROS Archives** (AROS) | ✓ | — | None — manual download + install; AROS has **no mainstream package manager** | AROS community | https://archives.arosworld.org/ | Per-package (mixed) |
| **AMIStore** (AmigaOS 4 app store) | ✓ | Partial | Store client + separate **Updater**: compares local vs remote versions, can auto-check on start and on a schedule (~every 4h), then user confirms download/install | A-EON Technology | http://www.amistore.net/ | Proprietary client |
| **AmiUpdate** (AmigaOS 4 online updater) | ✓ | Partial | Checks amiupdate.net for newer versions of OS components + registered apps, lists them, user selects; can auto-check but is user-triggered, not a silent daemon | Simon Archer (bundled with AmigaOS 4.1 from Update 6) | https://wiki.amigaos.net/wiki/AmigaOS_Updates | Bundled (proprietary) |
| **Grunch** (MorphOS + AmigaOS) | ✓ | **✓** | True package manager: search/download/install/**auto-update**/uninstall with dependency handling; update prompts can be disabled so installed software updates automatically (closest analogue to Snap auto-refresh) | "geit" (Guido Mersmann) | https://www.geit.de/eng_grunch.html | Freeware |
| **MorphOS system updates** | ✓ | — | None automatic — full/point OS releases distributed as downloadable images/installers; user installs manually | MorphOS Development Team | https://www.morphos-storage.net/ | Proprietary OS |
| **Classic AmigaOS 3.x drag-install** (68k) | ✓ | — | None — drag drawer, or run the Amiga **Installer** script; `.lha`/`.lzx` archives. No package manager, no auto-update | software by individual authors | https://aminet.net/ | Per-package (mixed) |
*AmiUpdate* and *AMIStore's Updater* are "Partial" because they can *check*
automatically/on schedule but still require the user to confirm the install — not
silent auto-appliers like Snap's `snapd`. **Grunch** is the single family tool that
can genuinely auto-update installed apps unattended. There is **no** legitimate
mainstream package manager for AROS or classic AmigaOS 3.x.
---
## WeKan / Electron applicability
**A WeKan Electron desktop app cannot run on any Amiga-family OS.** Electron bundles
Node.js + Chromium, and there is no port of Node or Chromium to 68k (AmigaOS 3.x), to
PowerPC AmigaOS 4 / MorphOS, or to AROS — these platforms lack the modern V8/Blink
toolchain, threading, and graphics stack Chromium requires. The only realistic way an
Amiga user reaches WeKan is via a **web browser pointed at a hosted WeKan server**
(and even then, heavy modern JavaScript strains these browsers) — never as a native
install, so none of the update mechanisms above apply to WeKan itself.
## See also
- Linux: [Linux.md](Linux.md) · Haiku: [Haiku.md](Haiku.md) · RISC OS: [RISC-OS.md](RISC-OS.md) · macOS: [Mac.md](Mac.md)
+23
View File
@@ -0,0 +1,23 @@
# Dev: Android install & update platforms (manual vs automatic, like Snap)
Android is the most pluralistic mobile OS: the default path is **Google Play**
background auto-update, but privileged alternative stores (**F-Droid**, **Aurora**) can
now auto-update too, and enterprise fleets use **Managed Google Play** push-installs.
**Google Play System Updates** (Project Mainline) patch OS modules without a full OTA.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Google Play Store** | ✓ | ✓ (default) | Background updates over Wi-Fi/any-network when idle+charging; global + per-app toggles | Google | https://support.google.com/googleplay/answer/113412 | Proprietary |
| **F-Droid + Privileged Extension** | ✓ | ✓ | Client polls repo index; Privileged Extension (or GrapheneOS/Calyx integration) installs updates silently (since 2025) | F-Droid | https://f-droid.org/packages/org.fdroid.fdroid.privileged/ | GPL/AGPL |
| **Aurora Store** | ✓ | ✓ | Anonymous/Google-account Play proxy; unattended updates via Android 12+ update-owner API | Aurora OSS | https://gitlab.com/AuroraOSS/AuroraStore | GPL-3.0 |
| **Sideloaded APK** | ✓ | — | User downloads `.apk`; no update channel | any source | https://developer.android.com/ | varies |
| **Play System Updates** (Mainline) | — | ✓ | APK/APEX modules delivered over Play infrastructure, no full OTA | Google/AOSP | https://source.android.com/docs/core/ota/modular-system | Apache-2.0 |
| **Managed Google Play / MDM** | — | ✓ | `autoInstallPolicy` / forced-install pushes and silently updates apps via EMM (Intune, Jamf…) | Google + EMM | https://support.google.com/work/android/answer/6137769 | Proprietary |
## WeKan applicability
WeKan is a **web/Meteor app** — native Electron desktop auto-update does not apply.
Ship WeKan as a **PWA** (installable via Chrome) or wrap it (**Bubblewrap/TWA**) for
Play/F-Droid/Aurora; the wrapper then rides normal store auto-update.
## See also
[iOS.md](iOS.md) · [ChromeOS.md](ChromeOS.md) · [Linux.md](Linux.md)
+24
View File
@@ -0,0 +1,24 @@
# Dev: OS/2 → ArcaOS install & update platforms (manual vs automatic)
**ArcaOS** (Arca Noae) is a proprietary OS built on IBM OS/2 Warp 4.52. Software
arrives over the Internet three ways: the netlabs **RPM/YUM** port, classic
**WarpIN** `.wpi` self-installers, and **ANPM** (a native GUI over yum/rpm). A paid
**Support & Maintenance** subscription adds a private YUM repo, but downloading/
applying is always user-initiated.
> **No Snap-style background auto-updater.** The closest is the user-run `yum`.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **RPM/YUM** (`yum update`) | ✓ | — | User-run yum pulls RPMs from remote repos | netlabs / bitwiseworks | https://trac.netlabs.org/rpm | GPL |
| **WarpIN** (`.wpi`) | ✓ | — | None — download `.wpi` and run | netlabs WarpIN | https://trac.netlabs.org/warpin | GPL (OSS) |
| **ANPM** (Arca Noae Package Manager) | ✓ | — | GUI over yum/rpm; queries remote repos on demand; also handles WarpIN | Arca Noae | https://www.arcanoae.com/wiki/anpm/ | GPLv3 |
| **Support & Maintenance subscription** | ✓ | — | Portal download (not background): reserved YUM repo + Download Center between ISO releases | Arca Noae, LLC | https://www.arcanoae.com/ | Proprietary service |
## WeKan / Electron applicability
Only theoretical — OS/2 has just old/limited Node ports and effectively no Electron, so
a modern WeKan desktop build would not run; at best a very old build might. Browser →
hosted server is the realistic path.
## See also
[ReactOS.md](ReactOS.md) · [FreeDOS.md](FreeDOS.md) · [Windows.md](Windows.md)
+77
View File
@@ -0,0 +1,77 @@
# Dev: BSD install & update platforms (manual vs automatic, like Snap)
How software and the base system are installed and updated on the **BSD** family —
**FreeBSD**, **OpenBSD**, **NetBSD**, **DragonFly BSD** — split into **manual** and
**automatic** (background/scheduled, like Linux Snap auto-refresh). WeKan's desktop
app is **Electron**; on BSD the realistic path is the **server** (via packages or the
Linux compat layer) or a browser.
> The BSDs cleanly separate **base system** (kernel + core userland, updated as a
> unit) from **third-party packages/ports**. None ships a Snap-style always-on
> background auto-refresh daemon; automation is done with **cron** + the native
> update tools. OpenBSD deliberately keeps updates manual/administrator-driven.
---
## FreeBSD
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkg** (binary packages) | ✓ | via cron | `pkg upgrade` on a cron/periodic job | FreeBSD | https://www.freebsd.org/ | BSD (OSS) |
| **Ports** (source) | ✓ | — | `portsnap`/git + `make` (manual builds) | FreeBSD | https://www.freebsd.org/ports/ | BSD (OSS) |
| **freebsd-update** (base) | ✓ | via cron | binary base updates; `freebsd-update cron` fetches on a timer, admin applies | FreeBSD | https://man.freebsd.org/freebsd-update | BSD (OSS) |
## OpenBSD
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkg_add** (packages) | ✓ | — | `pkg_add -u` (manual by design) | OpenBSD | https://www.openbsd.org/faq/faq15.html | BSD (OSS) |
| **syspatch** (base binary patches) | ✓ | via cron | official binary security patches; can be run from cron | OpenBSD | https://man.openbsd.org/syspatch | BSD (OSS) |
| **sysupgrade** (release upgrade) | ✓ | — | one-shot upgrade to next/-current release | OpenBSD | https://man.openbsd.org/sysupgrade | BSD (OSS) |
| **fw_update / pkg_add cron** | ✓ | via cron | firmware + package refresh scripted with cron | OpenBSD | https://man.openbsd.org/fw_update | BSD (OSS) |
## NetBSD
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkgin** (binary, over pkgsrc) | ✓ | via cron | `pkgin upgrade` on a cron job | NetBSD / pkgsrc | https://www.netbsd.org/ | BSD (OSS) |
| **pkgsrc** (source, also portable) | ✓ | — | `make` from the pkgsrc tree | The NetBSD Foundation | https://pkgsrc.org/ | BSD (OSS) |
| **sysupgrade / etcupdate + base sets** | ✓ | — | fetch base sets, extract, `etcupdate` | NetBSD | https://www.netbsd.org/docs/guide/en/chap-upgrading.html | BSD (OSS) |
## DragonFly BSD
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkg** (binary, DPorts) | ✓ | via cron | `pkg upgrade` on cron | DragonFly | https://www.dragonflybsd.org/ | BSD (OSS) |
| **DPorts** (source) | ✓ | — | source builds | DragonFly | https://www.dragonflybsd.org/docs/howtos/HowToDPorts/ | BSD (OSS) |
| **base upgrade** (`make buildworld`/git) | ✓ | — | rebuild base from source | DragonFly | https://www.dragonflybsd.org/docs/ | BSD (OSS) |
---
## Comparison at a glance
| BSD | Package auto-update | Base auto-update | How automation is done |
|---|:---:|:---:|---|
| FreeBSD | via cron (`pkg upgrade`) | `freebsd-update cron` (fetch) | cron / periodic(8) |
| OpenBSD | via cron (`pkg_add -u`) | `syspatch` via cron | cron; auto-apply discouraged upstream |
| NetBSD | via cron (`pkgin upgrade`) | manual base sets | cron |
| DragonFly | via cron (`pkg upgrade`) | manual (buildworld) | cron |
**No BSD has a Snap-style background auto-refresh daemon.** "Automatic" on BSD means
*you script the native tool with cron/periodic* — the fetch can be automatic, but
applying base updates is typically an explicit admin action (OpenBSD in particular
favors manual control).
---
## What this means for WeKan
- **No Electron desktop auto-update stack exists on BSD** (no Squirrel/Snap/AppImage
for BSD). A BSD user's realistic path is **WeKan server** — run it from a package /
from source, or via the **Linux compatibility layer** / a Linux VM / jail — and
update it with the OS's `pkg`/ports mechanism or by pulling a new build.
- For a desktop client, point a **browser** at a hosted WeKan server.
## See also
- Linux: [Linux.md](Linux.md) · macOS: [Mac.md](Mac.md) · Windows: [Windows.md](Windows.md)
+20
View File
@@ -0,0 +1,20 @@
# Dev: ChromeOS install & update platforms (manual vs automatic, like Snap)
ChromeOS has two update planes: **the OS itself** auto-updates silently in the
background (staged, A/B image swap, until the device's AUE date), and **apps** update
through their channels — Play Store Android apps, PWAs, and Crostini Linux `apt`. Legacy
Chrome Apps are being phased out (2025–2028) in favor of PWAs.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **ChromeOS OS auto-update** | (can defer) | ✓ | Silent staged A/B image download+swap over the Internet until Auto Update Expiration (AUE) | Google | https://support.google.com/chromebook/answer/177889 | Proprietary (Chromium OSS core) |
| **Play Store** (Android apps) | ✓ | ✓ | Same as Android Play background updates | Google | https://play.google.com/ | Proprietary |
| **PWAs / web apps** | ✓ (reinstall) | ✓ (content) | Service-worker/live web content refresh; not centrally version-managed | website owner | https://web.dev/progressive-web-apps/ | varies |
| **Linux (Crostini) apt** | ✓ | — | `apt update && apt upgrade` inside the Debian VM | Debian + Google | https://chromeos.dev/en/linux | GPL/varies |
## WeKan applicability
**The cleanest fit of any platform here** — install WeKan directly as a **PWA**; it then
tracks the live server. Electron desktop auto-update does not apply.
## See also
[Android.md](Android.md) · [iOS.md](iOS.md) · [Linux.md](Linux.md)
+21
View File
@@ -0,0 +1,21 @@
# Dev: FreeDOS install & update platforms (manual vs automatic)
**FreeDOS** is an open-source MS-DOS-compatible OS (1.3 series). Its network package
tooling is **FDNPKG** (and the 16-bit **fdnpkg16**), which install/update FreeDOS ZIP
packages from online HTTP/gopher repositories; **fdinst** is a local-only installer and
**fdimples** a curses front-end. Everything is user-run; **no auto-update**.
> FDNPKG's original author ended development at 0.99.7 (2020, code moved to SvarDOS);
> FreeDOS now ships/maintains **fdnpkg16**.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **FDNPKG / fdnpkg16** | ✓ | — | User-run; fetches ZIP packages from remote http/gopher repos (e.g. ibiblio.org FreeDOS repo) | Mateusz Viste (orig.); FreeDOS (fdnpkg16) | https://fdnpkg.sourceforge.net/ | MIT |
| **fdinst** | ✓ | — | None — local install/remove only, no networking | FreeDOS | http://help.fdos.org/en/hhstndrd/base/fdinst.htm | OSS |
## WeKan / Electron applicability
**No** — FreeDOS is 16-bit real-mode DOS; Node.js/Electron (and thus the WeKan desktop)
cannot run.
## See also
[ArcaOS.md](ArcaOS.md) · [Plan9.md](Plan9.md) · [Redox.md](Redox.md)
+21
View File
@@ -0,0 +1,21 @@
# Dev: HP-UX install & update platforms (manual vs automatic)
HP-UX uses **Software Distributor (SD-UX)**: `swinstall` installs from a **depot** (a
directory/server/media of filesets/products/bundles), `swlist` lists software, and
`swremove` uninstalls. OS fixes ship as **patch bundles** and individual patches,
applied with the same tooling. HP-UX 11i v3 is in long-term legacy status.
> **HP-UX has no automatic Internet update mechanism** — depots and patches are
> fetched and applied by an administrator.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Software Distributor** (`swinstall`/`swlist`/`swremove`), depots, patch bundles | ✓ | — | **None** — admin downloads depots/patches and runs `swinstall` | HPE | https://en.wikipedia.org/wiki/Software_Distributor | Proprietary |
## WeKan / Electron applicability
**No** — no modern Node.js or Electron/Chromium for HP-UX; neither the desktop client
nor the server realistically runs. A browser against a hosted WeKan server is the only
option.
## See also
[AIX.md](AIX.md) · [Solaris.md](Solaris.md) · [OpenVMS.md](OpenVMS.md) · [Linux.md](Linux.md)
+47
View File
@@ -0,0 +1,47 @@
# Dev: Haiku install & update platforms (manual vs automatic, like Snap)
How software is installed and updated on **Haiku** (the open-source, BeOS-inspired
OS), split into **manual** and **automatic** (background/self-updating, like Linux
Snap auto-refresh).
> Haiku uses a **package system built around read-only, mounted packages** rather
> than a file-scattering installer. Software ships as **`.hpkg`** files; when one is
> placed in `/system/packages` or `~/config/packages`, a virtual filesystem
> (**packagefs**) mounts its contents into the hierarchy on the fly, and a background
> **package daemon** activates it atomically (saving a rollback-able system "state").
> **Key point: Haiku has no scheduled/background *fetch* auto-update** — every update
> is user-initiated. Only the *activation* of an already-downloaded `.hpkg` is
> automatic. This is unlike Snap's `snapd` auto-refresh.
---
## Comparison table
| Platform | Manual | Automatic | Auto mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkgman** (CLI: `refresh`/`update`/`full-sync`/`install`) | ✓ | — | None — user runs it. (`update` fetches newer packages; system updates need a reboot.) | Haiku Project | https://www.haiku-os.org/guides/daily-tasks/updating-system/ | MIT |
| **HaikuDepot** (GUI software center) | ✓ | — | None — browse/install on demand; "Check for updates…" just launches SoftwareUpdater | Haiku Project | https://www.haiku-os.org/docs/userguide/en/applications/haikudepot.html | MIT |
| **SoftwareUpdater** (updater app) | ✓ | — | None — checks only when launched; no scheduling, no background scan, no notifications | Haiku Project | https://www.haiku-os.org/docs/userguide/en/applications/softwareupdater.html | MIT |
| **Manual `.hpkg` install** (drop into a packages dir) | ✓ | activation only | packagefs + `package_daemon` detect the new file and activate it automatically — but obtaining a newer file is fully manual | Haiku Project | https://www.haiku-os.org/docs/develop/packages/Infrastructure.html | MIT |
| **HaikuPorts + haikuporter** (dev publishing) | ✓ | — | None — dev writes a recipe, builds `.hpkg`, opens a GitHub PR; users then get it via the repo | HaikuPorts community | https://github.com/haikuports/haikuports | MIT (tooling) |
| **Plain zip / binary (unpackaged)** | ✓ | — | None — outside package management entirely | app vendor | (varies) | (varies) |
`pkgman` subcommands: `refresh` (update repo metadata), `update [pkg…]` (upgrade
installed packages), `full-sync` (bring system fully in line with repos — may
downgrade/remove), `install`, `search`, `uninstall`, `add-repo`/`drop-repo`. All are
on-demand.
---
## WeKan / Electron applicability
**The WeKan Electron desktop client cannot realistically run on Haiku.** There is no
Haiku port of Electron (upstream request closed **wontfix**; Electron ships only
Windows/macOS/Linux binaries). **Node.js has been ported to Haiku** (x86_64, in
HaikuDepot), so WeKan's *server* could conceivably run with effort — but the desktop
app cannot, and no auto-update path for it exists. The practical answer for a Haiku
user is to run WeKan as a **web app in a browser** against a hosted/self-hosted server.
## See also
- Linux: [Linux.md](Linux.md) · BSD: [BSD.md](BSD.md) · macOS: [Mac.md](Mac.md) · Windows: [Windows.md](Windows.md)
+18
View File
@@ -0,0 +1,18 @@
# Dev: HarmonyOS install & update platforms (manual vs automatic, like Snap)
**AppGallery** is Huawei's sole first-party store. **HarmonyOS NEXT** ("HarmonyOS 5")
dropped AOSP compatibility, so all apps are native **HAP** packages. Users with
auto-update enabled get phased/staged background rollouts (revised Aug 2025 to a fixed-
percentage 7-day auto-rollout).
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **AppGallery** | ✓ | ✓ | Background auto-update; phased release auto-covers auto-update-enabled users over 7 days | Huawei | https://appgallery.huawei.com/ | Proprietary |
| **Sideloaded HAP/APP** (dev) | ✓ | — | Manual install via DevEco/enterprise MDM; no consumer auto-update | Huawei tooling | https://developer.huawei.com/consumer/en/ | Proprietary |
## WeKan applicability
No practical native path — HarmonyOS NEXT PWA support is limited; use the **in-browser
web app** against a WeKan server.
## See also
[Android.md](Android.md) · [iOS.md](iOS.md)
+16
View File
@@ -0,0 +1,16 @@
# Dev: KaiOS install & update platforms (manual vs automatic, like Snap)
**KaiStore** is the sole store for these HTML5 feature-phone devices; the store client
self-updates silently in the background, and app updates are store-managed over the
Internet.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **KaiStore** | ✓ | ✓ | Background app updates; store client self-updates silently | KaiOS Technologies | https://www.kaiostech.com/store/ | Proprietary |
## WeKan applicability
**Not viable** — WeKan's UI is unsuited to feature-phone HTML5 constraints; no realistic
path.
## See also
[Android.md](Android.md) · [webOS.md](webOS.md) · [TV-OSes.md](TV-OSes.md)
+95
View File
@@ -0,0 +1,95 @@
# Dev: Linux install & update platforms (manual vs automatic, like Snap)
How software is installed and updated on **Linux**, split into **manual** (the user
runs a command / re-downloads) and **automatic** (background / scheduled /
self-updating). Snap is the reference point — this is the platform where Snap's
`snapd` auto-refresh actually lives. WeKan ships an official **Snap** and a **Docker**
image; the desktop app is **Electron**.
> Linux is the one platform with several *first-party* Snap-like auto-refresh stacks:
> **Snap** (`snapd`, auto-refresh by default), **Flatpak** (auto-update via GNOME
> Software / KDE Discover), and **AppImage + AppImageUpdate** (binary-delta
> self-update). Distro package managers (apt/dnf/pacman/zypper) are manual by default
> but trivially automated (`unattended-upgrades`, `dnf-automatic`).
---
## a) Manual update platforms
| Platform | Update step | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **apt / dpkg** (Debian, Ubuntu, Mint…) | `apt update && apt upgrade` | Debian | `.deb` | https://wiki.debian.org/Apt | GPL (OSS) |
| **dnf / rpm** (Fedora, RHEL, Rocky, Alma) | `dnf upgrade` | Red Hat / Fedora | `.rpm` | https://github.com/rpm-software-management/dnf | GPL (OSS) |
| **pacman** (Arch, Manjaro) | `pacman -Syu` | Arch Linux | pkg.tar.zst | https://wiki.archlinux.org/title/Pacman | GPL (OSS) |
| **zypper** (openSUSE, SLES) | `zypper update` / `zypper dup` | SUSE | `.rpm` | https://en.opensuse.org/SDB:Zypper_usage | GPL (OSS) |
| **apk** (Alpine) | `apk upgrade` | Alpine | `.apk` | https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper | GPL (OSS) |
| **portage / emerge** (Gentoo) | `emerge -uDN @world` | Gentoo | source ebuilds | https://wiki.gentoo.org/wiki/Portage | GPL (OSS) |
| **Nix** (NixOS, any distro) | bump inputs + `nixos-rebuild switch` | NixOS | Nix store | https://nixos.org/ | MIT (OSS) |
| **npm global** | `npm update -g` — CLI tools only | npm/GitHub | npm tarball | https://docs.npmjs.com/ | MIT (OSS) |
| **Raw tarball / `.run` / `make install`** | re-download, rebuild/replace | app author | source/binary | — | varies |
---
## b) Automatic update platforms (like Snap)
### Universal packaging with built-in auto-update (the true Snap-likes)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **Snap** | `snapd` **auto-refreshes** installed snaps in the background by default (4×/day, configurable; delta downloads; transactional, auto-rollback on failed refresh; channels stable/candidate/beta/edge). The closest thing to itself. | Canonical | `.snap` | https://snapcraft.io/ | GPL (OSS); proprietary store backend |
| **Flatpak** | Sandboxed apps from **Flathub**; auto-update via **GNOME Software** / **KDE Discover** (background check + install), or `flatpak update`. OSTree-based delta updates. | Flatpak / freedesktop | `.flatpak` | https://flatpak.org/ | LGPL (OSS) |
| **AppImage + AppImageUpdate** | A single-file `.app` that carries update info; **AppImageUpdate** (and libappimageupdate embedded in the app) does **binary-delta** self-update from a zsync feed. No system integration needed. | AppImage community | `.AppImage` | https://appimage.org/ · updater https://github.com/AppImageCommunity/AppImageUpdate | MIT (OSS) |
### Distro package managers in *automatic* mode
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **unattended-upgrades** (Debian/Ubuntu) | systemd timer auto-installs security (or all) `apt` updates in the background. | Debian | https://wiki.debian.org/UnattendedUpgrades | GPL (OSS) |
| **dnf-automatic** (Fedora/RHEL) | systemd timer that downloads/applies `dnf` updates automatically. | Red Hat | https://dnf.readthedocs.io/en/latest/automatic.html | GPL (OSS) |
| **PackageKit / GNOME Software / KDE Discover** | Desktop update stack: background checks and offline/auto updates for apt/dnf/flatpak. | freedesktop | https://www.freedesktop.org/software/PackageKit/ | GPL (OSS) |
| **transactional-update / rpm-ostree** (openSUSE MicroOS, Fedora Silverblue) | Atomic, auto-applied image updates with rollback (immutable distros). | SUSE / Fedora | https://coreos.github.io/rpm-ostree/ | GPL/Apache (OSS) |
### App-embedded self-updaters (Electron-relevant)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **electron-updater (electron-builder)** | On Linux, auto-update works for **AppImage** (via the built-in updater), and can generate deb/rpm; pulls from a feed (GitHub Releases, generic HTTPS). **No Squirrel on Linux** — AppImage is the auto-update target. | electron-userland | `.AppImage`/`.deb`/`.rpm` | https://www.electron.build/ | MIT (OSS) |
| **update.electronjs.org / Hazel / Nuts / Nucleus** | Feed servers for the Electron updater (cross-platform). | Electron / community | feed | https://github.com/electron/update.electronjs.org | MIT (OSS) |
> On Linux there is **no OS-level self-update for `.deb`/`.rpm` apps** outside the
> distro's own package manager — an app installed from a repo updates when the repo
> updates. For a self-contained auto-updating Electron build, ship an **AppImage**
> (electron-updater) or a **Snap**/**Flatpak** and let that stack auto-refresh.
---
## Comparison at a glance
| Platform | Manual | Automatic | Auto mechanism | For Electron app? |
|---|:---:|:---:|---|---|
| apt / dnf / pacman / zypper / apk | ✓ | via timer | unattended-upgrades / dnf-automatic | as repo pkg |
| portage / Nix | ✓ | — | manual/declarative | source |
| **Snap** | ✓ | ✓ | `snapd` auto-refresh (default) | ✓ **WeKan ships this** |
| **Flatpak** | ✓ | ✓ | GNOME Software / Discover | ✓ |
| **AppImage + AppImageUpdate** | ✓ | ✓ | zsync binary-delta self-update | ✓ (electron-updater) |
| electron-updater | — | ✓ | AppImage self-update / feed | ✓ recommended |
| unattended-upgrades / dnf-automatic | — | ✓ | systemd timer | as repo pkg |
| rpm-ostree / transactional-update | — | ✓ | atomic image + rollback | as flatpak |
---
## What this means for WeKan
- **WeKan already ships a Snap** — `snapd` auto-refresh gives Linux users Snap-like
background updates for free (this is the reference implementation). See the
multi-arch snap-build notes: [Snap-Core.md](../Forks/Snap-Core.md).
- **Desktop Electron auto-update on Linux:** ship an **AppImage** built by
electron-builder (electron-updater self-updates it), or a **Flatpak** on Flathub.
- **Server:** the normal path is **Docker** (pull a new image) or from source — outside
these desktop update frameworks.
## See also
- Windows: [Windows.md](Windows.md)
- macOS: [Mac.md](Mac.md)
- BSD: [BSD.md](BSD.md)
+140
View File
@@ -0,0 +1,140 @@
# Dev: macOS install & update platforms (manual vs automatic, like Snap)
A developer reference of **every practical way a macOS application can be installed
and updated**, split by whether updates are **manual** (the user re-downloads and
re-runs an installer / re-drags the `.app`) or **automatic** (background /
self-updating, the way a Linux **Snap** auto-refreshes). The purpose is choosing a
path for **automatic WeKan updates on Mac**. WeKan desktop is **Electron**, so both
app-embedded updaters and store/MDM mechanisms are covered.
> macOS, like Windows, has no single first-party equivalent of Snap's transactional,
> store-driven, delta auto-refresh for arbitrary apps. The closest options are the
> **Mac App Store (MAS)** (store-managed background auto-update, but sandboxed +
> reviewed) and, for self-distribution, the **Sparkle** framework or Electron's
> **Squirrel.Mac**-based `autoUpdater`. Everything else is a package manager, an
> MDM/managed-software stack, or manual drag-install.
---
## a) Manual update platforms
The user (or an admin) explicitly fetches and applies each new version.
| Platform | What it is / update step | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **`.dmg` / `.app` drag-install** | Mount the disk image, drag the `.app` to `/Applications`. "Update" = re-download and replace. No update mechanism of its own. | app author | `.dmg`, `.app` | — (Apple docs) | n/a |
| **`.pkg` installer** | Apple's `Installer.app` / `installer(8)` flat package. Update = run a new `.pkg`. Supports pre/post scripts; no self-update. | app author (Apple format) | `.pkg` | https://support.apple.com/guide/deployment/ | n/a |
| **Homebrew + Homebrew Cask** | The de-facto macOS package manager. `brew upgrade` (formulae) / `brew upgrade --cask` (GUI apps). **Manual by default**; automatable (see Homebrew Autoupdate below). | Homebrew (OSS) | formulae, `.app`/`.pkg`/`.dmg` casks | https://brew.sh/ | BSD-2 (OSS) |
| **MacPorts** | Ports-tree package manager. `sudo port selfupdate && sudo port upgrade outdated`. Manual only, no background service. | MacPorts (OSS) | source/binary ports | https://www.macports.org/ | BSD-3 (OSS) |
| **Nix / nix-darwin** | Declarative package/system manager. Update = bump inputs and `darwin-rebuild switch`. Reproducible; manual apply. | NixOS community (OSS) | Nix store | https://github.com/LnL7/nix-darwin | MIT (OSS) |
| **mas-cli** | CLI for the Mac App Store. `mas outdated` / `mas upgrade` — manual trigger of MAS updates from the terminal. | mas-cli (OSS) | MAS apps | https://github.com/mas-cli/mas | MIT (OSS) |
| **npm global** (`npm i -g`) | `npm update -g` — **CLI tools only**, not GUI apps. No auto-update. | npm / GitHub | npm tarball | https://docs.npmjs.com/ | MIT (OSS) |
---
## b) Automatic update platforms (like Snap)
These refresh in the background, on launch, on a schedule, or store-managed —
without the user re-downloading anything.
### Store / OS-managed (closest to Snap)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **Mac App Store (MAS)** | App Store client silently downloads & installs new versions in the background (System Settings → General → Software Update / App Store → *Automatic Updates*). The nearest macOS analogue to Snap's store-driven auto-refresh. Requires **sandboxing**, **notarization**, and **App Review**. | Apple | `.pkg`/`.app` (MAS receipt) | https://www.apple.com/app-store/ | Proprietary |
| **Setapp** | Subscription "app store" with an always-running client that **auto-updates** its catalog apps in the background. macOS (and iOS) — a real Snap-like managed-update experience, but a curated commercial catalog you must be accepted into. | MacPaw | client-managed | https://setapp.com/ | Proprietary (subscription) |
### App-embedded self-updaters (best fit for an Electron app like WeKan desktop)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **Sparkle** | The de-facto standard self-updater for non-MAS macOS apps. App embeds the framework and points at an **appcast** (RSS/XML); Sparkle checks on a schedule/launch, shows release notes, downloads (supports **delta** updates), verifies **EdDSA (Ed25519)** signatures, and installs on quit. | Sparkle project (OSS) | `.dmg`/`.zip`/`.pkg` via appcast | https://sparkle-project.org/ · repo https://github.com/sparkle-project/Sparkle | MIT (OSS) |
| **Squirrel.Mac** | The updater behind Electron's built-in `autoUpdater` on macOS. Reads a JSON feed, downloads a `.zip` in the background, installs on restart. **Requires the app be code-signed** (and notarized). | Squirrel project (OSS) | `.zip` + JSON feed | https://github.com/Squirrel/Squirrel.Mac | MIT (OSS) |
| **Electron `autoUpdater` (built-in)** | Wraps Squirrel.Mac on macOS. Needs an update **server/feed**; background download, install on restart. | OpenJS / Electron | Squirrel feed | https://www.electronjs.org/docs/latest/api/auto-updater | MIT (OSS) |
| **electron-updater (electron-builder)** | The most common Electron auto-updater. On macOS pulls a signed+notarized `.zip` + `latest-mac.yml` from a feed (**GitHub Releases**, generic HTTPS, S3, …), downloads in background, installs on quit. Uses Squirrel.Mac under the hood. | electron-userland (OSS) | `.zip`/`.dmg` + `latest-mac.yml` | https://www.electron.build/ | MIT (OSS) |
| **update.electronjs.org** | Free hosted update server for **open-source** Electron apps published to public **GitHub Releases**; serves the Squirrel.Mac feed. Requires code-signing (mandatory on macOS). | Electron project | Squirrel feed | https://github.com/electron/update.electronjs.org | MIT (OSS) |
| **Hazel / Nuts / Nucleus** | Self-hosted update servers that serve the Squirrel/electron-updater feed (GitHub Releases proxy or static). Cross-platform (Mac + Win). Dated (2018–2021). | Vercel / GitBook / Atlassian (OSS) | Squirrel feed | https://github.com/vercel/hazel | MIT/Apache (OSS) |
| **Google Keystone** | The launchd-based background updater behind Google Chrome on macOS (the Mac analogue of Google Omaha). Silent, always-on. Not distributable/reusable as a library — noted for completeness, **not recommended**. | Google | `.pkg` via Keystone | https://support.google.com/chrome/a/answer/7591084 | Proprietary |
> **macOS gotcha for Electron:** on macOS, `autoUpdater` / Squirrel.Mac **only works
> if the app is Developer-ID code-signed *and* notarized** — an unsigned build cannot
> self-update. Budget for an Apple Developer account ($99/yr), signing, and
> notarization in any auto-update plan.
### Package managers in *automatic* mode
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **Homebrew Autoupdate** | A tap that installs a **launchd** job running `brew update` + `brew upgrade` (incl. `--cask`) on an interval — turns Homebrew into a Snap-like background refresh. | DomT4 (OSS) | https://github.com/DomT4/homebrew-autoupdate | BSD-2 (OSS) |
| **mas + launchd / cron** | Schedule `mas upgrade` to auto-apply Mac App Store updates from a script. | community | https://github.com/mas-cli/mas | MIT (OSS) |
### Managed / MDM (admin-driven automatic deployment & patching)
The macOS analogue of Windows Intune/SCCM/PDQ — the most Snap-like *fleet* auto-update.
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **Munki** | Open-source managed-software system: clients poll a repo (catalogs/manifests) and **auto-install/upgrade** packages in the background. The closest open-source, self-hosted "Snap-like" experience on macOS. | Greg Neagle / community | https://www.munki.org/ · https://github.com/munki/munki | Apache-2.0 (OSS) |
| **AutoPkg** | Automates downloading + packaging the latest version of apps into recipes; feeds Munki/Jamf so downstream auto-update stays current. | AutoPkg community (OSS) | https://github.com/autopkg/autopkg | Apache-2.0 (OSS) |
| **Installomator** | Shell script that finds, downloads and installs the **latest** version of 700+ apps; commonly triggered by an MDM to keep apps patched. | Installomator community (OSS) | https://github.com/Installomator/Installomator | MIT (OSS) |
| **Jamf Pro / Jamf Now** | The market-leading Apple MDM: policies auto-deploy and patch apps across a fleet on a schedule. | Jamf (commercial) | https://www.jamf.com/ | Proprietary (paid) |
| **Kandji** | Apple MDM with "Auto Apps" — a curated catalog that **auto-updates** managed apps in the background. | Kandji (commercial) | https://www.kandji.io/ | Proprietary (paid) |
| **Mosyle** | Apple MDM with automated app deployment/patching (incl. self-service). | Mosyle (commercial) | https://mosyle.com/ | Proprietary (paid) |
| **Addigy** | Cloud Apple MDM with scheduled software/patch policies. | Addigy (commercial) | https://addigy.com/ | Proprietary (paid) |
| **Fleet** | Open-source osquery-based device management; software deployment + updates across macOS fleets. | Fleet (OSS + paid) | https://fleetdm.com/ | MIT core (OSS) + paid |
| **Microsoft Intune (macOS)** | Cloud MDM: assign macOS apps (`.pkg`/`.dmg`/VPP/Store) and push updates to enrolled Macs. | Microsoft | https://learn.microsoft.com/en-us/mem/intune/ | Proprietary (subscription) |
| **`softwareupdate` / DDM** | Apple's built-in CLI (`softwareupdate`) and **Declarative Device Management** for enforcing **OS** updates on a schedule — governs macOS itself, not your app (context). | Apple | https://support.apple.com/guide/deployment/ | Incl. with macOS |
---
## Comparison at a glance
| Platform | Manual | Automatic | Auto mechanism | For Electron app? |
|---|:---:|:---:|---|---|
| `.dmg` / `.app` drag-install | ✓ | — | none | packaging only |
| `.pkg` installer | ✓ | — | none | wrapper only |
| Homebrew + Cask | ✓ | via Autoupdate | launchd `brew upgrade` | distribution (cask) |
| MacPorts | ✓ | — | (manual by design) | distribution |
| Nix / nix-darwin | ✓ | — | declarative apply | distribution |
| mas-cli | ✓ | via launchd | scheduled `mas upgrade` | MAS apps |
| npm global | ✓ | — | none | CLI tools only |
| **Mac App Store** | ✓ | ✓ | Store client, background | ✓ (sandboxed, review) |
| **Setapp** | — | ✓ | client agent, background | ✓ (curated catalog) |
| **Sparkle** | — | ✓ | appcast poll + delta, install on quit | native / non-Electron |
| **Squirrel.Mac** | — | ✓ | JSON feed, bg zip, install on restart | **✓ built-in** |
| **electron-updater** | — | ✓ | `latest-mac.yml` feed, install on quit | **✓ recommended** |
| update.electronjs.org / Hazel / Nuts / Nucleus | — | ✓ | update feed server | ✓ (feed backends) |
| Google Keystone | — | ✓ | launchd background agent | ✗ (not reusable) |
| Homebrew Autoupdate | — | ✓ | launchd schedule | ✓ (if shipped as cask) |
| **Munki** | — | ✓ | client polls repo, bg install | ✓ (as pkg) — Snap-like fleet |
| AutoPkg / Installomator | — | ✓ | recipe/script feeds MDM | ✓ (as pkg) |
| Jamf / Kandji / Mosyle / Addigy / Fleet / Intune | — | ✓ | MDM push / schedule | ✓ (as pkg/VPP) |
| `softwareupdate` / DDM | — | ✓ | OS-update schedule (OS only) | n/a |
---
## What this means for WeKan (Electron on Mac)
- **Recommended self-update path:** **electron-builder + electron-updater** with a
**code-signed + notarized** `.zip` + `latest-mac.yml` served from a static HTTPS
feed or **GitHub Releases**. Same tooling as the Windows path, background download,
install on quit — the closest single-app analogue to Snap auto-refresh. (Signing +
notarization are mandatory on macOS.)
- **Zero-infrastructure alternative:** **update.electronjs.org + `update-electron-app`**,
since WeKan is open-source on public GitHub — rides Squirrel.Mac, still needs signing.
- **Store option:** the **Mac App Store** gives store-managed auto-update for free, but
Electron + MAS is painful (sandbox entitlements, review) and generally not worth it
for a server-companion desktop app.
- **Broadest passive reach:** ship a **Homebrew Cask**; users `brew upgrade --cask`,
or opt into **Homebrew Autoupdate** for background refresh.
- **Managed fleets:** **Munki** (open-source, self-hosted, the most Snap-like) or a
commercial Apple MDM (**Jamf / Kandji / Mosyle / Addigy / Intune**), typically fed by
**AutoPkg** / **Installomator** to stay current.
- **WeKan server on Mac** is normally run via Docker or from source, so its "updates"
are pulling a new image / rebuilding — outside these desktop update frameworks.
## See also
- Windows equivalent: [Windows.md](Windows.md)
- Linux Snap auto-refresh & multi-arch snap builds: [Snap-Core.md](../Forks/Snap-Core.md)
- macOS install docs: [Mac.md](../../../Platforms/Propietary/Mac/Mac.md)
+23
View File
@@ -0,0 +1,23 @@
# Dev: OpenVMS install & update platforms (manual vs automatic)
Modern **VSI OpenVMS** (x86-64 / Alpha / Itanium) uses **PCSI (POLYCENTER Software
Installation utility)**: `PRODUCT INSTALL` installs/manages `.PCSI` layered-product
kits, and updates ship as **ECO kits** (patch kits). Legacy products still use
**VMSINSTAL** (installs from savesets). Service updates are downloaded manually as
signed ZIPs from the VMS Software portal, unpacked, and installed with `PRODUCT INSTALL`.
> **OpenVMS has no automatic Internet update** — everything is administrator-initiated.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **PCSI** (`PRODUCT INSTALL`), ECO/patch kits | ✓ | — | **None** — ECO kits downloaded from the VMS Software portal, installed by hand | VMS Software, Inc. (VSI) | https://docs.vmssoftware.com/vsi-openvms-polycenter-software-installation-utility-manual/ | Proprietary |
| **VMSINSTAL** (legacy savesets) | ✓ | — | None | VSI | https://wiki.vmssoftware.com/VMSINSTAL | Proprietary |
Download/service portal: https://vmssoftware.com (support: https://sp.vmssoftware.com).
## WeKan / Electron applicability
**No** for the desktop — no Electron/Chromium. VSI ships a **Node.js** port, so the
WeKan **server** side is conceivable; the Electron client is not.
## See also
[AIX.md](AIX.md) · [Solaris.md](Solaris.md) · [HP-UX.md](HP-UX.md) · [zOS.md](zOS.md)
+19
View File
@@ -0,0 +1,19 @@
# Dev: Plan 9 / 9front install & update platforms (manual vs automatic)
**Plan 9** and its active fork **9front** have no binary package manager — the system
is a source tree you pull and rebuild. Original Plan 9 used **`replica/pull`** with
**fossil/venti** for the archival filesystem; 9front replaced replica with **git9**, so
the `sysupdate` script runs `git/pull` from the 9front repo and you `mk install` from
`/sys/src`. Manual and source-based; **no auto-update**.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **9front git9 / `sysupdate`** | ✓ | — | `git/pull` from `gits://git.9front.org` over the network, then recompile from source | 9front community | http://9front.org | MIT / LPL |
| **Plan 9 `replica/pull`** | ✓ | — | Network replica of the source tree | Plan 9 Foundation | http://p9f.org | MIT |
| **fossil / venti** | n/a | n/a | Archival filesystem + snapshots (storage, not an update tool) | 9front / Plan 9 | http://9front.org | MIT / LPL |
## WeKan / Electron applicability
**No** — Node.js/Electron are not ported; the WeKan desktop cannot run.
## See also
[Redox.md](Redox.md) · [SerenityOS.md](SerenityOS.md) · [BSD.md](BSD.md)
+53
View File
@@ -0,0 +1,53 @@
# Dev: RISC OS install & update platforms (manual vs automatic, like Snap)
How software is installed and updated on **RISC OS** (the Acorn-heritage 32-bit ARM
OS, maintained as open source by **RISC OS Open Ltd** for RISC OS 5), split into
**manual** and **automatic** (background/self-updating, like Linux Snap auto-refresh).
> RISC OS has **no traditional installer** for most software. A RISC OS application
> is an **application directory** whose name starts with `!` ("pling") — e.g.
> `!Draw`, `!PackMan`. The Filer shows it as one icon; everything the app needs lives
> inside that one directory, so **"installing" is literally dragging the `!App`
> directory out of the downloaded `.zip` to where you want it** (and deleting it to
> uninstall). Archives must be extracted **on RISC OS** (SparkFS/SparkPlug) to keep
> the RISC OS filetype metadata. Layered on top are the **PackMan** package manager
> and the **!Store / PlingStore** app store — but **none is a background auto-updater**;
> even PackMan requires the user to open it and click "Upgrade all".
---
## Comparison table
| Platform | Manual | Automatic | Auto mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **`!App` drag-install** (classic Filer method) | ✓ | — | None — download `.zip`, extract on RISC OS, drag the `!App` to `$.Apps`; re-download to update | OS convention | https://www.riscosopen.org/wiki/documentation/show/Getting%20RISC%20OS%20Software | n/a |
| **PackMan** (GUI package manager over RiscPkg/LibPkg) | ✓ | Semi (user-initiated) | User opens PackMan → *Update lists* → *Upgrade all* downloads & installs newer versions. No scheduled/background refresh | Alan Buckley | https://github.com/alanbu/packman | GPL (OSS) |
| **RiscPkg / RISC OS Packaging Project** (package format + repos) | ✓ | Semi (via PackMan) | Repositories provide an index; the front-end pulls updates on demand | RISC OS Packaging Project / ROOL | https://packages.riscosopen.org/ | GPL (OSS) |
| **!Store / PlingStore** (app store) | ✓ | Semi (notify + one-click) | From v1.10 (2014) compares version numbers and **prompts** the user, who triggers the update. No silent background updating | R-Comp / "!Store" | http://www.plingstore.org.uk/ (HTTP-only) | Proprietary/freeware client |
| **OS / ROM + `!Boot` (HardDisc4) updates** | ✓ | — | None — download a ROM image / disc build and re-flash/re-copy; nightly + stable builds published, fetched manually | RISC OS Open Ltd | https://www.riscosopen.org/content/downloads | Apache-2.0 (RISC OS 5) |
**RISC OS 5 vs 6:** RISC OS 5 (RISC OS Open, open source, Apache-2.0) publishes ROM +
`HardDisc4` builds (nightly + stable) fetched and flashed manually; some OS components
are also PackMan packages, but still only when the user runs PackMan — **no automatic
OS update service**. RISC OS 6 / 4.x (RISCOS Ltd → 3QD) is a separate, proprietary,
largely dormant branch using the same manual `!Boot`/disc model.
> **URL note:** the legacy `riscpkg.org` is HTTP-only and refuses HTTPS — treat it as
> semi-defunct; the live canonical package infrastructure is
> **`packages.riscosopen.org`**.
---
## WeKan / Electron applicability
**A WeKan Electron/Node desktop client cannot run on RISC OS.** Electron bundles
Chromium + V8/Node, which target modern 64-bit Windows/macOS/Linux (Electron has even
dropped 32-bit ARM Linux). RISC OS is a cooperatively-multitasked, single-address-space,
32-bit ARM OS with no POSIX/glibc layer and no V8/Node port, so neither Node nor a
current Chromium can build there. The only way to reach a WeKan board from RISC OS is a
native browser (NetSurf, Iris) — and WeKan's Meteor/JS-heavy UI is beyond what those
render — so RISC OS is not a supported WeKan client platform.
## See also
- Linux: [Linux.md](Linux.md) · Amiga: [Amiga.md](Amiga.md) · Haiku: [Haiku.md](Haiku.md) · macOS: [Mac.md](Mac.md)
+20
View File
@@ -0,0 +1,20 @@
# Dev: ReactOS install & update platforms (manual vs automatic)
**ReactOS** is an open-source, binary-compatible re-implementation of Windows NT
(0.4.15 + nightlies). Its Internet software channel is **RAPPS**, the "Application
Manager," which syncs a curated app catalog from a ReactOS server and downloads/
installs vetted third-party apps. There is **no automatic OS self-updater** — upgrading
ReactOS means installing a newer build.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **RAPPS** (Application Manager, `rapps`) | ✓ | partial | Auto-syncs the app-list DB from the ReactOS server on first launch; downloads/installs are user-chosen (HTTP/HTTPS/FTP) | ReactOS Project | https://github.com/reactos/rapps-db | GPL-2.0 |
| **ReactOS OS upgrade** | ✓ | — | None — install a new release/nightly build; no background updater | ReactOS Project | https://reactos.org/ | GPL-2.0 |
## WeKan / Electron applicability
Theoretically possible — being Win32-compatible, ReactOS could in principle run an
**older** Windows Electron build of WeKan, but with real stability caveats; a current
build is unlikely to run cleanly.
## See also
[ArcaOS.md](ArcaOS.md) · [Windows.md](Windows.md) · [Linux.md](Linux.md)
+19
View File
@@ -0,0 +1,19 @@
# Dev: Redox OS install & update platforms (manual vs automatic)
**Redox OS** is a Unix-like microkernel OS written in Rust. Its running-system package
tool is **`pkg`** (from **pkgutils**), which installs cryptographically verified
**pkgar** binary packages from a Redox package server. **cookbook** holds build recipes
and **redox_installer** builds/writes the system image. `pkg` is user-invoked; there is
**no background auto-updater**.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **pkg / pkgutils** (`pkg install`/`upgrade`) | ✓ | — | User-run; downloads pkgar binary packages from Redox's remote package server | Redox OS project | https://doc.redox-os.org/book/pkg.html | MIT |
| **cookbook** | ✓ | — | Package build recipes (source) | Redox OS project | https://gitlab.redox-os.org/redox-os/cookbook | MIT |
| **redox_installer** | ✓ | — | Builds/installs a system image from packages | Redox OS project | https://gitlab.redox-os.org/redox-os/installer | MIT |
## WeKan / Electron applicability
**No** — Node.js/Electron are not available on Redox; the WeKan desktop cannot run.
## See also
[Plan9.md](Plan9.md) · [SerenityOS.md](SerenityOS.md) · [Haiku.md](Haiku.md)
+20
View File
@@ -0,0 +1,20 @@
# Dev: Sailfish OS install & update platforms (manual vs automatic, like Snap)
**Sailfish OS** (Jolla) has the first-party **Jolla Store** (harbour) plus community
**OpenRepos** via the **Storeman** client; the underlying package system is RPM with
PackageKit/zypper (zypper is a "last resort" for apps). OS upgrades are versioned OTAs
from Settings. Sailfish OS 5.0 "Tampella" is current (2025).
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Jolla Store** | ✓ | — (notifies) | Store app checks & installs updates on user action | Jolla | https://jolla.com/ | Proprietary |
| **Storeman** (OpenRepos client) | ✓ | — | Adds OpenRepos RPM repos; installs/updates community apps | community (openrepos.net) | https://openrepos.net/ | OSS |
| **zypper / RPM** | ✓ | — | CLI `zypper`/`pkcon` install/upgrade | openSUSE/Jolla | https://docs.sailfishos.org/ | varies |
| **System OTA upgrade** | ✓ | — | Versioned OS image upgrade from Settings over the Internet | Jolla | https://docs.sailfishos.org/ | Proprietary/varies |
## WeKan applicability
Use the **browser/web app**; no first-party WeKan package (a community Storeman webapp
wrapper is the only native-ish option).
## See also
[Ubuntu-Touch.md](Ubuntu-Touch.md) · [postmarketOS.md](postmarketOS.md) · [Linux.md](Linux.md)
+17
View File
@@ -0,0 +1,17 @@
# Dev: SerenityOS install & update platforms (manual vs automatic)
**SerenityOS has no networked package manager and no binary repository or auto-update.**
Third-party software is added only via the **Ports** system — per-app shell scripts that
fetch upstream source, patch it, cross-compile it, and install it into the disk image at
build time. A `pkg` tool has been discussed by the community but is not shipped.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Ports system** | ✓ (build from source) | — | None as a manager — each port script downloads that app's upstream source at build time; no binary repo, no updater | SerenityOS community | https://github.com/SerenityOS/serenity | BSD-2-Clause |
| **Networked package manager** | — | — | Does not exist (only discussed) | — | https://serenityos.org/ | — |
## WeKan / Electron applicability
**No** — no Node.js/Electron port; the WeKan desktop cannot run.
## See also
[Redox.md](Redox.md) · [Plan9.md](Plan9.md) · [Haiku.md](Haiku.md)
+28
View File
@@ -0,0 +1,28 @@
# Dev: Solaris & illumos install & update platforms (manual vs automatic)
**Oracle Solaris 11** and the **illumos** distributions (OpenIndiana, OmniOS,
SmartOS) share the **Image Packaging System (IPS)**, driven by `pkg`. IPS is
network-native (installs from HTTP(S) publisher repos), folds patching into the same
tool, and uses ZFS **boot environments** so an update clones the running system and
applies changes to the clone — safe rollback by rebooting the old BE.
> **Solaris 11.4 is one of the few enterprise Unixes with a genuine automatic
> Internet updater** (`svc:/system/auto-update` SMF service). illumos distros are
> manual (`pkg update`).
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Oracle IPS `pkg`** (release/support repos, SRUs, boot environments) | ✓ | **✓** | `svc:/system/auto-update:default` SMF service (11.4 SRU 35+): scheduled, pulls SRUs from `pkg.oracle.com/solaris/support` | Oracle | https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/update-sys-add-sw/accessing-support-updates.html | Proprietary OS; IPS engine CDDL |
| **illumos IPS `pkg`** (OpenIndiana Hipster rolling; OmniOS weekly stable) | ✓ | — | None — admin-initiated `pkg update` | OpenIndiana / OmniOS | https://omnios.org/info/ipsrepos · https://www.openindiana.org/packages/ | CDDL / MIT |
| **pkgsrc + `pkgin`** (binary packages) | ✓ | — | None (`pkgin update && pkgin upgrade`) | NetBSD Foundation / pkgsrc | https://pkgin.net/ | BSD |
OmniOS ships non-disruptive weekly `pkg update`s (stable release every 6 months, LTS
every 4th). Legacy `pkgadd`/`pkgrm` still exist but are not the update path.
## WeKan / Electron applicability
**No** — Electron/Chromium has no Solaris/illumos build. Node.js *does* run on
illumos/Solaris (SmartOS is Node-centric), so WeKan's **server** is feasible; the
desktop app is not. Use a browser against a hosted server.
## See also
[Linux.md](Linux.md) · [BSD.md](BSD.md) · [AIX.md](AIX.md) · [Mac.md](Mac.md) · [Windows.md](Windows.md)
+19
View File
@@ -0,0 +1,19 @@
# Dev: TV / streaming OS install & update platforms (Android TV, Fire OS, Roku)
The mainstream TV/streaming OSes are all **store-managed with background auto-update
enabled by default** when idle + on Wi-Fi; users rarely install packages manually. (LG
**webOS** and Samsung **Tizen** TVs have their own pages: [webOS.md](webOS.md),
[Tizen.md](Tizen.md).)
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Google Play (Android TV / Google TV)** | ✓ | ✓ | Play background auto-update (same engine as phone Play) | Google | https://play.google.com/ | Proprietary |
| **Amazon Appstore (Fire OS)** | ✓ | ✓ | Appstore periodically checks & installs when idle/Wi-Fi/charged | Amazon | https://developer.amazon.com/docs/fire-tv/fire-os-overview.html | Proprietary |
| **Roku OS channel store** | rare | ✓ | Channels + OS update automatically in the background | Roku | https://www.roku.com/ | Proprietary |
## WeKan applicability
**Not a realistic WeKan target** (input-model mismatch); if ever needed, only the
browser/web-app path.
## See also
[webOS.md](webOS.md) · [Tizen.md](Tizen.md) · [Android.md](Android.md)
+20
View File
@@ -0,0 +1,20 @@
# Dev: Tizen install & update platforms (manual vs automatic, like Snap)
**Tizen** (Samsung) apps package as **TPK**. **Important 2026 change:** Samsung ended
Tizen mobile/wearable app service (Sep 30 2025) and shut the **Galaxy Store for Tizen
watch/mobile devices in January 2026**, so official download/auto-update of TPK apps on
Tizen watches is no longer available. Tizen persists mainly in Samsung **smart TVs**,
where the TV app store still auto-updates.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Galaxy Store / Tizen Store (TV)** | ✓ | ✓ | TV store background app auto-update | Samsung | https://www.tizen.org/ | Proprietary |
| **Galaxy Store (Tizen watch/mobile)** | — | — | **Discontinued Jan 2026** — no download/auto-update | Samsung | https://developer.tizen.org/ | Proprietary |
| **Sideloaded TPK** | ✓ | — | Manual install via Tizen Studio/SDB; no consumer auto-update | Samsung tooling | https://developer.tizen.org/ | varies |
## WeKan applicability
Only the smart-TV **browser web app**; the Tizen app-store path is effectively dead
(2026 shutdown) and the TV interaction model suits WeKan poorly.
## See also
[webOS.md](webOS.md) · [TV-OSes.md](TV-OSes.md) · [KaiOS.md](KaiOS.md)
+19
View File
@@ -0,0 +1,19 @@
# Dev: Ubuntu Touch install & update platforms (manual vs automatic, like Snap)
**Ubuntu Touch** (UBports) has two independent planes: **OpenStore** distributes
confined **click** app packages, while the OS is updated as a **whole system image** via
delta OTAs (image-based, not apt/dpkg for apps). App updates are user-triggered in
OpenStore; OS OTAs are prompted, not silent.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **OpenStore** | ✓ | — (notifies) | Client checks repo; user taps to update click packages | UBports | https://open-store.io/ | AGPL-3.0 (store) |
| **click packages** | ✓ | — | Confined app bundles installed via OpenStore | UBports | https://docs.ubports.com/ | varies |
| **System-image OTA** | ✓ (prompted) | semi (checks/notifies) | Delta between current and target image downloaded over the Internet; channel-based | UBports | https://ubports.com/ | GPL/varies |
## WeKan applicability
A **click-packaged web wrapper** (webapp-container) in OpenStore, updated manually via
OpenStore — or just the browser web app against a WeKan server.
## See also
[Sailfish.md](Sailfish.md) · [postmarketOS.md](postmarketOS.md) · [Linux.md](Linux.md)
+170
View File
@@ -0,0 +1,170 @@
# Dev: Windows install & update platforms (manual vs automatic, like Snap)
A developer reference of **every platform that can install and update a Windows
application**, split by whether updates are **manual** (the user re-downloads and
re-runs an installer) or **automatic** (background / self-updating, the way a Linux
**Snap** auto-refreshes). WeKan's desktop app is **Electron**, and WeKan server can
also run on Windows, so both desktop-app and enterprise-deployment mechanisms are
covered.
> Windows has no single first-party equivalent of Snap's transactional,
> store-driven, delta auto-refresh. The closest built-in options are the
> **Microsoft Store (MSIX)** and **App Installer (`.appinstaller`)**; everything
> else is either manual, an app-embedded updater (Electron/Squirrel/Omaha/Sparkle),
> or an enterprise fleet manager (Intune/SCCM/WSUS/PDQ).
---
## a) Manual update platforms
The user (or an admin) explicitly fetches and applies each new version. No
background refresh.
### Installer formats (the update is "download the new one and run it")
| Platform | What it is | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **MSI / Windows Installer** | The classic transactional installer/patching engine. Updates via a new MSI, an `.msp` patch, or major-upgrade. No self-update; an admin or GPO redeploys. | Microsoft | `.msi`, `.msp` | https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal | Proprietary (OS component) |
| **EXE / NSIS** | Scriptable installer that produces a self-contained `.exe`. No built-in updater — needs an external one (see electron-updater / WinSparkle). | Open source (NSIS project) | `.exe` | https://nsis.sourceforge.io/ | zlib/LGPL (OSS) |
| **EXE / Inno Setup** | Popular scriptable `.exe` installer. Same as NSIS: no self-update on its own. | Jordan Russell (OSS) | `.exe` | https://jrsoftware.org/isinfo.php | Free (OSS) |
| **Portable / ZIP** | Unzip-and-run, no installer. "Update" = replace the folder. | app author | `.zip`, portable `.exe` | — | varies |
### Package managers whose *default* mode is manual
| Platform | Update command (manual) | Maintainer | Format handled | URL | License |
|---|---|---|---|---|---|
| **winget** (Windows Package Manager) | `winget upgrade` / `winget upgrade --all` — **no built-in scheduler**; nothing runs on its own. | Microsoft | `.msi`, `.exe`, `.msix`, portable, Store | https://github.com/microsoft/winget-cli | MIT (OSS) |
| **Chocolatey** (CLI) | `choco upgrade all` — manual unless scheduled (see auto section). | Chocolatey Software / community | `.msi`, `.exe`, `.zip` (nupkg wrappers) | https://chocolatey.org/ | Apache-2.0 core (OSS); paid tiers |
| **Scoop** | `scoop update *` — manual only, no background service by design. | ScoopInstaller (OSS) | portable `.zip`, `.exe` | https://scoop.sh/ · repo https://github.com/ScoopInstaller/Scoop | MIT (OSS) |
| **npm global** (`npm i -g`) | `npm update -g` — **CLI tools only**, not GUI apps. No auto-update. | npm / GitHub | npm tarball | https://docs.npmjs.com/ | MIT (OSS) |
| **Ninite** (free web installer) | Re-run the generated `.exe` to update the whole curated selection. Fixed catalog — **not a self-distribution channel**. | Secure by Design | wraps vendor `.exe`/`.msi` | https://ninite.com/ | Free (personal) |
> **Note (winget):** winget itself is delivered as part of **App Installer** via the
> Microsoft Store, so the *tool* updates automatically — but the **apps it manages
> do not** unless you add a scheduled task or a helper (see WAU below).
---
## b) Automatic update platforms (like Snap)
These refresh in the background, on launch, on a schedule, or store-managed —
without the user re-running an installer.
### Store / OS-managed (closest to Snap)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **Microsoft Store** | Store client silently downloads & installs new versions in the background (delta updates). The nearest Windows analogue to Snap's store-driven auto-refresh. | Microsoft | **MSIX / AppX** | https://apps.microsoft.com/ | Proprietary |
| **MSIX + App Installer (`.appinstaller`)** | An `.appinstaller` XML manifest with an `<UpdateSettings>` block: `OnLaunch` + `HoursBetweenUpdateChecks`, or automatic background checks. Lets you self-host MSIX auto-update **without** the Store. | Microsoft | **MSIX / `.appinstaller`** | https://learn.microsoft.com/en-us/windows/msix/app-installer/app-installer-file-overview | Proprietary (OS component) |
| **ClickOnce** | `.application` manifest; the app checks its deployment URL and updates **on launch** (before or after start, configurable). Old but still supported; primarily .NET — **not usable for Electron**. | Microsoft | `.application` | https://learn.microsoft.com/en-us/visualstudio/deployment/clickonce-security-and-deployment | Proprietary |
| **Steam** | Always-running background client auto-updates installed titles (has a "Software" category). Offloads the whole update stack to Valve, but a closed store with revenue share. | Valve | any desktop app | https://store.steampowered.com/ | Proprietary |
| **Setapp / Stardock ODM** | Subscription "app store" clients with an auto-updating agent. **Setapp is macOS/iOS only** — there is **no general Windows equivalent**; Stardock ODM only auto-updates Stardock's own utilities. | MacPaw / Stardock | client-managed | https://setapp.com/ · https://www.stardock.com/ | Proprietary |
> ⚠️ **MSIX web-install caveat:** the **`ms-appinstaller:` protocol handler**
> (one-click "Install" from a web page) was **disabled by default** by Microsoft in
> Dec 2023 (App Installer `1.21.3421.0`) after malware abuse — `.msix`/`.appinstaller`
> files now must be **downloaded to disk first** so endpoint AV can scan them. The
> `.appinstaller` **auto-update-after-install** mechanism still works; only the
> browser one-click protocol was turned off. See
> https://learn.microsoft.com/en-us/windows/msix/app-installer/ and
> https://www.microsoft.com/en-us/msrc/blog/2023/12/microsoft-addresses-app-installer-abuse
### App-embedded self-updaters (best fit for an Electron app like WeKan desktop)
| Platform | Auto-update mechanism | Maintainer | Format | URL | License |
|---|---|---|---|---|---|
| **Squirrel.Windows** | The updater behind Electron's built-in `autoUpdater` on Windows. App pings a feed on launch, downloads deltas, swaps on restart. | OSS (Squirrel project) | `.nupkg` + `Setup.exe` | https://github.com/Squirrel/Squirrel.Windows | MIT (OSS) |
| **Electron `autoUpdater` (built-in)** | Built on Squirrel (Win) / Squirrel.Mac (Mac). Needs an update **server/feed**. | OpenJS / Electron | Squirrel feed | https://www.electronjs.org/docs/latest/api/auto-updater | MIT (OSS) |
| **electron-updater (electron-builder)** | The most common Electron auto-updater. NSIS-based; pulls from a feed (**GitHub Releases**, generic HTTP, S3, etc.), downloads in background, installs on quit. Independent of Squirrel. | electron-userland (OSS) | NSIS `.exe`, `.msi` | https://www.electron.build/ · repo https://github.com/electron-userland/electron-builder | MIT (OSS) |
| **update.electronjs.org** | Free hosted update server for **open-source** Electron apps published to public **GitHub Releases**; works with the built-in Squirrel `autoUpdater`. | Electron project | Squirrel feed | https://github.com/electron/update.electronjs.org | MIT (OSS) |
| **Hazel** | Self-hosted lightweight update server for Electron (GitHub Releases proxy). | Vercel (OSS) | Squirrel feed | https://github.com/vercel/hazel | MIT (OSS) |
| **Nuts** | Self-hosted update server backed by GitHub Releases, with download + changelog. | GitBook (OSS) | Squirrel feed | https://github.com/GitbookIO/nuts | Apache-2.0 (OSS) |
| **Nucleus** | Self-hosted update server for Electron (multi-app, S3/static). | Atlassian (OSS) | Squirrel / electron-updater feed | https://github.com/atlassian/nucleus | MIT (OSS) |
| **WinSparkle** | Windows port of macOS **Sparkle**; appcast (RSS) based auto-update for native/non-Electron Windows apps. Checks on schedule/launch, prompts, installs. | OSS | `.exe`/`.msi` via appcast | https://winsparkle.org/ | MIT (OSS) |
| **Google Omaha** | The open-source engine behind Google Chrome's silent auto-update: a background service that polls and installs. Heavy to operate but the gold standard for silent background refresh. | Google (OSS) | `.exe`/`.msi` via Omaha protocol | https://github.com/google/omaha | Apache-2.0 (OSS) |
> **Maturity caveat (avoid for new work):** **Squirrel.Windows** is stagnant (last
> release 2020), **Nucleus/Hazel/Nuts** are 2018–2021 vintage, and **Google Omaha**
> was archived (read-only) in 2025. For a new Electron app prefer **electron-updater**
> (self-hosted feed) or **update.electronjs.org** (GitHub Releases).
### Third-party package managers in *automatic* mode
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **Chocolatey (scheduled / Central Management)** | A **scheduled task** running `choco upgrade all`, or **Chocolatey Central Management** (self-service + reporting) in the commercial tiers. | Chocolatey Software | https://chocolatey.org/ | OSS core + paid |
| **Winget-AutoUpdate (WAU)** | Popular helper: installs a scheduled task running `winget upgrade` as SYSTEM on a Daily/Weekly/… cadence, with allow/block lists and user notifications. Turns winget into Snap-like background refresh. | Romanitho (OSS) | https://github.com/Romanitho/Winget-AutoUpdate | GPL-3.0 (OSS) |
| **Ninite Pro / Ninite Updater** | Background agent that keeps a curated app catalog patched automatically across machines. | Ninite (commercial) | https://ninite.com/ | Proprietary (paid) |
### Enterprise fleet / MDM (admin-driven automatic deployment & patching)
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **Microsoft Intune (+ Enterprise App Management)** | Cloud MDM: assign apps (Win32 `.intunewin`/MSIX/Store) via the Intune Management Extension. **Enterprise App Management** auto-update reached **GA in 2026** — a catalog app with a *Required* assignment auto-updates when a newer catalog version appears (no supersedence; no phased rollout / rollback yet). | Microsoft | https://learn.microsoft.com/en-us/intune/app-management/deployment/enterprise-app-management | Proprietary (subscription) |
| **Microsoft Configuration Manager (SCCM/MECM)** | On-prem/co-managed fleet deployment. **Automatic Deployment Rules (ADR)** sync → build update group → deploy to collections on a schedule; third-party apps via native catalogs or Patch My PC. Can target **server** collections. | Microsoft | https://learn.microsoft.com/en-us/mem/configmgr/ | Proprietary |
| **WSUS** | On-prem Windows Update server; auto-approval rules + GPO-scheduled client pull. Primarily OS/MS patches; third-party apps only via a publishing layer. **Deprecated (announced Sept 2024)** but still shipping in Windows Server 2025. | Microsoft | https://learn.microsoft.com/en-us/windows-server/administration/windows-server-update-services/get-started/windows-server-update-services-wsus | Proprietary (OS role) |
| **Windows Update for Business** (now "Update Client Policies") | Ring/deferral/deadline policy pushed via Intune/GPO; the OS update client pulls on schedule. **Governs OS patching, not your app** — context only. | Microsoft | https://learn.microsoft.com/en-us/intune/intune-service/protect/windows-update-for-business-configure | Incl. with Windows |
| **PDQ Deploy / PDQ Connect** | Auto-deploy and patch third-party apps on a schedule across a fleet (Deploy = on-prem/heartbeat; Connect = cloud agent). | PDQ.com (commercial) | https://www.pdq.com/ | Proprietary (paid) |
| **Patch My PC** | Watches vendor releases and automatically repackages/signs/publishes updated app definitions into **Intune / ConfigMgr / WSUS**, which then deploy them. | Patch My PC (commercial) | https://patchmypc.com/ | Proprietary (paid) |
### GUI update front-ends (not distribution channels)
| Platform | Auto-update mechanism | Maintainer | URL | License |
|---|---|---|---|---|
| **UniGetUI** (formerly WingetUI) | GUI over winget/Scoop/Chocolatey/npm/pip; polls and auto-applies or notifies, with a built-in self-updater. Your app appears automatically if it is in winget/Choco/Scoop. | Devolutions (OSS) | https://github.com/Devolutions/UniGetUI | MIT (OSS) |
| **PortableApps.com Platform** | Launcher for portable `.paf.exe` apps with a built-in (user-initiated) App Updater. Electron portable builds fit well. | Rare Ideas (OSS) | https://portableapps.com/ | OSS |
---
## Comparison at a glance
| Platform | Manual | Automatic | Auto mechanism | For Electron app? |
|---|:---:|:---:|---|---|
| Microsoft Store (MSIX) | ✓ | ✓ | Store client, background | ✓ (needs MSIX pkg) |
| App Installer `.appinstaller` | ✓ | ✓ | manifest `OnLaunch` / interval | ✓ (needs MSIX pkg) |
| ClickOnce | — | ✓ | on-launch check | ✗ (mainly .NET) |
| MSI / Windows Installer | ✓ | via GPO/Intune | admin redeploy | wrapper only |
| NSIS / Inno Setup (`.exe`) | ✓ | — | needs external updater | packaging only |
| winget | ✓ | via WAU/task | scheduled task | distribution |
| Chocolatey | ✓ | ✓ | scheduled task / CCM | distribution |
| Scoop | ✓ | — | (manual by design) | distribution |
| npm global | ✓ | — | none | CLI tools only |
| Ninite (web) | ✓ | — | re-run installer | ✗ (fixed catalog) |
| Microsoft Store (MSIX) — *see above* | ✓ | ✓ | Store client, background | ✓ (needs MSIX pkg) |
| Steam | — | ✓ | background client | ✓ (closed store) |
| Setapp / Stardock ODM | — | ✓ | client agent | ✗ (Setapp = Mac only) |
| **Squirrel.Windows** | — | ✓ | on-launch, deltas | **✓ built-in** |
| **electron-updater** | — | ✓ | background, install-on-quit | **✓ recommended** |
| update.electronjs.org / Hazel / Nuts / Nucleus | — | ✓ | update feed server | ✓ (feed backends) |
| WinSparkle | — | ✓ | appcast (RSS) | native apps |
| Google Omaha | — | ✓ | background service, silent | heavy, generic |
| Ninite Pro | — | ✓ | background agent | ✗ (catalog only) |
| Intune (+EAM) / SCCM / WSUS | — | ✓ | MDM push / ADR / schedule | ✓ (as Win32/MSIX) |
| PDQ / Patch My PC | — | ✓ | scheduled fleet patch | ✓ (as package) |
| UniGetUI / PortableApps | ✓ | ✓ | GUI polling front-end | ✓ (via backends) |
---
## What this means for WeKan
- **WeKan desktop is Electron.** The two realistic Snap-like auto-update paths are:
1. **electron-updater (electron-builder)** pulling from **GitHub Releases** — the
simplest, most common choice; background download, install on quit. Feed can be
self-hosted (Hazel / Nuts / Nucleus) if not using GitHub directly.
2. **MSIX + Microsoft Store** (or a self-hosted **`.appinstaller`**) — the most
"Snap-like" store-managed refresh, at the cost of MSIX packaging and (for the
Store) certification. Remember the `ms-appinstaller:` one-click protocol is
disabled by default, so web install means "download the file, then run it."
- **For plain distribution** (no forced auto-update), **winget** and **Chocolatey**
are the community-expected package managers; users update on their own, or admins
schedule it (Winget-AutoUpdate / a `choco upgrade all` task).
- **For managed fleets**, **Intune / SCCM / PDQ / Patch My PC** push updates centrally.
- **WeKan server on Windows** is normally run via Docker or from source, not as a
packaged desktop app, so its "updates" are pulling a new image / rebuilding —
outside these desktop update frameworks.
## See also
- Linux Snap auto-refresh, and why the exotic-arch snap builds differ:
[Snap-Core.md](../Forks/Snap-Core.md)
- [Install WeKan on Windows](../../../Platforms/Propietary/Windows/Install-Windows.md) · [Windows.md](../../../Platforms/Propietary/Windows/Windows.md)
+22
View File
@@ -0,0 +1,22 @@
# Dev: iOS / iPadOS install & update platforms (manual vs automatic, like Snap)
The **App Store** is the universal path with system-wide background auto-update;
**TestFlight** covers betas; **MDM** silently pushes managed apps; and since the EU DMA,
notarized **alternative marketplaces / web distribution** exist (EU only). "Add to Home
Screen" web apps never auto-update as installables — they reload the live site.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **App Store** | ✓ | ✓ (default) | Settings → App Store → App Updates; background download+install | Apple | https://www.apple.com/app-store/ | Proprietary |
| **TestFlight** (beta) | ✓ | ✓ | Automatic beta-build updates over the Internet | Apple | https://testflight.apple.com/ | Proprietary |
| **MDM-managed apps** (ABM/VPP) | — | ✓ | MDM silently installs/updates managed apps | Apple + MDM vendors | https://developer.apple.com/business/ | Proprietary |
| **EU alternative marketplaces / web distribution** | ✓ | ✓ | DMA-mandated third-party stores manage updates; apps still Apple-notarized (EU only) | Apple-authorized third parties | https://developer.apple.com/support/dma-and-apps-in-the-eu/ | Proprietary/varies |
| **Web app "Add to Home Screen"** (PWA) | ✓ (re-add) | — | Loads live web content each launch; no packaged update | website owner | https://developer.apple.com/ | varies |
## WeKan applicability
**Add to Home Screen PWA** against a WeKan server is the realistic path (no store
auto-update). A wrapped native app could use App Store background updates, but the web
app is the pragmatic route. Electron desktop auto-update does not apply.
## See also
[Android.md](Android.md) · [ChromeOS.md](ChromeOS.md) · [Mac.md](Mac.md)
+18
View File
@@ -0,0 +1,18 @@
# Dev: postmarketOS install & update platforms (manual vs automatic, like Snap)
**postmarketOS** is a true rolling **Alpine Linux** system: apps and OS are the same
**apk** package stream (no separate app store). Updates are user-run CLI (`apk upgrade`)
or via a desktop software center. As of v25.12 it uses **apk v3** (downloads all
packages before installing, so a flaky connection can't break the system).
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **Alpine `apk`** (apk-tools v3) | ✓ | — (user/cron) | `apk upgrade` fetches from Alpine/pmOS repos; download-then-install | postmarketOS / Alpine | https://postmarketos.org/ | GPL-2.0 |
| **Software center** (GNOME Software / Discover) | ✓ | notifies | GUI front-end over apk repos | desktop projects | https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper | GPL/varies |
## WeKan applicability
It's mainline Linux — run WeKan in the browser as a **PWA**, or self-host the server;
apps update via `apk` (not the WeKan client). See [Linux.md](Linux.md).
## See also
[Ubuntu-Touch.md](Ubuntu-Touch.md) · [Sailfish.md](Sailfish.md) · [Linux.md](Linux.md)
+17
View File
@@ -0,0 +1,17 @@
# Dev: webOS (LG TV) install & update platforms (manual vs automatic, like Snap)
**LG Content Store** is the app store on LG TVs; auto-update is a per-store or system
toggle (Settings → Support/General → Software Update → Auto Update). On webOS 22+
(2022–2026 models) app updates are increasingly bundled with system updates.
| Store/Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **LG Content Store** | ✓ | ✓ | Background app auto-update via store/system toggle; bundled with OS updates on webOS 22+ | LG | https://www.lg.com/us/lg-content-store | Proprietary |
| **System software update** | ✓ | ✓ | OTA firmware + bundled apps over the Internet | LG | https://webostv.developer.lge.com/ | Proprietary |
## WeKan applicability
Access via the TV **browser web app**; a Content Store web-app listing would be the only
auto-updating route, but WeKan's interaction model suits TV poorly.
## See also
[Tizen.md](Tizen.md) · [TV-OSes.md](TV-OSes.md) · [KaiOS.md](KaiOS.md)
+25
View File
@@ -0,0 +1,25 @@
# Dev: IBM z/OS install & update platforms (manual vs automatic)
z/OS mainframe maintenance is managed by **SMP/E (System Modification Program/
Extended)** using the classic `RECEIVE` / `APPLY` / `ACCEPT` cycle for PTFs. The
Internet-based automatic-download mechanism is **SMP/E `RECEIVE ORDER`**, which places
a service order with IBM's Automated Delivery server and downloads the package
automatically. Full-system/product delivery comes via **ServerPac**, increasingly as a
**z/OSMF portable software instance** deployed through **z/OSMF Software Management**.
> **`RECEIVE ORDER`** is z/OS's automatic Internet *retrieval* — the order and
> download happen over the Internet without manual media handling; the `APPLY` step
> stays an admin action.
| Mechanism | Manual | Automatic | Auto/Internet mechanism | Maintainer | URL | License |
|---|:---:|:---:|---|---|---|---|
| **SMP/E** `RECEIVE`/`APPLY`/`ACCEPT` (PTFs) | ✓ | partial | Ordering/download via RECEIVE ORDER (below); apply is admin-run | IBM | https://www.ibm.com/docs/en/zos/3.1.0?topic=service-smp-e | Proprietary |
| **SMP/E `RECEIVE ORDER`** (Internet Service Retrieval) | ✓ | **✓ (download)** | SMP/E job orders + downloads PTF/HOLDDATA directly from IBM's Automated Delivery server over the Internet | IBM | https://www.ibm.com/docs/en/zos/3.1.0?topic=sets-what-happens-during-internet-service-retrieval | Proprietary |
| **ServerPac / z/OSMF Software Management** (portable software instance) | ✓ | partial | z/OSMF downloads the order from IBM's server (GIMGTPKG); admin deploys/configures | IBM | https://www.ibm.com/support/z-content-solutions/serverpac-install-zosmf/ | Proprietary |
## WeKan / Electron applicability
**No** for the desktop — no Electron/Chromium on z/OS. IBM ships a **Node.js** port for
z/OS, so a WeKan **server** is conceivable; the Electron client is not.
## See also
[AIX.md](AIX.md) · [Solaris.md](Solaris.md) · [OpenVMS.md](OpenVMS.md) · [Linux.md](Linux.md)
+79
View File
@@ -0,0 +1,79 @@
# Autoupdate: install & update platforms per operating system
Developer reference comparing **how software is installed and updated** on each
operating system — split into **manual** (user re-downloads/re-runs) and **automatic**
(background/self-updating, like Linux **Snap** auto-refresh) — for the purpose of
**automatic WeKan updates**. WeKan's desktop app is **Electron**; the server is
Node/Meteor (Docker or from source). Each page ends with a note on WeKan/Electron
applicability.
## Desktop / server operating systems
| OS | Doc | Snap-like auto-update? |
|---|---|---|
| Windows | [Windows.md](OS/Windows.md) | Microsoft Store / MSIX; electron-updater |
| macOS | [Mac.md](OS/Mac.md) | Mac App Store; Sparkle; electron-updater |
| Linux | [Linux.md](OS/Linux.md) | **Snap** (native), Flatpak, AppImage |
| BSD (FreeBSD/OpenBSD/NetBSD/DragonFly) | [BSD.md](OS/BSD.md) | No — cron + native tools |
| Haiku | [Haiku.md](OS/Haiku.md) | No — pkgman is on-demand |
| Amiga family (AmigaOS 3/4, MorphOS, AROS) | [Amiga.md](OS/Amiga.md) | Only Grunch; mostly manual |
| RISC OS | [RISC-OS.md](OS/RISC-OS.md) | No — PackMan is user-initiated |
## Enterprise / legacy Unix & VMS
| OS | Doc | Auto Internet update? |
|---|---|---|
| Oracle Solaris / illumos | [Solaris.md](OS/Solaris.md) | Solaris 11.4 `auto-update` SMF; illumos no |
| IBM AIX | [AIX.md](OS/AIX.md) | SUMA (scheduled download) |
| HP-UX | [HP-UX.md](OS/HP-UX.md) | No |
| OpenVMS | [OpenVMS.md](OS/OpenVMS.md) | No |
| IBM z/OS | [zOS.md](OS/zOS.md) | SMP/E `RECEIVE ORDER` (download) |
## Alternative / retro / hobbyist desktop
| OS | Doc | Package manager |
|---|---|---|
| OS/2 → ArcaOS | [ArcaOS.md](OS/ArcaOS.md) | RPM/YUM, WarpIN, ANPM |
| ReactOS | [ReactOS.md](OS/ReactOS.md) | RAPPS |
| FreeDOS | [FreeDOS.md](OS/FreeDOS.md) | FDNPKG |
| Plan 9 / 9front | [Plan9.md](OS/Plan9.md) | source pull (git9/replica) |
| Redox OS | [Redox.md](OS/Redox.md) | pkg / pkgutils |
| SerenityOS | [SerenityOS.md](OS/SerenityOS.md) | none (Ports, build-from-source) |
## Mobile / embedded / TV
| OS | Doc | Store auto-update |
|---|---|---|
| Android | [Android.md](OS/Android.md) | Play / F-Droid / Aurora (background) |
| iOS / iPadOS | [iOS.md](OS/iOS.md) | App Store (background) |
| ChromeOS | [ChromeOS.md](OS/ChromeOS.md) | OS + Play + PWA (best PWA fit) |
| HarmonyOS | [HarmonyOS.md](OS/HarmonyOS.md) | AppGallery (background) |
| Ubuntu Touch | [Ubuntu-Touch.md](OS/Ubuntu-Touch.md) | OpenStore (notifies) + image OTA |
| Sailfish OS | [Sailfish.md](OS/Sailfish.md) | Jolla Store (notifies) |
| postmarketOS | [postmarketOS.md](OS/postmarketOS.md) | apk (Alpine) |
| KaiOS | [KaiOS.md](OS/KaiOS.md) | KaiStore (background) |
| Tizen | [Tizen.md](OS/Tizen.md) | TV only (store shut for watch/mobile 2026) |
| webOS (LG TV) | [webOS.md](OS/webOS.md) | LG Content Store (background) |
| Android TV / Fire OS / Roku | [TV-OSes.md](OS/TV-OSes.md) | store-managed (background) |
## WeKan-specific
| Topic | Doc |
|---|---|
| Snap multi-arch build constraints (core24 vs QEMU action) | [Snap-Core.md](Forks/Snap-Core.md) |
| Releasing the `wekan-ondra` / `wekan-gantt-gpl` snaps from release-all.yml | [Snap-Ondra-Gantt.md](Snap-Ondra-Gantt.md) |
| Releasing the Docker-based UCS App Center app (MongoDB 3.x → FerretDB migration) | [UCS.md](UCS.md) |
| WeKan for Nextcloud — ExApp build, App Store publishing, AI/Deck/SSO integrations | [Nextcloud.md](Nextcloud.md) |
---
### Bottom line for WeKan
- **Linux:** WeKan already ships a **Snap** (`snapd` auto-refresh) — the reference
Snap-like experience — plus Docker.
- **Windows / macOS desktop:** **electron-updater** (electron-builder) with a
signed(+notarized on macOS) feed is the closest single-app analogue to Snap.
- **Mobile / TV:** WeKan is a web app — the realistic path is a **PWA** (ChromeOS/
Android/iOS) or a browser, not native store auto-update.
- **Everything else (BSD, enterprise Unix, retro):** no Electron desktop path; run the
**server** where Node ports exist, or use a browser against a hosted WeKan.
+250
View File
@@ -0,0 +1,250 @@
# Dev: releasing the `wekan-ondra` and `wekan-gantt-gpl` snaps from release-all.yml
Goal: when `wekan/wekan`'s **release-all.yml** runs, in addition to the main `wekan`
snap it should also, for each variant repo
[`wekan/wekan-ondra`](https://github.com/wekan/wekan-ondra) and
[`wekan/wekan-gantt-gpl`](https://github.com/wekan/wekan-gantt-gpl):
1. **Replace** the variant repo's code with the newest `wekan` code (at the release tag).
2. **Modify `snapcraft.yaml`** so the snap `name:` becomes `wekan-ondra` /
`wekan-gantt-gpl` (instead of `wekan`).
3. **Build and publish** that snap to the **Snap Store**, all channels
**stable, candidate, beta, edge** — automatically, on every WeKan release.
This document lists **exactly which keys/secrets must be added** and the **workflow
changes** required.
## Remaining steps
> **Status:** the `snap-variants` job **is already wired into `release-all.yml`** — it is
> guarded and skips with a `::notice::` until the items below are done, so it never breaks
> an ordinary release.
1. **Register the snap names** (one-time, on the machine holding the WeKan Snap Store
account): `snapcraft register wekan-ondra` and `snapcraft register wekan-gantt-gpl`.
2. **Create/confirm** the GitHub repos `wekan/wekan-ondra` and `wekan/wekan-gantt-gpl`
(each is overwritten with the newest `wekan` code on every release — make sure nothing
unique lives only there).
3. **Re-export Snap Store credentials** covering all three names, then update `SNAP_AUTH`:
```sh
snapcraft export-login --snaps wekan,wekan-ondra,wekan-gantt-gpl \
--acls package_access,package_push,package_release,package_update snap-auth.txt
gh secret set SNAP_AUTH --repo wekan/wekan < snap-auth.txt
```
4. **Extend `WEKAN_REPO_TOKEN`** to have write access to the two variant repos (add them
to the token's repo scope; the stored secret value stays the same).
5. Done — the next release publishes both variant snaps to stable/candidate/beta/edge.
**How to add/update a secret:** GitHub → the `wekan/wekan` repo → **Settings → Secrets and
variables → Actions → New repository secret**; or the CLI `gh secret set NAME --repo
wekan/wekan` (paste the value) or `gh secret set NAME --repo wekan/wekan < file`.
The reference details (name registration, ACLs, the workflow job, snapcraft.yaml edits)
are in §1–§7 below.
---
## How the current `wekan` snap release works (baseline)
From `.github/workflows/release-all.yml`:
- `snapcraft.yaml` has `name: wekan`, `version: '<x.yy>'`, `base: core24`.
- Snap Store auth is the secret **`SNAP_AUTH`** (the output of
`snapcraft export-login`), consumed by **`snapcore/action-publish@v1`** with
`release: stable,candidate,beta,edge`.
- The snap is built by `snapcore/action-build@v1` (amd64/arm64 native) — see the
`snap-native` job. (ppc64el/s390x/riscv64 go via QEMU/Launchpad; see
[Snap-Core.md](Forks/Snap-Core.md).)
- Cross-repo pushes already exist: the secret **`WEKAN_REPO_TOKEN`** (a GitHub PAT
with write access to *other* `wekan/*` repos) is used to check out and push to the
website/charts repos. The variant repos will reuse this exact pattern.
The **snap name** is whatever `snapcraft.yaml` says at build time — so producing a
differently-named snap is just "edit `name:` before building", and publishing it to a
different Snap Store listing requires only that (a) the name is **registered** and
(b) the store credentials have **ACL** for that name.
---
## 1. One-time manual prerequisites (done once, outside CI)
These cannot be automated in the workflow; do them once with the `wekan` Snap Store
account and the GitHub org owner:
### 1a. Register the two snap names on the Snap Store
```sh
snapcraft register wekan-ondra
snapcraft register wekan-gantt-gpl
```
The names must be available to (or already owned by) the WeKan snap account. Until a
name is registered, publishing it fails. (If a name is taken by someone else, file a
name dispute at https://snapcraft.io/ .)
### 1b. Export Snap Store credentials that can publish ALL THREE names
The current `SNAP_AUTH` macaroon is (likely) scoped to the `wekan` snap only. Re-export
a login whose ACL covers `wekan` **and** both variants:
```sh
snapcraft export-login --snaps wekan,wekan-ondra,wekan-gantt-gpl \
--acls package_access,package_push,package_release,package_update \
snap-auth-all.txt
```
Then update the GitHub secret from that file (see §2). Alternatively, keep `SNAP_AUTH`
for `wekan` and add **separate** per-variant secrets (`SNAP_AUTH_ONDRA`,
`SNAP_AUTH_GANTT`) each scoped to one name — slightly safer blast radius, one more
secret each.
### 1c. Ensure the two GitHub repos exist and are writable by the token
`wekan/wekan-ondra` and `wekan/wekan-gantt-gpl` must exist. The push token (§2) must
have **write (contents)** access to both. Because step 2 of the goal **overwrites**
their code, confirm nothing unique lives only in those repos (the workflow will replace
their tree with `wekan`'s).
---
## 2. GitHub secrets / keys to add (repository or org level, `wekan/wekan`)
| Secret | New or existing | What it is | Why |
|---|---|---|---|
| **`WEKAN_REPO_TOKEN`** | **extend existing** | GitHub PAT (classic: `repo`; or fine-grained: **Contents: Read/Write**) whose repo scope **includes `wekan/wekan-ondra` and `wekan/wekan-gantt-gpl`** | To push the newest `wekan` code into the two variant repos. It already exists for website/charts — just add the two repos to its scope. |
| **`SNAP_AUTH`** | **re-export (widen ACL)** | `snapcraft export-login` macaroon with ACL over `wekan,wekan-ondra,wekan-gantt-gpl` | So `snapcore/action-publish` can push the variant snaps, not just `wekan`. |
| `SNAP_AUTH_ONDRA` / `SNAP_AUTH_GANTT` | **optional alternative** | Per-name `export-login` macaroons | If you prefer separate, narrowly-scoped store credentials per variant instead of one widened `SNAP_AUTH`. |
No other new keys are required — the build uses `snapcore/action-build` (no secret) and
the GitHub Release upload uses the built-in `GITHUB_TOKEN`.
> **Fine-grained PAT note:** a fine-grained token is limited to repos you explicitly
> select — you must add `wekan-ondra` and `wekan-gantt-gpl` to the token's selected
> repositories, or the push 403s. A classic `repo`-scoped PAT covers all org repos the
> user can write.
> **Deploy-key alternative:** instead of one PAT, add a **write deploy key** to each
> variant repo and store the private keys as `ONDRA_DEPLOY_KEY` / `GANTT_DEPLOY_KEY`.
> More secrets, but each key is scoped to exactly one repo.
---
## 3. Workflow changes in `release-all.yml`
Add one **matrix job** that runs after `prepare` (which outputs the version) and does
sync → rename → build → publish per variant. It reuses the existing snap build/publish
actions; only the `name:` edit and the code-sync are new.
```yaml
# ─── Variant snaps: wekan-ondra, wekan-gantt-gpl ────────────────────────────
# Overwrite each variant repo with the newest wekan code at the release tag,
# rename the snap in snapcraft.yaml, build it, and publish all channels.
snap-variants:
needs: [prepare, release] # release = the main GitHub Release exists
continue-on-error: true # a variant must never block the main release
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- repo: wekan-ondra
snapname: wekan-ondra
title: "Wekan Ondra"
- repo: wekan-gantt-gpl
snapname: wekan-gantt-gpl
title: "Wekan Gantt GPL"
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
# 1. Check out the newest wekan code at the release tag.
- name: Checkout wekan at the release tag
uses: actions/checkout@v7
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
path: wekan
# 2. Replace the variant repo's code with wekan's, then commit/tag/push.
- name: Sync wekan code into ${{ matrix.repo }} and push
env:
GH_TOKEN: ${{ secrets.WEKAN_REPO_TOKEN }}
run: |
set -euo pipefail
git clone "https://x-access-token:${GH_TOKEN}@github.com/wekan/${{ matrix.repo }}.git" variant
# Replace tracked content with wekan's tree (keep the variant's .git).
rsync -a --delete --exclude='.git' wekan/ variant/
cd variant
# 3. Rename the snap in snapcraft.yaml (name + human title).
sed -i "s/^name: wekan$/name: ${{ matrix.snapname }}/" snapcraft.yaml
sed -i "s/^title: .*/title: ${{ matrix.title }}/" snapcraft.yaml
git config user.name 'wekan-release-bot'
git config user.email 'release@wekan.team'
git add -A
git commit -m "Sync wekan v${VERSION} and set snap name ${{ matrix.snapname }}" || echo "no changes"
git tag -f "v${VERSION}"
git push origin HEAD:main
git push -f origin "v${VERSION}"
# 4. Build the renamed snap (native amd64; add arm64 runner if desired).
- name: Build the ${{ matrix.snapname }} snap
id: build
uses: snapcore/action-build@v1
with:
path: variant
# 5. Publish to the Snap Store, all channels.
- name: Publish ${{ matrix.snapname }} to the Snap Store
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable,candidate,beta,edge
```
Notes on the job above:
- **Multi-arch:** the snippet builds one arch (the runner's, amd64). To match the main
`wekan` snap's arch coverage, replicate the arch strategy from `snap-native`
(amd64 + arm64 native runners) and, if needed, `snap-qemu`/`snap-launchpad` for
ppc64el/s390x/riscv64 — parameterized by `snapname`. Most users are amd64/arm64.
- **Credentials:** if you chose per-variant secrets (§2), replace `secrets.SNAP_AUTH`
with `secrets.SNAP_AUTH_ONDRA` / `secrets.SNAP_AUTH_GANTT` via the matrix.
- **Version:** `snapcraft.yaml`'s `version:` is already correct because the code came
from the wekan release tag — no version edit needed (the main release's `bump` job
already set it).
- **Destructive push:** `rsync --delete` + `git push origin HEAD:main` makes the variant
repo a mirror of `wekan`. If a variant must keep its own README/patches, exclude them
in the `rsync` (`--exclude`) or apply a patch step after the sync.
---
## 4. What actually gets modified in `snapcraft.yaml`
Only the identity lines change; everything else (base, confinement, parts, apps) is
inherited from `wekan` unchanged:
```diff
- name: wekan
+ name: wekan-ondra # or wekan-gantt-gpl
- title: Wekan
+ title: Wekan Ondra # or Wekan Gantt GPL
```
Optionally also customize `summary:`/`description:`/`icon:` per variant for a distinct
Snap Store listing. `version:` stays as the shared WeKan version.
---
## Checklist
- [ ] `snapcraft register wekan-ondra` and `snapcraft register wekan-gantt-gpl` (§1a)
- [ ] Re-export `SNAP_AUTH` with ACL for all three names — or add `SNAP_AUTH_ONDRA` /
`SNAP_AUTH_GANTT` (§1b, §2)
- [ ] Extend `WEKAN_REPO_TOKEN` scope to include both variant repos — or add per-repo
deploy keys (§1c, §2)
- [ ] Confirm the two variant repos hold nothing that must survive the overwrite (§1c)
- [ ] Add the `snap-variants` job to `release-all.yml` (§3)
- [ ] Run one release and verify both snaps appear at
`https://snapcraft.io/wekan-ondra` and `https://snapcraft.io/wekan-gantt-gpl`
in all four channels
## See also
- [Snap-Core.md](Forks/Snap-Core.md) — snap multi-arch
build constraints (core24 vs the QEMU action).
- [Linux.md](OS/Linux.md) — Snap in the broader Linux update picture.
+251
View File
@@ -0,0 +1,251 @@
# Dev: releasing WeKan to Univention Corporate Server (UCS) from release-all.yml
Based on [docs/Platforms/FOSS/UCS/UCS.md](../../Platforms/FOSS/UCS/UCS.md) and the
[wekan/univention](https://github.com/wekan/univention) app repo.
Goal: from `wekan/wekan`'s **release-all.yml**, also release the **Docker-based UCS App
Center app** for the newest UCS, so UCS users **upgrade from their Update Center**. The
new release must move the app off **WeKan 6.09 + MongoDB 3.x** onto **WeKan 10.x +
FerretDB v1 (SQLite)** (from [wekan/FerretDB](https://github.com/wekan/FerretDB)),
**automatically migrating** the MongoDB 3.x data to FerretDB and the attachments/avatars
to the **filesystem** during the upgrade.
This document lists **what must be added to the WeKan GitHub repos** (secrets, files,
workflow jobs) to make that happen.
## Remaining steps
> **Status:** the `ucs` job **is already wired into `release-all.yml`** — guarded, it skips
> with a `::notice::` until the items below are done. It currently only **bumps the image
> tag** and uploads; the MongoDB 3.x → FerretDB SQLite compose + migration hook still need
> to be written (§2–§4).
1. **Univention App Provider account** with the `wekan` app assigned; get the
`univention-appcenter-control` CLI (App Provider self-service).
2. **Add secrets** `UNIVENTION_APP_PROVIDER_USER` and `UNIVENTION_APP_PROVIDER_PASSWORD`
(see "How to add a secret" below). Optionally `UNIVENTION_REGISTRY_AUTH` only if you
push images yourself instead of letting the App Provider mirror the public image.
3. **Extend `WEKAN_REPO_TOKEN`** to have write access to `wekan/univention`.
4. **Write the FerretDB migration** in `wekan/univention`: the new `component/compose`
(FerretDB SQLite + `data/files` volume), the migration lifecycle step wrapping the two
existing `.mjs` scripts, and fix `component/inst` (the MongoDB `mongo` shell is gone
under FerretDB). See §2–§4.
5. **Finish the upload command** in the `ucs` job — the `univention-appcenter-control
upload` app-version id / file arguments depend on your App Provider account.
6. Test the upgrade on a UCS VM (6.09/MongoDB 3.x → 10.x/FerretDB): boards, attachments,
avatars, LDAP login must survive; confirm the new version appears in the Update Center.
**How to add a secret:** GitHub → the `wekan/wekan` repo → **Settings → Secrets and
variables → Actions → New repository secret**; or `gh secret set NAME --repo wekan/wekan`
(paste the value) / `gh secret set NAME --repo wekan/wekan < file`.
---
## 1. How the UCS app works today (baseline)
The [wekan/univention](https://github.com/wekan/univention) repo is the **UCS App
Center app definition** (not the WeKan source). Key files:
- `component/compose` — Docker **Compose v2** with two services:
- `wekandb`: `docker.software-univention.de/wekan-wekandb:<ver>` running
`mongod --smallfiles --oplogSize 128` (**old MongoDB 3.x**), data volume
`/var/lib/univention-appcenter/apps/wekan/data/db`.
- `wekan`: `docker.software-univention.de/wekan-wekan:<ver>`, `MONGO_URL=mongodb://wekandb:27017/wekan`,
`ROOT_URL=https://<fqdn>/wekan`, and the full **LDAP** env block wired to UCS LDAP
(`DEFAULT_AUTHENTICATION_METHOD=ldap`, cert/UCR templating via `@%@…@%@` / `@!@…@!@`).
- `component/inst` — the **join script**: registers the service, writes the Apache
reverse-proxy vhost (`/wekan` + the sockjs websocket rule), creates `wekan-admin`, and
seeds default settings via `docker exec -i wekan-db mongo`.
- `component/uinst` — uninstall/unjoin.
- `meta/wekan.meta`, `meta/wekan_*.ini` — App Center metadata (ID `wekan`, categories,
description) and the versioned `.ini`.
- `meta/*.svg`, `meta/thumbnail_*.png` — logos/screenshots.
- `component/{all,amd64,i386}/Packages*`, `Release*` — the apt component repo metadata.
**Publishing today** is manual: images are copied into Univention's registry
`docker.software-univention.de/` and the app is submitted via the **Univention App
Provider** portal (the maintainer, xet7, did this for the current version).
The WeKan **Docker image** itself is already built and pushed by release-all.yml's
`docker` job (Docker Hub / Quay / GHCR); the migration tooling already exists in the
WeKan bundle:
- [`snap-src/bin/migrate-mongo3-to-ferretdb.mjs`](../../../snap-src/bin/migrate-mongo3-to-ferretdb.mjs)
— reads a **MongoDB 3.2** DB with the legacy `mongoexport` (the modern driver can't
connect to 3.2) and inserts into a running **FerretDB**; reassembles CollectionFS
**GridFS** attachments/avatars straight to the filesystem.
- [`snap-src/bin/migrate-gridfs-to-fs.mjs`](../../../snap-src/bin/migrate-gridfs-to-fs.mjs)
— moves any remaining GridFS binaries from FerretDB to
`files/attachments/` + `files/avatars/` and rewrites the path fields.
So the pieces exist — this task is **wiring them into an automatic UCS upgrade**.
---
## 2. Target architecture (WeKan 10.x on UCS)
Replace the two-container MongoDB app with a **FerretDB v1 SQLite** app + filesystem
storage. New `component/compose` (sketch — carries over the whole LDAP/ROOT_URL block
unchanged, only the DB and storage change):
```yaml
version: '2'
services:
ferretdb:
image: docker.software-univention.de/wekan-ferretdb:<wekan-version> # FerretDB v1, SQLite handler
container_name: wekan-db
restart: always
environment:
- FERRETDB_HANDLER=sqlite
- FERRETDB_SQLITE_URL=file:/data/db/
expose: [ "27017" ]
volumes:
- /var/lib/univention-appcenter/apps/wekan/data/db:/data/db # SQLite files
wekan:
image: docker.software-univention.de/wekan-wekan:<wekan-version> # WeKan 10.x
container_name: wekan-app
restart: always
ports: [ "8080:8080" ]
environment:
- WRITABLE_PATH=/data
- MONGO_URL=mongodb://wekandb:27017/wekan # points at FerretDB
- ROOT_URL=https://@%@hostname@%@.@%@domainname@%@/wekan
# … the entire existing LDAP_* / MAIL_* / WITH_API block, unchanged …
volumes:
- /var/lib/univention-appcenter/apps/wekan/data/files:/data/files # attachments + avatars on FS
```
Notes:
- **FerretDB v1 SQLite** speaks the MongoDB wire protocol, so WeKan still uses
`MONGO_URL`; only the backend container changes. (Alternatively run WeKan with
`WEKAN_DB=ferretdb` embedded, as the snap does — but a small sidecar keeps the compose
shape and volume layout closest to today's.)
- **Attachments/avatars → filesystem** via the `data/files` volume (`WRITABLE_PATH`),
matching what `migrate-gridfs-to-fs.mjs` writes.
- The WeKan image for UCS must contain the **FerretDB v1 binary** (from
[wekan/FerretDB](https://github.com/wekan/FerretDB) releases, `ferretdb-amd64`) and the
two migration `.mjs` scripts, OR they live in a dedicated **migration image** (below).
---
## 3. The automatic migration on upgrade (MongoDB 3.x → FerretDB SQLite)
UCS Docker App Center apps support **lifecycle scripts** in the app definition:
`update_available`, `preinst`, `store_data`, `restore_data_before_setup`,
`restore_data_after_setup`, `setup`, `inst` (join), `uinst`. The migration runs as a
one-shot during the version upgrade, before the new `wekan` container serves traffic:
1. **`store_data`** (old version) — already keeps the MongoDB 3.x data at
`…/apps/wekan/data/db`.
2. **`restore_data_after_setup`** or a dedicated **migration container** (new version):
1. Start a **temporary MongoDB 3.x** container bound to the old `data/db`
(`SRC_PORT`), read-only.
2. Start **FerretDB v1 SQLite** on the new `data/db` (empty).
3. Run `migrate-mongo3-to-ferretdb.mjs` with
`MONGO_BIN_DIR`=legacy mongoexport, `SRC_PORT`, `SRC_DB=wekan`,
`TARGET_MONGO_URL=mongodb://127.0.0.1:<ferretdb-port>/wekan`,
`FILES_DIR=…/data/files` → text + GridFS files migrated; progress served on
`MIGRATION_PORT`.
4. Run `migrate-gridfs-to-fs.mjs` (`SOURCE_MONGO_URL`=FerretDB, `WRITABLE_PATH=…/data`)
to move any remaining GridFS binaries to `files/attachments` + `files/avatars`.
5. On success, **stop and discard** the temporary MongoDB container; keep the old
`data/db.mongo/` as a backup for one release (safety), then prune.
3. **New compose starts** WeKan 10.x + FerretDB SQLite. WeKan's own **schema migrations**
run on first boot (the migration scripts intentionally leave schema as-is).
Because the migration reuses the exact scripts the snap already ships, the UCS path
should be a thin wrapper (a `migrate` shell entrypoint that sets the env above and calls
the two `.mjs` in order) — package it as a **`wekan-migrate` image** referenced only by
the upgrade step, or bake it into the WeKan image.
---
## 4. What to add to the WeKan GitHub repos
### 4a. `wekan/univention` (the app definition)
- New **`component/compose`** (FerretDB SQLite + `data/files`, §2).
- **Lifecycle/migration scripts**: a `restore_data_after_setup` (or `update_available` +
a migration container) implementing §3, plus the `migrate` wrapper that calls the two
`.mjs` scripts.
- Updated **`meta/wekan_<newstamp>.ini`** / `meta/wekan.meta` (new version, "requires
data migration" note, min UCS version).
- Update **`component/inst`**: the default-settings seeding currently does
`docker exec -i wekan-db mongo …` — MongoDB 3.x `mongo` shell is gone under FerretDB;
replace with a `mongosh`/FerretDB-compatible call or a WeKan REST/settings call.
### 4b. `wekan/wekan` (`release-all.yml`)
Add a **`ucs`** job (after the `docker` job that publishes the WeKan image) that:
1. Checks out `wekan/univention` (using `WEKAN_REPO_TOKEN`, see §5).
2. Regenerates `component/compose` and the `.ini` with the **new release version** and
the new image tags (FerretDB SQLite + files volume).
3. Commits/pushes those to `wekan/univention`.
4. **Publishes to the Univention App Provider** with `univention-appcenter-control`
(upload the app version + component + logos), authenticating with the App Provider
credentials (§5). Univention then mirrors the referenced public WeKan/FerretDB Docker
images into `docker.software-univention.de/` and the new version appears in users'
**App Center / Update Center**.
### 4c. `wekan/FerretDB`
- Ensure a **FerretDB v1 binary/image** for the UCS arch (amd64 at least) is published on
each release (`ferretdb-amd64` already is) so §2's `ferretdb` image / the WeKan image's
embedded binary can be built from it.
---
## 5. GitHub secrets / keys to add (`wekan/wekan`)
| Secret | New or existing | What it is | Why |
|---|---|---|---|
| **`UNIVENTION_APP_PROVIDER_USER`** | **new** | Univention App Provider account (email/username) | Authenticate `univention-appcenter-control` to upload the app version. |
| **`UNIVENTION_APP_PROVIDER_PASSWORD`** | **new** | App Provider password or API token | Same. |
| **`UNIVENTION_REGISTRY_AUTH`** | **new, only if pushing images yourself** | Credentials for `docker.software-univention.de` | Only if you push WeKan/FerretDB images directly instead of letting the App Provider mirror public images. Usually **not needed** — Univention mirrors public Docker Hub/GHCR images at ingestion. |
| **`WEKAN_REPO_TOKEN`** | **extend existing** | GitHub PAT with write to other `wekan/*` repos | Add `wekan/univention` to its scope so the `ucs` job can push the regenerated compose/meta. |
No new Docker Hub/Quay/GHCR secrets are needed — the WeKan image is already published by
the existing `docker` job (`DOCKERHUB_AUTH`/`QUAY_AUTH`/`GHCR_AUTH`), and Univention pulls
the public image.
> **App Provider access:** the account must be a registered **Univention App Provider**
> with the `wekan` app assigned. `univention-appcenter-control` (from the
> `univention-appcenter-dev` tools) is the CLI that uploads app versions; get it and the
> account from https://www.univention.com/products/univention-app-center/ (App Provider
> self-service).
---
## 6. Migration env → script mapping (reference)
| Step | Script | Key env |
|---|---|---|
| Mongo 3.x → FerretDB (text + GridFS) | `migrate-mongo3-to-ferretdb.mjs` | `MONGO_BIN_DIR`, `MONGO_LIB`, `SRC_PORT`, `SRC_DB=wekan`, `TARGET_MONGO_URL`, `FILES_DIR`, `MIGRATION_PORT` |
| Remaining GridFS → filesystem | `migrate-gridfs-to-fs.mjs` | `SOURCE_MONGO_URL` (FerretDB), `WRITABLE_PATH` (→ `files/attachments`, `files/avatars`) |
Both are re-runnable/idempotent (skip files already at destination), so a retried upgrade
is safe.
---
## 7. Checklist
- [ ] Register/obtain a **Univention App Provider** account with the `wekan` app assigned
- [ ] Add secrets `UNIVENTION_APP_PROVIDER_USER` / `UNIVENTION_APP_PROVIDER_PASSWORD`
(and `UNIVENTION_REGISTRY_AUTH` only if pushing images yourself)
- [ ] Extend `WEKAN_REPO_TOKEN` scope to include `wekan/univention`
- [ ] `wekan/FerretDB`: publish the FerretDB v1 SQLite binary/image per release (done for amd64)
- [ ] `wekan/univention`: new FerretDB-SQLite `component/compose` + `data/files` volume
- [ ] `wekan/univention`: migration lifecycle step wrapping the two `.mjs` scripts (§3)
- [ ] `wekan/univention`: fix `component/inst` default-settings seeding (no MongoDB `mongo` shell)
- [ ] `release-all.yml`: add the `ucs` job (regenerate + push app, upload via
`univention-appcenter-control`) (§4b)
- [ ] Test upgrade on a UCS VM: WeKan 6.09/MongoDB 3.x → 10.x/FerretDB; verify boards,
attachments, avatars, and LDAP login survive
- [ ] Confirm the new version appears in the UCS **App Center / Update Center**
## See also
- [docs/Platforms/FOSS/UCS/UCS.md](../../Platforms/FOSS/UCS/UCS.md) — the user-facing UCS page.
- [Snap-Core.md](Forks/Snap-Core.md) — the snap build that already runs the same MongoDB→FerretDB migration.
- [Snap-Ondra-Gantt.md](Snap-Ondra-Gantt.md) — the same "push to a downstream repo + publish"
pattern for variant snaps.
- [Linux.md](OS/Linux.md) — Docker in the broader Linux update picture.
+1 -1
View File
@@ -35,7 +35,7 @@ https://www.youtube.com/watch?v=ke-mbnZM3zE
- Meteor.js 2.x, Node.js 14.x https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4 , MongoDB 6.x
- CPU: amd64, arm64, s390x, Source Bundle at [Raspberry Pi](../Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
- Windows On-Premise: [Offline](../Platforms/Propietary/Windows/Offline.md)
- Mac: Docker, or at [Mac](../Platforms/Propietary/Mac.md)
- Mac: Docker, or at [Mac](../Platforms/Propietary/Mac/Mac.md)
- Newest at platforms: Source Bundle, Snap Candidate, Docker, Kubernetes https://wekan.fi
- Platforms that will be updated to be up-to-date: Snap Stable, Sandstorm, UCS https://wekan.fi
+1 -1
View File
@@ -21,7 +21,7 @@
- https://wekan.fi/install/
- [Raspberry Pi](../Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
- Windows [Offline](../Platforms/Propietary/Windows/Offline.md)
- Mac [Mac](../Platforms/Propietary/Mac.md)
- Mac [Mac](../Platforms/Propietary/Mac/Mac.md)
- Not like other software:
- But note that design of all software changes often, when they are in active development.
- Not bad parts of Trello:
@@ -43,7 +43,7 @@ When you run this, you see .jade template files where is search input box, and .
## Getting Started
- Currently Wekan development is done mainly at Ubuntu 20.10 64bit, but building Wekan does work on any Debian, Ubuntu, WSL Ubuntu 20.04, [Mac](../Platforms/Propietary/Mac.md). Sametime maybe on [Windows](../Platforms/Propietary/Windows/Windows.md) with `choco install -y meteor` and installing packages it then asks for with `meteor add ...` or `meteor npm install --save ...` and then running meteor again.
- Currently Wekan development is done mainly at Ubuntu 20.10 64bit, but building Wekan does work on any Debian, Ubuntu, WSL Ubuntu 20.04, [Mac](../Platforms/Propietary/Mac/Mac.md). Sametime maybe on [Windows](../Platforms/Propietary/Windows/Windows.md) with `choco install -y meteor` and installing packages it then asks for with `meteor add ...` or `meteor npm install --save ...` and then running meteor again.
- Look through old [pull requests](https://github.com/wekan/wekan/pulls)
- Read Wekan source code, you can use some git history viewer GUI like gitk
- Read [Meteor documentation](https://docs.meteor.com/) for [Meteor version](../../.meteor/release) in use, other versions mentioned at [Dockerfile](../../Dockerfile)
+1 -1
View File
@@ -41,7 +41,7 @@ It's a very specific niche, with limited amount competitors, with all of this ap
## Requirements
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
[Mac](../Platforms/Propietary/Mac.md) / [Windows](../Platforms/Propietary/Windows/Install-Wekan-from-source-on-Windows.md).
[Mac](../Platforms/Propietary/Mac/Mac.md) / [Windows](../Platforms/Propietary/Windows/Install-Wekan-from-source-on-Windows.md).
[More Platforms](../Platforms/FOSS/Platforms.md). [ARM progress](https://github.com/wekan/wekan/issues/1053#issuecomment-410919264).
- 1 GB RAM minimum free for Wekan. Production server should have miminum total 4 GB RAM.
For thousands of users, for example with [Docker](../../docker-compose.yml): 3 frontend servers,
+334
View File
@@ -0,0 +1,334 @@
# Design: Universal change History (view + restore) — the basis for Undo/Redo
Status: **Draft for approval** · Owner: xet7 · Related: card details view, Member settings,
`Activities`, `userPositionHistory`, `docs/Features/Undo/Undo.md`
This document specifies one unified **change-history** subsystem that records **every change a user
makes**, keeps it **append-only**, and lets changes be **restored**. It is surfaced from **many menus**, but every one is the **same table + restore** over the **same
store**, differing only by the **scope filter** it passes to `changeHistory.page`. A **History**
option is added to each of these menus (and the pattern generalises to any future entity menu — the
UI and method don't change, only the scope):
| Menu / location | View shows the history of… | Scope filter | Section |
| --- | --- | --- | --- |
| Card group menu (open card) | that **group** on that card, with a per-contributor avatar list | `{ cardId, group }` | 7 |
| Card menu (open card) | the **whole card** (all its groups) | `{ cardId }` | 7a |
| Member settings menu | that **user's** own changes | `{ userId }` | 7a |
| Board Settings | the whole **board** (all users/entities) | `{ boardId }` | 7a |
| Swimlane menu | that **swimlane** and its contents | `{ scope:'swimlane', scopeId }` | 7a |
| List menu | that **list** and its cards | `{ scope:'list', scopeId }` | 7a |
| *(future)* Checklist menu, Attachment menu, … | that entity | `{ entityType, entityId }` | 7a |
Scopes **nest**: a card's history ⊂ its list's ⊂ its swimlane's ⊂ the board's. Container scopes
(board/swimlane/list/card) therefore mean "this entity **and its descendants**" — implemented as an
OR over the relevant id columns (`boardId` / `swimlaneId` / `listId` / `cardId` / `entityId`), which
is why the write side (section 5) stores all the applicable id columns on every row.
Plus one keyboard front-end:
- **Undo / Redo** — `Ctrl+Z` / `Ctrl+Y` = "restore the current user's most recent change from this
history" / "re-apply it". Undo/Redo is therefore **not a separate feature**; it is the keyboard
restore of the newest own change. (v1 undo/redo of *position moves* already shipped in #6478 via
`userPositionHistory`; this design **generalises** that to every change and merges the two.)
It is a design doc only — no code is implied as final until this is approved.
> **Scope change (this revision):** the earlier draft covered only card *groups*. Per request, the
> model now records **every change** (all card fields/groups **and** board/list/swimlane/etc.
> structural changes), per user, and adds the Member-settings per-user view. `userPositionHistory`
> becomes a special case that this unified store supersedes.
---
## 1. Goal (from the request)
On the card open-details view, each group menu gains an option **History**. Clicking it opens a
big popup:
- **Left column** (always visible):
- Top: **History** entry — the current, newest history for that group.
- Below: an **avatar** (or initials when there is no avatar) for **each user** who has added or
changed something in that group. Clicking an avatar shows **all changes that user made to that
group**, newest first.
- **Right area** (always the same table layout):
- **Above the table** (LTR, left → right): **Search** (left), **pagination controls** (middle),
**Restore** (right).
- **Table columns** (LTR, left → right): **select checkbox** (per row), **what was changed**
(translatable — added / removed / edited / …), **content of change** (e.g. the new text),
**datetime** (formatted with the card's selected date format).
- Only the **current page** is loaded/rendered (server-side pagination).
- **RTL**: every element and the text order is mirrored (search right, pagination middle, restore
left; columns reversed).
- **Restore**: restores the selected change to that group. The restore is itself recorded in
history — on **both** the source contributor's history *and* the restorer's history (who restored
what to become current). History is **append-only**: you can restore *from* it, but never edit it.
## 2. The core blocker (why this is more than a UI)
WeKan already logs actions in the **`Activities`** collection, but that collection records *what
happened* (an `activityType` plus references: `cardId`, `listId`, `memberId`, …) — **not the
before/after values**. Example: editing a description logs `activityType: 'changedDescription'`
with the card id, but **not the previous text**.
Consequences:
- The **viewer** (what changed, when, by whom) can largely be built from `Activities`.
- **Restore** generally **cannot** be built from `Activities`, because the previous content is not
stored. Only `userPositionHistory` stores before/after (for positions).
Therefore the feature splits into two efforts:
1. A **content-versioning subsystem** that captures before/after content per group change (the large,
invasive part; prerequisite for restore).
2. The **history viewer + restore UI**.
## 3. Groups and change types
"Group" maps to the sections of the card details view. Proposed coverage (phase order later):
| Group | Entity / field | Change types |
| --- | --- | --- |
| Description | `cards.description` | edited |
| Title | `cards.title` | edited |
| Labels | `cards.labelIds[]` | added, removed |
| Members / Assignees | `cards.members[]`, `cards.assignees[]` | added, removed |
| Dates | received/start/due/end | added, edited, removed |
| Checklists | checklist + items | added, removed, edited, checked/unchecked, moved |
| Subtasks | linked subtask cards | added, removed, moved |
| Attachments | files/avatars | added, removed, renamed |
| Comments | `card_comments` | added, edited, removed |
| Custom fields | `cards.customFields[]` | edited |
Change types are a small closed set with i18n keys, e.g. `history-change-added`,
`history-change-removed`, `history-change-edited`, `history-change-moved`,
`history-change-restored`. Added to `imports/i18n/data/en.i18n.json` only (translations follow via
Transifex; see the translation-pull auto-heal note in the changelog).
## 4. Data model
One new append-only Mongo collection **`changeHistory`** (working name) covering **every** change,
whatever the entity. One document per change:
```js
{
_id,
boardId, // for permission scoping + publications (null for non-board changes, if any)
// What was changed — general, not card-only:
entityType, // 'card' | 'list' | 'swimlane' | 'board' | 'checklist' | 'checklistItem'
// | 'comment' | 'attachment' | 'customField' | ...
entityId, // the changed entity's _id
cardId, // set when the change belongs to a card (drives the card-group view); optional
group, // logical group for the card view: 'description' | 'labels' | 'members'
// | 'dates' | 'checklists' | 'title' | ... (optional for non-card changes)
changeType, // 'added' | 'removed' | 'edited' | 'moved' | 'restored'
// Content for display + restore — structured, not just strings:
previousContent, // blackbox; null for 'added'
newContent, // blackbox; null for 'removed'
userId, // WHO made the change — the axis the Member-settings view filters on
createdAt, // Date; formatted client-side with the viewer's/card's date format
// Undo/redo stack (folds in #6478's userPositionHistory fields):
undone, // Boolean — restored/undone, redoable until superseded
undoneAt, // Date — orders the redo stack
batchId, // groups a multi-entity change (e.g. multi-select move / multi-restore)
// Restore provenance (set only when changeType === 'restored'):
restoredFromId, // the changeHistory _id whose content was restored
restoredByUserId, // who performed the restore
}
```
Notes:
- **Append-only.** No client-exposed update/remove (except the internal `undone`/`undoneAt` flip for
the undo/redo stack). Retention cap via a server cron (à la `userPositionHistory.cleanup`).
- `previousContent`/`newContent` are `blackbox` so each entity/group stores what it needs
(`{ text }` for description, `{ labelId }` for a label, `{ millis }` for a date, `{ sort,
swimlaneId, listId, boardId }` for a move, …).
- **Supersedes `userPositionHistory`.** That collection's move rows map 1:1 onto this schema
(`entityType` card/list/swimlane, `changeType: 'moved'`, previous/new = the position). Migration:
keep `userPositionHistory` writing during transition, or one-time copy its rows in; the undo/redo
methods move to read `changeHistory`.
- Alternative considered: **extend `Activities`** with before/after content. Rejected for v1 —
`Activities` is deliberately schemaless/high-volume and drives notifications/webhooks; overloading
it risks those paths. A dedicated collection keeps concerns separate and independently cappable.
## 5. Write side (recording every change)
Record on the **server**, in **every** mutation path (not only card groups), capturing the value
**before** and **after**. A single helper:
```js
ChangeHistory.record({ boardId, entityType, entityId, cardId?, group?, changeType, previousContent, newContent, userId, batchId? });
```
called from the existing setters/methods — card fields (`Cards.setDescription`, title, label
add/remove, member/assignee add/remove, date setters, custom fields), card sub-entities
(checklist/checklist-item, comment, attachment mutations), and **structural** changes (list/swimlane
create/rename/move/archive, board-level changes). Position **moves** come in via the same helper
(replacing `userPositionHistory.trackChange`).
- **Best-effort:** never fail the mutation if recording throws (try/catch).
- **Import the collection** — do not reference it as an assumed global. The shipped position history
was inert precisely because its guard `typeof UserPositionHistory !== 'undefined'` was false
without an import (fixed in #6478). This is the single most important implementation lesson.
- Consider a thin, central choke point: many mutations already emit an `Activity`; recording history
next to `Activities.insert` (with the extra before/after content) avoids sprinkling calls
everywhere. Evaluate during phase 1.
## 6. Read side (one paginated/searchable method for all three views)
Only the current page is loaded. One method (not a naive reactive publication of the whole log)
serves the card-group view, the per-user Member-settings view, and any filter combination:
```js
Meteor.call('changeHistory.page', {
// scope (any subset; container scopes match the entity AND its descendants):
scope, // 'board' | 'swimlane' | 'list' | 'card' — the container kind
scopeId, // that container's _id (boardId / swimlaneId / listId / cardId)
group, // narrow a card scope to one group (card-group view)
userId, // one contributor — Member view, or an avatar click within another scope
// list controls:
search, // matches changeType label + content text
page, pageSize, // 1-based page, server clamps pageSize
}) -> { rows, total, page, pageSize, contributors: [{ userId, count }] }
```
The server turns `{scope, scopeId}` into the id-column filter (`board`→`boardId`; `swimlane`→
`swimlaneId` OR its lists'/cards' rows; `list`→`listId` OR its cards'; `card`→`cardId`), then applies
`userId`/`group`/`search` on top. Member-settings view passes just `{ userId }` (optionally
`+ scope:'board'` to limit to the current board).
- **Permission:** caller must have board-visible access to the scoped board(s) (reuse
`requireBoardVisible`). The Member-settings view is scoped to boards the **caller** can see; it
never leaks a user's changes on boards the caller can't access.
- **Search:** case-insensitive over the rendered change-type label and a text projection of
`newContent`/`previousContent` (same cross-environment numeric/text caveat as card search).
- **contributors** powers the card view's left-column avatar list (distinct `userId` + counts);
unused when the view is already pinned to one `userId`.
- Pure, unit-testable helpers (mirroring `models/lib/undoRedoSelection.js`): `paginate(rows, page,
size)`, `matchesSearch(row, term)`, `selectionToIds(selected)` — in `models/lib/…` with tests.
## 7. UI
Templates (Blaze/jade), all inside one popup opened from the group menu's **History** item:
- `cardGroupHistoryPopup` — 2-pane layout.
- Left: `historyNav` — a **History** button (default view = newest, all users) + a list of
`+userAvatar` (fallback initials) from `contributors`. Selecting one sets the `userId` filter.
- Right: `historyTable` —
- top bar: `.js-history-search` (left) · pagination `.js-history-prev/next` + "page X / N"
(middle) · `.js-history-restore` (right).
- table: header row + one row per `rows[]`: `input[type=checkbox].js-history-select`,
change-type label (`{{_ changeTypeKey}}`), content, `{{ formatWithCardDateFormat createdAt }}`.
- State: current `group`, `userId` filter, `search`, `page`, and a `Set` of selected row ids kept in
a `ReactiveDict` on the template instance (not the data context — see #6479: don't stash state on
Blaze data contexts).
- **RTL:** rely on `dir=rtl` + logical CSS (`margin-inline`, `text-align: start`, flex order) so the
top bar and columns mirror without duplicated markup. Verify live.
- **Date format:** reuse the card's configured date format helper so the datetime column matches the
rest of the card.
## 7a. Scoped views (card / member / board / swimlane / list / …)
Every non-card-group surface in the table above is **the same `historyTable`** with a different
`changeHistory.page` scope; there is **one** implementation, parametrised by scope. They share the
columns, search, pagination, RTL, and restore (+ dual re-logging) of section 7/8.
- **Contributor avatars** (left column) appear whenever the scope can span more than one user —
card, board, swimlane, list. They are omitted for the Member view (already one user). Clicking an
avatar adds `{ userId }` to the current scope (that user's changes **within** this scope).
- **Permission** per scope, reusing existing guards:
- card / swimlane / list → board-visible (write access to restore);
- board (Board Settings) → board **admin**;
- member (Member settings) → the user themselves, or an admin over boards the admin can see. A
Member view **never** leaks changes on boards the caller can't access.
- **Nesting** (see the table note): container scopes match the entity **and its descendants** via an
OR over id columns, so a swimlane's view includes its lists'/cards' changes, etc.
Concretely, adding "History" to a new menu = (1) a menu item that opens `historyPopup` with a scope,
(2) — nothing else. No new method, table, or restore code.
## 7c. Undo / Redo = restore the last own change
`Ctrl+Z` / `Ctrl+Y` are the keyboard front-end to this history for the **current user on the current
board**:
- **Undo** = restore the caller's **most recent, not-yet-undone** `changeHistory` row (mark it
`undone`), for **any** `entityType`/`changeType` — not just moves.
- **Redo** = re-apply the caller's **most-recently-undone** row.
- A **new change clears the redo stack** (delete/flag this user+board's `undone` rows).
This **generalises** the shipped #6478 methods: `userPositionHistory.undoLast/redoLast` become
`changeHistory.undoLast/redoLast` reading the unified store; the selection rule stays the pure,
tested `pickUndo`/`pickRedo`; the key bindings in `client/lib/keyboard.js` are unchanged. "Restore
selected row" (History UI) and "undo last" (keyboard) are the **same operation** on the same data.
> Undo restores content via the **same setters** as a normal edit (so validation/Activities run),
> and the restore is itself appended to history (see section 8) — so undo is auditable and itself
> undoable/redoable.
## 8. Restore
`Meteor.call('cardGroupHistory.restore', historyId)`:
1. Load the target history row; require board write access.
2. Apply `previousContent` (or `newContent`, per the row's semantics) back to the live group via the
**same setters** used for normal edits (so validation/activities still run).
3. Append **two** `cardGroupHistory` rows with `changeType: 'restored'` and `restoredFromId`:
- one attributed to the **source contributor** (`userId` of the restored row) — "their" data was
restored,
- one attributed to the **restorer** (`Meteor.userId()`) — who restored what to become current.
4. Restore of **multiple selected** rows applies in a defined order (oldest→newest of the selection)
and is a single logical batch (shared `batchId`).
5. History rows themselves are **never edited or deleted** by restore.
Per-change-type restore functions live next to each group's setter (description/labels/dates/…), so
each knows how to re-apply its own `previousContent`.
## 9. Security & integrity
- All read/restore methods gate on board access (`requireBoardVisible` / write access), like
`updateListSort` and `userPositionHistory.*`.
- No client-exposed update/delete on `cardGroupHistory` (append-only invariant).
- Retention cap per card/board via a server cron (reuse the `userPositionHistory.cleanup` pattern).
## 10. Phasing (each phase verified live before the next)
1. **`changeHistory` model + write helper + pure helpers (paging/search/selection/pick undo-redo) +
unit tests.** Migrate the shipped position undo/redo onto it: point
`changeHistory.undoLast/redoLast` at the new store and record card/list/swimlane **moves** there
(this both proves the model and keeps #6478 working). Ctrl+Z/Ctrl+Y now read `changeHistory`.
2. **First content group — Description:** record before/after on edit; **read method**
(`changeHistory.page`) + the **viewer UI** (table, search, pagination, avatars); LTR then RTL.
Ctrl+Z now also undoes a description edit.
3. **Restore** UI for Description (single + multi-select) with dual re-logging (identical to the
keyboard undo path).
4. **Member-settings "History"** (per-user) and **Board-settings "History"** (per-board) views —
both are just the same `changeHistory.page` method + `historyTable` UI with a different scope
(`{ userId }` vs `{ boardId }`), so they land together once step 3's table/restore exist.
5. **Roll out** to every remaining group/entity (title, labels, dates, members/assignees, checklists,
subtasks, attachments, comments, custom fields, board/swimlane structural changes), one per PR,
reusing the shared write helper + `page` method + UI.
6. **"History" menu item** wired into every card group menu (and the Member-settings menu from
step 4).
## 11. Open questions (need product decisions)
- Scope of "content of change" for non-text groups (label = its name/color? member = avatar+name?
attachment = filename?). Proposal above; confirm.
- Retention: how many history rows per card/board before pruning?
- Does history need to survive card **archive/restore** and **copy/move** across boards?
- Should restore of a **removed** entity re-create it (attachments, comments) or only text/scalar
fields in v1?
- Per-board on/off setting for history (storage cost)?
---
### Appendix: lessons already banked from this session
- **Import your collection helpers.** `userPositionHistory` recording was dead because
`trackChange` was guarded by `typeof … !== 'undefined'` without importing the collection (fixed in
#6478). The new history helper must be a real import.
- **Don't stash state on Blaze data contexts.** #6479 showed a re-render drops ad-hoc fields; keep
popup/table state on the template instance / `ReactiveDict`.
- **Pagination/search/selection as pure functions** (à la `models/lib/undoRedoSelection.js`) so the
logic is unit-testable without the Meteor/Blaze runtime.
+371
View File
@@ -0,0 +1,371 @@
# Feature: Undo / Redo (position moves) + high-impact confirmation
Status: **Implemented (v1)** in #6478 · Owner: xet7 · Related: `userPositionHistory`,
card/list/swimlane drag-and-drop, `docs/Features/History/History.md`
This documents the undo/redo feature **shipped in v1** for #6478 ("UI: Confirmation for actions with
impact") — which covers **position moves** (card / list / swimlane) only.
> **Direction (approved design):** undo/redo is being folded into one **universal change-history**
> subsystem that records **every** change and exposes it as History views on card groups, the whole
> card, Member settings, Board Settings, swimlane/list menus, etc. In that model, **Undo/Redo is not
> a separate feature** — `Ctrl+Z`/`Ctrl+Y` simply restore the current user's most recent change from
> that history, for **any** change type, and `userPositionHistory` is superseded by the unified
> `changeHistory` store. The single source of truth for that direction is
> **`docs/Features/History/History.md`**; this doc describes the current v1 (positions) and the
> migration path. See History.md §7c (Undo/Redo) and §10 (phasing: position undo/redo migrates onto
> `changeHistory` first).
---
## 1. Motivation
A user accidentally dragged a **list** into another swimlane on mobile, merging it with an existing
list, and spent ~15 minutes recovering — there was **no confirmation** and **no undo**. This feature
adds both for the move operations, using the already-present (but previously **inert**) position
history.
## 2. What ships in v1
- **Undo** with `Ctrl+Z` (`⌘Z`) and **Redo** with `Ctrl+Y` / `Ctrl+Shift+Z` (`⌘⇧Z`) for the last
**position change** on the current board: moving a **card**, **list**, or **swimlane**.
- A **confirmation** prompt before moving a **list to a different swimlane** on **touch / small
screens** (the exact, card-re-homing accident). Declining reverts the drag. Everyday same-swimlane
reordering is unaffected.
## 3. Data model — `userPositionHistory`
Pre-existing collection (`models/userPositionHistory.js`), one document per position change:
```
userId, boardId, entityType ('card'|'list'|'swimlane'|'checklist'|'checklistItem'),
entityId, actionType ('move'|'create'|'delete'|'restore'|'archive'),
previousState / newState (blackbox) + flattened previous*/new* (Sort, SwimlaneId, ListId, BoardId),
createdAt, isCheckpoint, checkpointName, batchId,
undone (Boolean), undoneAt (Date) // <- added in #6478 for the redo stack
```
Helper methods on a document:
- `canUndo()` — the entity still exists.
- `undo()` — restore the **previous** position (per entityType).
- `redo()` — **added in #6478**: re-apply the **new** position (mirror of `undo()`).
## 4. The bug that made undo inert (fixed in #6478)
`UserPositionHistory.trackChange(...)` is server-only, but callers referenced it as an **assumed
global** guarded by `typeof UserPositionHistory !== 'undefined'`. It is an ES-module default export,
**not** a global, so the guard was **false** and **nothing was ever recorded** — undo silently did
nothing. Fix: **import the collection** where `trackChange` is called.
> Lesson (also in the History doc): import your collection helpers; never gate on an assumed global.
## 5. Recording changes (write side)
`trackChange({ userId, boardId, entityType, entityId, actionType, previousState, newState })` inserts
a history row. Wired at:
- **Card moves** — `models/cards.js` (`Card.move`), already present (now actually runs).
- **List moves** — `server/models/lists.js` `updateListSort` (**added in #6478**; imports the
collection). Captures `{ sort, swimlaneId, boardId }` before the update.
- **Swimlane moves** — the model supports `entityType: 'swimlane'`; wire the swimlane reorder path
the same way when needed (follow-up).
Recording is **best-effort**: a failure to record never fails the move (wrapped in try/catch).
## 6. Undo / redo stack semantics
The undo stack is the board's real (non-checkpoint) changes in `createdAt` order; the redo stack is
the changes that have been undone.
- **Undo** picks the **most recent not-yet-undone** change, calls `undo()`, sets
`undone: true, undoneAt: now`.
- **Redo** picks the **most-recently-undone** change (largest `undoneAt`), calls `redo()`, clears
`undone`.
- **A new real change clears the redo stack** — `trackChange` deletes this user+board's `undone`
rows, so an undone-then-superseded change can never be redone into stale state.
Server methods (`server/models/userPositionHistory.js`):
- `userPositionHistory.undoLast(boardId)` → `{ undone, entityType, entityId }`
- `userPositionHistory.redoLast(boardId)` → `{ redone, entityType, entityId }`
Both gate on `requireBoardVisible`. The selection rule is a **pure, unit-tested** helper
`models/lib/undoRedoSelection.js` (`pickUndo` / `pickRedo`, tests in
`tests/undoRedoSelection.test.cjs`); the server applies the same rule via Mongo `sort`/`limit`.
## 7. Client bindings
`client/lib/keyboard.js` binds `ctrl+z, command+z` → `undoLast` and
`ctrl+y, ctrl+shift+z, command+shift+z` → `redoLast`, calling the method for
`Session.get('currentBoard')`, only when `Utils.canModifyBoard()`. The global hotkeys **filter**
already disables shortcuts inside inputs / textareas / contentEditable, so **native text undo/redo
keeps working** while typing.
## 8. High-impact confirmation
In `client/components/swimlanes/swimlanes.js` `saveSorting` (the list-drop handler): when the drop
is a move to a **different** swimlane (`isDifferentSwimlane`) **and** the viewport is touch/small
(`Utils.isMiniScreen() || Utils.isTouchScreenOrShowDesktopDragHandles()`), a `window.confirm`
(i18n `confirm-move-list-to-swimlane`) is shown; declining re-subscribes to the board (reverting the
drag) and aborts before any card re-homing. This complements undo (belt-and-suspenders for the
riskiest, hardest-to-notice action).
> Interaction with #6484: board-wide lists (`swimlaneId === null`) are no longer *accidentally*
> bound to a swimlane on a nudge, so this confirm only fires for genuinely swimlane-scoped list moves
> across swimlanes.
## 9. Retention
`userPositionHistory.cleanup` (server cron) caps history per user/board (keeps ~1000 newest
non-checkpoint rows). Toggle via `Meteor.settings.public.enableHistoryCleanup`.
## 10. Limitations / not in v1
- Undo/redo covers **position moves only** — not text edits, labels, members, dates, etc. Those need
before/after content versioning (see `docs/Features/History/History.md`).
- No on-screen Undo/Redo **buttons** yet (keyboard only) — a board-header control + a post-move
"Undo" toast are natural follow-ups.
- Swimlane-move recording is modelled but the reorder path is not yet wired.
- Cross-user concurrency: undo restores the previous stored position; if another user moved the same
entity meanwhile, the restore uses this history row's previous values (last-writer-wins, as
elsewhere in WeKan).
## 11. Tests
- `tests/undoRedoSelection.test.cjs` — pure `pickUndo`/`pickRedo` incl. LIFO symmetry, checkpoint
and already-undone skipping, and the redo-by-`undoneAt` order.
- Runtime paths (keyboard, Meteor methods, Blaze) require a live app to verify.
## 12. Extension checklist (adding a new undoable move)
1. In the mutation path, capture `previousState` **before** the write and call
`UserPositionHistory.trackChange({ … })` **after** — importing the collection, wrapped in
try/catch.
2. Ensure `undo()`/`redo()` and `canUndo()` handle the `entityType` (add a `case` if new).
3. Nothing else — `undoLast`/`redoLast`, the redo-stack clearing, and the key bindings are generic.
---
# Soft delete (delete = mark, never destroy)
Status: **Lists implemented (v1) · rest design** · Owner: xet7 · Related: #1023 ("undo button for
deleted lists etc"), History.md, the `archived` flag, `userPositionHistory`
(`actionType: 'delete'|'restore'`)
> **Implemented so far (v1):** deleting a **list** is now a soft delete — the list and its cards are
> marked (`deletedAt`/`deletedBy`/`deleteBatchId`), hidden from the board, recorded as a reversible
> `delete` in history, and restorable via `lists.restore` **or** undone with `Ctrl+Z`. This delivers
> **#1023** for lists. Core pieces that are live: `models/lib/softDelete.js` (pure helpers +
> `tests/softDelete.test.cjs`), the `enablePermanentDelete` feature flag + `Settings` field, the
> `deletedAt/deletedBy/deleteBatchId` schema fields on Lists & Cards, `deletedAt: null` filtering on
> the board/swimlane list render paths, the `lists.softRemove` / `lists.restore` / `lists.purge`
> methods (purge gated by `canPurge`), the client + REST delete paths, and `undo()/redo()` handling
> of `delete`/`restore`. **Still design:** extending the same pattern to swimlanes/boards/cards/
> checklists/comments/attachments, the Recycle Bin UI, and the Admin Panel / Features / Delete table
> (§16b–§16c).
> **Principle:** *Every* user-facing delete is a **soft delete** — it **marks** the document deleted
> instead of removing it. There is **no permanent delete in ordinary use**. Physical removal exists
> only behind **one narrow, guarded purge path** (below). This is the foundation that makes
> **undoing a deletion** possible (#1023): if nothing is destroyed, anything can be restored.
## 13. Why this exists / relation to undo & history
Undo v1 (§1–§12) restores **positions**. It cannot bring back a **deleted** list/card/swimlane
because the delete path physically `removeAsync`-es the rows — there is nothing left to restore. Soft
delete closes that gap: a delete becomes a reversible state change, so:
- **Undo of a delete** = clear the deleted mark (exactly like `undo()` clears a move), satisfying
**#1023**.
- **History** (History.md) records the `delete` and `restore` events like any other change; the
`userPositionHistory` schema **already** allows `actionType: 'delete'|'restore'|'archive'`.
- A **Recycle Bin** view can list soft-deleted items per board/user for explicit restore, independent
of the keyboard undo stack.
## 14. `archived` vs `deleted` — two distinct states
WeKan already has **archive** (`archived: true` + `archivedAt`) as a *user-visible, first-class*
"put it aside" state with its own UI (the Archive). **Soft delete is a separate, deeper state** —
"the user asked to delete this":
| State | Field(s) | Visible where | Meaning |
|--------------|-----------------------------|------------------------------|----------------------------------|
| Active | `deletedAt: null` | Board | Normal |
| Archived | `archived: true` | Archive view | Set aside, still "kept" |
| Soft-deleted | `deletedAt`, `deletedBy` | Recycle Bin only | Deleted, awaiting restore/purge |
Do **not** overload `archived` for delete — they have different UIs, different retention, and an item
can be archived *and* then deleted. New fields on every soft-deletable collection:
```
deletedAt (Date, default null) // null = live; set = soft-deleted
deletedBy (String userId, optional)
deleteBatchId (String, optional) // groups a cascade (list + its cards) into one restorable unit
```
## 15. Scope — what gets soft delete, what is exempt
**Soft-deletable (user content):** Boards, Swimlanes, Lists, Cards (incl. subtasks), Checklists,
ChecklistItems, CardComments, Attachments, Rules/Triggers/Actions, Labels, CustomFields,
Integrations, plus board-scoped settings the user can delete.
**Exempt (transient / infrastructure — hard delete stays):** `SessionData`, the `CronJob*`
collections, `TrelloImportJobs`, `InvitationCodes`, `UserPositionHistory`/`changeHistory` retention
cron, and *cascade bookkeeping* like `Activities` cleanup. These are not user content; soft-deleting
them would only bloat storage with no restore value. `log()`-style note: exemptions are **explicit**,
not accidental — each exempt `removeAsync` should carry a short `// hard-delete OK: transient` comment
so the audit is greppable.
## 16. The one narrow purge path (the only permanent delete)
Physical `removeAsync` of user content is allowed from **exactly two** places, both guarded and
audited:
1. **GDPR / account erasure** — deleting a user (`Users` erasure) may hard-delete or **anonymize**
that user's personal data. Legally required; cannot be soft-only, so it is **not** gated by the
toggle below.
2. **Explicit Global-Admin "Permanently delete"** — an admin action on a soft-deleted item (or
"empty Recycle Bin") that consciously destroys it, **gated by an Admin Panel setting** (below).
### 16a. Admin Panel / Features / Delete → "Enable permanent delete for Global Admin"
Permanent delete of user content is **off by default**. A new toggle lives at **Admin Panel /
Features / Delete**:
```
Admin Panel
└─ Features
└─ Delete
[ ] Enable permanent delete for Global Admin
(When off, nothing can be physically deleted from the Recycle Bin —
all deletes stay soft/restorable. GDPR account erasure is unaffected.)
```
- Stored as a feature flag alongside the existing ones (`models/lib/featureFlags.js` /
`getFeatureFlags`, the same mechanism the activities publication reads), e.g.
`enablePermanentDelete` on `Settings`.
- **Only a Global Admin** sees the "Permanently delete" / "Empty Recycle Bin" actions, and **only
when the flag is on**. Board admins never get it. Both the client (hide the action) and the server
(the `purge()` method re-checks `Meteor.user().isAdmin` **and** the flag) enforce it — client
hiding alone is not a guard.
- The physical-delete helper (`purge()`) is the *only* code allowed to call `removeAsync` on user
content; it hard-fails unless `isAdmin && enablePermanentDelete`, and logs an audit `Activity`
(`activityType: 'permanentDelete'`).
### 16b. The Delete panel is a History-style category table
**Admin Panel / Features / Delete** is not just the toggle — it renders a **History-design table**
(same layout as History.md: paginated, searchable, actor avatar, timestamp, RTL-aware) that lists
**everything currently soft-deleted, grouped by category**. It works whether or not permanent delete
is enabled:
- **Flag off:** the table is **read-only insight** — the admin can see what has been deleted (and how
much storage it holds) and could **Restore**, but the "Permanently delete" / "Empty" actions are
hidden/disabled.
- **Flag on:** each row and each category header gains a **checkbox**; the Global Admin **selects
what to delete permanently** and confirms. Selection is per-row or whole-category ("select all in
*Cards*").
Columns: category · title/description · board · deleted by (avatar) · deleted at · size · actions
(Restore / Permanently delete). A server method `softDelete.listDeleted(category, boardId, page)`
feeds it with the same pagination discipline as the activities feed (#2539) — only the visible page
is loaded, so a huge trash never slows the panel.
### 16c. GDPR categories — what a Global Admin may permanently delete, and what each purge must include
The category table is organized around the data classes GDPR erasure and data-minimization actually
require an operator to be able to remove. **Permanently deleting a category deletes the whole object
graph below it** — a purge that leaves orphaned children behind is a data leak, so each category
lists what *must* be included:
| Category | What it is | A permanent delete MUST also remove |
|----------|------------|-------------------------------------|
| **User account (PII)** | A person's identity: profile (name, email, avatar), login/OAuth/LDAP identifiers, sessions, API tokens, notification/subscription rows | Their avatar blob; memberships stripped from every board; **authored content anonymized or reassigned** (comments/activities → "Deleted user") per the board owner's choice; personal settings, invitation codes, password-reset tokens |
| **Boards** | A whole board | Its swimlanes, lists, cards (+ subtasks), checklists & items, card comments, labels, custom-field *values*, rules/triggers/actions, activities, and **all attachments' physical blobs** (S3/GridFS), board background images |
| **Swimlanes / Lists** | A column or row | Their cards and everything under those cards (as in Boards), via the `deleteBatchId` cascade |
| **Cards** | A single card (+ subtasks) | Its checklists & items, comments, activities, attachments (physical blobs), custom-field values, votes, time entries |
| **Attachments** | Files | The **physical bytes** in the storage adapter (S3 / GCS / Azure / GridFS / filesystem), not just the metadata row |
| **Comments & Activities** | Free-text that may contain PII | The rows themselves; referenced attachment blobs if any |
| **Custom-field values** | Per-card values (may hold PII) | The values on cards; definitions remain unless the field itself is deleted |
Rules for the purge implementation:
- **Cascade completeness:** deleting a parent purges the entire subtree; never leave orphaned cards,
checklist items, comments, or (critically) **attachment blobs** — those live in external storage and
are the easiest thing to leak.
- **PII beyond content:** account erasure must also clear the identity/auth surface (sessions, tokens,
OAuth/LDAP links, avatar), not only board content.
- **Anonymize vs. delete:** for authored content that others still rely on (a comment thread), the
operator chooses **anonymize** (reassign to a tombstone "Deleted user", scrub the PII) instead of a
hard delete — GDPR is satisfied by removing the *personal data*, not necessarily the row.
- **Audit, not silent:** every permanent delete writes an audit `Activity` (who, what category, how
many objects, when). The trash is never emptied on a timer.
Everything else **must** soft-delete. To keep this enforceable:
- Route user-content deletes through a single helper (§17), never a bare `removeAsync`.
- **No time-based auto-purge.** Trash is emptied only by the flag-gated Global-Admin action or GDPR
erasure — the "one narrow purge path" decision, chosen over a retention-window auto-purge, so
nothing ever disappears on a timer.
## 17. Mechanism — a `softRemove` helper + query default
Replace user-content `removeAsync` call sites (see the ~40 sites in `models/` and `server/models/`)
with a soft-delete helper that mirrors today's cascade but *marks* instead of destroys:
```js
// document method, e.g. on Lists / Cards / Swimlanes …
async softRemove(userId, batchId) {
const at = /* server time */;
await this.collection.updateAsync(this._id, {
$set: { deletedAt: at, deletedBy: userId, deleteBatchId: batchId },
});
// cascade: soft-remove children with the SAME batchId (a list marks its cards, etc.)
// record history: UserPositionHistory.trackChange({ actionType: 'delete', … })
}
```
Reads must hide soft-deleted docs **by default**. Two enforcement layers:
- **Publications / server queries** add `deletedAt: null` to their selectors (a shared
`notDeleted(selector)` helper keeps this uniform and testable). The Recycle Bin publication is the
one place that *opts in* to `deletedAt: { $ne: null }`.
- **Cascade & counts** (list card counts, board "N cards", exports) filter `deletedAt: null` too, so a
soft-deleted card doesn't inflate totals.
Restore is the inverse: `$unset` the three fields for the whole `deleteBatchId` (so restoring a list
brings back exactly the cards deleted with it, not ones deleted separately later).
## 18. Undo/redo & History integration
- **Undo of a delete:** `trackChange({ actionType: 'delete' })` on soft-remove; `undo()` gains a
`delete` case that clears the delete mark for the `deleteBatchId`; `redo()` re-applies it. This
extends the generic stack in §6 — `undoLast`/`redoLast` need no change.
- **History:** delete/restore appear in History.md's unified feed with actor avatar + timestamp; the
Recycle Bin is essentially a History view filtered to `actionType: 'delete'` with a Restore button.
## 19. Migration
1. Add `deletedAt`/`deletedBy`/`deleteBatchId` to the soft-deletable schemas (nullable; no backfill
needed — absent ⇒ live).
2. Introduce `notDeleted()` and `softRemove()` helpers; convert user-content `removeAsync` sites,
leaving exempt sites with an explicit `// hard-delete OK` comment.
3. Add `deletedAt: null` to the affected publications/queries **in the same change** as each
conversion (a converted delete that publications don't filter would make "deleted" items still
show — the highest-risk step; do it collection-by-collection).
4. Add the Recycle Bin view + the admin purge action + GDPR erasure purge.
5. Wire `delete`/`restore` into undo and History.
## 20. Risks / notes
- **Storage growth** is accepted (the chosen trade for "never lose data"); the admin purge and GDPR
erasure are the only relief valves. Document this for operators.
- **Query drift:** any *new* query that forgets `deletedAt: null` will leak deleted rows. The
`notDeleted()` helper + a source-guard test (grep that user-content publications include the filter)
mitigate this.
- **Unique constraints / slugs:** a soft-deleted board/label still occupies its slug; decide whether
restore or a new item wins the name (recommend: soft-deleted items release user-facing uniqueness,
re-acquire on restore or get suffixed).
- **Third-party storage (Attachments):** soft-deleting an attachment keeps the blob in S3/GridFS;
only the admin purge / GDPR erasure removes the physical bytes.
+1 -1
View File
@@ -54,7 +54,7 @@ Keep backups, Docker is more complex than others above. Use only if you have tim
* [Docker](Docker)
* [Windows](../Propietary/Windows/Windows.md) + build from source
* [Mac](../Propietary/Mac.md)
* [Mac](../Propietary/Mac/Mac.md)
* [SLES 64bit](Docker/Install-Wekan-Docker-on-SUSE-Linux-Enterprise-Server-12-SP1.md)
* [Proxy](https://github.com/wekan/wekan/issues/1480)
@@ -1,6 +1,6 @@
NOTE: If you use MongoDB Snap package below, most likely it works with many distro versions, like newest distros also. But if installing without Snap from https://www.mongodb.com/try/download/community , those packages are not for so many distros and versions.
This page is NOT only about Raspberry Pi. This page IS about install without container for ANY Linux/BSD/[macOS](../../Propietary/Mac.md)/[Windows](../../Propietary/Windows/Offline.md) using CPU of amd64/arm64/s390x/ppc64le.
This page is NOT only about Raspberry Pi. This page IS about install without container for ANY Linux/BSD/[macOS](../../Propietary/Mac/Mac.md)/[Windows](../../Propietary/Windows/Offline.md) using CPU of amd64/arm64/s390x/ppc64le.
WeKan only requires:
- WeKan bundle .zip file of Javascript etc for that CPU. Only difference per CPU is that fibers package is compiled for that CPU. Those .zip files are built this way, for example:
+5 -5
View File
@@ -1,14 +1,14 @@
- At some cases [Hardened kernel may prevent creating new Wekan boards at Sandstorm](https://github.com/wekan/wekan/issues/1398)
# Sandstorm at Debian and Ubuntu
### New Sandstorm WeKan at 2026-07
Backup your current Sandstorm first !
## 1. Backup Sandstorm
1. Please first backup your Sandstorm https://docs.sandstorm.io/en/latest/administering/backups/
2. Please first download all of your grains to .zip files from top menu row down arrow button, just in case.
## 2. Upgrade Sandstorm WeKan
Backup your Sandstorm first ! See above !
This new Sandstorm WeKan uses fork of FerretDB v1 SQLite at https://github.com/wekan/FerretDB
- FerretDB is drop-in replacement for MongoDB 7 Server
+1 -1
View File
@@ -1,6 +1,6 @@
## Build from source on Mac
[Build from source on Mac](../../Propietary/Mac.md)
[Build from source on Mac](../../Propietary/Mac/Mac.md)
## Build from source on VirtualBox
File renamed without changes.
Loaded 100 of 323 files, more files were not shown because too many files have changed in this diff. Show more