diff --git a/package-lock.json b/package-lock.json
index c53d03b..a9e72f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -58,9 +58,9 @@
}
},
"node_modules/@adobe/css-tools": {
- "version": "4.4.4",
- "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
- "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
"dev": true,
"license": "MIT"
},
@@ -416,9 +416,9 @@
}
},
"node_modules/@csstools/css-calc": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz",
- "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
"dev": true,
"funding": [
{
@@ -440,9 +440,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz",
- "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
+ "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
"dev": true,
"funding": [
{
@@ -457,7 +457,7 @@
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^6.0.2",
- "@csstools/css-calc": "^3.2.0"
+ "@csstools/css-calc": "^3.2.1"
},
"engines": {
"node": ">=20.19.0"
@@ -491,9 +491,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz",
- "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
+ "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
"dev": true,
"funding": [
{
@@ -1012,9 +1012,9 @@
}
},
"node_modules/@exodus/bytes": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
- "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1675,9 +1675,9 @@
}
},
"node_modules/@oxc-project/types": {
- "version": "0.127.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
- "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
+ "version": "0.132.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
+ "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2496,9 +2496,9 @@
"license": "MIT"
},
"node_modules/@reduxjs/toolkit": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
- "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
+ "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
@@ -2522,9 +2522,9 @@
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
- "version": "11.1.4",
- "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
- "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "version": "11.1.8",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
+ "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -2532,9 +2532,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
+ "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [
"arm64"
],
@@ -2549,9 +2549,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
+ "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [
"arm64"
],
@@ -2566,9 +2566,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
+ "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [
"x64"
],
@@ -2583,9 +2583,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
+ "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [
"x64"
],
@@ -2600,9 +2600,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
- "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
+ "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [
"arm"
],
@@ -2617,9 +2617,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
+ "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [
"arm64"
],
@@ -2637,9 +2637,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
+ "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [
"arm64"
],
@@ -2657,9 +2657,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
+ "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [
"ppc64"
],
@@ -2677,9 +2677,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
+ "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [
"s390x"
],
@@ -2697,9 +2697,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
+ "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [
"x64"
],
@@ -2717,9 +2717,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
+ "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [
"x64"
],
@@ -2737,9 +2737,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
+ "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [
"arm64"
],
@@ -2754,9 +2754,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
- "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
+ "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [
"wasm32"
],
@@ -2773,9 +2773,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
+ "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [
"arm64"
],
@@ -2790,9 +2790,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
+ "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [
"x64"
],
@@ -2807,9 +2807,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.7",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
- "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -2846,49 +2846,49 @@
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
- "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
+ "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "^5.19.0",
+ "enhanced-resolve": "^5.21.0",
"jiti": "^2.6.1",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.2.4"
+ "tailwindcss": "4.3.0"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz",
- "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
+ "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.4",
- "@tailwindcss/oxide-darwin-arm64": "4.2.4",
- "@tailwindcss/oxide-darwin-x64": "4.2.4",
- "@tailwindcss/oxide-freebsd-x64": "4.2.4",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.4",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.4",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
+ "@tailwindcss/oxide-android-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-x64": "4.3.0",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.0",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz",
- "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
+ "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
"cpu": [
"arm64"
],
@@ -2903,9 +2903,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz",
- "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
+ "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
"cpu": [
"arm64"
],
@@ -2920,9 +2920,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz",
- "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
+ "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
"cpu": [
"x64"
],
@@ -2937,9 +2937,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz",
- "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
+ "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
"cpu": [
"x64"
],
@@ -2954,9 +2954,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz",
- "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
+ "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
"cpu": [
"arm"
],
@@ -2971,9 +2971,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz",
- "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
+ "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
"cpu": [
"arm64"
],
@@ -2991,9 +2991,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz",
- "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
"cpu": [
"arm64"
],
@@ -3011,9 +3011,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz",
- "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
+ "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
"cpu": [
"x64"
],
@@ -3031,9 +3031,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz",
- "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
"cpu": [
"x64"
],
@@ -3051,9 +3051,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz",
- "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
+ "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -3069,10 +3069,10 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "^1.8.1",
- "@emnapi/runtime": "^1.8.1",
- "@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.1.1",
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.1"
},
@@ -3081,9 +3081,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
- "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
"cpu": [
"arm64"
],
@@ -3098,9 +3098,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz",
- "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
"cpu": [
"x64"
],
@@ -3115,15 +3115,15 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz",
- "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
+ "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.2.4",
- "@tailwindcss/oxide": "4.2.4",
- "tailwindcss": "4.2.4"
+ "@tailwindcss/node": "4.3.0",
+ "@tailwindcss/oxide": "4.3.0",
+ "tailwindcss": "4.3.0"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
@@ -3219,9 +3219,9 @@
}
},
"node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
- "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -3327,9 +3327,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/@types/estree-jsx": {
@@ -3366,13 +3366,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.6.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
- "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "version": "25.9.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.19.0"
+ "undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/picomatch": {
@@ -3383,9 +3383,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "19.2.14",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
- "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "version": "19.2.15",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3414,19 +3414,19 @@
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
- "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
+ "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
- "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@rolldown/pluginutils": "1.0.0-rc.7"
+ "@rolldown/pluginutils": "^1.0.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -3446,15 +3446,15 @@
}
},
"node_modules/@vitest/browser": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.5.tgz",
- "integrity": "sha512-iCDGI8c4yg+xmjUg2VsygdAUSIIB4x5Rht/P68OXy1hPELKXHDkzh87lkuTcdYmemRChDkEpB426MmDjzC0ziA==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.7.tgz",
+ "integrity": "sha512-N2JFGfXoEGVAut+kHeru9dD4BUMq/q5xDvBARNl0tUsly3m5KglLOu8VO/6MkDfOlgxXTycojkt6gBKsuyR+IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@blazediff/core": "1.9.1",
- "@vitest/mocker": "4.1.5",
- "@vitest/utils": "4.1.5",
+ "@vitest/mocker": "4.1.7",
+ "@vitest/utils": "4.1.7",
"magic-string": "^0.30.21",
"pngjs": "^7.0.0",
"sirv": "^3.0.2",
@@ -3465,18 +3465,18 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "4.1.5"
+ "vitest": "4.1.7"
}
},
"node_modules/@vitest/browser-playwright": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.5.tgz",
- "integrity": "sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.7.tgz",
+ "integrity": "sha512-OlTlJej7YN6VwV7zJJoNeaCsctF+JXpzpZ4oBHUbrQFfIq+0KW2f07rprCLh9N/zRIZ0v4Mchn1QDDmWMUhPKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/browser": "4.1.5",
- "@vitest/mocker": "4.1.5",
+ "@vitest/browser": "4.1.7",
+ "@vitest/mocker": "4.1.7",
"tinyrainbow": "^3.1.0"
},
"funding": {
@@ -3484,7 +3484,7 @@
},
"peerDependencies": {
"playwright": "*",
- "vitest": "4.1.5"
+ "vitest": "4.1.7"
},
"peerDependenciesMeta": {
"playwright": {
@@ -3493,16 +3493,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz",
- "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
+ "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.5",
- "@vitest/utils": "4.1.5",
+ "@vitest/spy": "4.1.7",
+ "@vitest/utils": "4.1.7",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -3511,13 +3511,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz",
- "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz",
+ "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.5",
+ "@vitest/spy": "4.1.7",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -3538,9 +3538,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz",
- "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz",
+ "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3551,13 +3551,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz",
- "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz",
+ "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.5",
+ "@vitest/utils": "4.1.7",
"pathe": "^2.0.3"
},
"funding": {
@@ -3565,14 +3565,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz",
- "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz",
+ "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.5",
- "@vitest/utils": "4.1.5",
+ "@vitest/pretty-format": "4.1.7",
+ "@vitest/utils": "4.1.7",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -3581,9 +3581,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz",
- "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz",
+ "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==",
"dev": true,
"license": "MIT",
"funding": {
@@ -3591,13 +3591,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz",
- "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz",
+ "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.5",
+ "@vitest/pretty-format": "4.1.7",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -4156,9 +4156,9 @@
"license": "MIT"
},
"node_modules/enhanced-resolve": {
- "version": "5.21.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
- "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
+ "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4558,9 +4558,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.16",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
- "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
+ "version": "4.12.22",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.22.tgz",
+ "integrity": "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -4708,9 +4708,9 @@
"license": "MIT"
},
"node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -5068,9 +5068,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.3.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
- "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
+ "version": "11.5.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
+ "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -5078,9 +5078,9 @@
}
},
"node_modules/lucide-react": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
- "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
+ "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -6137,13 +6137,13 @@
}
},
"node_modules/playwright": {
- "version": "1.59.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
- "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.59.1"
+ "playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
@@ -6156,9 +6156,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.59.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
- "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -6179,9 +6179,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.13",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
- "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -6199,7 +6199,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -6276,30 +6276,30 @@
}
},
"node_modules/react": {
- "version": "19.2.5",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
- "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.2.5",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
- "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.5"
+ "react": "^19.2.6"
}
},
"node_modules/react-is": {
- "version": "19.2.5",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
- "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz",
+ "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
"license": "MIT",
"peer": true
},
@@ -6331,9 +6331,9 @@
}
},
"node_modules/react-redux": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
- "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+ "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
@@ -6401,9 +6401,9 @@
}
},
"node_modules/react-router": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
- "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==",
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
+ "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -6423,12 +6423,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz",
- "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==",
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
+ "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
"license": "MIT",
"dependencies": {
- "react-router": "7.14.2"
+ "react-router": "7.15.1"
},
"engines": {
"node": ">=20.0.0"
@@ -6641,14 +6641,14 @@
"license": "MIT"
},
"node_modules/rolldown": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
- "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
+ "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.127.0",
- "@rolldown/pluginutils": "1.0.0-rc.17"
+ "@oxc-project/types": "=0.132.0",
+ "@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -6657,29 +6657,82 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
- }
- },
- "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
- "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
+ "@rolldown/binding-android-arm64": "1.0.2",
+ "@rolldown/binding-darwin-arm64": "1.0.2",
+ "@rolldown/binding-darwin-x64": "1.0.2",
+ "@rolldown/binding-freebsd-x64": "1.0.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.2",
+ "@rolldown/binding-linux-arm64-musl": "1.0.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-musl": "1.0.2",
+ "@rolldown/binding-openharmony-arm64": "1.0.2",
+ "@rolldown/binding-wasm32-wasi": "1.0.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.2",
+ "@rolldown/binding-win32-x64-msvc": "1.0.2"
+ }
+ },
+ "node_modules/rosie-skills": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/rosie-skills/-/rosie-skills-0.6.4.tgz",
+ "integrity": "sha512-ojfhSiQRdZ2QyWbmKAHOSAUbaLYrTc5zIH7mS1jKoP8KCFSQddwVhMyFqldckTeybTfW3zNcsZzyOTzGTN1SBA==",
"dev": true,
- "license": "MIT"
+ "license": "BSD-3-Clause",
+ "bin": {
+ "rosie-skills": "dist/bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "rosie-skills-darwin-arm64": "0.6.4",
+ "rosie-skills-freebsd-x64": "0.6.4",
+ "rosie-skills-linux-x64": "0.6.4"
+ }
+ },
+ "node_modules/rosie-skills-darwin-arm64": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/rosie-skills-darwin-arm64/-/rosie-skills-darwin-arm64-0.6.4.tgz",
+ "integrity": "sha512-rn1s5hqFKcxeiDEWWoFa1hdGPshR8TkwHLzy/cBavb9XJNAaUxbe3oQ78W9sQkRHAgRyzJYyk9tw68Qrdnizgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/rosie-skills-freebsd-x64": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/rosie-skills-freebsd-x64/-/rosie-skills-freebsd-x64-0.6.4.tgz",
+ "integrity": "sha512-SxCRduPBMtfjkQ+q56Yw9OLA3PyaqoALzt7kER7IDKuUVfM2O/1w8sa5xhTDiCvWkZJixnH5d5Ya6KT+/Mwcng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/rosie-skills-linux-x64": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/rosie-skills-linux-x64/-/rosie-skills-linux-x64-0.6.4.tgz",
+ "integrity": "sha512-D9Y9mfu7goB0s0X59uU3hcFeUTef3VbpCIDwFMzyvJrAq3XhRACWBDMHQsHlyWdHxTXPX/ILyW65RXyrJlgqng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
"node_modules/rxjs": {
"version": "7.8.2",
@@ -6711,9 +6764,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
+ "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"dev": true,
"license": "ISC",
"bin": {
@@ -6950,9 +7003,9 @@
"license": "MIT"
},
"node_modules/tailwind-merge": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
- "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
"license": "MIT",
"funding": {
"type": "github",
@@ -6960,9 +7013,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
- "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
+ "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"dev": true,
"license": "MIT"
},
@@ -6994,9 +7047,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
- "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz",
+ "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7031,22 +7084,22 @@
}
},
"node_modules/tldts": {
- "version": "7.0.30",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz",
- "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.1.1.tgz",
+ "integrity": "sha512-VuvOq9QVVdzQyIwynB0MRZlEup+u5BD62FjgmKvRDFO8u1RgAzpeg7Qd70hUmrxwkkecqoz1N6t1yGMygx7rnA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.0.30"
+ "tldts-core": "^7.1.1"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
- "version": "7.0.30",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz",
- "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.1.1.tgz",
+ "integrity": "sha512-v9zYcyFEAJBeyG7g4+y/HFL9i2cHqpV+9cHohNZIhA6xjO2MSVgijFgx6quQaRBDzM5FT8fs5NPjsNITOhlCzg==",
"dev": true,
"license": "MIT"
},
@@ -7147,9 +7200,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.19.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
- "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
@@ -7367,16 +7420,16 @@
}
},
"node_modules/vite": {
- "version": "8.0.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
- "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
+ "version": "8.0.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
+ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
- "postcss": "^8.5.10",
- "rolldown": "1.0.0-rc.17",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
},
"bin": {
@@ -7393,7 +7446,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.0",
+ "@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -7460,19 +7513,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",
- "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz",
+ "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.5",
- "@vitest/mocker": "4.1.5",
- "@vitest/pretty-format": "4.1.5",
- "@vitest/runner": "4.1.5",
- "@vitest/snapshot": "4.1.5",
- "@vitest/spy": "4.1.5",
- "@vitest/utils": "4.1.5",
+ "@vitest/expect": "4.1.7",
+ "@vitest/mocker": "4.1.7",
+ "@vitest/pretty-format": "4.1.7",
+ "@vitest/runner": "4.1.7",
+ "@vitest/snapshot": "4.1.7",
+ "@vitest/spy": "4.1.7",
+ "@vitest/utils": "4.1.7",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -7500,12 +7553,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.5",
- "@vitest/browser-preview": "4.1.5",
- "@vitest/browser-webdriverio": "4.1.5",
- "@vitest/coverage-istanbul": "4.1.5",
- "@vitest/coverage-v8": "4.1.5",
- "@vitest/ui": "4.1.5",
+ "@vitest/browser-playwright": "4.1.7",
+ "@vitest/browser-preview": "4.1.7",
+ "@vitest/browser-webdriverio": "4.1.7",
+ "@vitest/coverage-istanbul": "4.1.7",
+ "@vitest/coverage-v8": "4.1.7",
+ "@vitest/ui": "4.1.7",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -7646,9 +7699,9 @@
}
},
"node_modules/wrangler": {
- "version": "4.87.0",
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.87.0.tgz",
- "integrity": "sha512-lfhfKwLfQlowwgV0xhlYgE9fU3n0I30d4ccGY/rTCEm/n42Mjvlr0Ng3ZPNqlsrsKBcDR531V7dsPkgELvrk/Q==",
+ "version": "4.94.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.94.0.tgz",
+ "integrity": "sha512-GsNw0DomGFfeXFtKVTwn2X69UKcCxcTB0CXykjsMineJIxOeyrw7LovlHQ/3JU8KJHH7repLB+kOHvfTBA/Eew==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
@@ -7656,10 +7709,11 @@
"@cloudflare/unenv-preset": "2.16.1",
"blake3-wasm": "2.1.5",
"esbuild": "0.27.3",
- "miniflare": "4.20260430.0",
+ "miniflare": "4.20260521.0",
"path-to-regexp": "6.3.0",
+ "rosie-skills": "^0.6.3",
"unenv": "2.0.0-rc.24",
- "workerd": "1.20260430.1"
+ "workerd": "1.20260521.1"
},
"bin": {
"wrangler": "bin/wrangler.js",
@@ -7672,7 +7726,7 @@
"fsevents": "~2.3.2"
},
"peerDependencies": {
- "@cloudflare/workers-types": "^4.20260430.1"
+ "@cloudflare/workers-types": "^4.20260521.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
@@ -7681,9 +7735,9 @@
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260430.1.tgz",
- "integrity": "sha512-ADohZUHf7NBvPp2PdZig2Opxx+hDkk3ve7jrTne3JRx9kDSB73zc4LzcEeEN8LKkbAcqZmvfRJfpChSlusu0lA==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260521.1.tgz",
+ "integrity": "sha512-aiNdXmxlhwGjTSajL3I7uQPpN4lAOcXjvg5ZOlJKIywnevr798n9XCS6lvuqgniM3KjurBNWRRypMJntg/eSLg==",
"cpu": [
"x64"
],
@@ -7698,9 +7752,9 @@
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260430.1.tgz",
- "integrity": "sha512-/DoYC/1wHs+YRZzzqSQg1/EHB4hiv1yV5U8FnmapRRIzVaPtnt+ApeOXeMrIdKidgKOI8TqQzgBU8xbIM7Cl4Q==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260521.1.tgz",
+ "integrity": "sha512-ikN8aKSi4Ak28ndOkuSO5rq6lmV6wwDQu9F9Vu6J7EkwAOth74J/Hjn4j4EuFceW/npw2Ws0Y/muzA6WKHl4TA==",
"cpu": [
"arm64"
],
@@ -7715,9 +7769,9 @@
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260430.1.tgz",
- "integrity": "sha512-koJhBWvEVZPKCVFtMLp2iMHlYr+lFCF47wGbnlKdHVlemV0zTxJEyHI8aLlrhPLhBmOmYLp46rXw09/qJkRIhQ==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260521.1.tgz",
+ "integrity": "sha512-D/gUhvQcG0pJr5aJl6yUoi2JxbFpjVtDq9xUJHPjfkAjL28TUVgCR/e5r8YGirepv4I1DK7ihuii9LZ2GGMJbw==",
"cpu": [
"x64"
],
@@ -7732,9 +7786,9 @@
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260430.1.tgz",
- "integrity": "sha512-hMdapNAzNQZDXGGkg4Slydc3fRJP5FUZLJVVcZCW/+imhhJro9Z1rv5n/wfR+txKoSWhTYR8eOp8Pyi2bzLzlw==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260521.1.tgz",
+ "integrity": "sha512-vhjWPIHenczegTakhRPwEmTeaavCpNqsuo3RlLCkUdU47HrwLvy/4QersGggs4+kF4Do+IE/EznCGyT40xYcLA==",
"cpu": [
"arm64"
],
@@ -7749,9 +7803,9 @@
}
},
"node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260430.1.tgz",
- "integrity": "sha512-jS3ffixjb5USOwz4frw4WzCz0HrjVxkgyU3WiYb06N7hBAfN6eOrveAJ4QRef0+suK4V1vQFoB1oKdRBsXe9Dw==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260521.1.tgz",
+ "integrity": "sha512-wBolYC/+lnGIEbkkPdzFtjTOWip2uQH6maeAP1ZV0kyxi5SGpsa83+wD5rH5OOle+sHE5qJMdwCKjwRwj+FKJg==",
"cpu": [
"x64"
],
@@ -7766,17 +7820,17 @@
}
},
"node_modules/wrangler/node_modules/miniflare": {
- "version": "4.20260430.0",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260430.0.tgz",
- "integrity": "sha512-MWvMm3Siho9Yj7lbJZidLs8hbrRvIcOrif2mnsHQZdvoKfedpea+GaN8XJxbpRcq0B2WzNI1BB1ihdnqes3/ZA==",
+ "version": "4.20260521.0",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260521.0.tgz",
+ "integrity": "sha512-roRfxPq49OkuSeQsc43hRjSB1+HdHtDNKRwDEVk2hCjCBuBWxb5Wvwq88b0ULj6QVEJLN/+ZqF19M+h4VYJ/zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5",
"undici": "7.24.8",
- "workerd": "1.20260430.1",
- "ws": "8.18.0",
+ "workerd": "1.20260521.1",
+ "ws": "8.20.1",
"youch": "4.1.0-beta.10"
},
"bin": {
@@ -7797,9 +7851,9 @@
}
},
"node_modules/wrangler/node_modules/workerd": {
- "version": "1.20260430.1",
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260430.1.tgz",
- "integrity": "sha512-KEgIWyiw3Jmn+DCd/L3ePo5fmiiYb/UcwKvDWPf/nLLOiwShDFzDSsegU5NY/JcwgvO/QsLHVi2FYrbkcXNY5Q==",
+ "version": "1.20260521.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260521.1.tgz",
+ "integrity": "sha512-HzIThcZ0ZVEuzVxpY2IYZ3yssSrTjtrWXAVfmOl5rVwyqcu7aeZXGMiwrEmi9MOcC3wjy+BNv+hFrMMY5OrjQQ==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -7810,17 +7864,17 @@
"node": ">=16"
},
"optionalDependencies": {
- "@cloudflare/workerd-darwin-64": "1.20260430.1",
- "@cloudflare/workerd-darwin-arm64": "1.20260430.1",
- "@cloudflare/workerd-linux-64": "1.20260430.1",
- "@cloudflare/workerd-linux-arm64": "1.20260430.1",
- "@cloudflare/workerd-windows-64": "1.20260430.1"
+ "@cloudflare/workerd-darwin-64": "1.20260521.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260521.1",
+ "@cloudflare/workerd-linux-64": "1.20260521.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260521.1",
+ "@cloudflare/workerd-windows-64": "1.20260521.1"
}
},
"node_modules/wrangler/node_modules/ws": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
- "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7858,9 +7912,9 @@
}
},
"node_modules/ws": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
- "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7961,9 +8015,9 @@
}
},
"node_modules/zod": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz",
- "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/src/client/app.css b/src/client/app.css
index d0a91e8..a09fe98 100644
--- a/src/client/app.css
+++ b/src/client/app.css
@@ -55,7 +55,7 @@
--success: oklch(64% 0.24 115);
--success-bg: oklch(98% 0.04 115);
--success-border: oklch(85% 0.15 115);
- --warning: oklch(82% 0.18 65);
+ --warning: oklch(56% 0.18 65);
--warning-bg: oklch(98% 0.04 65);
--warning-border: oklch(90% 0.12 65);
--danger: oklch(62% 0.22 25);
@@ -550,9 +550,9 @@
}
@utility badge-warning {
- background-color: #fffbeb;
- color: #d97706;
- border-color: #fcd34d;
+ background-color: var(--warning-bg);
+ color: var(--warning);
+ border-color: var(--warning-border);
.dark & {
background-color: var(--warning-bg);
@@ -599,14 +599,14 @@
&.pending { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); }
&.running {
- @apply bg-info shadow-[0_0_0_3px_color-mix(in_oklch,var(--info)_25%,transparent)] animate-[pulse-ring_1.5s_var(--ease-out-quart)_infinite];
+ @apply bg-info;
}
&.done { @apply bg-success; }
&.failed { @apply bg-danger; }
}
@utility pulsing-dot {
- @apply w-[7px] h-[7px] rounded-full bg-info inline-block animate-[pulse-ring_1.5s_var(--ease-out-quart)_infinite];
+ @apply w-[7px] h-[7px] rounded-full bg-info inline-block;
}
/* ─────────────────────────────────────────────────────
@@ -865,3 +865,191 @@
& a { @apply text-primary underline underline-offset-2; }
& hr { @apply border-border my-[1.5em]; }
}
+
+/* ─────────────────────────────────────────────────────
+ Sonner Toast — Premium overrides
+───────────────────────────────────────────────────── */
+
+/* ── Outer list / viewport ───────────────────────── */
+[data-sonner-toaster] {
+ --offset: 1.25rem !important;
+ --width: min(22rem, calc(100vw - 2rem)) !important;
+ font-family: var(--font-sans) !important;
+}
+
+/* ── Base toast shell ─────────────────────────────── */
+.codra-toast {
+ display: flex !important;
+ align-items: flex-start !important;
+ gap: 0.625rem !important;
+ padding: 0.75rem 0.875rem !important;
+ border-radius: 0.625rem !important;
+ border-width: 1px !important;
+ border-style: solid !important;
+ font-family: var(--font-sans) !important;
+ font-size: 0.8125rem !important;
+ line-height: 1.45 !important;
+ box-shadow:
+ 0 4px 16px oklch(0% 0 0 / 0.10),
+ 0 1px 4px oklch(0% 0 0 / 0.06),
+ inset 0 1px 0 oklch(100% 0 0 / 0.05) !important;
+
+ /* light defaults (overridden per-variant below) */
+ background: oklch(99.5% 0.004 115) !important;
+ border-color: oklch(88% 0.008 115) !important;
+ color: oklch(15% 0.02 115) !important;
+
+ /* smooth entrance */
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1) !important;
+}
+
+.dark .codra-toast {
+ background: oklch(13% 0.018 115) !important;
+ border-color: oklch(22% 0.022 115) !important;
+ color: oklch(94% 0.006 115) !important;
+ box-shadow:
+ 0 6px 24px oklch(0% 0 0 / 0.5),
+ 0 1px 6px oklch(0% 0 0 / 0.3),
+ inset 0 1px 0 oklch(100% 0 0 / 0.04) !important;
+}
+
+/* ── Title ────────────────────────────────────────── */
+.codra-toast-title {
+ font-size: 0.8125rem !important;
+ font-weight: 600 !important;
+ letter-spacing: 0.005em !important;
+ line-height: 1.35 !important;
+}
+
+/* ── Description ──────────────────────────────────── */
+.codra-toast-description {
+ font-size: 0.74rem !important;
+ font-weight: 400 !important;
+ opacity: 0.72 !important;
+ margin-top: 0.15rem !important;
+ line-height: 1.5 !important;
+}
+
+/* ── Icon wrapper ─────────────────────────────────── */
+.codra-toast-icon {
+ margin-top: 0.05rem !important;
+ flex-shrink: 0 !important;
+}
+
+/* ── Close button ─────────────────────────────────── */
+.codra-toast-close {
+ top: 0.55rem !important;
+ right: 0.55rem !important;
+ width: 1.25rem !important;
+ height: 1.25rem !important;
+ border-radius: 0.3rem !important;
+ background: oklch(88% 0.006 115 / 0.6) !important;
+ border: 1px solid oklch(82% 0.008 115 / 0.8) !important;
+ color: oklch(40% 0.015 115) !important;
+ transition: background 150ms, opacity 150ms !important;
+}
+
+.dark .codra-toast-close {
+ background: oklch(22% 0.018 115 / 0.7) !important;
+ border-color: oklch(30% 0.02 115 / 0.8) !important;
+ color: oklch(65% 0.012 115) !important;
+}
+
+.codra-toast-close:hover {
+ background: oklch(82% 0.010 115) !important;
+ opacity: 1 !important;
+}
+
+.dark .codra-toast-close:hover {
+ background: oklch(28% 0.022 115) !important;
+}
+
+/* ── SUCCESS ─────────────────────────────────────── */
+.codra-toast-success {
+ background: oklch(98.5% 0.045 115) !important;
+ border-color: oklch(82% 0.16 115) !important;
+ color: oklch(28% 0.10 115) !important;
+}
+
+.dark .codra-toast-success {
+ background: oklch(16% 0.08 115) !important;
+ border-color: oklch(32% 0.14 115) !important;
+ color: oklch(90% 0.18 115) !important;
+}
+
+.codra-toast-success .codra-toast-description {
+ color: oklch(38% 0.10 115) !important;
+ opacity: 0.85 !important;
+}
+
+.dark .codra-toast-success .codra-toast-description {
+ color: oklch(72% 0.12 115) !important;
+ opacity: 0.9 !important;
+}
+
+/* ── ERROR ───────────────────────────────────────── */
+.codra-toast-error {
+ background: oklch(98.5% 0.03 25) !important;
+ border-color: oklch(80% 0.14 25) !important;
+ color: oklch(32% 0.14 25) !important;
+}
+
+.dark .codra-toast-error {
+ background: oklch(15% 0.07 25) !important;
+ border-color: oklch(35% 0.14 25) !important;
+ color: oklch(85% 0.08 25) !important;
+}
+
+.codra-toast-error .codra-toast-description {
+ color: oklch(42% 0.12 25) !important;
+ opacity: 0.85 !important;
+}
+
+.dark .codra-toast-error .codra-toast-description {
+ color: oklch(68% 0.10 25) !important;
+ opacity: 0.9 !important;
+}
+
+/* ── LOADING ─────────────────────────────────────── */
+.codra-toast-loading {
+ background: oklch(98% 0.004 115) !important;
+ border-color: oklch(86% 0.010 115) !important;
+ color: oklch(20% 0.020 115) !important;
+}
+
+.dark .codra-toast-loading {
+ background: oklch(14% 0.020 115) !important;
+ border-color: oklch(24% 0.025 115) !important;
+ color: oklch(88% 0.008 115) !important;
+}
+
+/* spinner inherits accent color */
+.codra-toast-loader svg {
+ color: var(--primary) !important;
+}
+
+/* ── WARNING ─────────────────────────────────────── */
+.codra-toast-warning {
+ background: oklch(98.5% 0.04 65) !important;
+ border-color: oklch(82% 0.13 65) !important;
+ color: oklch(35% 0.12 65) !important;
+}
+
+.dark .codra-toast-warning {
+ background: oklch(16% 0.08 65) !important;
+ border-color: oklch(35% 0.14 65) !important;
+ color: oklch(82% 0.14 65) !important;
+}
+
+/* ── INFO ────────────────────────────────────────── */
+.codra-toast-info {
+ background: oklch(98.5% 0.03 250) !important;
+ border-color: oklch(80% 0.12 250) !important;
+ color: oklch(30% 0.12 250) !important;
+}
+
+.dark .codra-toast-info {
+ background: oklch(15% 0.07 250) !important;
+ border-color: oklch(33% 0.12 250) !important;
+ color: oklch(80% 0.12 250) !important;
+}
diff --git a/src/client/components/features/dashboard/updates-email-prompt.tsx b/src/client/components/features/dashboard/updates-email-prompt.tsx
index bbcff85..2f94167 100644
--- a/src/client/components/features/dashboard/updates-email-prompt.tsx
+++ b/src/client/components/features/dashboard/updates-email-prompt.tsx
@@ -35,12 +35,12 @@ export function UpdatesEmailPrompt() {
try {
const response = await api.subscribeUpdates(email);
setStatus(response);
- toast.success('Updates email saved', {
- description: 'You will only get important Codra release and security notes.',
+ toast.success('You’re subscribed', {
+ description: 'We’ll only reach out for important releases and security notices.',
});
} catch (error) {
- toast.error('Could not save updates email', {
- description: error instanceof Error ? error.message : 'Please try again.',
+ toast.error('Subscription failed', {
+ description: 'We couldn’t save your email. Please check it and try again.',
});
} finally {
setSubmitting(false);
diff --git a/src/client/components/features/job-detail/job-findings-list.tsx b/src/client/components/features/job-detail/job-findings-list.tsx
index c9ff752..084eb1f 100644
--- a/src/client/components/features/job-detail/job-findings-list.tsx
+++ b/src/client/components/features/job-detail/job-findings-list.tsx
@@ -21,7 +21,6 @@ export function JobFindingsList({ job }: JobFindingsListProps) {
Findings
- {job.status === 'running' && }
View by
diff --git a/src/client/components/features/job-detail/job-meta-cards.tsx b/src/client/components/features/job-detail/job-meta-cards.tsx
index acdae17..0bc5915 100644
--- a/src/client/components/features/job-detail/job-meta-cards.tsx
+++ b/src/client/components/features/job-detail/job-meta-cards.tsx
@@ -1,122 +1,220 @@
-import { ExternalLink } from 'lucide-react';
+import { ExternalLink, Check, Minus, X, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@client/components/ui/card';
import { Badge, StatusBadge } from '@client/components/ui/badge';
-import type { JobDetail } from '@shared/schema';
+import type { JobDetail, JobStep } from '@shared/schema';
interface JobMetaCardsProps {
job: JobDetail;
}
+function elapsedSec(step: JobStep): string | null {
+ if (step.finishedAt && step.startedAt) {
+ const start = new Date(step.startedAt).getTime();
+ const end = new Date(step.finishedAt).getTime();
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
+ const ms = end - start;
+ return `${(ms / 1000).toFixed(1)}s`;
+ }
+ return null;
+}
+
+function StepRow({ step, index, total }: { step: JobStep; index: number; total: number }) {
+ const isRunning = step.status === 'running';
+ const isDone = step.status === 'done';
+ const isFailed = step.status === 'failed';
+ const isPending = step.status === 'pending';
+ const isLast = index === total - 1;
+
+ const elapsed = elapsedSec(step);
+
+ // Left accent bar color
+ const accentColor = isDone
+ ? 'bg-success'
+ : isRunning
+ ? 'bg-info'
+ : isFailed
+ ? 'bg-danger'
+ : 'bg-border';
+
+ // Icon
+ const iconEl = isDone ? (
+
+ ) : isFailed ? (
+
+ ) : isRunning ? (
+
+ ) : (
+
+ );
+
+ return (
+
0 ? 'pt-3' : ''} ${!isLast ? 'border-b border-border/30' : ''}`}>
+ {/* Left accent strip */}
+
+
+
+
+ {/* Step name + icon */}
+
+ {iconEl}
+
+ {step.name}
+
+
+
+ {/* Right side: status or time */}
+
+ {isRunning && (
+
+ In progress
+
+ )}
+ {elapsed && (
+
+ {elapsed}
+
+ )}
+ {!elapsed && !isRunning && (
+ —
+ )}
+
+
+
+
+ );
+}
+
export function JobMetaCards({ job }: JobMetaCardsProps) {
const isPartialReview = job.status === 'done' && job.errorMessage?.startsWith('Partial review:');
+ const steps = job.steps ?? [];
+ const shortCommitSha = job.commitSha?.slice(0, 7) ?? 'unknown';
return (
- {/* Details */}
+
+ {/* ── Job details ── */}
Job details
-
-
+
+
+ {/* Metadata grid */}
+
{[
{ label: 'Status', value: },
- { label: 'Verdict', value: job.verdict ? : — },
+ { label: 'Verdict', value: job.verdict
+ ?
+ : —
+ },
{ label: 'Trigger', value: {job.trigger} },
- { label: 'Tokens', value: {(job.totalInputTokens + job.totalOutputTokens).toLocaleString()} },
+ { label: 'Tokens', value:
+
+ {(job.totalInputTokens + job.totalOutputTokens).toLocaleString()}
+
+ },
].map(({ label, value }) => (
-
- {label}
+ -
+ {label}
+
- {value}
))}
+
+
{job.reviewId && (
)}
+
{job.retryOfJobId && (
-
- Retry of
+ - Retry of
-
-
+
{job.retryOfJobId.slice(0, 8)}…
)}
-
-
- Created
-
- {new Date(job.createdAt).toLocaleString()}
+
+
+
- Created
+ - {new Date(job.createdAt).toLocaleString()}
+ {/* Error / partial message */}
{job.errorMessage && (
-
+
{isPartialReview ? 'Partial review' : 'Error'}
-
{job.errorMessage}
+
+ {job.errorMessage}
+
)}
- {/* Steps */}
+ {/* ── Progress steps ── */}
Progress steps
- {(job.steps ?? []).length === 0 ? (
- No detailed steps available yet.
+ {steps.length === 0 ? (
+ No steps recorded yet.
) : (
-
- {(job.steps ?? []).map((step, idx) => (
-
-
-
- {step.status === 'running'
- ? 'Processing…'
- : step.finishedAt && step.startedAt
- ? `${((new Date(step.finishedAt).getTime() - new Date(step.startedAt).getTime()) / 1000).toFixed(1)}s`
- : '—'}
-
-
+
+ {steps.map((step, idx) => (
+
))}
)}
diff --git a/src/client/components/features/job-detail/job-progress.tsx b/src/client/components/features/job-detail/job-progress.tsx
index 5c8bcf6..fc794db 100644
--- a/src/client/components/features/job-detail/job-progress.tsx
+++ b/src/client/components/features/job-detail/job-progress.tsx
@@ -1,3 +1,4 @@
+import { FileCode2, Hourglass } from 'lucide-react';
import type { JobDetail } from '@shared/schema';
interface JobProgressProps {
@@ -7,23 +8,86 @@ interface JobProgressProps {
export function JobProgress({ job }: JobProgressProps) {
if (job.status !== 'running' && job.status !== 'queued') return null;
- const finishedFilesCount = job.files.filter((f) => f.fileStatus === 'done').length;
- const totalFilesCount = job.fileCount || 0;
- const progressPercent = totalFilesCount > 0 ? Math.round((finishedFilesCount / totalFilesCount) * 100) : 0;
+ const finishedCount = job.files.filter(f => f.fileStatus === 'done' || f.fileStatus === 'skipped').length;
+ const total = job.fileCount || 0;
+ const pct = total > 0 ? Math.round((finishedCount / total) * 100) : 0;
+ const isQueued = job.status === 'queued';
+
+ const activeFile = job.files.find(f => f.fileStatus === 'pending');
+ const activeFilePath = activeFile?.filePath ?? null;
+
+ // Shorten file path for display: keep last 2 segments
+ const displayPath = activeFilePath
+ ? activeFilePath.split('/').slice(-2).join('/')
+ : null;
+ const prefixPath = activeFilePath && activeFilePath.includes('/')
+ ? activeFilePath.split('/').slice(0, -2).join('/') + '/'
+ : null;
return (
-
-
-
- {job.status === 'queued' ? 'Queued…' : 'Reviewing files…'}
-
- {finishedFilesCount} / {totalFilesCount} files
-
-
+
+ {/* Subtle grid texture */}
+
+
+
+ {/* Top row: label + count */}
+
+
+ {isQueued
+ ?
+ :
+ }
+
+ {isQueued ? 'Waiting in queue' : 'Reviewing files'}
+
+
+
+ {isQueued ? '—' : `${finishedCount} / ${total}`}
+
+
+
+ {/* Progress track */}
+ className="h-[3px] rounded-full bg-primary-foreground/15 overflow-hidden"
+ role="progressbar"
+ aria-valuenow={isQueued ? 0 : pct}
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-label={isQueued ? 'Review waiting in queue' : 'File review progress'}
+ >
+
+
+
+ {/* Active file + percent */}
+ {!isQueued && (
+
+
+ {prefixPath && (
+ {prefixPath}
+ )}
+ {displayPath
+ ? {displayPath}
+ : —
+ }
+
+
{pct}%
+
+ )}
);
diff --git a/src/client/components/features/reviews/live-review-stepper.tsx b/src/client/components/features/reviews/live-review-stepper.tsx
index 3c5d7cf..8bb5ef4 100644
--- a/src/client/components/features/reviews/live-review-stepper.tsx
+++ b/src/client/components/features/reviews/live-review-stepper.tsx
@@ -8,84 +8,51 @@ interface LiveReviewStepperProps {
export function LiveReviewStepper({ job, compact = true }: LiveReviewStepperProps) {
const { status, steps = [] } = job;
- // Define our 4 target steps
- const stepperLabels = ['Queued', 'Scanning', 'Analyzing', 'Done'];
-
- // Determine state for each of our 4 steps: 'pending' | 'running' | 'done' | 'failed'
- let stepStates: Array<'pending' | 'running' | 'done' | 'failed'> = ['pending', 'pending', 'pending', 'pending'];
let activeLabel = '';
if (status === 'queued') {
- stepStates = ['running', 'pending', 'pending', 'pending'];
activeLabel = 'Queued';
} else if (status === 'done') {
- stepStates = ['done', 'done', 'done', 'done'];
activeLabel = 'Done';
} else if (status === 'failed') {
- // Basic mapping for failure
const failedStep = steps.find(s => s.status === 'failed');
- if (failedStep) {
- if (['Initializing', 'Fetching Diff'].includes(failedStep.name)) {
- stepStates = ['done', 'failed', 'pending', 'pending'];
- activeLabel = 'Scanning Failed';
- } else if (['Reviewing Files', 'Generating Summary'].includes(failedStep.name)) {
- stepStates = ['done', 'done', 'failed', 'pending'];
- activeLabel = 'Analysis Failed';
- } else {
- stepStates = ['done', 'done', 'done', 'failed'];
- activeLabel = 'Finalizing Failed';
- }
+ if (failedStep && ['Initializing', 'Fetching Diff'].includes(failedStep.name)) {
+ activeLabel = 'Scan failed';
+ } else if (failedStep && ['Reviewing Files', 'Generating Summary'].includes(failedStep.name)) {
+ activeLabel = 'Review failed';
} else {
- stepStates = ['done', 'done', 'done', 'failed'];
activeLabel = 'Failed';
}
} else if (status === 'running') {
const runningStep = steps.find(s => s.status === 'running');
-
if (!runningStep || ['Initializing', 'Fetching Diff'].includes(runningStep.name)) {
- stepStates = ['done', 'running', 'pending', 'pending'];
activeLabel = 'Scanning';
- } else if (['Reviewing Files', 'Generating Summary'].includes(runningStep.name)) {
- stepStates = ['done', 'done', 'running', 'pending'];
- activeLabel = 'Analyzing';
+ } else if (runningStep.name === 'Reviewing Files') {
+ activeLabel = 'Reviewing';
+ } else if (runningStep.name === 'Generating Summary') {
+ activeLabel = 'Summarising';
} else if (runningStep.name === 'Completing') {
- stepStates = ['done', 'done', 'done', 'running'];
- activeLabel = 'Finalizing';
+ activeLabel = 'Finishing';
} else {
- stepStates = ['done', 'done', 'running', 'pending'];
- activeLabel = 'Analyzing';
+ activeLabel = 'Running';
}
} else if (status === 'superseded') {
- stepStates = ['done', 'done', 'done', 'done']; // Treat as finished but maybe different color?
activeLabel = 'Superseded';
}
+ const styles: Record
= {
+ running: 'bg-info/10 text-info border-info/20',
+ queued: 'bg-secondary text-muted-foreground border-border/60',
+ done: 'bg-success/10 text-success border-success/20',
+ failed: 'bg-danger/10 text-danger border-danger/20',
+ superseded: 'bg-secondary text-muted-foreground border-border/40',
+ };
+
+ const cls = styles[status] ?? styles.queued;
+
return (
-
-
- {stepStates.map((state, i) => (
-
- ))}
-
- {!compact && (
-
- {activeLabel}
-
- )}
- {compact && status === 'running' && (
-
- {activeLabel}…
-
- )}
- {compact && status === 'queued' && (
-
- Queued
-
- )}
-
+
+ {activeLabel}
+
);
}
diff --git a/src/client/hooks/use-job-detail.ts b/src/client/hooks/use-job-detail.ts
index 0bd6f78..ba67efb 100644
--- a/src/client/hooks/use-job-detail.ts
+++ b/src/client/hooks/use-job-detail.ts
@@ -8,45 +8,71 @@ export function useJobDetail(id: string) {
const [job, setJob] = useState(null);
const [error, setError] = useState(null);
const [isRetrying, setIsRetrying] = useState(false);
- const pollInterval = useRef(null);
+ const pollTimeout = useRef(null);
+ const etag = useRef(null);
+ const latestJob = useRef(null);
+
+ const isTerminal = (candidate: JobDetail | null) => candidate?.status === 'done' || candidate?.status === 'failed' || candidate?.status === 'superseded';
+
+ const getPollDelay = (candidate: JobDetail | null) => {
+ if (!candidate || isTerminal(candidate)) return null;
+
+ const nextRetryAt = candidate.nextRetryAt ? new Date(candidate.nextRetryAt).getTime() : null;
+ const waitingForRetry = nextRetryAt !== null && Number.isFinite(nextRetryAt) && nextRetryAt > Date.now();
+ const baseDelay = waitingForRetry ? Math.min(Math.max(nextRetryAt - Date.now(), 10_000), 15_000) : 3_000;
+
+ return document.visibilityState === 'hidden' ? Math.max(baseDelay, 45_000) : baseDelay;
+ };
const fetchJob = async (silent = false) => {
try {
- const response = await api.getJob(id);
- setJob(response.job);
+ const response = await api.getJob(id, { etag: etag.current });
+ if (response.etag) etag.current = response.etag;
+ if (!response.notModified && response.data) {
+ latestJob.current = response.data.job;
+ setJob(response.data.job);
+ }
setError(null);
- if (response.job.status === 'done' || response.job.status === 'failed') stopPolling();
+ schedulePolling();
} catch (loadError) {
if (!silent) setError(loadError instanceof Error ? loadError.message : 'Failed to load job.');
+ schedulePolling();
}
};
- const startPolling = () => {
- if (pollInterval.current) return;
- pollInterval.current = window.setInterval(() => fetchJob(true), 3000);
- };
-
const stopPolling = () => {
- if (pollInterval.current) {
- window.clearInterval(pollInterval.current);
- pollInterval.current = null;
+ if (pollTimeout.current) {
+ window.clearTimeout(pollTimeout.current);
+ pollTimeout.current = null;
}
};
+ const schedulePolling = () => {
+ stopPolling();
+ const delay = getPollDelay(latestJob.current);
+ if (delay === null) return;
+ pollTimeout.current = window.setTimeout(() => fetchJob(true), delay);
+ };
+
useEffect(() => {
if (id) {
+ etag.current = null;
+ latestJob.current = null;
fetchJob();
}
return () => stopPolling();
}, [id]);
useEffect(() => {
- if (job && (job.status === 'queued' || job.status === 'running')) {
- startPolling();
- } else {
- stopPolling();
- }
- }, [job?.status]);
+ latestJob.current = job;
+ schedulePolling();
+ }, [job?.status, job?.nextRetryAt]);
+
+ useEffect(() => {
+ const reschedule = () => schedulePolling();
+ document.addEventListener('visibilitychange', reschedule);
+ return () => document.removeEventListener('visibilitychange', reschedule);
+ }, [id, job?.status, job?.nextRetryAt]);
const handleRetry = async () => {
if (!job) return;
diff --git a/src/client/lib/api.ts b/src/client/lib/api.ts
index 368aeeb..3a469d4 100644
--- a/src/client/lib/api.ts
+++ b/src/client/lib/api.ts
@@ -51,6 +51,52 @@ async function request(input: string, init?: RequestInit) {
return (await response.json()) as T;
}
+async function requestWithMeta(input: string, init?: RequestInit) {
+ const method = init?.method?.toUpperCase() ?? 'GET';
+ const headers = new Headers(init?.headers);
+
+ if (!headers.has('content-type')) {
+ headers.set('content-type', 'application/json');
+ }
+
+ if (!SAFE_METHODS.has(method)) {
+ headers.set('x-requested-with', 'XMLHttpRequest');
+ }
+
+ const response = await fetch(input, {
+ credentials: 'same-origin',
+ ...init,
+ headers,
+ });
+
+ if (response.status === 401) {
+ if (location.pathname !== '/login') {
+ location.href = '/login';
+ }
+ throw new Error('Unauthorized');
+ }
+
+ const etag = response.headers.get('etag');
+ const lastModified = response.headers.get('last-modified');
+
+ if (response.status === 304) {
+ return { status: response.status, etag, lastModified, notModified: true as const };
+ }
+
+ if (!response.ok) {
+ const payload = (await response.json().catch(() => null)) as { error?: string } | null;
+ throw new Error(payload?.error ?? `Request failed with ${response.status}`);
+ }
+
+ return {
+ status: response.status,
+ etag,
+ lastModified,
+ notModified: false as const,
+ data: (await response.json()) as T,
+ };
+}
+
export const api = {
getSession() {
return request('/api/auth/session');
@@ -79,8 +125,12 @@ export const api = {
const query = searchParams.toString();
return request(`/api/jobs${query ? `?${query}` : ''}`);
},
- getJob(id: string) {
- return request(`/api/jobs/${id}`);
+ getJob(id: string, options: { etag?: string | null } = {}) {
+ const headers = new Headers();
+ if (options.etag) {
+ headers.set('if-none-match', options.etag);
+ }
+ return requestWithMeta(`/api/jobs/${id}`, { headers });
},
retryJob(id: string) {
return request(`/api/jobs/${id}/retry`, {
diff --git a/src/client/main.tsx b/src/client/main.tsx
index 2415a19..95b7dbf 100644
--- a/src/client/main.tsx
+++ b/src/client/main.tsx
@@ -26,6 +26,25 @@ function ToasterWrapper() {
position="bottom-right"
richColors
closeButton
+ gap={8}
+ toastOptions={{
+ duration: 4000,
+ classNames: {
+ toast: 'codra-toast',
+ title: 'codra-toast-title',
+ description: 'codra-toast-description',
+ actionButton: 'codra-toast-action',
+ cancelButton: 'codra-toast-cancel',
+ closeButton: 'codra-toast-close',
+ icon: 'codra-toast-icon',
+ loader: 'codra-toast-loader',
+ success: 'codra-toast-success',
+ error: 'codra-toast-error',
+ warning: 'codra-toast-warning',
+ info: 'codra-toast-info',
+ loading: 'codra-toast-loading',
+ },
+ }}
/>
);
}
diff --git a/src/client/pages/job-logs.tsx b/src/client/pages/job-logs.tsx
index 779a69f..2e3d071 100644
--- a/src/client/pages/job-logs.tsx
+++ b/src/client/pages/job-logs.tsx
@@ -1,9 +1,138 @@
import { useParams, Link } from 'react-router-dom';
-import { ChevronLeft } from 'lucide-react';
+import {
+ ChevronLeft, FileCode2, Clock, Cpu, Hash,
+ AlertCircle, CheckCircle2, SkipForward, Hourglass,
+ ChevronDown,
+} from 'lucide-react';
import { useJobDetail } from '@client/hooks/use-job-detail';
import { JobDetailSkeleton } from '@client/components/features/job-detail/job-skeleton';
import { Alert } from '@client/components/ui/alert';
-import { PageHeader } from '@client/components/layout/page-header';
+import type { FileReviewRecord } from '@shared/schema';
+
+function fmtMs(ms: number | null) {
+ if (ms === null) return null;
+ if (ms < 1000) return `${ms}ms`;
+ return `${(ms / 1000).toFixed(1)}s`;
+}
+
+function fmtK(n: number | null) {
+ if (n === null) return null;
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
+}
+
+type FileStatus = FileReviewRecord['fileStatus'];
+
+const STATUS_META: Record = {
+ done: { Icon: CheckCircle2, iconCls: 'text-success', pill: 'bg-success/10 text-success border-success/20', label: 'Done' },
+ skipped: { Icon: SkipForward, iconCls: 'text-muted-foreground', pill: 'bg-secondary text-muted-foreground border-border/50', label: 'Skipped' },
+ failed: { Icon: AlertCircle, iconCls: 'text-danger', pill: 'bg-danger/10 text-danger border-danger/20', label: 'Failed' },
+ pending: { Icon: Hourglass, iconCls: 'text-muted-foreground', pill: 'bg-secondary text-muted-foreground border-border/50', label: 'Pending' },
+};
+
+function FileCard({ file }: { file: FileReviewRecord }) {
+ const meta = STATUS_META[file.fileStatus] ?? STATUS_META.pending;
+ const { Icon } = meta;
+ const duration = fmtMs(file.durationMs);
+ const inTok = fmtK(file.inputTokens);
+ const outTok = fmtK(file.outputTokens);
+ const modelShort = file.modelUsed?.split('/').pop() ?? null;
+
+ return (
+
+
+
+ {/* Status icon */}
+
+
+ {/* File path */}
+
+ {file.filePath}
+
+
+ {/* Meta chips — hidden on small screens */}
+
+ {modelShort && (
+
+ {modelShort}
+
+ )}
+ {duration && (
+
+ {duration}
+
+ )}
+ {(inTok || outTok) && (
+
+ {inTok ?? '—'}↑ {outTok ?? '—'}↓
+
+ )}
+
+
+ {/* Status pill */}
+
+ {meta.label}
+
+
+ {/* Chevron */}
+
+
+
+ {/* Expanded content */}
+
+
+ {/* Mobile meta strip */}
+
+ {modelShort && {modelShort}}
+ {duration && {duration}}
+ {inTok && {inTok}↑ {outTok ?? '—'}↓}
+
+
+ {/* File-level error */}
+ {file.fileStatus === 'failed' && file.errorMessage && (
+
+
+ Review error
+
+
+ {file.errorMessage}
+
+
+ )}
+
+ {/* Two-column content */}
+
+
+
+ Prompt / diff
+
+
+ {file.diffInput ?? '— No prompt saved —'}
+
+
+
+
+ Raw model output
+
+
+ {file.rawAiOutput ?? '— No output saved —'}
+
+
+
+
+
+ );
+}
export function JobLogsPage() {
const { id = '' } = useParams();
@@ -11,47 +140,75 @@ export function JobLogsPage() {
if (!job) return ;
+ const counts = {
+ done: job.files.filter(f => f.fileStatus === 'done').length,
+ skipped: job.files.filter(f => f.fileStatus === 'skipped').length,
+ failed: job.files.filter(f => f.fileStatus === 'failed').length,
+ total: job.files.length,
+ };
+
return (
-
-
- Back to Job Details
-
-
-
+ {/* Back */}
+
+
+ Back to Job Details
+
- {error && {error}}
+ {/* Page header */}
+
+
+
Raw Logs
+
Review logs
+
+ {job.owner}/{job.repo} · PR #{job.prNumber} · {job.commitSha.slice(0, 7)}
+
+
-
- {job.files.length === 0 ? (
-
No files processed.
- ) : (
- job.files.map((file) => (
-
-
{file.filePath}
-
-
-
- Prompt / diff
-
-
{file.diffInput ?? 'No prompt saved.'}
-
-
-
- Raw model output
-
-
{file.rawAiOutput ?? 'No raw output saved.'}
-
+ {/* Summary counts */}
+ {counts.total > 0 && (
+
+ {[
+ { label: 'Files', val: counts.total, cls: 'text-foreground' },
+ { label: 'Reviewed', val: counts.done, cls: 'text-success' },
+ { label: 'Skipped', val: counts.skipped, cls: 'text-muted-foreground' },
+ { label: 'Failed', val: counts.failed, cls: counts.failed > 0 ? 'text-danger' : 'text-muted-foreground' },
+ ].map(({ label, val, cls }) => (
+
+ {val}
+ {label}
-
- ))
+ ))}
+
)}
+
+ {error &&
{error}}
+
+ {/* File list */}
+ {job.files.length === 0 ? (
+
+
+
+
No files processed yet
+ {(job.status === 'running' || job.status === 'queued') && (
+
+ Logs appear here once files are reviewed
+
+ )}
+
+
+ ) : (
+
+ {job.files.map(file => (
+
+ ))}
+
+ )}
);
}
diff --git a/src/client/pages/repos.tsx b/src/client/pages/repos.tsx
index 0b70032..d45b9c8 100644
--- a/src/client/pages/repos.tsx
+++ b/src/client/pages/repos.tsx
@@ -238,7 +238,7 @@ function RepoModelModal({
if (!repo || !dirty) return;
setSaving('apply');
setError(null);
- const tid = toast.loading(`Saving model strategy for ${repo.owner}/${repo.repo}...`);
+ const tid = toast.loading('Applying model strategy…');
try {
await api.updateRepoConfig(repo.owner, repo.repo, {
model: {
@@ -249,11 +249,11 @@ function RepoModelModal({
});
setInitialRoute(route);
onModelApplied(repo, route);
- toast.success('Model strategy saved', { id: tid });
+ toast.success('Strategy saved', { id: tid, description: `${repo.owner}/${repo.repo} now uses a custom model chain.` });
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to save model strategy.';
setError(msg);
- toast.error('Save failed', { id: tid, description: msg });
+ toast.error('Could not save strategy', { id: tid, description: 'Your changes were not applied. Please try again.' });
} finally {
setSaving(null);
}
@@ -263,7 +263,7 @@ function RepoModelModal({
if (!repo) return;
setSaving('reset');
setError(null);
- const tid = toast.loading(`Using global strategy for ${repo.owner}/${repo.repo}...`);
+ const tid = toast.loading('Resetting to global defaults…');
try {
await api.updateRepoConfig(repo.owner, repo.repo, {
model: {
@@ -276,11 +276,11 @@ function RepoModelModal({
setRoute(globalRoute);
setInitialRoute(globalRoute);
onModelReset(repo);
- toast.success('Repo now uses global strategy', { id: tid });
+ toast.success('Reset to global strategy', { id: tid, description: `${repo.owner}/${repo.repo} will inherit account defaults.` });
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to reset model strategy.';
setError(msg);
- toast.error('Reset failed', { id: tid, description: msg });
+ toast.error('Reset failed', { id: tid, description: 'Could not remove the custom strategy. Try again.' });
} finally {
setSaving(null);
}
@@ -379,14 +379,19 @@ export function ReposPage() {
const handleToggleEnabled = async (repo: RepoConfigRecord, nextEnabled: boolean) => {
const targetId = repoId(repo);
setPendingToggles(current => new Set(current).add(targetId));
- const tid = toast.loading(`${nextEnabled ? 'Enabling' : 'Pausing'} ${targetId}...`);
+ const tid = toast.loading(nextEnabled ? 'Enabling code reviews…' : 'Pausing code reviews…');
try {
await api.updateRepoConfig(repo.owner, repo.repo, { enabled: nextEnabled });
mergeRepo(targetId, { enabled: nextEnabled });
- toast.success(nextEnabled ? 'Reviews enabled' : 'Reviews paused', { id: tid, description: targetId });
+ toast.success(
+ nextEnabled ? 'Reviews active' : 'Reviews paused',
+ { id: tid, description: nextEnabled
+ ? `${targetId} will receive automated review comments.`
+ : `${targetId} is now quiet — no new reviews will be posted.`
+ },
+ );
} catch (err) {
- const msg = err instanceof Error ? err.message : 'Failed to update repository.';
- toast.error('Update failed', { id: tid, description: msg });
+ toast.error('Could not update repository', { id: tid, description: 'The change did not go through. Please try again.' });
} finally {
setPendingToggles(current => {
const next = new Set(current);
@@ -416,19 +421,21 @@ export function ReposPage() {
if (syncing) return;
setSyncing(true);
setError(null);
- const tid = toast.loading('Syncing repositories from GitHub...');
+ const tid = toast.loading('Syncing with GitHub…');
try {
const result = await api.syncRepos();
const syncedCount = result?.synced?.length ?? 0;
- toast.success('Repositories synced', {
+ toast.success('Repositories up to date', {
id: tid,
- description: `${syncedCount} ${syncedCount === 1 ? 'repository' : 'repositories'} synced successfully`,
+ description: syncedCount > 0
+ ? `${syncedCount} ${syncedCount === 1 ? 'repository' : 'repositories'} refreshed from GitHub.`
+ : 'Everything is already in sync.',
});
loadRepos();
} catch (e) {
const msg = e instanceof Error ? e.message : 'Sync failed.';
setError(msg);
- toast.error('Sync failed', { id: tid, description: msg });
+ toast.error('Sync failed', { id: tid, description: 'Could not reach GitHub. Check your connection and try again.' });
} finally {
setSyncing(false);
}
diff --git a/src/client/pages/settings.tsx b/src/client/pages/settings.tsx
index cc75810..f09f86e 100644
--- a/src/client/pages/settings.tsx
+++ b/src/client/pages/settings.tsx
@@ -98,7 +98,7 @@ export function SettingsPage() {
} catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to load settings';
setError(msg);
- toast.error('Failed to load settings', { description: msg });
+ toast.error('Could not load settings', { description: 'Something went wrong fetching your configuration.' });
} finally {
setLoading(false);
}
@@ -110,18 +110,18 @@ export function SettingsPage() {
if (!globalConfig || !globalDirty) return;
setSaving('global');
setError(null);
- const tid = toast.loading('Saving global strategy...');
+ const tid = toast.loading('Saving model strategy…');
try {
await api.updateGlobalConfig(globalConfig);
setSavedGlobalConfig(globalConfig);
toast.success('Global strategy saved', {
id: tid,
- description: `Primary model: ${getModelLabel(globalConfig.main)}`,
+ description: 'All repositories without a custom strategy will use these settings.',
});
} catch (e) {
const msg = e instanceof Error ? e.message : 'Update failed';
setError(msg);
- toast.error('Failed to save strategy', { id: tid, description: msg });
+ toast.error('Could not save strategy', { id: tid, description: 'Your changes were not applied. Please try again.' });
} finally {
setSaving(null);
}
@@ -137,16 +137,16 @@ export function SettingsPage() {
if (!current) return;
setSaving(id);
setError(null);
- const tid = toast.loading(`Updating ${id}...`);
+ const tid = toast.loading('Updating quota…');
try {
await api.updateModelConfig(id, quotaPayload(current));
const saved = { ...current, updatedAt: new Date().toISOString() };
markConfigSaved(id, saved);
- toast.success('Model quota updated', { id: tid, description: id });
+ toast.success('Quota updated', { id: tid, description: 'Rate limits have been applied to this model.' });
} catch (e) {
const msg = e instanceof Error ? e.message : 'Update failed';
setError(msg);
- toast.error('Failed to update quota', { id: tid, description: msg });
+ toast.error('Quota update failed', { id: tid, description: 'The limit change did not save. Please try again.' });
} finally {
setSaving(null);
}
@@ -156,7 +156,11 @@ export function SettingsPage() {
if (dirtyConfigs.length === 0) return;
setSaving('quotas');
setError(null);
- const tid = toast.loading(`Saving ${dirtyConfigs.length} quota ${dirtyConfigs.length === 1 ? 'change' : 'changes'}...`);
+ const tid = toast.loading(
+ dirtyConfigs.length === 1
+ ? 'Saving 1 quota change…'
+ : `Saving ${dirtyConfigs.length} quota changes…`,
+ );
try {
await Promise.all(dirtyConfigs.map(cfg => api.updateModelConfig(cfg.modelId, quotaPayload(cfg))));
const now = new Date().toISOString();
@@ -165,11 +169,11 @@ export function SettingsPage() {
setSavedConfigs(current =>
configs.map(cfg => (dirtyIds.has(cfg.modelId) ? { ...cfg, updatedAt: now } : current.find(saved => saved.modelId === cfg.modelId) ?? cfg)),
);
- toast.success('Quotas saved', { id: tid });
+ toast.success('All quotas saved', { id: tid, description: `${dirtyConfigs.length} ${dirtyConfigs.length === 1 ? 'model' : 'models'} updated successfully.` });
} catch (e) {
const msg = e instanceof Error ? e.message : 'Update failed';
setError(msg);
- toast.error('Failed to save quotas', { id: tid, description: msg });
+ toast.error('Could not save quotas', { id: tid, description: 'One or more limits failed to save. Try again.' });
} finally {
setSaving(null);
}
diff --git a/src/server/core/job-recovery.ts b/src/server/core/job-recovery.ts
index 9b3a80a..c00c193 100644
--- a/src/server/core/job-recovery.ts
+++ b/src/server/core/job-recovery.ts
@@ -59,3 +59,13 @@ export async function runBestEffortJobMaintenance(env: AppBindings) {
logger.error('Opportunistic job maintenance failed', error instanceof Error ? error : new Error(String(error)));
}
}
+
+export function scheduleBestEffortJobMaintenance(
+ env: AppBindings,
+ executionCtx?: Pick
,
+) {
+ const task = runBestEffortJobMaintenance(env);
+ if (executionCtx) {
+ executionCtx.waitUntil(task);
+ }
+}
diff --git a/src/server/core/model-output.ts b/src/server/core/model-output.ts
index 856912e..3c14633 100644
--- a/src/server/core/model-output.ts
+++ b/src/server/core/model-output.ts
@@ -5,11 +5,11 @@ import { findClosestValidLine, findPositionForLine, getValidNewLines, getValidPo
import type { FileDiff } from './diff';
import { jsonrepair } from 'jsonrepair';
-const MAX_LOGGED_MODEL_OUTPUT_CHARS = 2_000;
+const MAX_LOGGED_JSON_CHARS = 2_000;
-function truncateForLog(value: string) {
- if (value.length <= MAX_LOGGED_MODEL_OUTPUT_CHARS) return value;
- return `${value.slice(0, MAX_LOGGED_MODEL_OUTPUT_CHARS)}... [truncated ${value.length - MAX_LOGGED_MODEL_OUTPUT_CHARS} chars]`;
+function truncateJsonForLog(value: string) {
+ if (value.length <= MAX_LOGGED_JSON_CHARS) return value;
+ return `${value.slice(0, MAX_LOGGED_JSON_CHARS)}... [truncated ${value.length - MAX_LOGGED_JSON_CHARS} chars]`;
}
function hasReviewKeys(input: string) {
@@ -260,7 +260,10 @@ export function parseFileReviewResponse(raw: string, file: FileDiff): {
throw new Error('Model response did not contain review JSON keys.');
}
} catch (e) {
- logger.error('Failed to extract JSON from model response', { raw: truncateForLog(raw), error: e });
+ logger.error('Failed to extract JSON from model response', {
+ rawLength: raw.length,
+ error: e instanceof Error ? e.message : String(e),
+ });
throw new Error('Could not find JSON root in model response.');
}
@@ -276,14 +279,14 @@ export function parseFileReviewResponse(raw: string, file: FileDiff): {
try {
repaired = jsonrepair(preprocessed);
} catch (e) {
- logger.warn('jsonrepair failed to fix model output, using preprocessed text', { preprocessed: truncateForLog(preprocessed), error: e });
+ logger.warn('jsonrepair failed to fix model output, using preprocessed text', { preprocessed: truncateJsonForLog(preprocessed), error: e });
}
let parsedJson: any;
try {
parsedJson = JSON.parse(repaired);
} catch (e) {
- logger.error('Critical JSON parse error after extraction and repair', { repaired: truncateForLog(repaired), error: e });
+ logger.error('Critical JSON parse error after extraction and repair', { repaired: truncateJsonForLog(repaired), error: e });
throw new Error(`Invalid JSON format: ${e instanceof Error ? e.message : 'Unknown error'}`);
}
diff --git a/src/server/core/review.ts b/src/server/core/review.ts
index 57187fa..c4d4923 100644
--- a/src/server/core/review.ts
+++ b/src/server/core/review.ts
@@ -18,11 +18,11 @@ type PersistedReviewJob = ReturnType;
export type ReviewJobRunResult = { action: 'ack' } | { action: 'retry'; delaySeconds: number };
-const REVIEW_CHUNK_FILE_LIMIT = 2;
-const REVIEW_CHUNK_WALL_CLOCK_MS = 8 * 60 * 1000;
-const JOB_LEASE_SECONDS = 10 * 60;
+const REVIEW_CHUNK_FILE_LIMIT = 3;
+const REVIEW_CHUNK_WALL_CLOCK_MS = 12 * 60 * 1000;
+const JOB_LEASE_SECONDS = 15 * 60;
const BUSY_RETRY_SECONDS = 60;
-const RETRYABLE_MODEL_FAILURE_RETRY_SECONDS = 60;
+const RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS = [60, 5 * 60, 15 * 60];
const MAX_RETRYABLE_FILE_REVIEW_FAILURES = 3;
function isRetryableFileReviewErrorMessage(message: string | null | undefined) {
@@ -45,6 +45,21 @@ function isRetryableFileReviewErrorMessage(message: string | null | undefined) {
);
}
+function retryableModelFailureDelaySeconds(failureCount: number | null | undefined) {
+ if (!failureCount || failureCount < 1) return RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS[0];
+ const index = Math.min(failureCount - 1, RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS.length - 1);
+ return RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS[index];
+}
+
+function getRetryableModelFailureDelaySeconds(error: unknown) {
+ const record = error && typeof error === 'object' ? error as { retryAfterSeconds?: unknown } : null;
+ const retryAfterSeconds =
+ typeof record?.retryAfterSeconds === 'number'
+ ? record.retryAfterSeconds
+ : null;
+ return retryAfterSeconds ?? RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS[0];
+}
+
function shouldRetryExistingFileReview(review: { file_status: string; error_msg: string | null }) {
return review.file_status === 'failed' && isRetryableFileReviewErrorMessage(review.error_msg);
}
@@ -187,7 +202,7 @@ export async function runReviewJob(env: AppBindings, message: ReviewJobMessage):
if (phase === 'prepare') {
await runPreparePhase(env, job, leaseOwner, github);
} else if (phase === 'finalize') {
- await runFinalizePhase(env, job, leaseOwner, github, model, formatter);
+ await runFinalizePhase(env, job, leaseOwner, github, formatter);
} else {
await runReviewPhase(env, job, leaseOwner, github, model);
}
@@ -203,12 +218,13 @@ export async function runReviewJob(env: AppBindings, message: ReviewJobMessage):
}
if (isRetryableModelError(error)) {
+ const delaySeconds = getRetryableModelFailureDelaySeconds(error);
logger.warn(`Review job hit transient model/provider failure; scheduling delayed continuation: ${job.owner}/${job.repo} PR #${job.prNumber}`, {
error: messageText,
phase,
- delaySeconds: RETRYABLE_MODEL_FAILURE_RETRY_SECONDS,
+ delaySeconds,
});
- await enqueueJobPhase(env, job.id, phase, RETRYABLE_MODEL_FAILURE_RETRY_SECONDS);
+ await enqueueJobPhase(env, job.id, phase, delaySeconds);
await releaseJobLease(env, job.id, leaseOwner);
return { action: 'ack' };
}
@@ -417,6 +433,8 @@ async function runReviewPhase(
const currentReviews = new Map(allExistingReviews.filter((review) => review.job_id === job.id).map((review) => [review.file_path, review]));
const parentReviews = new Map(allExistingReviews.filter((review) => review.job_id !== job.id && review.file_status === 'done').map((review) => [review.file_path, review]));
+ const reviewTasks: Array> = [];
+
for (const file of files) {
const existingReview = currentReviews.get(file.path);
if (existingReview && countsAsHandledFileReview(existingReview)) {
@@ -424,12 +442,15 @@ async function runReviewPhase(
}
const inherited = parentReviews.get(file.path);
- if (inherited) {
+ const reviewTask = async () => {
+ if (!inherited) {
+ await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, existingReview);
+ return;
+ }
+
if (!canInheritParentFileReview(config, inherited)) {
logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`);
await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, existingReview);
- processedThisChunk += 1;
- await heartbeatAndCheckSuperseded(env, job.id, leaseOwner);
} else {
await upsertFileReview(env, job.id, {
filePath: file.path,
@@ -450,20 +471,30 @@ async function runReviewPhase(
errorMessage: null,
});
currentReviews.set(file.path, inherited);
- processedThisChunk += 1;
- await heartbeatAndCheckSuperseded(env, job.id, leaseOwner);
}
- } else {
- await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, existingReview);
- processedThisChunk += 1;
- await heartbeatAndCheckSuperseded(env, job.id, leaseOwner);
- }
+ };
+
+ reviewTasks.push(reviewTask());
+ processedThisChunk += 1;
if (processedThisChunk >= REVIEW_CHUNK_FILE_LIMIT || Date.now() - startedAt >= REVIEW_CHUNK_WALL_CLOCK_MS) {
break;
}
}
+ const results = await Promise.allSettled(reviewTasks);
+ await heartbeatAndCheckSuperseded(env, job.id, leaseOwner);
+
+ const rejected = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
+ if (rejected.length > 0) {
+ rejected.forEach((result, index) => {
+ logger.error(`Review chunk task ${index + 1}/${rejected.length} failed`, result.reason);
+ });
+ throw rejected.length === 1
+ ? rejected[0].reason
+ : new AggregateError(rejected.map((result) => result.reason), `${rejected.length} review chunk tasks failed`);
+ }
+
const latestReviews = await getFileReviewsForJobs(env, [job.id]);
const reviewedPaths = new Set(latestReviews.filter(countsAsHandledFileReview).map((review) => review.file_path));
const completedCount = files.filter((file) => reviewedPaths.has(file.path)).length;
@@ -567,6 +598,10 @@ async function reviewAndPersistFile(
error: errorMessage,
attempts: failureCount,
});
+ Object.defineProperty(error, 'retryAfterSeconds', {
+ value: retryableModelFailureDelaySeconds(failureCount),
+ configurable: true,
+ });
throw error;
}
@@ -606,7 +641,6 @@ async function runFinalizePhase(
job: PersistedReviewJob,
leaseOwner: string,
github: GitHubService,
- model: ModelService,
formatter: FormatterService,
) {
await updateJobStep(env, job.id, 'Generating Summary', { status: 'running' });
@@ -640,12 +674,6 @@ async function runFinalizePhase(
const hasFailures = fileSummaries.some((file) => file.verdict === 'failed');
const failedFileCount = fileSummaries.filter((file) => file.verdict === 'failed').length;
const verdictSummary = formatter.summarizeVerdict(reviewedComments, hasFailures);
- const summaryResponse = await model.generateSummary({
- prTitle: pr.title ?? null,
- verdict: verdictSummary.verdict,
- fileSummaries,
- config,
- });
await updateJobStep(env, job.id, 'Generating Summary', { status: 'done' });
await heartbeatAndCheckSuperseded(env, job.id, leaseOwner);
@@ -699,11 +727,11 @@ async function runFinalizePhase(
verdict: verdictSummary.verdict,
fileCount: files.length,
commentCount: reviewedComments.length,
- totalInputTokens: fileInputTokens + (summaryResponse.inputTokens ?? 0),
- totalOutputTokens: fileOutputTokens + (summaryResponse.outputTokens ?? 0),
+ totalInputTokens: fileInputTokens,
+ totalOutputTokens: fileOutputTokens,
summaryMarkdown: formattedSummary,
reviewId: review.id,
- summaryModel: summaryResponse.modelUsed,
+ summaryModel: null,
errorMessage: partialErrorMessage,
});
await updateJobStep(env, job.id, 'Completing', { status: 'done' });
@@ -724,7 +752,7 @@ async function enqueueJobPhase(
phase: 'prepare' | 'review' | 'finalize',
delaySeconds = 0,
) {
- await markJobContinuationQueued(env, jobId);
+ await markJobContinuationQueued(env, jobId, delaySeconds);
await env.REVIEW_QUEUE.send(
{
jobId,
diff --git a/src/server/db/jobs.ts b/src/server/db/jobs.ts
index ffe3cbd..b97b83a 100644
--- a/src/server/db/jobs.ts
+++ b/src/server/db/jobs.ts
@@ -82,7 +82,34 @@ export function bytesToHex(value: ByteaValue) {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
}
+function latestTimestamp(...values: Array) {
+ const now = Date.now();
+ return values.reduce((latest, value) => {
+ if (!value) return latest;
+ if (new Date(value).getTime() > now) return latest;
+ if (!latest) return value;
+ return new Date(value).getTime() > new Date(latest).getTime() ? value : latest;
+ }, null);
+}
+
export function mapJob(row: JobRow) {
+ const lastQueueMessageAt = row.last_queue_message_at ? new Date(row.last_queue_message_at).getTime() : null;
+ const nextRetryAt =
+ row.status === 'running' &&
+ row.lease_owner === null &&
+ lastQueueMessageAt !== null &&
+ Number.isFinite(lastQueueMessageAt) &&
+ lastQueueMessageAt > Date.now()
+ ? row.last_queue_message_at
+ : null;
+ const updatedAt = latestTimestamp(
+ row.created_at,
+ row.started_at,
+ row.finished_at,
+ row.heartbeat_at,
+ row.last_queue_message_at,
+ ) ?? row.created_at;
+
return jobSummarySchema.parse({
id: row.id,
owner: row.owner,
@@ -100,6 +127,8 @@ export function mapJob(row: JobRow) {
totalInputTokens: row.total_input_tokens ?? 0,
totalOutputTokens: row.total_output_tokens ?? 0,
createdAt: row.created_at,
+ updatedAt,
+ nextRetryAt,
startedAt: row.started_at,
finishedAt: row.finished_at,
errorMessage: row.error_msg,
@@ -402,6 +431,12 @@ export async function claimJobLease(
OR lease_expires_at < now()
OR lease_owner = $2
)
+ AND NOT (
+ status = 'running'
+ AND lease_owner IS NULL
+ AND last_queue_message_at IS NOT NULL
+ AND last_queue_message_at > now()
+ )
RETURNING *
)
SELECT c.*, r.owner, r.repo, r.installation_id
@@ -424,8 +459,10 @@ export async function claimJobLease(
return { status: 'terminal', row };
}
- const expiresAt = row.lease_expires_at ? new Date(row.lease_expires_at).getTime() : 0;
- const secondsUntilExpiry = Math.ceil((expiresAt - Date.now()) / 1000);
+ const leaseExpiresAt = row.lease_expires_at ? new Date(row.lease_expires_at).getTime() : 0;
+ const delayedUntil = row.lease_owner === null && row.last_queue_message_at ? new Date(row.last_queue_message_at).getTime() : 0;
+ const retryAt = Math.max(leaseExpiresAt, delayedUntil);
+ const secondsUntilExpiry = Math.ceil((retryAt - Date.now()) / 1000);
return {
status: 'busy',
row,
@@ -467,16 +504,20 @@ export async function releaseJobLease(env: Pick, jobI
);
}
-export async function markJobContinuationQueued(env: Pick, jobId: string) {
+export async function markJobContinuationQueued(env: Pick, jobId: string, delaySeconds = 0) {
await queryRows(
env,
`
UPDATE jobs
- SET last_queue_message_at = now()
+ SET heartbeat_at = now(),
+ last_queue_message_at = CASE
+ WHEN $2::int > 0 THEN now() + ($2::text || ' seconds')::interval
+ ELSE now()
+ END
WHERE id = $1
AND status = 'running'
`,
- [jobId],
+ [jobId, delaySeconds],
);
}
@@ -664,7 +705,8 @@ export async function updateJobStep(
env,
`
UPDATE jobs
- SET steps = CASE
+ SET heartbeat_at = now(),
+ steps = CASE
WHEN EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(steps, '[]'::jsonb)) s WHERE s->>'name' = $2)
THEN (
SELECT jsonb_agg(
diff --git a/src/server/models/cloudflare.ts b/src/server/models/cloudflare.ts
index 4814f25..36d4bc2 100644
--- a/src/server/models/cloudflare.ts
+++ b/src/server/models/cloudflare.ts
@@ -4,8 +4,56 @@ import { TimeoutError } from '@server/core/timeout';
import type { ModelResponse } from './types';
/** Max wall-clock time allowed for a single Workers-AI call. */
-const CLOUDFLARE_TIMEOUT_MS = 45_000;
-const CLOUDFLARE_MAX_RETRIES = 1;
+const CLOUDFLARE_TIMEOUT_MS = 180_000;
+const CLOUDFLARE_MAX_RETRIES = 0;
+const CLOUDFLARE_MAX_OUTPUT_TOKENS = 8192;
+const REVIEW_RESPONSE_SCHEMA = {
+ type: 'object',
+ additionalProperties: false,
+ required: ['findings', 'overall_explanation', 'overall_correctness', 'overall_confidence_score'],
+ properties: {
+ findings: {
+ type: 'array',
+ maxItems: 10,
+ items: {
+ type: 'object',
+ additionalProperties: false,
+ required: ['title', 'body', 'priority', 'code_location'],
+ properties: {
+ title: { type: 'string', maxLength: 100 },
+ body: { type: 'string' },
+ confidence_score: { type: 'number', minimum: 0, maximum: 1 },
+ priority: { type: 'integer', minimum: 0, maximum: 3 },
+ code_location: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ absolute_file_path: { type: 'string' },
+ line: { type: 'integer', minimum: 1 },
+ line_range: {
+ type: 'object',
+ additionalProperties: false,
+ required: ['start', 'end'],
+ properties: {
+ start: { type: 'integer', minimum: 1 },
+ end: { type: 'integer', minimum: 1 },
+ },
+ },
+ },
+ anyOf: [
+ { required: ['line'] },
+ { required: ['line_range'] },
+ ],
+ },
+ code_suggestion: { type: 'string' },
+ },
+ },
+ },
+ overall_explanation: { type: 'string' },
+ overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },
+ overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },
+ },
+} as const;
type UnknownRecord = Record;
@@ -35,6 +83,18 @@ function getNumber(value: unknown, key: string) {
return typeof child === 'number' ? child : null;
}
+function synthesizeInconclusiveReview(model: string, reason: string): string {
+ logger.warn(`Cloudflare model ${model} returned no parseable review content; synthesizing inconclusive review JSON`, {
+ reason,
+ });
+ return JSON.stringify({
+ findings: [],
+ overall_correctness: 'patch is incorrect',
+ overall_explanation: `Cloudflare model ${model} returned no parseable review content (${reason}). The file review is inconclusive.`,
+ overall_confidence_score: 0,
+ });
+}
+
function extractMessageContent(content: unknown): string | null {
if (isText(content)) return content.trim();
@@ -69,14 +129,16 @@ function extractCloudflareText(result: unknown, model: string): string {
if (content) return content;
const finishReason = isRecord(choice) ? choice.finish_reason ?? choice.stop_reason : null;
- if (finishReason) {
- throw new Error(`Cloudflare model ${model} returned no review content (finish_reason=${finishReason}).`);
+ const reasoning = isText(message?.reasoning) ? message.reasoning : isText(message?.reasoning_content) ? message.reasoning_content : null;
+ if (reasoning) {
+ return synthesizeInconclusiveReview(model, `reasoning-only response${finishReason ? `, finish_reason=${String(finishReason)}` : ''}`);
}
- if (isText(message?.reasoning) || isText(message?.reasoning_content)) {
- throw new Error(`Cloudflare model ${model} returned reasoning without review content.`);
+
+ if (finishReason) {
+ return synthesizeInconclusiveReview(model, `finish_reason=${String(finishReason)}`);
}
- throw new Error(`Cloudflare model ${model} returned an empty response.`);
+ return synthesizeInconclusiveReview(model, 'empty response');
}
function extractCloudflareUsage(result: unknown) {
@@ -116,11 +178,23 @@ export async function reviewWithCloudflare(
const result = await Promise.race([
env.AI.run(model as any, {
messages: [
- { role: 'system', content: input.systemPrompt },
- { role: 'user', content: input.userPrompt },
+ {
+ role: 'system',
+ content: `${input.systemPrompt}\n\nReturn only the JSON object. Do not include chain-of-thought, analysis, markdown, code fences, or explanatory prose.`,
+ },
+ { role: 'user', content: `${input.userPrompt}\n\nRespond with the required JSON object only.` },
],
- max_completion_tokens: 4096,
+ max_completion_tokens: CLOUDFLARE_MAX_OUTPUT_TOKENS,
+ response_format: {
+ type: 'json_schema',
+ json_schema: {
+ name: 'codra_file_review',
+ strict: true,
+ schema: REVIEW_RESPONSE_SCHEMA,
+ },
+ },
temperature: 0,
+ top_p: 0.1,
}),
timeoutPromise,
]);
diff --git a/src/server/models/google.ts b/src/server/models/google.ts
index b742768..ac31761 100644
--- a/src/server/models/google.ts
+++ b/src/server/models/google.ts
@@ -4,9 +4,13 @@ import { withTimeout } from '@server/core/timeout';
import type { ModelResponse } from './types';
/** Max wall-clock time allowed for a single Google AI Studio call. */
-const GOOGLE_TIMEOUT_MS = 45_000;
+const GOOGLE_TIMEOUT_MS = 180_000;
const GOOGLE_MAX_RETRIES = 1;
-const GOOGLE_MAX_OUTPUT_TOKENS = 3072;
+const GOOGLE_MAX_OUTPUT_TOKENS = 4096;
+
+function isRetryableGoogleStatus(status: number) {
+ return status === 408 || status === 503 || status === 524;
+}
export async function reviewWithGoogle(
env: Pick,
@@ -57,13 +61,18 @@ export async function reviewWithGoogle(
if (!response.ok) {
const errorText = await response.text();
const isRateLimit = response.status === 429;
- const isRetryable = !isRateLimit && response.status >= 500;
+ const isRetryable = isRetryableGoogleStatus(response.status);
- logger.error(`Google request failed with ${response.status}`, {
+ const logData = {
error: errorText,
attempt,
- willRetry: isRetryable && attempt < maxRetries
- });
+ willRetry: isRetryable && attempt < maxRetries,
+ };
+ if (isRetryable && attempt < maxRetries) {
+ logger.warn(`Google request failed with ${response.status}; retrying`, logData);
+ } else {
+ logger.error(`Google request failed with ${response.status}`, logData);
+ }
if (isRateLimit) {
throw new Error(`Google request failed with ${response.status}: ${errorText}`);
diff --git a/src/server/routes/api/jobs.ts b/src/server/routes/api/jobs.ts
index 88c2c2d..28503c6 100644
--- a/src/server/routes/api/jobs.ts
+++ b/src/server/routes/api/jobs.ts
@@ -1,16 +1,29 @@
import { Hono } from 'hono';
+import type { Context } from 'hono';
import { jobsQuerySchema } from '@shared/schema';
import type { AppEnv } from '@server/env';
import { bytesToHex, getJobDetail, getJobForProcessing, insertJob, listJobs, mapJob, supersedeOlderJobs } from '@server/db/jobs';
import { jsonError } from '@server/core/http';
-import { runBestEffortJobMaintenance } from '@server/core/job-recovery';
+import { scheduleBestEffortJobMaintenance } from '@server/core/job-recovery';
import { loadRepoConfig } from '@server/core/config';
+function jobEtag(input: { id: string; status: string; updatedAt: string; fileCount: number; commentCount: number }) {
+ return `"job-${input.id}-${input.status}-${input.fileCount}-${input.commentCount}-${new Date(input.updatedAt).getTime()}"`;
+}
+
+function getExecutionContext(c: Context) {
+ try {
+ return c.executionCtx;
+ } catch {
+ return undefined;
+ }
+}
+
export function createJobsRouter() {
const app = new Hono();
app.get('/', async (c) => {
- await runBestEffortJobMaintenance(c.env);
+ scheduleBestEffortJobMaintenance(c.env, getExecutionContext(c));
const rawQuery = c.req.query();
const query = jobsQuerySchema.parse(rawQuery);
@@ -20,14 +33,36 @@ export function createJobsRouter() {
});
app.get('/:id', async (c) => {
- await runBestEffortJobMaintenance(c.env);
+ scheduleBestEffortJobMaintenance(c.env, getExecutionContext(c));
+
+ const rawJob = await getJobForProcessing(c.env, c.req.param('id'));
+ if (!rawJob) {
+ return jsonError('Job not found.', 404);
+ }
+
+ const summary = mapJob(rawJob);
+ const etag = jobEtag(summary);
+ const lastModified = new Date(summary.updatedAt).toUTCString();
+ if (c.req.header('if-none-match') === etag) {
+ return new Response(null, {
+ status: 304,
+ headers: {
+ ETag: etag,
+ 'Last-Modified': lastModified,
+ },
+ });
+ }
const job = await getJobDetail(c.env, c.req.param('id'));
if (!job) {
return jsonError('Job not found.', 404);
}
- return c.json({ job });
+ const response = c.json({ job });
+ response.headers.set('ETag', etag);
+ response.headers.set('Last-Modified', lastModified);
+ response.headers.set('Cache-Control', 'private, no-cache');
+ return response;
});
app.post('/:id/retry', async (c) => {
diff --git a/src/server/routes/webhook.ts b/src/server/routes/webhook.ts
index 3241b73..2fbaa1a 100644
--- a/src/server/routes/webhook.ts
+++ b/src/server/routes/webhook.ts
@@ -1,4 +1,5 @@
import { Hono } from 'hono';
+import type { Context } from 'hono';
import { isSupportedGitHubWebhookEvent, type GitHubWebhookPayload } from '@shared/github';
import type { AppEnv } from '@server/env';
import { loadRepoConfig } from '@server/core/config';
@@ -8,10 +9,7 @@ import { jsonError } from '@server/core/http';
import { findExistingJobForHead, insertJob, supersedeOlderJobs } from '@server/db/jobs';
import { recordWebhookDelivery } from '@server/db/webhook-deliveries';
-export function createWebhookRouter() {
- const app = new Hono();
-
- app.post('/', async (c) => {
+export async function handleGitHubWebhook(c: Context) {
const eventName = c.req.header('x-github-event');
const deliveryId = c.req.header('x-github-delivery');
const signature = c.req.header('x-hub-signature-256');
@@ -131,7 +129,12 @@ export function createWebhookRouter() {
});
return c.json({ ok: true, message: 'queued' }, 202);
- });
+}
+
+export function createWebhookRouter() {
+ const app = new Hono();
+
+ app.post('/', handleGitHubWebhook);
return app;
}
diff --git a/src/server/services/model.ts b/src/server/services/model.ts
index 649a744..a06b604 100644
--- a/src/server/services/model.ts
+++ b/src/server/services/model.ts
@@ -18,7 +18,7 @@ const MODEL_ALIASES: Record = {
'gemma-4-31b': 'gemma-4-31b-it',
'gemma-4-26b': 'gemma-4-26b-a4b-it',
};
-type ModelProvider = 'cloudflare';
+type ModelProvider = 'cloudflare' | 'google';
export class RetryableModelError extends Error {
readonly retryable = true;
@@ -44,6 +44,10 @@ function isCloudflareModel(model: string) {
return model.startsWith('@cf/');
}
+function getModelProvider(model: string): ModelProvider {
+ return isCloudflareModel(model) ? 'cloudflare' : 'google';
+}
+
function normalizeModel(model: string) {
return normalizeModelId(MODEL_ALIASES[model] ?? model);
}
@@ -209,7 +213,8 @@ export class ModelService {
let lastTransientError: unknown;
let sawTransientFailure = false;
for (const currentModel of modelsToTry) {
- if (isCloudflareModel(currentModel) && await this.isProviderUnavailable('cloudflare')) {
+ const provider = getModelProvider(currentModel);
+ if (provider === 'cloudflare' && await this.isProviderUnavailable('cloudflare')) {
logger.warn(`Skipping Cloudflare model ${currentModel} because Cloudflare AI allocation is unavailable for job ${this.options.jobId ?? 'unknown'}`);
continue;
}
diff --git a/src/server/worker-env.d.ts b/src/server/worker-env.d.ts
index e5014d6..664f4e6 100644
--- a/src/server/worker-env.d.ts
+++ b/src/server/worker-env.d.ts
@@ -1,34 +1,35 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types ./src/server/worker-env.d.ts` (hash: c82d96387e983e77c06e253e7ea4c4b1)
-// Runtime types generated with workerd@1.20260430.1 2026-04-16 nodejs_compat
+// Generated by Wrangler by running `wrangler types ./src/server/worker-env.d.ts` (hash: 2cf95a373086a6483897fb06140fae41)
+// Runtime types generated with workerd@1.20260521.1 2026-04-16 nodejs_compat
+interface __BaseEnv_Env {
+ APP_KV: KVNamespace;
+ HYPERDRIVE: Hyperdrive;
+ REVIEW_QUEUE: Queue;
+ AI: Ai;
+ ASSETS: Fetcher;
+ APP_URL: "https://app.codra.devarshi.dev";
+ AUTH_CALLBACK_URL: "https://app.codra.devarshi.dev/auth/github/callback";
+ BOT_USERNAME: "codra-app";
+ GITHUB_APP_SLUG: "codra-app-personal";
+ DASHBOARD_ALLOWED_USERS: "devarshishimpi";
+ ENVIRONMENT: "production";
+ CF_DLQ_ID: "ed6f5472cbd146f49ce94d3004eddb0f";
+ APP_PRIVATE_KEY: string;
+ GITHUB_APP_ID: string;
+ GITHUB_APP_WEBHOOK_SECRET: string;
+ GITHUB_CLIENT_ID: string;
+ GITHUB_CLIENT_SECRET: string;
+ GEMINI_API_KEY: string;
+ CF_API_TOKEN: string;
+ CF_ACCOUNT_ID: string;
+}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./index");
}
- interface Env {
- APP_KV: KVNamespace;
- HYPERDRIVE: Hyperdrive;
- REVIEW_QUEUE: Queue;
- AI: Ai;
- ASSETS: Fetcher;
- APP_URL: "https://app.codra.devarshi.dev";
- AUTH_CALLBACK_URL: "https://app.codra.devarshi.dev/auth/github/callback";
- BOT_USERNAME: "codra-app";
- GITHUB_APP_SLUG: "codra-app-personal";
- DASHBOARD_ALLOWED_USERS: "devarshishimpi";
- ENVIRONMENT: "production";
- CF_DLQ_ID: "ed6f5472cbd146f49ce94d3004eddb0f";
- APP_PRIVATE_KEY: string;
- GITHUB_APP_ID: string;
- GITHUB_APP_WEBHOOK_SECRET: string;
- GITHUB_CLIENT_ID: string;
- GITHUB_CLIENT_SECRET: string;
- GEMINI_API_KEY: string;
- CF_API_TOKEN: string;
- CF_ACCOUNT_ID: string;
- }
+ interface Env extends __BaseEnv_Env {}
}
-interface Env extends Cloudflare.Env {}
+interface Env extends __BaseEnv_Env {}
type StringifyValues> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
@@ -941,7 +942,7 @@ interface CustomEventCustomEventInit {
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)
*/
declare class Blob {
- constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions);
+ constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions);
/**
* The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes.
*
@@ -10155,12 +10156,17 @@ interface ArtifactsTokenListResult {
/** Total number of tokens for the repository. */
total: number;
}
-/** Handle for a single repository. Returned by Artifacts.get(). */
+/**
+ * Handle for a single repository. Returned by Artifacts.get().
+ *
+ * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs.
+ */
interface ArtifactsRepo extends ArtifactsRepoInfo {
/**
* Create an access token for this repo.
* @param scope Token scope: "write" (default) or "read".
* @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000).
+ * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range.
*/
createToken(scope?: 'write' | 'read', ttl?: number): Promise;
/** List tokens for this repo (metadata only, no plaintext). */
@@ -10169,6 +10175,7 @@ interface ArtifactsRepo extends ArtifactsRepoInfo {
* Revoke a token by plaintext or ID.
* @param tokenOrId Plaintext token or token ID.
* @returns true if revoked, false if not found.
+ * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty.
*/
revokeToken(tokenOrId: string): Promise;
// ── Fork ──
@@ -10176,6 +10183,9 @@ interface ArtifactsRepo extends ArtifactsRepoInfo {
* Fork this repo to a new repo.
* @param name Target repository name.
* @param opts Optional: description, readOnly flag, defaultBranchOnly (default true).
+ * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid.
+ * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists.
+ * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running.
*/
fork(name: string, opts?: {
description?: string;
@@ -10183,13 +10193,41 @@ interface ArtifactsRepo extends ArtifactsRepoInfo {
defaultBranchOnly?: boolean;
}): Promise;
}
-/** Artifacts binding — namespace-level operations. */
+// ── Error types ──────────────────────────────────────────────────────────────
+/**
+ * Error codes returned by Artifacts binding operations.
+ *
+ * Each code maps to a numeric code available on `ArtifactsError.numericCode`.
+ */
+type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR';
+/**
+ * Error thrown by Artifacts binding operations.
+ *
+ * Uses a string `.code` discriminator following the Cloudflare platform
+ * convention (StreamError, ImagesError, etc.). The `.numericCode` matches
+ * the REST API `errors[].code` values.
+ */
+interface ArtifactsError extends Error {
+ readonly name: 'ArtifactsError';
+ /** String error code for programmatic matching. */
+ readonly code: ArtifactsErrorCode;
+ /** Numeric error code matching the REST API. */
+ readonly numericCode: number;
+}
+// ── Binding ──────────────────────────────────────────────────────────────────
+/**
+ * Artifacts binding — namespace-level operations.
+ *
+ * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs.
+ */
interface Artifacts {
/**
* Create a new repository with an initial access token.
* @param name Repository name (alphanumeric, dots, hyphens, underscores).
* @param opts Optional: readOnly flag, description, default branch name.
* @returns Repo metadata with initial token.
+ * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid.
+ * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists.
*/
create(name: string, opts?: {
readOnly?: boolean;
@@ -10200,12 +10238,23 @@ interface Artifacts {
* Get a handle to an existing repository.
* @param name Repository name.
* @returns Repo handle.
+ * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist.
+ * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing.
+ * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking.
*/
get(name: string): Promise;
/**
* Import a repository from an external git remote.
* @param params Source URL and optional branch/depth, plus target name and options.
* @returns Repo metadata with initial token.
+ * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid.
+ * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS.
+ * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository.
+ * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication.
+ * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist.
+ * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached.
+ * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits.
+ * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists.
*/
import(params: {
source: {
@@ -10233,6 +10282,7 @@ interface Artifacts {
* Delete a repository and all associated tokens.
* @param name Repository name.
* @returns true if deleted, false if not found.
+ * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid.
*/
delete(name: string): Promise;
}
@@ -11332,11 +11382,11 @@ interface SendEmail {
send(message: EmailMessage): Promise;
send(builder: {
from: string | EmailAddress;
- to: string | string[];
+ to: string | EmailAddress | (string | EmailAddress)[];
subject: string;
replyTo?: string | EmailAddress;
- cc?: string | string[];
- bcc?: string | string[];
+ cc?: string | EmailAddress | (string | EmailAddress)[];
+ bcc?: string | EmailAddress | (string | EmailAddress)[];
headers?: Record;
text?: string;
html?: string;
@@ -12101,13 +12151,13 @@ declare namespace Cloudflare {
type GlobalProp = K extends keyof GlobalProps ? GlobalProps[K] : Default;
// The type of the program's main module exports, if known. Requires `GlobalProps` to declare the
// `mainModule` property.
- type MainModule = GlobalProp<'mainModule', {}>;
+ type MainModule = GlobalProp<"mainModule", {}>;
// The type of ctx.exports, which contains loopback bindings for all top-level exports.
type Exports = {
- [K in keyof MainModule]: LoopbackForExport &
+ [K in keyof MainModule]: LoopbackForExport
// If the export is listed in `durableNamespaces`, then it is also a
// DurableObjectNamespace.
- (K extends GlobalProp<'durableNamespaces', never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace : DurableObjectNamespace : DurableObjectNamespace : {});
+ & (K extends GlobalProp<"durableNamespaces", never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace : DurableObjectNamespace : DurableObjectNamespace : {});
};
}
declare namespace CloudflareWorkersModule {
@@ -12178,24 +12228,15 @@ declare namespace CloudflareWorkersModule {
attempt: number;
config: WorkflowStepConfig;
};
- export interface RollbackContext {
- error: Error;
- output: NonNullable | undefined;
- stepName: string;
- }
- export interface StepPromise extends Promise {
- rollback(fn: (ctx: RollbackContext) => Promise): StepPromise;
- rollback(config: WorkflowStepConfig, fn: (ctx: RollbackContext) => Promise): StepPromise;
- }
export abstract class WorkflowStep {
- do>(name: string, callback: (ctx: WorkflowStepContext) => Promise): StepPromise;
- do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise): StepPromise;
+ do>(name: string, callback: (ctx: WorkflowStepContext) => Promise): Promise;
+ do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise): Promise;
sleep: (name: string, duration: WorkflowSleepDuration) => Promise;
sleepUntil: (name: string, timestamp: Date | number) => Promise;
waitForEvent>(name: string, options: {
type: string;
timeout?: WorkflowTimeoutDuration | number;
- }): StepPromise>;
+ }): Promise>;
}
export type WorkflowInstanceStatus = 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown';
export abstract class WorkflowEntrypoint | unknown = unknown> implements Rpc.WorkflowEntrypointBranded {
@@ -13179,6 +13220,9 @@ declare namespace TailStream {
// 1. This is an Onset event
// 2. We are not inheriting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation)
readonly spanId?: string;
+ // W3C trace flags from an upstream traceparent. Absent when no upstream
+ // sampling decision was made.
+ readonly traceFlags?: number;
}
interface TailEvent {
// invocation id of the currently invoked worker stage.
@@ -13553,6 +13597,27 @@ interface WorkflowError {
code?: number;
message: string;
}
+interface WorkflowInstanceRestartOptions {
+ /**
+ * Restart from a specific step. If omitted, the instance restarts from the beginning.
+ * The step must exist in the instance's execution history.
+ */
+ from?: {
+ /**
+ * The step name as defined in your workflow code.
+ */
+ name: string;
+ /**
+ * 1-indexed occurrence of this step name. Use when the same step name appears multiple times (e.g. in a loop).
+ * @default 1
+ */
+ count?: number;
+ /**
+ * Step type filter. Use when different step types share the same name.
+ */
+ type?: 'do' | 'sleep' | 'waitForEvent';
+ };
+}
declare abstract class WorkflowInstance {
public id: string;
/**
@@ -13568,9 +13633,11 @@ declare abstract class WorkflowInstance {
*/
public terminate(): Promise;
/**
- * Restart the instance.
+ * Restart the instance. Optionally restart from a specific step, preserving
+ * cached results for all steps before it.
+ * @param options Options for the restart, including an optional step to restart from.
*/
- public restart(): Promise;
+ public restart(options?: WorkflowInstanceRestartOptions): Promise;
/**
* Returns the current status of the instance.
*/
diff --git a/src/shared/schema.ts b/src/shared/schema.ts
index 63bc503..e9cf80d 100644
--- a/src/shared/schema.ts
+++ b/src/shared/schema.ts
@@ -184,6 +184,8 @@ export const jobSummarySchema = z.object({
totalInputTokens: z.number().int(),
totalOutputTokens: z.number().int(),
createdAt: dateStringSchema,
+ updatedAt: dateStringSchema,
+ nextRetryAt: dateStringSchema.nullable().optional(),
startedAt: dateStringSchema.nullable(),
finishedAt: dateStringSchema.nullable(),
errorMessage: z.string().nullable(),
diff --git a/test/model-service.spec.ts b/test/model-service.spec.ts
index dc5aed9..72630e9 100644
--- a/test/model-service.spec.ts
+++ b/test/model-service.spec.ts
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { isRetryableModelError, ModelService } from '@server/services/model';
import { reviewWithCloudflare } from '@server/models/cloudflare';
+import { reviewWithGoogle } from '@server/models/google';
import { createTestEnv } from './helpers';
import { defaultRepoConfig } from '@shared/schema';
@@ -50,7 +51,7 @@ describe('ModelService', () => {
});
});
- it('rejects Cloudflare reasoning-only responses instead of trying to parse the response envelope', async () => {
+ it('turns Cloudflare reasoning-only responses into inconclusive review JSON', async () => {
const env = createTestEnv({
AI: {
async run() {
@@ -70,15 +71,114 @@ describe('ModelService', () => {
} as any,
});
- await expect(
- reviewWithCloudflare(env, '@cf/moonshotai/kimi-k2.6', {
- systemPrompt: 'system',
- userPrompt: 'user',
- }),
- ).rejects.toThrow('returned no review content');
+ const response = await reviewWithCloudflare(env, '@cf/moonshotai/kimi-k2.6', {
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ });
+ const parsed = JSON.parse(response.rawText);
+
+ expect(parsed.findings).toEqual([]);
+ expect(parsed.overall_correctness).toBe('patch is incorrect');
+ expect(parsed.overall_explanation).toContain('inconclusive');
+ });
+
+ it('does not parse Cloudflare reasoning as review JSON when final content is missing', async () => {
+ const env = createTestEnv({
+ AI: {
+ async run() {
+ return {
+ choices: [
+ {
+ message: {
+ content: null,
+ reasoning: 'Reasoning mentioned an object like {"foo":"bar"} but never produced final JSON.',
+ },
+ finish_reason: 'length',
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 8192 },
+ };
+ },
+ } as any,
+ });
+
+ const response = await reviewWithCloudflare(env, '@cf/zai-org/glm-4.7-flash', {
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ });
+ const parsed = JSON.parse(response.rawText);
+
+ expect(parsed.findings).toEqual([]);
+ expect(parsed.overall_explanation).toContain('reasoning-only response');
+ });
+
+ it('asks Cloudflare chat models for strict review JSON', async () => {
+ let inputs: any;
+ const env = createTestEnv({
+ AI: {
+ async run(_model: string, request: any) {
+ inputs = request;
+ return {
+ choices: [
+ {
+ message: {
+ content: '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}',
+ },
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
+ };
+ },
+ } as any,
+ });
+
+ await reviewWithCloudflare(env, '@cf/zai-org/glm-4.7-flash', {
+ systemPrompt: 'system',
+ userPrompt: 'user',
+ });
+
+ expect(inputs.response_format).toMatchObject({
+ type: 'json_schema',
+ json_schema: {
+ name: 'codra_file_review',
+ strict: true,
+ },
+ });
+ expect(inputs.messages[0].content).toContain('Return only the JSON object');
+ expect(inputs.max_completion_tokens).toBe(8192);
+ expect(inputs.chat_template_kwargs).toBeUndefined();
+ expect(inputs.reasoning_effort).toBeUndefined();
});
- it('retries the same Cloudflare model once before failing it', async () => {
+ it('retries Google once for transient 524 edge timeouts', async () => {
+ const fetchMock = vi.spyOn(globalThis, 'fetch')
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({ error: { code: 524, message: 'A timeout occurred.' } }),
+ { status: 524, headers: { 'content-type': 'application/json' } },
+ ),
+ )
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ candidates: [{ content: { parts: [{ text: '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}' }] } }],
+ usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1 },
+ }),
+ { status: 200, headers: { 'content-type': 'application/json' } },
+ ),
+ );
+
+ const response = await reviewWithGoogle(
+ { GEMINI_API_KEY: 'test-key' },
+ 'gemma-4-31b-it',
+ { systemPrompt: 'system', userPrompt: 'user' },
+ );
+
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(response.rawText).toContain('"findings"');
+ });
+
+ it('does not spend an extra queue slice retrying the same Cloudflare model inline', async () => {
let attempts = 0;
const env = createTestEnv({
AI: {
@@ -95,7 +195,80 @@ describe('ModelService', () => {
userPrompt: 'user',
}),
).rejects.toThrow('temporary provider error');
- expect(attempts).toBe(2);
+ expect(attempts).toBe(1);
+ });
+
+ it('tries the smaller Google fallback after the primary Google model fails', async () => {
+ let cloudflareCalls = 0;
+ const fetchMock = vi.spyOn(globalThis, 'fetch')
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ error: {
+ code: 500,
+ message: 'Internal error encountered.',
+ status: 'INTERNAL',
+ },
+ }),
+ { status: 500, headers: { 'content-type': 'application/json' } },
+ ),
+ )
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ candidates: [{ content: { parts: [{ text: '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}' }] } }],
+ usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1 },
+ }),
+ { status: 200, headers: { 'content-type': 'application/json' } },
+ ),
+ );
+ const env = createTestEnv({
+ AI: {
+ async run() {
+ cloudflareCalls++;
+ return {
+ response: JSON.stringify({
+ findings: [],
+ overall_correctness: 'patch is correct',
+ overall_explanation: 'ok',
+ overall_confidence_score: 0.9,
+ }),
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
+ };
+ },
+ } as any,
+ GEMINI_API_KEY: 'test-key',
+ });
+ const service = new ModelService(env);
+
+ const response = await service.reviewFile({
+ file: {
+ path: 'src/app.ts',
+ lineCount: 1,
+ hunks: [],
+ isDeleted: false,
+ isBinary: false,
+ isNew: false,
+ previousPath: null,
+ },
+ prTitle: 'Test',
+ prDescription: null,
+ config: {
+ ...defaultRepoConfig,
+ model: {
+ main: 'gemma-4-31b-it',
+ fallbacks: ['gemma-4-26b-a4b-it', '@cf/zai-org/glm-4.7-flash'],
+ size_overrides: [],
+ },
+ },
+ totalLineCount: 1,
+ });
+
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(String(fetchMock.mock.calls[0][0])).toContain('/models/gemma-4-31b-it:generateContent');
+ expect(String(fetchMock.mock.calls[1][0])).toContain('/models/gemma-4-26b-a4b-it:generateContent');
+ expect(cloudflareCalls).toBe(0);
+ expect(response.modelUsed).toBe('gemma-4-26b-a4b-it');
});
it('marks exhausted transient provider failures as retryable for the queue', async () => {
@@ -260,7 +433,7 @@ describe('ModelService', () => {
const userPrompt = requestBody.contents[0].parts[0].text as string;
expect(fetchMock).toHaveBeenCalledOnce();
- expect(requestBody.generationConfig.maxOutputTokens).toBe(3072);
+ expect(requestBody.generationConfig.maxOutputTokens).toBe(4096);
expect(userPrompt).toContain('[NOTE: This diff has been truncated from 900 lines to 800 lines for brevity.]');
expect(userPrompt).toContain('const value799 = 799;');
expect(userPrompt).not.toContain('const value800 = 800;');
diff --git a/test/review-flow.spec.ts b/test/review-flow.spec.ts
index 7da7c31..dece307 100644
--- a/test/review-flow.spec.ts
+++ b/test/review-flow.spec.ts
@@ -63,7 +63,10 @@ vi.mock('@server/services/model', () => {
async generateSummary() {
return {
modelUsed: 'sum-model',
+ provider: 'google',
rawText: '{"summary": "test"}',
+ inputTokens: 3,
+ outputTokens: 2,
};
}
}
@@ -399,8 +402,83 @@ dbDescribe('Review Flow Lifecycle', () => {
reviewSpy.mockRestore();
}, REVIEW_FLOW_TIMEOUT_MS);
+ it('reviews files in a chunk concurrently', async () => {
+ const { GitHubService } = await import('@server/services/github');
+ const { ModelService } = await import('@server/services/model');
+ const repo = `test-repo-${Date.now()}-concurrent`;
+ const headSha = sha('8');
+ const baseSha = sha('9');
+ const getDiffSpy = vi.spyOn(GitHubService.prototype, 'getPullRequestDiff').mockResolvedValue(
+ generateMockDiff([
+ { path: 'src/one.ts', content: 'console.log(1);' },
+ { path: 'src/two.ts', content: 'console.log(2);' },
+ { path: 'src/three.ts', content: 'console.log(3);' },
+ ]),
+ );
+ let active = 0;
+ let maxActive = 0;
+ const reviewSpy = vi.spyOn(ModelService.prototype as any, 'reviewFile').mockImplementation(async (params: any) => {
+ active += 1;
+ maxActive = Math.max(maxActive, active);
+ await new Promise((resolve) => setTimeout(resolve, 25));
+ active -= 1;
+ return {
+ parsed: {
+ comments: [],
+ verdict: 'approve',
+ fileSummary: `Reviewed ${params.file.path}`,
+ overallCorrectness: 'no issues',
+ confidenceScore: 0.9,
+ },
+ modelUsed: 'test-model',
+ provider: 'test-provider',
+ inputTokens: 10,
+ outputTokens: 5,
+ rawText: '{}',
+ userPrompt: '',
+ };
+ });
+
+ const job = await insertJob(env, {
+ installationId: '123',
+ owner: 'test-owner',
+ repo,
+ prNumber: 6,
+ prTitle: 'Concurrent Test',
+ prAuthor: 'author',
+ commitSha: headSha,
+ baseSha,
+ trigger: 'auto',
+ headRef: 'feature',
+ baseRef: 'main',
+ configSnapshot: defaultRepoConfig,
+ });
+ await updateJobFileCount(env, job.id, 3);
+ await updateJobStep(env, job.id, 'Preparation', { status: 'done' });
+
+ await runWithDb(env, async () => {
+ (env.REVIEW_QUEUE as any).sent.length = 0;
+ const result = await runReviewJob(env, {
+ jobId: job.id,
+ deliveryId: 'delivery-concurrent',
+ phase: 'review',
+ });
+
+ expect(result).toEqual({ action: 'ack' });
+ expect(maxActive).toBe(3);
+ expect((env.REVIEW_QUEUE as any).sent[0]).toMatchObject({ jobId: job.id, phase: 'finalize' });
+ });
+
+ const reviews = await getFileReviewsForJobs(env, [job.id]);
+ expect(reviews.filter((review) => review.file_status === 'done')).toHaveLength(3);
+
+ reviewSpy.mockRestore();
+ getDiffSpy.mockRestore();
+ }, REVIEW_FLOW_TIMEOUT_MS);
+
it('marks completed jobs with skipped files as partial reviews', async () => {
const { GitHubService } = await import('@server/services/github');
+ const { ModelService } = await import('@server/services/model');
const repo = `test-repo-${Date.now()}-partial`;
const headSha = sha('e');
const baseSha = sha('f');
@@ -425,6 +503,7 @@ dbDescribe('Review Flow Lifecycle', () => {
baseRef: 'main',
configSnapshot: defaultRepoConfig,
});
+ const summarySpy = vi.spyOn(ModelService.prototype as any, 'generateSummary');
await updateJobFileCount(env, job.id, 2);
await updateJobStep(env, job.id, 'Preparation', { status: 'done' });
await updateJobStep(env, job.id, 'Reviewing Files', { status: 'done' });
@@ -474,6 +553,10 @@ dbDescribe('Review Flow Lifecycle', () => {
const finalJob = await getJobForProcessing(env, job.id);
expect(finalJob?.status).toBe('done');
expect(finalJob?.error_msg).toContain('Partial review: 1 of 2 files');
+ expect(finalJob?.summary_markdown).toMatch(/^### Codra Review/);
+ expect(finalJob?.summary_model).toBeNull();
+ expect(summarySpy).not.toHaveBeenCalled();
+ summarySpy.mockRestore();
getDiffSpy.mockRestore();
}, REVIEW_FLOW_TIMEOUT_MS);
});
diff --git a/test/webhook-handling.spec.ts b/test/webhook-handling.spec.ts
index 66eb5ff..965e603 100644
--- a/test/webhook-handling.spec.ts
+++ b/test/webhook-handling.spec.ts
@@ -119,6 +119,38 @@ describe('Webhook Handling Suite', () => {
expect(queue.sent[0].payload).toBeUndefined();
});
+ it('rejects GitHub webhooks posted to the site root', async () => {
+ const repoName = `root-repo-${Date.now()}`;
+ const rawPayload = createMockPRWebhook({
+ action: 'opened',
+ repository: { name: repoName, owner: { login: 'test-owner' } }
+ });
+ rawPayload.pull_request.head.sha = 'c'.repeat(40);
+ rawPayload.pull_request.base.sha = 'd'.repeat(40);
+ const body = JSON.stringify(rawPayload);
+ const signature = await signPayload(env.GITHUB_APP_WEBHOOK_SECRET, body);
+
+ const response = await app.request(
+ 'http://codra.test/',
+ {
+ method: 'POST',
+ headers: {
+ 'x-github-event': 'pull_request',
+ 'x-github-delivery': `root-delivery-${Date.now()}`,
+ 'x-hub-signature-256': signature,
+ 'content-type': 'application/json',
+ },
+ body,
+ },
+ env,
+ );
+
+ expect(response.status).toBe(404);
+
+ const queue = env.REVIEW_QUEUE as any;
+ expect(queue.sent).toHaveLength(0);
+ });
+
it('acknowledges unsupported GitHub events without queueing review work', async () => {
const rawPayload = createMockPRWebhook({
action: 'opened',