Merge branch 'main' into fix/recurring-task-bucket-placement
This commit is contained in:
commit
784362cf17
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<jwt-secret>/$NEW_SECRET/g" /etc/vikunja/config.yml
|
||||
sed -i "s/<rootpath>/\/opt\/vikunja\//g" /etc/vikunja/config.yml
|
||||
sed -i "s/path: \"\.\/vikunja.db\"/path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml
|
||||
|
|
|
|||
|
|
@ -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/<jwt-secret>/$NEW_SECRET/g" /etc/vikunja/config.yml
|
||||
sed -i "s/<rootpath>/\/opt\/vikunja\//g" /etc/vikunja/config.yml
|
||||
sed -i "s/path: \"\.\/vikunja.db\"/path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ func FullInit() {
|
|||
// Start processing events
|
||||
go func() {
|
||||
models.RegisterListeners()
|
||||
user.RegisterListeners()
|
||||
migrationHandler.RegisterListeners()
|
||||
ws.RegisterListeners()
|
||||
err := events.InitEvents()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
//
|
||||
// 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 && 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,78 @@ 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() != "" {
|
||||
|
|
|
|||
|
|
@ -1,45 +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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
@ -159,7 +158,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)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue