fix: tech debt batch 1 — TS errors, vulnerabilities, dead code

- Fixed 12 TypeScript errors across analytics.ts, observer-project-detail.tsx, bulk-upload/page.tsx, settings/profile/page.tsx
- npm audit: 8 vulnerabilities resolved (1 critical, 4 high, 3 moderate)
- Deleted 3 dead files: live-control.ts (618 lines), feature-flags.ts, file-type-categories.ts
- Removed typescript.ignoreBuildErrors: true — TS errors now block builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 23:51:44 +01:00
parent 1ebdf5f9c9
commit 1356809cb1
9 changed files with 149 additions and 842 deletions

View File

@@ -4,8 +4,7 @@ const nextConfig: NextConfig = {
output: 'standalone', output: 'standalone',
serverExternalPackages: ['@prisma/client', 'minio'], serverExternalPackages: ['@prisma/client', 'minio'],
typescript: { typescript: {
// We run tsc --noEmit separately before each push ignoreBuildErrors: false,
ignoreBuildErrors: true,
}, },
experimental: { experimental: {
optimizePackageImports: [ optimizePackageImports: [

263
package-lock.json generated
View File

@@ -3830,9 +3830,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3844,9 +3844,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3858,9 +3858,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3872,9 +3872,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3886,9 +3886,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3900,9 +3900,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3914,9 +3914,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3928,9 +3928,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3942,9 +3942,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3956,9 +3956,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3970,9 +3970,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -3984,9 +3984,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -3998,9 +3998,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -4012,9 +4012,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -4026,9 +4026,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -4040,9 +4040,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -4054,9 +4054,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -4068,9 +4068,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4082,9 +4082,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4096,9 +4096,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4110,9 +4110,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4124,9 +4124,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4138,9 +4138,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -4152,9 +4152,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4166,9 +4166,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -5426,13 +5426,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -5967,9 +5967,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7504,11 +7504,14 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.3.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optional": true, "optional": true,
"engines": {
"node": ">=20"
},
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
} }
@@ -8603,9 +8606,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fast-xml-parser": { "node_modules/fast-xml-parser": {
"version": "4.5.3", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -8614,7 +8617,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"strnum": "^1.1.1" "strnum": "^1.0.5"
}, },
"bin": { "bin": {
"fxparser": "src/cli/cli.js" "fxparser": "src/cli/cli.js"
@@ -10325,12 +10328,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/jspdf": { "node_modules/jspdf": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==", "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0", "fast-png": "^6.2.0",
"fflate": "^0.8.1" "fflate": "^0.8.1"
}, },
@@ -10936,9 +10939,9 @@
} }
}, },
"node_modules/markdown-it": { "node_modules/markdown-it": {
"version": "14.1.0", "version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1", "argparse": "^2.0.1",
@@ -11877,9 +11880,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -14149,9 +14152,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -14165,31 +14168,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -15637,9 +15640,9 @@
} }
}, },
"node_modules/underscore": { "node_modules/underscore": {
"version": "1.13.7", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici-types": { "node_modules/undici-types": {

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useCallback, useRef, useEffect, useMemo } from 'react' import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner' import { toast } from 'sonner'

View File

@@ -94,10 +94,10 @@ export default function ProfileSettingsPage() {
setExpertiseTags(user.expertiseTags || []) setExpertiseTags(user.expertiseTags || [])
setDigestFrequency(user.digestFrequency || 'none') setDigestFrequency(user.digestFrequency || 'none')
setPreferredWorkload(user.preferredWorkload ?? null) setPreferredWorkload(user.preferredWorkload ?? null)
const avail = user.availabilityJson as { startDate?: string; endDate?: string } | null const avail = user.availabilityJson as Array<{ start?: string; end?: string }> | null
if (avail) { if (avail && avail.length > 0) {
setAvailabilityStart(avail.startDate || '') setAvailabilityStart(avail[0].start || '')
setAvailabilityEnd(avail.endDate || '') setAvailabilityEnd(avail[0].end || '')
} }
setProfileLoaded(true) setProfileLoaded(true)
} }
@@ -114,10 +114,10 @@ export default function ProfileSettingsPage() {
expertiseTags, expertiseTags,
digestFrequency: digestFrequency as 'none' | 'daily' | 'weekly', digestFrequency: digestFrequency as 'none' | 'daily' | 'weekly',
preferredWorkload: preferredWorkload ?? undefined, preferredWorkload: preferredWorkload ?? undefined,
availabilityJson: (availabilityStart || availabilityEnd) ? { availabilityJson: (availabilityStart || availabilityEnd) ? [{
startDate: availabilityStart || undefined, start: availabilityStart || '',
endDate: availabilityEnd || undefined, end: availabilityEnd || '',
} : undefined, }] : undefined,
}) })
toast.success('Profile updated successfully') toast.success('Profile updated successfully')
refetch() refetch()

