mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 21:44:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbcbee41c9 | ||
|
|
8d30efa117 | ||
|
|
6f70544c60 | ||
|
|
acc86fa86a | ||
|
|
96c59b2d01 | ||
|
|
1a88240137 | ||
|
|
d8bda40806 | ||
|
|
2c2e8a6fee | ||
|
|
cf55da122a | ||
|
|
ccb7a8e91c | ||
|
|
963f72ac88 | ||
|
|
144bed0eda | ||
|
|
1f11ad2602 | ||
|
|
2054ea2b54 | ||
|
|
8ae0ee5446 | ||
|
|
c1ffed59a7 | ||
|
|
58c0e6d1f8 | ||
|
|
cf1823b510 | ||
|
|
2a878d55e7 | ||
|
|
48a60a1141 | ||
|
|
3bfc6cce56 | ||
|
|
8cf7dd68cc | ||
|
|
742fc9221a | ||
|
|
add47dc065 | ||
|
|
5af16df201 | ||
|
|
adaecfe8c2 | ||
|
|
08cfe2d788 | ||
|
|
ef6b2794b8 | ||
|
|
1284afc588 | ||
|
|
354b7804b5 | ||
|
|
72042b7d79 | ||
|
|
b8a21e4ce3 | ||
|
|
b71461d90c | ||
|
|
b8bb004650 | ||
|
|
bb6b0d5477 | ||
|
|
e0991d6602 | ||
|
|
077e2196f5 | ||
|
|
e010c09e87 | ||
|
|
356b1152bb | ||
|
|
66a2f8ddc8 | ||
|
|
ef81c25232 | ||
|
|
2be9253839 | ||
|
|
2cf878215d | ||
|
|
c00f24d668 | ||
|
|
95552881fc | ||
|
|
5b3394af2f | ||
|
|
d55c5e2c3c | ||
|
|
01d55828d2 | ||
|
|
219011e5eb | ||
|
|
91ccef062b | ||
|
|
97feb986a8 | ||
|
|
a184a3a717 | ||
|
|
75ec34e837 | ||
|
|
c795d13450 | ||
|
|
ea4a10c212 | ||
|
|
7691829cdb | ||
|
|
63bda22faa | ||
|
|
a9f2e9f50c | ||
|
|
861ded439e | ||
|
|
687a4af861 | ||
|
|
d67d27f0d8 | ||
|
|
3736f8431a | ||
|
|
01088ea9d4 | ||
|
|
5e014d0967 | ||
|
|
c87b4cb510 | ||
|
|
8dd6bd9db5 | ||
|
|
8cb4050cf2 | ||
|
|
a4cc0c2182 | ||
|
|
98a03bf045 | ||
|
|
4dc09c62b9 | ||
|
|
1c8ba2afc3 | ||
|
|
b027594ca8 | ||
|
|
08a2e5525e | ||
|
|
3d190ca022 | ||
|
|
aa15e72682 | ||
|
|
bd8e417be1 | ||
|
|
fc0649a756 | ||
|
|
969f96d742 | ||
|
|
0f87c3bf8b | ||
|
|
834698bb5e | ||
|
|
cd7f9f7bc9 | ||
|
|
519c04b8ef | ||
|
|
3f5173b960 | ||
|
|
c21edb160b | ||
|
|
cb80198d97 | ||
|
|
035195f2b4 | ||
|
|
197254fe16 | ||
|
|
aafe71fd19 | ||
|
|
15e756f4e0 | ||
|
|
97b0240ca4 | ||
|
|
68723ff43b | ||
|
|
cfd53cc969 | ||
|
|
bd72f5d64c | ||
|
|
e33e50d9c8 | ||
|
|
7718118654 | ||
|
|
772ae061f1 | ||
|
|
c6f960bbe1 | ||
|
|
ac20cd6442 | ||
|
|
c6aa769bc0 | ||
|
|
7699cc1e90 | ||
|
|
1312fefb12 | ||
|
|
1008561517 | ||
|
|
1cd32001d4 | ||
|
|
0535215225 | ||
|
|
8aacc22360 | ||
|
|
437c64c9a0 | ||
|
|
061eba8b7a | ||
|
|
176c5ef2d8 | ||
|
|
9a0c0613a5 | ||
|
|
22c7d5376b | ||
|
|
3c9524f61a | ||
|
|
d0a1e43f36 | ||
|
|
49285dad19 | ||
|
|
eab1546b88 | ||
|
|
46924985f8 | ||
|
|
58344db0d7 | ||
|
|
e246403bb6 | ||
|
|
e9e7071744 | ||
|
|
8287865b23 | ||
|
|
e8ced84172 | ||
|
|
9be41ab34b | ||
|
|
f3354f5cc8 | ||
|
|
3d1276fc1f | ||
|
|
765d04bf7c | ||
|
|
4ca33d29f7 | ||
|
|
f6f021f8cc | ||
|
|
26319c974b | ||
|
|
50dd05ceee | ||
|
|
6d5a594b07 | ||
|
|
ae29555862 | ||
|
|
c4784225d5 | ||
|
|
5730f7dcc5 | ||
|
|
b29a47778f | ||
|
|
7b7fada1b8 | ||
|
|
41c747ef5f | ||
|
|
99299c2a91 | ||
|
|
c29cf3b985 | ||
|
|
b1d33c5362 | ||
|
|
a76f7d28e3 | ||
|
|
d7f36d70a0 | ||
|
|
63a8657e0a | ||
|
|
b846e6d565 | ||
|
|
580276079e | ||
|
|
9a387acce0 | ||
|
|
7374b7e00d | ||
|
|
e15c920872 | ||
|
|
d1ac53b367 | ||
|
|
c951b40e5e | ||
|
|
be6ebb2503 | ||
|
|
d6ddcffcc2 |
No files matched your search
@@ -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.
@@ -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)."
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
| {{_ '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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) || []);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}}")
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
Reference in new issue
Block a user