From 69b9bc48971a0e1bb8828627ca210d06e2d5ed51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:55:39 +0200 Subject: [PATCH 1/2] build(deps-dev): Bump esbuild from 0.27.0 to 0.28.1 (#951) * build(deps-dev): Bump esbuild from 0.27.0 to 0.28.1 Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.0 to 0.28.1. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2025.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.27.0...v0.28.1) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.28.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * build(deps-dev): sync pnpm.overrides esbuild to 0.28.1 Dependabot only bumped apps/web/package.json but not the root pnpm.overrides which pinned esbuild to 0.27.0. This caused ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on CI. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ruslan Konviser --- apps/web/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 256 +++++++++++++++++++++--------------------- 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index e245f043a..b767dc9bb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -189,7 +189,7 @@ "cross-env": "^10.1.0", "drizzle-kit": "^0.31.7", "drizzle-seed": "^0.3.1", - "esbuild": "^0.27.0", + "esbuild": "^0.28.1", "esbuild-register": "^3.6.0", "eslint": "^9", "eslint-plugin-react": "^7.37.5", diff --git a/package.json b/package.json index 8620b12df..276ce41b0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "overrides": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3", - "esbuild": "0.27.0", + "esbuild": "0.28.1", "esbuild-register": "3.6.0", "@opentelemetry/api": "1.9.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90f1f2ad8..1e9bd84a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: '@types/react': 19.2.7 '@types/react-dom': 19.2.3 - esbuild: 0.27.0 + esbuild: 0.28.1 esbuild-register: 3.6.0 '@opentelemetry/api': 1.9.0 @@ -198,7 +198,7 @@ importers: version: 10.42.0 '@sentry/nextjs': specifier: ^10.27.0 - version: 10.42.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.97.3))(react@19.2.5)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)) + version: 10.42.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.97.3))(react@19.2.5)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)) '@sentry/replay': specifier: ^7.116.0 version: 7.116.0 @@ -549,11 +549,11 @@ importers: specifier: ^0.3.1 version: 0.3.1(drizzle-orm@0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(postgres@3.4.8)) esbuild: - specifier: 0.27.0 - version: 0.27.0 + specifier: 0.28.1 + version: 0.28.1 esbuild-register: specifier: 3.6.0 - version: 3.6.0(esbuild@0.27.0) + version: 3.6.0(esbuild@0.28.1) eslint: specifier: ^9 version: 9.39.4(jiti@2.6.1) @@ -2289,158 +2289,158 @@ packages: resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} deprecated: 'Merged into tsx: https://tsx.is' - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -8403,10 +8403,10 @@ packages: esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: - esbuild: 0.27.0 + esbuild: 0.28.1 - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -15519,7 +15519,7 @@ snapshots: '@esbuild-kit/core-utils@3.3.2': dependencies: - esbuild: 0.27.0 + esbuild: 0.28.1 source-map-support: 0.5.21 '@esbuild-kit/esm-loader@2.6.5': @@ -15527,82 +15527,82 @@ snapshots: '@esbuild-kit/core-utils': 3.3.2 get-tsconfig: 4.13.6 - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': @@ -19746,7 +19746,7 @@ snapshots: '@sentry/types': 7.116.0 '@sentry/utils': 7.116.0 - '@sentry/nextjs@10.42.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.97.3))(react@19.2.5)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0))': + '@sentry/nextjs@10.42.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.97.3))(react@19.2.5)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 @@ -19758,7 +19758,7 @@ snapshots: '@sentry/opentelemetry': 10.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) '@sentry/react': 10.42.0(react@19.2.5) '@sentry/vercel-edge': 10.42.0 - '@sentry/webpack-plugin': 5.1.1(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)) + '@sentry/webpack-plugin': 5.1.1(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)) next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.97.3) rollup: 4.59.0 stacktrace-parser: 0.1.11 @@ -19859,11 +19859,11 @@ snapshots: '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@sentry/core': 10.42.0 - '@sentry/webpack-plugin@5.1.1(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0))': + '@sentry/webpack-plugin@5.1.1(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1))': dependencies: '@sentry/bundler-plugin-core': 5.1.1 uuid: 9.0.1 - webpack: 5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0) + webpack: 5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1) transitivePeerDependencies: - encoding - supports-color @@ -22705,8 +22705,8 @@ snapshots: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.27.0 - esbuild-register: 3.6.0(esbuild@0.27.0) + esbuild: 0.28.1 + esbuild-register: 3.6.0(esbuild@0.28.1) transitivePeerDependencies: - supports-color @@ -22940,41 +22940,41 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-register@3.6.0(esbuild@0.27.0): + esbuild-register@3.6.0(esbuild@0.28.1): dependencies: debug: 4.4.3(supports-color@5.5.0) - esbuild: 0.27.0 + esbuild: 0.28.1 transitivePeerDependencies: - supports-color - esbuild@0.27.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -27790,16 +27790,16 @@ snapshots: terminal-columns@1.4.1: {} - terser-webpack-plugin@5.3.17(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)): + terser-webpack-plugin@5.3.17(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.0 - webpack: 5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0) + webpack: 5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1) optionalDependencies: '@swc/core': 1.15.33(@swc/helpers@0.5.19) - esbuild: 0.27.0 + esbuild: 0.28.1 terser-webpack-plugin@5.3.17(webpack@5.105.4): dependencies: @@ -27905,7 +27905,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.0 + esbuild: 0.28.1 get-tsconfig: 4.13.6 optionalDependencies: fsevents: 2.3.3 @@ -28399,7 +28399,7 @@ snapshots: - esbuild - uglify-js - webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0): + webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -28423,7 +28423,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.27.0)) + terser-webpack-plugin: 5.3.17(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)(webpack@5.105.4(@swc/core@1.15.33(@swc/helpers@0.5.19))(esbuild@0.28.1)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: From ef29b9887e52945b8b9ba191047ff52844de2c4f Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Wed, 17 Jun 2026 11:18:17 +0200 Subject: [PATCH 2/2] feat(items): bump sparkline + flicker-free rollback on upvote (rebased from #945) (#961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(items): bump sparkline + flicker-free rollback on upvote The Statistics card's optimistic vote sync already updates `totals.votes` on develop. This adds two refinements (rebased slice of #945): - Today's sparkline bar now moves on the same frame as the vote button, not just the total — the chart and the number stay visually in sync. - onError restores a snapshot of the activity cache instead of invalidating (refetching), which removes the brief refetch flicker on rollback when a vote fails (offline / 5xx). - Exports a shared `ITEM_ACTIVITY_QUERY_KEY` constant and `ItemActivityPayload` type from `item-stats-section.tsx` so mutators target the same cache key without restating the literal. Co-authored-by: joel-kalema Co-Authored-By: Claude Opus 4.7 * fix(items): cancel activity refetch + derive vote delta from snapshot Addresses Augment review on #961: - cancelQueries now covers both ['item-votes', id] and the activity query key in vote/unvote onMutate, so an in-flight activity refetch cannot clobber the optimistic update we are about to write. - Derives the signed delta inside setQueryData(old) instead of from the captured snapshot, then reuses the applied delta when patching the activity cache so the item-votes update and Statistics sparkline stay consistent under concurrent refetches. - docs/log.md entry under 2026-06-17 per spec-037 DoD. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: joel-kalema Co-authored-by: Claude Opus 4.7 --- .../item-detail/item-stats-section.tsx | 29 ++-- apps/web/hooks/use-item-vote.ts | 130 +++++++++++++----- docs/log.md | 21 +++ 3 files changed, 135 insertions(+), 45 deletions(-) diff --git a/apps/web/components/item-detail/item-stats-section.tsx b/apps/web/components/item-detail/item-stats-section.tsx index 3eb38cc06..20affb457 100644 --- a/apps/web/components/item-detail/item-stats-section.tsx +++ b/apps/web/components/item-detail/item-stats-section.tsx @@ -6,6 +6,19 @@ import { useLocale, useTranslations } from 'next-intl'; import { Eye, ThumbsUp, Heart, MessageSquare, Star, Clock, BarChart3 } from 'lucide-react'; import type { ItemActivityDay, ItemEngagementMetrics } from '@/lib/db/queries/engagement.queries'; +/** + * Cache-key prefix for the per-item activity payload. Exported so the vote + * hook (and any future mutator) can target the same cache via + * `queryClient.setQueriesData({ queryKey: [ITEM_ACTIVITY_QUERY_KEY, slug] })` + * to apply optimistic updates without restating the literal. + */ +export const ITEM_ACTIVITY_QUERY_KEY = 'item-activity' as const; + +export interface ItemActivityPayload { + totals: ItemEngagementMetrics; + series: ItemActivityDay[]; +} + interface ItemStatsSectionProps { itemSlug: string; publishedAt?: string; @@ -157,11 +170,6 @@ function Sparkline({ series, metric }: SparklineProps) { ); } -interface ActivityPayload { - totals: ItemEngagementMetrics; - series: ItemActivityDay[]; -} - /** * Compact sidebar Statistics card with an inline sparkline. * @@ -171,18 +179,23 @@ interface ActivityPayload { * are clickable — selecting one highlights it with `theme-primary` and * re-plots the sparkline below from `/api/items/[slug]/activity`. Rating * and Listed rows are static. + * + * Data lives in the shared React Query cache under + * `[ITEM_ACTIVITY_QUERY_KEY, itemSlug, days]` so mutations (e.g. the upvote + * button via `useItemVote`) can patch it optimistically — the Upvotes total + * and today's sparkline point update on the same frame as the vote. */ export function ItemStatsSection({ itemSlug, publishedAt, days = 30 }: ItemStatsSectionProps) { const t = useTranslations(); const locale = useLocale(); const [selected, setSelected] = useState('views'); - const { data, isSuccess: loaded } = useQuery({ - queryKey: ['item-activity', itemSlug, days], + const { data, isSuccess: loaded } = useQuery({ + queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemSlug, days], queryFn: async () => { const res = await fetch(`/api/items/${encodeURIComponent(itemSlug)}/activity?days=${days}`); if (!res.ok) throw new Error('Failed to fetch activity'); - return res.json() as Promise; + return res.json() as Promise; }, enabled: !!itemSlug, staleTime: 1000 * 60 * 5, diff --git a/apps/web/hooks/use-item-vote.ts b/apps/web/hooks/use-item-vote.ts index 468d8623e..6af093d1e 100644 --- a/apps/web/hooks/use-item-vote.ts +++ b/apps/web/hooks/use-item-vote.ts @@ -5,12 +5,48 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { useCurrentUser } from './use-current-user'; import { serverClient, apiUtils } from '@/lib/api/server-api-client'; +import { + ITEM_ACTIVITY_QUERY_KEY, + type ItemActivityPayload +} from '@/components/item-detail/item-stats-section'; interface ItemVoteResponse { count: number; userVote: 'up' | 'down' | null; } +/** + * Apply a signed delta to every cached activity payload for an item (the + * cache has one entry per `days` window). Bumps both `totals.votes` and + * today's sparkline point so the sidebar Statistics card visibly moves on + * the same frame as the vote button. + */ +function patchActivityForVoteDelta( + queryClient: ReturnType, + itemId: string, + delta: number +) { + if (delta === 0) return; + queryClient.setQueriesData( + { queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] }, + (old) => { + if (!old) return old; + const lastIdx = old.series.length - 1; + const newSeries = + lastIdx >= 0 + ? [ + ...old.series.slice(0, lastIdx), + { ...old.series[lastIdx], votes: (old.series[lastIdx]?.votes ?? 0) + delta } + ] + : old.series; + return { + totals: { ...old.totals, votes: (old.totals?.votes ?? 0) + delta }, + series: newSeries + }; + } + ); +} + export function useItemVote(itemId: string) { const { user } = useCurrentUser(); const loginModal = useLoginModal(); @@ -83,32 +119,42 @@ export function useItemVote(itemId: string) { return; } - await queryClient.cancelQueries({ queryKey: ['item-votes', itemId] }); + // Cancel both query keys we're about to mutate so in-flight refetches + // can't overwrite the optimistic update after we've snapshotted. + await Promise.all([ + queryClient.cancelQueries({ queryKey: ['item-votes', itemId] }), + queryClient.cancelQueries({ queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] }) + ]); const previousVotes = queryClient.getQueryData(['item-votes', itemId]); + // Snapshot the activity cache so we can roll back on error without + // triggering a refetch flicker. + const previousActivity = queryClient.getQueriesData({ + queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] + }); + // Derive the signed delta from the SAME snapshot we used to feed the + // item-votes cache update — so the value patched into the activity + // cache stays consistent with what we wrote into item-votes. + let appliedDelta = 0; queryClient.setQueryData(['item-votes', itemId], (old) => { - if (!old) return { count: type === 'up' ? 1 : -1, userVote: type }; - - const countDiff = old.userVote === type ? -1 : old.userVote === null ? 1 : 2; + if (!old) { + const seedCount = type === 'up' ? 1 : -1; + appliedDelta = seedCount; + return { count: seedCount, userVote: type }; + } + const oldUserVote = old.userVote; + const countDiff = oldUserVote === type ? -1 : oldUserVote === null ? 1 : 2; + const delta = type === 'up' ? countDiff : -countDiff; + appliedDelta = delta; return { - count: old.count + (type === 'up' ? countDiff : -countDiff), - userVote: old.userVote === type ? null : type + count: old.count + delta, + userVote: oldUserVote === type ? null : type }; }); - // Optimistically update the Statistics card so upvotes appear instantly - const prevVote = previousVotes?.userVote ?? null; - const voteChange = prevVote === type ? -1 : prevVote === null ? 1 : 2; - const voteDelta = type === 'up' ? voteChange : -voteChange; - queryClient.setQueriesData<{ totals: { votes: number; [k: string]: any }; series: any[] }>( - { queryKey: ['item-activity', itemId], exact: false }, - (old) => { - if (!old) return old; - return { ...old, totals: { ...old.totals, votes: old.totals.votes + voteDelta } }; - } - ); + patchActivityForVoteDelta(queryClient, itemId, appliedDelta); - return { previousVotes }; + return { previousVotes, previousActivity }; }, onSuccess: (data) => { // Update cache with server data to ensure consistency @@ -117,14 +163,17 @@ export function useItemVote(itemId: string) { queryClient.setQueryData(['item-votes', itemId], data); } // Sync the Statistics card with the authoritative server count - queryClient.invalidateQueries({ queryKey: ['item-activity', itemId] }); + queryClient.invalidateQueries({ queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] }); }, onError: (error, _, context) => { if (context?.previousVotes) { queryClient.setQueryData(['item-votes', itemId], context.previousVotes); } - // Revert the optimistic Statistics update - queryClient.invalidateQueries({ queryKey: ['item-activity', itemId] }); + if (context?.previousActivity) { + for (const [key, value] of context.previousActivity) { + queryClient.setQueryData(key, value); + } + } // Don't show error toast if user is not logged in (handled by login modal) if (!error.message.includes('sign in') && !error.message.includes('Authentication required')) { @@ -165,29 +214,33 @@ export function useItemVote(itemId: string) { return; } - await queryClient.cancelQueries({ queryKey: ['item-votes', itemId] }); + await Promise.all([ + queryClient.cancelQueries({ queryKey: ['item-votes', itemId] }), + queryClient.cancelQueries({ queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] }) + ]); const previousVotes = queryClient.getQueryData(['item-votes', itemId]); + const previousActivity = queryClient.getQueriesData({ + queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] + }); + // Derive the signed delta from the SAME `old` snapshot we hand to + // setQueryData so the activity patch stays consistent with the + // item-votes update. + let appliedDelta = 0; queryClient.setQueryData(['item-votes', itemId], (old) => { if (!old) return { count: 0, userVote: null }; + const oldUserVote = old.userVote; + const delta = oldUserVote === 'up' ? -1 : oldUserVote === 'down' ? 1 : 0; + appliedDelta = delta; return { - count: old.count + (old.userVote === 'up' ? -1 : old.userVote === 'down' ? 1 : 0), + count: old.count + delta, userVote: null }; }); - // Optimistically update the Statistics card - const prevVote = previousVotes?.userVote ?? null; - const voteDelta = prevVote === 'up' ? -1 : prevVote === 'down' ? 1 : 0; - queryClient.setQueriesData<{ totals: { votes: number; [k: string]: any }; series: any[] }>( - { queryKey: ['item-activity', itemId], exact: false }, - (old) => { - if (!old) return old; - return { ...old, totals: { ...old.totals, votes: old.totals.votes + voteDelta } }; - } - ); + patchActivityForVoteDelta(queryClient, itemId, appliedDelta); - return { previousVotes }; + return { previousVotes, previousActivity }; }, onSuccess: (data) => { // Update cache with server data to ensure consistency @@ -196,14 +249,17 @@ export function useItemVote(itemId: string) { queryClient.setQueryData(['item-votes', itemId], data); } // Sync the Statistics card with the authoritative server count - queryClient.invalidateQueries({ queryKey: ['item-activity', itemId] }); + queryClient.invalidateQueries({ queryKey: [ITEM_ACTIVITY_QUERY_KEY, itemId] }); }, onError: (error, _, context) => { if (context?.previousVotes) { queryClient.setQueryData(['item-votes', itemId], context.previousVotes); } - // Revert the optimistic Statistics update - queryClient.invalidateQueries({ queryKey: ['item-activity', itemId] }); + if (context?.previousActivity) { + for (const [key, value] of context.previousActivity) { + queryClient.setQueryData(key, value); + } + } toast.error(error.message || 'An error occurred while removing your vote'); } }); diff --git a/docs/log.md b/docs/log.md index 7bced331f..9351edd88 100644 --- a/docs/log.md +++ b/docs/log.md @@ -31,6 +31,27 @@ why** at a higher level than per-commit diffs. --- +## 2026-06-17 — Feat: instant sparkline bump + flicker-free rollback on upvote + +- spec-037: the item-detail Statistics card already tracked total votes + optimistically via `useItemVote`'s `setQueryData(['item-votes', …])`, but + the sparkline (today's bar) and the activity totals lagged a full network + round-trip behind the vote button — and a failed mutation triggered a + cache refetch instead of a snapshot rollback, producing a brief flicker. +- Exported `ITEM_ACTIVITY_QUERY_KEY` + `ItemActivityPayload` from + `apps/web/components/item-detail/item-stats-section.tsx` so mutators can + surgically patch the activity cache without restating the cache key. +- `apps/web/hooks/use-item-vote.ts` now `cancelQueries` for both + `['item-votes', id]` and `[ITEM_ACTIVITY_QUERY_KEY, id]` before + snapshotting, derives the signed delta inside `setQueryData(old)` so the + count + userVote + activity bump stay consistent under concurrent + refetches, applies that same delta to the activity totals + today's + sparkline point on the same frame as the vote, and on error restores + the snapshot via `setQueryData(key, value)` to avoid the refetch flicker. + `onSuccess` still invalidates the activity query so the next render + reconciles with the authoritative server count. +- Credits @joel-kalema (rebased from #945 onto fresh develop). PR #961. + ## 2026-06-16 — Fix: CI-safe git-CMS writes + favorite-toggle e2e race - spec-039: the e2e suite's authenticated write-flow specs (admin create