From 94562235564e69cb20fe6196f690d6763d4daee8 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 14:31:32 +0200 Subject: [PATCH 01/14] fix: prevent package postinstall hang when generating jwt secret The postinstall scripts generated the jwt secret with: cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 This relies on SIGPIPE to terminate the infinite `cat /dev/urandom` once `head` has read its single line. Inside a dpkg/apt maintainer-script context the SIGPIPE disposition is not reliably delivered, so `cat /dev/urandom` spins forever, the postinstall never returns, and the whole `dpkg -i` / upgrade hangs. Read a bounded 512 bytes with `head -c` instead so nothing depends on SIGPIPE to terminate. 512 random bytes yield ~124 alphanumerics on average, so the trailing `head -c 32` reliably produces a full 32-char secret while staying dependency-free. Fixes #2660 --- build/after-install-openrc.sh | 2 +- build/after-install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/after-install-openrc.sh b/build/after-install-openrc.sh index c0e01cd03..8c3e9a638 100755 --- a/build/after-install-openrc.sh +++ b/build/after-install-openrc.sh @@ -2,7 +2,7 @@ rc-update add vikunja default # Fix the config to contain proper values -NEW_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) +NEW_SECRET=$(head -c 512 /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) sed -i "s//$NEW_SECRET/g" /etc/vikunja/config.yml sed -i "s//\/opt\/vikunja\//g" /etc/vikunja/config.yml sed -i "s/path: \"\.\/vikunja.db\"/path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml diff --git a/build/after-install.sh b/build/after-install.sh index a8f2840d7..1b6399a49 100644 --- a/build/after-install.sh +++ b/build/after-install.sh @@ -3,7 +3,7 @@ systemctl enable vikunja.service # Fix the config to contain proper values -NEW_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) +NEW_SECRET=$(head -c 512 /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) sed -i "s//$NEW_SECRET/g" /etc/vikunja/config.yml sed -i "s//\/opt\/vikunja\//g" /etc/vikunja/config.yml sed -i "s/path: \"\.\/vikunja.db\"/path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml From e0fa2bbed4025d2e3952caecd0ef09394e03c97f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 12:50:59 +0000 Subject: [PATCH 02/14] chore(deps): update dependency vue-tsc to v3.3.3 --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 53 +++++++---------------------------------- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index bd2bac561..4a2b7ac37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -155,7 +155,7 @@ "vite-plugin-vue-devtools": "8.1.2", "vite-svg-loader": "5.1.1", "vitest": "4.1.7", - "vue-tsc": "3.3.2", + "vue-tsc": "3.3.3", "wait-on": "9.0.10", "workbox-cli": "7.4.1", "ws": "8.21.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 518753a69..66f30f6e9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -326,8 +326,8 @@ importers: specifier: 4.1.7 version: 4.1.7(@types/node@24.12.4)(happy-dom@20.9.0)(jsdom@27.4.0)(vite@7.3.3(@types/node@24.12.4)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.31.6)(yaml@2.8.3)) vue-tsc: - specifier: 3.3.2 - version: 3.3.2(typescript@5.9.3) + specifier: 3.3.3 + version: 3.3.3(typescript@5.9.3) wait-on: specifier: 9.0.10 version: 9.0.10 @@ -2139,42 +2139,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2334,79 +2328,66 @@ packages: resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} @@ -2609,28 +2590,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -3211,8 +3188,8 @@ packages: typescript: optional: true - '@vue/language-core@3.3.2': - resolution: {integrity: sha512-CLwjSfHlPLhjd2qhuS3tTFtnOIWHXAM5u4X1DxmzlQ8j5bmOYlKCsSusOP7jCRJnlVg0mCTQtHU3vwFvopZGoQ==} + '@vue/language-core@3.3.3': + resolution: {integrity: sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==} '@vue/reactivity@3.5.27': resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} @@ -4984,28 +4961,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -6010,56 +5983,48 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - libc: glibc sass-embedded-linux-arm@1.100.0: resolution: {integrity: sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - libc: glibc sass-embedded-linux-musl-arm64@1.100.0: resolution: {integrity: sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - libc: musl sass-embedded-linux-musl-arm@1.100.0: resolution: {integrity: sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - libc: musl sass-embedded-linux-musl-riscv64@1.100.0: resolution: {integrity: sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - libc: musl sass-embedded-linux-musl-x64@1.100.0: resolution: {integrity: sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - libc: musl sass-embedded-linux-riscv64@1.100.0: resolution: {integrity: sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - libc: glibc sass-embedded-linux-x64@1.100.0: resolution: {integrity: sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - libc: glibc sass-embedded-unknown-all@1.100.0: resolution: {integrity: sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==} @@ -6948,8 +6913,8 @@ packages: peerDependencies: vue: ^3.5.0 - vue-tsc@3.3.2: - resolution: {integrity: sha512-n7nQoA3YWW/eiDR8jMiv/uJvlg0uLGs+YgUrsTrf9EZaYSt3tuvMZb5V8+7Mvh/EH5pnY/hoVdgfjH+XcK+wwA==} + vue-tsc@3.3.3: + resolution: {integrity: sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -10172,7 +10137,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/language-core@3.3.2': + '@vue/language-core@3.3.3': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.27 @@ -14277,10 +14242,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.27(typescript@5.9.3) - vue-tsc@3.3.2(typescript@5.9.3): + vue-tsc@3.3.3(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.2 + '@vue/language-core': 3.3.3 typescript: 5.9.3 vue@3.5.27(typescript@5.9.3): From 1af6d7763bfc4f27758363202f2303e3f98743c6 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:18:51 +0200 Subject: [PATCH 03/14] fix(desktop): show tray icon in packaged builds The tray icon was loaded from desktop/build/icon.png, but build/ is electron-builder's default buildResources directory, whose contents are not packaged into the app. The icon therefore existed when running from source but was missing in every released build, leaving an empty tray icon. Load the icon from the packaged app root instead and add icon.png there, rendered from the circular logo.svg so it has transparent corners rather than the square full-bleed source artwork. Fixes #2668 --- desktop/icon.png | Bin 0 -> 62055 bytes desktop/main.js | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 desktop/icon.png diff --git a/desktop/icon.png b/desktop/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..21b9fc4a6f219513a4a6abea0d4d40a4a4ae6041 GIT binary patch literal 62055 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 a^*fTopiTL9b$|u|Kd6&VHuc8?lK&4X!*31% literal 0 HcmV?d00001 diff --git a/desktop/main.js b/desktop/main.js index f670f8fe9..ca84f4c1f 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -397,7 +397,11 @@ function toggleQuickEntry() { // ─── System tray ───────────────────────────────────────────────────── function setupTray() { if (!tray) { - const iconPath = path.join(__dirname, 'build', 'icon.png') + // NOTE: load the icon from the app root, not build/. The build/ directory is + // electron-builder's buildResources dir and is NOT packaged into the app, so + // referencing build/icon.png here works in dev but yields an empty tray icon + // in packaged releases (see issue #2668). + const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath).resize({width: 16, height: 16}) tray = new Tray(icon) tray.setToolTip('Vikunja') From ec2f154e1059709cc1e0cd62794c126aa10eba40 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 14:57:15 +0200 Subject: [PATCH 04/14] feat(keyvalue): add RememberFor for TTL-cached values --- pkg/modules/keyvalue/keyvalue.go | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/modules/keyvalue/keyvalue.go b/pkg/modules/keyvalue/keyvalue.go index 507f7322b..c71e00063 100644 --- a/pkg/modules/keyvalue/keyvalue.go +++ b/pkg/modules/keyvalue/keyvalue.go @@ -17,6 +17,8 @@ package keyvalue import ( + "time" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/modules/keyvalue/memory" @@ -142,3 +144,38 @@ func RememberValue[T any](key string, fn func() (T, error)) (T, error) { return val, nil } + +// expiringValue wraps a cached value with the time it expires. +type expiringValue[T any] struct { + Value T + ExpiresAt time.Time +} + +// RememberFor is like RememberValue but treats the cached value as stale once it is +// older than ttl. On a miss or once expired, it executes fn, caches the result for +// ttl and returns it. If fn returns an error, nothing is cached. +// T must be a concrete (non-pointer) type. +func RememberFor[T any](key string, ttl time.Duration, fn func() (T, error)) (T, error) { + var cached expiringValue[T] + exists, err := GetWithValue(key, &cached) + if err != nil { + var zero T + return zero, err + } + if exists && time.Now().Before(cached.ExpiresAt) { + return cached.Value, nil + } + + val, err := fn() + if err != nil { + var zero T + return zero, err + } + + if err := Put(key, expiringValue[T]{Value: val, ExpiresAt: time.Now().Add(ttl)}); err != nil { + var zero T + return zero, err + } + + return val, nil +} From 051f734f3d17d442a7199fc324e7df2c8cf824a8 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:03:08 +0200 Subject: [PATCH 05/14] refactor(metrics): count entities on demand with a TTL cache Instead of priming a counter at startup and keeping it in sync via events, each entity count is now read directly from the database and cached for 30s (countCacheTTL). The cache is the correctness guarantee: counts are at most one TTL stale and self-healing, so they can never permanently drift. This fixes vikunja_user_count never updating after registration (#2650): the count no longer depends on every mutation path dispatching an event. --- pkg/metrics/metrics.go | 65 +++++++++++++++++++++++++++++------------- pkg/routes/metrics.go | 43 ---------------------------- 2 files changed, 45 insertions(+), 63 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 00dce43eb..2ff7c1c6d 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -17,8 +17,9 @@ package metrics import ( - "strconv" + "time" + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/modules/keyvalue" @@ -36,6 +37,22 @@ const ( AttachmentsCountKey = `attachments_count` ) +// countCacheTTL is how long a cached entity count is served before it is recomputed +// from the database. The counts are inherently approximate (Prometheus samples them), +// so a short staleness window is fine and keeps the cache self-healing — a missed +// InvalidateCount call costs at most this much staleness, never a permanent drift. +const countCacheTTL = 30 * time.Second + +// countTables maps each count metric key to the database table it counts. +var countTables = map[string]string{ + ProjectCountKey: "projects", + UserCountKey: "users", + TaskCountKey: "tasks", + TeamCountKey: "teams", + FilesCountKey: "files", + AttachmentsCountKey: "task_attachments", +} + var registry *prometheus.Registry func GetRegistry() *prometheus.Registry { @@ -53,7 +70,10 @@ func registerPromMetric(key, description string) { Name: "vikunja_" + key, Help: description, }, func() float64 { - count, _ := GetCount(key) + count, err := GetCount(key) + if err != nil { + log.Errorf("Could not get count for metric %s: %s", key, err) + } return float64(count) })) if err != nil { @@ -65,8 +85,8 @@ func registerPromMetric(key, description string) { func InitMetrics() { GetRegistry() - registerPromMetric(ProjectCountKey, "The number of projects on this instance") - registerPromMetric(UserCountKey, "The total number of shares on this instance") + registerPromMetric(ProjectCountKey, "The total number of projects on this instance") + registerPromMetric(UserCountKey, "The total number of users on this instance") registerPromMetric(TaskCountKey, "The total number of tasks on this instance") registerPromMetric(TeamCountKey, "The total number of teams on this instance") registerPromMetric(FilesCountKey, "The total number of files on this instance") @@ -76,26 +96,31 @@ func InitMetrics() { setupActiveLinkSharesMetric() } -// GetCount returns the current count from keyvalue -func GetCount(key string) (count int64, err error) { - cnt, exists, err := keyvalue.Get(key) - if err != nil { - return 0, err - } - if !exists { +// GetCount returns the current count for the given metric key. The value is counted +// directly from the database and cached for countCacheTTL, so repeated scrapes don't +// hit the database on every request. +func GetCount(key string) (int64, error) { + return keyvalue.RememberFor(key, countCacheTTL, func() (int64, error) { + return countFromDatabase(key) + }) +} + +// countFromDatabase runs a COUNT(*) for the table backing the given metric key. +func countFromDatabase(key string) (int64, error) { + table, has := countTables[key] + if !has { return 0, nil } - if s, is := cnt.(string); is { - count, err = strconv.ParseInt(s, 10, 64) - } else { - count = cnt.(int64) - } + s := db.NewSession() + defer s.Close() - return + return s.Table(table).Count() } -// SetCount sets the project count to a given value -func SetCount(count int64, key string) error { - return keyvalue.Put(key, count) +// InvalidateCount drops the cached count for a key so the next read recomputes it from +// the database. Use it where instant freshness is worth the extra COUNT(*); everywhere +// else the countCacheTTL keeps the value reasonably up to date on its own. +func InvalidateCount(key string) error { + return keyvalue.Del(key) } diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go index 3b5ba3575..14180ceb4 100644 --- a/pkg/routes/metrics.go +++ b/pkg/routes/metrics.go @@ -20,12 +20,10 @@ import ( "crypto/subtle" "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/models" auth2 "code.vikunja.io/api/pkg/modules/auth" - "code.vikunja.io/api/pkg/user" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" @@ -39,47 +37,6 @@ func setupMetrics(a *echo.Group) { metrics.InitMetrics() - type countable struct { - Key string - Type interface{} - } - - for _, c := range []countable{ - { - metrics.ProjectCountKey, - models.Project{}, - }, - { - metrics.UserCountKey, - user.User{}, - }, - { - metrics.TaskCountKey, - models.Task{}, - }, - { - metrics.TeamCountKey, - models.Team{}, - }, - { - metrics.FilesCountKey, - files.File{}, - }, - { - metrics.AttachmentsCountKey, - models.TaskAttachment{}, - }, - } { - // Set initial totals - total, err := models.GetTotalCount(c.Type) - if err != nil { - log.Fatalf("Could not get initial count for %v, error was %s", c.Type, err) - } - if err := metrics.SetCount(total, c.Key); err != nil { - log.Fatalf("Could not set initial count for %v, error was %s", c.Type, err) - } - } - r := a.Group("/metrics") if config.MetricsUsername.GetString() != "" && config.MetricsPassword.GetString() != "" { From 06000b7a03d632097ab618d7ae95434c0dcd891d Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:03:13 +0200 Subject: [PATCH 06/14] refactor(metrics): drop the user count listener The user count is now counted on demand, so the increment-on-create listener is no longer needed. --- pkg/user/listeners.go | 31 ++++--------------------------- pkg/yaegi_symbols/vikunja_user.go | 1 - 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/pkg/user/listeners.go b/pkg/user/listeners.go index 7fdbe61a3..dfc4a5a2e 100644 --- a/pkg/user/listeners.go +++ b/pkg/user/listeners.go @@ -16,30 +16,7 @@ package user -import ( - "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/metrics" - "code.vikunja.io/api/pkg/modules/keyvalue" - "github.com/ThreeDotsLabs/watermill/message" -) - -func RegisterListeners() { - events.RegisterListener((&CreatedEvent{}).Name(), &IncreaseUserCounter{}) -} - -/////// -// User Events - -// IncreaseUserCounter represents a listener -type IncreaseUserCounter struct { -} - -// Name defines the name for the IncreaseUserCounter listener -func (s *IncreaseUserCounter) Name() string { - return "increase.user.counter" -} - -// Handle is executed when the event IncreaseUserCounter listens on is fired -func (s *IncreaseUserCounter) Handle(_ *message.Message) (err error) { - return keyvalue.IncrBy(metrics.UserCountKey, 1) -} +// RegisterListeners registers all event listeners for the user package. +// The user count metric is now counted on demand (see pkg/metrics), so there are +// currently no listeners to register. This hook is kept for future user events. +func RegisterListeners() {} diff --git a/pkg/yaegi_symbols/vikunja_user.go b/pkg/yaegi_symbols/vikunja_user.go index 7ec6a5e3b..86b3aed7a 100644 --- a/pkg/yaegi_symbols/vikunja_user.go +++ b/pkg/yaegi_symbols/vikunja_user.go @@ -159,7 +159,6 @@ func init() { "ErrUsernameReserved": reflect.ValueOf((*user.ErrUsernameReserved)(nil)), "ErrWrongUsernameOrPassword": reflect.ValueOf((*user.ErrWrongUsernameOrPassword)(nil)), "FailedLoginAttemptNotification": reflect.ValueOf((*user.FailedLoginAttemptNotification)(nil)), - "IncreaseUserCounter": reflect.ValueOf((*user.IncreaseUserCounter)(nil)), "InvalidTOTPNotification": reflect.ValueOf((*user.InvalidTOTPNotification)(nil)), "Login": reflect.ValueOf((*user.Login)(nil)), "PasswordAccountLockedAfterInvalidTOTPNotification": reflect.ValueOf((*user.PasswordAccountLockedAfterInvalidTOTPNotification)(nil)), From 72a231620d2a7234731a0dc080826766a7e9366e Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:03:14 +0200 Subject: [PATCH 07/14] refactor(metrics): drop the project/task/team/attachment count listeners These counts are now read from the database on demand. The events themselves stay - they are still used by webhooks and notifications. --- pkg/models/listeners.go | 118 ---------------------------- pkg/yaegi_symbols/vikunja_models.go | 8 -- 2 files changed, 126 deletions(-) diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index b0a580c21..83ec34c9b 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -26,8 +26,6 @@ import ( "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" - "code.vikunja.io/api/pkg/metrics" - "code.vikunja.io/api/pkg/modules/keyvalue" "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/user" @@ -38,16 +36,6 @@ import ( // RegisterListeners registers all event listeners func RegisterListeners() { - if config.MetricsEnabled.GetBool() { - events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{}) - events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{}) - events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{}) - events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{}) - events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{}) - events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{}) - events.RegisterListener((&TaskAttachmentCreatedEvent{}).Name(), &IncreaseAttachmentCounter{}) - events.RegisterListener((&TaskAttachmentDeletedEvent{}).Name(), &DecreaseAttachmentCounter{}) - } events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &SendTaskCommentNotification{}) events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SendTaskAssignedNotification{}) events.RegisterListener((&TaskDeletedEvent{}).Name(), &SendTaskDeletedNotification{}) @@ -99,34 +87,6 @@ func RegisterListeners() { ////// // Task Events -// IncreaseTaskCounter represents a listener -type IncreaseTaskCounter struct { -} - -// Name defines the name for the IncreaseTaskCounter listener -func (s *IncreaseTaskCounter) Name() string { - return "task.counter.increase" -} - -// Handle is executed when the event IncreaseTaskCounter listens on is fired -func (s *IncreaseTaskCounter) Handle(_ *message.Message) (err error) { - return keyvalue.IncrBy(metrics.TaskCountKey, 1) -} - -// DecreaseTaskCounter represents a listener -type DecreaseTaskCounter struct { -} - -// Name defines the name for the DecreaseTaskCounter listener -func (s *DecreaseTaskCounter) Name() string { - return "task.counter.decrease" -} - -// Handle is executed when the event DecreaseTaskCounter listens on is fired -func (s *DecreaseTaskCounter) Handle(_ *message.Message) (err error) { - return keyvalue.DecrBy(metrics.TaskCountKey, 1) -} - func notifyMentionedUsers(sess *xorm.Session, task *Task, text string, n notifications.NotificationWithSubject) (users map[int64]*user.User, err error) { users, err = FindMentionedUsersInText(sess, text) if err != nil { @@ -583,34 +543,6 @@ func (s *HandleTaskUpdateLastUpdated) Handle(msg *message.Message) (err error) { return sess.Commit() } -// IncreaseAttachmentCounter represents a listener -type IncreaseAttachmentCounter struct { -} - -// Name defines the name for the IncreaseAttachmentCounter listener -func (s *IncreaseAttachmentCounter) Name() string { - return "increase.attachment.counter" -} - -// Handle is executed when the event IncreaseAttachmentCounter listens on is fired -func (s *IncreaseAttachmentCounter) Handle(_ *message.Message) (err error) { - return keyvalue.IncrBy(metrics.AttachmentsCountKey, 1) -} - -// DecreaseAttachmentCounter represents a listener -type DecreaseAttachmentCounter struct { -} - -// Name defines the name for the DecreaseAttachmentCounter listener -func (s *DecreaseAttachmentCounter) Name() string { - return "decrease.attachment.counter" -} - -// Handle is executed when the event DecreaseAttachmentCounter listens on is fired -func (s *DecreaseAttachmentCounter) Handle(_ *message.Message) (err error) { - return keyvalue.DecrBy(metrics.AttachmentsCountKey, 1) -} - // UpdateTaskInSavedFilterViews represents a listener type UpdateTaskInSavedFilterViews struct { } @@ -738,28 +670,6 @@ func (l *UpdateTaskInSavedFilterViews) Handle(msg *message.Message) (err error) /////// // Project Event Listeners -type IncreaseProjectCounter struct { -} - -func (s *IncreaseProjectCounter) Name() string { - return "project.counter.increase" -} - -func (s *IncreaseProjectCounter) Handle(_ *message.Message) (err error) { - return keyvalue.IncrBy(metrics.ProjectCountKey, 1) -} - -type DecreaseProjectCounter struct { -} - -func (s *DecreaseProjectCounter) Name() string { - return "project.counter.decrease" -} - -func (s *DecreaseProjectCounter) Handle(_ *message.Message) (err error) { - return keyvalue.DecrBy(metrics.ProjectCountKey, 1) -} - // SendProjectCreatedNotification represents a listener type SendProjectCreatedNotification struct { } @@ -1259,34 +1169,6 @@ func (wl *WebhookListener) Handle(msg *message.Message) (err error) { /////// // Team Events -// IncreaseTeamCounter represents a listener -type IncreaseTeamCounter struct { -} - -// Name defines the name for the IncreaseTeamCounter listener -func (s *IncreaseTeamCounter) Name() string { - return "team.counter.increase" -} - -// Handle is executed when the event IncreaseTeamCounter listens on is fired -func (s *IncreaseTeamCounter) Handle(_ *message.Message) (err error) { - return keyvalue.IncrBy(metrics.TeamCountKey, 1) -} - -// DecreaseTeamCounter represents a listener -type DecreaseTeamCounter struct { -} - -// Name defines the name for the DecreaseTeamCounter listener -func (s *DecreaseTeamCounter) Name() string { - return "team.counter.decrease" -} - -// Handle is executed when the event DecreaseTeamCounter listens on is fired -func (s *DecreaseTeamCounter) Handle(_ *message.Message) (err error) { - return keyvalue.DecrBy(metrics.TeamCountKey, 1) -} - // CleanupTaskAssignmentsAfterTeamRemoval represents a listener type CleanupTaskAssignmentsAfterTeamRemoval struct{} diff --git a/pkg/yaegi_symbols/vikunja_models.go b/pkg/yaegi_symbols/vikunja_models.go index f33da550c..cd2621c81 100644 --- a/pkg/yaegi_symbols/vikunja_models.go +++ b/pkg/yaegi_symbols/vikunja_models.go @@ -315,10 +315,6 @@ func init() { "CleanupTaskAssignmentsAfterTeamRemoval": reflect.ValueOf((*models.CleanupTaskAssignmentsAfterTeamRemoval)(nil)), "DataExportReadyNotification": reflect.ValueOf((*models.DataExportReadyNotification)(nil)), "DatabaseNotifications": reflect.ValueOf((*models.DatabaseNotifications)(nil)), - "DecreaseAttachmentCounter": reflect.ValueOf((*models.DecreaseAttachmentCounter)(nil)), - "DecreaseProjectCounter": reflect.ValueOf((*models.DecreaseProjectCounter)(nil)), - "DecreaseTaskCounter": reflect.ValueOf((*models.DecreaseTaskCounter)(nil)), - "DecreaseTeamCounter": reflect.ValueOf((*models.DecreaseTeamCounter)(nil)), "ErrAPITokenInvalid": reflect.ValueOf((*models.ErrAPITokenInvalid)(nil)), "ErrAttachmentDoesNotBelongToTask": reflect.ValueOf((*models.ErrAttachmentDoesNotBelongToTask)(nil)), "ErrBucketDoesNotBelongToProjectView": reflect.ValueOf((*models.ErrBucketDoesNotBelongToProjectView)(nil)), @@ -405,10 +401,6 @@ func init() { "HandleTaskUpdateLastUpdated": reflect.ValueOf((*models.HandleTaskUpdateLastUpdated)(nil)), "HandleTaskUpdatedMentions": reflect.ValueOf((*models.HandleTaskUpdatedMentions)(nil)), "HandleUserDataExport": reflect.ValueOf((*models.HandleUserDataExport)(nil)), - "IncreaseAttachmentCounter": reflect.ValueOf((*models.IncreaseAttachmentCounter)(nil)), - "IncreaseProjectCounter": reflect.ValueOf((*models.IncreaseProjectCounter)(nil)), - "IncreaseTaskCounter": reflect.ValueOf((*models.IncreaseTaskCounter)(nil)), - "IncreaseTeamCounter": reflect.ValueOf((*models.IncreaseTeamCounter)(nil)), "Label": reflect.ValueOf((*models.Label)(nil)), "LabelByTaskIDsOptions": reflect.ValueOf((*models.LabelByTaskIDsOptions)(nil)), "LabelTask": reflect.ValueOf((*models.LabelTask)(nil)), From 9e3e884dac7771eabaf84b728fc2db3e443b3412 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:03:14 +0200 Subject: [PATCH 08/14] refactor(metrics): drop inline file count tracking The file count is now read from the database on demand. --- pkg/files/files.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/files/files.go b/pkg/files/files.go index a103a5237..a8d702913 100644 --- a/pkg/files/files.go +++ b/pkg/files/files.go @@ -28,8 +28,6 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/log" - "code.vikunja.io/api/pkg/metrics" - "code.vikunja.io/api/pkg/modules/keyvalue" "code.vikunja.io/api/pkg/web" "github.com/c2h5oh/datasize" @@ -205,7 +203,7 @@ func (f *File) Delete(s *xorm.Session) (err error) { return err } - return keyvalue.DecrBy(metrics.FilesCountKey, 1) + return nil } // Save saves a file to storage @@ -214,5 +212,5 @@ func (f *File) Save(fcontent io.ReadSeeker) error { if err != nil { return fmt.Errorf("failed to save file: %w", err) } - return keyvalue.IncrBy(metrics.FilesCountKey, 1) + return nil } From 0248bdf5e7f97f2f491764d9a5be3faa3bf3a82a Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:03:18 +0200 Subject: [PATCH 09/14] feat(metrics): invalidate the user count cache on registration Registration is the one hot path where instant freshness is worth an extra COUNT(*), so bust the cache there rather than waiting for the TTL. --- pkg/routes/api/v1/user_register.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go index 96a9c02d9..9db52c88a 100644 --- a/pkg/routes/api/v1/user_register.go +++ b/pkg/routes/api/v1/user_register.go @@ -22,6 +22,8 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" @@ -85,5 +87,13 @@ func RegisterUser(c *echo.Context) error { return err } + // Bust the cached user count so the new registration shows up in metrics + // immediately instead of after the regular cache expiry. + if config.MetricsEnabled.GetBool() { + if err := metrics.InvalidateCount(metrics.UserCountKey); err != nil { + log.Errorf("Could not invalidate user count metric: %s", err) + } + } + return c.JSON(http.StatusOK, newUser) } From 054050b1e2d69d1fbc6f388346ee048975490e2d Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:09:37 +0200 Subject: [PATCH 10/14] test(keyvalue): cover RememberFor TTL caching --- pkg/modules/keyvalue/keyvalue_test.go | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pkg/modules/keyvalue/keyvalue_test.go b/pkg/modules/keyvalue/keyvalue_test.go index 9d7c10a8b..b95fc2ec5 100644 --- a/pkg/modules/keyvalue/keyvalue_test.go +++ b/pkg/modules/keyvalue/keyvalue_test.go @@ -19,6 +19,7 @@ package keyvalue import ( "errors" "testing" + "time" "code.vikunja.io/api/pkg/modules/keyvalue/memory" "github.com/stretchr/testify/assert" @@ -81,3 +82,53 @@ func TestRememberErrorDoesNotStore(t *testing.T) { require.NoError(t, err2) assert.False(t, exists) } + +func TestRememberForReturnsCachedWithinTTL(t *testing.T) { + store = memory.NewStorage() + + called := 0 + fn := func() (int64, error) { + called++ + return int64(called), nil + } + + val, err := RememberFor("foo", time.Hour, fn) + require.NoError(t, err) + assert.Equal(t, int64(1), val) + + // Still within the TTL, so fn must not be called again. + val, err = RememberFor("foo", time.Hour, fn) + require.NoError(t, err) + assert.Equal(t, int64(1), val) + assert.Equal(t, 1, called) +} + +func TestRememberForRecomputesAfterExpiry(t *testing.T) { + store = memory.NewStorage() + + // Seed an already-expired value. + require.NoError(t, Put("foo", expiringValue[int64]{Value: 1, ExpiresAt: time.Now().Add(-time.Minute)})) + + called := 0 + val, err := RememberFor("foo", time.Hour, func() (int64, error) { + called++ + return 2, nil + }) + + require.NoError(t, err) + assert.Equal(t, int64(2), val) + assert.Equal(t, 1, called) +} + +func TestRememberForErrorDoesNotStore(t *testing.T) { + store = memory.NewStorage() + + _, err := RememberFor("foo", time.Hour, func() (int64, error) { + return 0, errors.New("fail") + }) + + require.Error(t, err) + _, exists, err2 := Get("foo") + require.NoError(t, err2) + assert.False(t, exists) +} From 71dcb096be99653734539e4357f4ad3fcdb86e82 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:09:37 +0200 Subject: [PATCH 11/14] test(metrics): verify counts are read from the right table --- pkg/models/metrics_count_test.go | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 pkg/models/metrics_count_test.go diff --git a/pkg/models/metrics_count_test.go b/pkg/models/metrics_count_test.go new file mode 100644 index 000000000..c8f64bac6 --- /dev/null +++ b/pkg/models/metrics_count_test.go @@ -0,0 +1,62 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/metrics" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMetricsCountFromDatabase verifies that each metric key counts the right table +// straight from the database. This guards the count key -> table name mapping; the +// caching/expiry/invalidation behaviour itself is covered by the keyvalue RememberFor +// tests. +func TestMetricsCountFromDatabase(t *testing.T) { + cases := map[string]string{ + metrics.UserCountKey: "users", + metrics.ProjectCountKey: "projects", + metrics.TaskCountKey: "tasks", + metrics.TeamCountKey: "teams", + metrics.FilesCountKey: "files", + metrics.AttachmentsCountKey: "task_attachments", + } + + db.LoadAndAssertFixtures(t) + + s := db.NewSession() + defer s.Close() + + for key, table := range cases { + t.Run(table, func(t *testing.T) { + // Drop any value cached by a previous test so we recompute from the DB. + require.NoError(t, metrics.InvalidateCount(key)) + + expected, err := s.Table(table).Count() + require.NoError(t, err) + + count, err := metrics.GetCount(key) + require.NoError(t, err) + assert.Equal(t, expected, count) + assert.Positive(t, count, "fixtures should contain at least one %s", table) + }) + } +} From 9a810f7632b63de99791531046fe01e5dea7eef0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:35:21 +0200 Subject: [PATCH 12/14] refactor(user): remove the now-empty listeners file The user package no longer registers any event listeners, so drop the empty RegisterListeners hook and its caller. --- pkg/initialize/init.go | 1 - pkg/user/listeners.go | 22 ---------------------- pkg/yaegi_symbols/vikunja_user.go | 1 - 3 files changed, 24 deletions(-) delete mode 100644 pkg/user/listeners.go diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go index df11ef01f..dca17cb60 100644 --- a/pkg/initialize/init.go +++ b/pkg/initialize/init.go @@ -145,7 +145,6 @@ func FullInit() { // Start processing events go func() { models.RegisterListeners() - user.RegisterListeners() migrationHandler.RegisterListeners() ws.RegisterListeners() err := events.InitEvents() diff --git a/pkg/user/listeners.go b/pkg/user/listeners.go deleted file mode 100644 index dfc4a5a2e..000000000 --- a/pkg/user/listeners.go +++ /dev/null @@ -1,22 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-present Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package user - -// RegisterListeners registers all event listeners for the user package. -// The user count metric is now counted on demand (see pkg/metrics), so there are -// currently no listeners to register. This hook is kept for future user events. -func RegisterListeners() {} diff --git a/pkg/yaegi_symbols/vikunja_user.go b/pkg/yaegi_symbols/vikunja_user.go index 86b3aed7a..268869221 100644 --- a/pkg/yaegi_symbols/vikunja_user.go +++ b/pkg/yaegi_symbols/vikunja_user.go @@ -99,7 +99,6 @@ func init() { "ListAllUsers": reflect.ValueOf(user.ListAllUsers), "ListUsers": reflect.ValueOf(user.ListUsers), "RegisterDeletionNotificationCron": reflect.ValueOf(user.RegisterDeletionNotificationCron), - "RegisterListeners": reflect.ValueOf(user.RegisterListeners), "RegisterTokenCleanupCron": reflect.ValueOf(user.RegisterTokenCleanupCron), "RequestDeletion": reflect.ValueOf(user.RequestDeletion), "RequestUserPasswordResetToken": reflect.ValueOf(user.RequestUserPasswordResetToken), From e31d73b3dfc991383bdd5c5adb4d99db855121cc Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 15:37:38 +0200 Subject: [PATCH 13/14] fix(keyvalue): treat undecodable cached values as a cache miss A GetWithValue deserialization error in RememberFor was returned as fatal. On a Redis upgrade the metrics counters live under the same keys as before but were stored as plain int64, so the first decode into the new envelope would fail and the metric would break permanently. Treat such errors as a miss and recompute/overwrite so the cache self-heals. --- pkg/modules/keyvalue/keyvalue.go | 10 +++++----- pkg/modules/keyvalue/keyvalue_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/pkg/modules/keyvalue/keyvalue.go b/pkg/modules/keyvalue/keyvalue.go index c71e00063..137ea8f62 100644 --- a/pkg/modules/keyvalue/keyvalue.go +++ b/pkg/modules/keyvalue/keyvalue.go @@ -155,14 +155,14 @@ type expiringValue[T any] struct { // older than ttl. On a miss or once expired, it executes fn, caches the result for // ttl and returns it. If fn returns an error, nothing is cached. // T must be a concrete (non-pointer) type. +// +// A value that cannot be deserialized into the expected type is treated as a cache +// miss and overwritten, so the cache self-heals across upgrades that change what a key +// stores (e.g. a key that previously held a plain int64 in Redis). func RememberFor[T any](key string, ttl time.Duration, fn func() (T, error)) (T, error) { var cached expiringValue[T] exists, err := GetWithValue(key, &cached) - if err != nil { - var zero T - return zero, err - } - if exists && time.Now().Before(cached.ExpiresAt) { + if err == nil && exists && time.Now().Before(cached.ExpiresAt) { return cached.Value, nil } diff --git a/pkg/modules/keyvalue/keyvalue_test.go b/pkg/modules/keyvalue/keyvalue_test.go index b95fc2ec5..ca32d8aec 100644 --- a/pkg/modules/keyvalue/keyvalue_test.go +++ b/pkg/modules/keyvalue/keyvalue_test.go @@ -132,3 +132,28 @@ func TestRememberForErrorDoesNotStore(t *testing.T) { require.NoError(t, err2) assert.False(t, exists) } + +// getWithValueErrorStore simulates a backend that cannot deserialize an existing value +// into the requested type, e.g. a key that held a plain int64 before the cache started +// storing a struct (the pre-refactor metrics counters in Redis). +type getWithValueErrorStore struct { + *memory.Storage +} + +func (s *getWithValueErrorStore) GetWithValue(string, interface{}) (bool, error) { + return false, errors.New("decode error") +} + +func TestRememberForRecomputesWhenStoredValueCannotBeDeserialized(t *testing.T) { + store = &getWithValueErrorStore{memory.NewStorage()} + + called := 0 + val, err := RememberFor("foo", time.Hour, func() (int64, error) { + called++ + return 42, nil + }) + + require.NoError(t, err) + assert.Equal(t, int64(42), val) + assert.Equal(t, 1, called) +} From 137f31bb205c3fe254a2db929bafb538b3271f85 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 30 May 2026 16:12:17 +0200 Subject: [PATCH 14/14] fix(docker): make /tmp world-writable so exports work under any UID The scratch image shipped /tmp owned by 1000:1000 and writable only by UID 1000, so containers run under a different user (e.g. Unraid's 99:100, OpenShift random UIDs, or any `user:` override) could not create the temp file used for data exports, failing with: error creating temp file: open /tmp/vikunja-export-*.zip: permission denied The builder-stage `chmod 1777 /tmp` did not survive into the final image (see #2316, which had to add --chown to make it writable for UID 1000), so the world-writable intent was lost. Force the mode at copy time with BuildKit's --chmod=1777, restoring a normal sticky, world-writable /tmp that works for every UID. Closes go-vikunja/vikunja#2755 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8075aeee2..ce2e4aec2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ WORKDIR /app/vikunja ENTRYPOINT [ "/app/vikunja/vikunja" ] EXPOSE 3456 -COPY --from=apibuilder --chown=1000:1000 /tmp /tmp +COPY --from=apibuilder --chown=1000:1000 --chmod=1777 /tmp /tmp USER 1000