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}
))} +
-
Commit
+
Commit
- - {job.commitSha.slice(0, 7)} - - + {job.commitSha ? ( + + {shortCommitSha} + + + ) : ( + {shortCommitSha} + )}
+ {job.reviewId && (
-
Review
+
Review
- View on GitHub + GitHub
)} + {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.name} -
- - {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',