View File

@@ -933,12 +933,14 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
// Group files by round // Group files by round
type FileItem = (typeof project.files)[number] type FileItem = (typeof project.files)[number]
const roundMap = new Map<string, { roundId: string | null; roundName: string; sortOrder: number; files: FileItem[] }>() const roundMap = new Map<string, { roundId: string | null; roundName: string; sortOrder: number; files: FileItem[] }>()
// Build roundId→round lookup from competitionRounds
const roundLookup = new Map(competitionRounds.map((r, idx) => [r.id, { name: r.name, sortOrder: idx }]))
for (const f of project.files) { for (const f of project.files) {
const key = (f as any).roundId ?? '__none__' const key = f.roundId ?? '__none__'
if (!roundMap.has(key)) { if (!roundMap.has(key)) {
const round = (f as any).round as { id: string; name: string; sortOrder: number } | null const round = f.roundId ? roundLookup.get(f.roundId) : null
roundMap.set(key, { roundMap.set(key, {
roundId: round?.id ?? null, roundId: f.roundId ?? null,
roundName: round?.name ?? 'Other Files', roundName: round?.name ?? 'Other Files',
sortOrder: round?.sortOrder ?? 999, sortOrder: round?.sortOrder ?? 999,
files: [], files: [],

View File

@@ -1,49 +0,0 @@
import { prisma } from '@/lib/prisma'
/**
* Feature flag keys — used to control progressive rollout of new architecture.
* Stored as SystemSetting records with category FEATURE_FLAGS.
*/
export const FEATURE_FLAGS = {
/** Use Competition/Round model instead of Pipeline/Track/Stage */
USE_COMPETITION_MODEL: 'feature.useCompetitionModel',
} as const
type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
/**
* Check if a feature flag is enabled (server-side).
* Returns false if the flag doesn't exist in the database.
*/
export async function isFeatureEnabled(flag: FeatureFlagKey): Promise<boolean> {
try {
const setting = await prisma.systemSettings.findUnique({
where: { key: flag },
})
// Default to true for competition model (legacy Pipeline system removed)
if (!setting) return flag === FEATURE_FLAGS.USE_COMPETITION_MODEL ? true : false
return setting.value === 'true'
} catch {
return flag === FEATURE_FLAGS.USE_COMPETITION_MODEL ? true : false
}
}
/**
* Set a feature flag value (server-side, admin only).
*/
export async function setFeatureFlag(
flag: FeatureFlagKey,
enabled: boolean,
): Promise<void> {
await prisma.systemSettings.upsert({
where: { key: flag },
update: { value: String(enabled) },
create: {
key: flag,
value: String(enabled),
type: 'BOOLEAN',
category: 'FEATURE_FLAGS',
description: `Feature flag: ${flag}`,
},
})
}

View File

@@ -1,30 +0,0 @@
export type FileTypeCategory = {
id: string
label: string
mimeTypes: string[]
extensions: string[]
}
export const FILE_TYPE_CATEGORIES: FileTypeCategory[] = [
{ id: 'pdf', label: 'PDF', mimeTypes: ['application/pdf'], extensions: ['.pdf'] },
{ id: 'word', label: 'Word', mimeTypes: ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], extensions: ['.doc', '.docx'] },
{ id: 'powerpoint', label: 'PowerPoint', mimeTypes: ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], extensions: ['.ppt', '.pptx'] },
{ id: 'excel', label: 'Excel', mimeTypes: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], extensions: ['.xls', '.xlsx'] },
{ id: 'images', label: 'Images', mimeTypes: ['image/*'], extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'] },
{ id: 'videos', label: 'Videos', mimeTypes: ['video/*'], extensions: ['.mp4', '.mov', '.avi', '.webm'] },
]
/** Get active category IDs from a list of mime types */
export function getActiveCategoriesFromMimeTypes(mimeTypes: string[]): string[] {
if (!mimeTypes || !Array.isArray(mimeTypes)) return []
return FILE_TYPE_CATEGORIES.filter((cat) =>
cat.mimeTypes.some((mime) => mimeTypes.includes(mime))
).map((cat) => cat.id)
}
/** Convert category IDs to flat mime type array */
export function categoriesToMimeTypes(categoryIds: string[]): string[] {
return FILE_TYPE_CATEGORIES.filter((cat) => categoryIds.includes(cat.id)).flatMap(
(cat) => cat.mimeTypes
)
}

View File

@@ -1379,11 +1379,10 @@ export const analyticsRouter = router({
bucket: true, objectKey: true, pageCount: true, textPreview: true, bucket: true, objectKey: true, pageCount: true, textPreview: true,
detectedLang: true, langConfidence: true, analyzedAt: true, detectedLang: true, langConfidence: true, analyzedAt: true,
roundId: true, roundId: true,
round: { select: { id: true, name: true, roundType: true, sortOrder: true } },
requirementId: true, requirementId: true,
requirement: { select: { id: true, name: true, description: true, isRequired: true } }, requirement: { select: { id: true, name: true, description: true, isRequired: true } },
}, },
orderBy: [{ round: { sortOrder: 'asc' } }, { createdAt: 'asc' }], orderBy: [{ createdAt: 'asc' }],
}, },
teamMembers: { teamMembers: {
include: { include: {

View File

@@ -1,618 +0,0 @@
/**
* Live Control Service
*
* Manages real-time control of live final events within a round.
* Handles session management, project cursor navigation, queue reordering,
* pause/resume, and cohort voting windows.
*
* The LiveProgressCursor tracks the current position in a live presentation
* sequence. Cohorts group projects for voting with configurable windows.
*/
import type { PrismaClient } from '@prisma/client'
import { logAudit } from '@/server/utils/audit'
// ─── Types ──────────────────────────────────────────────────────────────────
export interface SessionResult {
success: boolean
sessionId: string | null
cursorId: string | null
errors?: string[]
}
export interface CursorState {
roundId: string
sessionId: string
activeProjectId: string | null
activeOrderIndex: number
isPaused: boolean
totalProjects: number
}
// ─── Helpers ────────────────────────────────────────────────────────────────
function generateSessionId(): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 8)
return `live-${timestamp}-${random}`
}
// ─── Start Session ──────────────────────────────────────────────────────────
/**
* Create or reset a LiveProgressCursor for a round. If a cursor already exists,
* it is reset to the beginning. A new sessionId is always generated.
*/
export async function startSession(
roundId: string,
actorId: string,
prisma: PrismaClient | any
): Promise<SessionResult> {
try {
// Verify round exists and is a LIVE_FINAL type
const round = await prisma.round.findUnique({
where: { id: roundId },
})
if (!round) {
return {
success: false,
sessionId: null,
cursorId: null,
errors: [`Round ${roundId} not found`],
}
}
if (round.roundType !== 'LIVE_FINAL') {
return {
success: false,
sessionId: null,
cursorId: null,
errors: [
`Round "${round.name}" is type ${round.roundType}, expected LIVE_FINAL`,
],
}
}
// Find the first project in the first cohort
const firstCohortProject = await prisma.cohortProject.findFirst({
where: {
cohort: { roundId },
},
orderBy: { sortOrder: 'asc' as const },
select: { projectId: true },
})
const sessionId = generateSessionId()
// Upsert the cursor (one per round)
const cursor = await prisma.liveProgressCursor.upsert({
where: { roundId },
create: {
roundId,
sessionId,
activeProjectId: firstCohortProject?.projectId ?? null,
activeOrderIndex: 0,
isPaused: false,
},
update: {
sessionId,
activeProjectId: firstCohortProject?.projectId ?? null,
activeOrderIndex: 0,
isPaused: false,
},
})
// Decision audit log
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.session_started',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
actorId,
detailsJson: {
roundId,
sessionId,
firstProjectId: firstCohortProject?.projectId ?? null,
},
},
})
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_SESSION_STARTED',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
detailsJson: { roundId, sessionId },
})
return {
success: true,
sessionId,
cursorId: cursor.id,
}
} catch (error) {
console.error('[LiveControl] Failed to start session:', error)
return {
success: false,
sessionId: null,
cursorId: null,
errors: [
error instanceof Error ? error.message : 'Failed to start live session',
],
}
}
}
// ─── Set Active Project ─────────────────────────────────────────────────────
/**
* Set the currently active project in the live session.
* Validates that the project belongs to a cohort in this round and performs
* a version check on the cursor's sessionId to prevent stale updates.
*/
export async function setActiveProject(
roundId: string,
projectId: string,
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; errors?: string[] }> {
try {
// Verify cursor exists
const cursor = await prisma.liveProgressCursor.findUnique({
where: { roundId },
})
if (!cursor) {
return {
success: false,
errors: ['No live session found for this round. Start a session first.'],
}
}
// Verify project is in a cohort for this round
const cohortProject = await prisma.cohortProject.findFirst({
where: {
projectId,
cohort: { roundId },
},
select: { id: true, sortOrder: true },
})
if (!cohortProject) {
return {
success: false,
errors: [
`Project ${projectId} is not in any cohort for round ${roundId}`,
],
}
}
// Update cursor
await prisma.liveProgressCursor.update({
where: { roundId },
data: {
activeProjectId: projectId,
activeOrderIndex: cohortProject.sortOrder,
},
})
// Audit
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.cursor_updated',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
actorId,
detailsJson: {
roundId,
projectId,
orderIndex: cohortProject.sortOrder,
action: 'setActiveProject',
},
},
})
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_SET_ACTIVE_PROJECT',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
detailsJson: { projectId, orderIndex: cohortProject.sortOrder },
})
return { success: true }
} catch (error) {
console.error('[LiveControl] Failed to set active project:', error)
return {
success: false,
errors: [
error instanceof Error
? error.message
: 'Failed to set active project',
],
}
}
}
// ─── Jump to Project ────────────────────────────────────────────────────────
/**
* Jump to a project by its order index in the cohort queue.
*/
export async function jumpToProject(
roundId: string,
orderIndex: number,
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; projectId?: string; errors?: string[] }> {
try {
const cursor = await prisma.liveProgressCursor.findUnique({
where: { roundId },
})
if (!cursor) {
return {
success: false,
errors: ['No live session found for this round'],
}
}
// Find the CohortProject at the given sort order
const cohortProject = await prisma.cohortProject.findFirst({
where: {
cohort: { roundId },
sortOrder: orderIndex,
},
select: { projectId: true, sortOrder: true },
})
if (!cohortProject) {
return {
success: false,
errors: [`No project found at order index ${orderIndex}`],
}
}
// Update cursor
await prisma.liveProgressCursor.update({
where: { roundId },
data: {
activeProjectId: cohortProject.projectId,
activeOrderIndex: orderIndex,
},
})
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.cursor_updated',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
actorId,
detailsJson: {
roundId,
projectId: cohortProject.projectId,
orderIndex,
action: 'jumpToProject',
},
},
})
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_JUMP_TO_PROJECT',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
detailsJson: { orderIndex, projectId: cohortProject.projectId },
})
return { success: true, projectId: cohortProject.projectId }
} catch (error) {
console.error('[LiveControl] Failed to jump to project:', error)
return {
success: false,
errors: [
error instanceof Error ? error.message : 'Failed to jump to project',
],
}
}
}
// ─── Reorder Queue ──────────────────────────────────────────────────────────
/**
* Reorder the presentation queue by updating CohortProject sortOrder values.
* newOrder is an array of cohortProjectIds in the desired order.
*/
export async function reorderQueue(
roundId: string,
newOrder: string[],
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; errors?: string[] }> {
try {
// Verify all provided IDs belong to cohorts in this round
const cohortProjects = await prisma.cohortProject.findMany({
where: {
id: { in: newOrder },
cohort: { roundId },
},
select: { id: true },
})
const validIds = new Set(cohortProjects.map((cp: any) => cp.id))
const invalidIds = newOrder.filter((id) => !validIds.has(id))
if (invalidIds.length > 0) {
return {
success: false,
errors: [
`CohortProject IDs not found in round ${roundId}: ${invalidIds.join(', ')}`,
],
}
}
// Update sortOrder for each item
await prisma.$transaction(
newOrder.map((cohortProjectId, index) =>
prisma.cohortProject.update({
where: { id: cohortProjectId },
data: { sortOrder: index },
})
)
)
const cursor = await prisma.liveProgressCursor.findUnique({
where: { roundId },
})
if (cursor) {
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.queue_reordered',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
actorId,
detailsJson: {
roundId,
newOrderCount: newOrder.length,
},
},
})
}
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_REORDER_QUEUE',
entityType: 'Round',
entityId: roundId,
detailsJson: { reorderedCount: newOrder.length },
})
return { success: true }
} catch (error) {
console.error('[LiveControl] Failed to reorder queue:', error)
return {
success: false,
errors: [
error instanceof Error ? error.message : 'Failed to reorder queue',
],
}
}
}
// ─── Pause / Resume ─────────────────────────────────────────────────────────
/**
* Toggle the pause state of a live session.
*/
export async function pauseResume(
roundId: string,
isPaused: boolean,
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; errors?: string[] }> {
try {
const cursor = await prisma.liveProgressCursor.findUnique({
where: { roundId },
})
if (!cursor) {
return {
success: false,
errors: ['No live session found for this round'],
}
}
await prisma.liveProgressCursor.update({
where: { roundId },
data: { isPaused },
})
await prisma.decisionAuditLog.create({
data: {
eventType: isPaused ? 'live.session_paused' : 'live.session_resumed',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
actorId,
detailsJson: {
roundId,
isPaused,
sessionId: cursor.sessionId,
},
},
})
await logAudit({
prisma,
userId: actorId,
action: isPaused ? 'LIVE_SESSION_PAUSED' : 'LIVE_SESSION_RESUMED',
entityType: 'LiveProgressCursor',
entityId: cursor.id,
detailsJson: { roundId, isPaused },
})
return { success: true }
} catch (error) {
console.error('[LiveControl] Failed to pause/resume:', error)
return {
success: false,
errors: [
error instanceof Error
? error.message
: 'Failed to toggle pause state',
],
}
}
}
// ─── Cohort Window Management ───────────────────────────────────────────────
/**
* Open a cohort's voting window. Sets isOpen to true and records the
* window open timestamp.
*/
export async function openCohortWindow(
cohortId: string,
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; errors?: string[] }> {
try {
const cohort = await prisma.cohort.findUnique({
where: { id: cohortId },
})
if (!cohort) {
return { success: false, errors: [`Cohort ${cohortId} not found`] }
}
if (cohort.isOpen) {
return {
success: false,
errors: [`Cohort "${cohort.name}" is already open`],
}
}
const now = new Date()
await prisma.cohort.update({
where: { id: cohortId },
data: {
isOpen: true,
windowOpenAt: now,
},
})
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.cohort_opened',
entityType: 'Cohort',
entityId: cohortId,
actorId,
detailsJson: {
cohortName: cohort.name,
roundId: cohort.roundId,
openedAt: now.toISOString(),
},
},
})
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_COHORT_OPENED',
entityType: 'Cohort',
entityId: cohortId,
detailsJson: { cohortName: cohort.name, roundId: cohort.roundId },
})
return { success: true }
} catch (error) {
console.error('[LiveControl] Failed to open cohort window:', error)
return {
success: false,
errors: [
error instanceof Error
? error.message
: 'Failed to open cohort window',
],
}
}
}
/**
* Close a cohort's voting window. Sets isOpen to false and records the
* window close timestamp.
*/
export async function closeCohortWindow(
cohortId: string,
actorId: string,
prisma: PrismaClient | any
): Promise<{ success: boolean; errors?: string[] }> {
try {
const cohort = await prisma.cohort.findUnique({
where: { id: cohortId },
})
if (!cohort) {
return { success: false, errors: [`Cohort ${cohortId} not found`] }
}
if (!cohort.isOpen) {
return {
success: false,
errors: [`Cohort "${cohort.name}" is already closed`],
}
}
const now = new Date()
await prisma.cohort.update({
where: { id: cohortId },
data: {
isOpen: false,
windowCloseAt: now,
},
})
await prisma.decisionAuditLog.create({
data: {
eventType: 'live.cohort_closed',
entityType: 'Cohort',
entityId: cohortId,
actorId,
detailsJson: {
cohortName: cohort.name,
roundId: cohort.roundId,
closedAt: now.toISOString(),
},
},
})
await logAudit({
prisma,
userId: actorId,
action: 'LIVE_COHORT_CLOSED',
entityType: 'Cohort',
entityId: cohortId,
detailsJson: { cohortName: cohort.name, roundId: cohort.roundId },
})
return { success: true }
} catch (error) {
console.error('[LiveControl] Failed to close cohort window:', error)
return {
success: false,
errors: [
error instanceof Error
? error.message
: 'Failed to close cohort window',
],
}
}
}