mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f6fecf44 | ||
|
|
6cb4ae2d64 | ||
|
|
3ecb90fc04 | ||
|
|
dc135f6e7d | ||
|
|
5b8e47649e | ||
|
|
fb1df544dd | ||
|
|
f031b885d7 | ||
|
|
a4c1ce3d41 | ||
|
|
5b0ad4d334 | ||
|
|
6b409c5dad | ||
|
|
298ffcfd3b | ||
|
|
cf217784d1 | ||
|
|
bb7fe88bd5 | ||
|
|
ffc6652be0 | ||
|
|
db90fb9e61 | ||
|
|
6784726bac | ||
|
|
0c4b8f4d93 | ||
|
|
b98abe7b00 | ||
|
|
e0c76099f0 | ||
|
|
b4bfc75947 | ||
|
|
dcd5f8fdd5 | ||
|
|
7273220780 | ||
|
|
0d90011aff | ||
|
|
2df6a77072 | ||
|
|
ade9f16db4 | ||
|
|
8cbfe3daee | ||
|
|
1bf68f54b2 | ||
|
|
619eda0a9d | ||
|
|
3d2ab08294 | ||
|
|
6a12d968e0 | ||
|
|
9f23e6384b | ||
|
|
5d44e0b98b | ||
|
|
e65e06865a | ||
|
|
b8bdbcd2d7 | ||
|
|
3476cd17c7 | ||
|
|
cab231ee3a | ||
|
|
d693ae8d7d | ||
|
|
5054dab8b6 | ||
|
|
9c06bd5772 | ||
|
|
beefe441d7 | ||
|
|
0e0a8bbb33 | ||
|
|
5f40f612f5 | ||
|
|
b0703befd7 | ||
|
|
7f42db4670 | ||
|
|
6142396ae9 | ||
|
|
3076f2317e | ||
|
|
62df991532 | ||
|
|
50abd8f95a | ||
|
|
cafbb1761b | ||
|
|
a5bcb2e27c | ||
|
|
0804cd19e8 | ||
|
|
3b109d14e8 | ||
|
|
3f5c70e643 | ||
|
|
455e498482 | ||
|
|
3ab2b9dccb | ||
|
|
d79f90fdfe | ||
|
|
1ca7bd3bb6 | ||
|
|
525cd07eee | ||
|
|
dc8ff6e6ca | ||
|
|
c671a7fc8c | ||
|
|
d5092c9b2d | ||
|
|
68e59ab66f | ||
|
|
11f87c2115 | ||
|
|
21644652e2 | ||
|
|
6f3174b23d | ||
|
|
fb0c7f7f60 | ||
|
|
d5f707fd37 | ||
|
|
be8976ed8c | ||
|
|
b0712e6656 | ||
|
|
ad46cb6102 | ||
|
|
8f11e8ac66 | ||
|
|
bb8233655c | ||
|
|
b5945cf596 | ||
|
|
6e60644bdb | ||
|
|
ccdd1a0846 | ||
|
|
04737bd76e | ||
|
|
7f0706e45f | ||
|
|
ab8da931cf | ||
|
|
013b6b5266 | ||
|
|
0115622a9a | ||
|
|
975316f5cd | ||
|
|
54aad554aa | ||
|
|
60e40199c9 | ||
|
|
ff3f35ac43 | ||
|
|
8549a62c85 | ||
|
|
8b9202d677 | ||
|
|
f4f22bbe24 | ||
|
|
32b561cfbf | ||
|
|
1c28b40fd4 | ||
|
|
7f5cc03ea9 | ||
|
|
60116bc72f | ||
|
|
66d8706c4d | ||
|
|
143607ef22 | ||
|
|
0f52b6a787 | ||
|
|
87f4536e9d | ||
|
|
cdde52392b | ||
|
|
62f93fa46c | ||
|
|
5b3a3fb744 | ||
|
|
349f4aa046 | ||
|
|
ae1f37658c | ||
|
|
df94214798 | ||
|
|
6c22e43be8 | ||
|
|
5e5baf5ed7 | ||
|
|
713c13831b | ||
|
|
e400ecf548 | ||
|
|
e9db3a8c2f | ||
|
|
260a2afacc | ||
|
|
a8cf44cfaa | ||
|
|
d53e7cafbb | ||
|
|
f278d9ae68 | ||
|
|
a700152e58 | ||
|
|
b5f9097681 | ||
|
|
b227a93193 | ||
|
|
52c62021a9 | ||
|
|
692a258758 | ||
|
|
8847798f3c | ||
|
|
087d9fd0d8 | ||
|
|
da55f1c413 | ||
|
|
28b4c6687c | ||
|
|
10db1c3e76 | ||
|
|
1a376b5dde | ||
|
|
e20dd132c1 | ||
|
|
64da38b412 | ||
|
|
2a6bdadc15 | ||
|
|
7598859a47 | ||
|
|
4a94793f68 | ||
|
|
d4b19389eb | ||
|
|
b24bf69d6c | ||
|
|
5cc9b1dcbb | ||
|
|
4dbf99010c | ||
|
|
544e1b14b4 | ||
|
|
5ef2adf6fb | ||
|
|
ccc03913fc | ||
|
|
af62ebd97a | ||
|
|
f25c6553bd | ||
|
|
c72ae4cf05 | ||
|
|
43dcf56a3b | ||
|
|
1778929869 | ||
|
|
d50277721c | ||
|
|
f1749b2af7 | ||
|
|
d0d98bcadc | ||
|
|
743a47c462 | ||
|
|
cfa5707a40 | ||
|
|
b8a7a2e114 | ||
|
|
1237e4c2de | ||
|
|
61792f59c1 | ||
|
|
664c6e1943 | ||
|
|
9d90b869cd | ||
|
|
453770ef98 | ||
|
|
a7f9018a60 | ||
|
|
b6fe0a7256 | ||
|
|
02e3c35d69 | ||
|
|
4766b83da7 | ||
|
|
011c172c8c | ||
|
|
cb18f9b735 | ||
|
|
abe8af0788 | ||
|
|
997f1fb9df | ||
|
|
9828081c76 | ||
|
|
d2ec3ee400 | ||
|
|
43006f9025 | ||
|
|
fcfd5bce9b | ||
|
|
4cbefed339 | ||
|
|
7d594c95ae | ||
|
|
7d5f29e114 | ||
|
|
29b617769a | ||
|
|
f0d25a2431 | ||
|
|
09805a8a91 | ||
|
|
6f7ad270d1 | ||
|
|
bd1c611688 | ||
|
|
ae9be62ef3 | ||
|
|
41cd166358 | ||
|
|
e39dcd4ebf | ||
|
|
4f1a60aa7b | ||
|
|
5757cfc8ca | ||
|
|
c7c8a200ff | ||
|
|
71fcf2b454 | ||
|
|
1dbc26d200 | ||
|
|
84fee7b660 | ||
|
|
3b7a276347 | ||
|
|
02cf16d8c6 | ||
|
|
54944717ee | ||
|
|
b99d21d9f1 | ||
|
|
3dfd84973c | ||
|
|
dd4611e43f | ||
|
|
69e6b8bcd1 | ||
|
|
6ab29dc81e | ||
|
|
9ae6a91daf | ||
|
|
c3674330ac | ||
|
|
be5ddd77ef | ||
|
|
9fc88a783c | ||
|
|
748fd2d391 | ||
|
|
4ac24dc546 | ||
|
|
4c630d4ba8 | ||
|
|
a563d3c4b2 | ||
|
|
95b186e50a | ||
|
|
bbc525ef27 | ||
|
|
bfb3fd246b | ||
|
|
3a968052b9 | ||
|
|
d424bd6386 | ||
|
|
3824066f7f | ||
|
|
774d752620 | ||
|
|
dbca7cd72d | ||
|
|
8497e7c537 | ||
|
|
7f44459c9f | ||
|
|
d5c69e0259 | ||
|
|
598aa8dfa1 | ||
|
|
0360ca1583 | ||
|
|
9c51391a75 | ||
|
|
2ed13a1593 | ||
|
|
ca50c9d1aa | ||
|
|
548d7b483e | ||
|
|
71bbc9f67e | ||
|
|
05fd6fd3a5 | ||
|
|
e8939bc1e6 | ||
|
|
bc8d58011c | ||
|
|
0bf8cae81c | ||
|
|
984ec160da | ||
|
|
a2df80e64d | ||
|
|
3aaf8f9b61 | ||
|
|
1dfc223ca0 | ||
|
|
604cc455e0 | ||
|
|
e85072a14a | ||
|
|
1621f3784a | ||
|
|
ba6ad20a30 | ||
|
|
3847157753 | ||
|
|
4e77133aca | ||
|
|
77c696e37b | ||
|
|
e3f17be14d | ||
|
|
5ca4ebe09a | ||
|
|
3117e419bb | ||
|
|
3eba4aa782 | ||
|
|
127f1fb04e | ||
|
|
14b4f5b5ed | ||
|
|
e6a4abd394 | ||
|
|
25cdc4db0d | ||
|
|
c062d93d57 | ||
|
|
e014d5c816 | ||
|
|
84ab805ec1 | ||
|
|
a9a5ac7a07 | ||
|
|
0119e62da5 | ||
|
|
284d4b25ea | ||
|
|
75df523ef0 | ||
|
|
e5c844fca7 | ||
|
|
8a84707726 | ||
|
|
9f7eac158e | ||
|
|
9b729b502e | ||
|
|
cef965f73f | ||
|
|
43dd98f8a1 | ||
|
|
2cd772fcf5 | ||
|
|
eaa1d823de | ||
|
|
1a41c72c21 | ||
|
|
7943a17432 | ||
|
|
d41e17d6a0 | ||
|
|
4ec2cbcd5a | ||
|
|
1ce2d8cc88 | ||
|
|
8f76c7b2b0 | ||
|
|
b07c4d0a7b | ||
|
|
5b43b7159e | ||
|
|
7ce71dbb2e | ||
|
|
d615e599f4 | ||
|
|
aad00cdba2 | ||
|
|
07800da9af | ||
|
|
da0d338e33 | ||
|
|
c3c091635d | ||
|
|
8c499823cb | ||
|
|
1844425e8f | ||
|
|
68dd14140f | ||
|
|
1ba32f5269 | ||
|
|
30f36a7f31 | ||
|
|
20b9c9ddd6 | ||
|
|
0e9aed5a4b | ||
|
|
27d8468be0 | ||
|
|
d01526bbff | ||
|
|
ab3b48b334 | ||
|
|
b0da9097c1 | ||
|
|
158130cbfc | ||
|
|
53328f40c6 | ||
|
|
e7eeaf0b10 | ||
|
|
609b07a8db | ||
|
|
7545bb5506 | ||
|
|
457713968c | ||
|
|
372a58c112 | ||
|
|
527962966b | ||
|
|
a37e7d1cc7 | ||
|
|
33ad40d012 | ||
|
|
44b5e12de9 | ||
|
|
1207b573b5 | ||
|
|
a26295a2fb | ||
|
|
b2da9ac8d4 | ||
|
|
53dcf7379d | ||
|
|
dfa6c78d4f | ||
|
|
795850ed05 | ||
|
|
9ef7f4a07a | ||
|
|
cb2394a95d | ||
|
|
ac368de06a | ||
|
|
b26494766f | ||
|
|
68ab63c7fe | ||
|
|
e3e71b254a | ||
|
|
fc4eadedc9 | ||
|
|
9dca403782 | ||
|
|
dd90995db7 | ||
|
|
f8e31745d8 |
No files matched your search
@@ -1,5 +1,3 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
|
||||
wekandb-dev:
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Build meteor-spk bundle
|
||||
|
||||
# Manually triggered. Assembles WeKan's modernized meteor-spk (the Sandstorm packaging
|
||||
# tool + its meteor-spk.deps: Node 24, FerretDB, the Mongo 3.x CLIs and the modern
|
||||
# sandstorm-http-bridge) and zips it as meteor-spk-YYYY-MM-DD_HH_MM_SS.zip — the same kind
|
||||
# of self-contained meteor-spk bundle that https://github.com/sandstorm-io/meteor-spk
|
||||
# releases, but with WeKan's Node 24 / FerretDB deps baked in. Download it to `meteor-spk
|
||||
# pack` WeKan (or any Meteor 3.x app) without re-running build-deps.sh each time.
|
||||
#
|
||||
# gh workflow run meteor-spk.yml
|
||||
#
|
||||
# This only assembles + zips the tool; it does NOT pack or sign a WeKan .spk, so it needs
|
||||
# no SANDSTORM_KEYRING. The .spk itself is built by sandstorm.yml; the Sandstorm App
|
||||
# Market publish is done by sandstorm-store.yml.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Branch, tag or SHA to build the meteor-spk deps from (default: main)'
|
||||
required: false
|
||||
default: 'main'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
meteor-spk:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref || 'main' }}
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
|
||||
# Same as sandstorm.yml: Ubuntu 24.04's AppArmor blocks the unprivileged user
|
||||
# namespaces the Sandstorm install relies on; relax it so the install (whose
|
||||
# sandstorm-http-bridge build-deps.sh copies in) can run on the runner.
|
||||
run: |
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git zip
|
||||
|
||||
- name: Install Sandstorm (dev, unattended)
|
||||
# build-deps.sh's [bridge] step copies the modern
|
||||
# /opt/sandstorm/latest/bin/sandstorm-http-bridge into the deps, so Sandstorm
|
||||
# must be installed first — same ordering as sandstorm.yml.
|
||||
run: curl https://install.sandstorm.io | sudo bash -s -- -d
|
||||
|
||||
- name: Assemble modern meteor-spk.deps (Node 24 + FerretDB + Mongo 3.x CLIs)
|
||||
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io; build-deps.sh swaps in
|
||||
# Node 24, FerretDB, the Mongo 3.x CLIs, the migration launcher/importer, keeps
|
||||
# niscud, and swaps in the modern sandstorm-http-bridge. Produces
|
||||
# $HOME/projects/meteor-spk/meteor-spk-0.6.0 (the meteor-spk tool + deps).
|
||||
run: bash sandstorm-src/build-deps.sh
|
||||
|
||||
- name: Zip the meteor-spk bundle
|
||||
id: zip
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STAMP="$(date +'%Y-%m-%d_%H_%M_%S')"
|
||||
ZIP="meteor-spk-${STAMP}.zip"
|
||||
BUNDLE_DIR="$HOME/projects/meteor-spk"
|
||||
if [ ! -d "$BUNDLE_DIR/meteor-spk-0.6.0" ]; then
|
||||
echo "::error::Expected meteor-spk bundle at $BUNDLE_DIR/meteor-spk-0.6.0 was not produced by build-deps.sh."
|
||||
exit 1
|
||||
fi
|
||||
# Zip so it extracts to a top-level meteor-spk-0.6.0/ directory, like the
|
||||
# upstream meteor-spk release archives. Exclude the base tarball build-deps.sh
|
||||
# downloaded next to it.
|
||||
( cd "$BUNDLE_DIR" && zip -r -y -q "$GITHUB_WORKSPACE/$ZIP" meteor-spk-0.6.0 -x '*.tar.xz' )
|
||||
ZIP_BYTES=$(stat -c%s "$GITHUB_WORKSPACE/$ZIP")
|
||||
echo "Packed $ZIP: $((ZIP_BYTES/1024/1024)) MiB ($ZIP_BYTES bytes)"
|
||||
ls -la "$GITHUB_WORKSPACE/$ZIP"
|
||||
echo "zip=$ZIP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.zip.outputs.zip }}
|
||||
path: ${{ steps.zip.outputs.zip }}
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload the meteor-spk bundle to the newest WeKan release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || true)"
|
||||
if [ -z "${TAG:-}" ] || [ "$TAG" = "null" ]; then
|
||||
echo "::warning::No existing GitHub Release found; the meteor-spk bundle is available only as a workflow artifact."
|
||||
exit 0
|
||||
fi
|
||||
echo "Uploading ${{ steps.zip.outputs.zip }} to the newest release: $TAG"
|
||||
gh release upload "$TAG" "${{ steps.zip.outputs.zip }}" --clobber
|
||||
+331
-101
@@ -7,13 +7,20 @@ name: Release All Platforms
|
||||
# Or use the convenience script (bumps versions, commits, pushes, then triggers):
|
||||
# ./releases/release-all.sh 9.35 9.36
|
||||
#
|
||||
# The Docker image and snap cover the platforms MongoDB ships server binaries for
|
||||
# (amd64, arm64; win64 + mac bundles too); MongoDB 7.x is the default database.
|
||||
# Additionally, WeKan server bundles are built for every other architecture
|
||||
# Node.js 24.x publishes binaries for — s390x and ppc64le — and attached to the
|
||||
# GitHub Release as extra downloads (run a separate MongoDB for those). These
|
||||
# extra-arch bundles are NOT part of the Docker image or snap. (armv7l/32-bit ARM
|
||||
# is not built: Node.js 24 ships no armv7l binaries.)
|
||||
# WeKan's default database is MongoDB, which ships server binaries only for amd64
|
||||
# and arm64 (+ the win64/mac bundles). On every OTHER architecture that has a
|
||||
# Node.js 24 build — ppc64le, s390x, riscv64 — there is no MongoDB, so those
|
||||
# bundles ship FerretDB v1 instead: the wekan/FerretDB fork with its embedded
|
||||
# pure-Go SQLite backend, which speaks the MongoDB wire protocol. FerretDB is
|
||||
# built + released SEPARATELY (wekan/FerretDB, its own build.sh) as individual
|
||||
# per-arch binaries; each bundle job downloads ONLY the ferretdb-<arch> binary for
|
||||
# the platform it targets and bakes it in, so amd64/arm64 users can opt into it
|
||||
# too (WEKAN_DB=ferretdb, `snap set wekan
|
||||
# database=ferretdb`, or Docker env). Whichever backend is chosen binds the same
|
||||
# MongoDB port, so MONGO_URL is unchanged. ppc64le/s390x/riscv64 are now covered
|
||||
# by the Docker image as well (self-contained, FerretDB auto-started). riscv64
|
||||
# uses unofficial-builds.nodejs.org for Node 24. (armv7l/32-bit ARM is still not
|
||||
# built: there is NO Node.js 24 build for it anywhere, so nothing could run it.)
|
||||
#
|
||||
# This workflow does EVERYTHING for a release remotely (nothing is built or
|
||||
# pushed on the maintainer's machine), running jobs in parallel where possible:
|
||||
@@ -26,17 +33,27 @@ name: Release All Platforms
|
||||
# only AFTER the `docker` job has published the image (skipped if
|
||||
# docker fails)
|
||||
# - build-*: builds the amd64 bundle (full Meteor 3 build), then rebuilds native
|
||||
# modules for arm64 from that amd64 base
|
||||
# modules for arm64 from that amd64 base. Each bundle job downloads
|
||||
# only the ferretdb-<arch> binary it needs from the newest wekan/FerretDB
|
||||
# release (FerretDB is built + released separately by its own build.sh —
|
||||
# not compiled here) and embeds it
|
||||
# - release: creates the GitHub Release with the newest CHANGELOG.md notes and
|
||||
# uploads the core wekan-<version>-{amd64,arm64}.zip bundles
|
||||
# - build-win64 / build-mac-arm64 / build-extra-arches: built AFTER release, each
|
||||
# rebuilds the amd64 bundle's native modules for its platform (Windows,
|
||||
# macOS arm64, and QEMU-emulated s390x/ppc64le) and attaches its
|
||||
# .zip to the Release as an extra download. Decoupled from `release` so
|
||||
# a slow/flaky build never blocks it; not in the Docker image or snap
|
||||
# - docker: builds and pushes the multi-arch image to Docker Hub, Quay.io, GHCR
|
||||
# - snap: builds the snap on Launchpad (amd64 + arm64), pushes it to the Snap
|
||||
# Store and attaches the .snap files to the GitHub Release
|
||||
# macOS arm64, and QEMU-emulated ppc64le/s390x/riscv64) and attaches
|
||||
# its .zip to the Release. Windows/macOS strip the Linux FerretDB back
|
||||
# out (MongoDB-only). Decoupled from `release` so a slow/flaky build
|
||||
# never blocks it
|
||||
# - docker: builds and pushes the multi-arch image (amd64, arm64, ppc64le, s390x,
|
||||
# riscv64) to Docker Hub, Quay.io, GHCR
|
||||
# - snap: builds the snap on Launchpad from the default core24 snapcraft.yaml
|
||||
# (platforms: amd64, arm64, s390x, ppc64el, riscv64), pushes it to the
|
||||
# Snap Store candidate + beta + edge channels and attaches the .snap
|
||||
# files to the GitHub Release. core24 allows candidate (core26 does
|
||||
# not); the stable channel is published MANUALLY later, not here.
|
||||
# `snap set wekan database=ferretdb` switches the running backend to
|
||||
# FerretDB v1 (snapcraft-core26.yaml is the core26 variant)
|
||||
#
|
||||
# Required GitHub Secrets:
|
||||
# GHCR_AUTH - base64("username:token") for ghcr.io
|
||||
@@ -258,7 +275,7 @@ jobs:
|
||||
# The full Meteor build always runs here on x86_64; every other platform then
|
||||
# only rebuilds the native npm modules from this bundle.
|
||||
build-amd64:
|
||||
needs: prepare
|
||||
needs: [prepare]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
@@ -308,6 +325,35 @@ jobs:
|
||||
cd .build/bundle/programs/server
|
||||
npm install
|
||||
|
||||
- name: Embed Node.js, FerretDB (amd64) and launcher into the bundle
|
||||
# Make the release .zip self-contained (offline): bundle Node.js + FerretDB
|
||||
# + a start-wekan.sh that runs WeKan on the bundled Node against the bundled
|
||||
# FerretDB (SQLite) by default, storing files on the filesystem under
|
||||
# WRITABLE_PATH. This amd64 bundle is the base every other Linux arch is
|
||||
# derived from; those jobs overwrite bundle/node and bundle/ferretdb with
|
||||
# their own arch's binaries and inherit this launcher.
|
||||
run: |
|
||||
# Bundled Node.js: the runner's node (setup-node) is linux-x64 Node 24,
|
||||
# matching this base bundle's arch.
|
||||
cp -L "$(command -v node)" .build/bundle/node
|
||||
chmod +x .build/bundle/node
|
||||
# Bundled FerretDB (amd64): download only this arch's binary from the
|
||||
# newest wekan/FerretDB release (per-arch assets, no ferretdb.zip).
|
||||
curl -fSL --retry 5 --retry-delay 10 -o .build/bundle/ferretdb \
|
||||
"https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-amd64"
|
||||
chmod +x .build/bundle/ferretdb
|
||||
# Bundled MongoDB Database Tools (amd64) from wekan/mongo-tools (per-arch,
|
||||
# no mongodb.com). This base amd64 bundle is inherited by the other Linux
|
||||
# arches, which overwrite these with their own arch's tools below.
|
||||
MT="https://github.com/wekan/mongo-tools/releases/latest/download"
|
||||
for t in bsondump mongodump mongoexport mongofiles mongoimport mongorestore mongostat mongotop; do
|
||||
curl -fSL --retry 5 --retry-delay 10 -o ".build/bundle/$t" "$MT/$t-amd64"
|
||||
chmod +x ".build/bundle/$t"
|
||||
done
|
||||
# Self-contained launcher (defaults to FerretDB SQLite + WRITABLE_PATH).
|
||||
cp releases/ferretdb/start-wekan.sh .build/bundle/
|
||||
chmod +x .build/bundle/start-wekan.sh
|
||||
|
||||
- name: Create amd64 zip
|
||||
run: |
|
||||
cd .build
|
||||
@@ -360,6 +406,23 @@ jobs:
|
||||
node:${NODE_VERSION}-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Embed Node.js + FerretDB (arm64) into the bundle
|
||||
# The launcher (start-wekan.sh) is inherited from the amd64 base bundle;
|
||||
# swap bundle/node (runner is arm64 -> linux-arm64 Node 24) and bundle/ferretdb.
|
||||
run: |
|
||||
cp -L "$(command -v node)" bundle/node
|
||||
chmod +x bundle/node
|
||||
# Download only the arm64 FerretDB binary from the newest wekan/FerretDB release.
|
||||
curl -fSL --retry 5 --retry-delay 10 -o bundle/ferretdb \
|
||||
"https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-arm64"
|
||||
chmod +x bundle/ferretdb
|
||||
# Overwrite the inherited amd64 MongoDB Database Tools with arm64 ones.
|
||||
MT="https://github.com/wekan/mongo-tools/releases/latest/download"
|
||||
for t in bsondump mongodump mongoexport mongofiles mongoimport mongorestore mongostat mongotop; do
|
||||
curl -fSL --retry 5 --retry-delay 10 -o "bundle/$t" "$MT/$t-arm64"
|
||||
chmod +x "bundle/$t"
|
||||
done
|
||||
|
||||
- name: Create arm64 zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-arm64.zip bundle
|
||||
@@ -392,6 +455,11 @@ jobs:
|
||||
tag_name: v${{ needs.prepare.outputs.version }}
|
||||
name: v${{ needs.prepare.outputs.version }}
|
||||
body: ${{ needs.prepare.outputs.changelog }}
|
||||
# FerretDB binaries are NOT attached here: the per-arch ferretdb-<arch>
|
||||
# binaries live on the wekan/FerretDB releases
|
||||
# (https://github.com/wekan/FerretDB/releases), which is where the WeKan
|
||||
# builds fetch them from. Each wekan-<version>-<arch>.zip bundle embeds
|
||||
# its own arch's FerretDB binary.
|
||||
files: |
|
||||
wekan-${{ needs.prepare.outputs.version }}-amd64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
|
||||
@@ -436,6 +504,12 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
7z x wekan-${VERSION}-amd64.zip
|
||||
# Drop the Linux node + FerretDB binaries + MongoDB Database Tools + POSIX
|
||||
# launcher inherited from the amd64 base bundle; the Windows node.exe,
|
||||
# ferretdb.exe, *.exe tools and start-wekan.bat are added in later steps.
|
||||
rm -f bundle/node bundle/ferretdb bundle/.ferretdb-default bundle/start-wekan.sh
|
||||
rm -f bundle/bsondump bundle/mongodump bundle/mongoexport bundle/mongofiles \
|
||||
bundle/mongoimport bundle/mongorestore bundle/mongostat bundle/mongotop
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
@@ -458,40 +532,47 @@ jobs:
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
|
||||
- name: Add MongoDB tools, mongosh and start-wekan.bat to the bundle
|
||||
- name: Add MongoDB Database Tools to the bundle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use the same MongoDB tools/mongosh versions as the snap, which
|
||||
# releases/version.sh keeps at the newest release for each bundle.
|
||||
TOOLS_VER=$(grep -oE 'mongodb-database-tools-ubuntu2204-\$\{TOOLS_ARCH\}-[0-9]+\.[0-9]+\.[0-9]+' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
MONGOSH_VER=$(grep -oE 'mongosh-[0-9]+\.[0-9]+\.[0-9]+-linux' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
echo "MongoDB Database Tools: $TOOLS_VER mongosh: $MONGOSH_VER"
|
||||
|
||||
# MongoDB Database Tools (Windows): bsondump, mongodump, mongoexport,
|
||||
# mongofiles, mongoimport, mongorestore, mongostat, mongotop.
|
||||
TOOLS_DIR="mongodb-database-tools-windows-x86_64-${TOOLS_VER}"
|
||||
curl -fL -o tools.zip \
|
||||
"https://fastdl.mongodb.org/tools/db/${TOOLS_DIR}.zip"
|
||||
7z x -y tools.zip > /dev/null
|
||||
cp "${TOOLS_DIR}/bin/"*.exe bundle/
|
||||
|
||||
# mongosh (Windows): mongosh.exe + mongosh_crypt_v1.dll.
|
||||
MONGOSH_DIR="mongosh-${MONGOSH_VER}-win32-x64"
|
||||
curl -fL -o mongosh.zip \
|
||||
"https://downloads.mongodb.com/compass/${MONGOSH_DIR}.zip"
|
||||
7z x -y mongosh.zip > /dev/null
|
||||
cp "${MONGOSH_DIR}/bin/mongosh.exe" bundle/
|
||||
cp "${MONGOSH_DIR}/bin/mongosh_crypt_v1.dll" bundle/
|
||||
|
||||
# Windows launcher from the repo root.
|
||||
cp src/start-wekan.bat bundle/
|
||||
# MongoDB Database Tools (Windows) built by wekan/mongo-tools (pure Go),
|
||||
# one <tool>-win64.exe per tool from that repo's newest release — no
|
||||
# mongodb.com dependency. No mongosh: WeKan no longer bundles the MongoDB
|
||||
# Shell; scripted DB access uses the bundled Node.js 24 + `mongodb` driver.
|
||||
# (The Windows launcher, start-wekan.bat, is added by the FerretDB embed
|
||||
# step below from releases/ferretdb/start-wekan.bat.)
|
||||
BASE="https://github.com/wekan/mongo-tools/releases/latest/download"
|
||||
for tool in bsondump mongodump mongoexport mongofiles mongoimport mongorestore mongostat mongotop; do
|
||||
curl -fL --retry 5 --retry-delay 10 -o "bundle/${tool}.exe" "${BASE}/${tool}-win64.exe"
|
||||
done
|
||||
|
||||
echo "=== bundle contents ==="
|
||||
ls -1 bundle/
|
||||
|
||||
- name: Embed Node.js, FerretDB (win64) and launcher into the bundle
|
||||
shell: bash
|
||||
# Make the Windows .zip self-contained: bundle node.exe (from the runner's
|
||||
# win-x64 Node 24), ferretdb.exe (downloaded per-arch from the newest
|
||||
# wekan/FerretDB release) and start-wekan.bat. FerretDB embedding is
|
||||
# tolerant: if the win64 asset is missing the bundle stays MongoDB-only
|
||||
# (start-wekan.bat then won't find ferretdb.exe).
|
||||
run: |
|
||||
# Bundled Node.js (win-x64 node.exe from setup-node).
|
||||
NODE_SRC="$(command -v node)"
|
||||
[ -f "$NODE_SRC" ] || NODE_SRC="${NODE_SRC}.exe"
|
||||
cp "$NODE_SRC" bundle/node.exe
|
||||
# Bundled FerretDB (win64): download only this arch's binary.
|
||||
if curl -fSL --retry 5 --retry-delay 10 -o bundle/ferretdb.exe \
|
||||
"https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-win64.exe"; then
|
||||
echo "Embedded FerretDB win64 -> bundle/ferretdb.exe"
|
||||
else
|
||||
rm -f bundle/ferretdb.exe
|
||||
echo "No win64 FerretDB asset in the release; Windows bundle stays MongoDB-only."
|
||||
fi
|
||||
# Self-contained Windows launcher (defaults to FerretDB SQLite).
|
||||
cp src/releases/ferretdb/start-wekan.bat bundle/
|
||||
|
||||
- name: Create win64 zip
|
||||
shell: cmd
|
||||
run: 7z a wekan-%VERSION%-win64.zip bundle
|
||||
@@ -532,6 +613,12 @@ jobs:
|
||||
- name: Rebuild native modules for macOS arm64
|
||||
run: |
|
||||
unzip -q wekan-${VERSION}-amd64.zip
|
||||
# Replace the Linux node + FerretDB binaries + MongoDB Database Tools
|
||||
# inherited from the amd64 base bundle with the macOS builds (added in a
|
||||
# later step). The POSIX launcher start-wekan.sh works on macOS, so keep it.
|
||||
rm -f bundle/node bundle/ferretdb bundle/.ferretdb-default
|
||||
rm -f bundle/bsondump bundle/mongodump bundle/mongoexport bundle/mongofiles \
|
||||
bundle/mongoimport bundle/mongorestore bundle/mongostat bundle/mongotop
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
@@ -551,6 +638,31 @@ jobs:
|
||||
npm install
|
||||
cd ../../..
|
||||
|
||||
- name: Embed Node.js + FerretDB (mac-arm64) into the bundle
|
||||
# Bundle the macOS arm64 Node.js (runner's darwin-arm64 Node 24) and the
|
||||
# macOS FerretDB binary, downloaded per-arch from the newest wekan/FerretDB
|
||||
# release. FerretDB is tolerant: if the mac-arm64 asset is missing the
|
||||
# bundle stays MongoDB-only (start-wekan.sh then won't find ferretdb).
|
||||
run: |
|
||||
cp -L "$(command -v node)" bundle/node
|
||||
chmod +x bundle/node
|
||||
if curl -fSL --retry 5 --retry-delay 10 -o bundle/ferretdb \
|
||||
"https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-mac-arm64"; then
|
||||
chmod +x bundle/ferretdb
|
||||
echo "Embedded FerretDB mac-arm64 -> bundle/ferretdb"
|
||||
else
|
||||
rm -f bundle/ferretdb
|
||||
echo "No mac-arm64 FerretDB asset in the release; macOS bundle stays MongoDB-only."
|
||||
rm -f bundle/start-wekan.sh
|
||||
fi
|
||||
# macOS MongoDB Database Tools (mac-arm64) from wekan/mongo-tools; tolerant
|
||||
# (skip any tool not built for macOS).
|
||||
MT="https://github.com/wekan/mongo-tools/releases/latest/download"
|
||||
for t in bsondump mongodump mongoexport mongofiles mongoimport mongorestore mongostat mongotop; do
|
||||
curl -fSL --retry 5 --retry-delay 10 -o "bundle/$t" "$MT/$t-mac-arm64" \
|
||||
&& chmod +x "bundle/$t" || { rm -f "bundle/$t"; echo "No $t-mac-arm64; skipping"; }
|
||||
done
|
||||
|
||||
- name: Create mac-arm64 zip
|
||||
run: zip -r wekan-${VERSION}-mac-arm64.zip bundle
|
||||
|
||||
@@ -568,11 +680,14 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: gh release upload "v${VERSION}" "wekan-${VERSION}-mac-arm64.zip" --clobber
|
||||
|
||||
# Extra-architecture Linux bundles (s390x, ppc64le).
|
||||
# armv7l (32-bit ARM) is intentionally excluded: Node.js 24 ships no armv7l
|
||||
# binaries (official or unofficial), so there is no node:24 linux/arm/v7 image
|
||||
# to rebuild native modules against and no Node 24 runtime to run a bundle on
|
||||
# an armv7 device. See https://nodejs.org/dist/latest-v24.x/ (no -linux-armv7l).
|
||||
# Extra-architecture Linux bundles (ppc64le, s390x, riscv64) — the arches that
|
||||
# have a Node.js 24 build but NO MongoDB server, so they ship FerretDB instead.
|
||||
# ppc64le and s390x use official nodejs.org binaries; riscv64 uses the
|
||||
# unofficial-builds.nodejs.org binary (nodejs.org ships no riscv64). armv7l
|
||||
# (32-bit ARM) is intentionally excluded: there is NO Node.js 24 build for it
|
||||
# anywhere (official or unofficial), so there is no runtime to run the bundle on
|
||||
# an armv7 device, regardless of RAM. See https://nodejs.org/dist/latest-v24.x/
|
||||
# (no -linux-armv7l) and https://unofficial-builds.nodejs.org/ (riscv64 only).
|
||||
build-extra-arches:
|
||||
needs: [prepare, build-amd64, release]
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -583,8 +698,13 @@ jobs:
|
||||
include:
|
||||
- arch: s390x
|
||||
platform: linux/s390x
|
||||
node_base: official
|
||||
- arch: ppc64le
|
||||
platform: linux/ppc64le
|
||||
node_base: official
|
||||
- arch: riscv64
|
||||
platform: linux/riscv64
|
||||
node_base: unofficial
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
@@ -620,11 +740,62 @@ jobs:
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
# The official node:${NODE_VERSION} Docker image only publishes amd64,
|
||||
# arm64 and ppc64le manifests — no s390x, no riscv64 — so
|
||||
# `docker run --platform linux/s390x node:24-slim` fails with "no matching
|
||||
# manifest". Instead build on an ubuntu:26.04 base (which publishes every
|
||||
# arch) and install the Node.js binary tarball inside the emulated
|
||||
# container: ppc64le/s390x from nodejs.org (official), riscv64 from
|
||||
# unofficial-builds.nodejs.org (nodejs.org ships no riscv64). Resolve the
|
||||
# exact latest ${NODE_VERSION}.x version on the host (fast, no emulation)
|
||||
# and pass it in along with which mirror to use.
|
||||
NODE_FULL=$(curl -fsSL https://nodejs.org/dist/index.json \
|
||||
| python3 -c "import json,sys; print(next(r['version'] for r in json.load(sys.stdin) if r['version'].startswith('v${NODE_VERSION}.')))")
|
||||
echo "Using Node ${NODE_FULL} (${{ matrix.node_base }}) for ${{ matrix.arch }}"
|
||||
docker run --rm --platform ${{ matrix.platform }} \
|
||||
-e NODE_FULL="$NODE_FULL" -e ARCH="${{ matrix.arch }}" \
|
||||
-e NODE_BASE="${{ matrix.node_base }}" \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:${NODE_VERSION}-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
ubuntu:26.04 \
|
||||
bash -c '
|
||||
set -eux
|
||||
apt-get update -qq
|
||||
apt-get install -y -q build-essential python3 curl xz-utils ca-certificates
|
||||
if [ "$NODE_BASE" = unofficial ]; then
|
||||
NODE_URL="https://unofficial-builds.nodejs.org/download/release/${NODE_FULL}/node-${NODE_FULL}-linux-${ARCH}.tar.xz"
|
||||
else
|
||||
NODE_URL="https://nodejs.org/dist/${NODE_FULL}/node-${NODE_FULL}-linux-${ARCH}.tar.xz"
|
||||
fi
|
||||
curl -fsSL -o /tmp/node.tar.xz "$NODE_URL"
|
||||
mkdir -p /opt/node
|
||||
tar -xJf /tmp/node.tar.xz -C /opt/node --strip-components=1
|
||||
export PATH=/opt/node/bin:$PATH
|
||||
node --version
|
||||
npm install
|
||||
# Bundle the target-arch Node.js binary for the self-contained
|
||||
# launcher. /bundle is the host bundle/ dir (see -v mount above).
|
||||
cp /opt/node/bin/node /bundle/node
|
||||
chmod +x /bundle/node
|
||||
'
|
||||
|
||||
- name: Embed FerretDB (${{ matrix.arch }}) into the bundle
|
||||
# These arches have no MongoDB server, so FerretDB is their default DB.
|
||||
# The launcher (start-wekan.sh) is inherited from the amd64 base bundle;
|
||||
# download only this arch's binary from the newest wekan/FerretDB release.
|
||||
# A marker file makes FerretDB the default backend at runtime (see the
|
||||
# launcher + Docker entrypoint).
|
||||
run: |
|
||||
curl -fSL --retry 5 --retry-delay 10 -o bundle/ferretdb \
|
||||
"https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-${{ matrix.arch }}"
|
||||
chmod +x bundle/ferretdb
|
||||
# Overwrite the inherited amd64 MongoDB Database Tools with this arch's ones.
|
||||
MT="https://github.com/wekan/mongo-tools/releases/latest/download"
|
||||
for t in bsondump mongodump mongoexport mongofiles mongoimport mongorestore mongostat mongotop; do
|
||||
curl -fSL --retry 5 --retry-delay 10 -o "bundle/$t" "$MT/$t-${{ matrix.arch }}"
|
||||
chmod +x "bundle/$t"
|
||||
done
|
||||
touch bundle/.ferretdb-default
|
||||
|
||||
- name: Create ${{ matrix.arch }} zip
|
||||
run: zip -r wekan-${VERSION}-${{ matrix.arch }}.zip bundle
|
||||
@@ -647,9 +818,11 @@ jobs:
|
||||
# Mirrors releases/docker-build.sh step 3 (multi-arch buildx --push).
|
||||
# Needs `release` because the Dockerfile downloads wekan-<version>-<arch>.zip
|
||||
# from the GitHub Release; building before those assets exist 404s and (without
|
||||
# set -e in the Dockerfile) shipped a broken image missing /build/main.js.
|
||||
# set -e in the Dockerfile) shipped a broken image missing /build/main.js. Also
|
||||
# needs `build-extra-arches` because the image now covers ppc64le/s390x/riscv64,
|
||||
# whose zips (with FerretDB baked in) are attached by that job after `release`.
|
||||
docker:
|
||||
needs: [prepare, release]
|
||||
needs: [prepare, release, build-extra-arches]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
@@ -687,14 +860,14 @@ jobs:
|
||||
# Release for each built platform. Even though this job needs `release`
|
||||
# (so the assets are uploaded), GitHub's releases/download/<tag>/<asset>
|
||||
# URL can briefly 404 right after upload (CDN/propagation lag), which used
|
||||
# to fail the multi-arch build mid-way. Poll here until both the amd64 and
|
||||
# arm64 bundles return 200, so the (expensive, emulated) buildx run only
|
||||
# starts once the assets are actually fetchable. The Dockerfile also
|
||||
# to fail the multi-arch build mid-way. Poll here until every arch bundle
|
||||
# baked into the image returns 200, so the (expensive, emulated) buildx run
|
||||
# only starts once the assets are actually fetchable. The Dockerfile also
|
||||
# retries per-arch as a second layer of safety.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base="https://github.com/wekan/wekan/releases/download/v${VERSION}"
|
||||
for arch in amd64 arm64; do
|
||||
for arch in amd64 arm64 ppc64le s390x riscv64; do
|
||||
url="${base}/wekan-${VERSION}-${arch}.zip"
|
||||
echo "Waiting for ${url} ..."
|
||||
ok=0
|
||||
@@ -771,7 +944,7 @@ jobs:
|
||||
# report in the Admin Panel — a stale, different version.
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--platform linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/riscv64 \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
-t wekanteam/wekan:latest \
|
||||
@@ -784,11 +957,85 @@ jobs:
|
||||
|
||||
# ─── 9. Build the snap on Launchpad, push to the store, attach to release ────
|
||||
# The snap's `wekan` part downloads wekan-<version>-<arch>.zip from the GitHub
|
||||
# Release, so this job runs only after `release` has published those assets.
|
||||
# `snapcraft remote-build` builds every platform listed in snapcraft.yaml
|
||||
# (amd64 + arm64) on Launchpad.
|
||||
snap:
|
||||
# Release, so these jobs run only after `release` has published those assets.
|
||||
#
|
||||
# snap-native + snap-launchpad replace the old single `snap` job. The mainstream
|
||||
# arches (amd64 + arm64) build NATIVELY on GitHub-hosted runners — fast, no
|
||||
# Launchpad queue. The exotic arches (s390x, ppc64el, riscv64) have no native
|
||||
# GitHub runners and snapcraft cannot cross-build, so they stay on Launchpad but
|
||||
# in a NON-BLOCKING, per-arch matrix so a slow/failed arch (riscv64 queues for
|
||||
# hours) never holds up the release or the other arches. Each arch publishes to
|
||||
# the Snap Store (candidate + beta + edge) and attaches to the Release the moment
|
||||
# it finishes — all 5 arches, published independently.
|
||||
snap-native:
|
||||
needs: [prepare, release]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
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
|
||||
# snapcraft.yaml's `version:` names the produced file (wekan_<version>_<arch>.snap).
|
||||
# Assert it matches the tag so a stale version fails fast here.
|
||||
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: Build the ${{ matrix.arch }} snap natively
|
||||
# Runs on a native ${{ matrix.arch }} GitHub runner, so snapcraft builds the
|
||||
# platform whose `build-on` matches this host — no Launchpad, no emulation.
|
||||
id: build
|
||||
uses: snapcore/action-build@v1
|
||||
|
||||
- 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 (candidate + beta + edge)
|
||||
uses: snapcore/action-publish@v1
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: 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]
|
||||
# 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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [s390x, ppc64el, riscv64]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
@@ -800,11 +1047,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install snapcraft
|
||||
# The Snap Store intermittently rejects the install with
|
||||
# 'error: cannot install "snapcraft": too many requests' (a 429-style
|
||||
# rate limit) that instantly fails the whole snap job before any build.
|
||||
# Retry a few times so one transient store throttle does not fail the
|
||||
# release (same spirit as the remote-build retry below).
|
||||
# The Snap Store intermittently rejects the install with a 429-style rate
|
||||
# limit; retry so one transient throttle does not fail the build.
|
||||
run: |
|
||||
set -uo pipefail
|
||||
for i in 1 2 3 4 5; do
|
||||
@@ -824,15 +1068,6 @@ jobs:
|
||||
chmod 600 ~/.local/share/snapcraft/launchpad-credentials
|
||||
|
||||
- name: Verify snapcraft.yaml version matches the release
|
||||
# The `bump` job (releases/version.sh) is the single source of truth for
|
||||
# the snap version and updates snapcraft.yaml before the tag is created.
|
||||
# snapcraft.yaml's `version:` names the produced files
|
||||
# (wekan_<version>_<arch>.snap), so if a stale tag carried the wrong
|
||||
# version the build would produce wekan_<old>_*.snap and the upload/attach
|
||||
# globs (wekan_${VERSION}_*.snap) would match nothing — passing the literal
|
||||
# glob to `snapcraft upload` ("is not a valid file", exit 64). Assert it
|
||||
# here so that mismatch fails fast (before a ~20 min Launchpad build) with
|
||||
# a clear pointer, instead of re-bumping the version inside this build job.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! grep -qxF "version: '${VERSION}'" snapcraft.yaml; then
|
||||
@@ -841,31 +1076,26 @@ jobs:
|
||||
fi
|
||||
echo "snapcraft.yaml is at version '${VERSION}'."
|
||||
|
||||
- name: Build snaps on Launchpad (amd64 + arm64)
|
||||
# `snapcraft remote-build` is experimental and talks to Launchpad over
|
||||
# HTTPS after pushing the whole repo; it intermittently dies with a
|
||||
# transient TLS drop (e.g. "SSLEOFError: EOF occurred in violation of
|
||||
# protocol", exit 70) that has nothing to do with the build itself.
|
||||
# Retry a few times so one network hiccup doesn't fail the release.
|
||||
- name: Build the ${{ matrix.arch }} snap on Launchpad
|
||||
# --build-for ${{ matrix.arch }}: build ONLY this leg's arch so a slow/failed
|
||||
# arch never blocks the others. remote-build can exit 0 without producing a
|
||||
# .snap (it just downloads logs), so require the file. Retry a few times to
|
||||
# survive transient TLS drops to Launchpad.
|
||||
run: |
|
||||
set -uo pipefail
|
||||
shopt -s nullglob
|
||||
attempts=3
|
||||
for i in $(seq 1 "$attempts"); do
|
||||
echo "::group::Remote build attempt $i/$attempts"
|
||||
snapcraft remote-build --launchpad-accept-public-upload && rc=0 || rc=$?
|
||||
echo "::group::Remote build ${{ matrix.arch }} attempt $i/$attempts"
|
||||
snapcraft remote-build --launchpad-accept-public-upload \
|
||||
--build-for ${{ matrix.arch }} && rc=0 || rc=$?
|
||||
echo "::endgroup::"
|
||||
# snapcraft remote-build can exit 0 even when the Launchpad build
|
||||
# FAILED (it just downloads the build logs and produces no .snap), so
|
||||
# don't trust the exit code alone — require that .snap files exist.
|
||||
snaps=( wekan_${VERSION}_*.snap )
|
||||
snaps=( wekan_${VERSION}_${{ matrix.arch }}.snap )
|
||||
if [ "$rc" -eq 0 ] && [ "${#snaps[@]}" -gt 0 ]; then
|
||||
echo "Remote build succeeded on attempt $i: ${snaps[*]}"
|
||||
echo "Remote build ${{ matrix.arch }} succeeded on attempt $i: ${snaps[*]}"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i: snapcraft exit=$rc, .snap files produced=${#snaps[@]}."
|
||||
# Surface the Launchpad build logs so a real build failure is
|
||||
# diagnosable here, instead of as a later cryptic upload error.
|
||||
for log in snapcraft-wekan-*.txt; do
|
||||
echo "::group::Launchpad build log: $log (last 150 lines)"
|
||||
tail -n 150 "$log"
|
||||
@@ -876,34 +1106,34 @@ jobs:
|
||||
sleep 60
|
||||
fi
|
||||
done
|
||||
echo "::error::Remote build failed after $attempts attempts (no wekan_${VERSION}_*.snap produced)." >&2
|
||||
echo "::error::Remote build ${{ matrix.arch }} failed after $attempts attempts (no wekan_${VERSION}_${{ matrix.arch }}.snap produced)." >&2
|
||||
exit 1
|
||||
|
||||
- name: Push snaps to the Snap Store (edge + beta channels)
|
||||
- name: Push ${{ matrix.arch }} to the Snap Store (candidate + beta + edge)
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
snaps=( wekan_${VERSION}_*.snap )
|
||||
snaps=( wekan_${VERSION}_${{ matrix.arch }}.snap )
|
||||
if [ "${#snaps[@]}" -eq 0 ]; then
|
||||
echo "::error::No wekan_${VERSION}_*.snap files to upload."
|
||||
echo "::error::No wekan_${VERSION}_${{ matrix.arch }}.snap to upload."
|
||||
exit 1
|
||||
fi
|
||||
# base: core24 allows publishing to candidate (core26 does not). Stable is
|
||||
# published MANUALLY later, once the build has proven stable enough.
|
||||
for f in "${snaps[@]}"; do
|
||||
echo "Uploading $f to the Snap Store (edge,beta)..."
|
||||
snapcraft upload --release=edge,beta "$f"
|
||||
echo "Uploading $f to the Snap Store (candidate,beta,edge)..."
|
||||
snapcraft upload --release=candidate,beta,edge "$f"
|
||||
done
|
||||
|
||||
- name: Attach snaps to the GitHub Release
|
||||
- name: Attach the ${{ matrix.arch }} snap to the GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
snaps=( wekan_${VERSION}_*.snap )
|
||||
if [ "${#snaps[@]}" -eq 0 ]; then
|
||||
echo "::error::No wekan_${VERSION}_*.snap files to attach."
|
||||
exit 1
|
||||
snaps=( wekan_${VERSION}_${{ matrix.arch }}.snap )
|
||||
if [ "${#snaps[@]}" -gt 0 ]; then
|
||||
gh release upload "v${VERSION}" "${snaps[@]}" --clobber
|
||||
fi
|
||||
gh release upload "v${VERSION}" "${snaps[@]}" --clobber
|
||||
@@ -0,0 +1,145 @@
|
||||
name: Publish .spk to Sandstorm App Market
|
||||
|
||||
# Manually triggered. Takes the Sandstorm .spk that sandstorm.yml already built and
|
||||
# attached to a WeKan GitHub Release, renames it to wekan-<VERSION>.spk (VERSION is the
|
||||
# WeKan release version, e.g. 9.88), and publishes it to the Sandstorm App Market with
|
||||
# spk publish wekan-<VERSION>.spk
|
||||
# exactly like releases/release-sandstorm.sh does locally.
|
||||
#
|
||||
# gh workflow run sandstorm-store.yml
|
||||
# gh workflow run sandstorm-store.yml -f version=9.88
|
||||
# gh workflow run sandstorm-store.yml -f version=9.88 -f tag=v9.88
|
||||
#
|
||||
# Needs SANDSTORM_KEYRING (base64 of ~/.sandstorm-keyring) — `spk publish` proves app
|
||||
# ownership with the WeKan Sandstorm app's private key from that keyring — and installs
|
||||
# Sandstorm + assembles meteor-spk the same way sandstorm.yml does, so `spk` is present.
|
||||
# It does NOT rebuild the .spk; it publishes the one already on the release.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'WeKan version for the store filename wekan-<VERSION>.spk (e.g. 9.88). Default: derived from the release tag.'
|
||||
required: false
|
||||
default: ''
|
||||
tag:
|
||||
description: 'Release tag whose .spk to publish (default: newest release).'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
sandstorm-store:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
|
||||
run: |
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git
|
||||
|
||||
- name: Install Sandstorm (dev, unattended)
|
||||
# Provides the `spk` command used to publish, and the sandstorm-http-bridge that
|
||||
# build-deps.sh copies into the meteor-spk deps.
|
||||
run: curl https://install.sandstorm.io | sudo bash -s -- -d
|
||||
|
||||
- name: Assemble modern meteor-spk.deps (same as sandstorm.yml)
|
||||
run: |
|
||||
bash sandstorm-src/build-deps.sh
|
||||
echo "$HOME/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Restore the Sandstorm signing keyring
|
||||
env:
|
||||
SANDSTORM_KEYRING: ${{ secrets.SANDSTORM_KEYRING }}
|
||||
run: |
|
||||
if [ -n "${SANDSTORM_KEYRING:-}" ]; then
|
||||
# spk publish reads the app signing key from ~/.sandstorm-keyring (a FILE in
|
||||
# $HOME), same location meteor-spk pack uses.
|
||||
printf '%s' "$SANDSTORM_KEYRING" | base64 -d > "$HOME/.sandstorm-keyring"
|
||||
chmod 600 "$HOME/.sandstorm-keyring"
|
||||
echo "Restored ~/.sandstorm-keyring ($(wc -c < "$HOME/.sandstorm-keyring") bytes)."
|
||||
else
|
||||
echo "::error::SANDSTORM_KEYRING secret is not set. 'spk publish' proves ownership of the WeKan Sandstorm app with the private key from this keyring, so publishing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download the newest .spk from the WeKan release and name it wekan-<VERSION>.spk
|
||||
id: fetch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
IN_TAG: ${{ github.event.inputs.tag }}
|
||||
IN_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 1) Which release to publish from.
|
||||
TAG="${IN_TAG:-}"
|
||||
if [ -z "$TAG" ]; then
|
||||
TAG="$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -z "${TAG:-}" ] || [ "$TAG" = "null" ]; then
|
||||
echo "::error::No release found to publish the .spk from."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2) VERSION for the store filename wekan-<VERSION>.spk. Use the input, else
|
||||
# derive it from the tag (strip a leading 'v', e.g. v9.88 -> 9.88).
|
||||
VERSION="${IN_VERSION:-}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="${TAG#v}"
|
||||
fi
|
||||
echo "Release: $TAG -> store filename: wekan-${VERSION}.spk"
|
||||
|
||||
# 3) Download the newest .spk asset on that release. sandstorm.yml attaches it
|
||||
# as wekan-sandstorm-YYYY_MM_DD-HH_MM_SS.spk; if several exist, take the
|
||||
# lexicographically-newest (the timestamp sorts chronologically).
|
||||
ASSET="$(gh release view "$TAG" --json assets \
|
||||
--jq '[.assets[].name | select(endswith(".spk"))] | sort | last' 2>/dev/null || true)"
|
||||
if [ -z "${ASSET:-}" ] || [ "$ASSET" = "null" ]; then
|
||||
echo "::error::Release $TAG has no .spk asset. Run sandstorm.yml first to build and attach one."
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloading .spk asset: $ASSET"
|
||||
gh release download "$TAG" --pattern "$ASSET" --dir . --clobber
|
||||
|
||||
SPK="wekan-${VERSION}.spk"
|
||||
mv -f "$ASSET" "$SPK"
|
||||
ls -la "$SPK"
|
||||
echo "spk=$SPK" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish to the Sandstorm App Market
|
||||
env:
|
||||
SPK: ${{ steps.fetch.outputs.spk }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Resolve the spk binary: prefer PATH, then the Sandstorm install location.
|
||||
SPK_BIN="$(command -v spk || true)"
|
||||
if [ -z "$SPK_BIN" ]; then
|
||||
for c in /opt/sandstorm/latest/bin/spk /opt/sandstorm/spk /usr/local/bin/spk; do
|
||||
[ -x "$c" ] && SPK_BIN="$c" && break
|
||||
done
|
||||
fi
|
||||
if [ -z "$SPK_BIN" ]; then
|
||||
echo "::error::Could not find the 'spk' command from the Sandstorm install."
|
||||
exit 1
|
||||
fi
|
||||
echo "Publishing $SPK with $SPK_BIN ..."
|
||||
# Same command as releases/release-sandstorm.sh: spk publish wekan-<VERSION>.spk
|
||||
"$SPK_BIN" publish "$SPK"
|
||||
@@ -0,0 +1,144 @@
|
||||
name: Build Sandstorm .spk
|
||||
|
||||
# Manually triggered. Builds ONLY the Sandstorm .spk package (nothing else) and
|
||||
# uploads it as wekan-sandstorm-YYYY_MM_DD-HH_MM_SS.spk to the NEWEST WeKan GitHub
|
||||
# Release as an attachment, so you can download it, test whether the Sandstorm
|
||||
# build works, and see any errors — without running a full release.
|
||||
#
|
||||
# gh workflow run sandstorm.yml
|
||||
#
|
||||
# EXPERIMENTAL in CI (same caveats as the build-sandstorm job in release-all.yml):
|
||||
# * Sandstorm needs unprivileged user namespaces; GitHub's Ubuntu runners are VMs
|
||||
# that allow them, but this is the fragile part.
|
||||
# * meteor-spk SIGNS the .spk with the app private key from the Sandstorm keyring;
|
||||
# provide it as the SANDSTORM_KEYRING secret (base64 of ~/.sandstorm keyring).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Branch, tag or SHA to build the .spk from (default: main)'
|
||||
required: false
|
||||
default: 'main'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
sandstorm:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Build the dispatched ref so a fix branch can be tested before it is
|
||||
# merged to main; defaults to main for a plain `gh workflow run`.
|
||||
ref: ${{ github.event.inputs.ref || 'main' }}
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Enable unprivileged user namespaces (Ubuntu 24.04 restricts them)
|
||||
# Ubuntu 24.04's AppArmor defaults kernel.apparmor_restrict_unprivileged_userns=1,
|
||||
# which blocks the unprivileged user namespaces that the Sandstorm install and
|
||||
# the spk supervisor rely on (the "fragile part" noted above). Relax it so
|
||||
# `install.sandstorm.io` and meteor-spk can create sandboxes on the runner.
|
||||
# `|| true`: on kernels/runners without these knobs, setting them is a no-op.
|
||||
run: |
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1 2>/dev/null || true
|
||||
|
||||
- name: Install system dependencies and Meteor
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget curl build-essential g++ make python3 unzip xz-utils git
|
||||
if [ ! -f "$HOME/.meteor/meteor" ]; then curl https://install.meteor.com/ | sh; fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Sandstorm (dev, unattended)
|
||||
# Installed BEFORE assembling the deps: build-deps.sh's [bridge] step copies
|
||||
# the modern /opt/sandstorm/latest/bin/sandstorm-http-bridge over the ancient
|
||||
# bundled one, so the Sandstorm install must be in place first.
|
||||
run: curl https://install.sandstorm.io | sudo bash -s -- -d
|
||||
|
||||
- name: Assemble modern meteor-spk.deps (Node 24 + FerretDB + Mongo 3.x CLIs)
|
||||
# Base = upstream meteor-spk 0.6.0 from dl.sandstorm.io (has mongod 3.0 +
|
||||
# niscud). build-deps.sh swaps in Node 24, adds FerretDB + the Mongo 3.x
|
||||
# CLIs + the migration launcher/importer, keeps niscud, and swaps in the
|
||||
# modern sandstorm-http-bridge from the Sandstorm install above. No dependency
|
||||
# on the retired releases.wekan.team or the old projects.7z.
|
||||
run: |
|
||||
bash sandstorm-src/build-deps.sh
|
||||
echo "$HOME/projects/meteor-spk/meteor-spk-0.6.0" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Restore the Sandstorm signing keyring
|
||||
env:
|
||||
SANDSTORM_KEYRING: ${{ secrets.SANDSTORM_KEYRING }}
|
||||
run: |
|
||||
if [ -n "${SANDSTORM_KEYRING:-}" ]; then
|
||||
# meteor-spk / spk read the app signing key from ~/.sandstorm-keyring —
|
||||
# a FILE directly in $HOME (see the pack crash path
|
||||
# "/home/runner/.sandstorm-keyring"). Write it exactly there, NOT to
|
||||
# ~/.sandstorm/sandstorm-keyring, or `meteor-spk pack` aborts with
|
||||
# "open(~/.sandstorm-keyring): No such file or directory".
|
||||
printf '%s' "$SANDSTORM_KEYRING" | base64 -d > "$HOME/.sandstorm-keyring"
|
||||
chmod 600 "$HOME/.sandstorm-keyring"
|
||||
echo "Restored ~/.sandstorm-keyring ($(wc -c < "$HOME/.sandstorm-keyring") bytes)."
|
||||
else
|
||||
echo "::error::SANDSTORM_KEYRING secret is not set. meteor-spk pack signs the .spk with the WeKan Sandstorm app's private key, which lives in this keyring, so packing cannot proceed without it. Add the base64 of your ~/.sandstorm-keyring file as the SANDSTORM_KEYRING repository secret (e.g. 'base64 -w0 ~/.sandstorm-keyring')."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Meteor npm dependencies
|
||||
# PUPPETEER_SKIP_DOWNLOAD: puppeteer is a devDependency used only by tests,
|
||||
# not by the server, so it never belongs in the packed .spk. Skipping its
|
||||
# Chromium download (~150 MB) avoids any risk of it bloating the build and
|
||||
# speeds up CI.
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: '1'
|
||||
run: |
|
||||
(meteor update --npm 2>/dev/null || true) && meteor npm install
|
||||
|
||||
- name: Build the Sandstorm .spk
|
||||
id: build
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: '1'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STAMP="$(date +'%Y_%m_%d-%H_%M_%S')"
|
||||
SPK="wekan-sandstorm-${STAMP}.spk"
|
||||
meteor-spk pack "$SPK"
|
||||
# Report the packed size and warn if it exceeds Cloudflare's 100 MB upload
|
||||
# cap (a proxied browser upload to a Cloudflare-fronted Sandstorm 413s above
|
||||
# this; install via a DNS-only host / SSH tunnel, or trim further).
|
||||
SPK_BYTES=$(stat -c%s "$SPK")
|
||||
echo "Packed $SPK: $((SPK_BYTES/1024/1024)) MiB ($SPK_BYTES bytes)"
|
||||
if [ "$SPK_BYTES" -gt $((100*1024*1024)) ]; then
|
||||
echo "::warning::$SPK is $((SPK_BYTES/1024/1024)) MiB — over Cloudflare's 100 MB upload limit. Upload via a DNS-only host / SSH tunnel to Sandstorm, or trim the bundle further."
|
||||
fi
|
||||
ls -la "$SPK"
|
||||
echo "spk=$SPK" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.build.outputs.spk }}
|
||||
path: ${{ steps.build.outputs.spk }}
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload the .spk to the newest WeKan release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || true)"
|
||||
if [ -z "${TAG:-}" ] || [ "$TAG" = "null" ]; then
|
||||
echo "::error::No existing GitHub Release found to attach the .spk to."
|
||||
exit 1
|
||||
fi
|
||||
echo "Uploading ${{ steps.build.outputs.spk }} to the newest release: $TAG"
|
||||
gh release upload "$TAG" "${{ steps.build.outputs.spk }}" --clobber
|
||||
+14
@@ -70,3 +70,17 @@ test-results/
|
||||
_build-local-test
|
||||
*/build-assets-local-test
|
||||
*/build-chunks-local-test
|
||||
|
||||
# Local-only working copies: other wekan repos cloned under this repo for editing,
|
||||
# and build inputs. These are separate git repos / large machine-specific dirs;
|
||||
# never commit them.
|
||||
# - projects/ : unarchived meteor-spk fork (Node/Mongo/FerretDB binaries)
|
||||
# - sandstorm/ : clone of sandstorm-io/sandstorm (platform source, for reference)
|
||||
# - FerretDB/ : clone of wekan/FerretDB
|
||||
# - mongo-tools/ : clone of wekan/mongo-tools
|
||||
# - mongosh/ : clone of wekan/mongosh
|
||||
/projects/
|
||||
/sandstorm/
|
||||
/FerretDB/
|
||||
/mongo-tools/
|
||||
/mongosh/
|
||||
+1427
-13
File diff suppressed because it is too large.
Load diff
+36
-10
@@ -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.78
|
||||
ARG VERSION=9.89
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
@@ -185,24 +185,35 @@ apt-get update --assume-yes
|
||||
apt-get upgrade --assume-yes
|
||||
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
|
||||
|
||||
# Multi-arch mapping: Docker TARGETARCH -> Node.js arch name + WeKan bundle name
|
||||
# arm/v7 uses TARGETARCH=arm; armhf has no MongoDB Community -> uses FerretDB
|
||||
# Multi-arch mapping: Docker TARGETARCH -> Node.js arch name + WeKan bundle name.
|
||||
# amd64/arm64 have MongoDB Community; ppc64le/s390x/riscv64 have no MongoDB server
|
||||
# and ship FerretDB v1 instead (the ferretdb binary is baked into their .zip and
|
||||
# started by wekan-entrypoint.sh). riscv64's Node.js 24 comes from
|
||||
# unofficial-builds.nodejs.org (nodejs.org ships no riscv64). armv7l is excluded:
|
||||
# there is no Node.js 24 build for it anywhere.
|
||||
NODE_BASE="official"
|
||||
case "${TARGETARCH}" in
|
||||
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
|
||||
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
|
||||
"arm") NODE_ARCH="armv7l" WEKAN_ARCH="armhf" ;;
|
||||
"ppc64le") NODE_ARCH="ppc64le" WEKAN_ARCH="ppc64le" ;;
|
||||
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
|
||||
"riscv64") NODE_ARCH="riscv64" WEKAN_ARCH="riscv64" NODE_BASE="unofficial" ;;
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Node.js Installation (official nodejs.org builds for Node 22)
|
||||
# Node.js installation. Official nodejs.org builds for amd64/arm64/ppc64le/s390x;
|
||||
# unofficial-builds.nodejs.org for riscv64 (nodejs.org ships no riscv64 binary).
|
||||
cd /tmp
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc"
|
||||
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc | shasum -a 256 -c -
|
||||
if [ "${NODE_BASE}" = "unofficial" ]; then
|
||||
NODE_DIST="https://unofficial-builds.nodejs.org/download/release/${NODE_VERSION}"
|
||||
else
|
||||
NODE_DIST="https://nodejs.org/dist/${NODE_VERSION}"
|
||||
fi
|
||||
wget "${NODE_DIST}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "${NODE_DIST}/SHASUMS256.txt"
|
||||
grep " node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz\$" SHASUMS256.txt | shasum -a 256 -c -
|
||||
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc
|
||||
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
|
||||
# NPM configuration
|
||||
@@ -229,6 +240,14 @@ rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
npm install --prefix ./bundle/programs/server
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# The .zip bundle now ships a self-contained launcher + its own Node.js for the
|
||||
# offline downloads; the Docker image installs its own Node and uses
|
||||
# wekan-entrypoint.sh, so drop the redundant bundled node + launchers. Keeps
|
||||
# /build/ferretdb (used by the entrypoint) and the per-arch MongoDB Database Tools
|
||||
# (bsondump, mongodump, mongorestore, … from wekan/mongo-tools, embedded in the
|
||||
# bundle) for backup/restore inside the container. Saves ~80 MB per arch.
|
||||
rm -f /build/node /build/start-wekan.sh /build/start-wekan.bat
|
||||
|
||||
# Restore original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
@@ -247,10 +266,17 @@ mkdir -p /data
|
||||
chown wekan:wekan --recursive /data
|
||||
EOR
|
||||
|
||||
# Database-backend selector entrypoint. Every arch's bundle now ships a FerretDB
|
||||
# binary (baked into the .zip and moved to /build/ferretdb above); this script
|
||||
# starts FerretDB v1 (SQLite) or leaves the DB external based on WEKAN_DB and the
|
||||
# /build/.ferretdb-default marker (present only on MongoDB-less arches). See
|
||||
# releases/ferretdb/wekan-entrypoint.sh.
|
||||
COPY --chmod=755 releases/ferretdb/wekan-entrypoint.sh /build/wekan-entrypoint.sh
|
||||
|
||||
USER wekan
|
||||
ENV PORT=8080
|
||||
EXPOSE $PORT
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /build
|
||||
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"]
|
||||
CMD ["bash", "/build/wekan-entrypoint.sh"]
|
||||
@@ -103,10 +103,10 @@ By working directly with WeKan ® you get the benefit of active maintenance and
|
||||
|
||||
## Getting Started with Development
|
||||
|
||||
The default branch uses Meteor 3.5-rc.2 with Node.js 24.x.
|
||||
The main branch uses Meteor 3.5 with Node.js 24.x.
|
||||
See [CHANGELOG.md](https://github.com/wekan/wekan/blob/main/CHANGELOG.md) for the latest runtime updates.
|
||||
|
||||
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
|
||||
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-install-dependencies-build-wekan-and-run-the-dev-server). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
|
||||
|
||||
Please refer to the [developer documentation](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md) for more information.
|
||||
|
||||
@@ -138,11 +138,17 @@ curl https://install.meteor.com/ | sh
|
||||
|
||||
### Building WeKan
|
||||
|
||||
The `rebuild-wekan.sh` script guides you through a three-stage process:
|
||||
The `rebuild-wekan.sh` script shows a two-level menu. The top level groups options into categories:
|
||||
|
||||
1. **Install dependencies (Option 1)** - Downloads all required Meteor packages and npm modules
|
||||
2. **Build WeKan (Option 2)** - Compiles the application
|
||||
3. **Run WeKan (Option 3)** - Starts the development server at http://localhost:4000
|
||||
```
|
||||
1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit
|
||||
```
|
||||
|
||||
You pick a category number, then the item number inside it (each submenu also has a `Back` entry). Building from source is a three-stage process:
|
||||
|
||||
1. **Setup -> Install dependencies** - Downloads all required Meteor packages and npm modules
|
||||
2. **Setup -> Build WeKan** - Compiles the application
|
||||
3. **Dev server -> localhost:3000** - Starts the development server at http://localhost:3000
|
||||
|
||||
```bash
|
||||
# Clone your fork
|
||||
@@ -152,25 +158,27 @@ cd wekan
|
||||
# Make the script executable
|
||||
chmod +x rebuild-wekan.sh
|
||||
|
||||
# Step 1: Install dependencies
|
||||
# Step 1: Install dependencies (Setup -> Install dependencies)
|
||||
./rebuild-wekan.sh
|
||||
# Press 1 and Enter
|
||||
# Press 1 (Setup) and Enter, then 1 (Install dependencies) and Enter
|
||||
|
||||
# Step 2: Build WeKan (after dependencies complete)
|
||||
# Step 2: Build WeKan (Setup -> Build WeKan, after dependencies complete)
|
||||
./rebuild-wekan.sh
|
||||
# Press 2 and Enter
|
||||
# Press 1 (Setup) and Enter, then 2 (Build WeKan) and Enter
|
||||
|
||||
# Step 3: Run WeKan in development mode
|
||||
# Step 3: Run WeKan in development mode (Dev server -> localhost:3000)
|
||||
./rebuild-wekan.sh
|
||||
# Press 3 and Enter
|
||||
# Press 2 (Dev server) and Enter, then 1 (localhost:3000) and Enter
|
||||
```
|
||||
|
||||
If a dev server is already running on that port, the **Dev server** options stop it automatically and start a fresh server on the same port.
|
||||
|
||||
|
||||
### WSL Users
|
||||
|
||||
WSL users can use Snap Candidate. See [install docs](https://wekan.fi/install/).
|
||||
|
||||
The development server will start at http://localhost:4000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
|
||||
The development server will start at http://localhost:3000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
|
||||
|
||||
## Screenshot
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v9.78.0"
|
||||
appVersion: "v9.89.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -37,6 +37,8 @@ If *nix: chmod +x api.py => ./api.py users
|
||||
python3 api.py lists BOARDID # Lists of BOARDID
|
||||
python3 api.py list BOARDID LISTID # Info of LISTID
|
||||
python3 api.py createlist BOARDID LISTTITLE # Create list
|
||||
python3 api.py editlist BOARDID LISTID NEWLISTTITLE # Edit list title
|
||||
python3 api.py editlistcolor BOARDID LISTID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit list color
|
||||
python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
|
||||
python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
@@ -432,6 +434,49 @@ if arguments == 5:
|
||||
|
||||
if arguments >= 4:
|
||||
|
||||
if sys.argv[1] == 'editlist':
|
||||
|
||||
# ------- EDIT LIST START -----------
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
newlisttitle = sys.argv[4]
|
||||
edlist = wekanurl + apiboards + boardid + s + l + s + listid
|
||||
print(edlist)
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {'title': '{}'.format(newlisttitle)}
|
||||
body = requests.put(edlist, data=put_data, headers=headers)
|
||||
print("=== EDIT LIST ===\n")
|
||||
body = requests.get(edlist, headers=headers)
|
||||
data2 = body.text.replace('}', "}\n")
|
||||
print(data2)
|
||||
# ------- EDIT LIST END -----------
|
||||
|
||||
if sys.argv[1] == 'editlistcolor':
|
||||
|
||||
# ------- EDIT LIST COLOR START -----------
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
newcolor = sys.argv[4]
|
||||
|
||||
valid_colors = ['white', 'green', 'yellow', 'orange', 'red', 'purple', 'blue', 'sky', 'lime', 'pink', 'black',
|
||||
'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', 'slateblue', 'magenta', 'gold', 'navy',
|
||||
'gray', 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo']
|
||||
|
||||
if newcolor not in valid_colors:
|
||||
print("Invalid color. Choose a color from the list.")
|
||||
sys.exit(1)
|
||||
|
||||
edlist = wekanurl + apiboards + boardid + s + l + s + listid
|
||||
print(edlist)
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {'color': '{}'.format(newcolor)}
|
||||
body = requests.put(edlist, data=put_data, headers=headers)
|
||||
print("=== EDIT LIST COLOR ===\n")
|
||||
body = requests.get(edlist, headers=headers)
|
||||
data2 = body.text.replace('}', "}\n")
|
||||
print(data2)
|
||||
# ------- EDIT LIST COLOR END -----------
|
||||
|
||||
if sys.argv[1] == 'newuser':
|
||||
|
||||
# ------- CREATE NEW USER START -----------
|
||||
|
||||
+10
-4
@@ -1,7 +1,13 @@
|
||||
// Fix dynamic-import chunk loading when Wekan runs under a sub-path (ROOT_URL includes a pathname,
|
||||
// e.g. https://example.com/wekan). Rspack uses __webpack_public_path__ to prefix chunk URLs; by
|
||||
// default it is set to /build-chunks/ (no context path). We update it here, at module evaluation
|
||||
// time, before any import() expression runs (those are deferred to Meteor.startup callbacks).
|
||||
// e.g. https://example.com/wekan). Rspack's runtime builds every chunk URL as
|
||||
// __webpack_public_path__ + chunkName, and the chunk name ALREADY includes the "build-chunks/"
|
||||
// prefix (its runtime does `u = e => "build-chunks/" + e + "." + hash + ".js"`). The default
|
||||
// public path is "/", so at the site root chunks correctly resolve to "/build-chunks/<chunk>".
|
||||
// Under a sub-path we must prefix that sub-path — but must NOT re-add "build-chunks/" ourselves,
|
||||
// or the URL becomes "/wekan/build-chunks/build-chunks/<chunk>" and 404s (issue #6445, which broke
|
||||
// the lazy-loaded language chunks). So set the public path to "<sub-path>/" and let rspack append
|
||||
// "build-chunks/" itself. Done here at module evaluation time, before any import() expression runs
|
||||
// (those are deferred to Meteor.startup callbacks).
|
||||
/* global __webpack_public_path__:writable */
|
||||
try {
|
||||
const _cfg = typeof window !== 'undefined' && window.__meteor_runtime_config__;
|
||||
@@ -9,7 +15,7 @@ try {
|
||||
if (_rootUrl) {
|
||||
const _rootPath = new URL(_rootUrl).pathname.replace(/\/+$/, '');
|
||||
if (_rootPath && _rootPath !== '/') {
|
||||
__webpack_public_path__ = _rootPath + '/build-chunks/';
|
||||
__webpack_public_path__ = _rootPath + '/';
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
@@ -15,6 +15,20 @@
|
||||
.reactions-popup .add-comment-reaction:hover {
|
||||
background-color: #b0c4de;
|
||||
}
|
||||
/* Eye / eye-slash icon toggle for showing/hiding activities. Open eye
|
||||
(.eye-icon) = activities shown; crossed eye (.eye-slash-icon) = hidden.
|
||||
Matches the password-visibility toggle used on the login/register pages. */
|
||||
.activities-eye-toggle {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.activities-eye-toggle .eye-icon,
|
||||
.activities-eye-toggle .eye-slash-icon {
|
||||
line-height: 1;
|
||||
}
|
||||
.activities {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@ template(name="activity")
|
||||
|
||||
//- comment activity ----------------------------------------------------
|
||||
if($eq activity.activityType 'deleteComment')
|
||||
| {{{_ 'activity-deleteComment' activity.commentId}}}.
|
||||
| {{{_ 'activity-deleteComment' (sanitize activity.commentDisplayText)}}}.
|
||||
|
||||
if($eq activity.activityType 'editComment')
|
||||
| {{{_ 'activity-editComment' activity.commentId}}}.
|
||||
| {{{_ 'activity-editComment' (sanitize activity.commentDisplayText)}}}.
|
||||
|
||||
if($eq activity.activityType 'addComment')
|
||||
| {{{_ 'activity-on' cardLink}}}
|
||||
@@ -133,6 +133,9 @@ template(name="activity")
|
||||
if($eq activity.activityType 'a-changedTitle')
|
||||
| {{{_ 'activity-changedTitle' (sanitize activity.value) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'a-changedDescription')
|
||||
| {{{_ 'activity-changedDescription' cardLink}}}.
|
||||
|
||||
//- customField activity ------------------------------------------------
|
||||
if($eq activity.activityType 'createCustomField')
|
||||
| {{_ 'activity-customfield-created' customField}}.
|
||||
|
||||
@@ -6,11 +6,13 @@ import '/client/lib/dragscrollTouch';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import { toFullCalendarFirstDay } from '/client/lib/calendarFirstDay';
|
||||
import { weekNumberByFirstDay } from '/models/lib/weekStart';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndex } = Utils;
|
||||
@@ -568,17 +570,32 @@ Template.boardBody.onRendered(function () {
|
||||
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
|
||||
}
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member
|
||||
// #6446: enable swimlane drag-drop — which also carries moving cards BETWEEN
|
||||
// swimlanes — for any board member with write access, not only board admins.
|
||||
// The old !isBoardAdmin() check disabled this sortable for every non-admin, so
|
||||
// a normal member could still move a card WITHIN a swimlane but not BETWEEN
|
||||
// swimlanes (a card crossing swimlanes is received across this sortable), and
|
||||
// could not reorder swimlanes. canModifyCard() is the client write-access
|
||||
// predicate (board member, not comment-only / worker / read-only), matching
|
||||
// the server's allowIsBoardMemberWithWriteAccess.
|
||||
$swimlanesDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
||||
!Utils.canModifyCard(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
dragscroll.reset();
|
||||
Utils.setBackgroundImage();
|
||||
// #4978: apply the board background inside a reactive autorun instead of a
|
||||
// one-shot call. Switching directly between boards (e.g. the favorites bar)
|
||||
// reuses this template instance and never re-fires onRendered, so the
|
||||
// background used to stay on the previously shown board. Reading the current
|
||||
// board inside setBackgroundImage() tracks both board switches and background
|
||||
// edits; it performs no DB writes, so there is no autorun write loop.
|
||||
this.autorun(() => {
|
||||
Utils.setBackgroundImage();
|
||||
});
|
||||
});
|
||||
|
||||
Template.boardBody.onDestroyed(function () {
|
||||
@@ -929,6 +946,11 @@ Template.calendarView.onRendered(function () {
|
||||
document.documentElement.dir = TAPi18n.getLanguageDirection();
|
||||
|
||||
this.autorun(function () {
|
||||
// #5656: refetch calendar events whenever the active board Filter changes
|
||||
// so the Calendar view stays in sync with the other views. Reading the
|
||||
// Filter's reactive state here establishes the dependency; the events()
|
||||
// callback below applies the actual selector.
|
||||
Filter.isActive();
|
||||
const calendarEl = document.getElementById('calendar-view');
|
||||
if (calendarEl && calendarEl._wekanCalendar) {
|
||||
calendarEl._wekanCalendar.refetchEvents();
|
||||
@@ -959,6 +981,12 @@ Template.calendarView.helpers({
|
||||
editable: true,
|
||||
selectable: true,
|
||||
weekNumbers: true,
|
||||
// #4946: FullCalendar's default weekNumberCalculation is locale-based
|
||||
// (Sunday-anchored for many locales), so the week-number column ignored
|
||||
// the user's start-day-of-week setting even though `firstDay` moved the
|
||||
// grid. Compute the number from the SAME firstDay so the label matches the
|
||||
// way the week is laid out.
|
||||
weekNumberCalculation: date => weekNumberByFirstDay(date, firstDay),
|
||||
// Use non-localized AM/PM time format to avoid confusing notations like 上/下/中
|
||||
// Use full 'am'/'pm' instead of single-letter 'a'/'p' for clarity
|
||||
eventTimeFormat: {
|
||||
@@ -1002,6 +1030,12 @@ Template.calendarView.helpers({
|
||||
isRTL: TAPi18n.isRTL(),
|
||||
events(fetchInfo, callback) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
// #5656: apply the active board Filter (member / assignee / due-date /
|
||||
// label / custom-field) so the Calendar view shows the same scoped set
|
||||
// of cards as the Board / Lists / Swimlanes views.
|
||||
const filterSelector = Filter.isActive()
|
||||
? Filter._getMongoSelector()
|
||||
: undefined;
|
||||
const events = [];
|
||||
const pushEvent = function (card, title, start, end, extraCls) {
|
||||
start = start || card.startAt;
|
||||
@@ -1026,12 +1060,12 @@ Template.calendarView.helpers({
|
||||
});
|
||||
};
|
||||
currentBoard
|
||||
.cardsInInterval(fetchInfo.start, fetchInfo.end)
|
||||
.cardsInInterval(fetchInfo.start, fetchInfo.end, filterSelector)
|
||||
.forEach(function (card) {
|
||||
pushEvent(card);
|
||||
});
|
||||
currentBoard
|
||||
.cardsDueInBetween(fetchInfo.start, fetchInfo.end)
|
||||
.cardsDueInBetween(fetchInfo.start, fetchInfo.end, filterSelector)
|
||||
.forEach(function (card) {
|
||||
pushEvent(
|
||||
card,
|
||||
|
||||
@@ -1078,6 +1078,13 @@ body.grey-icons-enabled .checkmark-no-grey {
|
||||
margin: 8px; /* Space between board items */
|
||||
}
|
||||
|
||||
/* #6439: highlight the board a dragged board will be dropped before, when
|
||||
reordering in the Custom (drag order) sort mode. */
|
||||
.js-board.board-reorder-over {
|
||||
outline: 2px dashed #3498db;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reset background for add-board button */
|
||||
.js-add-board {
|
||||
background-color: transparent !important;
|
||||
|
||||
@@ -6,6 +6,10 @@ import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { BoardMultiSelection } from '/client/lib/boardMultiSelection';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import {
|
||||
isDragReorderEnabled,
|
||||
computeReorderedSortIndex,
|
||||
} from '/models/lib/boardSortReorder';
|
||||
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
|
||||
@@ -59,6 +63,15 @@ const DEFAULT_WORKSPACE_ICON = '📁';
|
||||
// Matches the Admin Panel > People page size.
|
||||
const BOARDS_PER_PAGE = 25;
|
||||
|
||||
// #6439: the effective All Boards sort mode for the current user, defaulting to
|
||||
// 'custom' (manual drag order) when unknown. Used to gate board drag-reordering.
|
||||
function currentAllBoardsSortBy() {
|
||||
const cu = ReactiveCache.getCurrentUser();
|
||||
return cu && typeof cu.getAllBoardsSortBy === 'function'
|
||||
? cu.getAllBoardsSortBy()
|
||||
: 'custom';
|
||||
}
|
||||
|
||||
function getCurrentWorkspacesTree() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const tree =
|
||||
@@ -973,6 +986,55 @@ Template.boardList.events({
|
||||
document.querySelectorAll('.workspace-node.board-drag-hint, .js-select-menu.board-drag-hint').forEach((el) => {
|
||||
el.classList.remove('board-drag-hint');
|
||||
});
|
||||
document.querySelectorAll('.js-board.board-reorder-over').forEach((el) => {
|
||||
el.classList.remove('board-reorder-over');
|
||||
});
|
||||
},
|
||||
// #6439: reorder boards on the All Boards page by dropping one board onto
|
||||
// another. Only active in the "custom" (manual drag order) sort mode — in the
|
||||
// sorted modes the order comes from the title, so a board is not a valid drop
|
||||
// target (which is why, before this, the browser showed the not-allowed cursor
|
||||
// and nothing persisted). The reorder decision/index math lives in the pure,
|
||||
// unit-tested models/lib/boardSortReorder.js.
|
||||
'dragover .js-board'(evt, tpl) {
|
||||
if (!isDragReorderEnabled(currentAllBoardsSortBy())) return;
|
||||
// A multi-board drag is a share/assign gesture, not a reorder.
|
||||
const dt = evt.originalEvent.dataTransfer;
|
||||
if (dt && dt.getData('application/x-board-multi') === 'true') return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (dt) dt.dropEffect = 'move';
|
||||
evt.currentTarget.classList.add('board-reorder-over');
|
||||
},
|
||||
'dragleave .js-board'(evt) {
|
||||
evt.currentTarget.classList.remove('board-reorder-over');
|
||||
},
|
||||
'drop .js-board'(evt, tpl) {
|
||||
if (!isDragReorderEnabled(currentAllBoardsSortBy())) return;
|
||||
const dt = evt.originalEvent.dataTransfer;
|
||||
if (dt && dt.getData('application/x-board-multi') === 'true') return;
|
||||
const draggedId = dt ? dt.getData('text/plain') : '';
|
||||
if (!draggedId) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt.currentTarget.classList.remove('board-reorder-over');
|
||||
const targetId = this._id;
|
||||
// #6442: current display order of the board grid. Read each board's _id from
|
||||
// its Blaze data context (the same source dragstart uses via `this._id`) —
|
||||
// NOT el.classList[0]. On `li.js-board(class="{{_id}} …")` Jade emits the
|
||||
// literal `js-board` class FIRST, so classList[0] is the string "js-board"
|
||||
// for every board; the old code therefore produced ['js-board','js-board',…],
|
||||
// computeReorderedSortIndex could not find the dragged/target ids among them,
|
||||
// returned null, and the drop silently did nothing (board snapped back).
|
||||
const orderedIds = Array.from(tpl.findAll('.js-board'))
|
||||
.map((el) => Blaze.getData(el)?._id)
|
||||
.filter(Boolean);
|
||||
const mapping = computeReorderedSortIndex(orderedIds, draggedId, targetId);
|
||||
if (!mapping) return;
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser && typeof currentUser.setBoardSortIndexes === 'function') {
|
||||
currentUser.setBoardSortIndexes(mapping);
|
||||
}
|
||||
},
|
||||
'click .js-clone-board'(evt) {
|
||||
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||
|
||||
@@ -66,7 +66,7 @@ template(name="tableView")
|
||||
td.table-view-cell-labels
|
||||
.div
|
||||
each label in row.labels
|
||||
span.card-label(class="card-label-{{label.color}}" title="{{label.name}}")
|
||||
span.card-label(class="{{labelColorClass label.color}}" style="{{labelColorStyle label.color}}" title="{{label.name}}")
|
||||
+viewer
|
||||
= label.name
|
||||
if showReceivedColumn
|
||||
|
||||
@@ -432,11 +432,16 @@ export async function handleFileUpload(card, files) {
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
// Use HTTP transport instead of DDP so file chunks go over a dedicated
|
||||
// fetch POST rather than flooding the WebSocket/DDP channel. DDP
|
||||
// upload causes repeated DDP reconnects in Safari, showing the
|
||||
// "Loading, please wait" offline banner and stalling progress at ~95%.
|
||||
transport: 'http',
|
||||
// Use HTTP transport (a dedicated fetch POST per chunk) rather than DDP,
|
||||
// which floods the WebSocket channel and causes repeated reconnects in
|
||||
// Safari (the "Loading, please wait" offline banner, stalling at ~95%).
|
||||
// EXCEPT on Sandstorm: the grain's sandstorm-http-bridge strips Meteor-Files'
|
||||
// custom upload headers — x-start/x-mtok/x-chunkid/x-fileid/x-eof are not on
|
||||
// Sandstorm's request-header whitelist — so the server never sees x-start,
|
||||
// treats every request as a chunk continuation, and fails with "Can't
|
||||
// continue upload, session expired" [408]. DDP uploads go over method calls
|
||||
// (no custom HTTP headers), so they work through the bridge.
|
||||
transport: Meteor.settings?.public?.sandstorm ? 'ddp' : 'http',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
|
||||
@@ -528,7 +533,9 @@ Template.previewClipboardImagePopup.events({
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
fileName: file.name || file.type.replace('image/', 'clipboard.'),
|
||||
chunkSize: 'dynamic',
|
||||
transport: 'http',
|
||||
// DDP on Sandstorm (its bridge strips Meteor-Files' x-* upload headers),
|
||||
// HTTP elsewhere — see the note in the other upload handler above.
|
||||
transport: Meteor.settings?.public?.sandstorm ? 'ddp' : 'http',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
const uploader = await Attachments.insertAsync(
|
||||
|
||||
@@ -6,7 +6,7 @@ template(name="cardDetails")
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}' style='{{colorStyle}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
@@ -127,7 +127,7 @@ template(name="cardDetails")
|
||||
| {{_ 'labels'}}
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
span.card-label(class="card-label-{{color}}" title=name)
|
||||
span.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if canModifyCard
|
||||
@@ -741,12 +741,12 @@ template(name="cardDetails")
|
||||
i.fa.fa-history
|
||||
| {{ _ 'activities'}}
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'show-activities'}}")
|
||||
if showActivities
|
||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
|
||||
label.toggle-label(for="toggleShowActivitiesCard")
|
||||
if showActivities
|
||||
a.activities-eye-toggle.js-toggle-show-activities-card(title="{{_ 'hide-activities'}}" aria-label="{{_ 'hide-activities'}}")
|
||||
i.fa.fa-eye.eye-icon
|
||||
else
|
||||
a.activities-eye-toggle.js-toggle-show-activities-card(title="{{_ 'show-activities'}}" aria-label="{{_ 'show-activities'}}")
|
||||
i.fa.fa-eye-slash.eye-slash-icon
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
if isLoaded.get
|
||||
@@ -1199,6 +1199,10 @@ template(name="setCardColorPopup")
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
// #5514: native color wheel for picking a custom RGB / hex color
|
||||
.color-wheel-wrap
|
||||
label(for="cardColorWheel") {{_ 'custom-color'}}
|
||||
input.js-card-color-wheel#cardColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ import Users from '/models/users';
|
||||
import Lists from '/models/lists';
|
||||
import CardComments from '/models/cardComments';
|
||||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { isHexColor, toHex } from '/models/lib/contrastColor';
|
||||
import { uniqBy } from '/imports/lib/collectionHelpers';
|
||||
import { memberTargetBoardId } from '/models/lib/linkedCardMembers';
|
||||
import { isSubmitKey } from '/models/lib/editorSubmitKey';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
@@ -1046,7 +1048,7 @@ Template.cardDetails.events({
|
||||
const card = Template.currentData();
|
||||
await card.toggleHideCheckedChecklistItems();
|
||||
},
|
||||
async 'change #toggleShowActivitiesCard'() {
|
||||
async 'click .js-toggle-show-activities-card'() {
|
||||
const card = Template.currentData();
|
||||
await card.toggleShowActivities();
|
||||
},
|
||||
@@ -1498,9 +1500,11 @@ Template.editCardTitleForm.events({
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
'keydown .js-edit-card-title'(event) {
|
||||
// If enter key was pressed, submit the data
|
||||
// Unless the shift key is also being pressed
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
// #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)) {
|
||||
$('.js-submit-edit-card-title-form').click();
|
||||
}
|
||||
},
|
||||
@@ -2032,6 +2036,10 @@ Template.setCardColorPopup.helpers({
|
||||
}
|
||||
return tpl.currentColor.get() === color;
|
||||
},
|
||||
// #5514: current color as a '#rrggbb' hex for the native color wheel <input>.
|
||||
currentColorHex() {
|
||||
return toHex(Template.instance().currentColor.get()) || '#0079bf';
|
||||
},
|
||||
});
|
||||
|
||||
Template.setCardColorPopup.events({
|
||||
@@ -2039,6 +2047,13 @@ Template.setCardColorPopup.events({
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
// #5514: picking from the native color wheel stores a custom hex.
|
||||
'input .js-card-color-wheel'(event, tpl) {
|
||||
const value = event.currentTarget.value;
|
||||
if (isHexColor(value)) {
|
||||
tpl.currentColor.set(value);
|
||||
}
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/* #5514: native color-wheel (custom RGB / hex) picker wrapper, shared by the
|
||||
label / list / swimlane / card color popups. */
|
||||
.color-wheel-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 6px 0 10px 0;
|
||||
}
|
||||
.color-wheel-wrap label {
|
||||
margin: 0;
|
||||
}
|
||||
.color-wheel-wrap input[type="color"] {
|
||||
width: 44px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -4,9 +4,12 @@ template(name="formLabel")
|
||||
|
||||
label {{_ "select-color"}}
|
||||
.palette-colors: each labels
|
||||
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
|
||||
span.card-label.palette-color.js-palette-color(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
.color-wheel-wrap
|
||||
label(for="labelColorWheel") {{_ 'custom-color'}}
|
||||
input.js-label-color-wheel#labelColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
|
||||
template(name="createLabelPopup")
|
||||
form.create-label
|
||||
@@ -31,8 +34,8 @@ template(name="cardLabelsPopup")
|
||||
a.card-label-edit-button.fa.fa-pencil.js-edit-label
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="{{labelColorClass color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}" style="{{labelColorStyle color}}")
|
||||
+viewer
|
||||
= name
|
||||
if(isLabelSelected ../_id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { isHexColor, toHex } from '/models/lib/contrastColor';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
@@ -28,6 +29,11 @@ Template.formLabel.helpers({
|
||||
isSelected(color) {
|
||||
return Template.instance().currentColor.get() === color;
|
||||
},
|
||||
// #5514: current color as a '#rrggbb' hex for the color-wheel <input>, which
|
||||
// only accepts hex. Named colors map to their palette hex; fall back to green.
|
||||
currentColorHex() {
|
||||
return toHex(Template.instance().currentColor.get()) || '#3cb500';
|
||||
},
|
||||
});
|
||||
|
||||
Template.formLabel.events({
|
||||
@@ -36,6 +42,13 @@ Template.formLabel.events({
|
||||
const selectedColor = paletteData?.color || Template.currentData()?.color || getFallbackLabelColor();
|
||||
tpl.currentColor.set(selectedColor);
|
||||
},
|
||||
// #5514: picking a color from the native color wheel stores a custom hex.
|
||||
'input .js-label-color-wheel'(event, tpl) {
|
||||
const value = event.currentTarget.value;
|
||||
if (isHexColor(value)) {
|
||||
tpl.currentColor.set(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.createLabelPopup.helpers({
|
||||
@@ -131,7 +144,16 @@ Template.createLabelPopup.events({
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
// #5514: a named swatch (has the check icon) wins; otherwise fall back to
|
||||
// the custom color-wheel hex, then to the default palette color.
|
||||
let color = selectedColorData?.color;
|
||||
if (!color) {
|
||||
const wheel = templateInstance.find('.js-label-color-wheel');
|
||||
if (wheel && isHexColor(wheel.value)) {
|
||||
color = wheel.value;
|
||||
}
|
||||
}
|
||||
color = color || getFallbackLabelColor();
|
||||
board.addLabel(name, color);
|
||||
Popup.back();
|
||||
},
|
||||
@@ -158,7 +180,16 @@ Template.editLabelPopup.events({
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
// #5514: a named swatch (has the check icon) wins; otherwise fall back to
|
||||
// the custom color-wheel hex, then to the default palette color.
|
||||
let color = selectedColorData?.color;
|
||||
if (!color) {
|
||||
const wheel = templateInstance.find('.js-label-color-wheel');
|
||||
if (wheel && isHexColor(wheel.value)) {
|
||||
color = wheel.value;
|
||||
}
|
||||
}
|
||||
color = color || getFallbackLabelColor();
|
||||
board.editLabel(this._id, name, color);
|
||||
Popup.back();
|
||||
},
|
||||
|
||||
@@ -183,6 +183,13 @@
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
margin-inline-end: 1.5vw;
|
||||
/* #6444: isolate each title's bidirectional context. The title is a
|
||||
dir="auto" .viewer; under a global dir="rtl" document, non-isolated
|
||||
dir="auto" runs share bidi state, so typing a strong RTL (e.g. Arabic)
|
||||
character into another list's dir="auto" composer re-resolved the shared
|
||||
context and visibly reflowed OTHER lists' minicard titles (no data change,
|
||||
reverts on refresh). Isolation makes each title a self-contained run. */
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
@@ -448,6 +455,10 @@
|
||||
min-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
/* #6444: isolate the composer's dir="auto" bidi run so typing a strong RTL
|
||||
character here cannot perturb the rendering of the surrounding minicard
|
||||
titles under a dir="rtl" document. */
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
.parent-prefix {
|
||||
color: #b3b3b3;
|
||||
|
||||
@@ -3,7 +3,8 @@ template(name="minicard")
|
||||
class="{{#if isLinkedCard}}linked-card{{/if}}"
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="{{agingClass}}"
|
||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}"
|
||||
style="{{colorStyle}}")
|
||||
if canMoveCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
.handle
|
||||
@@ -93,11 +94,11 @@ template(name="minicard")
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
unless hiddenMinicardLabelText
|
||||
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
|
||||
span.js-card-label.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if hiddenMinicardLabelText
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
.minicard-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title="{{name}}")
|
||||
|
||||
.minicard-custom-fields
|
||||
each customFieldsWD
|
||||
|
||||
@@ -6,6 +6,11 @@ import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import ChecklistItems from '/models/checklistItems';
|
||||
import Cards from '/models/cards';
|
||||
import { resolveCoverId } from '/models/lib/linkedCardCover';
|
||||
import {
|
||||
parseChecklistItemTitles,
|
||||
buildChecklistItemPayload,
|
||||
} from '/models/lib/checklistItemTitles';
|
||||
import { normalizeDependencies } from '/models/metadata/dependencies';
|
||||
|
||||
function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
|
||||
@@ -164,8 +169,12 @@ Template.minicard.helpers({
|
||||
}
|
||||
},
|
||||
cover() {
|
||||
if (!this.coverId) return null;
|
||||
const attachment = ReactiveCache.getAttachment(this.coverId);
|
||||
// #5666: for a linked card the cover lives on the real card it points at, so
|
||||
// resolve the cover id through it (mirroring getTitle/getDue/...); a plain
|
||||
// card resolves to its own coverId.
|
||||
const coverId = resolveCoverId(this, id => ReactiveCache.getCard(id));
|
||||
if (!coverId) return null;
|
||||
const attachment = ReactiveCache.getAttachment(coverId);
|
||||
if (!attachment) return null;
|
||||
const coverLink = typeof attachment.link === 'function' ? attachment.link() : '';
|
||||
if (!coverLink) return null;
|
||||
@@ -376,6 +385,28 @@ Template.minicardChecklist.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
// #6440: create the checklist item(s) for the minicard add-item form. Shared by
|
||||
// the Save-button click handler and the (rarely reached) form-submit handler.
|
||||
// The Save button needs an explicit click handler because the minicard — and so
|
||||
// the add-item <form> — is rendered inside the `a.minicard-wrapper` anchor, where
|
||||
// the native form `submit` event never reaches the Blaze event map; without this
|
||||
// the "+" add-item on the minicard did nothing. The blank-input guard and the
|
||||
// title parsing live in the pure, unit-tested models/lib/checklistItemTitles.js.
|
||||
function addMinicardChecklistItems(textarea, checklist) {
|
||||
if (!textarea || !checklist) return;
|
||||
const titles = parseChecklistItemTitles(textarea.value);
|
||||
if (titles.length === 0) return;
|
||||
const items = checklist.items();
|
||||
const lastItem = items[items.length - 1] || null;
|
||||
let sortIndex = Utils.calculateIndexData(lastItem, null).base;
|
||||
titles.forEach(title => {
|
||||
ChecklistItems.insert(buildChecklistItemPayload(title, checklist, sortIndex));
|
||||
sortIndex += 1;
|
||||
});
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
Template.minicardChecklist.events({
|
||||
'click .js-convert-checklist-item-to-card'(event) {
|
||||
event.stopPropagation();
|
||||
@@ -417,25 +448,26 @@ Template.minicardChecklist.events({
|
||||
}
|
||||
$form.find('.js-close-inlined-form').trigger('click');
|
||||
},
|
||||
// #6440: explicit click handler for the minicard "+" add-item Save button.
|
||||
// The native form `submit` event does not fire on the minicard (the form is
|
||||
// inside the `a.minicard-wrapper` anchor), so — unlike card-detail — we cannot
|
||||
// rely on `submit .js-add-checklist-item`. This mirrors the already-working
|
||||
// `click .js-submit-edit-checklist-item-form` above.
|
||||
'click .js-submit-add-checklist-item-form'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const $form = $(event.currentTarget).closest('form');
|
||||
const textarea = $form.find('textarea.js-add-checklist-item')[0];
|
||||
const formData = Blaze.getData($form[0]) || Template.currentData() || {};
|
||||
addMinicardChecklistItems(textarea, formData.checklist);
|
||||
},
|
||||
// Kept for completeness (e.g. card-detail-style contexts); on the minicard this
|
||||
// rarely fires, so the click handler above is the real path. See #6440.
|
||||
'submit .js-add-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
if (!textarea) return;
|
||||
const title = textarea.value.trim();
|
||||
const data = Template.currentData() || {};
|
||||
const checklist = data.checklist;
|
||||
if (!checklist || !title) return;
|
||||
const items = checklist.items();
|
||||
const lastItem = items[items.length - 1] || null;
|
||||
const sortIndex = Utils.calculateIndexData(lastItem, null).base;
|
||||
ChecklistItems.insert({
|
||||
title,
|
||||
checklistId: checklist._id,
|
||||
cardId: checklist.cardId,
|
||||
sort: sortIndex,
|
||||
});
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
addMinicardChecklistItems(textarea, data.checklist);
|
||||
},
|
||||
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
|
||||
Popup.back();
|
||||
|
||||
@@ -736,9 +736,28 @@ form.inlined-form .list-composer {
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
/* #5439: always show a scrollbar on the list body across desktop and mobile
|
||||
browsers (overlay scrollbars on macOS/iOS/Android/Firefox otherwise auto-
|
||||
hide it). Keep these declarations in sync with models/lib/scrollbarCss.js
|
||||
(alwaysVisibleScrollbarCss), which is unit tested. */
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.35) rgba(0, 0, 0, 0.08);
|
||||
scrollbar-gutter: stable;
|
||||
padding: 5px 11px;
|
||||
}
|
||||
.list-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.list-body::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.list-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.list-body .minicards {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -46,7 +46,7 @@ template(name="addCardForm")
|
||||
if getLabels
|
||||
.minicard-labels
|
||||
each getLabels
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
.minicard-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title="{{name}}")
|
||||
textarea.minicard-composer-textarea.js-card-title(autofocus dir="auto")
|
||||
if members.get
|
||||
.minicard-members.js-minicard-composer-members
|
||||
|
||||
@@ -10,6 +10,9 @@ import { Filter } from '/client/lib/filter';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { isLinkableCardTarget } from '/models/lib/linkedCardTarget';
|
||||
import { listCardsSelector } from '/models/lib/swimlaneFilter';
|
||||
import { sortCardsByTitle } from '/models/lib/sortCardsByTitle';
|
||||
import { isLazyCards, BoardListCardCounts, windowCountId } from '/client/lib/lazyCards';
|
||||
import autosize from 'autosize';
|
||||
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
@@ -339,20 +342,36 @@ Template.listBody.helpers({
|
||||
if (!sortBy) {
|
||||
sortBy = defaultSort;
|
||||
}
|
||||
const selector = {
|
||||
listId: Template.currentData()._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId) {
|
||||
// Fallback: also show cards with no swimlaneId (null/empty) so that
|
||||
// pre-migration / orphaned cards are always visible without a DB migration.
|
||||
selector.$or = [
|
||||
{ swimlaneId },
|
||||
{ swimlaneId: null }, // null covers null AND missing field
|
||||
{ swimlaneId: '' }, // empty string from shared-lists era
|
||||
];
|
||||
// #6441: build the swimlane-membership fallback as a single `swimlaneId:
|
||||
// { $in: [...] }` clause (via the shared, unit-tested helper) instead of a
|
||||
// bare top-level `$or`, so it never competes with the board Filter's own
|
||||
// top-level `$or` (labels/members/exceptions) and the label filter applies
|
||||
// board-wide across every swimlane.
|
||||
// #6443: in the first swimlane also surface orphaned cards (a swimlaneId
|
||||
// pointing at a deleted swimlane) so they are not invisible in swimlane view.
|
||||
const list = Template.currentData();
|
||||
const selector = listCardsSelector(
|
||||
list._id,
|
||||
swimlaneId,
|
||||
list.orphanedCardsSwimlaneIds
|
||||
? list.orphanedCardsSwimlaneIds(swimlaneId)
|
||||
: undefined,
|
||||
);
|
||||
const mongoSelector = Filter.mongoSelector(selector);
|
||||
// Lazy card loading: the board publication ships no cards, so subscribe to
|
||||
// just this list/swimlane window (grows with `limit` as the user scrolls)
|
||||
// plus its reactive total count. Default mode keeps everything in minimongo
|
||||
// already, so these subscriptions are skipped.
|
||||
if (isLazyCards() && list.boardId) {
|
||||
tpl.subscribe('boardCardsWindow', list.boardId, mongoSelector, sortBy, limit);
|
||||
tpl.subscribe(
|
||||
'boardListCardCount',
|
||||
windowCountId(list._id, swimlaneId),
|
||||
list.boardId,
|
||||
mongoSelector,
|
||||
);
|
||||
}
|
||||
const ret = ReactiveCache.getCards(Filter.mongoSelector(selector), {
|
||||
const ret = ReactiveCache.getCards(mongoSelector, {
|
||||
// sort: ['sort'],
|
||||
sort: sortBy,
|
||||
limit,
|
||||
@@ -363,6 +382,13 @@ Template.listBody.helpers({
|
||||
showSpinner(swimlaneId) {
|
||||
const tpl = Template.instance();
|
||||
const list = Template.currentData();
|
||||
if (isLazyCards()) {
|
||||
// In lazy mode minimongo only holds the loaded window, so the total comes
|
||||
// from the server count published into BoardListCardCounts.
|
||||
const countDoc = BoardListCardCounts.findOne(windowCountId(list._id, swimlaneId));
|
||||
const total = countDoc ? countDoc.count : 0;
|
||||
return total > tpl.cardlimit.get();
|
||||
}
|
||||
return list.cards(swimlaneId).length > tpl.cardlimit.get();
|
||||
},
|
||||
|
||||
@@ -717,7 +743,9 @@ Template.linkCardPopup.helpers({
|
||||
if (tpl.selectedListId.get()) selector.listId = tpl.selectedListId.get();
|
||||
|
||||
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
|
||||
return ret;
|
||||
// #5394: sort the Cards dropdown alphabetically by title
|
||||
// (case-insensitive) so a card can be found on boards with many cards.
|
||||
return sortCardsByTitle(ret);
|
||||
},
|
||||
|
||||
isTitleDefault(title) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
template(name="listHeader")
|
||||
.list-header.js-list-header.nodragscroll(
|
||||
class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
|
||||
class=colorClass)
|
||||
class=colorClass
|
||||
style=colorStyle)
|
||||
//- #459: keyboard/screen-reader list reordering (visually hidden, focusable).
|
||||
.list-move-buttons.sr-only
|
||||
a.js-list-move-left(href="#" role="button" aria-label="{{_ 'move-list-left'}}") {{_ 'move-list-left'}}
|
||||
@@ -15,7 +16,7 @@ template(name="listHeader")
|
||||
i.fa.fa-caret-left
|
||||
else
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
if showCardsCountForList cardsCountWhole
|
||||
br
|
||||
span.cardCount {{cardsCount}}
|
||||
if isMiniScreen
|
||||
@@ -26,10 +27,10 @@ template(name="listHeader")
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cardsCountWhole}}
|
||||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if showCardsCountForList cardsCountWhole
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cardsCountWhole}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
@@ -47,11 +48,11 @@ template(name="listHeader")
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cardsCountWhole}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if showCardsCountForList cardsCountWhole
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cardsCountWhole}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
@@ -279,6 +280,10 @@ template(name="setListColorPopup")
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
// #5514: native color wheel for picking a custom RGB / hex color
|
||||
.color-wheel-wrap
|
||||
label(for="listColorWheel") {{_ 'custom-color'}}
|
||||
input.js-list-color-wheel#listColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import dragscroll from '@wekanteam/dragscroll';
|
||||
import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import Cards from '/models/cards';
|
||||
import { LIST_COLORS } from '/models/metadata/colors';
|
||||
import { isHexColor, toHex } from '/models/lib/contrastColor';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { lazyListCardCount } from '/client/lib/lazyCards';
|
||||
|
||||
let listsColors;
|
||||
Meteor.startup(() => {
|
||||
@@ -59,24 +61,33 @@ Template.listHeader.helpers({
|
||||
swimlaneId = list.swimlaneId || '';
|
||||
}
|
||||
|
||||
const ret = list.cards(swimlaneId).length;
|
||||
return ret;
|
||||
// In lazy mode minimongo holds only the loaded window, so use the accurate
|
||||
// server count for the same selector; fall back to the loaded length.
|
||||
const lazyCount = lazyListCardCount(list, swimlaneId);
|
||||
return lazyCount !== null ? lazyCount : list.cards(swimlaneId).length;
|
||||
},
|
||||
|
||||
reachedWipLimit() {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') <= list.cards().length
|
||||
);
|
||||
const lazyCount = lazyListCardCount(list, undefined);
|
||||
const count = lazyCount !== null ? lazyCount : list.cards().length;
|
||||
return list.getWipLimit('enabled') && list.getWipLimit('value') <= count;
|
||||
},
|
||||
|
||||
exceededWipLimit() {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
);
|
||||
const lazyCount = lazyListCardCount(list, undefined);
|
||||
const count = lazyCount !== null ? lazyCount : list.cards().length;
|
||||
return list.getWipLimit('enabled') && list.getWipLimit('value') < count;
|
||||
},
|
||||
|
||||
// Accurate whole-list card count (used by the badge visibility, WIP-limit
|
||||
// number and pluralization in the template). In lazy mode `cards.length`
|
||||
// (= list.cards().length) is only the loaded window, so prefer the server count.
|
||||
cardsCountWhole() {
|
||||
const list = Template.currentData();
|
||||
const lazyCount = lazyListCardCount(list, undefined);
|
||||
return lazyCount !== null ? lazyCount : list.cards().length;
|
||||
},
|
||||
|
||||
showCardsCountForList(count) {
|
||||
@@ -141,10 +152,9 @@ Template.listHeader.helpers({
|
||||
Template.listHeader.onCreated(function () {
|
||||
this.reachedWipLimit = function () {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') <= list.cards().length
|
||||
);
|
||||
const lazyCount = lazyListCardCount(list, undefined);
|
||||
const count = lazyCount !== null ? lazyCount : list.cards().length;
|
||||
return list.getWipLimit('enabled') && list.getWipLimit('value') <= count;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -305,11 +315,10 @@ Template.setWipLimitPopup.events({
|
||||
async 'click .js-enable-wip-limit'() {
|
||||
const list = Template.currentData();
|
||||
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
|
||||
if (
|
||||
!list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
await list.setWipLimit(list.cards().length);
|
||||
const lazyCount = lazyListCardCount(list, undefined);
|
||||
const count = lazyCount !== null ? lazyCount : list.cards().length;
|
||||
if (!list.getWipLimit('enabled') && list.getWipLimit('value') < count) {
|
||||
await list.setWipLimit(count);
|
||||
}
|
||||
Meteor.call('enableWipLimit', list._id);
|
||||
},
|
||||
@@ -476,6 +485,10 @@ Template.setListColorPopup.helpers({
|
||||
return tpl.currentColor.get() === color;
|
||||
}
|
||||
},
|
||||
// #5514: current color as a '#rrggbb' hex for the native color wheel <input>.
|
||||
currentColorHex() {
|
||||
return toHex(Template.instance().currentColor.get()) || '#0079bf';
|
||||
},
|
||||
});
|
||||
|
||||
Template.setListColorPopup.events({
|
||||
@@ -483,6 +496,13 @@ Template.setListColorPopup.events({
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
// #5514: picking from the native color wheel stores a custom hex.
|
||||
'input .js-list-color-wheel'(event, tpl) {
|
||||
const value = event.currentTarget.value;
|
||||
if (isHexColor(value)) {
|
||||
tpl.currentColor.set(value);
|
||||
}
|
||||
},
|
||||
async 'submit form'(event, tpl) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
|
||||
@@ -83,14 +83,23 @@ Template.userFormsLayout.onRendered(() => {
|
||||
})();
|
||||
});
|
||||
|
||||
// #4394 (security): a one-shot $('.at-signup-link').hide() does not survive
|
||||
// the useraccounts form re-rendering that happens after a failed login (e.g.
|
||||
// an LDAP "User not Found") — the links were recreated without the inline
|
||||
// display:none and reappeared even though registration / forgot-password
|
||||
// were disabled in the Admin Panel. Toggle a class on the stable <body>
|
||||
// ancestor and hide the links via CSS (userForm.css) so the rule keeps
|
||||
// applying to every freshly re-rendered copy of the links.
|
||||
Meteor.call('isDisableRegistration', (_, result) => {
|
||||
if (result) {
|
||||
document.body.classList.add('registration-disabled');
|
||||
$('.at-signup-link').hide();
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.call('isDisableForgotPassword', (_, result) => {
|
||||
if (result) {
|
||||
document.body.classList.add('forgotpwd-disabled');
|
||||
$('.at-pwd-link').hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ template(name="myCards")
|
||||
td
|
||||
.div
|
||||
each label in card.labelIds
|
||||
span.card-label(class="card-label-{{labelColor board label}}" title=name)
|
||||
span.card-label(class="{{labelColorClass (labelColor board label)}}" style="{{labelColorStyle (labelColor board label)}}" title=name)
|
||||
| {{labelName board label}}
|
||||
td
|
||||
if card.dueAt
|
||||
|
||||
@@ -106,38 +106,19 @@ Template.boardActions.events({
|
||||
const boardId = Session.get('currentBoard');
|
||||
const destBoardId = tpl.find('#board-id').value;
|
||||
const desc = Utils.getTriggerActionDesc(event, tpl);
|
||||
if (actionSelected === 'top') {
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'moveCardToTop',
|
||||
listName,
|
||||
swimlaneName,
|
||||
boardId: destBoardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
}
|
||||
if (actionSelected === 'bottom') {
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'moveCardToBottom',
|
||||
listName,
|
||||
swimlaneName,
|
||||
boardId: destBoardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
}
|
||||
// #5536: create the rule on the server (see server/rulesButton.js →
|
||||
// rules.createRule) instead of three optimistic client Collection.insert()
|
||||
// calls. The optimistic writes landed in minimongo limbo once the wizard
|
||||
// subscription stopped, so the cross-board move action rendered BLANK; and
|
||||
// the client allow/deny rules rejected them outright for non-owner members.
|
||||
// The action keeps its own destination `boardId` (the board to move to).
|
||||
Meteor.call('rules.createRule', boardId, ruleName, trigger, {
|
||||
actionType: actionSelected === 'bottom' ? 'moveCardToBottom' : 'moveCardToTop',
|
||||
listName,
|
||||
swimlaneName,
|
||||
boardId: destBoardId,
|
||||
desc,
|
||||
});
|
||||
},
|
||||
'click .js-add-gen-move-action'(event, tpl) {
|
||||
const desc = Utils.getTriggerActionDesc(event, tpl);
|
||||
@@ -203,20 +184,16 @@ Template.boardActions.events({
|
||||
const boardId = Session.get('currentBoard');
|
||||
const destBoardId = tpl.find('#board-id-link').value;
|
||||
const desc = Utils.getTriggerActionDesc(event, tpl);
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
// #5536: create on the server so the cross-board link action does not land
|
||||
// blank in minimongo limbo / get rejected by client allow/deny. The action
|
||||
// keeps its own destination `boardId` (the board to link the card into).
|
||||
Meteor.call('rules.createRule', boardId, ruleName, trigger, {
|
||||
actionType: 'linkCard',
|
||||
listName,
|
||||
swimlaneName,
|
||||
boardId: destBoardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
},
|
||||
'click .js-sort-list-action'(event, tpl) {
|
||||
const data = Template.currentData();
|
||||
|
||||
@@ -100,8 +100,15 @@ template(name="cardActions")
|
||||
option(value="receivedAt") {{_'r-df-received-at'}}
|
||||
div.trigger-dropdown
|
||||
input(id="reldate-days",type="number",value="0")
|
||||
div.trigger-dropdown
|
||||
select(id="reldate-unit")
|
||||
option(value="minutes") {{_'r-unit-minutes'}}
|
||||
option(value="hours") {{_'r-unit-hours'}}
|
||||
option(value="days",selected="selected") {{_'r-unit-days'}}
|
||||
option(value="weeks") {{_'r-unit-weeks'}}
|
||||
option(value="months") {{_'r-unit-months'}}
|
||||
div.trigger-text
|
||||
| {{_'r-days-after'}}
|
||||
| {{_'r-later'}}
|
||||
div.trigger-button.js-set-reldate-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
||||
@@ -238,12 +238,16 @@ Template.cardActions.events({
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, tpl);
|
||||
const dateField = tpl.find('#reldate-datefield').value;
|
||||
// `days` keeps its name for backward compatibility: it is the numeric
|
||||
// amount, interpreted together with `unit` (minutes/hours/days/weeks/months).
|
||||
const days = parseInt(tpl.find('#reldate-days').value, 10) || 0;
|
||||
const unit = tpl.find('#reldate-unit').value;
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'setDateRelative',
|
||||
dateField,
|
||||
days,
|
||||
unit,
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
|
||||
function boardRuleIds() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
@@ -62,9 +60,10 @@ Template.rulesList.events({
|
||||
getSelected().forEach(ruleId => {
|
||||
const rule = ReactiveCache.getRule(ruleId);
|
||||
if (rule) {
|
||||
Rules.remove(rule._id);
|
||||
Actions.remove(rule.actionId);
|
||||
Triggers.remove(rule.triggerId);
|
||||
// Delete the rule + its trigger + action server-side (see
|
||||
// server/rulesButton.js `rules.deleteRule`): three client-side
|
||||
// Collection.remove() calls were rejected with 403 "Access denied".
|
||||
Meteor.call('rules.deleteRule', rule._id);
|
||||
}
|
||||
});
|
||||
setSelected([]);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
|
||||
Template.rulesMain.onCreated(function () {
|
||||
this.rulesCurrentTab = new ReactiveVar('rulesList');
|
||||
@@ -83,9 +80,12 @@ function sanitizeObject(obj) {
|
||||
Template.rulesMain.events({
|
||||
'click .js-delete-rule'() {
|
||||
const rule = Template.currentData();
|
||||
Rules.remove(rule._id);
|
||||
Actions.remove(rule.actionId);
|
||||
Triggers.remove(rule.triggerId);
|
||||
// Delete the rule + its trigger + action in one server call. Doing this
|
||||
// client-side as three separate Collection.remove() calls failed with 403
|
||||
// "Access denied" whenever a trigger/action document's boardId did not
|
||||
// resolve to a board in the allow() rule; the method authorizes once and
|
||||
// removes all three server-side.
|
||||
Meteor.call('rules.deleteRule', rule._id);
|
||||
},
|
||||
'click .js-goto-trigger'(event, tpl) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -12,7 +12,7 @@ template(name="rulesWorkflow")
|
||||
.workflow-node.workflow-node-when
|
||||
span.workflow-node-label {{_ 'r-when'}}
|
||||
span.workflow-node-text= builderTriggerLabel
|
||||
a.workflow-slot-clear.js-clear-when(title="{{_ 'remove'}}") ✕
|
||||
a.workflow-slot-clear.js-clear-when(title="{{_ 'r-remove'}}") ✕
|
||||
else
|
||||
span.workflow-slot-placeholder {{_ 'r-when'}}: {{_ 'r-drop-trigger'}}
|
||||
.workflow-arrow
|
||||
@@ -22,7 +22,7 @@ template(name="rulesWorkflow")
|
||||
.workflow-node.workflow-node-then
|
||||
span.workflow-node-label {{_ 'r-action'}}
|
||||
span.workflow-node-text= builderActionLabel
|
||||
a.workflow-slot-clear.js-clear-then(title="{{_ 'remove'}}") ✕
|
||||
a.workflow-slot-clear.js-clear-then(title="{{_ 'r-remove'}}") ✕
|
||||
else
|
||||
span.workflow-slot-placeholder {{_ 'r-action'}}: {{_ 'r-drop-action'}}
|
||||
.workflow-builder-controls
|
||||
|
||||
@@ -12,31 +12,44 @@ import Triggers from '/models/triggers';
|
||||
// can be built entirely by dragging. Parameterized rules (specific list, label,
|
||||
// member, schedule time, …) are still created with the form builder on the
|
||||
// List view.
|
||||
// Each palette entry carries an i18n `labelKey` (translated at render time via
|
||||
// TAPi18n.__) instead of a hardcoded English string, so the whole workflow view
|
||||
// follows the UI language. `labelParams` is passed to TAPi18n.__ for labels that
|
||||
// need a placeholder (e.g. the scheduled time). Existing rule keys are reused
|
||||
// where they fit; workflow-only labels use new `r-w-*` keys (see en.i18n.json).
|
||||
const TRIGGER_PALETTE = [
|
||||
{ label: 'Card is created', doc: { activityType: 'createCard', listName: '*', swimlaneName: '*', cardTitle: '*', userId: '*' } },
|
||||
{ label: 'Card is moved', doc: { activityType: 'moveCard', listName: '*', oldListName: '*', swimlaneName: '*', cardTitle: '*', userId: '*' } },
|
||||
{ label: 'Card is archived', doc: { activityType: 'archivedCard', userId: '*' } },
|
||||
{ label: 'Card is unarchived', doc: { activityType: 'restoredCard', userId: '*' } },
|
||||
{ label: 'Any label is added', doc: { activityType: 'addedLabel', labelId: '*', userId: '*' } },
|
||||
{ label: 'Any label is removed', doc: { activityType: 'removedLabel', labelId: '*', userId: '*' } },
|
||||
{ label: 'A member is added', doc: { activityType: 'joinMember', username: '*', userId: '*' } },
|
||||
{ label: 'A member is removed', doc: { activityType: 'unjoinMember', username: '*', userId: '*' } },
|
||||
{ label: 'A checklist is added', doc: { activityType: 'addChecklist', checklistName: '*', userId: '*' } },
|
||||
{ label: 'An attachment is added', doc: { activityType: 'addAttachment', userId: '*' } },
|
||||
{ label: 'Every day at 09:00', doc: { activityType: 'scheduledTrigger', scheduleKind: 'calendar', scheduleType: 'daily', atTime: '09:00', listName: '*', swimlaneName: '*' } },
|
||||
{ labelKey: 'r-w-card-created', doc: { activityType: 'createCard', listName: '*', swimlaneName: '*', cardTitle: '*', userId: '*' } },
|
||||
{ labelKey: 'r-when-a-card-is-moved', doc: { activityType: 'moveCard', listName: '*', oldListName: '*', swimlaneName: '*', cardTitle: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-card-archived', doc: { activityType: 'archivedCard', userId: '*' } },
|
||||
{ labelKey: 'r-w-card-unarchived', doc: { activityType: 'restoredCard', userId: '*' } },
|
||||
{ labelKey: 'r-w-label-added', doc: { activityType: 'addedLabel', labelId: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-label-removed', doc: { activityType: 'removedLabel', labelId: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-member-added', doc: { activityType: 'joinMember', username: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-member-removed', doc: { activityType: 'unjoinMember', username: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-checklist-added', doc: { activityType: 'addChecklist', checklistName: '*', userId: '*' } },
|
||||
{ labelKey: 'r-w-attachment-added', doc: { activityType: 'addAttachment', userId: '*' } },
|
||||
{ labelKey: 'r-w-every-day-at', labelParams: { time: '09:00' }, doc: { activityType: 'scheduledTrigger', scheduleKind: 'calendar', scheduleType: 'daily', atTime: '09:00', listName: '*', swimlaneName: '*' } },
|
||||
];
|
||||
|
||||
const ACTION_PALETTE = [
|
||||
{ label: 'Move card to top', doc: { actionType: 'moveCardToTop', listName: '*', swimlaneName: '*' } },
|
||||
{ label: 'Move card to bottom', doc: { actionType: 'moveCardToBottom', listName: '*', swimlaneName: '*' } },
|
||||
{ label: 'Archive card', doc: { actionType: 'archive' } },
|
||||
{ label: 'Unarchive card', doc: { actionType: 'unarchive' } },
|
||||
{ label: 'Mark card complete', doc: { actionType: 'markCardComplete' } },
|
||||
{ label: 'Mark card incomplete', doc: { actionType: 'markCardIncomplete' } },
|
||||
{ label: 'Remove all members', doc: { actionType: 'removeMember', username: '*' } },
|
||||
{ label: 'Set received date to now', doc: { actionType: 'setDate', dateField: 'receivedAt' } },
|
||||
{ labelKey: 'r-d-move-to-top-gen', doc: { actionType: 'moveCardToTop', listName: '*', swimlaneName: '*' } },
|
||||
{ labelKey: 'r-d-move-to-bottom-gen', doc: { actionType: 'moveCardToBottom', listName: '*', swimlaneName: '*' } },
|
||||
{ labelKey: 'r-d-archive', doc: { actionType: 'archive' } },
|
||||
{ labelKey: 'r-d-unarchive', doc: { actionType: 'unarchive' } },
|
||||
{ labelKey: 'r-mark-complete', doc: { actionType: 'markCardComplete' } },
|
||||
{ labelKey: 'r-mark-incomplete', doc: { actionType: 'markCardIncomplete' } },
|
||||
{ labelKey: 'r-remove-all', doc: { actionType: 'removeMember', username: '*' } },
|
||||
{ labelKey: 'r-w-set-received-now', doc: { actionType: 'setDate', dateField: 'receivedAt' } },
|
||||
];
|
||||
|
||||
// Translate a palette entry's label for display / storage in the current UI
|
||||
// language (matches the classic Rules view, which also stores already-translated
|
||||
// description text at creation time).
|
||||
function paletteLabel(entry) {
|
||||
if (!entry) return '';
|
||||
return TAPi18n.__(entry.labelKey, entry.labelParams);
|
||||
}
|
||||
|
||||
Template.rulesWorkflow.onCreated(function () {
|
||||
this.builderTrigger = new ReactiveVar(null);
|
||||
this.builderAction = new ReactiveVar(null);
|
||||
@@ -52,24 +65,22 @@ Template.rulesWorkflow.helpers({
|
||||
return Utils.getCurrentBoard();
|
||||
},
|
||||
triggerPalette() {
|
||||
return TRIGGER_PALETTE.map((t, idx) => ({ idx, label: t.label }));
|
||||
return TRIGGER_PALETTE.map((t, idx) => ({ idx, label: paletteLabel(t) }));
|
||||
},
|
||||
actionPalette() {
|
||||
return ACTION_PALETTE.map((a, idx) => ({ idx, label: a.label }));
|
||||
return ACTION_PALETTE.map((a, idx) => ({ idx, label: paletteLabel(a) }));
|
||||
},
|
||||
builderTrigger() {
|
||||
return Template.instance().builderTrigger.get();
|
||||
},
|
||||
builderTriggerLabel() {
|
||||
const t = Template.instance().builderTrigger.get();
|
||||
return t ? t.label : '';
|
||||
return paletteLabel(Template.instance().builderTrigger.get());
|
||||
},
|
||||
builderAction() {
|
||||
return Template.instance().builderAction.get();
|
||||
},
|
||||
builderActionLabel() {
|
||||
const a = Template.instance().builderAction.get();
|
||||
return a ? a.label : '';
|
||||
return paletteLabel(Template.instance().builderAction.get());
|
||||
},
|
||||
createDisabled() {
|
||||
const tpl = Template.instance();
|
||||
@@ -96,9 +107,11 @@ function persistRule(tpl) {
|
||||
if (!t || !a) return;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const titleField = tpl.find('.js-workflow-rule-title');
|
||||
const title = (titleField.value || '').trim() || `${t.label} → ${a.label}`;
|
||||
const triggerId = Triggers.insert({ ...t.doc, boardId, desc: t.label });
|
||||
const actionId = Actions.insert({ ...a.doc, boardId, desc: a.label });
|
||||
const tLabel = paletteLabel(t);
|
||||
const aLabel = paletteLabel(a);
|
||||
const title = (titleField.value || '').trim() || `${tLabel} → ${aLabel}`;
|
||||
const triggerId = Triggers.insert({ ...t.doc, boardId, desc: tLabel });
|
||||
const actionId = Actions.insert({ ...a.doc, boardId, desc: aLabel });
|
||||
Rules.insert({ title, triggerId, actionId, boardId });
|
||||
tpl.builderTrigger.set(null);
|
||||
tpl.builderAction.set(null);
|
||||
@@ -140,7 +153,7 @@ Template.rulesWorkflow.events({
|
||||
const a = ACTION_PALETTE[tpl.dragItem.idx];
|
||||
const boardId = Session.get('currentBoard');
|
||||
const oldActionId = rule.actionId;
|
||||
const newActionId = Actions.insert({ ...a.doc, boardId, desc: a.label });
|
||||
const newActionId = Actions.insert({ ...a.doc, boardId, desc: paletteLabel(a) });
|
||||
Rules.update(ruleId, { $set: { actionId: newActionId } });
|
||||
if (oldActionId) Actions.remove(oldActionId);
|
||||
tpl.dragItem = null;
|
||||
@@ -163,8 +176,9 @@ Template.rulesWorkflow.events({
|
||||
const ruleId = node && node.dataset.ruleId;
|
||||
const rule = ReactiveCache.getRule(ruleId);
|
||||
if (!rule) return;
|
||||
Rules.remove(rule._id);
|
||||
Actions.remove(rule.actionId);
|
||||
Triggers.remove(rule.triggerId);
|
||||
// Delete the rule + its trigger + action server-side (see
|
||||
// server/rulesButton.js `rules.deleteRule`): three client-side
|
||||
// Collection.remove() calls were rejected with 403 "Access denied".
|
||||
Meteor.call('rules.deleteRule', rule._id);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
template(name="adminFeatures")
|
||||
.setting-content
|
||||
unless currentUser.isAdmin
|
||||
| {{_ 'error-notAuthorized'}}
|
||||
else
|
||||
.content-title.ext-box
|
||||
span
|
||||
span.emoji-icon
|
||||
i.fa.fa-flask
|
||||
| {{_ 'features'}}
|
||||
.content-body
|
||||
.side-menu
|
||||
ul
|
||||
li(class="{{#if isPerformanceActive}}active{{/if}}")
|
||||
a.js-features-menu(data-id="performance")
|
||||
span.emoji-icon
|
||||
i.fa.fa-tachometer
|
||||
| {{_ 'features-performance'}}
|
||||
.main-body
|
||||
if isPerformanceActive
|
||||
ul.setting-detail
|
||||
li.layout-form
|
||||
.title {{_ 'cards-loading'}}
|
||||
.form-group
|
||||
select#cards-loading.js-cards-loading
|
||||
each cardsLoadingOptions
|
||||
if this.selected
|
||||
option(value="{{value}}" selected) {{_ label}}
|
||||
else
|
||||
option(value="{{value}}") {{_ label}}
|
||||
p.description {{_ 'cards-loading-description'}}
|
||||
p.description {{_ 'cards-loading-lazy-note'}}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import Settings from '/models/settings';
|
||||
|
||||
// Admin Panel / Features — a home for optional / performance / tier-gated
|
||||
// capabilities. First entry: "Card loading" (all | lazy), which also mirrors the
|
||||
// CARDS_LOADING env var. Grouped by category in the side menu so more feature
|
||||
// toggles (and, later, pricing-tier gating) can be added here.
|
||||
Template.adminFeatures.onCreated(function () {
|
||||
this.activePane = new ReactiveVar('performance');
|
||||
Meteor.subscribe('setting');
|
||||
});
|
||||
|
||||
Template.adminFeatures.helpers({
|
||||
isPerformanceActive() {
|
||||
return Template.instance().activePane.get() === 'performance';
|
||||
},
|
||||
cardsLoadingOptions() {
|
||||
const current = (ReactiveCache.getCurrentSetting() || {}).cardsLoading || 'all';
|
||||
return [
|
||||
{ value: 'all', label: 'cards-loading-all', selected: current === 'all' },
|
||||
{ value: 'lazy', label: 'cards-loading-lazy', selected: current === 'lazy' },
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
Template.adminFeatures.events({
|
||||
'click .js-features-menu'(event, tpl) {
|
||||
tpl.activePane.set(event.currentTarget.dataset.id);
|
||||
},
|
||||
'change .js-cards-loading'(event) {
|
||||
const value = event.currentTarget.value === 'lazy' ? 'lazy' : 'all';
|
||||
const setting = ReactiveCache.getCurrentSetting();
|
||||
if (setting) {
|
||||
Settings.update(setting._id, { $set: { cardsLoading: value } });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -51,6 +51,22 @@ template(name="attachments")
|
||||
span.emoji-icon
|
||||
i.fa.fa-cloud
|
||||
| {{_ 'gcs-storage'}}
|
||||
li(class="{{#if isDatabaseMigrationActive}}active{{/if}}")
|
||||
a.js-attachments-menu(data-id="database-migration")
|
||||
span.emoji-icon
|
||||
i.fa.fa-exchange
|
||||
| {{_ 'database-migration'}}
|
||||
li(class="{{#if isBackupActive}}active{{/if}}")
|
||||
a.js-attachments-menu(data-id="backup")
|
||||
span.emoji-icon
|
||||
i.fa.fa-archive
|
||||
| {{_ 'backup'}}
|
||||
if isSandstorm
|
||||
li(class="{{#if isSandstormActive}}active{{/if}}")
|
||||
a.js-attachments-menu(data-id="sandstorm")
|
||||
span.emoji-icon
|
||||
i.fa.fa-hdd-o
|
||||
| Sandstorm
|
||||
|
||||
.main-body
|
||||
if loading.get
|
||||
@@ -475,6 +491,144 @@ template(name="attachments")
|
||||
if cloudTestError.gcs
|
||||
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.gcs}}
|
||||
|
||||
else if isDatabaseMigrationActive
|
||||
ul#attachment-database-migration-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'database-migration'}}
|
||||
.description {{_ 'database-migration-description'}}
|
||||
.form-group
|
||||
button.js-migrate-to-ferretdb.btn.btn-primary(type="button" disabled=migrateRunning) {{_ 'database-migrate-to-ferretdb'}}
|
||||
button.js-migrate-to-mongodb.btn.btn-primary(type="button" disabled=migrateRunning) {{_ 'database-migrate-to-mongodb'}}
|
||||
if migrateStatus
|
||||
.move-progress-section
|
||||
.move-progress-counter
|
||||
| {{_ 'database-migration-phase'}}: {{migrateStatus.phase}}
|
||||
if migrateStatus.collection
|
||||
| — {{migrateStatus.collection}} ({{migrateStatus.collDone}}/{{migrateStatus.collTotal}}), {{migrateStatus.collectionsDone}}/{{migrateStatus.collectionsTotal}}
|
||||
if migrateStatus.error
|
||||
p.form-text.text-danger {{migrateStatus.error}}
|
||||
if migrateSuccess
|
||||
p.form-text.text-success {{_ 'database-migration-done'}}
|
||||
|
||||
else if isBackupActive
|
||||
ul#attachment-backup-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'backup'}}
|
||||
.description {{_ 'backup-description'}}
|
||||
.form-group
|
||||
label
|
||||
input.js-backup-attachments(type="checkbox" checked="checked")
|
||||
| {{_ 'attachments'}}
|
||||
label
|
||||
input.js-backup-avatars(type="checkbox" checked="checked")
|
||||
| {{_ 'avatars'}}
|
||||
label
|
||||
input.js-backup-data(type="checkbox" checked="checked")
|
||||
| {{_ 'backup-data'}}
|
||||
.form-group
|
||||
label {{_ 'backup-storage'}}
|
||||
select.wekan-form-control.js-backup-storage
|
||||
option(value="filesystem") {{_ 'filesystem-storage'}}
|
||||
option(value="s3") {{_ 's3-minio-storage'}}
|
||||
option(value="azure") {{_ 'azure-blob-storage'}}
|
||||
option(value="gcs") {{_ 'gcs-storage'}}
|
||||
.form-group
|
||||
button.js-run-backup.btn.btn-primary(type="button" disabled=backupRunning) {{_ 'backup-now'}}
|
||||
if backupStatus
|
||||
p.form-text {{_ 'database-migration-phase'}}: {{backupStatus.phase}} {{backupStatus.detail}}
|
||||
if backupStatus.error
|
||||
p.form-text.text-danger {{backupStatus.error}}
|
||||
if backupSuccess
|
||||
p.form-text.text-success {{_ 'backup-done'}}: {{backupStatus.file}}
|
||||
|
||||
h4 {{_ 'backup-schedule'}}
|
||||
.form-group
|
||||
label {{_ 'backup-frequency'}}
|
||||
select.wekan-form-control.js-backup-frequency
|
||||
option(value="off" selected=scheduleOff) {{_ 'backup-frequency-off'}}
|
||||
option(value="daily" selected=scheduleDaily) {{_ 'backup-frequency-daily'}}
|
||||
option(value="weekly" selected=scheduleWeekly) {{_ 'backup-frequency-weekly'}}
|
||||
option(value="monthly" selected=scheduleMonthly) {{_ 'backup-frequency-monthly'}}
|
||||
.form-group
|
||||
label {{_ 'backup-time'}}
|
||||
input.wekan-form-control.js-backup-time(type="time" value=scheduleTime)
|
||||
.form-group
|
||||
label {{_ 'backup-day-of-week'}}
|
||||
input.wekan-form-control.js-backup-dow(type="text" value=scheduleDow placeholder="Sunday")
|
||||
.form-group
|
||||
label {{_ 'backup-day-of-month'}}
|
||||
input.wekan-form-control.js-backup-dom(type="number" min="1" max="28" value=scheduleDom)
|
||||
.form-group
|
||||
button.js-save-backup-schedule.btn.btn-primary(type="button") {{_ 'save'}}
|
||||
|
||||
h4 {{_ 'backup-restore'}}
|
||||
.form-group
|
||||
button.js-list-backups.btn.btn-secondary(type="button") {{_ 'backup-list'}}
|
||||
if backupList
|
||||
table.gridfs-stats-table
|
||||
tr
|
||||
th
|
||||
th {{_ 'backup-storage'}}
|
||||
th {{_ 'backup-datetime'}}
|
||||
th {{_ 'backup-path'}}
|
||||
each backupList
|
||||
tr
|
||||
td
|
||||
input.js-backup-select(type="radio" name="backup-select" value=path)
|
||||
td {{storage}}
|
||||
td {{datetime}}
|
||||
td {{path}}
|
||||
.form-group
|
||||
label {{_ 'backup-restore-mode'}}
|
||||
select.wekan-form-control.js-restore-mode
|
||||
option(value="add-missing") {{_ 'backup-restore-add-missing'}}
|
||||
option(value="replace-all") {{_ 'backup-restore-replace-all'}}
|
||||
.form-group
|
||||
button.js-restore-backup.btn.btn-warning(type="button" disabled=backupRunning) {{_ 'backup-restore'}}
|
||||
|
||||
else if isSandstormActive
|
||||
ul#attachment-sandstorm-setting.setting-detail
|
||||
li
|
||||
h3 Sandstorm
|
||||
.description {{_ 'sandstorm-migration-description'}}
|
||||
if sandstormStatus
|
||||
.form-group
|
||||
strong {{_ 'sandstorm-migration-status'}}
|
||||
if sandstormStatus.migrationSuccess
|
||||
span.text-success {{_ 'sandstorm-migration-success'}}
|
||||
else if sandstormStatus.migrationDone
|
||||
span.text-danger {{_ 'sandstorm-migration-failed'}}
|
||||
else
|
||||
span.text-muted {{_ 'sandstorm-migration-pending'}}
|
||||
if sandstormStatus.status
|
||||
p.form-text.text-muted {{_ 'database-migration-phase'}}: {{sandstormStatus.status.phase}}
|
||||
table.gridfs-stats-table
|
||||
tr
|
||||
th {{_ 'sandstorm-storage-item'}}
|
||||
th {{_ 'sandstorm-disk-usage'}}
|
||||
tr
|
||||
td {{_ 'sandstorm-raw-mongodb'}}
|
||||
td {{rawMongoSize}}
|
||||
tr
|
||||
td FerretDB (SQLite)
|
||||
td {{ferretSize}}
|
||||
tr
|
||||
td {{_ 'attachments'}}
|
||||
td {{attachmentsSize}}
|
||||
tr
|
||||
td {{_ 'avatars'}}
|
||||
td {{avatarsSize}}
|
||||
if sandstormStatus.rawMongoExists
|
||||
.form-group
|
||||
.description {{_ 'sandstorm-delete-raw-mongodb-description'}}
|
||||
button.js-sandstorm-delete-raw-mongodb.btn.btn-warning(type="button" disabled=sandstormDeleteDisabled) {{_ 'sandstorm-delete-raw-mongodb'}}
|
||||
if sandstormDeleteResult
|
||||
p.form-text.text-success {{_ 'sandstorm-raw-mongodb-deleted'}} ({{sandstormDeleteFreed}})
|
||||
if sandstormDeleteError
|
||||
p.form-text.text-danger {{sandstormDeleteError}}
|
||||
else
|
||||
p.form-text.text-muted {{_ 'loading'}}
|
||||
|
||||
template(name="moveAttachments")
|
||||
.move-storage-form
|
||||
.move-storage-selects
|
||||
|
||||
@@ -267,6 +267,28 @@ Template.attachments.onCreated(function () {
|
||||
this.cloudTestResults = new ReactiveVar({});
|
||||
this.cloudTestErrors = new ReactiveVar({});
|
||||
this.loading = new ReactiveVar(false);
|
||||
this.migrateStatus = new ReactiveVar(null);
|
||||
this.migratePoll = null;
|
||||
this.backupStatus = new ReactiveVar(null);
|
||||
this.backupList = new ReactiveVar(null);
|
||||
this.backupSchedule = new ReactiveVar(null);
|
||||
this.selectedBackup = new ReactiveVar('');
|
||||
this.backupPoll = null;
|
||||
Meteor.call('getBackupSchedule', (err, s) => { if (!err && s) this.backupSchedule.set(s); });
|
||||
|
||||
// Sandstorm: MongoDB 3 -> FerretDB migration status + raw-MongoDB disk usage.
|
||||
this.sandstormStatus = new ReactiveVar(null);
|
||||
this.sandstormDeleteResult = new ReactiveVar(null);
|
||||
this.sandstormDeleteError = new ReactiveVar('');
|
||||
// Fetch (once) when the Sandstorm tab is opened. Migration runs in start.js
|
||||
// before the app boots, so the status is static by the time the UI is shown.
|
||||
this.autorun(() => {
|
||||
if (this.activeSection.get() === 'sandstorm') {
|
||||
Meteor.call('sandstormMigrationStatus', (err, res) => {
|
||||
if (!err && res) this.sandstormStatus.set(res);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.autorun(() => {
|
||||
const ready = this.storageSettingsSubscription.ready();
|
||||
@@ -279,6 +301,54 @@ Template.attachments.onCreated(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Template.attachments.onDestroyed(function () {
|
||||
if (this.migratePoll) {
|
||||
Meteor.clearTimeout(this.migratePoll);
|
||||
this.migratePoll = null;
|
||||
}
|
||||
if (this.backupPoll) {
|
||||
Meteor.clearTimeout(this.backupPoll);
|
||||
this.backupPoll = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Text-data database migration (MongoDB <-> FerretDB v1 SQLite) helpers.
|
||||
function pollMigrateStatus(tpl) {
|
||||
Meteor.call('migrateTextDatabaseStatus', (err, status) => {
|
||||
if (err || !status) return;
|
||||
tpl.migrateStatus.set(status);
|
||||
if (status.running) {
|
||||
tpl.migratePoll = Meteor.setTimeout(() => pollMigrateStatus(tpl), 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startDbMigration(tpl, direction) {
|
||||
const which = direction === 'toFerretDB' ? 'FerretDB v1 (SQLite)' : 'MongoDB';
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(TAPi18n.__('database-migration-confirm', { db: which }))) {
|
||||
return;
|
||||
}
|
||||
tpl.migrateStatus.set({ running: true, phase: 'starting', direction });
|
||||
Meteor.call('migrateTextDatabase', direction, error => {
|
||||
if (error) {
|
||||
tpl.migrateStatus.set({ phase: 'error', error: error.reason || error.message, success: false });
|
||||
} else {
|
||||
pollMigrateStatus(tpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pollBackupStatus(tpl) {
|
||||
Meteor.call('backupStatus', (err, status) => {
|
||||
if (err || !status) return;
|
||||
tpl.backupStatus.set(status);
|
||||
if (status.running) {
|
||||
tpl.backupPoll = Meteor.setTimeout(() => pollBackupStatus(tpl), 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Template.attachments.helpers({
|
||||
loading() {
|
||||
return Template.instance().loading;
|
||||
@@ -298,6 +368,89 @@ Template.attachments.helpers({
|
||||
isGcsActive() {
|
||||
return Template.instance().activeSection.get() === 'gcs';
|
||||
},
|
||||
isDatabaseMigrationActive() {
|
||||
return Template.instance().activeSection.get() === 'database-migration';
|
||||
},
|
||||
migrateStatus() {
|
||||
return Template.instance().migrateStatus.get();
|
||||
},
|
||||
migrateRunning() {
|
||||
const s = Template.instance().migrateStatus.get();
|
||||
return !!(s && s.running);
|
||||
},
|
||||
migrateSuccess() {
|
||||
const s = Template.instance().migrateStatus.get();
|
||||
return !!(s && s.success === true);
|
||||
},
|
||||
isBackupActive() {
|
||||
return Template.instance().activeSection.get() === 'backup';
|
||||
},
|
||||
isSandstorm() {
|
||||
return !!(
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm
|
||||
);
|
||||
},
|
||||
isSandstormActive() {
|
||||
return Template.instance().activeSection.get() === 'sandstorm';
|
||||
},
|
||||
sandstormStatus() {
|
||||
return Template.instance().sandstormStatus.get();
|
||||
},
|
||||
rawMongoSize() {
|
||||
const s = Template.instance().sandstormStatus.get();
|
||||
return s ? filesize(s.rawMongoBytes || 0) : '';
|
||||
},
|
||||
ferretSize() {
|
||||
const s = Template.instance().sandstormStatus.get();
|
||||
return s ? filesize(s.ferretBytes || 0) : '';
|
||||
},
|
||||
attachmentsSize() {
|
||||
const s = Template.instance().sandstormStatus.get();
|
||||
return s ? filesize(s.attachmentsBytes || 0) : '';
|
||||
},
|
||||
avatarsSize() {
|
||||
const s = Template.instance().sandstormStatus.get();
|
||||
return s ? filesize(s.avatarsBytes || 0) : '';
|
||||
},
|
||||
sandstormDeleteDisabled() {
|
||||
const s = Template.instance().sandstormStatus.get();
|
||||
// Only allow deleting the raw MongoDB files after a confirmed-successful
|
||||
// migration, and only while they still exist.
|
||||
return !(s && s.migrationSuccess && s.rawMongoExists);
|
||||
},
|
||||
sandstormDeleteResult() {
|
||||
return Template.instance().sandstormDeleteResult.get();
|
||||
},
|
||||
sandstormDeleteFreed() {
|
||||
const r = Template.instance().sandstormDeleteResult.get();
|
||||
return r ? filesize(r.freedBytes || 0) : '';
|
||||
},
|
||||
sandstormDeleteError() {
|
||||
return Template.instance().sandstormDeleteError.get();
|
||||
},
|
||||
backupStatus() {
|
||||
return Template.instance().backupStatus.get();
|
||||
},
|
||||
backupRunning() {
|
||||
const s = Template.instance().backupStatus.get();
|
||||
return !!(s && s.running);
|
||||
},
|
||||
backupSuccess() {
|
||||
const s = Template.instance().backupStatus.get();
|
||||
return !!(s && s.success === true && s.phase === 'completed');
|
||||
},
|
||||
backupList() {
|
||||
return Template.instance().backupList.get();
|
||||
},
|
||||
scheduleOff() { const s = Template.instance().backupSchedule.get(); return !s || !s.frequency || s.frequency === 'off'; },
|
||||
scheduleDaily() { const s = Template.instance().backupSchedule.get(); return !!(s && s.frequency === 'daily'); },
|
||||
scheduleWeekly() { const s = Template.instance().backupSchedule.get(); return !!(s && s.frequency === 'weekly'); },
|
||||
scheduleMonthly() { const s = Template.instance().backupSchedule.get(); return !!(s && s.frequency === 'monthly'); },
|
||||
scheduleTime() { const s = Template.instance().backupSchedule.get(); return (s && s.time) || '04:00'; },
|
||||
scheduleDow() { const s = Template.instance().backupSchedule.get(); return (s && s.dayOfWeek) || 'Sunday'; },
|
||||
scheduleDom() { const s = Template.instance().backupSchedule.get(); return (s && s.dayOfMonth) || 1; },
|
||||
avatarsUploadBlocked() {
|
||||
const settings = Template.instance().attachmentStorageSettings.get();
|
||||
return settings?.limitSettings?.avatarsUploadBlocked === true;
|
||||
@@ -561,6 +714,84 @@ Template.attachments.events({
|
||||
|
||||
tpl.activeSection.set(targetID);
|
||||
},
|
||||
'click .js-sandstorm-delete-raw-mongodb'(event, tpl) {
|
||||
event.preventDefault();
|
||||
if (!window.confirm(TAPi18n.__('sandstorm-delete-raw-mongodb-confirm'))) {
|
||||
return;
|
||||
}
|
||||
tpl.sandstormDeleteError.set('');
|
||||
Meteor.call('sandstormDeleteRawMongo', (error, result) => {
|
||||
if (error) {
|
||||
tpl.sandstormDeleteError.set(error.reason || error.message);
|
||||
return;
|
||||
}
|
||||
tpl.sandstormDeleteResult.set(result);
|
||||
// Refresh disk usage / status after deletion.
|
||||
Meteor.call('sandstormMigrationStatus', (err, res) => {
|
||||
if (!err && res) tpl.sandstormStatus.set(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
'click .js-migrate-to-ferretdb'(event, tpl) {
|
||||
event.preventDefault();
|
||||
startDbMigration(tpl, 'toFerretDB');
|
||||
},
|
||||
'click .js-migrate-to-mongodb'(event, tpl) {
|
||||
event.preventDefault();
|
||||
startDbMigration(tpl, 'toMongoDB');
|
||||
},
|
||||
'click .js-run-backup'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const opts = {
|
||||
attachments: tpl.$('.js-backup-attachments').is(':checked'),
|
||||
avatars: tpl.$('.js-backup-avatars').is(':checked'),
|
||||
data: tpl.$('.js-backup-data').is(':checked'),
|
||||
};
|
||||
const storage = tpl.$('.js-backup-storage').val() || 'filesystem';
|
||||
tpl.backupStatus.set({ running: true, phase: 'starting' });
|
||||
Meteor.call('runBackup', opts, storage, error => {
|
||||
if (error) tpl.backupStatus.set({ phase: 'error', error: error.reason || error.message, success: false });
|
||||
else pollBackupStatus(tpl);
|
||||
});
|
||||
},
|
||||
'click .js-save-backup-schedule'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const schedule = {
|
||||
enabled: (tpl.$('.js-backup-frequency').val() || 'off') !== 'off',
|
||||
frequency: tpl.$('.js-backup-frequency').val() || 'off',
|
||||
time: tpl.$('.js-backup-time').val() || '04:00',
|
||||
dayOfWeek: tpl.$('.js-backup-dow').val() || 'Sunday',
|
||||
dayOfMonth: parseInt(tpl.$('.js-backup-dom').val(), 10) || 1,
|
||||
attachments: tpl.$('.js-backup-attachments').is(':checked'),
|
||||
avatars: tpl.$('.js-backup-avatars').is(':checked'),
|
||||
data: tpl.$('.js-backup-data').is(':checked'),
|
||||
storage: tpl.$('.js-backup-storage').val() || 'filesystem',
|
||||
};
|
||||
Meteor.call('saveBackupSchedule', schedule, (error, saved) => {
|
||||
if (!error && saved) tpl.backupSchedule.set(saved);
|
||||
});
|
||||
},
|
||||
'click .js-list-backups'(event, tpl) {
|
||||
event.preventDefault();
|
||||
Meteor.call('listBackups', (error, list) => {
|
||||
if (!error) tpl.backupList.set(list || []);
|
||||
});
|
||||
},
|
||||
'change .js-backup-select'(event, tpl) {
|
||||
tpl.selectedBackup.set($(event.currentTarget).val());
|
||||
},
|
||||
'click .js-restore-backup'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const zipPath = tpl.selectedBackup.get();
|
||||
if (!zipPath) { window.alert(TAPi18n.__('backup-restore-select-first')); return; }
|
||||
const mode = tpl.$('.js-restore-mode').val() || 'add-missing';
|
||||
if (!window.confirm(TAPi18n.__('backup-restore-confirm'))) return;
|
||||
tpl.backupStatus.set({ running: true, phase: 'restore' });
|
||||
Meteor.call('restoreBackup', zipPath, mode, error => {
|
||||
if (error) tpl.backupStatus.set({ phase: 'error', error: error.reason || error.message, success: false });
|
||||
else pollBackupStatus(tpl);
|
||||
});
|
||||
},
|
||||
'change select.js-attachment-limit-unit'(event, tpl) {
|
||||
const fieldName = event.currentTarget.dataset.field;
|
||||
const selectedUnit = event.currentTarget.value;
|
||||
|
||||
@@ -29,15 +29,31 @@ template(name='statistics')
|
||||
tr
|
||||
th {{_ 'Node_version'}}
|
||||
td {{statistics.process.nodeVersion}}
|
||||
tr
|
||||
th {{_ 'Database_type'}}
|
||||
td {{statistics.mongo.databaseType}}
|
||||
tr
|
||||
th {{_ 'MongoDB_version'}}
|
||||
td {{statistics.mongo.mongoVersion}}
|
||||
tr
|
||||
th {{_ 'Database_commit'}}
|
||||
td {{statistics.mongo.databaseCommit}}
|
||||
if statistics.mongo.ferretdbVersion
|
||||
tr
|
||||
th {{_ 'FerretDB_version'}}
|
||||
td {{statistics.mongo.ferretdbVersion}}
|
||||
tr
|
||||
th {{_ 'FerretDB_commit'}}
|
||||
td {{statistics.mongo.ferretdbCommit}}
|
||||
tr
|
||||
th {{_ 'MongoDB_storage_engine'}}
|
||||
td {{statistics.mongo.mongoStorageEngine}}
|
||||
tr
|
||||
th {{_ 'MongoDB_Oplog_enabled'}}
|
||||
td {{formatBoolean statistics.mongo.mongoOplogEnabled}}
|
||||
tr
|
||||
th {{_ 'Reactivity_mode'}}
|
||||
td {{statistics.mongo.reactivity}}
|
||||
tr
|
||||
th {{_ 'OS_Type'}}
|
||||
td {{statistics.os.type}}
|
||||
|
||||
@@ -378,3 +378,62 @@ table td:first-child {
|
||||
.roles-settings .roles-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Admin Panel > People > Domains table: search + column sort + pagination,
|
||||
matching the Board Table view / other admin tables. */
|
||||
.domain-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.domain-controls input.js-domain-search {
|
||||
flex: 1 1 auto;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 0.4vw;
|
||||
}
|
||||
|
||||
.domain-controls button.js-domain-search-button {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #bbb;
|
||||
background: #fff;
|
||||
border-radius: 0.4vw;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.domain-pagination button {
|
||||
min-width: 32px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #bbb;
|
||||
background: #fff;
|
||||
border-radius: 0.4vw;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-pagination button.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.domain-page-info {
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
th.js-domain-sort {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -122,7 +122,7 @@ template(name="people")
|
||||
else if templatesSetting.get
|
||||
+templatesGeneral
|
||||
else if domainSetting.get
|
||||
+domainGeneral(domainList=domainList)
|
||||
+domainGeneral
|
||||
|
||||
|
||||
template(name="orgGeneral")
|
||||
@@ -239,11 +239,18 @@ template(name="selectAllUser")
|
||||
input.allUserChkBox(type="checkbox", id="chkSelectAll")
|
||||
|
||||
template(name="domainGeneral")
|
||||
.domain-controls
|
||||
input.js-domain-search(type="text" placeholder="{{_ 'search'}}" aria-label="{{_ 'search'}}")
|
||||
button.js-domain-search-button(type="button") {{_ 'search'}}
|
||||
.domain-pagination
|
||||
button.js-domain-prev-page(type="button" class="{{#unless hasPrevPage}}disabled{{/unless}}" aria-label="{{_ 'previous'}}") {{_ 'previous'}}
|
||||
span.domain-page-info {{currentPage}} / {{totalPages}}
|
||||
button.js-domain-next-page(type="button" class="{{#unless hasNextPage}}disabled{{/unless}}" aria-label="{{_ 'next'}}") {{_ 'next'}}
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th {{_ 'domain'}}
|
||||
th {{_ 'domain-user-count'}}
|
||||
th.js-domain-sort(scope="col" data-sort="domain") {{_ 'domain'}} {{domainSortIndicator 'domain'}}
|
||||
th.js-domain-sort(scope="col" data-sort="count") {{_ 'domain-user-count'}} {{domainSortIndicator 'count'}}
|
||||
tbody
|
||||
if domainList.length
|
||||
each domainList
|
||||
|
||||
@@ -14,6 +14,7 @@ import InviteToBoardRolesSettings, {
|
||||
const orgsPerPage = 25;
|
||||
const teamsPerPage = 25;
|
||||
const usersPerPage = 25;
|
||||
const domainsPerPage = 25;
|
||||
let userOrgsTeamsAction = ""; //poosible actions 'addOrg', 'addTeam', 'removeOrg' or 'removeTeam' when adding or modifying a user
|
||||
let selectedUserChkBoxUserIds = [];
|
||||
|
||||
@@ -28,9 +29,9 @@ Template.people.onCreated(function () {
|
||||
this.lockedUsersSetting = new ReactiveVar(false);
|
||||
this.rolesSetting = new ReactiveVar(false);
|
||||
this.templatesSetting = new ReactiveVar(false);
|
||||
// #5850: Admin Panel > People > Domains tab.
|
||||
// #5850: Admin Panel > People > Domains tab. The domains table itself
|
||||
// (domainGeneral) now owns its data via getDomainsWithUserCountsPage.
|
||||
this.domainSetting = new ReactiveVar(false);
|
||||
this.domains = new ReactiveVar([]);
|
||||
this.subscribe('inviteToBoardRolesSettings');
|
||||
this.findOrgsOptions = new ReactiveVar({});
|
||||
this.findTeamsOptions = new ReactiveVar({});
|
||||
@@ -205,12 +206,9 @@ Template.people.onCreated(function () {
|
||||
this.templatesSetting.set('templates-setting' === targetID);
|
||||
this.domainSetting.set('domains-setting' === targetID);
|
||||
|
||||
// #5850: load the domains + per-domain user counts when the tab opens.
|
||||
if ('domains-setting' === targetID) {
|
||||
Meteor.call('getDomainsWithUserCounts', (err, res) => {
|
||||
if (!err) this.domains.set(res || []);
|
||||
});
|
||||
}
|
||||
// The Domains table (domainGeneral) now fetches its own single page from
|
||||
// getDomainsWithUserCountsPage (server-side search / sort / pagination), so
|
||||
// the parent no longer eagerly loads every domain into the browser.
|
||||
|
||||
// When switching to locked users tab, refresh the locked users list
|
||||
if ('locked-users-setting' === targetID) {
|
||||
@@ -292,9 +290,6 @@ Template.people.helpers({
|
||||
domainSetting() {
|
||||
return Template.instance().domainSetting;
|
||||
},
|
||||
domainList() {
|
||||
return Template.instance().domains.get();
|
||||
},
|
||||
orgList() {
|
||||
const tpl = Template.instance();
|
||||
// The 'org' publication already returns only the current page (server-side
|
||||
@@ -1897,3 +1892,96 @@ Template.settingsUserPopup.helpers({
|
||||
return Template.instance().errorMessage.get();
|
||||
},
|
||||
});
|
||||
|
||||
// Admin Panel > People > Domains table. Self-contained (like the Board Table
|
||||
// view): it keeps its own search / sort / page state and fetches only ONE page
|
||||
// from the server method getDomainsWithUserCountsPage, so the whole domain list
|
||||
// is never loaded into the browser.
|
||||
Template.domainGeneral.onCreated(function () {
|
||||
this.searchQuery = new ReactiveVar('');
|
||||
this.sortField = new ReactiveVar('domain'); // domain | count
|
||||
this.sortDirection = new ReactiveVar(1); // 1 ascending, -1 descending
|
||||
this.page = new ReactiveVar(1);
|
||||
this.pageData = new ReactiveVar({ rows: [], total: 0, totalPages: 1 });
|
||||
|
||||
this.autorun(() => {
|
||||
const params = {
|
||||
search: this.searchQuery.get(),
|
||||
sortField: this.sortField.get(),
|
||||
sortDirection: this.sortDirection.get(),
|
||||
page: this.page.get(),
|
||||
perPage: domainsPerPage,
|
||||
};
|
||||
Meteor.call('getDomainsWithUserCountsPage', params, (err, res) => {
|
||||
if (!err && res) {
|
||||
this.pageData.set(res);
|
||||
// Server clamps the page into range; mirror that so the controls agree.
|
||||
if (typeof res.page === 'number' && res.page !== this.page.get()) {
|
||||
this.page.set(res.page);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.domainGeneral.helpers({
|
||||
domainList() {
|
||||
return Template.instance().pageData.get().rows;
|
||||
},
|
||||
currentPage() {
|
||||
return Template.instance().page.get();
|
||||
},
|
||||
totalPages() {
|
||||
return Template.instance().pageData.get().totalPages || 1;
|
||||
},
|
||||
hasPrevPage() {
|
||||
return Template.instance().page.get() > 1;
|
||||
},
|
||||
hasNextPage() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.page.get() < (tpl.pageData.get().totalPages || 1);
|
||||
},
|
||||
// Excel-like sort arrow on the active column, matching the Board Table view.
|
||||
domainSortIndicator(field) {
|
||||
const tpl = Template.instance();
|
||||
if (tpl.sortField.get() !== field) return '';
|
||||
return tpl.sortDirection.get() === 1 ? '▲' : '▼';
|
||||
},
|
||||
});
|
||||
|
||||
Template.domainGeneral.events({
|
||||
'click .js-domain-search-button'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.searchQuery.set(tpl.$('.js-domain-search').val() || '');
|
||||
tpl.page.set(1);
|
||||
},
|
||||
'keydown .js-domain-search'(event, tpl) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
tpl.searchQuery.set(tpl.$('.js-domain-search').val() || '');
|
||||
tpl.page.set(1);
|
||||
}
|
||||
},
|
||||
'click .js-domain-prev-page'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const current = tpl.page.get();
|
||||
if (current > 1) tpl.page.set(current - 1);
|
||||
},
|
||||
'click .js-domain-next-page'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const current = tpl.page.get();
|
||||
if (current < (tpl.pageData.get().totalPages || 1)) tpl.page.set(current + 1);
|
||||
},
|
||||
'click .js-domain-sort'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const field = event.currentTarget.dataset.sort;
|
||||
if (!field) return;
|
||||
if (tpl.sortField.get() === field) {
|
||||
tpl.sortDirection.set(tpl.sortDirection.get() * -1);
|
||||
} else {
|
||||
tpl.sortField.set(field);
|
||||
tpl.sortDirection.set(1);
|
||||
}
|
||||
tpl.page.set(1);
|
||||
},
|
||||
});
|
||||
@@ -14,6 +14,11 @@ template(name="settingHeaderBar")
|
||||
i.fa.fa-users
|
||||
span {{_ 'people'}}
|
||||
|
||||
a.setting-header-btn.informations(class=isFeaturesActive href="{{pathFor 'admin-features'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-flask
|
||||
span {{_ 'features'}}
|
||||
|
||||
a.setting-header-btn.informations(class=isAdminReportsActive href="{{pathFor 'admin-reports'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-file-text-o
|
||||
|
||||
@@ -7,6 +7,9 @@ Template.settingHeaderBar.helpers({
|
||||
isPeopleActive() {
|
||||
return FlowRouter.getRouteName() === 'people' ? 'active' : '';
|
||||
},
|
||||
isFeaturesActive() {
|
||||
return FlowRouter.getRouteName() === 'admin-features' ? 'active' : '';
|
||||
},
|
||||
isAdminReportsActive() {
|
||||
return FlowRouter.getRouteName() === 'admin-reports' ? 'active' : '';
|
||||
},
|
||||
|
||||
@@ -55,9 +55,14 @@ template(name='homeSidebar')
|
||||
i.fa.fa-comment-o
|
||||
| {{_ 'activities'}}
|
||||
|
||||
a.flex.js-toggle-show-activities(title="{{_ 'show-activities'}}")
|
||||
i.fa(class="{{#if showActivities}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
span {{_ 'show-activities'}}
|
||||
if showActivities
|
||||
a.flex.activities-eye-toggle.js-toggle-show-activities(title="{{_ 'hide-activities'}}" aria-label="{{_ 'hide-activities'}}")
|
||||
i.fa.fa-eye.eye-icon
|
||||
span {{_ 'hide-activities'}}
|
||||
else
|
||||
a.flex.activities-eye-toggle.js-toggle-show-activities(title="{{_ 'show-activities'}}" aria-label="{{_ 'show-activities'}}")
|
||||
i.fa.fa-eye-slash.eye-slash-icon
|
||||
span {{_ 'show-activities'}}
|
||||
+activities(mode="board")
|
||||
|
||||
template(name="membersWidget")
|
||||
@@ -80,7 +85,7 @@ template(name="membersWidget")
|
||||
+userAvatar(userId=currentBoard.userId showStatus=true)
|
||||
.clearfix
|
||||
else
|
||||
each currentBoard.activeMembers
|
||||
each currentBoard.membersForSidebar
|
||||
+userAvatar(userId=this.userId showStatus=true)
|
||||
if isSandstorm
|
||||
if currentUser.isBoardMember
|
||||
@@ -823,7 +828,7 @@ template(name="labelsWidget")
|
||||
| {{_ 'labels'}}
|
||||
.board-widget-content
|
||||
each currentBoard.labels
|
||||
a.card-label(class="card-label-{{color}}"
|
||||
a.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}"
|
||||
class="{{#if currentUser.isNotCommentOnly}}{{#if currentUser.isNotWorker}}js-label{{/if}}{{/if}}")
|
||||
span.card-label-name
|
||||
+viewer
|
||||
|
||||
@@ -29,7 +29,7 @@ template(name="filterSidebar")
|
||||
each currentBoard.labels
|
||||
li
|
||||
a.name.js-toggle-label-filter(data-filter-id="{{_id}}")
|
||||
span.card-label.square(class="card-label-{{color}}")
|
||||
span.card-label.square(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
|
||||
span.sidebar-list-item-description
|
||||
if name
|
||||
= name
|
||||
@@ -188,7 +188,7 @@ template(name="multiselectionSidebar")
|
||||
each currentBoard.labels
|
||||
li
|
||||
a.name.js-toggle-label-multiselection
|
||||
span.card-label.square(class="card-label-{{color}}")
|
||||
span.card-label.square(class="{{labelColorClass color}}" style="{{labelColorStyle color}}")
|
||||
span.sidebar-list-item-description
|
||||
if name
|
||||
= name
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
template(name="swimlaneHeader")
|
||||
.swimlane-header-wrap.js-swimlane-header(class=colorClass title="{{_ 'rename'}}")
|
||||
.swimlane-header-wrap.js-swimlane-header(class=colorClass style=colorStyle title="{{_ 'rename'}}")
|
||||
if this.isTemplateContainer
|
||||
+swimlaneFixedHeader(this)
|
||||
else
|
||||
@@ -120,6 +120,10 @@ template(name="setSwimlaneColorPopup")
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
// #5514: native color wheel for picking a custom RGB / hex color
|
||||
.flush-left.color-wheel-wrap
|
||||
label(for="swimlaneColorWheel") {{_ 'custom-color'}}
|
||||
input.js-swimlane-color-wheel#swimlaneColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
// Buttons aligned left too
|
||||
.flush-left
|
||||
button.primary.confirm.js-submit(style="margin-inline-start:0") {{_ 'save'}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { SWIMLANE_COLORS } from '/models/metadata/colors';
|
||||
import { isHexColor, toHex } from '/models/lib/contrastColor';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
const { calculateIndexData } = Utils;
|
||||
@@ -166,6 +167,10 @@ Template.setSwimlaneColorPopup.helpers({
|
||||
isSelected(color) {
|
||||
return Template.instance().currentColor.get() === color;
|
||||
},
|
||||
// #5514: current color as a '#rrggbb' hex for the native color wheel <input>.
|
||||
currentColorHex() {
|
||||
return toHex(Template.instance().currentColor.get()) || '#0079bf';
|
||||
},
|
||||
});
|
||||
|
||||
Template.setSwimlaneColorPopup.events({
|
||||
@@ -178,6 +183,13 @@ Template.setSwimlaneColorPopup.events({
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
// #5514: picking from the native color wheel stores a custom hex.
|
||||
'input .js-swimlane-color-wheel'(event, tpl) {
|
||||
const value = event.currentTarget.value;
|
||||
if (isHexColor(value)) {
|
||||
tpl.currentColor.set(value);
|
||||
}
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
await tpl.currentSwimlane.setColor(tpl.currentColor.get());
|
||||
|
||||
@@ -118,3 +118,42 @@
|
||||
height: clamp(40px, 5vw, 60px);
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
/* Account-status distinction on the avatar itself (see userAvatar.js
|
||||
memberAccountClass / isImportedMember / isInactiveMember):
|
||||
- imported-member: an un-reconciled board-import placeholder that is not yet mapped
|
||||
to a real account — dashed ring + amber "?" corner badge.
|
||||
- inactive-member: deactivated on this board (e.g. a person not in LDAP after
|
||||
reconciliation) — greyscale + dimmed, grey corner badge.
|
||||
A placeholder is usually both; a real, active account gets neither. */
|
||||
.member.imported-member .avatar {
|
||||
border: 2px dashed #7f8c9a;
|
||||
box-sizing: border-box;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.member.inactive-member .avatar {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.member .member-account-badge {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
line-height: 13px;
|
||||
padding: 0 1px;
|
||||
border-radius: 7px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 1.5px #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
.member .member-account-badge.imported {
|
||||
background: #f39c12;
|
||||
}
|
||||
.member .member-account-badge.inactive {
|
||||
background: #95a5a6;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
template(name="userAvatar")
|
||||
a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{#if userData}}{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}{{/if}}")
|
||||
a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}} {{memberAccountClass}}" title="{{#if userData}}{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}{{#if isImportedMember}} — {{_ 'imported-member-no-account'}}{{else}}{{#if isInactiveMember}} — {{_ 'inactive-member'}}{{/if}}{{/if}}{{/if}}")
|
||||
if userData
|
||||
if userData.profile.avatarUrl
|
||||
img.avatar.avatar-image(src="{{avatarUrl}}" alt="")
|
||||
@@ -8,6 +8,11 @@ template(name="userAvatar")
|
||||
else
|
||||
+userAvatarInitials(userId=this.userId)
|
||||
|
||||
if isImportedMember
|
||||
span.member-account-badge.imported(title="{{_ 'imported-member-no-account'}}") ?
|
||||
else if isInactiveMember
|
||||
span.member-account-badge.inactive(title="{{_ 'inactive-member'}}")
|
||||
|
||||
if showStatus
|
||||
span.member-presence-status(class=presenceStatusClassName)
|
||||
span.member-type(class=memberType)
|
||||
|
||||
@@ -11,11 +11,55 @@ Template.userAvatar.helpers({
|
||||
fields: {
|
||||
profile: 1,
|
||||
username: 1,
|
||||
authenticationMethod: 1,
|
||||
isActive: 1,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
},
|
||||
|
||||
// Distinguish, on the avatar itself, whether a real account backs this member:
|
||||
// - imported-member: an inert placeholder created by a board import that has not been
|
||||
// reconciled/mapped to a real account yet (dashed ring + "?" badge).
|
||||
// - inactive-member: deactivated on this board (e.g. a person not in LDAP after
|
||||
// reconciliation) (greyscale + dim).
|
||||
// A placeholder is typically both. A real, active account gets neither.
|
||||
memberAccountClass() {
|
||||
const user = ReactiveCache.getUser(this.userId, {
|
||||
fields: { authenticationMethod: 1, isActive: 1 },
|
||||
});
|
||||
if (!user) return '';
|
||||
const classes = [];
|
||||
if (user.authenticationMethod === 'imported') classes.push('imported-member');
|
||||
let inactive = user.isActive === false;
|
||||
if (!inactive) {
|
||||
const board = Utils.getCurrentBoard && Utils.getCurrentBoard();
|
||||
const m = board && Array.isArray(board.members)
|
||||
? board.members.find(x => x.userId === this.userId)
|
||||
: null;
|
||||
if (m && m.isActive === false) inactive = true;
|
||||
}
|
||||
if (inactive) classes.push('inactive-member');
|
||||
return classes.join(' ');
|
||||
},
|
||||
|
||||
isImportedMember() {
|
||||
const user = ReactiveCache.getUser(this.userId, { fields: { authenticationMethod: 1 } });
|
||||
return !!(user && user.authenticationMethod === 'imported');
|
||||
},
|
||||
|
||||
isInactiveMember() {
|
||||
const user = ReactiveCache.getUser(this.userId, { fields: { authenticationMethod: 1, isActive: 1 } });
|
||||
if (!user) return false;
|
||||
if (user.authenticationMethod === 'imported') return false; // shown via the imported badge
|
||||
if (user.isActive === false) return true;
|
||||
const board = Utils.getCurrentBoard && Utils.getCurrentBoard();
|
||||
const m = board && Array.isArray(board.members)
|
||||
? board.members.find(x => x.userId === this.userId)
|
||||
: null;
|
||||
return !!(m && m.isActive === false);
|
||||
},
|
||||
|
||||
avatarUrl() {
|
||||
const user = ReactiveCache.getUser(this.userId, { fields: { profile: 1 } });
|
||||
const base = (user && user.profile && user.profile.avatarUrl) || '';
|
||||
|
||||
@@ -98,6 +98,19 @@ body.mobile-mode .auth-layout .auth-dialog .at-form .password-input-container .p
|
||||
margin-top: 2vh;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
/* #4394 (security): when registration / forgot-password are disabled in the
|
||||
Admin Panel, keep the corresponding links hidden even after useraccounts
|
||||
re-renders the form on a failed login. The class lives on the stable <body>
|
||||
ancestor (set in layouts.js), so this rule re-hides every recreated copy of
|
||||
the link — unlike a one-shot inline display:none that the re-render dropped. */
|
||||
body.registration-disabled .at-signup-link,
|
||||
body.registration-disabled .at-signUp {
|
||||
display: none !important;
|
||||
}
|
||||
body.forgotpwd-disabled .at-pwd-link,
|
||||
body.forgotpwd-disabled .at-forgotPwd {
|
||||
display: none !important;
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form .at-signup-link .at-signUp,
|
||||
.auth-layout .auth-dialog .at-form .at-signin-link .at-signUp,
|
||||
.auth-layout .auth-dialog .at-form .at-forgotPwd .at-signUp,
|
||||
|
||||
@@ -29,6 +29,27 @@ import {
|
||||
calendar,
|
||||
} from '/imports/lib/dateUtils';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { isHexColor, contrastText } from '/models/lib/contrastColor';
|
||||
|
||||
// #5514: a color (label / list / swimlane / card) may be either a named palette
|
||||
// color rendered via a CSS class (e.g. `card-label-green`) or a custom
|
||||
// '#rrggbb' hex chosen from the color wheel. For a hex value there is no CSS
|
||||
// class, so we emit an inline `background-color` plus an automatically readable
|
||||
// text `color` (contrastText). These helpers let templates keep the class path
|
||||
// for named colors and switch to inline styles for hex.
|
||||
|
||||
// Class for a label chip: `card-label-<name>` for named colors, empty for hex.
|
||||
Blaze.registerHelper('labelColorClass', color =>
|
||||
isHexColor(color) ? '' : `card-label-${color}`,
|
||||
);
|
||||
|
||||
// Inline style for a label chip: background + readable text for a hex color,
|
||||
// empty for a named color (its CSS class already sets both).
|
||||
Blaze.registerHelper('labelColorStyle', color =>
|
||||
isHexColor(color)
|
||||
? `background-color:${color};color:${contrastText(color)};`
|
||||
: '',
|
||||
);
|
||||
|
||||
Blaze.registerHelper('currentBoard', () => {
|
||||
const ret = Utils.getCurrentBoard();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '/client/components/settings/adminFeatures.jade';
|
||||
import '/client/components/settings/adminReports.jade';
|
||||
import '/client/components/settings/attachments.jade';
|
||||
import '/client/components/settings/attachmentSettings.jade';
|
||||
@@ -11,6 +12,7 @@ import '/client/components/settings/settingBody.jade';
|
||||
import '/client/components/settings/settingHeader.jade';
|
||||
import '/client/components/settings/translationBody.jade';
|
||||
|
||||
import '/client/components/settings/adminFeatures.js';
|
||||
import '/client/components/settings/adminReports.js';
|
||||
import '/client/components/settings/attachments.js';
|
||||
import '/client/components/settings/connectionMethod.js';
|
||||
|
||||
@@ -18,6 +18,84 @@ window.ExportHtml = Popup => {
|
||||
}
|
||||
};
|
||||
|
||||
// The board is rendered with infinite scroll — each list only keeps ~10 cards
|
||||
// in the DOM (client/components/lists/listBody.js). A naive DOM-clone export
|
||||
// would therefore drop every card past the fold. Before cloning we lift every
|
||||
// list's `cardlimit` so the whole board renders, then wait for Blaze to flush.
|
||||
const expandAllCardsForExport = async () => {
|
||||
let changed = false;
|
||||
Array.from(document.querySelectorAll('.list-body')).forEach(el => {
|
||||
try {
|
||||
let view = Blaze.getView(el);
|
||||
while (view) {
|
||||
if (view.template && view.template.viewName === 'Template.listBody' && view.templateInstance) {
|
||||
const inst = view.templateInstance();
|
||||
if (inst && inst.cardlimit && inst.cardlimit.get() < 1e6) {
|
||||
inst.cardlimit.set(1e6); // render all cards in this list
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
} catch (e) { /* best effort per list */ }
|
||||
});
|
||||
if (changed) {
|
||||
// Let the reactive re-render settle before we snapshot the DOM.
|
||||
await new Promise(resolve => Tracker.afterFlush(() => setTimeout(resolve, 800)));
|
||||
}
|
||||
};
|
||||
|
||||
// Write the zip to disk WITHOUT holding the whole archive in memory. When the
|
||||
// browser supports the File System Access API we pipe JSZip's internal stream
|
||||
// straight to the chosen file, chunk by chunk, applying backpressure so a
|
||||
// board with many attachments/covers never buffers the entire .zip in RAM.
|
||||
// Older browsers fall back to the previous in-memory blob download.
|
||||
const saveZipStreaming = async (zip, filename) => {
|
||||
const genOpts = {
|
||||
type: 'uint8array',
|
||||
streamFiles: true,
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 },
|
||||
};
|
||||
if (typeof window.showSaveFilePicker === 'function') {
|
||||
let handle = null;
|
||||
try {
|
||||
handle = await window.showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [{ description: 'Zip archive', accept: { 'application/zip': ['.zip'] } }],
|
||||
});
|
||||
} catch (e) {
|
||||
// User dismissed the save dialog — nothing to do.
|
||||
if (e && e.name === 'AbortError') return;
|
||||
handle = null; // any other failure → fall back to blob download
|
||||
}
|
||||
if (handle) {
|
||||
const writable = await handle.createWritable();
|
||||
await new Promise((resolve, reject) => {
|
||||
const helper = zip.generateInternalStream(genOpts);
|
||||
helper
|
||||
.on('data', data => {
|
||||
// Pause the zip stream until this chunk is flushed to disk.
|
||||
helper.pause();
|
||||
writable.write(data).then(() => helper.resume(), reject);
|
||||
})
|
||||
.on('error', reject)
|
||||
.on('end', () => { writable.close().then(resolve, reject); })
|
||||
.resume();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: build the blob in memory (older browsers only).
|
||||
const content = await zip.generateAsync({
|
||||
type: 'blob',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 },
|
||||
});
|
||||
saveAs(content, filename);
|
||||
};
|
||||
|
||||
const getPageHtmlString = (clonedElement = null) => {
|
||||
const element = clonedElement || window.document.querySelector('html');
|
||||
return `<!doctype html>${element.outerHTML}`;
|
||||
@@ -526,6 +604,10 @@ body { font-family: sans-serif !important; }
|
||||
const zipFilename = boardTitle || boardSlug;
|
||||
const zipDirName = zipFilename; // Directory name inside the ZIP
|
||||
|
||||
// Render EVERY card first (infinite scroll otherwise leaves most cards out
|
||||
// of the DOM), then snapshot.
|
||||
await expandAllCardsForExport();
|
||||
|
||||
// Clone the HTML element to process for export without modifying live DOM
|
||||
const htmlElement = window.document.querySelector('html');
|
||||
const clonedHtmlElement = htmlElement.cloneNode(true);
|
||||
@@ -549,8 +631,8 @@ body { font-family: sans-serif !important; }
|
||||
|
||||
addBoardHTMLToZip(boardSlug, zip, clonedHtmlElement, zipDirName);
|
||||
|
||||
const content = await zip.generateAsync({ type: 'blob' });
|
||||
saveAs(content, `${zipFilename}.zip`);
|
||||
// Stream the .zip straight to disk (low memory) with a blob fallback.
|
||||
await saveZipStreaming(zip, `${zipFilename}.zip`);
|
||||
// No page reload - impersonation session is preserved!
|
||||
};
|
||||
};
|
||||
+19
-29
@@ -19,8 +19,9 @@ import {
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import { weekRange } from '/models/lib/weekStart';
|
||||
// Sidebar is imported late to avoid circular dependency (sidebar.js needs its
|
||||
// jade template loaded first, but router.js → filter.js would load it too early)
|
||||
let _Sidebar;
|
||||
@@ -119,44 +120,33 @@ class DateFilter {
|
||||
// weeks up to today +/- offset. This considers the user's preferred
|
||||
// start of week day (as defined by Meteor).
|
||||
relativeWeek(offset, week) {
|
||||
if (this._filterState == 'thisweek') {
|
||||
// #4881: toggle per-week so clicking the same button again clears it, but
|
||||
// clicking the OTHER week switches to it instead of just resetting (the old
|
||||
// code reset on either state, so you could not go straight from "this week"
|
||||
// to "next week").
|
||||
const targetState = week === 'next' ? 'nextweek' : 'thisweek';
|
||||
if (this._filterState === targetState) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._filterState == 'nextweek') {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// getStartDayOfWeek returns the offset from Sunday of the user's
|
||||
// preferred starting day of the week. This date should be added
|
||||
// to the moment start of week to get the real start of week date.
|
||||
// The default is 1, meaning Monday.
|
||||
// #4881: the old window came from startOf(now(), 'week'), but native
|
||||
// dateUtils.startOf() has no 'week' case, so it returned "now" unchanged and
|
||||
// the window started at today + startDayOfWeek — i.e. it selected NEXT
|
||||
// week's cards for "this week" and ignored the configured start weekday.
|
||||
// weekRange() computes the correct window that CONTAINS today for the user's
|
||||
// configured start day of week (0=Sunday..6=Saturday, default Monday).
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const weekStartDay = currentUser ? currentUser.getStartDayOfWeek() : 1;
|
||||
const weekOffset = week === 'next' ? 1 : 0;
|
||||
const { start: WeekStart, end: WeekEnd } = weekRange(now(), weekStartDay, weekOffset);
|
||||
|
||||
if (week === 'this') {
|
||||
// Create week start and end dates
|
||||
var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay, 'days'), 'day');
|
||||
var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
|
||||
|
||||
this._updateState('thisweek');
|
||||
} else if (week === 'next') {
|
||||
// Create next week start and end dates
|
||||
var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay + 7, 'days'), 'day');
|
||||
var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
|
||||
|
||||
this._updateState('nextweek');
|
||||
}
|
||||
|
||||
var startDate = WeekStart;
|
||||
var endDate = WeekEnd;
|
||||
this._updateState(targetState);
|
||||
|
||||
if (offset >= 0) {
|
||||
this._filter = { $gte: startDate, $lte: endDate };
|
||||
this._filter = { $gte: WeekStart, $lte: WeekEnd };
|
||||
} else {
|
||||
this._filter = { $lte: startDate, $gte: endDate };
|
||||
this._filter = { $lte: WeekStart, $gte: WeekEnd };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { listCardsSelector } from '/models/lib/swimlaneFilter';
|
||||
const { windowCountId: windowCountIdImpl, resolveCardsLoadingMode } = require('/models/lib/cardsLoading');
|
||||
|
||||
// Client-only mirror of the server `boardListCardCounts` docs published by the
|
||||
// 'boardListCardCount' publication (one doc per list/swimlane window, holding
|
||||
// the total card count so the board knows whether more cards remain to scroll
|
||||
// in). Only used in lazy card-loading mode.
|
||||
export const BoardListCardCounts = new Mongo.Collection('boardListCardCounts');
|
||||
|
||||
// Effective card-loading mode. Prefers the admin Setting doc (reactive, so an
|
||||
// Admin Panel change is picked up), falls back to the public env value, then to
|
||||
// 'all'. See server/cards-loading.js and Admin Panel / Features.
|
||||
export function cardsLoadingMode() {
|
||||
const setting = ReactiveCache.getCurrentSetting();
|
||||
const fromDoc = setting && setting.cardsLoading;
|
||||
const fromEnv =
|
||||
Meteor.settings && Meteor.settings.public && Meteor.settings.public.cardsLoading;
|
||||
return resolveCardsLoadingMode(fromDoc || fromEnv);
|
||||
}
|
||||
|
||||
export function isLazyCards() {
|
||||
return cardsLoadingMode() === 'lazy';
|
||||
}
|
||||
|
||||
// Stable id for one (list, swimlane) window's count doc (shared, unit-tested).
|
||||
export function windowCountId(listId, swimlaneId) {
|
||||
return windowCountIdImpl(listId, swimlaneId);
|
||||
}
|
||||
|
||||
// Accurate total card count for `list.cards(swimlaneId)` in lazy mode, where
|
||||
// minimongo holds only the loaded window. Builds the SAME selector
|
||||
// list.cards(swimlaneId) uses (listCardsSelector + the board Filter), subscribes
|
||||
// (deduped) to the server-side count for it, and returns that count. Returns null
|
||||
// when not in lazy mode or the count has not arrived yet, so callers can fall
|
||||
// back to the loaded-window length. Because the selector is identical to the one
|
||||
// list.cards() runs against minimongo, the server count is exactly the accurate
|
||||
// version of whatever list.cards(swimlaneId).length would report.
|
||||
export function lazyListCardCount(list, swimlaneId) {
|
||||
if (!isLazyCards() || !list || !list.boardId) return null;
|
||||
const selector = listCardsSelector(
|
||||
list._id,
|
||||
swimlaneId,
|
||||
list.orphanedCardsSwimlaneIds ? list.orphanedCardsSwimlaneIds(swimlaneId) : undefined,
|
||||
);
|
||||
const mongoSelector = Filter.mongoSelector(selector);
|
||||
const id = windowCountId(list._id, swimlaneId);
|
||||
const inst = typeof Template !== 'undefined' && Template.instance ? Template.instance() : null;
|
||||
if (inst && inst.subscribe) {
|
||||
inst.subscribe('boardListCardCount', id, list.boardId, mongoSelector);
|
||||
}
|
||||
const doc = BoardListCardCounts.findOne(id);
|
||||
return doc ? doc.count : null;
|
||||
}
|
||||
+27
-101
@@ -4,6 +4,7 @@ import { Tracker } from 'meteor/tracker';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { computePopupOffset } from '/client/lib/popupOffset';
|
||||
|
||||
window.Popup = new (class {
|
||||
constructor() {
|
||||
@@ -222,108 +223,33 @@ window.Popup = new (class {
|
||||
return () => {
|
||||
Utils.windowResizeDep.depend();
|
||||
|
||||
if (Utils.isMiniScreen()) return { left: 0, top: 0 };
|
||||
// #5667: read the current page scroll and the opener's document offset,
|
||||
// then delegate the geometry to the pure, unit-tested computePopupOffset.
|
||||
// It lays the popup out in VIEWPORT coordinates — so a date-picker opened
|
||||
// low on a scrolled page stays fully visible instead of extending past the
|
||||
// visible area — and returns document coordinates for the absolute style.
|
||||
const hasOpener = !!($element && $element.length);
|
||||
const offset = hasOpener ? $element.offset() : null;
|
||||
const opener = hasOpener
|
||||
? { top: offset.top, left: offset.left, height: $element.outerHeight() }
|
||||
: null;
|
||||
|
||||
const viewportWidth = $(window).width();
|
||||
const viewportHeight = $(window).height();
|
||||
const viewportPadding = 10;
|
||||
// Calculate actual popup width based on CSS: min(380px, 55vw)
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55);
|
||||
|
||||
// Card details popup: dock it to the top of the viewport so it overlays
|
||||
// the global and board header bars (its z-index already sits above them)
|
||||
// instead of opening downward from the clicked minicard.
|
||||
if (popupName === 'cardDetailsPopup') {
|
||||
const cardWidth = Math.min(600, viewportWidth * 0.8);
|
||||
const centeredLeft = (viewportWidth - cardWidth) / 2;
|
||||
// top is forced to 0 in CSS (.pop-over[data-popup="cardDetailsPopup"])
|
||||
// so the card overlays the header bars; use the full viewport height.
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: 0,
|
||||
maxHeight: viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
|
||||
if (!$element || $element.length === 0) {
|
||||
return {
|
||||
left: viewportPadding,
|
||||
top: viewportPadding,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
const offset = $element.offset();
|
||||
const openerTop = offset.top;
|
||||
const openerBottom = offset.top + $element.outerHeight();
|
||||
const clampedLeft = Math.max(
|
||||
viewportPadding,
|
||||
Math.min(offset.left, viewportWidth - popupWidth - viewportPadding),
|
||||
);
|
||||
|
||||
// Check if this is an admin panel edit popup
|
||||
const isAdminEditPopup = $element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team');
|
||||
|
||||
if (isAdminEditPopup) {
|
||||
// Center the popup horizontally and use full height
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft), // Ensure popup doesn't go off screen
|
||||
top: viewportPadding, // Start from top with small margin
|
||||
maxHeight: viewportHeight - viewportPadding * 2, // Use full height minus small margins
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate available heights around the opener
|
||||
const spaceBelow = viewportHeight - openerBottom - viewportPadding;
|
||||
const spaceAbove = openerTop - viewportPadding;
|
||||
const preferBelow = spaceBelow >= spaceAbove;
|
||||
|
||||
// For language popup, don't use dynamic height to avoid overlapping board
|
||||
const isLanguagePopup = $element.hasClass('js-change-language');
|
||||
let availableHeight;
|
||||
|
||||
if (isLanguagePopup) {
|
||||
// For language popup, position content area below right vertical scrollbar
|
||||
const languageTop = openerBottom;
|
||||
const languageAvailable = Math.max(
|
||||
0,
|
||||
viewportHeight - languageTop - viewportPadding,
|
||||
);
|
||||
const calculatedHeight = Math.min(
|
||||
languageAvailable,
|
||||
viewportHeight * 0.5,
|
||||
); // Max 50% of viewport
|
||||
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: languageTop,
|
||||
maxHeight: Math.max(calculatedHeight, 0),
|
||||
};
|
||||
} else {
|
||||
// For other popups, choose the side with more space and keep popup fully in viewport.
|
||||
if (preferBelow) {
|
||||
availableHeight = Math.max(0, Math.min(spaceBelow, viewportHeight * 0.8));
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: openerBottom,
|
||||
maxHeight: availableHeight,
|
||||
};
|
||||
}
|
||||
|
||||
availableHeight = Math.max(0, Math.min(spaceAbove, viewportHeight * 0.8));
|
||||
const top = Math.max(viewportPadding, openerTop - availableHeight);
|
||||
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top,
|
||||
maxHeight: availableHeight,
|
||||
};
|
||||
}
|
||||
return computePopupOffset({
|
||||
viewportWidth: $(window).width(),
|
||||
viewportHeight: $(window).height(),
|
||||
scrollTop: $(window).scrollTop() || 0,
|
||||
scrollLeft: $(window).scrollLeft() || 0,
|
||||
opener,
|
||||
popupName,
|
||||
isMiniScreen: Utils.isMiniScreen(),
|
||||
isAdminEditPopup:
|
||||
hasOpener &&
|
||||
($element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team')),
|
||||
isLanguagePopup: hasOpener && $element.hasClass('js-change-language'),
|
||||
viewportPadding: 10,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
// Pure geometry for Popup._getOffset — extracted so the positioning math is
|
||||
// unit-testable without a DOM / Meteor (see tests/popupOffset.test.cjs).
|
||||
//
|
||||
// #5667 ("Problem when scrolling in a map with date fields"): a date-picker
|
||||
// calendar (due/start/end date, or a date custom field) opened low on the screen
|
||||
// appeared partly outside the visible area, and — because the pop-over is
|
||||
// `position: absolute` (document coordinates) — trying to scroll it into view
|
||||
// moved it along with the page, so the calendar could never be seen fully.
|
||||
//
|
||||
// Root cause: _getOffset computed the space above / below the opener (and the
|
||||
// clamped `top`) from the opener's DOCUMENT offset (`$el.offset()`) mixed with
|
||||
// the VIEWPORT height, ignoring the page scroll. On a scrolled page that placed
|
||||
// the anchored popup outside the visible viewport.
|
||||
//
|
||||
// Fix: do the space / clamp math in VIEWPORT coordinates (subtract
|
||||
// scrollTop/scrollLeft), then convert the chosen top/left back to DOCUMENT
|
||||
// coordinates (add the scroll back) for the absolute style. The popup box is then
|
||||
// always laid out fully within the visible viewport. When the page is not
|
||||
// scrolled (scrollTop === scrollLeft === 0) the output is identical to the
|
||||
// previous behaviour, so nothing regresses in the common case.
|
||||
|
||||
function computePopupOffset(params) {
|
||||
const {
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
scrollTop = 0,
|
||||
scrollLeft = 0,
|
||||
// Opener document offset { top, left, height }, or null for a programmatic
|
||||
// open with no reference element.
|
||||
opener = null,
|
||||
popupName,
|
||||
isMiniScreen = false,
|
||||
isAdminEditPopup = false,
|
||||
isLanguagePopup = false,
|
||||
viewportPadding = 10,
|
||||
} = params;
|
||||
|
||||
if (isMiniScreen) return { left: 0, top: 0 };
|
||||
|
||||
// Actual popup width from CSS: min(380px, 55vw).
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55);
|
||||
|
||||
// Card details popup: docked to the top of the viewport (CSS also forces
|
||||
// top:0) so it overlays the header bars instead of opening from the minicard.
|
||||
if (popupName === 'cardDetailsPopup') {
|
||||
const cardWidth = Math.min(600, viewportWidth * 0.8);
|
||||
const centeredLeft = (viewportWidth - cardWidth) / 2;
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: scrollTop, // viewport top in document coords
|
||||
maxHeight: viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// No opener element: fall back to the top-left of the VISIBLE viewport.
|
||||
if (!opener) {
|
||||
return {
|
||||
left: viewportPadding + scrollLeft,
|
||||
top: viewportPadding + scrollTop,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
// Opener position in viewport coordinates (this is the #5667 fix: the previous
|
||||
// code used the raw document offset here).
|
||||
const openerTopVp = opener.top - scrollTop;
|
||||
const openerBottomVp = openerTopVp + opener.height;
|
||||
const openerLeftVp = opener.left - scrollLeft;
|
||||
|
||||
const clampedLeftVp = Math.max(
|
||||
viewportPadding,
|
||||
Math.min(openerLeftVp, viewportWidth - popupWidth - viewportPadding),
|
||||
);
|
||||
const clampedLeft = clampedLeftVp + scrollLeft;
|
||||
|
||||
// Admin panel edit popups: horizontally centered, anchored to the viewport top.
|
||||
if (isAdminEditPopup) {
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: viewportPadding + scrollTop,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
const spaceBelow = viewportHeight - openerBottomVp - viewportPadding;
|
||||
const spaceAbove = openerTopVp - viewportPadding;
|
||||
const preferBelow = spaceBelow >= spaceAbove;
|
||||
|
||||
// Language popup: fixed-ish height below the opener, capped at 50% viewport.
|
||||
if (isLanguagePopup) {
|
||||
const languageTopVp = openerBottomVp;
|
||||
const languageAvailable = Math.max(
|
||||
0,
|
||||
viewportHeight - languageTopVp - viewportPadding,
|
||||
);
|
||||
const calculatedHeight = Math.min(languageAvailable, viewportHeight * 0.5);
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: languageTopVp + scrollTop,
|
||||
maxHeight: Math.max(calculatedHeight, 0),
|
||||
};
|
||||
}
|
||||
|
||||
// Other popups: open on the side with more room and stay fully in the viewport.
|
||||
if (preferBelow) {
|
||||
const maxHeight = Math.max(0, Math.min(spaceBelow, viewportHeight * 0.8));
|
||||
return { left: clampedLeft, top: openerBottomVp + scrollTop, maxHeight };
|
||||
}
|
||||
|
||||
const maxHeight = Math.max(0, Math.min(spaceAbove, viewportHeight * 0.8));
|
||||
const topVp = Math.max(viewportPadding, openerTopVp - maxHeight);
|
||||
return { left: clampedLeft, top: topVp + scrollTop, maxHeight };
|
||||
}
|
||||
|
||||
module.exports = { computePopupOffset };
|
||||
@@ -17,7 +17,25 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
||||
// Find the repo root by walking up from process.cwd()/PWD, NOT from the bare `__dirname`
|
||||
// global: under `meteor test` referencing `__dirname` makes Meteor inject a
|
||||
// `const __dirname` that collides with the one its CommonJS module wrapper already
|
||||
// provides, crashing the whole server bundle at boot with "Identifier '__dirname' has
|
||||
// already been declared" (same fix as server/lib/tests/dependencies.openapi.tests.js).
|
||||
function findRepoRoot(marker) {
|
||||
const seeds = [process.cwd(), process.env.PWD, process.env.OLDPWD].filter(Boolean);
|
||||
for (const seed of seeds) {
|
||||
let dir = seed;
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
try { fs.accessSync(path.join(dir, marker)); return dir; } catch (e) { /* walk up */ }
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
return process.cwd();
|
||||
}
|
||||
const repoRoot = findRepoRoot(path.join('client', 'components', 'rules', 'triggers', 'boardTriggers.jade'));
|
||||
const jadePath = path.join(
|
||||
repoRoot,
|
||||
'client',
|
||||
|
||||
+16
-6
@@ -4,16 +4,26 @@ import { Tracker } from 'meteor/tracker';
|
||||
import { findWhere, where, uniqBy, groupBy, indexBy, debounce, once } from '/imports/lib/collectionHelpers';
|
||||
import Settings from '/models/settings';
|
||||
import Users from '/models/users';
|
||||
import { computeBoardBackground } from '/models/lib/boardBackground';
|
||||
|
||||
export const Utils = {
|
||||
async setBackgroundImage(url) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (currentBoard.backgroundImageURL !== undefined) {
|
||||
$(".board-wrapper").css({"background":"url(" + currentBoard.backgroundImageURL + ")","background-size":"cover"});
|
||||
$(".swimlane,.swimlane .list,.swimlane .list .list-body,.swimlane .list:first-child .list-body").css({"background-color":"transparent"});
|
||||
$(".minicard").css({"opacity": "0.9"});
|
||||
} else if (currentBoard["background-color"]) {
|
||||
await currentBoard.setColor(currentBoard["background-color"]);
|
||||
const bg = computeBoardBackground(currentBoard);
|
||||
const swimlaneSelector =
|
||||
".swimlane,.swimlane .list,.swimlane .list .list-body,.swimlane .list:first-child .list-body";
|
||||
if (bg.type === "image") {
|
||||
$(".board-wrapper").css({ "background": "url(" + bg.url + ")", "background-size": "cover" });
|
||||
$(swimlaneSelector).css({ "background-color": "transparent" });
|
||||
$(".minicard").css({ "opacity": "0.9" });
|
||||
} else {
|
||||
// #4978: clear any inline background left over from a previously shown
|
||||
// board, so switching image -> color or image -> plain (e.g. via the
|
||||
// favorites bar) actually removes the old image. The board's color, when
|
||||
// any, is applied reactively via the `.board-wrapper` colorClass.
|
||||
$(".board-wrapper").css({ "background": "", "background-size": "" });
|
||||
$(swimlaneSelector).css({ "background-color": "" });
|
||||
$(".minicard").css({ "opacity": "" });
|
||||
}
|
||||
},
|
||||
/** returns the current board id
|
||||
|
||||
+69
-21
@@ -10,6 +10,27 @@ import { Utils } from '/client/lib/utils';
|
||||
|
||||
let previousPath;
|
||||
|
||||
// On Sandstorm the grain is authenticated by the platform, not by a local WeKan
|
||||
// account. That login is delivered asynchronously over the DDP connection (via
|
||||
// connection.setUserId(), which — unlike a normal password login — does NOT set
|
||||
// Meteor.loggingIn()), so for the first moments after a grain opens Meteor.userId()
|
||||
// is still null even though the user is, and always will be, authenticated. There
|
||||
// is no local sign-in page in a grain. useraccounts' ensureSignedIn would bounce the
|
||||
// grain straight to the (nonexistent) atSignIn route on that brief null window,
|
||||
// stranding the user on a "Must be logged in" page instead of opening All Boards.
|
||||
const isSandstorm = Meteor.settings?.public?.sandstorm;
|
||||
|
||||
// Drop-in replacement for AccountsTemplates.ensureSignedIn used as a route
|
||||
// triggersEnter: a no-op on Sandstorm (the platform guarantees authentication and
|
||||
// the route repopulates reactively once Meteor.userId() lands), and the normal
|
||||
// useraccounts guard everywhere else.
|
||||
function ensureSignedInUnlessSandstorm(context, redirect, stop) {
|
||||
if (isSandstorm) {
|
||||
return undefined;
|
||||
}
|
||||
return AccountsTemplates.ensureSignedIn(context, redirect, stop);
|
||||
}
|
||||
|
||||
FlowRouter.triggers.exit([
|
||||
({ path }) => {
|
||||
previousPath = path;
|
||||
@@ -21,8 +42,11 @@ FlowRouter.triggers.exit([
|
||||
// redirected to; the chosen view is passed to boardList through the
|
||||
// `boardListMenu` Session value.
|
||||
function renderBoardList(ctx, menu) {
|
||||
// Redirect to sign-in immediately if user is not logged in
|
||||
if (!Meteor.userId()) {
|
||||
// Redirect to sign-in immediately if user is not logged in — except on Sandstorm,
|
||||
// where the platform authenticates asynchronously (see isSandstorm note above) and
|
||||
// there is no sign-in page. There we render the All Boards list, which reactively
|
||||
// fills in once the grain login lands, instead of bouncing to atSignIn.
|
||||
if (!Meteor.userId() && !isSandstorm) {
|
||||
FlowRouter.go('atSignIn');
|
||||
return;
|
||||
}
|
||||
@@ -48,7 +72,7 @@ function renderBoardList(ctx, menu) {
|
||||
|
||||
FlowRouter.route('/', {
|
||||
name: 'home',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
renderBoardList(this, 'starred');
|
||||
},
|
||||
@@ -56,7 +80,7 @@ FlowRouter.route('/', {
|
||||
|
||||
FlowRouter.route('/templates', {
|
||||
name: 'allboards-templates',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
renderBoardList(this, 'templates');
|
||||
},
|
||||
@@ -64,7 +88,7 @@ FlowRouter.route('/templates', {
|
||||
|
||||
FlowRouter.route('/remaining', {
|
||||
name: 'allboards-remaining',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
renderBoardList(this, 'remaining');
|
||||
},
|
||||
@@ -72,7 +96,7 @@ FlowRouter.route('/remaining', {
|
||||
|
||||
FlowRouter.route('/public', {
|
||||
name: 'public',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -96,7 +120,7 @@ FlowRouter.route('/public', {
|
||||
|
||||
FlowRouter.route('/accessibility', {
|
||||
name: 'accessibility',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -120,7 +144,7 @@ FlowRouter.route('/accessibility', {
|
||||
|
||||
FlowRouter.route('/support', {
|
||||
name: 'support',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -144,7 +168,7 @@ FlowRouter.route('/support', {
|
||||
|
||||
FlowRouter.route('/b/:id/:slug/rules', {
|
||||
name: 'board-rules',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action(params) {
|
||||
const currentBoard = params.id;
|
||||
Session.set('currentBoard', currentBoard);
|
||||
@@ -272,7 +296,7 @@ FlowRouter.route('/shortcuts', {
|
||||
|
||||
FlowRouter.route('/b/templates', {
|
||||
name: 'template-container',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -296,7 +320,7 @@ FlowRouter.route('/b/templates', {
|
||||
|
||||
FlowRouter.route('/my-cards', {
|
||||
name: 'my-cards',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
@@ -316,7 +340,7 @@ FlowRouter.route('/my-cards', {
|
||||
|
||||
FlowRouter.route('/due-cards', {
|
||||
name: 'due-cards',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
@@ -336,7 +360,7 @@ FlowRouter.route('/due-cards', {
|
||||
|
||||
FlowRouter.route('/global-search', {
|
||||
name: 'global-search',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
@@ -371,7 +395,7 @@ FlowRouter.route('/global-search', {
|
||||
// Mobile Bookmarks page
|
||||
FlowRouter.route('/bookmarks', {
|
||||
name: 'bookmarks',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action() {
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
@@ -409,7 +433,7 @@ FlowRouter.route('/broken-cards', {
|
||||
|
||||
FlowRouter.route('/import/:source', {
|
||||
name: 'import',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
triggersEnter: [ensureSignedInUnlessSandstorm],
|
||||
action(params) {
|
||||
if (Session.get('currentBoard')) {
|
||||
Session.set('fromBoard', Session.get('currentBoard'));
|
||||
@@ -434,7 +458,7 @@ FlowRouter.route('/import/:source', {
|
||||
FlowRouter.route('/setting', {
|
||||
name: 'setting',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -459,7 +483,7 @@ FlowRouter.route('/setting', {
|
||||
FlowRouter.route('/information', {
|
||||
name: 'information',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -483,7 +507,7 @@ FlowRouter.route('/information', {
|
||||
FlowRouter.route('/people', {
|
||||
name: 'people',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -507,7 +531,7 @@ FlowRouter.route('/people', {
|
||||
FlowRouter.route('/admin-reports', {
|
||||
name: 'admin-reports',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -531,7 +555,7 @@ FlowRouter.route('/admin-reports', {
|
||||
FlowRouter.route('/attachments', {
|
||||
name: 'attachments',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -555,7 +579,7 @@ FlowRouter.route('/attachments', {
|
||||
FlowRouter.route('/translation', {
|
||||
name: 'translation',
|
||||
triggersEnter: [
|
||||
AccountsTemplates.ensureSignedIn,
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
@@ -576,6 +600,30 @@ FlowRouter.route('/translation', {
|
||||
},
|
||||
});
|
||||
|
||||
FlowRouter.route('/admin-features', {
|
||||
name: 'admin-features',
|
||||
triggersEnter: [
|
||||
ensureSignedInUnlessSandstorm,
|
||||
() => {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
Session.set('currentCard', null);
|
||||
Session.set('popupCardId', null);
|
||||
Session.set('popupCardBoardId', null);
|
||||
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
EscapeActions.executeAll();
|
||||
},
|
||||
],
|
||||
action() {
|
||||
this.render('defaultLayout', {
|
||||
headerBar: 'settingHeaderBar',
|
||||
content: 'adminFeatures',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
FlowRouter.route('*', {
|
||||
action() {
|
||||
this.render('defaultLayout', { content: 'notFound' });
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
# WeKan with FerretDB 2 + PostgreSQL (DocumentDB extension)
|
||||
#
|
||||
# Use this compose file instead of the default docker-compose.yml when you
|
||||
# prefer PostgreSQL as the backing store. For the lighter, single-container,
|
||||
# no-PostgreSQL option, see the default docker-compose.yml (FerretDB v1 with the
|
||||
# embedded SQLite backend, using the wekan/FerretDB release binaries). For the
|
||||
# classic MongoDB 7 option, see docker-compose-mongodb-v7.yml.
|
||||
#
|
||||
# Supported on all platforms where Node.js 22 runs:
|
||||
# linux/amd64, linux/arm64, linux/arm/v7, linux/ppc64le, linux/s390x
|
||||
#
|
||||
# Based on: docs/Databases/FerretDB2-PostgreSQL.md
|
||||
#
|
||||
# Setup:
|
||||
# docker compose -f docker-compose-ferretdb-v2-postgresql.yml up -d
|
||||
#
|
||||
# To stop:
|
||||
# docker compose -f docker-compose-ferretdb-v2-postgresql.yml stop
|
||||
|
||||
services:
|
||||
|
||||
# PostgreSQL 17 with the DocumentDB extension (FerretDB 2 backing store).
|
||||
postgres-documentdb:
|
||||
image: ghcr.io/ferretdb/postgres-documentdb:17
|
||||
container_name: wekan-postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ferretdb
|
||||
POSTGRES_PASSWORD: ${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}
|
||||
POSTGRES_DB: ferretdb
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- wekan-tier
|
||||
|
||||
# FerretDB 2 - MongoDB-compatible protocol layer over PostgreSQL (on :27017).
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb:2
|
||||
container_name: wekan-ferretdb
|
||||
restart: always
|
||||
environment:
|
||||
FERRETDB_POSTGRESQL_URL: postgres://ferretdb:${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}@postgres-documentdb/ferretdb
|
||||
depends_on:
|
||||
- postgres-documentdb
|
||||
networks:
|
||||
- wekan-tier
|
||||
# Expose only on localhost by default; remove to keep it internal to the network.
|
||||
ports:
|
||||
- 127.0.0.1:27017:27017
|
||||
|
||||
wekan:
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== WEKAN FROM GITHUB/QUAY/DOCKER HUB ====
|
||||
# All of GitHub, Quay and Docker Hub have latest, but because
|
||||
# latest tag changes when is newest release,
|
||||
# when upgrading would be better to use version tag.
|
||||
# a) Using specific version tag is better:
|
||||
# image: ghcr.io/wekan/wekan:v6.89
|
||||
# image: quay.io/wekan/wekan:v6.89
|
||||
# image: wekanteam/wekan:v6.89
|
||||
# b) GitHub Container registry.
|
||||
# b1) Latest release tag:
|
||||
image: ghcr.io/wekan/wekan:latest
|
||||
# b2) Newest git commit automatic build:
|
||||
#image: ghcr.io/wekan/wekan:main
|
||||
# c) Quay:
|
||||
#image: quay.io/wekan/wekan:latest
|
||||
# d) Docker Hub:
|
||||
#image: wekanteam/wekan:latest
|
||||
#-------------------------------------------------------------------------------------
|
||||
container_name: wekan-app
|
||||
# On CentOS 7 there is seccomp issue with glibc 6,
|
||||
# so CentOS 7 users shoud use these security_opt seccomp:unconfined
|
||||
# settings to get WeKan working. See:
|
||||
# - https://github.com/wekan/wekan/issues/4585
|
||||
# - https://github.com/wekan/wekan/issues/4587
|
||||
#security_opt:
|
||||
# - seccomp:unconfined
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== BUILD wekan-app DOCKER CONTAINER FROM SOURCE, if you uncomment these ====
|
||||
# ==== and use commands: docker compose up -d --build
|
||||
#build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
#-------------------------------------------------------------------------------------
|
||||
ports:
|
||||
# Docker outsideport:insideport. Do not add anything extra here.
|
||||
# For example, if you want to have wekan on port 3001,
|
||||
# use 3001:8080 . Do not add any extra address etc here, that way it does not work.
|
||||
# remove port mapping if you use nginx reverse proxy, port 8080 is already exposed to wekan-tier network
|
||||
- 80:8080
|
||||
environment:
|
||||
#-------------------- USING MONGODB CHANGE STREAMS WITH REPLICA SETS AT CURRENT DATABASE --------------------
|
||||
# If you would not like to use Change Streams and replica set for improving speed, change to use polling:
|
||||
#- METEOR_REACTIVITY_ORDER=polling
|
||||
# https://forums.meteor.com/t/meteor-3-5-beta-change-streams-performance-improvements/64461#change-streams-setup-3
|
||||
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
|
||||
# Keep oplog/polling fallback available for deployments that cannot use Change Streams:
|
||||
# https://github.com/wekan/wekan/issues/6307#issuecomment-4299349231
|
||||
# FerretDB has no MongoDB change streams / replica-set oplog, so use polling:
|
||||
- METEOR_REACTIVITY_ORDER=polling
|
||||
- DDP_TRANSPORT=uws
|
||||
#- DDP_TRANSPORT=sockjs
|
||||
#-----------------------------------------------------------------
|
||||
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
|
||||
- WRITABLE_PATH=/data
|
||||
#-----------------------------------------------------------------
|
||||
# ==== AWS S3 FOR FILES ====
|
||||
# Any region. For example:
|
||||
# us-standard,us-west-1,us-west-2,
|
||||
# eu-west-1,eu-central-1,
|
||||
# ap-southeast-1,ap-northeast-1,sa-east-1
|
||||
#
|
||||
#- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
|
||||
#- S3_SECRET_FILE=/run/secrets/s3_secret
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
|
||||
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
- MONGO_URL=mongodb://ferretdb:${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}@ferretdb:27017/wekan?authMechanism=PLAIN
|
||||
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
|
||||
#---------------------------------------------------------------
|
||||
# ==== ROOT_URL SETTING ====
|
||||
# Change ROOT_URL to your real Wekan URL, for example:
|
||||
# If you have Caddy/Nginx/Apache providing SSL
|
||||
# - https://example.com
|
||||
# - https://boards.example.com
|
||||
# This can be problematic with avatars https://github.com/wekan/wekan/issues/1776
|
||||
# - https://example.com/wekan
|
||||
# If without https, can be only wekan node, no need for Caddy/Nginx/Apache if you don't need them
|
||||
# - http://example.com
|
||||
# - http://boards.example.com
|
||||
# - http://192.168.1.100 <=== using at local LAN
|
||||
- ROOT_URL=http://localhost # <=== using only at same laptop/desktop where Wekan is installed
|
||||
#---------------------------------------------------------------
|
||||
# ==== EMAIL SETTINGS ====
|
||||
# Email settings are only at MAIL_URL and MAIL_FROM.
|
||||
# Admin Panel has test button, but it's not used for settings.
|
||||
# see https://github.com/wekan/wekan/blob/main/docs/Email/Troubleshooting-Mail.md
|
||||
# For SSL in email, change smtp:// to smtps://
|
||||
# NOTE: Special characters need to be url-encoded in MAIL_URL.
|
||||
# You can encode those characters for example at: https://www.urlencoder.org
|
||||
#- MAIL_URL=smtp://user:pass@mailserver.example.com:25/
|
||||
- MAIL_URL=smtp://<mail_url>:25/?ignoreTLS=true&tls={rejectUnauthorized:false}
|
||||
- MAIL_FROM=Wekan Notifications <noreply.wekan@mydomain.com>
|
||||
# Currently MAIL_SERVICE is not in use.
|
||||
#- MAIL_SERVICE=Outlook365
|
||||
#- MAIL_SERVICE_USER=firstname.lastname@hotmail.com
|
||||
#- MAIL_SERVICE_PASSWORD=SecretPassword
|
||||
#- MAIL_SERVICE_PASSWORD_FILE=/run/secrets/mail_service_password
|
||||
#---------------------------------------------------------------
|
||||
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
|
||||
# Add more Node heap, this is done by default at Dockerfile:
|
||||
# - NODE_OPTIONS="--max_old_space_size=4096"
|
||||
# Add more stack, this is done at Dockerfile:
|
||||
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: MONGO OPLOG SETTINGS =====
|
||||
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-378343587
|
||||
# HIGHLY RECOMMENDED for pub/sub performance!
|
||||
# MongoDB oplog is used by Meteor for real-time data synchronization.
|
||||
# Without oplog, Meteor falls back to polling which increases:
|
||||
# - CPU usage by 3-5x
|
||||
# - Network traffic significantly
|
||||
# - Latency from 50ms to 2000ms
|
||||
# Must configure MongoDB replica set first
|
||||
# See: https://blog.meteor.com/tuning-meteor-mongo-livedata-for-scalability-13fe9deb8908
|
||||
# FerretDB emulates OpLog via --replication-set-name=rs0.
|
||||
# HIGHLY RECOMMENDED — enables real-time DDP updates (sub-100ms latency).
|
||||
#- MONGO_OPLOG_URL=mongodb://wekan-ferretdb:27017/local?replicaSet=rs0
|
||||
# MONGO_OPLOG_URL intentionally unset: FerretDB v1 has no replica-set oplog (WeKan uses polling).
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
|
||||
# https://github.com/edemaine/kadira-compose
|
||||
# https://github.com/meteor/meteor-apm-agent
|
||||
# https://blog.meteor.com/kadira-apm-is-now-open-source-490469ffc85f
|
||||
#- APM_OPTIONS_ENDPOINT=http://<kadira-ip>:11011
|
||||
#- APM_APP_ID=
|
||||
#- APM_APP_SECRET=
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: LOGS AND STATS ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Features/Logs.md
|
||||
#
|
||||
# Daily export of Wekan changes as JSON to Logstash and ElasticSearch / Kibana (ELK)
|
||||
# https://github.com/wekan/wekan-logstash
|
||||
#
|
||||
# Statistics Python script for Wekan Dashboard
|
||||
# https://github.com/wekan/wekan-stats
|
||||
#
|
||||
# Console, file, and zulip logger on database changes https://github.com/wekan/wekan/pull/1010
|
||||
# with fix to replace console.log by winston logger https://github.com/wekan/wekan/pull/1033
|
||||
# but there could be bug https://github.com/wekan/wekan/issues/1094
|
||||
#
|
||||
# There is Feature Request: Logging date and time of all activity with summary reports,
|
||||
# and requesting reason for changing card to other column https://github.com/wekan/wekan/issues/1598
|
||||
#---------------------------------------------------------------
|
||||
# ==== NUMBER OF SEARCH RESULTS PER PAGE BY DEFAULT ====
|
||||
#- RESULTS_PER_PAGE=20
|
||||
#---------------------------------------------------------------
|
||||
# ==== AFTER OIDC LOGIN, ADD USERS AUTOMATICALLY TO THIS BOARD ID ====
|
||||
# https://github.com/wekan/wekan/pull/5098
|
||||
#- DEFAULT_BOARD_ID=abcd1234
|
||||
#---------------------------------------------------------------
|
||||
# ==== WEKAN API AND EXPORT BOARD ====
|
||||
# Wekan Export Board works when WITH_API=true.
|
||||
# https://github.com/wekan/wekan/blob/main/docs/API/REST-API.md
|
||||
# https://github.com/wekan/wekan-gogs
|
||||
# If you disable Wekan API with false, Export Board does not work.
|
||||
- WITH_API=true
|
||||
#---------------------------------------------------------------
|
||||
# ==== PASSWORD BRUTE FORCE PROTECTION ====
|
||||
#https://atmospherejs.com/lucasantoniassi/accounts-lockout
|
||||
#Defaults below. Uncomment to change. wekan/server/accounts-lockout.js
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15
|
||||
#---------------------------------------------------------------
|
||||
# ==== ACCOUNT OPTIONS ====
|
||||
# https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config
|
||||
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
||||
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
||||
#---------------------------------------------------------------
|
||||
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
||||
# https://github.com/wekan/wekan/pull/2560
|
||||
- RICHER_CARD_COMMENT_EDITOR=false
|
||||
#---------------------------------------------------------------
|
||||
# ==== CARD LOADING: all (default) or lazy ====
|
||||
# 'all' = load every card of a board into the browser (default).
|
||||
# 'lazy' = each list loads only the cards currently visible
|
||||
# (infinite scroll) plus a live count, so boards with
|
||||
# thousands of cards use less memory/bandwidth.
|
||||
# Also changeable at runtime in Admin Panel / Features.
|
||||
- CARDS_LOADING=all
|
||||
#---------------------------------------------------------------
|
||||
# ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
|
||||
# https://github.com/wekan/wekan/issues/2518
|
||||
- CARD_OPENED_WEBHOOK_ENABLED=false
|
||||
#---------------------------------------------------------------
|
||||
# ==== Allow configuration to validate uploaded attachments ====
|
||||
#-ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM=/usr/local/bin/avscan {file}
|
||||
#-ATTACHMENTS_UPLOAD_MIME_TYPES=image/*,text/*
|
||||
#-ATTACHMENTS_UPLOAD_MAX_SIZE=5000000
|
||||
#---------------------------------------------------------------
|
||||
# ==== Allow configuration to validate uploaded avatars ====
|
||||
#-AVATARS_UPLOAD_EXTERNAL_PROGRAM=/usr/local/bin/avscan {file}
|
||||
#-AVATARS_UPLOAD_MIME_TYPES=image/*
|
||||
#-AVATARS_UPLOAD_MAX_SIZE=500000
|
||||
#---------------------------------------------------------------
|
||||
# ==== Allow to shrink attached/pasted image ====
|
||||
# https://github.com/wekan/wekan/pull/2544
|
||||
#- MAX_IMAGE_PIXEL=1024
|
||||
#- IMAGE_COMPRESS_RATIO=80
|
||||
#---------------------------------------------------------------
|
||||
# ==== NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE =====
|
||||
# Number of days after a notification is read before we remove it.
|
||||
# Default: 2
|
||||
#- NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2
|
||||
#---------------------------------------------------------------
|
||||
# ==== BIGEVENTS DUE ETC NOTIFICATIONS =====
|
||||
# https://github.com/wekan/wekan/pull/2541
|
||||
# Introduced a system env var BIGEVENTS_PATTERN default as "NONE",
|
||||
# so any activityType matches the pattern, system will send out
|
||||
# notifications to all board members no matter they are watching
|
||||
# or tracking the board or not. Owner of the wekan server can
|
||||
# disable the feature by setting this variable to "NONE" or
|
||||
# change the pattern to any valid regex. i.e. '|' delimited
|
||||
# activityType names.
|
||||
# a) Example
|
||||
#- BIGEVENTS_PATTERN=due
|
||||
# b) All
|
||||
#- BIGEVENTS_PATTERN=received|start|due|end
|
||||
# c) Disabled
|
||||
- BIGEVENTS_PATTERN=NONE
|
||||
#---------------------------------------------------------------
|
||||
# ==== NOTIFY ON ASSIGN =====
|
||||
# Notify the user directly when they are added as a card member or
|
||||
# assignee. Set false to disable.
|
||||
#- NOTIFY_ON_ASSIGN=true
|
||||
#---------------------------------------------------------------
|
||||
# ==== EMAIL DUE DATE NOTIFICATION =====
|
||||
# https://github.com/wekan/wekan/pull/2536
|
||||
# System timelines will be showing any user modification for
|
||||
# dueat startat endat receivedat, also notification to
|
||||
# the watchers and if any card is due, about due or past due.
|
||||
#
|
||||
# Notify due days, default is None, 2 days before and on the event day
|
||||
#- NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=2,0
|
||||
#
|
||||
# Notify due at hour of day. Default every morning at 8am. Can be 0-23.
|
||||
# If env variable has parsing error, use default. Notification sent to watchers.
|
||||
#- NOTIFY_DUE_AT_HOUR_OF_DAY=8
|
||||
#-----------------------------------------------------------------
|
||||
# ==== EMAIL NOTIFICATION TIMEOUT, ms =====
|
||||
# Default: 30000 ms = 30s
|
||||
#- EMAIL_NOTIFICATION_TIMEOUT=30000
|
||||
#-----------------------------------------------------------------
|
||||
# ==== CORS =====
|
||||
# CORS: Set Access-Control-Allow-Origin header.
|
||||
#- CORS=*
|
||||
# CORS_ALLOW_HEADERS: Set Access-Control-Allow-Headers header. "Authorization,Content-Type" is required for cross-origin use of the API.
|
||||
#- CORS_ALLOW_HEADERS=Authorization,Content-Type
|
||||
# CORS_EXPOSE_HEADERS: Set Access-Control-Expose-Headers header. This is not needed for typical CORS situations
|
||||
#- CORS_EXPOSE_HEADERS=*
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MATOMO INTEGRATION ====
|
||||
# Optional: Integration with Matomo https://matomo.org that is installed to your server
|
||||
# The address of the server where Matomo is hosted.
|
||||
#- MATOMO_ADDRESS=https://example.com/matomo
|
||||
# The value of the site ID given in Matomo server for Wekan
|
||||
#- MATOMO_SITE_ID=1
|
||||
# The option do not track which enables users to not be tracked by matomo
|
||||
#- MATOMO_DO_NOT_TRACK=true
|
||||
# The option that allows matomo to retrieve the username:
|
||||
#- MATOMO_WITH_USERNAME=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== BROWSER POLICY AND TRUSTED IFRAME URL ====
|
||||
# Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.
|
||||
# Setting this to false is not recommended, it also disables all other browser policy protections
|
||||
# and allows all iframing etc. See wekan/server/policy.js
|
||||
- BROWSER_POLICY_ENABLED=true
|
||||
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
||||
#- TRUSTED_URL=https://intra.example.com
|
||||
#-----------------------------------------------------------------
|
||||
# ==== METRICS ALLOWED IP ADDRESSES ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Features/Metrics.md
|
||||
#- METRICS_ALLOWED_IP_ADDRESSES=192.168.0.100,192.168.0.200
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OUTGOING WEBHOOKS ====
|
||||
# What to send to Outgoing Webhook, or leave out. If commented out the default values will be: cardId,listId,oldListId,boardId,comment,user,card,commentId,swimlaneId,customerField,customFieldValue
|
||||
#- WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,board,list,swimlane,commentId
|
||||
#-----------------------------------------------------------------
|
||||
# ==== Debug OIDC OAuth2 etc ====
|
||||
#- DEBUG=true
|
||||
#---------------------------------------------
|
||||
# ==== AUTOLOGIN WITH OIDC/OAUTH2 ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/autologin.md
|
||||
#- OIDC_REDIRECTION_ENABLED=true
|
||||
# OIDC RP-initiated logout endpoint (end_session_endpoint). When set, the
|
||||
# "Log Out" menu redirects to this endpoint so the identity provider session
|
||||
# is also ended and the user is returned to Wekan (ROOT_URL) via
|
||||
# post_logout_redirect_uri, instead of landing on the provider home page
|
||||
# (which errors for non-admin users). See https://github.com/wekan/wekan/issues/6158
|
||||
# Keycloak example:
|
||||
#- OAUTH2_LOGOUT_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/logout
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 ORACLE on premise identity manager OIM ====
|
||||
#- ORACLE_OIM_ENABLED=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 AZURE ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Azure/Azure.md
|
||||
# 1) Register the application with Azure. Make sure you capture
|
||||
# the application ID as well as generate a secret key.
|
||||
# 2) Configure the environment variables. This differs slightly
|
||||
# by installation type, but make sure you have the following:
|
||||
#- OAUTH2_ENABLED=true
|
||||
# Optional OAuth2 CA Cert, see https://github.com/wekan/wekan/issues/3299
|
||||
#- OAUTH2_CA_CERT=ABCD1234
|
||||
# Use OAuth2 ADFS additional changes. Also needs OAUTH2_ENABLED=true setting.
|
||||
#- OAUTH2_ADFS_ENABLED=false
|
||||
# Azure AD B2C. https://github.com/wekan/wekan/issues/5242
|
||||
#- OAUTH2_B2C_ENABLED=false
|
||||
# SECURITY (GHSA-mp7g-hj5q-gxhq): Link an OIDC login to a pre-existing
|
||||
# Wekan account (password/LDAP) with the same email. OFF by default; when
|
||||
# false an OIDC login whose email already exists is rejected instead of
|
||||
# merged, preventing account takeover via spoofed email claims. Only
|
||||
# enable if you fully trust your OIDC provider's email claims, and even
|
||||
# then the provider must send email_verified=true for the merge to happen.
|
||||
#- OAUTH2_MERGE_EXISTING_USERS=false
|
||||
# When false, OAuth2/OIDC login is refused for users that do not already
|
||||
# have a Wekan account (matched by verified email).
|
||||
#- OAUTH2_AUTO_REGISTRATION=true
|
||||
# Comma/space separated OAuth2/OIDC group names whose members become Wekan
|
||||
# admins. Empty = off.
|
||||
#- OAUTH2_ADMIN_GROUPS=
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# Application GUID captured during app registration:
|
||||
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
|
||||
# Secret key generated during app registration:
|
||||
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_SERVER_URL=https://login.microsoftonline.com/
|
||||
#- OAUTH2_AUTH_ENDPOINT=/oauth2/v2.0/authorize
|
||||
#- OAUTH2_USERINFO_ENDPOINT=https://graph.microsoft.com/oidc/userinfo
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/oauth2/v2.0/token
|
||||
# The claim name you want to map to the unique ID field:
|
||||
#- OAUTH2_ID_MAP=email
|
||||
# The claim name you want to map to the username field:
|
||||
#- OAUTH2_USERNAME_MAP=email
|
||||
# The claim name you want to map to the full name field:
|
||||
#- OAUTH2_FULLNAME_MAP=name
|
||||
# The claim name you want to map to the email field:
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 Nextcloud ====
|
||||
# 1) Register the application with Nextcloud: https://your.nextcloud/index.php/settings/admin/security
|
||||
# Make sure you capture the application ID as well as generate a secret key.
|
||||
# Use https://your.wekan/_oauth/oidc for the redirect URI.
|
||||
# 2) Configure the environment variables. This differs slightly
|
||||
# by installation type, but make sure you have the following:
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# Application GUID captured during app registration:
|
||||
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
|
||||
# Secret key generated during app registration:
|
||||
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_SERVER_URL=https://your-nextcloud.tld
|
||||
#- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/index.php/apps/oauth2/api/v1/token
|
||||
# The claim name you want to map to the unique ID field:
|
||||
#- OAUTH2_ID_MAP=id
|
||||
# The claim name you want to map to the username field:
|
||||
#- OAUTH2_USERNAME_MAP=id
|
||||
# The claim name you want to map to the full name field:
|
||||
#- OAUTH2_FULLNAME_MAP=display-name
|
||||
# The claim name you want to map to the email field:
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 KEYCLOAK ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Keycloak/Keycloak.md <== MAPPING INFO, REQUIRED
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
#- OAUTH2_CLIENT_ID=<Keycloak create Client ID>
|
||||
#- OAUTH2_SERVER_URL=<Keycloak server url - https://keycloak.example.com>
|
||||
#- OAUTH2_AUTH_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/auth
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/userinfo
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/token
|
||||
#- OAUTH2_SECRET=<keycloak client secret>
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_ID_MAP=sub
|
||||
#- OAUTH2_USERNAME_MAP=preferred_username
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
#- OAUTH2_FULLNAME_MAP=name
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 DOORKEEPER ====
|
||||
# https://github.com/wekan/wekan/issues/1874
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/OAuth2.md2
|
||||
# Enable the OAuth2 connection
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/blob/main/docs/Login/OAuth2.md
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# OAuth2 Client ID.
|
||||
#- OAUTH2_CLIENT_ID=abcde12345
|
||||
# OAuth2 Secret.
|
||||
#- OAUTH2_SECRET=54321abcde
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
# OAuth2 Server URL.
|
||||
#- OAUTH2_SERVER_URL=https://chat.example.com
|
||||
# OAuth2 Authorization Endpoint.
|
||||
#- OAUTH2_AUTH_ENDPOINT=/oauth/authorize
|
||||
# OAuth2 Userinfo Endpoint.
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
|
||||
# OAuth2 Token Endpoint.
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/oauth/token
|
||||
# OAUTH2 ID Token Whitelist Fields.
|
||||
#- OAUTH2_ID_TOKEN_WHITELIST_FIELDS=""
|
||||
# OAUTH2 Request Permissions.
|
||||
#- OAUTH2_REQUEST_PERMISSIONS=openid profile email
|
||||
# OAuth2 ID Mapping
|
||||
#- OAUTH2_ID_MAP=
|
||||
# OAuth2 Username Mapping
|
||||
#- OAUTH2_USERNAME_MAP=
|
||||
# OAuth2 Fullname Mapping
|
||||
#- OAUTH2_FULLNAME_MAP=
|
||||
# OAuth2 Email Mapping
|
||||
#- OAUTH2_EMAIL_MAP=
|
||||
#-----------------------------------------------------------------
|
||||
# ==== LDAP: UNCOMMENT ALL TO ENABLE LDAP ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/LDAP.md
|
||||
# For Snap settings see https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/Snap/Supported-settings-keys.md
|
||||
# Most settings work both on Snap and Docker below.
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#
|
||||
# The default authentication method used if a user does not exist to create and authenticate. Can be set as ldap.
|
||||
# (this is set properly in the Admin Panel, changing this item does not remove Password login option)
|
||||
#- DEFAULT_AUTHENTICATION_METHOD=ldap
|
||||
#
|
||||
# Enable or not the connection by the LDAP
|
||||
#- LDAP_ENABLE=true
|
||||
#
|
||||
# The port of the LDAP server
|
||||
#- LDAP_PORT=389
|
||||
#
|
||||
# The host server for the LDAP server
|
||||
#- LDAP_HOST=localhost
|
||||
#
|
||||
#-----------------------------------------------------------------
|
||||
# ==== LDAP AD Simple Auth ====
|
||||
#
|
||||
# Set to true, if you want to connect with Active Directory by Simple Authentication.
|
||||
# When using AD Simple Auth, LDAP_BASEDN is not needed.
|
||||
#
|
||||
# Example:
|
||||
#- LDAP_AD_SIMPLE_AUTH=true
|
||||
#
|
||||
# === LDAP User Authentication ===
|
||||
#
|
||||
# a) Option to login to the LDAP server with the user's own username and password, instead of
|
||||
# an administrator key. Default: false (use administrator key).
|
||||
#
|
||||
# b) When using AD Simple Auth, set to true, when login user is used for binding,
|
||||
# and LDAP_BASEDN is not needed.
|
||||
#
|
||||
# Example:
|
||||
#- LDAP_USER_AUTHENTICATION=true
|
||||
#
|
||||
# Which field is used to find the user for the user authentication. Default: uid.
|
||||
#- LDAP_USER_AUTHENTICATION_FIELD=uid
|
||||
#
|
||||
# === LDAP Default Domain ===
|
||||
#
|
||||
# a) In case AD SimpleAuth is configured, the default domain is appended to the given
|
||||
# loginname for creating the correct username for the bind request to AD.
|
||||
#
|
||||
# b) The default domain of the ldap it is used to create email if the field is not map
|
||||
# correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
#
|
||||
# Example :
|
||||
#- LDAP_DEFAULT_DOMAIN=mydomain.com
|
||||
#
|
||||
#-----------------------------------------------------------------
|
||||
# ==== LDAP BASEDN Auth ====
|
||||
#
|
||||
# The base DN for the LDAP Tree
|
||||
#- LDAP_BASEDN=ou=user,dc=example,dc=org
|
||||
#
|
||||
#-----------------------------------------------------------------
|
||||
# Fallback on the default authentication method
|
||||
#- LDAP_LOGIN_FALLBACK=false
|
||||
#
|
||||
# Reconnect to the server if the connection is lost
|
||||
#- LDAP_RECONNECT=true
|
||||
#
|
||||
# Overall timeout, in milliseconds
|
||||
#- LDAP_TIMEOUT=10000
|
||||
#
|
||||
# Specifies the timeout for idle LDAP connections in milliseconds
|
||||
#- LDAP_IDLE_TIMEOUT=10000
|
||||
#
|
||||
# Connection timeout, in milliseconds
|
||||
#- LDAP_CONNECT_TIMEOUT=10000
|
||||
#
|
||||
# If the LDAP needs a user account to search
|
||||
#- LDAP_AUTHENTIFICATION=true
|
||||
#
|
||||
# The search user DN - You need quotes when you have spaces in parameters
|
||||
# 2 examples:
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=CN=ldap admin,CN=users,DC=domainmatter,DC=lan
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=CN=wekan_adm,OU=serviceaccounts,OU=admin,OU=prod,DC=mydomain,DC=com
|
||||
#
|
||||
# The password for the search user
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=pwd
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD_FILE=/run/secrets/ldap_auth_password
|
||||
#
|
||||
# Enable logs for the module
|
||||
#- LDAP_LOG_ENABLED=true
|
||||
#
|
||||
# If the sync of the users should be done in the background
|
||||
#- LDAP_BACKGROUND_SYNC=false
|
||||
#
|
||||
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
|
||||
# The format must be as specified in:
|
||||
# https://bunkat.github.io/later/parsers.html#text
|
||||
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
|
||||
# At which interval does the background task sync in milliseconds.
|
||||
# Leave this unset, so it uses default, and does not crash.
|
||||
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
|
||||
- LDAP_BACKGROUND_SYNC_INTERVAL=''
|
||||
#
|
||||
#- LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false
|
||||
#
|
||||
#- LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false
|
||||
#
|
||||
# LDAP_BACKGROUND_SYNC_DISABLE_NONEXISTANT_USERS : When true, the LDAP
|
||||
# background sync disables users no longer present in LDAP (and re-enables
|
||||
# them when they reappear).
|
||||
#- LDAP_BACKGROUND_SYNC_DISABLE_NONEXISTANT_USERS=false
|
||||
#
|
||||
# If using LDAPS: LDAP_ENCRYPTION=ssl
|
||||
#- LDAP_ENCRYPTION=false
|
||||
#
|
||||
# The certification for the LDAPS server. Certificate needs to be included in this docker compose.yml file.
|
||||
#- LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+G2FIdAgIC...-----END CERTIFICATE-----
|
||||
#
|
||||
# Reject Unauthorized Certificate
|
||||
#- LDAP_REJECT_UNAUTHORIZED=false
|
||||
#
|
||||
# Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
|
||||
#- LDAP_USER_SEARCH_FILTER=
|
||||
#
|
||||
# base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
|
||||
#- LDAP_USER_SEARCH_SCOPE=one
|
||||
#
|
||||
# Which field is used to find the user, like uid / sAMAccountName
|
||||
#- LDAP_USER_SEARCH_FIELD=sAMAccountName
|
||||
#
|
||||
# Used for pagination (0=unlimited)
|
||||
#- LDAP_SEARCH_PAGE_SIZE=0
|
||||
#
|
||||
# The limit number of entries (0=unlimited)
|
||||
#- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
#
|
||||
# Enable the login restriction group filter. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
|
||||
# When true, only members of LDAP_GROUP_FILTER_GROUP_NAME are allowed to log in.
|
||||
# NOTE: This flag ONLY controls the login restriction. Admin status sync
|
||||
# (LDAP_SYNC_ADMIN_STATUS / LDAP_SYNC_ADMIN_GROUPS) and group->role sync
|
||||
# (LDAP_SYNC_GROUP_ROLES) query LDAP groups independently and do NOT require
|
||||
# this flag to be true. The group filter metadata below
|
||||
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
|
||||
# must still be configured for any group search to work.
|
||||
#- LDAP_GROUP_FILTER_ENABLE=false
|
||||
#
|
||||
# The object class for filtering. Example: group
|
||||
#- LDAP_GROUP_FILTER_OBJECTCLASS=
|
||||
#
|
||||
# The attribute of a group identifying it. Example: cn
|
||||
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
|
||||
#
|
||||
# The attribute inside a group object listing its members. Example: member
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
|
||||
#
|
||||
# The format of the value of LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE. Example: 'dn' if the users dn is saved as value into the attribute.
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
|
||||
#
|
||||
# The group name (id) that matches all users.
|
||||
#- LDAP_GROUP_FILTER_GROUP_NAME=
|
||||
#
|
||||
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier). Example: guid
|
||||
#- LDAP_UNIQUE_IDENTIFIER_FIELD=
|
||||
#
|
||||
# LDAP_UTF8_NAMES_SLUGIFY : Convert the username to utf8
|
||||
#- LDAP_UTF8_NAMES_SLUGIFY=true
|
||||
#
|
||||
# LDAP_USERNAME_FIELD : Which field contains the ldap username. username / sAMAccountName
|
||||
#- LDAP_USERNAME_FIELD=sAMAccountName
|
||||
#
|
||||
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname. fullname / sAMAccountName
|
||||
#- LDAP_FULLNAME_FIELD=fullname
|
||||
#
|
||||
#- LDAP_MERGE_EXISTING_USERS=false
|
||||
#
|
||||
# Allow existing account matching by e-mail address when username does not match
|
||||
#- LDAP_EMAIL_MATCH_ENABLE=true
|
||||
#
|
||||
# LDAP_EMAIL_MATCH_REQUIRE : require existing account matching by e-mail address when username does match
|
||||
#- LDAP_EMAIL_MATCH_REQUIRE=true
|
||||
#
|
||||
# LDAP_EMAIL_MATCH_VERIFIED : require existing account email address to be verified for matching
|
||||
#- LDAP_EMAIL_MATCH_VERIFIED=true
|
||||
#
|
||||
# LDAP_EMAIL_FIELD : which field contains the LDAP e-mail address
|
||||
#- LDAP_EMAIL_FIELD=mail
|
||||
#-----------------------------------------------------------------
|
||||
#- LDAP_SYNC_USER_DATA=false
|
||||
#
|
||||
#- LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
|
||||
#
|
||||
#- LDAP_SYNC_GROUP_ROLES=
|
||||
#
|
||||
# The default domain of the ldap it is used to create email if the field is not map correctly
|
||||
# with the LDAP_SYNC_USER_DATA_FIELDMAP is defined in setting LDAP_DEFAULT_DOMAIN above.
|
||||
#
|
||||
# Enable/Disable syncing of admin status based on ldap groups:
|
||||
# NOTE: Admin status sync and group->role sync (LDAP_SYNC_GROUP_ROLES) query
|
||||
# LDAP groups on their own. They no longer require LDAP_GROUP_FILTER_ENABLE=true,
|
||||
# which only controls the login restriction filter. The group filter metadata
|
||||
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
|
||||
# must still be configured for the group search to work.
|
||||
#- LDAP_SYNC_ADMIN_STATUS=true
|
||||
#
|
||||
# Comma separated list of admin group names to sync.
|
||||
#- LDAP_SYNC_ADMIN_GROUPS=group1,group2
|
||||
#
|
||||
# When true, sync a user's LDAP groups as Wekan Organizations.
|
||||
#- LDAP_SYNC_ORGANIZATIONS=false
|
||||
#
|
||||
# Comma separated allowlist of LDAP group names to sync as Organizations. Empty = all of the user's groups.
|
||||
#- LDAP_SYNC_ORGANIZATIONS_GROUPS=
|
||||
#
|
||||
# When true, sync a user's LDAP groups as Wekan Teams.
|
||||
#- LDAP_SYNC_TEAMS=false
|
||||
#
|
||||
# Comma separated allowlist of LDAP group names to sync as Teams. Empty = all of the user's groups.
|
||||
#- LDAP_SYNC_TEAMS_GROUPS=
|
||||
#---------------------------------------------------------------------
|
||||
# Login to LDAP automatically with HTTP header.
|
||||
# In below example for siteminder, at right side of = is header name.
|
||||
#- HEADER_LOGIN_ID=HEADERUID
|
||||
#- HEADER_LOGIN_FIRSTNAME=HEADERFIRSTNAME
|
||||
#- HEADER_LOGIN_LASTNAME=HEADERLASTNAME
|
||||
#- HEADER_LOGIN_EMAIL=HEADEREMAILADDRESS
|
||||
# SECURITY (GHSA-jggc-qvfc-jr6x): comma-separated allowlist of source IPs
|
||||
# permitted to use header login. The source IP is the real TCP peer of the
|
||||
# connection (your reverse proxy), NOT the spoofable X-Forwarded-For header.
|
||||
# REQUIRED when header login is enabled: if empty/unset, header login fails
|
||||
# CLOSED and authenticates no one.
|
||||
#- HEADER_LOGIN_TRUSTED_IPS=127.0.0.1,10.0.0.2
|
||||
# Optional: if WeKan sits behind MULTIPLE proxy hops, list the intermediate
|
||||
# proxy IPs here. X-Forwarded-For is then honored ONLY when the immediate
|
||||
# TCP peer is one of these trusted proxies, and the right-most hop that is
|
||||
# not itself a trusted proxy (the real client) is matched against
|
||||
# HEADER_LOGIN_TRUSTED_IPS above.
|
||||
#- HEADER_LOGIN_TRUSTED_PROXIES=10.0.0.1,10.0.0.2
|
||||
#---------------------------------------------------------------------
|
||||
# ==== LOGOUT TIMER, probably does not work yet ====
|
||||
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
|
||||
# example : LOGOUT_WITH_TIMER=true
|
||||
#- LOGOUT_WITH_TIMER=
|
||||
#
|
||||
# LOGOUT_IN : The number of days
|
||||
# example : LOGOUT_IN=1
|
||||
#- LOGOUT_IN=
|
||||
#
|
||||
# LOGOUT_ON_HOURS : The number of hours
|
||||
# example : LOGOUT_ON_HOURS=9
|
||||
#- LOGOUT_ON_HOURS=
|
||||
#
|
||||
# LOGOUT_ON_MINUTES : The number of minutes
|
||||
# example : LOGOUT_ON_MINUTES=55
|
||||
#- LOGOUT_ON_MINUTES=
|
||||
#-------------------------------------------------------------------
|
||||
# Hide password login form
|
||||
# - PASSWORD_LOGIN_ENABLED=true
|
||||
#-------------------------------------------------------------------
|
||||
#- CAS_ENABLED=true
|
||||
#- CAS_BASE_URL=https://cas.example.com/cas
|
||||
#- CAS_LOGIN_URL=https://cas.example.com/login
|
||||
#- CAS_VALIDATE_URL=https://cas.example.com/cas/p3/serviceValidate
|
||||
#---------------------------------------------------------------------
|
||||
#- SAML_ENABLED=true
|
||||
#- SAML_PROVIDER=
|
||||
#- SAML_ENTRYPOINT=
|
||||
#- SAML_ISSUER=
|
||||
#- SAML_CERT=
|
||||
#- SAML_IDPSLO_REDIRECTURL=
|
||||
#- SAML_PRIVATE_KEYFILE=
|
||||
#- SAML_PUBLIC_CERTFILE=
|
||||
#- SAML_IDENTIFIER_FORMAT=
|
||||
#- SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE=
|
||||
#- SAML_ATTRIBUTES=
|
||||
#---------------------------------------------------------------------
|
||||
# Wait spinner to use
|
||||
# - WAIT_SPINNER=Bounce
|
||||
#---------------------------------------------------------------------
|
||||
# Wait for MongoDB to be healthy (replica-set primary elected) before
|
||||
# starting WeKan, so index creation does not crash with "Topology is closed".
|
||||
# If you use FerretDB instead, comment out the wekandb block below and
|
||||
# uncomment the wekan-ferretdb block.
|
||||
depends_on:
|
||||
ferretdb:
|
||||
condition: service_started
|
||||
# wekan-migrate:
|
||||
# condition: service_completed_successfully
|
||||
# wekan-ferretdb:
|
||||
# condition: service_healthy
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-files:/data:rw
|
||||
|
||||
volumes:
|
||||
wekan-files:
|
||||
driver: local
|
||||
postgres-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
wekan-tier:
|
||||
driver: bridge
|
||||
@@ -1,82 +0,0 @@
|
||||
# WeKan with FerretDB 2 + PostgreSQL (DocumentDB extension)
|
||||
#
|
||||
# Use this compose file instead of docker-compose.yml on platforms where
|
||||
# MongoDB Community Edition is not available, or when you prefer PostgreSQL
|
||||
# as the backing store.
|
||||
#
|
||||
# Supported on all platforms where Node.js 22 runs:
|
||||
# linux/amd64, linux/arm64, linux/arm/v7, linux/ppc64le, linux/s390x
|
||||
#
|
||||
# Based on: docs/Databases/FerretDB2-PostgreSQL.md
|
||||
#
|
||||
# Setup:
|
||||
# docker compose -f docker-compose-ferretdb.yml up -d
|
||||
#
|
||||
# To stop:
|
||||
# docker compose -f docker-compose-ferretdb.yml stop
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
# PostgreSQL 17 with DocumentDB extension (FerretDB 2 backing store)
|
||||
# ghcr.io/ferretdb/postgres-documentdb is the official PostgreSQL image
|
||||
# pre-installed with the pg_documentdb extension required by FerretDB 2.
|
||||
postgres-documentdb:
|
||||
image: ghcr.io/ferretdb/postgres-documentdb:17
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ferretdb
|
||||
POSTGRES_PASSWORD: ${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}
|
||||
POSTGRES_DB: ferretdb
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- wekan-ferretdb
|
||||
|
||||
# FerretDB 2 — MongoDB-compatible protocol layer over PostgreSQL
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb:2
|
||||
restart: always
|
||||
environment:
|
||||
FERRETDB_POSTGRESQL_URL: postgres://ferretdb:${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}@postgres-documentdb/ferretdb
|
||||
ports:
|
||||
# Expose the MongoDB-compatible port only on localhost for security.
|
||||
# Remove "127.0.0.1:" prefix if WeKan runs on a different host.
|
||||
- 127.0.0.1:27017:27017
|
||||
depends_on:
|
||||
- postgres-documentdb
|
||||
networks:
|
||||
- wekan-ferretdb
|
||||
|
||||
# WeKan application
|
||||
wekan-app:
|
||||
image: ghcr.io/wekan/wekan:latest
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-ferretdb
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
# FerretDB speaks the MongoDB wire protocol on port 27017.
|
||||
# The database name "wekan" is created automatically on first run.
|
||||
MONGO_URL: mongodb://ferretdb:${FERRETDB_POSTGRES_PASSWORD:-ferretdb_secret}@ferretdb:27017/wekan?authMechanism=PLAIN
|
||||
ROOT_URL: ${ROOT_URL:-http://localhost:8080}
|
||||
PORT: 8080
|
||||
WITH_API: true
|
||||
WRITABLE_PATH: /data
|
||||
# Mail settings (optional)
|
||||
MAIL_URL: ${MAIL_URL:-}
|
||||
MAIL_FROM: ${MAIL_FROM:-}
|
||||
volumes:
|
||||
- wekan-data:/data
|
||||
depends_on:
|
||||
- ferretdb
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
wekan-data:
|
||||
|
||||
networks:
|
||||
wekan-ferretdb:
|
||||
driver: bridge
|
||||
File diff suppressed because it is too large.
Load diff
@@ -36,8 +36,6 @@
|
||||
# to a fresh value (5003, 5004, …). The MongoDB service and the
|
||||
# reverse proxy in front of WeKan don't need any change.
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
+87
-356
@@ -1,278 +1,77 @@
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== CREATING USERS AND LOGGING IN TO WEKAN ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Adding-users.md
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== FORGOT PASSWORD ====
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Forgot-Password.md
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== Upgrading Wekan to new version =====
|
||||
# NOTE: MongoDB has changed from 3.x to 4.x, in that case you need backup/restore with --noIndexRestore
|
||||
# see https://github.com/wekan/wekan/tree/main/docs/Backup
|
||||
# 1) Stop Wekan:
|
||||
# docker compose stop
|
||||
# 2) Remove old Wekan app (wekan-app only, not that wekan-db container that has all your data)
|
||||
# docker rm wekan-app
|
||||
# 3) Get newest docker-compose.yml from https://github.com/wekan/wekan to have correct image,
|
||||
# for example: "image: quay.io/wekan/wekan" or version tag "image: quay.io/wekan/wekan:v4.52"
|
||||
# 4) Start Wekan:
|
||||
# docker compose up -d
|
||||
#----------------------------------------------------------------------------------
|
||||
# ==== OPTIONAL: DEDICATED DOCKER USER ====
|
||||
# 1) Optionally create a dedicated user for Wekan, for example:
|
||||
# sudo useradd -d /home/wekan -m -s /bin/bash wekan
|
||||
# 2) Add this user to the docker group, then logout+login or reboot:
|
||||
# sudo usermod -aG docker wekan
|
||||
# 3) Then login as user wekan.
|
||||
# 4) Create this file /home/wekan/docker-compose.yml with your modifications.
|
||||
#----------------------------------------------------------------------------------
|
||||
# ==== RUN DOCKER AS SERVICE ====
|
||||
# 1a) Running Docker as service, on Systemd like Debian 9, Ubuntu 16.04, CentOS 7:
|
||||
# sudo systemctl enable docker
|
||||
# sudo systemctl start docker
|
||||
# 1b) Running Docker as service, on init.d like Debian 8, Ubuntu 14.04, CentOS 6:
|
||||
# sudo update-rc.d docker defaults
|
||||
# sudo service docker start
|
||||
# ----------------------------------------------------------------------------------
|
||||
# ==== USAGE OF THIS docker-compose.yml ====
|
||||
# 1) For seeing does Wekan work, try this and check with your web browser:
|
||||
# docker compose up
|
||||
# 2) Stop Wekan and start Wekan in background:
|
||||
# docker compose stop
|
||||
# docker compose up -d
|
||||
# 3) See running Docker containers:
|
||||
# docker ps
|
||||
# 4) Stop Docker containers:
|
||||
# docker compose stop
|
||||
# ----------------------------------------------------------------------------------
|
||||
# ===== INSIDE DOCKER CONTAINERS, AND BACKUP/RESTORE ====
|
||||
# https://github.com/wekan/wekan/tree/main/docs/Backup
|
||||
# If really necessary, repair MongoDB: https://github.com/wekan/wekan-mongodb/issues/6#issuecomment-424004116
|
||||
# 1) Going inside containers:
|
||||
# a) Wekan app, does not contain data
|
||||
# docker exec -it wekan-app bash
|
||||
# b) MongoDB, contains all data
|
||||
# docker exec -it wekan-db bash
|
||||
# 2) Copying database to outside of container:
|
||||
# docker exec -it wekan-db bash
|
||||
# cd /data
|
||||
# mongodump
|
||||
# exit
|
||||
# docker cp wekan-db:/data/dump .
|
||||
# 3) Restoring database
|
||||
# # 1) Stop wekan
|
||||
# docker stop wekan-app
|
||||
# # 2) Go inside database container
|
||||
# docker exec -it wekan-db bash
|
||||
# # 3) and data directory
|
||||
# cd /data
|
||||
# # 4) Remove previous dump
|
||||
# rm -rf dump
|
||||
# # 5) Exit db container
|
||||
# exit
|
||||
# # 6) Copy dump to inside docker container
|
||||
# docker cp dump wekan-db:/data/
|
||||
# # 7) Go inside database container
|
||||
# docker exec -it wekan-db bash
|
||||
# # 8) and data directory
|
||||
# cd /data
|
||||
# # 9) Restore
|
||||
# mongorestore --drop
|
||||
# # 10) Exit db container
|
||||
# exit
|
||||
# # 11) Start wekan
|
||||
# docker start wekan-app
|
||||
#-------------------------------------------------------------------------
|
||||
# This docker-compose.yml uses FerretDB v1 with SQLite from
|
||||
# https://github.com/wekan/FerretDB by default.
|
||||
#
|
||||
# WeKan with FerretDB v1 + embedded SQLite (no PostgreSQL, no MongoDB)
|
||||
#
|
||||
# This is the DEFAULT WeKan compose file (docker-compose.yml): a light,
|
||||
# self-contained database — FerretDB v1 (the wekan/FerretDB fork,
|
||||
# https://github.com/wekan/FerretDB) with its embedded pure-Go SQLite backend,
|
||||
# which speaks the MongoDB wire protocol. There is no separate PostgreSQL or
|
||||
# MongoDB server — FerretDB stores everything in SQLite files on a volume, and
|
||||
# WeKan stores attachments/avatars on the filesystem.
|
||||
# For the classic MongoDB 7 option instead, see docker-compose-mongodb-v7.yml;
|
||||
# for the PostgreSQL-backed FerretDB 2 option, see
|
||||
# docker-compose-ferretdb-v2-postgresql.yml.
|
||||
#
|
||||
# The FerretDB binary is downloaded (once, then cached on the volume) from the
|
||||
# newest wekan/FerretDB release: https://github.com/wekan/FerretDB/releases
|
||||
# Each platform is a separate ferretdb-<arch> asset there; the container downloads
|
||||
# only the one matching its architecture (amd64, arm64, ppc64le, s390x, riscv64, ...).
|
||||
#
|
||||
# Setup (this is the default docker-compose.yml, so no -f is needed):
|
||||
# docker compose up -d
|
||||
#
|
||||
# To stop:
|
||||
# docker compose stop
|
||||
|
||||
services:
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== DATABASE: FerretDB 2 + PostgreSQL 16 ====
|
||||
# FerretDB is a MongoDB-compatible proxy that stores data in PostgreSQL.
|
||||
# It supports the full MongoDB wire protocol used by Meteor/WeKan,
|
||||
# including OpLog via replication-set emulation (--replication-set-name=rs0).
|
||||
#
|
||||
# Upgrade path from MongoDB:
|
||||
# The wekan-migrate service below runs ONCE before wekan starts.
|
||||
# It copies all data from an existing MongoDB instance (if reachable at
|
||||
# SOURCE_MONGO_URL) to FerretDB, upgrades the schema to v8.43, and
|
||||
# moves GridFS / CollectionFS binary files to the filesystem.
|
||||
# Progress is visible in a browser while migration runs.
|
||||
#
|
||||
# If you are upgrading from a MongoDB-based install:
|
||||
# 1. Keep your old MongoDB container running (or start it separately).
|
||||
# 2. Set SOURCE_MONGO_URL in the wekan-migrate environment below.
|
||||
# 3. Run: docker compose up -d
|
||||
# 4. Watch migration at http://localhost:8080/ until "completed".
|
||||
# 5. Shut down old MongoDB once satisfied.
|
||||
#
|
||||
# If this is a fresh install, wekan-migrate exits instantly (nothing to migrate).
|
||||
#-------------------------------------------------------------------------------------
|
||||
#
|
||||
#wekan-postgres:
|
||||
# image: postgres:16-alpine
|
||||
# container_name: wekan-postgres
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# expose:
|
||||
# - 5432
|
||||
# environment:
|
||||
# - POSTGRES_DB=wekan
|
||||
# - POSTGRES_USER=wekan
|
||||
# - POSTGRES_PASSWORD=wekan
|
||||
# # Tune for a kanban workload (low connection count, moderate write rate)
|
||||
# - POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
|
||||
# volumes:
|
||||
# - /etc/localtime:/etc/localtime:ro
|
||||
# - wekan-postgres:/var/lib/postgresql/data
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -U wekan -d wekan"]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
#
|
||||
#wekan-ferretdb:
|
||||
# image: ghcr.io/ferretdb/ferretdb:2
|
||||
# container_name: wekan-ferretdb
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# expose:
|
||||
# - 27017
|
||||
# environment:
|
||||
# # FerretDB stores everything in PostgreSQL
|
||||
# - FERRETDB_POSTGRESQL_URL=postgresql://wekan:wekan@wekan-postgres:5432/wekan
|
||||
# # Emulate MongoDB replica-set for Meteor OpLog (real-time updates)
|
||||
# - FERRETDB_REPLICATION_SET_NAME=rs0
|
||||
# # Suppress verbose debug output
|
||||
# - FERRETDB_LOG_LEVEL=warn
|
||||
# depends_on:
|
||||
# wekan-postgres:
|
||||
# condition: service_healthy
|
||||
# healthcheck:
|
||||
# test: ["CMD", "ferretdb", "--version"]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
#
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== MIGRATION SERVICE ====
|
||||
# Runs once, before WeKan starts. Exits 0 on success, 1 on fatal error.
|
||||
# WeKan only starts after this service exits successfully
|
||||
# (depends_on condition: service_completed_successfully).
|
||||
#
|
||||
# On a FRESH install (no SOURCE_MONGO_URL or source unreachable) it exits
|
||||
# immediately — no penalty for new deployments.
|
||||
#
|
||||
# SOURCE_MONGO_URL: point to your old MongoDB container/host.
|
||||
# e.g. mongodb://wekandb:27017/wekan
|
||||
# mongodb://192.168.1.10:27017/wekan
|
||||
# Leave it unset or empty to skip migration.
|
||||
#-------------------------------------------------------------------------------------
|
||||
#wekan-migrate:
|
||||
# image: node:24.17.0-slim
|
||||
# container_name: wekan-migrate
|
||||
# restart: "no"
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# working_dir: /migrate
|
||||
# # Install the mongodb driver then run the migration script.
|
||||
# # npm ci is used if package-lock.json exists; npm install otherwise.
|
||||
# command: >
|
||||
# sh -c "
|
||||
# cd /migrate &&
|
||||
# npm install --omit=dev --prefer-offline 2>/dev/null || npm install --omit=dev &&
|
||||
# node migrate-mongodb-to-ferretdb.mjs
|
||||
# "
|
||||
# environment:
|
||||
# # ---- Source (old MongoDB) ----
|
||||
# # Set to your old MongoDB URL to trigger data migration.
|
||||
# # Leave empty for a fresh install (migration is skipped automatically).
|
||||
# - SOURCE_MONGO_URL=
|
||||
# #- SOURCE_MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
#
|
||||
# # ---- Target (FerretDB) ----
|
||||
# - TARGET_MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
#
|
||||
# # ---- File storage ----
|
||||
# # Must match the WRITABLE_PATH used by the wekan service.
|
||||
# - WRITABLE_PATH=/data
|
||||
#
|
||||
# # ---- Progress dashboard ----
|
||||
# # Browse to http://localhost:8080/ while migration runs.
|
||||
# - MIGRATION_PORT=8080
|
||||
#
|
||||
# # ---- Tuning (optional) ----
|
||||
# #- BATCH_SIZE=200
|
||||
# #- DRY_RUN=true # Log only, no writes — useful for testing
|
||||
# #- FORCE_MIGRATE=true # Re-run even if already marked complete
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
# depends_on:
|
||||
# wekan-ferretdb:
|
||||
# condition: service_healthy
|
||||
# volumes:
|
||||
# - /etc/localtime:/etc/localtime:ro
|
||||
# - wekan-files:/data:rw
|
||||
# - ./releases:/migrate:ro
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== LEGACY MONGODB (commented out) ====
|
||||
# Uncomment to keep old MongoDB running alongside FerretDB during migration.
|
||||
# After migration is verified, remove this block entirely.
|
||||
#-------------------------------------------------------------------------------------
|
||||
wekandb:
|
||||
image: mongo:7
|
||||
container_name: wekan-db
|
||||
# FerretDB v1 with the embedded SQLite backend (MongoDB-compatible on :27017).
|
||||
# A tiny Debian container fetches the wekan/FerretDB v1 release binary for its
|
||||
# architecture (cached on the volume so restarts do not re-download) and runs it.
|
||||
# SQLite data lives in /data/sqlite on the ferretdb-data volume.
|
||||
ferretdb:
|
||||
image: debian:bookworm-slim
|
||||
container_name: wekan-ferretdb
|
||||
restart: always
|
||||
#command: mongod --logpath /dev/null --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --quiet
|
||||
#command: mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --quiet
|
||||
command: >
|
||||
sh -c '
|
||||
mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 32 --timeZoneInfo /usr/share/zoneinfo --setParameter logicalSessionRefreshMillis=900000 --setParameter localLogicalSessionTimeoutMinutes=45 --oplogSize 20480 --replSet rs0 --bind_ip_all --quiet &
|
||||
until mongosh --host 127.0.0.1 --quiet --eval "
|
||||
try {
|
||||
const status = rs.status();
|
||||
quit(status.ok === 1 ? 0 : 1);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.codeName === \"NotYetInitialized\" ||
|
||||
e.message.includes(\"no replset config has been received\") ||
|
||||
e.message.includes(\"not yet initialized\")
|
||||
) {
|
||||
rs.initiate({
|
||||
_id: \"rs0\",
|
||||
members: [{ _id: 0, host: \"wekandb:27017\" }]
|
||||
});
|
||||
quit(1);
|
||||
}
|
||||
quit(1);
|
||||
}
|
||||
" >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
wait
|
||||
'
|
||||
environment:
|
||||
DO_NOT_TRACK: "1"
|
||||
FERRETDB_TELEMETRY: disable
|
||||
# Pin a specific release instead of the newest by setting e.g.
|
||||
# FERRETDB_RELEASE=download/v1.24.2
|
||||
FERRETDB_RELEASE: ${FERRETDB_RELEASE:-latest/download}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
BIN=/data/bin/ferretdb
|
||||
if [ ! -x "$$BIN" ]; then
|
||||
echo "Fetching FerretDB v1 from the wekan/FerretDB release ..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends curl ca-certificates >/dev/null
|
||||
arch="$$(dpkg --print-architecture)"
|
||||
case "$$arch" in ppc64el) arch=ppc64le ;; esac
|
||||
mkdir -p /data/bin
|
||||
# Download only this arch's binary (per-arch release assets, no ferretdb.zip).
|
||||
curl -fSL --retry 5 -o "$$BIN" \
|
||||
"https://github.com/wekan/FerretDB/releases/$${FERRETDB_RELEASE}/ferretdb-$${arch}"
|
||||
chmod +x "$$BIN"
|
||||
fi
|
||||
mkdir -p /data/files/db
|
||||
exec "$$BIN" \
|
||||
--handler=sqlite \
|
||||
--sqlite-url=file:/data/files/db/ \
|
||||
--listen-addr=0.0.0.0:27017 \
|
||||
--telemetry=disable \
|
||||
--log-level=error
|
||||
volumes:
|
||||
- ferretdb-data:/data
|
||||
networks:
|
||||
- wekan-tier
|
||||
expose:
|
||||
- 27017
|
||||
# Healthcheck: report healthy only once the replica set has an elected
|
||||
# primary, so the wekan app (depends_on: condition: service_healthy) does
|
||||
# not start before MongoDB is ready and crash with "Topology is closed".
|
||||
# start_period gives a large database time to replay its journal after an
|
||||
# upgrade before failures are counted.
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-db:/data/db
|
||||
- wekan-db-dump:/dump
|
||||
# Expose only on localhost by default; remove to keep it internal to the network.
|
||||
ports:
|
||||
- 127.0.0.1:27017:27017
|
||||
|
||||
wekan:
|
||||
#-------------------------------------------------------------------------------------
|
||||
@@ -326,9 +125,12 @@ services:
|
||||
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
|
||||
# Keep oplog/polling fallback available for deployments that cannot use Change Streams:
|
||||
# https://github.com/wekan/wekan/issues/6307#issuecomment-4299349231
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- DDP_TRANSPORT=uws
|
||||
#- DDP_TRANSPORT=sockjs
|
||||
# FerretDB has no MongoDB change streams / replica-set oplog, so use polling:
|
||||
- DEFAULT_METEOR_REACTIVITY_ORDER=polling
|
||||
- METEOR_REACTIVITY_ORDER=polling
|
||||
# uws does not work at s390x. sockjs works.
|
||||
#- DDP_TRANSPORT=uws
|
||||
- DDP_TRANSPORT=sockjs
|
||||
#-----------------------------------------------------------------
|
||||
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
|
||||
- WRITABLE_PATH=/data
|
||||
@@ -344,7 +146,7 @@ services:
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
|
||||
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
- MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
- MONGO_URL=mongodb://ferretdb:27017/wekan
|
||||
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
|
||||
#---------------------------------------------------------------
|
||||
# ==== ROOT_URL SETTING ====
|
||||
@@ -395,7 +197,7 @@ services:
|
||||
# FerretDB emulates OpLog via --replication-set-name=rs0.
|
||||
# HIGHLY RECOMMENDED — enables real-time DDP updates (sub-100ms latency).
|
||||
#- MONGO_OPLOG_URL=mongodb://wekan-ferretdb:27017/local?replicaSet=rs0
|
||||
- MONGO_OPLOG_URL=mongodb://wekandb:27017/local?replicaSet=rs0
|
||||
# MONGO_OPLOG_URL intentionally unset: FerretDB v1 has no replica-set oplog (WeKan uses polling).
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
|
||||
# https://github.com/edemaine/kadira-compose
|
||||
@@ -454,6 +256,14 @@ services:
|
||||
# https://github.com/wekan/wekan/pull/2560
|
||||
- RICHER_CARD_COMMENT_EDITOR=false
|
||||
#---------------------------------------------------------------
|
||||
# ==== CARD LOADING: all (default) or lazy ====
|
||||
# 'all' = load every card of a board into the browser (default).
|
||||
# 'lazy' = each list loads only the cards currently visible
|
||||
# (infinite scroll) plus a live count, so boards with
|
||||
# thousands of cards use less memory/bandwidth.
|
||||
# Also changeable at runtime in Admin Panel / Features.
|
||||
- CARDS_LOADING=all
|
||||
#---------------------------------------------------------------
|
||||
# ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
|
||||
# https://github.com/wekan/wekan/issues/2518
|
||||
- CARD_OPENED_WEBHOOK_ENABLED=false
|
||||
@@ -976,8 +786,8 @@ services:
|
||||
# If you use FerretDB instead, comment out the wekandb block below and
|
||||
# uncomment the wekan-ferretdb block.
|
||||
depends_on:
|
||||
wekandb:
|
||||
condition: service_healthy
|
||||
ferretdb:
|
||||
condition: service_started
|
||||
# wekan-migrate:
|
||||
# condition: service_completed_successfully
|
||||
# wekan-ferretdb:
|
||||
@@ -985,92 +795,13 @@ services:
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-files:/data:rw
|
||||
# Secrets are optional. Uncomment below and ensure files exist to use them.
|
||||
#secrets:
|
||||
# - ldap_auth_password
|
||||
# - oauth2_secret
|
||||
# - mail_service_password
|
||||
# - mongo_password
|
||||
# - s3_secret
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
|
||||
# When using Wekan both at office LAN and remote VPN:
|
||||
# 1) Have above Wekan docker container config with LAN IP address
|
||||
# 2) Copy all of above wekan container config below, look above of this part above and all config below it,
|
||||
# before above depends_on: part:
|
||||
#
|
||||
# wekan:
|
||||
# #-------------------------------------------------------------------------------------
|
||||
# # ==== MONGODB AND METEOR VERSION ====
|
||||
# # a) For Wekan Meteor 1.8.x version at meteor-1.8 branch, .....
|
||||
#
|
||||
#
|
||||
# and change name to different name like wekan2 or wekanvpn, and change ROOT_URL to server VPN IP
|
||||
# address.
|
||||
# 3) This way both Wekan containers can use same MongoDB database
|
||||
# and see the same Wekan boards.
|
||||
# 4) You could also add 3rd Wekan container for 3rd network etc.
|
||||
# EXAMPLE:
|
||||
# wekan2:
|
||||
# ....COPY CONFIG FROM ABOVE TO HERE...
|
||||
# environment:
|
||||
# - ROOT_URL='http://10.10.10.10'
|
||||
# ...COPY CONFIG FROM ABOVE TO HERE...
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
# OPTIONAL NGINX CONFIG FOR REVERSE PROXY
|
||||
# nginx:
|
||||
# image: nginx
|
||||
# container_name: nginx
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# depends_on:
|
||||
# - wekan
|
||||
# ports:
|
||||
# - 80:80
|
||||
# - 443:443
|
||||
# volumes:
|
||||
# - ./nginx/ssl:/etc/nginx/ssl/:ro
|
||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
## Alternative volume config:
|
||||
## volumes:
|
||||
## - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
## - ./nginx/ssl/ssl.conf:/etc/nginx/conf.d/ssl/ssl.conf:ro
|
||||
## - ./nginx/ssl/testvm-ehu.crt:/etc/nginx/conf.d/ssl/certs/mycert.crt:ro
|
||||
## - ./nginx/ssl/testvm-ehu.key:/etc/nginx/conf.d/ssl/certs/mykey.key:ro
|
||||
## - ./nginx/ssl/pphrase:/etc/nginx/conf.d/ssl/pphrase:ro
|
||||
|
||||
volumes:
|
||||
wekan-files:
|
||||
driver: local
|
||||
#wekan-postgres:
|
||||
# driver: local
|
||||
# Legacy MongoDB volumes — kept so existing data survives until migration is done.
|
||||
# Remove once migration is verified and old MongoDB is decommissioned.
|
||||
wekan-db:
|
||||
driver: local
|
||||
wekan-db-dump:
|
||||
ferretdb-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
wekan-tier:
|
||||
driver: bridge
|
||||
|
||||
# Docker Compose Secrets
|
||||
# Create secret files on the host system before running docker-compose up
|
||||
# Example: echo "your_password_here" > ldap_auth_password.txt
|
||||
# Then use: docker-compose up -d
|
||||
# Secrets are optional. Uncomment to enable and ensure files exist at provided paths.
|
||||
#secrets:
|
||||
# ldap_auth_password:
|
||||
# file: ./secrets/ldap_auth_password.txt
|
||||
# oauth2_secret:
|
||||
# file: ./secrets/oauth2_secret.txt
|
||||
# mail_service_password:
|
||||
# file: ./secrets/mail_service_password.txt
|
||||
# mongo_password:
|
||||
# file: ./secrets/mongo_password.txt
|
||||
# s3_secret:
|
||||
# file: ./secrets/s3_secret.txt
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## WeKan
|
||||
|
||||
- Alternatively, use wekan-app Docker container from https://raw.githubusercontent.com/wekan/wekan/refs/heads/main/docker-compose.yml
|
||||
- Alternatively, use the ready-made FerretDB 2 + PostgreSQL compose file from https://raw.githubusercontent.com/wekan/wekan/refs/heads/main/docker-compose-ferretdb-v2-postgresql.yml (the default docker-compose.yml uses FerretDB v1 with SQLite instead)
|
||||
|
||||
```
|
||||
git clone --branch main --depth 1 https://github.com/wekan/wekan.git
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '2'
|
||||
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== CREATING USERS AND LOGGING IN TO WEKAN ====
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# Alternative Architectures, JavaScript Engines and Rewrite Options
|
||||
|
||||
This page documents which CPU architectures WeKan can run on, **why** some
|
||||
architectures are not supported, and what the realistic options are for
|
||||
supporting more of them — alternative JavaScript engines and full server
|
||||
rewrites (Go, Tcl/Tk, QuickJS, …).
|
||||
|
||||
It exists because the question keeps coming up: *"WeKan won't run on my 32-bit
|
||||
ARM board / old x86 / ReactOS — can we fix that?"* The short answer is that the
|
||||
limit is **Node.js**, not the database, and getting past it means either
|
||||
compiling Node from source (where still possible) or rewriting the server in a
|
||||
portable language.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- WeKan's **server** is Meteor 3, which runs **only on Node.js 24**. The browser
|
||||
client is architecture-independent. So the supported-architecture list is
|
||||
exactly *"where is there a Node.js 24 build?"*.
|
||||
- The **database is not the bottleneck**: [FerretDB v1](https://github.com/wekan/FerretDB)
|
||||
(pure Go, SQLite backend, MongoDB wire protocol) cross-compiles to far more
|
||||
architectures than Node does.
|
||||
- **Node.js 24 dropped 32-bit**: no `armv7l`/`armhf` binaries (downgraded to
|
||||
*Experimental*, source-build only), no 32-bit x86 Linux (`i386`) at all.
|
||||
- **Other JS runtimes do not help**: Deno and Bun cover *fewer* architectures
|
||||
than Node (x64/arm64 only). Portable engines (QuickJS, JavaScriptCore,
|
||||
SpiderMonkey, JVM-based Rhino/GraalJS) *do* run on 32-bit, but **none of them
|
||||
run Meteor/WeKan** — Meteor is locked to Node.
|
||||
- If universal architecture coverage is the goal, a **Go server** (matching
|
||||
FerretDB) is the best target; **Tcl/Tk and QuickJS are worse fits** for a web
|
||||
app. All of these are large rewrites, not drop-in swaps.
|
||||
|
||||
## The two layers
|
||||
|
||||
Keep these separate — they have very different portability:
|
||||
|
||||
| Layer | Technology | Portability |
|
||||
| --- | --- | --- |
|
||||
| Client | HTML/CSS/JS in the browser | Architecture-independent (runs wherever a browser runs) |
|
||||
| **Server** | **Meteor 3 → Node.js 24** | **Only where a Node.js 24 build exists** |
|
||||
| Database | MongoDB *or* FerretDB v1 (Go) | MongoDB: amd64/arm64 only. FerretDB: almost everything |
|
||||
|
||||
Only the **server binary** (the Node.js runtime) constrains the architecture.
|
||||
|
||||
## Architecture support matrix
|
||||
|
||||
"Node 24" is what limits WeKan; "FerretDB v1" shows the database is available
|
||||
well beyond that. Sources:
|
||||
[nodejs.org/dist/latest-v24.x](https://nodejs.org/dist/latest-v24.x/) (official),
|
||||
[unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org/download/release/)
|
||||
(extra arches), and [wekan/FerretDB releases](https://github.com/wekan/FerretDB/releases).
|
||||
|
||||
| Arch | Node.js 24 | MongoDB | FerretDB v1 | WeKan runs? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| amd64 (x64) | ✅ official | ✅ | ✅ | ✅ |
|
||||
| arm64 (aarch64) | ✅ official | ✅ | ✅ | ✅ |
|
||||
| ppc64le | ✅ official | ❌ | ✅ | ✅ (FerretDB) |
|
||||
| s390x | ✅ official | ❌ | ✅ | ✅ (FerretDB) |
|
||||
| riscv64 | ✅ unofficial (glibc) | ❌ | ✅ | ✅ (FerretDB) |
|
||||
| **loong64** | ✅ unofficial (glibc) | ❌ | ✅ | ⚠️ binaries exist, but **not buildable in CI** — see below |
|
||||
| **armhf / armv7l** | ❌ (source-build only, Experimental) | ❌ | ✅ | ❌ (no Node binary) |
|
||||
| **armel (armv5)** | ❌ | ❌ | ✅ | ❌ (no Node binary) |
|
||||
| **i386 (32-bit x86 Linux)** | ❌ (port removed) | ❌ | ✅ | ❌ (no Node binary) |
|
||||
|
||||
### Why `armhf` / `armv7l` is not supported
|
||||
|
||||
`armhf` = `armv7l` = 32-bit ARMv7 hard-float (the same thing; the `l` is
|
||||
little-endian; Debian calls it `armhf`, Docker calls it `linux/arm/v7`).
|
||||
|
||||
Node.js **downgraded ARMv7 to "Experimental" in v24** ("Downgraded as of Node.js
|
||||
24" in Node's `BUILDING.md`). That means the source still compiles for ARMv7, but
|
||||
**no binaries are published or tested**. Neither nodejs.org nor
|
||||
unofficial-builds ship an `armv7l` tarball for v24, and NodeSource does not ship
|
||||
`armhf` for the 24.x line. Distributions (Debian, Alpine) build Node from source
|
||||
for `armhf`, but their versions lag behind 24.x and are their own (often musl)
|
||||
builds, not the portable glibc tarball WeKan's self-contained bundle embeds.
|
||||
|
||||
→ The only way to get an `armhf`/`armv7l` WeKan bundle is to **compile Node 24
|
||||
from source** in CI (slow under emulation, Experimental tier, unsupported).
|
||||
|
||||
### Why `i386` (32-bit x86 Linux) is not supported
|
||||
|
||||
Node.js **removed the 32-bit x86 Linux port** years ago. There is no binary and
|
||||
no practical source build. So 32-bit x86 Linux cannot run current WeKan at all,
|
||||
regardless of effort. (FerretDB has an `i386` binary, but there is nothing to run
|
||||
the WeKan server with.)
|
||||
|
||||
### Why `loong64` is not built in CI (even though binaries exist)
|
||||
|
||||
LoongArch64 has both a Node 24 unofficial-builds binary **and** a FerretDB v1
|
||||
binary, so in principle WeKan could run on it. But the release workflow cannot
|
||||
**build** the bundle/Docker image for it:
|
||||
|
||||
1. **QEMU cannot emulate LoongArch.** `docker/setup-qemu-action`
|
||||
([tonistiigi/binfmt](https://github.com/tonistiigi/binfmt)) supports
|
||||
`amd64, arm64, riscv64, ppc64le, s390x, 386, arm/v7, arm/v6` — **no
|
||||
loong64**. The extra-arch bundles rebuild native modules (e.g. bcrypt) by
|
||||
running an emulated container of the target arch; with no emulator, that step
|
||||
is impossible.
|
||||
2. **No official base image.** Ubuntu/Debian do not publish an official
|
||||
`linux/loong64` container image (LoongArch is a debian-ports / community
|
||||
architecture), so there is nothing to build inside.
|
||||
|
||||
FerretDB's own image builds for `linux/loong64` only because Go **cross-compiles**
|
||||
from the amd64 builder (it never runs loong64 code at build time). WeKan's native
|
||||
`.node` addons would instead need a real LoongArch build environment — a
|
||||
`loongarch64-linux-gnu` cross-toolchain plus a (non-mainstream) loong64 runtime
|
||||
image. That is significantly more work than the riscv64 path and is why loong64
|
||||
is currently left out.
|
||||
|
||||
## Alternative JavaScript engines
|
||||
|
||||
The engine itself (V8) *can* target 32-bit — the problem is that the **Node.js
|
||||
project stopped shipping those binaries** and **Meteor is bound to Node**.
|
||||
Swapping the engine does not run Meteor.
|
||||
|
||||
| Engine / runtime | armhf / armv7 | i386 | Runs Meteor/WeKan? |
|
||||
| --- | --- | --- | --- |
|
||||
| **Node.js 24** (V8) | source-build only (exp.) | ❌ | — (current) |
|
||||
| **Deno** (V8, Rust) | ❌ x64/arm64 only | ❌ | ❌ |
|
||||
| **Bun** (JavaScriptCore, Zig) | ❌ x64/arm64 only | ❌ | ❌ |
|
||||
| **QuickJS** (Bellard, pure C) | ✅ any arch | ✅ | ❌ (no Node API / npm) |
|
||||
| **Duktape / MuJS / JerryScript** | ✅ | ✅ | ❌ (ES5, embeddable) |
|
||||
| **JavaScriptCore / SpiderMonkey** | ✅ (WebKit/Firefox build there) | ✅ | ❌ |
|
||||
| **Rhino / GraalJS** (on a JVM) | ✅ (OpenJDK armhf/i386) | ✅ | ❌ |
|
||||
|
||||
Two takeaways:
|
||||
|
||||
- The modern Node alternatives, **Deno and Bun, are *worse*** for architecture
|
||||
coverage (x64/arm64 only) — they do not help with 32-bit.
|
||||
- Several engines (**QuickJS, JavaScriptCore, SpiderMonkey, JVM-based**) *do* run
|
||||
on `armhf`/`i386`, but **none run Meteor/WeKan**. Meteor requires Node.js
|
||||
specifically (V8 + libuv + Node's native APIs + Meteor's Node-based build), so
|
||||
using another engine means rewriting the server for that runtime — as large a
|
||||
job as a language rewrite.
|
||||
|
||||
## Full server rewrite options
|
||||
|
||||
If the goal is to run on architectures Node cannot reach, the server has to be
|
||||
rewritten in a portable language. The browser client stays as-is; the new server
|
||||
just serves the SPA, the API, reactive sync, accounts and methods, talking to
|
||||
FerretDB/MongoDB.
|
||||
|
||||
### Go (recommended for portability)
|
||||
|
||||
- **Cross-compiles everywhere** — `i386`, `armv7`, `armhf`, `armel`, `loong64`,
|
||||
`mips`, … exactly as [FerretDB](https://github.com/wekan/FerretDB) already does,
|
||||
as a single static binary with no Node-ABI or native-module problems.
|
||||
- Speaks the **MongoDB wire protocol natively** to FerretDB/MongoDB.
|
||||
- Mature HTTP/WebSocket ecosystem for the API and reactive sync.
|
||||
- **Cost:** this is effectively a **different product** — Meteor's DDP, reactive
|
||||
pub/sub, accounts, methods and client sync all have to be rebuilt. Very large
|
||||
effort.
|
||||
|
||||
### QuickJS (the "keep JavaScript" option)
|
||||
|
||||
- Pure C, compiles on virtually **any architecture**, tiny footprint.
|
||||
- **Cost:** no npm ecosystem and no Node API, so the server is written against a
|
||||
minimal runtime — about as much work as the Go rewrite, with a smaller
|
||||
ecosystem and weaker performance. Niche.
|
||||
|
||||
### Tcl/Tk (not recommended for this)
|
||||
|
||||
- **Tcl** is extremely portable (pure C, builds on any arch: `i386`, `armv7`,
|
||||
`armhf`, `armv5`, `mips`, …).
|
||||
- **But Tk is a desktop GUI toolkit**, not a web-server stack. WeKan is a
|
||||
**web application** (browser SPA + server + database); Tk would produce a
|
||||
native desktop window — a completely different product and UX.
|
||||
- Tcl *can* run a web server (NaviServer/AOLserver, tclhttpd, Wapp), but it is a
|
||||
niche stack with no reactive-SPA story. For a web app this is a **step
|
||||
backwards** compared to Go.
|
||||
|
||||
### Comparison
|
||||
|
||||
| Option | Arch coverage | Effort | Fit for WeKan (web app) |
|
||||
| --- | --- | --- | --- |
|
||||
| Compile Node 24 from source (armv7 only) | + armv7 (not i386) | Medium, fragile, Experimental | Keeps Meteor; no rewrite |
|
||||
| **Go server** | Universal (like FerretDB) | Very large (full rewrite) | Best; static binary, native FerretDB |
|
||||
| QuickJS server | Universal | Very large | OK; keeps JS, small ecosystem |
|
||||
| Tcl/Tk | Universal (Tcl) | Very large | Poor; GUI/scripting, not a web stack |
|
||||
|
||||
## Hardware / OS notes
|
||||
|
||||
These are about the **FerretDB (Go) binary** on specific targets; remember the
|
||||
WeKan *server* still needs a Node.js 24 build, which is the actual blocker above.
|
||||
|
||||
- **ODroid-U3 (ARMv7-A, Linux)** — FerretDB `armhf` (GOARM=7) runs fine. The
|
||||
board has enough RAM for FerretDB v1; only the missing Node 24 `armhf` binary
|
||||
stops WeKan itself.
|
||||
- **Raspberry Pi OS 32-bit** — FerretDB `armhf` (v7) runs on Pi 2/3/4 (ARMv7);
|
||||
Pi 1/Zero (ARMv6) need the `armel` (GOARM=5) binary. Again, the WeKan server is
|
||||
blocked only by Node.
|
||||
- **ReactOS 32-bit** — the matching binary would be FerretDB `win32`
|
||||
(windows/386), but it is **doubtful**: Go 1.21+ targets Windows 10 / Server
|
||||
2016-level APIs, while ReactOS is around XP/2003, so Go's runtime may use APIs
|
||||
ReactOS does not implement. (On 32-bit *Linux* the `linux/386` FerretDB binary
|
||||
works.)
|
||||
|
||||
## Conclusion
|
||||
|
||||
- The architecture limit is **Node.js**, not FerretDB. FerretDB (Go) already runs
|
||||
on 32-bit ARM, 32-bit x86 and more.
|
||||
- With current tooling, the practical WeKan set is **amd64, arm64, ppc64le,
|
||||
s390x, riscv64**. `loong64` is blocked by tooling (no QEMU emulation, no base
|
||||
image), not by Node/FerretDB. `armhf`/`armel`/`i386` are blocked by Node.
|
||||
- Alternative JS engines do **not** unblock this — Deno/Bun cover fewer arches,
|
||||
and portable engines cannot run Meteor.
|
||||
- The only way to universal architecture coverage is a **portable-language server
|
||||
rewrite**, for which **Go is the best fit** (matching FerretDB), QuickJS the
|
||||
"keep JS" niche option, and Tcl/Tk a poor fit for a web application.
|
||||
@@ -55,28 +55,32 @@ cd wekan
|
||||
|
||||
Because it downgrades @meteorjs/rspack and breaks builds.
|
||||
|
||||
### 3. Select option 1 to install dependencies, and then Enter.
|
||||
### 3. Install dependencies, build WeKan, and run the dev server
|
||||
|
||||
`rebuild-wekan.sh` shows a two-level menu. The top level is `1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit`; pick a category number, then the item number inside it (each submenu also has a `Back` entry).
|
||||
|
||||
```
|
||||
# Setup -> Install dependencies
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
1 # Install dependencies
|
||||
|
||||
1
|
||||
|
||||
# Setup -> Build WeKan
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
2 # Build WeKan
|
||||
|
||||
2
|
||||
|
||||
# Dev server -> localhost:3000
|
||||
./rebuild-wekan.sh
|
||||
|
||||
3
|
||||
2 # Dev server
|
||||
1 # localhost:3000
|
||||
```
|
||||
That does:
|
||||
- 1 install dependencies,
|
||||
- 2 builds wekan
|
||||
- 3 starts wekan in development mode with command `meteor`, so it can detect if some file changes and try to rebuild automatically.
|
||||
- **Setup -> Install dependencies** installs dependencies,
|
||||
- **Setup -> Build WeKan** builds wekan,
|
||||
- **Dev server -> localhost:3000** starts wekan in development mode with command `meteor`, so it can detect if some file changes and try to rebuild automatically. If a dev server is already running on that port, it is stopped first and a fresh one started on the same port.
|
||||
|
||||
And then [register and login](../Login/Adding-users.md) with webbrowser at http://localhost:4000
|
||||
And then [register and login](../Login/Adding-users.md) with webbrowser at http://localhost:3000
|
||||
|
||||
### 4. Make and test your changes
|
||||
|
||||
|
||||
@@ -78,23 +78,40 @@ git clone git@github.com:YourGithubUsername/wekan.git
|
||||
|
||||
cd wekan
|
||||
```
|
||||
### 3. Select option 1 to install dependencies, and then Enter.
|
||||
### 3. Run the menu: Setup -> Install dependencies, then Setup -> Build WeKan, then Dev server -> localhost:3000
|
||||
|
||||
`rebuild-wekan.sh` shows a two-level menu. The top level is:
|
||||
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
1
|
||||
|
||||
./rebuild-wekan.sh
|
||||
|
||||
2
|
||||
|
||||
./rebuild-wekan.sh
|
||||
|
||||
3
|
||||
1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit
|
||||
```
|
||||
That does: 1 install dependencies, 2 builds wekan, 3 starts wekan in development mode with command `meteor`, so it can detect if some file changes and try to rebuild automatically and reload webbroser. But, still sometimes, it may need stopping with Ctrl-c and full build with option 2.
|
||||
|
||||
And then [register and login](../Login/Adding-users.md) at http://localhost:4000
|
||||
Pick a category number, then the item number inside it (each submenu also has a `Back` entry to return to the top level). To build from source and run it:
|
||||
|
||||
```
|
||||
# Install dependencies: Setup -> Install dependencies
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
1 # Install dependencies
|
||||
|
||||
# Build WeKan: Setup -> Build WeKan
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
2 # Build WeKan
|
||||
|
||||
# Run in development mode: Dev server -> localhost:3000
|
||||
./rebuild-wekan.sh
|
||||
2 # Dev server
|
||||
1 # localhost:3000
|
||||
```
|
||||
|
||||
The **Setup -> Build WeKan** step compiles WeKan, and **Dev server -> localhost:3000** starts it in development mode with the `meteor` command, so it detects file changes and tries to rebuild automatically and reload the web browser. Still, sometimes it may need stopping with Ctrl-c and a full **Setup -> Build WeKan** again.
|
||||
|
||||
If a dev server is already running, the **Dev server** options now stop it automatically before starting a fresh one — freeing **both** the app port and the rspack dev server's port `8080` (a leftover rspack dev server on 8080 otherwise makes the new server crash with `Error: listen EADDRINUSE ... :8080`), so you don't have to kill the old processes yourself.
|
||||
|
||||
The other **Dev server** options run WeKan the same way on different addresses/ports: `localhost:3000 + trace warnings`, `localhost:3000 + bundle visualizer`, `CURRENT-IP:3000`, `CURRENT-IP:3000 + MONGO_URL 27019`, and `CUSTOM-IP:PORT` (which asks you for the IP and port). There is also a **Kill all dev servers** option that frees every dev/test port the script uses at once (3000/3001, 3100/3101, 4000/4001 and 8080) — handy if a previous run left something behind.
|
||||
|
||||
And then [register and login](../Login/Adding-users.md) at http://localhost:3000
|
||||
|
||||
### OPTIONAL, NOT NEEDED: 5. Add new plugin package
|
||||
```
|
||||
@@ -145,19 +162,19 @@ Change to Meteor 3 branch:
|
||||
```
|
||||
git checkout feature-meteor3
|
||||
```
|
||||
Build wekan:
|
||||
Build wekan (Setup -> Build WeKan):
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
2
|
||||
1 # Setup
|
||||
2 # Build WeKan
|
||||
```
|
||||
If there are errors, try to fix them.
|
||||
|
||||
Or try to run wekan:
|
||||
Or try to run wekan (Dev server -> localhost:3000):
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
3
|
||||
2 # Dev server
|
||||
1 # localhost:3000
|
||||
```
|
||||
# Updating
|
||||
|
||||
|
||||
+15
-9
@@ -78,23 +78,29 @@ git clone git@github.com:YourGithubUsername/wekan.git
|
||||
|
||||
cd wekan
|
||||
```
|
||||
### 3. Select option 1 to install dependencies, and then Enter.
|
||||
### 3. Run the menu: Setup -> Install dependencies, Setup -> Build WeKan, Dev server -> localhost:3000
|
||||
|
||||
`rebuild-wekan.sh` shows a two-level menu (`1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit`); pick a category number, then the item number inside it.
|
||||
|
||||
```
|
||||
# Setup -> Install dependencies
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
1 # Install dependencies
|
||||
|
||||
1
|
||||
|
||||
# Setup -> Build WeKan
|
||||
./rebuild-wekan.sh
|
||||
1 # Setup
|
||||
2 # Build WeKan
|
||||
|
||||
2
|
||||
|
||||
# Dev server -> localhost:3000
|
||||
./rebuild-wekan.sh
|
||||
|
||||
3
|
||||
2 # Dev server
|
||||
1 # localhost:3000
|
||||
```
|
||||
That does: 1 install dependencies, 2 builds wekan, 3 starts wekan in development mode with command `meteor`, so it can detect if some file changes and try to rebuild automatically and reload webbroser. But, still sometimes, it may need stopping with Ctrl-c and full build with option 2.
|
||||
That does: **Setup -> Install dependencies** installs dependencies, **Setup -> Build WeKan** builds wekan, **Dev server -> localhost:3000** starts wekan in development mode with command `meteor`, so it can detect if some file changes and try to rebuild automatically and reload the web browser. But, still sometimes, it may need stopping with Ctrl-c and a full **Setup -> Build WeKan** again. If a dev server is already running on that port, the Dev server options stop it first and start a fresh one on the same port.
|
||||
|
||||
And then [register and login](../Login/Adding-users.md) at http://localhost:4000
|
||||
And then [register and login](../Login/Adding-users.md) at http://localhost:3000
|
||||
|
||||
### OPTIONAL, NOT NEEDED: 5. Add new plugin package
|
||||
```
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## WeKan Docker compose files (which database?)
|
||||
|
||||
Since the default database changed, the compose files in https://github.com/wekan/wekan are:
|
||||
|
||||
- **docker-compose.yml** — the default: **FerretDB v1 with embedded SQLite** (from https://github.com/wekan/FerretDB). Light and self-contained, no separate database server. Just `docker compose up -d`.
|
||||
- **docker-compose-mongodb-v7.yml** — the classic **MongoDB 7** setup. Use it with `docker compose -f docker-compose-mongodb-v7.yml up -d`, or rename/copy it to `docker-compose.yml`.
|
||||
- **docker-compose-ferretdb-v2-postgresql.yml** — **FerretDB 2 with PostgreSQL** (DocumentDB extension).
|
||||
- **docker-compose-multitenancy.yml** — MongoDB multitenancy setup.
|
||||
|
||||
Note: the older instructions below were written for the MongoDB setup; where they say `docker-compose.yml` for MongoDB-specific things (and the `#L...` line-number links), they now refer to **docker-compose-mongodb-v7.yml**.
|
||||
|
||||
## Building custom Docker image
|
||||
|
||||
This is only for developers. For normal users, do not add these options, just start with `docker compose up -d`
|
||||
|
||||
@@ -118,6 +118,8 @@ sudo kill -9 PID-NUMBER-HERE
|
||||
|
||||
## 11) Rebuild Wekan
|
||||
|
||||
In the menu, pick **Setup -> Build WeKan** (press `1` then `2`):
|
||||
|
||||
```
|
||||
cd ~/repos
|
||||
./rebuild-wekan.sh
|
||||
|
||||
@@ -69,9 +69,11 @@ git merge upstream/master
|
||||
cd ~/repos
|
||||
./rebuild-wekan.sh
|
||||
```
|
||||
First with option 1, to install deps.
|
||||
The menu is two levels: pick a category (`1) Setup 2) Dev server 3) Tests 4) Docker 5) Tools 6) Quit`), then an item inside it.
|
||||
|
||||
Then same `./rebuild-wekan.sh` again with option 2, to build Wekan.
|
||||
First **Setup -> Install dependencies** (press `1` then `1`), to install deps.
|
||||
|
||||
Then run `./rebuild-wekan.sh` again and pick **Setup -> Build WeKan** (press `1` then `2`), to build Wekan.
|
||||
|
||||
## 5) Developing with Standalone Wekan
|
||||
|
||||
@@ -81,7 +83,7 @@ meteor --port 4000
|
||||
```
|
||||
Wekan is at http://localhost:4000
|
||||
|
||||
When you make changes to files, Meteor automatically notices that, and rebuilds Wekan. If Meteor does not notice change, you need to Ctrl-c and `./rebuild-wekan.sh` option 2.
|
||||
When you make changes to files, Meteor automatically notices that, and rebuilds Wekan. If Meteor does not notice change, you need to Ctrl-c and run `./rebuild-wekan.sh` -> **Setup -> Build WeKan** (press `1` then `2`).
|
||||
|
||||
## 6) Using MongoDB
|
||||
|
||||
@@ -106,7 +108,7 @@ https://www.nosqlbooster.com/downloads
|
||||
|
||||
When your feature works at Standalone Wekan, you can also test it at Sandstorm.
|
||||
|
||||
First build wekan with option 2:
|
||||
First build wekan with **Setup -> Build WeKan** (press `1` then `2`):
|
||||
```
|
||||
cd ~/repos/wekan
|
||||
./rebuild-wekan.sh
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
# WeKan on Sandstorm — Meteor 3.5 / Node.js 24 build + MongoDB 3 → FerretDB migration
|
||||
|
||||
Status: **design / not yet implemented**. Nothing here has been built or run end‑to‑end
|
||||
in a grain yet — every step marked **TEST** must be verified against a real Sandstorm
|
||||
install (ideally a copy of an existing WeKan grain) before shipping.
|
||||
|
||||
This document describes how to build a modern WeKan `.spk` (Meteor 3.5, Node.js 24) that
|
||||
replaces the packaged MongoDB 3.0 database with **FerretDB v1 (embedded SQLite)**, migrating
|
||||
any existing grain's MongoDB 3.0 data on first launch — reusing the exact migration logic the
|
||||
WeKan **snap** already ships.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why
|
||||
|
||||
The current Sandstorm build uses **meteor‑spk 0.6.0** (`https://dl.sandstorm.io/meteor-spk-0.6.0.tar.xz`,
|
||||
built 2021‑10‑23). Its `meteor-spk.deps` payload is a 2015‑era design:
|
||||
|
||||
| Component | meteor‑spk 0.6.0 ships | WeKan needs |
|
||||
|---|---|---|
|
||||
| `bin/node` | Node **14.17.5** (stock, from nodejs.org) | **Node 24.x** (Meteor 3.5) |
|
||||
| `bin/mongod` | MongoDB **3.0.7** (WiredTiger) | keep — read‑only migration source only |
|
||||
| `bin/niscud` | MongoDB **2.x** (Kenton's Niscu fork) | keep (niscu→3.0 migration) |
|
||||
| `lib/` | glibc **2.31** (Ubuntu 20.04) | regenerate on 22.04 (glibc 2.35) for node24/ferretdb |
|
||||
| `start.js` | launches Mongo 3.0 + niscu→3.0 migration | rewrite for FerretDB |
|
||||
|
||||
`start.js` in 0.6.0 is functionally identical to 0.5.1 (only reformatted). It still launches
|
||||
mongod 3.0 with `--nohttpinterface` (a flag **removed in MongoDB 3.6+**), so a newer mongod
|
||||
can't even be dropped in without changing it.
|
||||
|
||||
Note on the Node fork: `sandstorm-io/node` is **not** needed. It is frozen at Node 8.11.4
|
||||
(2018) and its only real patch is a V8 thread‑table optimization for **node‑fibers** — and
|
||||
Meteor 3 removed fibers entirely. meteor‑spk already ships **stock** upstream Node (0.6.0's
|
||||
`bin/node` is the official nodejs.org 14.17.5 build). Use stock **Node 24**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target architecture
|
||||
|
||||
Steady state after migration: WeKan (Node 24) talks to **FerretDB v1 (SQLite)** over the
|
||||
MongoDB wire protocol — no MongoDB server runs at all. mongod 3.0 is used **once**, only to
|
||||
read an existing grain's old data during the one‑time migration.
|
||||
|
||||
```
|
||||
grain (single sandbox, writable = /var)
|
||||
├─ /var/wiredTigerDb ← EXISTING MongoDB 3.0 data (old grains only; read-only source)
|
||||
├─ /var/files/ ← WRITABLE_PATH (new)
|
||||
│ ├─ attachments/ ← GridFS attachments extracted here (Meteor-Files on fs)
|
||||
│ ├─ avatars/ ← GridFS avatars extracted here
|
||||
│ └─ db/ ← FerretDB SQLite dir (wekan.sqlite) ← steady-state DB
|
||||
└─ /var/.migration-to-ferretdb-done ← idempotency marker
|
||||
```
|
||||
|
||||
### Ports (loopback inside the grain)
|
||||
|
||||
| Port | Role |
|
||||
|---|---|
|
||||
| 4000 | app HTTP (behind `sandstorm-http-bridge`) |
|
||||
| 4001 | FerretDB v1 (steady state) — `MONGO_URL=mongodb://127.0.0.1:4001/wekan` |
|
||||
| 4003 | temporary mongod 3.0 (migration read only) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Sandbox compatibility (verified from the sandstorm source)
|
||||
|
||||
Checked `sandstorm/src/sandstorm/seccomp-bpf/filter.s`. The grain seccomp filter is an
|
||||
allowlist whose **default action is `ENOSYS`, not SIGSYS‑kill**, so Go/glibc gracefully fall
|
||||
back on missing syscalls. Relevant results:
|
||||
|
||||
- **Allowed** (needed by Node 24 / Go FerretDB): `getrandom`, `statfs`/`fstatfs`,
|
||||
`sched_getaffinity`, `clone`, `futex`, `epoll_*`, `eventfd2`, `fork`, `mmap`, `mprotect`.
|
||||
- **Denied → ENOSYS** (tolerated via fallback): `membarrier`, `rseq`, `clone3`, `restart_syscall`.
|
||||
|
||||
Conclusion: a grain can run Node 24 + FerretDB (Go) + mongod 3.0 concurrently — the same
|
||||
multi‑process model the current build already uses for mongod+node. Nothing else is needed
|
||||
from the sandstorm platform repo.
|
||||
|
||||
**TEST (highest risk):** Node 24's glibc uses `clone3` for thread creation and relies on the
|
||||
ENOSYS→`clone` fallback (glibc ≥ 2.34). Verify a trivial Node 24 grain spawns threads before
|
||||
investing in the rest.
|
||||
|
||||
---
|
||||
|
||||
## 4. `meteor-spk.deps` payload changes
|
||||
|
||||
Start from the 0.6.0 base, then:
|
||||
|
||||
- **Replace** `bin/node` with **Node 24** — use the exact build from Meteor 3.5's dev bundle
|
||||
(`meteor node -e "console.log(process.version)"`) so native‑addon ABIs match the WeKan bundle.
|
||||
- **Keep** `bin/mongod` (3.0.7) — read‑only migration source.
|
||||
- **Keep** `bin/niscud` (MongoDB 2.x) **and** the old bundled `node_modules/{mongodb,bson,
|
||||
mongodb-core,es6-promise,readable-stream}` — used by `start.js`'s **niscu → MongoDB 3.0**
|
||||
stage (Stage 1) for very old grains. Migration support for old versions is permanent and is
|
||||
not dropped. (0.6.0 already ships both; nothing extra to add.)
|
||||
- **Add** `migratemongo/bin/{mongoexport,mongo}` + `migratemongo/lib/` (the old glibc those
|
||||
3.x CLIs need). The importer reads the 3.0 data with these — the modern Node driver cannot
|
||||
speak to a 3.0/3.2 server. Reuse the same tools the snap's
|
||||
[`migratemongo`](https://github.com/wekan/migratemongo) provides (amd64, x86_64).
|
||||
- **Add** `ferretdb` — the `ferretdb-amd64` binary downloaded directly
|
||||
(newest release of [wekan/FerretDB](https://github.com/wekan/FerretDB/releases); the same
|
||||
`ferretdb-amd64` asset `release-all.yml` embeds).
|
||||
- **Add** `migrate-mongo3-to-ferretdb.mjs` — copy verbatim from
|
||||
[`snap-src/bin/migrate-mongo3-to-ferretdb.mjs`](../../../../../snap-src/bin/migrate-mongo3-to-ferretdb.mjs);
|
||||
it is entirely env/path‑driven and needs no changes.
|
||||
- The **importer** (Stage 2) resolves its `mongodb`/`bson` from the WeKan bundle via
|
||||
`NODE_PATH=<bundle>/programs/server/node_modules` (the modern driver, separate process from
|
||||
`start.js`, so it does not clash with the old driver Stage 1 uses).
|
||||
- **Regenerate `lib/`, `lib64/`, `usr/lib`** on Ubuntu 22.04/24.04 (glibc 2.35) for node24 +
|
||||
ferretdb. Keep the **old** libs separately under `migratemongo/lib` for mongod3/mongoexport
|
||||
(dual‑lib pattern, exactly as the snap does with `MM_LIB`).
|
||||
|
||||
> The 0.5.1 fork's `lib/x86_64-linux-gnu/*` were **dangling symlinks into
|
||||
> `/home/user/Lataukset/...`** (a Downloads folder). Do not carry those forward — the tree
|
||||
> must be self‑contained and regenerated with `gather-deps` on a clean modern base.
|
||||
|
||||
amd64 only. (arm64 Sandstorm is out of scope here; the migratemongo 3.x binaries are x86_64.)
|
||||
|
||||
---
|
||||
|
||||
## 5. `start.js` (rewritten launcher)
|
||||
|
||||
The canonical launcher is committed at
|
||||
[`sandstorm-src/start.js`](../../../../../sandstorm-src/start.js); the spk build copies it into
|
||||
`meteor-spk.deps/start.js`. It is a CJS launcher, spawned by the pkgdef as `... node start.js`.
|
||||
On first launch it runs the migration chain for whatever the grain holds —
|
||||
**niscu (2.x) → MongoDB 3.0** (Stage 1, the preserved legacy path, for very old grains) then
|
||||
**MongoDB 3.0 → FerretDB** (Stage 2) — then runs WeKan on FerretDB. Fresh grains and
|
||||
already-migrated grains skip straight to running on FerretDB.
|
||||
Migration logic is a re‑pathed port of the snap's
|
||||
[`migration-control`](../../../../../snap-src/bin/migration-control) +
|
||||
[`migrate-mongo3-to-ferretdb.mjs`](../../../../../snap-src/bin/migrate-mongo3-to-ferretdb.mjs).
|
||||
All app-payload paths resolve from `__dirname` (the grain mount root), and it waits for
|
||||
mongod 3.0 to answer a ping before running the importer. Abridged shape:
|
||||
|
||||
```js
|
||||
// meteor-spk.deps/start.js (see sandstorm-src/start.js for the full file)
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const APP_PORT = process.env.PORT || '4000';
|
||||
const DB_PORT = '4001'; // FerretDB steady-state
|
||||
const SRC_PORT = '4003'; // temp mongod 3.0 (migration read)
|
||||
const FILES_DIR = '/var/files';
|
||||
const SQLITE_DIR = path.join(FILES_DIR, 'db');
|
||||
const OLD_MONGO = '/var/wiredTigerDb';
|
||||
const MARKER = '/var/.migration-to-ferretdb-done';
|
||||
const STATUS = path.join(FILES_DIR, 'db', 'migration-status.json');
|
||||
const APPROOT = __dirname; // where .meteor-spk/bundle is mounted
|
||||
const MM_LIB = path.join(APPROOT, 'migratemongo/lib');
|
||||
|
||||
for (const d of [FILES_DIR, SQLITE_DIR,
|
||||
path.join(FILES_DIR, 'attachments'), path.join(FILES_DIR, 'avatars')]) {
|
||||
fs.mkdirSync(d, { recursive: true });
|
||||
}
|
||||
|
||||
function startFerret(port) {
|
||||
return spawn(path.join(APPROOT, 'ferretdb'),
|
||||
['--handler=sqlite', `--sqlite-url=file:${SQLITE_DIR}/`,
|
||||
`--listen-addr=127.0.0.1:${port}`, '--telemetry=disable'],
|
||||
{ stdio: 'inherit',
|
||||
env: { ...process.env, DO_NOT_TRACK: '1', FERRETDB_TELEMETRY: 'disable' } });
|
||||
}
|
||||
|
||||
function migrateIfNeeded() {
|
||||
const hasOld = fs.existsSync(path.join(OLD_MONGO, 'WiredTiger'));
|
||||
if (fs.existsSync(MARKER) || !hasOld) return; // fresh install or already migrated
|
||||
|
||||
console.log('** Migrating MongoDB 3 -> FerretDB (one-time) ...');
|
||||
const oldLibEnv = { ...process.env,
|
||||
LD_LIBRARY_PATH: `${MM_LIB}:${process.env.LD_LIBRARY_PATH || ''}` };
|
||||
|
||||
// 1. old mongod 3.0 serving the existing data (read-only source)
|
||||
spawnSync('/bin/mongod',
|
||||
['--dbpath', OLD_MONGO, '--bind_ip', '127.0.0.1', '--port', SRC_PORT,
|
||||
'--storageEngine', 'wiredTiger', '--fork', '--logpath', '/var/migration-mongod.log'],
|
||||
{ stdio: 'inherit', env: oldLibEnv });
|
||||
|
||||
// 2. FerretDB target (the PERMANENT SQLite dir — no temp/switch needed)
|
||||
const ferret = startFerret(DB_PORT);
|
||||
|
||||
// 3. importer: CLI-read 3.0 -> driver-insert FerretDB; GridFS -> /var/files/{attachments,avatars}
|
||||
const rc = spawnSync(path.join(APPROOT, 'node'),
|
||||
[path.join(APPROOT, 'migrate-mongo3-to-ferretdb.mjs')], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env,
|
||||
NODE_PATH: path.join(APPROOT, 'programs/server/node_modules'),
|
||||
MONGO_BIN_DIR: path.join(APPROOT, 'migratemongo/bin'),
|
||||
MONGO_LIB: MM_LIB,
|
||||
SRC_PORT, SRC_DB: 'wekan',
|
||||
TARGET_MONGO_URL: `mongodb://127.0.0.1:${DB_PORT}/wekan`,
|
||||
FILES_DIR,
|
||||
MIGRATION_PORT: APP_PORT, // progress dashboard shown at the grain URL
|
||||
STATUS_FILE: STATUS }, // importer writes final state here (see §6)
|
||||
}).status;
|
||||
|
||||
try { ferret.kill(); } catch {}
|
||||
spawnSync('/bin/mongo', ['--port', SRC_PORT, '--quiet', '--eval',
|
||||
'db.getSiblingDB("admin").shutdownServer()'], { env: oldLibEnv });
|
||||
|
||||
if (rc === 0) {
|
||||
fs.writeFileSync(MARKER, new Date().toISOString()); // never re-migrate; old data KEPT
|
||||
} else {
|
||||
console.error('** Migration failed; will retry on next grain start. Old data kept.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function runApp() {
|
||||
const ferret = startFerret(DB_PORT); // steady-state DB
|
||||
process.env.MONGO_URL = `mongodb://127.0.0.1:${DB_PORT}/wekan`;
|
||||
process.env.ROOT_URL = process.env.ROOT_URL || `http://127.0.0.1:${APP_PORT}`;
|
||||
process.env.PORT = APP_PORT;
|
||||
process.env.WRITABLE_PATH = FILES_DIR;
|
||||
process.on('exit', () => { try { ferret.kill(); } catch {} });
|
||||
setTimeout(() => require('./main.js'), 1500); // let FerretDB start listening
|
||||
}
|
||||
|
||||
migrateIfNeeded();
|
||||
runApp();
|
||||
```
|
||||
|
||||
Design choices vs the snap:
|
||||
- **No temp‑then‑switch**: the migration writes straight into the permanent FerretDB SQLite
|
||||
dir (`/var/files/db`), because in a grain there is only one database going forward.
|
||||
- **Idempotent**: the marker prevents re‑migration; the old `/var/wiredTigerDb` is **never
|
||||
deleted automatically** — the admin deletes it explicitly from the UI (§7).
|
||||
|
||||
---
|
||||
|
||||
## 6. What the migration does (importer)
|
||||
|
||||
`migrate-mongo3-to-ferretdb.mjs` (unchanged from the snap):
|
||||
|
||||
- **Text collections** — `mongoexport` each (Extended JSON) from mongod 3.0, `insertMany`
|
||||
into FerretDB via the modern Node driver (batch 200, upsert on conflict).
|
||||
- **Attachments + avatars** — from CollectionFS GridFS (`cfs_gridfs.<bucket>.{files,chunks}`
|
||||
+ `cfs.<bucket>.filerecord`): each file is reassembled from its chunks and written to
|
||||
`/var/files/attachments` or `/var/files/avatars`, and a **Meteor‑Files** record pointing at
|
||||
that filesystem path is inserted into FerretDB. This is exactly WeKan's "MongoDB → filesystem"
|
||||
attachment move, done inline.
|
||||
- **Schema** is left as‑is; WeKan's own Board Settings / Migrations upgrade it on first use.
|
||||
- **Progress** is served over HTTP at `MIGRATION_PORT` (= the app port), so a user opening the
|
||||
grain during migration sees a live dashboard.
|
||||
|
||||
**Add for the UI (§7):** have the importer also write a final status JSON to
|
||||
`STATUS_FILE` (`/var/files/db/migration-status.json`) — the `state` object it already builds:
|
||||
`{ startedAt, finishedAt, phase, success, collections, files, errors }`. One `fs.writeFileSync`
|
||||
at the end of `run()`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Admin Panel / Attachments / **Sandstorm** (new, `isSandstorm` only)
|
||||
|
||||
**Implemented** — server: [`server/methods/sandstormMigration.js`](../../../../../server/methods/sandstormMigration.js);
|
||||
client: the **Sandstorm** tab in [`client/components/settings/attachments.jade`](../../../../../client/components/settings/attachments.jade) /
|
||||
[`.js`](../../../../../client/components/settings/attachments.js); strings in
|
||||
[`imports/i18n/data/en.i18n.json`](../../../../../imports/i18n/data/en.i18n.json). The importer
|
||||
writes `migration-status.json` (STATUS_FILE) that the panel reads.
|
||||
|
||||
When `isSandstorm === true` (`Meteor.settings.public.sandstorm`, see
|
||||
[models/settings.js](../../../../../models/settings.js#L19)), a new **Sandstorm** section appears
|
||||
in Admin Panel / Attachments (next to Backup), showing:
|
||||
|
||||
1. **Migration status** — Success / Pending / Failed + timestamp and per‑collection / file
|
||||
counts, read from `/var/files/db/migration-status.json` and the marker.
|
||||
2. **Raw MongoDB disk usage** — bytes currently used by the old raw MongoDB 3.0 files under
|
||||
`/var/wiredTigerDb` (the reclaimable space), plus FerretDB SQLite size and attachments/avatars
|
||||
size for context.
|
||||
3. **Delete raw MongoDB files** — a guarded button that removes `/var/wiredTigerDb` to free
|
||||
grain disk. Enabled **only** when migration succeeded (marker present, `success:true`) and
|
||||
FerretDB actually holds data.
|
||||
|
||||
### Server methods
|
||||
|
||||
New file `server/methods/sandstormMigration.js` (server‑only; admin‑gated exactly like
|
||||
[`server/methods/backup.js`](../../../../../server/methods/backup.js) via `ReactiveCache.getCurrentUser()` + `isAdmin`):
|
||||
|
||||
```js
|
||||
const OLD_MONGO = '/var/wiredTigerDb';
|
||||
const STATUS = (process.env.WRITABLE_PATH || '/var/files') + '/db/migration-status.json';
|
||||
const MARKER = '/var/.migration-to-ferretdb-done';
|
||||
|
||||
function dirBytes(dir) { // recursive size; tolerant of missing dir
|
||||
let total = 0;
|
||||
const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
const p = path.join(d, e.name);
|
||||
if (e.isDirectory()) walk(p);
|
||||
else { try { total += fs.statSync(p).size; } catch {} }
|
||||
} };
|
||||
try { if (fs.existsSync(dir)) walk(dir); } catch {}
|
||||
return total;
|
||||
}
|
||||
|
||||
Meteor.methods({
|
||||
async sandstormMigrationStatus() {
|
||||
const user = await ReactiveCache.getCurrentUser();
|
||||
if (!user || !user.isAdmin) return false;
|
||||
let status = null;
|
||||
try { status = JSON.parse(fs.readFileSync(STATUS, 'utf8')); } catch {}
|
||||
return {
|
||||
isSandstorm: true,
|
||||
migrationDone: fs.existsSync(MARKER),
|
||||
status,
|
||||
rawMongoBytes: dirBytes(OLD_MONGO),
|
||||
rawMongoExists: fs.existsSync(OLD_MONGO),
|
||||
ferretBytes: dirBytes(path.dirname(STATUS)),
|
||||
};
|
||||
},
|
||||
|
||||
async sandstormDeleteRawMongo() {
|
||||
const user = await ReactiveCache.getCurrentUser();
|
||||
if (!user || !user.isAdmin) throw new Meteor.Error('not-authorized');
|
||||
// Safety: only after a confirmed-successful migration.
|
||||
if (!fs.existsSync(MARKER)) throw new Meteor.Error('migration-not-done');
|
||||
let ok = false;
|
||||
try { ok = JSON.parse(fs.readFileSync(STATUS, 'utf8')).success === true; } catch {}
|
||||
if (!ok) throw new Meteor.Error('migration-not-successful');
|
||||
const freed = dirBytes(OLD_MONGO);
|
||||
fs.rmSync(OLD_MONGO, { recursive: true, force: true });
|
||||
return { deleted: true, freedBytes: freed };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
New tab in [client/components/settings/attachments.{jade,js}](../../../../../client/components/settings/attachments.js),
|
||||
rendered only when `isSandstorm`. Poll `sandstormMigrationStatus` (like the Backup tab polls
|
||||
`backupStatus`), render the status + a human‑readable size (`filesize`), and a
|
||||
**Delete raw MongoDB files** button behind a confirm dialog that calls `sandstormDeleteRawMongo`
|
||||
and refreshes.
|
||||
|
||||
> Deleting the raw files is irreversible. Gate it behind: migration `success:true`, an explicit
|
||||
> confirm, and (recommended) a typed confirmation. The data still exists in FerretDB + on the
|
||||
> filesystem after migration, so this only reclaims the now‑redundant Mongo 3.0 copy.
|
||||
|
||||
---
|
||||
|
||||
## 8. pkgdef / env changes ([sandstorm-pkgdef.capnp](../../../../../sandstorm-pkgdef.capnp))
|
||||
|
||||
- `WRITABLE_PATH`: `/var/wekan-uploads` → **`/var/files`**.
|
||||
- Do **not** set `MONGO_URL` in `environ` (start.js sets it to the FerretDB port).
|
||||
- Bump `appVersion` and `appMarketingVersion`.
|
||||
- Keep `argv = ["/sandstorm-http-bridge", "4000", "--", "node", "start.js"]`.
|
||||
- `SANDSTORM=1` and `METEOR_SETTINGS={"public":{"sandstorm":true}}` are already present — that
|
||||
drives `isSandstorm` for the new admin panel.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build / CI ([.github/workflows/sandstorm.yml](../../../../../.github/workflows/sandstorm.yml))
|
||||
|
||||
- **Base = upstream meteor‑spk 0.6.0**, downloaded from `https://dl.sandstorm.io/meteor-spk-0.6.0.tar.xz`.
|
||||
It already contains `bin/mongod` (3.0.7), `bin/niscud` (2.x) and the old `node_modules` the
|
||||
niscu→3.0 stage needs. **Nothing is needed from the old `projects.7z`** (it only holds
|
||||
0.4.1/0.5.0/0.5.1, and its swapped node is ancient — we install Node 24 anyway). The dead
|
||||
`releases.wekan.team/dev/meteor-spk/projects.7z` fetch is removed.
|
||||
- The assembly script ([`sandstorm-src/build-deps.sh`](../../../../../sandstorm-src/build-deps.sh))
|
||||
modernizes `meteor-spk.deps`: swap in Node 24, add `ferretdb-amd64` (per-arch asset from the wekan/FerretDB release), add
|
||||
`migratemongo/{bin,lib}` (`mongoexport` + `mongo` + old libs), copy
|
||||
[`sandstorm-src/start.js`](../../../../../sandstorm-src/start.js) → `meteor-spk.deps/start.js`
|
||||
and [`snap-src/bin/migrate-mongo3-to-ferretdb.mjs`](../../../../../snap-src/bin/migrate-mongo3-to-ferretdb.mjs)
|
||||
→ `meteor-spk.deps/`, **keep `niscud`**, and regenerate the lib tree with `gather-deps` on ubuntu‑24.04.
|
||||
- Fetch the extra binaries (Node 24, `ferretdb-amd64`, migratemongo CLIs) from **GitHub release
|
||||
assets** — `releases.wekan.team` no longer exists, so any required build files live on GitHub
|
||||
releases (e.g. `wekan/wekan` or a dedicated release).
|
||||
|
||||
---
|
||||
|
||||
## 10. Risks & test checklist
|
||||
|
||||
- [ ] **TEST** Node 24 threads under seccomp (clone3→clone ENOSYS fallback). *Highest risk.*
|
||||
- [ ] **TEST** mongod 3.0.7 opens `/var/wiredTigerDb` inside the grain with the old libs.
|
||||
- [ ] **TEST** FerretDB (Go) starts and listens in the grain.
|
||||
- [ ] **TEST** full migration on a **copy** of a real old WeKan grain (text + attachments + avatars).
|
||||
- [ ] **TEST** fresh install (no `/var/wiredTigerDb`): skips migration, runs on empty FerretDB.
|
||||
- [ ] **TEST** grain disk headroom — migration transiently holds old Mongo + new SQLite + extracted
|
||||
files at once, within the grain quota.
|
||||
- [ ] **TEST** `LD_LIBRARY_PATH` split — node24/ferretdb on the fresh 2.35 libs; mongo‑3.0 CLIs on
|
||||
the old libs; no cross‑contamination.
|
||||
- [ ] **TEST** Admin Panel / Attachments / Sandstorm: status accuracy, disk numbers, guarded delete.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open questions
|
||||
|
||||
- FerretDB v1 SQLite filename: **confirmed** — for db `wekan` the backend writes
|
||||
`<sqlite-dir>/wekan.sqlite` (plus SQLite WAL sidecars `wekan.sqlite-shm` / `wekan.sqlite-wal`).
|
||||
So the disk-usage sum of the `db/` dir already covers it.
|
||||
- Bundle mount root in the grain: `sandstorm-src/start.js` resolves app-payload paths from
|
||||
`__dirname` (so `bin/mongod`, `ferretdb`, `migratemongo/`, `main.js` are all relative to the
|
||||
launcher). Confirm from the packed spk that the added binaries land where `start.js` expects
|
||||
(e.g. `migratemongo/` and `ferretdb` at the mount root, CLIs under `migratemongo/bin`).
|
||||
- Migration support from old versions (niscu 2.x and MongoDB 3.0) is **permanent** — it is not
|
||||
dropped, so `niscud`, mongod 3.0 and the CLI-based importer stay in the package.
|
||||
@@ -2,6 +2,59 @@
|
||||
|
||||
# Sandstorm at Debian and Ubuntu
|
||||
|
||||
### New Sandstorm WeKan at 2026-07
|
||||
|
||||
Backup your current Sandstorm first !
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
- FerretDB implements MongoDB 7 server wire protocol
|
||||
- FerretDB converts MongoDB Javascript queries to SQLite SQL queries
|
||||
- FerretDB saves data to SQLite database
|
||||
- Fork has additional MongoDB features
|
||||
- Fork is maintained by xet7
|
||||
|
||||
Migrating old MongoDB 3 data
|
||||
|
||||
- If there is old WeKan data, it is automatically migrated when opening old WeKan grain.
|
||||
Attachments are migrated from MongoDB to files/attachments.
|
||||
Text data is migrated from MongoDB to SQLite at files/db/wekan.sqlite .
|
||||
- After migrating, you may get error, that browser can not open that page embedded to other page.
|
||||
So you need to close WeKan grain and open it again. Sorry, trying to fix it later.
|
||||
|
||||
All Boards page
|
||||
|
||||
- WeKan starts at All Boards page Favorites
|
||||
- All your boards are at first at Remaining, until you Star to have your board at Favorites
|
||||
- You can move boards from Remaining to your personal Workspaces or Sub-Workspaces
|
||||
|
||||
After migrating, free some disk space
|
||||
|
||||
- If after migrating you would like to free disk space of WeKan grain
|
||||
by deleting old MongoDB raw database files
|
||||
- At right top corner, click your WeKan username at WeKan board (not at Sandstorm black backround) /
|
||||
Member Settings / Admin Panel / Attachments / Sandstorm / Delete raw MongoDB database files.
|
||||
|
||||
Cover image
|
||||
|
||||
- You can enable it for minicard and card at:
|
||||
Right Sidebar/Board Settings/Card Settings/Cover Image
|
||||
(at bottom of Card Settings list).
|
||||
|
||||
Download
|
||||
|
||||
1. Please wait before there is more testing of new version, and appears to Official Sandstorm Store.
|
||||
2. Currently newest at [Experimental Sanstorm Store](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h?experimental=true)
|
||||
3. Later will be at [Official Sandstorm Store](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h)
|
||||
4. But, if you use CloudFlare, there may be 100 MB file size upload limit,
|
||||
it prevents uploading wekan.spk as file to your Sandstorm.
|
||||
5. New experimental wekan.spk install file at WeKan 9.87 or newer
|
||||
at https://github.com/wekan/wekan/releases
|
||||
|
||||
### Sandstorm Radicale: Calendars and Contacts
|
||||
|
||||
- 2025-08-05
|
||||
@@ -141,7 +194,8 @@ Install to your own server. Automatic updates, tested before release. Sandstorm
|
||||
|
||||
## Wekan App
|
||||
|
||||
Wekan at [experimental](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h?experimental=true) or [official](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h) Sandstorm App Market. Note: Only use official. Experimental versions are broken.
|
||||
Wekan at [experimental](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h?experimental=true) or
|
||||
[official](https://apps.sandstorm.io/app/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h) Sandstorm App Market. Note: Only use official. Experimental versions are broken.
|
||||
|
||||
Newest Wekap app .spk file download at https://releases.wekan.team/sandstorm/
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# WeKan Snap — CPU platforms and databases
|
||||
|
||||
This page documents, **for the WeKan snap**, which CPU architectures are built,
|
||||
which database binaries each one ships, how new installs choose their database,
|
||||
and how the one-time MongoDB 3 → FerretDB v1 migration works.
|
||||
|
||||
## Summary
|
||||
|
||||
- **New installs use FerretDB v1 (SQLite) by default on every CPU architecture.**
|
||||
FerretDB v1 is the wekan/FerretDB fork with an embedded pure-Go SQLite backend
|
||||
that speaks the MongoDB wire protocol, so WeKan talks to it exactly like MongoDB.
|
||||
- **MongoDB 7 *server* binaries (mongod) are included for amd64 and arm64 only.**
|
||||
The other architectures (s390x, riscv64, ppc64le) have no MongoDB server — they
|
||||
run FerretDB v1 only.
|
||||
- **Every Snap platform includes the FerretDB v1 server and the MongoDB Database
|
||||
Tools** (`mongodump`, `mongorestore`, `mongofiles`, `mongoexport`, …), on every
|
||||
architecture.
|
||||
- **`mongosh` is NOT bundled at all.** WeKan already ships Node.js 24 and the
|
||||
`mongodb` driver, and all scripted database access (readiness checks,
|
||||
replica-set init, schema migration) uses that via `$SNAP/bin/db-eval` instead of
|
||||
the MongoDB Shell. This removes a large, CVE-prone binary and works identically
|
||||
on every architecture. See [MongoDB client binaries](#mongodb-client-binaries-source-and-availability).
|
||||
- **The MongoDB Database Tools are downloaded from WeKan's own fork, not from the
|
||||
MongoDB website** — `mongodump-<arch>` etc. from
|
||||
[wekan/mongo-tools](https://github.com/wekan/mongo-tools/releases).
|
||||
- **Existing MongoDB 7 data (amd64/arm64) is NOT migrated to FerretDB v1** — that
|
||||
snap keeps using its existing MongoDB 7.
|
||||
- **Only the amd64 snap migrates existing MongoDB 3 data to FerretDB v1** (see
|
||||
[Migration](#mongodb-3--ferretdb-v1-migration-amd64-only) below).
|
||||
|
||||
## Per-architecture matrix
|
||||
|
||||
| CPU arch | MongoDB 7 server (mongod) | MongoDB Database Tools | FerretDB v1 server | MongoDB 3 → FerretDB migration | Default DB (new install) |
|
||||
|----------|:------------------------:|:----------------------:|:------------------:|:------------------------------:|:------------------------:|
|
||||
| amd64 | ✅ | ✅ | ✅ | ✅ (if MongoDB 3 data present) | FerretDB v1 |
|
||||
| arm64 | ✅ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| s390x | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| ppc64le | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| riscv64 | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
|
||||
- **MongoDB 7 server (mongod)**: amd64/arm64 only (MongoDB ships no server for the others).
|
||||
- **MongoDB Database Tools**: every arch — WeKan builds them for all platforms from
|
||||
[wekan/mongo-tools](https://github.com/wekan/mongo-tools) (they are pure Go).
|
||||
- **mongosh**: not bundled on any arch — replaced by the bundled Node.js 24 +
|
||||
`mongodb` driver (`$SNAP/bin/db-eval`).
|
||||
|
||||
All architectures default new installs to FerretDB v1 with `DDP_TRANSPORT=sockjs`
|
||||
and polling reactivity.
|
||||
|
||||
## New installs
|
||||
|
||||
On a fresh install, every architecture starts on **FerretDB v1 (SQLite)** with
|
||||
these snap defaults (in [`snap-src/bin/config`](../../../../../snap-src/bin/config)):
|
||||
|
||||
- `database` = `ferretdb` — FerretDB v1 is the default backend.
|
||||
- `ddp-transport` (`DDP_TRANSPORT`) = `sockjs` — maximum proxy compatibility.
|
||||
- Reactivity = **polling**. FerretDB v1 has no replica-set oplog or change
|
||||
streams, so WeKan uses polling for live updates (it does this automatically
|
||||
when the backend is FerretDB).
|
||||
|
||||
On amd64/arm64 you can still switch an install to the bundled MongoDB 7 server:
|
||||
|
||||
```
|
||||
sudo snap set wekan database=mongodb # amd64/arm64 only (mongod is bundled there)
|
||||
sudo snap set wekan database=ferretdb # the default
|
||||
```
|
||||
|
||||
## MongoDB client binaries: source and availability
|
||||
|
||||
WeKan does not download MongoDB clients from the MongoDB website:
|
||||
|
||||
- **MongoDB Database Tools** come from [wekan/mongo-tools](https://github.com/wekan/mongo-tools/releases).
|
||||
These are pure Go, so its `build-binaries.yml` workflow cross-compiles **every
|
||||
architecture** (`bsondump`, `mongodump`, `mongoexport`, `mongofiles`,
|
||||
`mongoimport`, `mongorestore`, `mongostat`, `mongotop`), including arches MongoDB
|
||||
never shipped tools for (riscv64, loong64). Every WeKan build (snap, bundle,
|
||||
Docker, Sandstorm) downloads the one `<tool>-<arch>` it needs from the newest
|
||||
release.
|
||||
- **`mongosh` is not used at all.** It is a large Node.js-packaged binary (it
|
||||
embeds its own Node and drags in a heavy, CVE-prone dependency tree) that cannot
|
||||
be cross-compiled for s390x/ppc64le/riscv64. Since WeKan already bundles Node.js
|
||||
24 and the `mongodb` driver, every scripted MongoDB/FerretDB operation the snap
|
||||
used mongosh for — readiness `ping`, replica-set `initiate`/`status`, the v8.43
|
||||
schema migration — now runs through `$SNAP/bin/db-eval` (a tiny wrapper around
|
||||
the bundled Node + driver). This works identically on every architecture and
|
||||
removed the mongosh binary from the snap entirely.
|
||||
|
||||
The `mongo` shell that IS still present is only the legacy MongoDB 3.2 `mongo`
|
||||
(from `migratemongo`, amd64 only), used at migration time to read old MongoDB 3
|
||||
data — the modern driver cannot speak to a 3.2 server. It is not used at runtime.
|
||||
|
||||
## MongoDB 3 → FerretDB v1 migration (amd64 only)
|
||||
|
||||
Only the **amd64** snap ships the MongoDB 3-specific binaries (the legacy MongoDB
|
||||
3.2 `mongo` shell + `mongofiles`, bundled via `migratemongo`). They are used **at
|
||||
migration time only** — a one-time conversion of old data — and are **never a
|
||||
runtime component**: after the migration WeKan runs on FerretDB v1 and the
|
||||
MongoDB 3 binaries are not used again. No other architecture ships them at all
|
||||
(s390x/ppc64le/riscv64 never had a MongoDB 3, so there is nothing to migrate).
|
||||
|
||||
The migration runs only on amd64, and only when existing **MongoDB 3** data (from
|
||||
an old WeKan 6.09-era snap) is present. During the migration **both databases run
|
||||
at the same time on different ports** — MongoDB 3 on its port and FerretDB v1 on
|
||||
its port — and data moves directly from one to the other:
|
||||
|
||||
- **Attachments and avatars** (binary files): read from the MongoDB CollectionFS
|
||||
GridFS and MongoDB Meteor-Files GridFS collections with the MongoDB 3-specific
|
||||
`mongofiles` command, and written to:
|
||||
- `$SNAP_COMMON/files/attachments/`
|
||||
- `$SNAP_COMMON/files/avatars/`
|
||||
- **Text data** (all boards and other non-file data — everything that is *not*
|
||||
attachments or avatars): copied directly from the MongoDB 3 port to the
|
||||
FerretDB v1 port using the legacy MongoDB 3 `mongo` CLI (mongo shell); the
|
||||
modern `mongosh` cannot talk to a MongoDB 3 server.
|
||||
|
||||
After a successful migration the snap switches to `database=ferretdb`.
|
||||
|
||||
Architectures other than amd64 ship no MongoDB server and no MongoDB 3 client, so
|
||||
there is nothing to migrate from — they are FerretDB v1 from the first boot.
|
||||
|
||||
## Where the FerretDB v1 binary comes from
|
||||
|
||||
FerretDB v1 is built and released separately in
|
||||
[wekan/FerretDB](https://github.com/wekan/FerretDB/releases) as one binary per
|
||||
architecture (`ferretdb-<arch>`; there is no `ferretdb.zip`). Each WeKan build
|
||||
downloads only the single binary for the platform it targets, e.g.:
|
||||
|
||||
```
|
||||
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-amd64
|
||||
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-arm64
|
||||
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-s390x
|
||||
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-riscv64
|
||||
https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-ppc64le
|
||||
```
|
||||
|
||||
For the snap, that binary is embedded inside the `wekan-<version>-<arch>.zip`
|
||||
bundle the snap's `wekan` part downloads, and staged to `$SNAP/ferretdb`
|
||||
(started by `ferretdb-control`).
|
||||
|
||||
## How the bundled binary versions are kept current
|
||||
|
||||
- The **MongoDB 7 server (mongod)** is the one component still fetched from
|
||||
MongoDB (amd64/arm64 only). The `bump` job in
|
||||
[`.github/workflows/release-all.yml`](../../../../../.github/workflows/release-all.yml)
|
||||
runs [`releases/version.sh`](../../../../../releases/version.sh), which probes
|
||||
upstream and rewrites the pinned MongoDB 7 server version (and the WeKan bundle
|
||||
version/URLs) in `snapcraft.yaml`.
|
||||
- The **FerretDB v1** server and the **MongoDB client binaries** are not pinned:
|
||||
every build fetches the newest per-arch asset from the WeKan forks —
|
||||
`ferretdb-<arch>` from [wekan/FerretDB](https://github.com/wekan/FerretDB/releases),
|
||||
`<tool>-<arch>` from [wekan/mongo-tools](https://github.com/wekan/mongo-tools/releases),
|
||||
and `mongosh-<arch>` from [wekan/mongosh](https://github.com/wekan/mongosh/releases).
|
||||
|
||||
## Snap channels
|
||||
|
||||
The default `snapcraft.yaml` is built on `base: core24`, which allows the
|
||||
**candidate** channel (core26 does not). The automated release workflow publishes
|
||||
it to the **candidate**, **beta**, and **edge** channels; the **stable** channel is
|
||||
published manually later, once it is proven.
|
||||
`snapcraft-core26.yaml` is the same WeKan on the newer `base: core26`
|
||||
(`build-base: devel`, `grade: devel`), kept for testing the next base; it can only
|
||||
go to Beta/Edge until core26 is a released stable base.
|
||||
|
||||
## Related
|
||||
|
||||
- FerretDB v1 fork: https://github.com/wekan/FerretDB
|
||||
- Snap install: [Install.md](Install.md)
|
||||
- Snap settings keys: [Supported-settings-keys.md](Supported-settings-keys.md)
|
||||
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-9.78-win64.zip](https://github.com/wekan/wekan/releases/download/v9.78/wekan-9.78-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
1. [wekan-9.89-win64.zip](https://github.com/wekan/wekan/releases/download/v9.89/wekan-9.89-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
|
||||
|
||||
@@ -22,7 +22,7 @@ Right click and download files 1-4:
|
||||
|
||||
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
|
||||
7. Unzip `wekan-9.78-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-9.89-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Menerima",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Cipta ruangan",
|
||||
"createCustomFieldPopup-title": "Cipta ruangan",
|
||||
"current": "sekarang",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Tidak boleh undur semula. Ianya akan menghapuskan ruangan khas daripada semua kad dan padamkan sejarahnya.",
|
||||
"custom-field-checkbox": "kotak semak",
|
||||
"custom-field-currency": "Matawang",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Nama Lengkap",
|
||||
"header-logo-title": "Kembali ke laman panel anda",
|
||||
"show-activities": "Tunjukkan Aktiviti",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Buat Panel",
|
||||
"home": "Beranda",
|
||||
"import": "Impor",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Berlakon sebagai pengguna",
|
||||
"link": "Pautan",
|
||||
"import-board": "import papan",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Tidak Diketahui)",
|
||||
"Node_version": "Versi Node",
|
||||
"Meteor_version": "Versi Meteor",
|
||||
"MongoDB_version": "Versi MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "Mesin penyimpanan MongoDb",
|
||||
"MongoDB_Oplog_enabled": "Oplog MongoDB dihidupkan",
|
||||
"OS_Arch": "Arch OS",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Picu",
|
||||
"r-action": "Tindakan",
|
||||
"r-when-a-card": "Ketika kartu",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Storan",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Pulihkan",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Aanvaar",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Skep veld",
|
||||
"createCustomFieldPopup-title": "Skep veld",
|
||||
"current": "huidige",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Daar is geen ongedaan maak nie. Dit sal hierdie pasgemaakte veld van alle kaarte verwyder en sy geskiedenis vernietig.",
|
||||
"custom-field-checkbox": "Merkblokkie",
|
||||
"custom-field-currency": "Geldeenheid",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Volle Naam",
|
||||
"header-logo-title": "Gaan terug na jou bordbladsy.",
|
||||
"show-activities": "Wys aktiwiteite",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Skep bord",
|
||||
"home": "Tuis",
|
||||
"import": "Invoer",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Verpersoonlik gebruiker",
|
||||
"link": "Skakel",
|
||||
"import-board": "invoerbord",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Onbekend)",
|
||||
"Node_version": "Node weergawe",
|
||||
"Meteor_version": "Meteoor weergawe",
|
||||
"MongoDB_version": "MongoDB weergawe",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB stoor enjin",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog geaktiveer",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Sneller",
|
||||
"r-action": "Aksie",
|
||||
"r-when-a-card": "Wanneer 'n kaart",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Berging",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Herstel",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "SVE-kerns",
|
||||
"cpu-usage": "SVE Gebruik",
|
||||
"current-action": "Huidige Aksie",
|
||||
"database-migration": "Databasismigrasie",
|
||||
"database-migration-description": "Opdatering van databasisstruktuur vir verbeterde funksionaliteit en werkverrigting. Hierdie proses kan 'n paar minute neem.",
|
||||
"database-migrations": "Databasismigrasies",
|
||||
"days-old": "Dae oud",
|
||||
"duration": "Duur",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Aanvaar",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Skep veld",
|
||||
"createCustomFieldPopup-title": "Skep veld",
|
||||
"current": "huidige",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Daar is geen ongedaan maak nie. Dit sal hierdie pasgemaakte veld van alle kaarte verwyder en sy geskiedenis vernietig.",
|
||||
"custom-field-checkbox": "Merkblokkie",
|
||||
"custom-field-currency": "Geldeenheid",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Volle Naam",
|
||||
"header-logo-title": "Gaan terug na jou bordbladsy.",
|
||||
"show-activities": "Wys aktiwiteite",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Skep bord",
|
||||
"home": "Tuis",
|
||||
"import": "Invoer",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Verpersoonlik gebruiker",
|
||||
"link": "Skakel",
|
||||
"import-board": "invoerbord",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Onbekend)",
|
||||
"Node_version": "Node weergawe",
|
||||
"Meteor_version": "Meteoor weergawe",
|
||||
"MongoDB_version": "MongoDB weergawe",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB stoor enjin",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog geaktiveer",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Sneller",
|
||||
"r-action": "Aksie",
|
||||
"r-when-a-card": "Wanneer 'n kaart",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Berging",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Herstel",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "SVE-kerns",
|
||||
"cpu-usage": "SVE Gebruik",
|
||||
"current-action": "Huidige Aksie",
|
||||
"database-migration": "Databasismigrasie",
|
||||
"database-migration-description": "Opdatering van databasisstruktuur vir verbeterde funksionaliteit en werkverrigting. Hierdie proses kan 'n paar minute neem.",
|
||||
"database-migrations": "Databasismigrasies",
|
||||
"days-old": "Dae oud",
|
||||
"duration": "Duur",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "قبول",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "انشاء حقل",
|
||||
"createCustomFieldPopup-title": "انشاء حقل",
|
||||
"current": "الحالي",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "این اقدام فیلد سفارشی را بهمراه تمامی تاریخچه آن از کارت ها پاک می کند و برگشت پذیر نمی باشد",
|
||||
"custom-field-checkbox": "جعبه انتخابی",
|
||||
"custom-field-currency": "عملة",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "الإسم الكامل",
|
||||
"header-logo-title": "الرجوع إلى صفحة اللوحات",
|
||||
"show-activities": "عرض الأنشطة",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "إنشاء لوحة",
|
||||
"home": "الرئيسية",
|
||||
"import": "وارد کردن",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "جعل هویت کاربر",
|
||||
"link": "رابط",
|
||||
"import-board": "استيراد لوحة",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(غير معروف)",
|
||||
"Node_version": "إصدار النود",
|
||||
"Meteor_version": "نسخه متئور",
|
||||
"MongoDB_version": "ورژن MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "موتور ذخیره سازی MongoDB",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog فعال است",
|
||||
"OS_Arch": "معمارية نظام التشغيل",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "تریگر",
|
||||
"r-action": "عملیات",
|
||||
"r-when-a-card": "زمانی که کارت",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Stockage",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "استعادة",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "Cœurs du CPU ",
|
||||
"cpu-usage": "Utilisation du CPU",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Migration base de données",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Migrations base de données",
|
||||
"days-old": "Jours d'ancienneté",
|
||||
"duration": "Durée",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "قبول",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "انشاء حقل",
|
||||
"createCustomFieldPopup-title": "انشاء حقل",
|
||||
"current": "الحالي",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "این اقدام فیلد سفارشی را بهمراه تمامی تاریخچه آن از کارت ها پاک می کند و برگشت پذیر نمی باشد",
|
||||
"custom-field-checkbox": "جعبه انتخابی",
|
||||
"custom-field-currency": "عملة",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "الإسم الكامل",
|
||||
"header-logo-title": "الرجوع إلى صفحة اللوحات",
|
||||
"show-activities": "عرض الأنشطة",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "إنشاء لوحة",
|
||||
"home": "الرئيسية",
|
||||
"import": "وارد کردن",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "جعل هویت کاربر",
|
||||
"link": "رابط",
|
||||
"import-board": "استيراد لوحة",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(غير معروف)",
|
||||
"Node_version": "إصدار النود",
|
||||
"Meteor_version": "نسخه متئور",
|
||||
"MongoDB_version": "ورژن MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "موتور ذخیره سازی MongoDB",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog فعال است",
|
||||
"OS_Arch": "معمارية نظام التشغيل",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "تریگر",
|
||||
"r-action": "عملیات",
|
||||
"r-when-a-card": "زمانی که کارت",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Storage",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "استعادة",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "قبول",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "انشاء حقل",
|
||||
"createCustomFieldPopup-title": "انشاء حقل",
|
||||
"current": "الحالي",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "این اقدام فیلد سفارشی را بهمراه تمامی تاریخچه آن از کارت ها پاک می کند و برگشت پذیر نمی باشد",
|
||||
"custom-field-checkbox": "جعبه انتخابی",
|
||||
"custom-field-currency": "عملة",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "الإسم الكامل",
|
||||
"header-logo-title": "الرجوع إلى صفحة اللوحات",
|
||||
"show-activities": "عرض الأنشطة",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "إنشاء لوحة",
|
||||
"home": "الرئيسية",
|
||||
"import": "وارد کردن",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "جعل هویت کاربر",
|
||||
"link": "رابط",
|
||||
"import-board": "استيراد لوحة",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(غير معروف)",
|
||||
"Node_version": "إصدار النود",
|
||||
"Meteor_version": "نسخه متئور",
|
||||
"MongoDB_version": "ورژن MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "موتور ذخیره سازی MongoDB",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog فعال است",
|
||||
"OS_Arch": "معمارية نظام التشغيل",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "تریگر",
|
||||
"r-action": "عملیات",
|
||||
"r-when-a-card": "زمانی که کارت",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Storage",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "استعادة",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "قبول",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "انشاء حقل",
|
||||
"createCustomFieldPopup-title": "انشاء حقل",
|
||||
"current": "الحالي",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "این اقدام فیلد سفارشی را بهمراه تمامی تاریخچه آن از کارت ها پاک می کند و برگشت پذیر نمی باشد",
|
||||
"custom-field-checkbox": "جعبه انتخابی",
|
||||
"custom-field-currency": "عملة",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "الإسم الكامل",
|
||||
"header-logo-title": "الرجوع إلى صفحة اللوحات",
|
||||
"show-activities": "عرض الأنشطة",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "إنشاء لوحة",
|
||||
"home": "الرئيسية",
|
||||
"import": "وارد کردن",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "جعل هویت کاربر",
|
||||
"link": "رابط",
|
||||
"import-board": "استيراد لوحة",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(غير معروف)",
|
||||
"Node_version": "إصدار النود",
|
||||
"Meteor_version": "نسخه متئور",
|
||||
"MongoDB_version": "ورژن MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "موتور ذخیره سازی MongoDB",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog فعال است",
|
||||
"OS_Arch": "معمارية نظام التشغيل",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "تریگر",
|
||||
"r-action": "عملیات",
|
||||
"r-when-a-card": "زمانی که کارت",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Storage",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "استعادة",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Aceptar",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Crear un campo",
|
||||
"createCustomFieldPopup-title": "Crear un campo",
|
||||
"current": "actual",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Se eliminará este campo personalizado de todas las tarjetas y se destruirá su historial. Esta acción no puede deshacerse.",
|
||||
"custom-field-checkbox": "Casilla de verificación",
|
||||
"custom-field-currency": "Moneda",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Nombre completo",
|
||||
"header-logo-title": "Volver a tu página de tableros",
|
||||
"show-activities": "Mostrar actividades",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Crear tablero",
|
||||
"home": "Inicio",
|
||||
"import": "Importar",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Impersonalizar",
|
||||
"link": "Enlace",
|
||||
"import-board": "importar un tablero",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Desconocido)",
|
||||
"Node_version": "Versión de Node",
|
||||
"Meteor_version": "Versión de Meteor",
|
||||
"MongoDB_version": "Versión de MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "Motor de almacenamiento de MongoDB",
|
||||
"MongoDB_Oplog_enabled": "Oplog de MongoDB habilitado",
|
||||
"OS_Arch": "Arquitectura del sistema",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "disparador",
|
||||
"r-action": "acción",
|
||||
"r-when-a-card": "Cuando una tarjeta",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Almacenamiento",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Restaurar",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duración",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Qəbul edin",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Sahə yaradın",
|
||||
"createCustomFieldPopup-title": "Sahə yaradın",
|
||||
"current": "cari",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Geri qaytarma yoxdur. Bu, bu fərdi sahəni bütün kartlardan siləcək və onun tarixçəsini məhv edəcək.",
|
||||
"custom-field-checkbox": "Yoxlama qutusu",
|
||||
"custom-field-currency": "Valyuta",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Tam adı",
|
||||
"header-logo-title": "Lövhələr səhifəsinə qayıdın.",
|
||||
"show-activities": "Fəaliyyətləri göstərin",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Şura yaradın",
|
||||
"home": "Ev",
|
||||
"import": "İdxal",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Təqlid edən istifadəçi",
|
||||
"link": "Link",
|
||||
"import-board": "idxal lövhəsi",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Naməlum)",
|
||||
"Node_version": "Node versiyası",
|
||||
"Meteor_version": "Meteor versiyası",
|
||||
"MongoDB_version": "MongoDB versiyası",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB saxlama mühərriki",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oploq aktivləşdirildi",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Tətik",
|
||||
"r-action": "Fəaliyyət",
|
||||
"r-when-a-card": "Bir kart olanda",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Saxlama",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Bərpa et",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU nüvələri",
|
||||
"cpu-usage": "CPU İstifadəsi",
|
||||
"current-action": "Cari Fəaliyyət",
|
||||
"database-migration": "Verilənlər bazasının miqrasiyası",
|
||||
"database-migration-description": "Təkmil funksionallıq və performans üçün verilənlər bazası strukturunun yenilənməsi. Bu proses bir neçə dəqiqə çəkə bilər.",
|
||||
"database-migrations": "Verilənlər Bazasının Miqrasiyaları",
|
||||
"days-old": "Günlər köhnə",
|
||||
"duration": "Müddət",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Qəbul edin",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Sahə yaradın",
|
||||
"createCustomFieldPopup-title": "Sahə yaradın",
|
||||
"current": "cari",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Geri qaytarma yoxdur. Bu, bu fərdi sahəni bütün kartlardan siləcək və onun tarixçəsini məhv edəcək.",
|
||||
"custom-field-checkbox": "Yoxlama qutusu",
|
||||
"custom-field-currency": "Valyuta",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Tam adı",
|
||||
"header-logo-title": "Lövhələr səhifəsinə qayıdın.",
|
||||
"show-activities": "Fəaliyyətləri göstərin",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Şura yaradın",
|
||||
"home": "Ev",
|
||||
"import": "İdxal",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Təqlid edən istifadəçi",
|
||||
"link": "Link",
|
||||
"import-board": "idxal lövhəsi",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Naməlum)",
|
||||
"Node_version": "Node versiyası",
|
||||
"Meteor_version": "Meteor versiyası",
|
||||
"MongoDB_version": "MongoDB versiyası",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB saxlama mühərriki",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oploq aktivləşdirildi",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Tətik",
|
||||
"r-action": "Fəaliyyət",
|
||||
"r-when-a-card": "Bir kart olanda",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Saxlama",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Bərpa et",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU nüvələri",
|
||||
"cpu-usage": "CPU İstifadəsi",
|
||||
"current-action": "Cari Fəaliyyət",
|
||||
"database-migration": "Verilənlər bazasının miqrasiyası",
|
||||
"database-migration-description": "Təkmil funksionallıq və performans üçün verilənlər bazası strukturunun yenilənməsi. Bu proses bir neçə dəqiqə çəkə bilər.",
|
||||
"database-migrations": "Verilənlər Bazasının Miqrasiyaları",
|
||||
"days-old": "Günlər köhnə",
|
||||
"duration": "Müddət",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Qəbul edin",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Sahə yaradın",
|
||||
"createCustomFieldPopup-title": "Sahə yaradın",
|
||||
"current": "cari",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Geri qaytarma yoxdur. Bu, bu fərdi sahəni bütün kartlardan siləcək və onun tarixçəsini məhv edəcək.",
|
||||
"custom-field-checkbox": "Yoxlama qutusu",
|
||||
"custom-field-currency": "Valyuta",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Tam adı",
|
||||
"header-logo-title": "Lövhələr səhifəsinə qayıdın.",
|
||||
"show-activities": "Fəaliyyətləri göstərin",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Şura yaradın",
|
||||
"home": "Ev",
|
||||
"import": "İdxal",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Təqlid edən istifadəçi",
|
||||
"link": "Link",
|
||||
"import-board": "idxal lövhəsi",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Naməlum)",
|
||||
"Node_version": "Node versiyası",
|
||||
"Meteor_version": "Meteor versiyası",
|
||||
"MongoDB_version": "MongoDB versiyası",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB saxlama mühərriki",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oploq aktivləşdirildi",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Tətik",
|
||||
"r-action": "Fəaliyyət",
|
||||
"r-when-a-card": "Bir kart olanda",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Saxlama",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Bərpa et",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU nüvələri",
|
||||
"cpu-usage": "CPU İstifadəsi",
|
||||
"current-action": "Cari Fəaliyyət",
|
||||
"database-migration": "Verilənlər bazasının miqrasiyası",
|
||||
"database-migration-description": "Təkmil funksionallıq və performans üçün verilənlər bazası strukturunun yenilənməsi. Bu proses bir neçə dəqiqə çəkə bilər.",
|
||||
"database-migrations": "Verilənlər Bazasının Miqrasiyaları",
|
||||
"days-old": "Günlər köhnə",
|
||||
"duration": "Müddət",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Accept",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Create Field",
|
||||
"createCustomFieldPopup-title": "Create Field",
|
||||
"current": "current",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "There is no undo. This will remove this custom field from all cards and destroy its history.",
|
||||
"custom-field-checkbox": "Checkbox",
|
||||
"custom-field-currency": "Currency",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Full Name",
|
||||
"header-logo-title": "Go back to your boards page.",
|
||||
"show-activities": "Show Activities",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Create Board",
|
||||
"home": "Home",
|
||||
"import": "Import",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Impersonate user",
|
||||
"link": "Link",
|
||||
"import-board": "import board",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Unknown)",
|
||||
"Node_version": "Node version",
|
||||
"Meteor_version": "Meteor version",
|
||||
"MongoDB_version": "MongoDB version",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB storage engine",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog enabled",
|
||||
"OS_Arch": "OS Arch",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Trigger",
|
||||
"r-action": "Action",
|
||||
"r-when-a-card": "When a card",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Storage",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Restore",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "CPU Cores",
|
||||
"cpu-usage": "CPU Usage",
|
||||
"current-action": "Current Action",
|
||||
"database-migration": "Database Migration",
|
||||
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
|
||||
"database-migrations": "Database Migrations",
|
||||
"days-old": "Days Old",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"accept": "Приемам",
|
||||
"activity-changedTitle": "changed title to %s of %s",
|
||||
"activity-changedDescription": "changed description of %s",
|
||||
"act-deleteCard": "deleted card __card__ from list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"act-removeBoard": "deleted board __board__",
|
||||
"act-removeList": "deleted list __list__ at board __board__",
|
||||
@@ -441,6 +442,7 @@
|
||||
"createCustomField": "Създай Поле",
|
||||
"createCustomFieldPopup-title": "Създай Поле",
|
||||
"current": "сегашен",
|
||||
"custom-color": "Custom color",
|
||||
"custom-field-delete-pop": "Опозив ове радње неће бити могућ. Радњом се уклања ова придружена рубрика из свих предмета и уништива њена претходна историја употребе.",
|
||||
"custom-field-checkbox": "Чекбокс",
|
||||
"custom-field-currency": "Валута",
|
||||
@@ -582,9 +584,12 @@
|
||||
"fullname": "Име",
|
||||
"header-logo-title": "Назад към страницата с Вашите табла.",
|
||||
"show-activities": "Писани записник",
|
||||
"hide-activities": "Hide Activities",
|
||||
"headerBarCreateBoardPopup-title": "Създай Табло",
|
||||
"home": "Начало",
|
||||
"import": "Импорт",
|
||||
"imported-member-no-account": "Imported member — no matching account yet",
|
||||
"inactive-member": "Inactive member",
|
||||
"impersonate-user": "Увуци се у налог овог корисника",
|
||||
"link": "Връзка",
|
||||
"import-board": "Импортирай Табло",
|
||||
@@ -895,7 +900,12 @@
|
||||
"no-name": "(Непознат)",
|
||||
"Node_version": "Версия на Node",
|
||||
"Meteor_version": "Версия на Meteor",
|
||||
"MongoDB_version": "Версия на MongoDB",
|
||||
"Database_type": "Database type",
|
||||
"Database_commit": "Database commit",
|
||||
"FerretDB_version": "FerretDB version",
|
||||
"FerretDB_commit": "FerretDB commit",
|
||||
"Reactivity_mode": "Reactivity mode (changeStreams / oplog / polling)",
|
||||
"MongoDB_version": "MongoDB compatible version",
|
||||
"MongoDB_storage_engine": "MongoDB енджин за данни",
|
||||
"MongoDB_Oplog_enabled": "MongoDB Oplog включен",
|
||||
"OS_Arch": "Архитектура на ОС",
|
||||
@@ -1016,6 +1026,17 @@
|
||||
"r-workflow-help": "Drag a trigger and an action into the builder, then add the rule. Drag an action onto an existing rule to change what it does.",
|
||||
"r-drop-trigger": "drop a trigger here",
|
||||
"r-drop-action": "drop an action here",
|
||||
"r-w-card-created": "Card is created",
|
||||
"r-w-card-archived": "Card is archived",
|
||||
"r-w-card-unarchived": "Card is unarchived",
|
||||
"r-w-label-added": "Any label is added",
|
||||
"r-w-label-removed": "Any label is removed",
|
||||
"r-w-member-added": "A member is added",
|
||||
"r-w-member-removed": "A member is removed",
|
||||
"r-w-checklist-added": "A checklist is added",
|
||||
"r-w-attachment-added": "An attachment is added",
|
||||
"r-w-every-day-at": "Every day at __time__",
|
||||
"r-w-set-received-now": "Set received date to now",
|
||||
"r-export-json": "Export rules to JSON",
|
||||
"r-export-csv": "Export rules to CSV",
|
||||
"r-import-json": "Import rules from JSON",
|
||||
@@ -1069,6 +1090,12 @@
|
||||
"r-mark-incomplete": "Mark card incomplete",
|
||||
"r-move-all-cards": "Move all cards in list",
|
||||
"r-set-date-relative": "Set date relative to now",
|
||||
"r-later": "later",
|
||||
"r-unit-minutes": "minute(s)",
|
||||
"r-unit-hours": "hour(s)",
|
||||
"r-unit-days": "day(s)",
|
||||
"r-unit-weeks": "week(s)",
|
||||
"r-unit-months": "month(s)",
|
||||
"r-trigger": "Добави спусък",
|
||||
"r-action": "Действие",
|
||||
"r-when-a-card": "Когато карта",
|
||||
@@ -1810,6 +1837,57 @@
|
||||
"azure-container": "Container",
|
||||
"gcs-storage": "Google Cloud Storage",
|
||||
"gcs-storage-description": "Google Cloud Storage.",
|
||||
"database-migration": "Database migration",
|
||||
"database-migration-description": "Migrate text-based data (everything except attachments and avatars, which stay on the filesystem) between MongoDB and FerretDB v1 (SQLite). The other database must be running and reachable (default ports: FerretDB mongodb://127.0.0.1:27018, MongoDB mongodb://127.0.0.1:27019; override with WEKAN_FERRETDB_URL / WEKAN_MONGODB_URL). After it finishes, point MONGO_URL at the other database and restart WeKan (on Snap: snap set wekan database=ferretdb or =mongodb).",
|
||||
"database-migrate-to-ferretdb": "Migrate text data to FerretDB v1 (SQLite)",
|
||||
"database-migrate-to-mongodb": "Migrate text data to MongoDB",
|
||||
"database-migration-confirm": "Copy all text-based collections to __db__? Attachments and avatars on the filesystem are not affected. The target database must be running.",
|
||||
"database-migration-phase": "Phase",
|
||||
"database-migration-done": "Text-data migration completed.",
|
||||
"sandstorm-migration-description": "When WeKan runs inside a Sandstorm grain, its old MongoDB 3 database is migrated to FerretDB v1 (SQLite) on first launch, and attachments and avatars are moved from the database to the grain's files/attachments and files/avatars directories. This shows whether that migration succeeded and how much disk space the old raw MongoDB database files still use inside the grain, and lets you delete those raw files to free space.",
|
||||
"sandstorm-migration-status": "Migration status",
|
||||
"sandstorm-migration-success": "Succeeded",
|
||||
"sandstorm-migration-failed": "Failed",
|
||||
"sandstorm-migration-pending": "Not migrated yet",
|
||||
"sandstorm-storage-item": "Складиште",
|
||||
"sandstorm-disk-usage": "Disk usage",
|
||||
"sandstorm-raw-mongodb": "Raw MongoDB 3 database files",
|
||||
"sandstorm-delete-raw-mongodb": "Delete raw MongoDB database files",
|
||||
"sandstorm-delete-raw-mongodb-description": "After a successful migration the data lives in FerretDB and on the filesystem, so the old raw MongoDB 3 database files can be deleted to free grain disk space. This cannot be undone.",
|
||||
"sandstorm-delete-raw-mongodb-confirm": "Delete the raw MongoDB 3 database files to free disk space? The data has already been migrated to FerretDB and the filesystem. This cannot be undone.",
|
||||
"sandstorm-raw-mongodb-deleted": "Raw MongoDB database files deleted, freed",
|
||||
"collections": "collections",
|
||||
"features": "Features",
|
||||
"features-performance": "Performance",
|
||||
"cards-loading": "Card loading",
|
||||
"cards-loading-all": "All cards (default)",
|
||||
"cards-loading-lazy": "Lazy (only visible cards)",
|
||||
"cards-loading-description": "How each board loads its cards. All cards: every card is loaded into the browser (simplest, all features work, but a board with thousands of cards sends a lot of data to each viewer). Lazy: each list loads only the cards currently visible (infinite scroll) plus a live count, so very large boards use less memory and bandwidth. Also set by the CARDS_LOADING environment variable.",
|
||||
"cards-loading-lazy-note": "Lazy card loading is experimental: card counters and WIP limits are accurate, but the Calendar/Table/Gantt views and multi-select reflect only the cards loaded so far. Reload open boards after changing this.",
|
||||
"backup": "Backup",
|
||||
"backup-description": "Back up Attachments, Avatars and/or Data (all text-based collections that are not attachments or avatars). The .zip is streamed directly to the selected storage — no temporary file — as backup/YYYY/MM/DD/HH_MM_SS/backup.zip, containing YYYY_MM_DD-HH_MM_SS/attachments, /avatars and /data. Cloud storages (S3/MinIO, Azure, GCS) use the credentials configured in their storage tabs.",
|
||||
"backup-data": "Data (text)",
|
||||
"backup-storage": "Save to storage",
|
||||
"backup-now": "Backup now",
|
||||
"backup-done": "Backup completed",
|
||||
"backup-schedule": "Scheduled backups",
|
||||
"backup-frequency": "Frequency",
|
||||
"backup-frequency-off": "Off",
|
||||
"backup-frequency-daily": "Daily",
|
||||
"backup-frequency-weekly": "Weekly",
|
||||
"backup-frequency-monthly": "Monthly",
|
||||
"backup-time": "Time (HH:MM)",
|
||||
"backup-day-of-week": "Day of week (weekly)",
|
||||
"backup-day-of-month": "Day of month (monthly, 1-28)",
|
||||
"backup-restore": "Възстанови",
|
||||
"backup-list": "List backups",
|
||||
"backup-datetime": "Date and time",
|
||||
"backup-path": "File path",
|
||||
"backup-restore-mode": "Restore mode",
|
||||
"backup-restore-add-missing": "Add missing data only",
|
||||
"backup-restore-replace-all": "Replace all data",
|
||||
"backup-restore-select-first": "Select a backup from the list first.",
|
||||
"backup-restore-confirm": "Restore the selected backup? Attachments/avatars are written to the filesystem and data to the database according to the chosen mode.",
|
||||
"gcs-project-id": "Project ID",
|
||||
"gcs-bucket": "Bucket",
|
||||
"gcs-key-filename": "Service account key file path",
|
||||
@@ -1987,8 +2065,6 @@
|
||||
"cpu-cores": "Процесорска језгра",
|
||||
"cpu-usage": "Заузеће процесора",
|
||||
"current-action": "Текућа радња",
|
||||
"database-migration": "Обнова базе података",
|
||||
"database-migration-description": "Преслажем структуру базе података да би била боља и бржа. Процес може потрајати неколико минута.",
|
||||
"database-migrations": "Преслагање базе података",
|
||||
"days-old": "дана стар",
|
||||
"duration": "Трајање",
|
||||
|
||||
Loaded 100 of 403 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user