diff --git a/admin-service/package-lock.json b/admin-service/package-lock.json index 2e8464cf..957dfe94 100644 --- a/admin-service/package-lock.json +++ b/admin-service/package-lock.json @@ -10,32 +10,32 @@ "dependencies": { "@gsap/react": "^2.1.2", "@hookform/resolvers": "^5.2.2", - "@tanstack/react-query": "^5.101.0", + "@tanstack/react-query": "^5.101.2", "@vercel/analytics": "^2.0.1", - "antd": "^6.4.4", - "axios": "^1.18.0", + "antd": "^6.5.0", + "axios": "^1.18.1", "dayjs": "^1.11.21", "dotenv": "^17.4.2", "gsap": "^3.15.0", - "lucide-react": "^1.18.0", + "lucide-react": "^1.22.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-hook-form": "^7.79.0", - "react-router-dom": "^7.17.0", - "recharts": "^3.8.1", - "three": "^0.184.0", - "vercel": "^54.14.0", + "react-hook-form": "^7.80.0", + "react-router-dom": "^7.18.0", + "recharts": "^3.9.0", + "three": "^0.185.0", + "vercel": "^54.18.2", "zod": "^4.4.3", "zustand": "^5.0.14" }, "devDependencies": { - "@types/node": "^25.9.3", + "@types/node": "^25.9.4", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@types/three": "^0.184.1", - "@vitejs/plugin-react": "^6.0.1", + "@types/three": "^0.185.0", + "@vitejs/plugin-react": "^6.0.3", "typescript": "^6.0.3", - "vite": "^8.0.16", + "vite": "^8.1.0", "vitest": "^4.1.8" } }, @@ -92,13 +92,13 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz", - "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", + "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", - "@ant-design/icons-svg": "^4.4.2", + "@ant-design/icons-svg": "^4.5.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, @@ -111,9 +111,9 @@ } }, "node_modules/@ant-design/icons-svg": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz", + "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==", "license": "MIT" }, "node_modules/@ant-design/react-slick": { @@ -203,9 +203,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", "license": "MIT", "optional": true, "peer": true, @@ -215,9 +215,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "license": "MIT", "optional": true, "peer": true, @@ -231,7 +231,6 @@ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -991,13 +990,13 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -1409,12 +1408,12 @@ } }, "node_modules/@rc-component/cascader": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.16.1.tgz", - "integrity": "sha512-wxLopwM+EBed0zNNGdnGE4coYoqcO+XD42fHgn+pDvO+XzhNFbdgSlSNXdKocIYqccvqgWvoxDPNb0OVRdi59A==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.17.0.tgz", + "integrity": "sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==", "license": "MIT", "dependencies": { - "@rc-component/select": "~1.7.1", + "@rc-component/select": "~1.8.0", "@rc-component/tree": "~1.3.2", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" @@ -1483,12 +1482,12 @@ } }, "node_modules/@rc-component/dialog": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.9.0.tgz", - "integrity": "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.10.0.tgz", + "integrity": "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==", "license": "MIT", "dependencies": { - "@rc-component/motion": "^1.1.3", + "@rc-component/motion": "^1.3.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" @@ -1594,13 +1593,13 @@ } }, "node_modules/@rc-component/mentions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.9.0.tgz", - "integrity": "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.10.0.tgz", + "integrity": "sha512-CI1njYUVY0NjHtLhNoVmXlJyy568Sfep9Wsak6vmGjtT6uazx98djGYlCXz2xkHhEm73g91Y3MTvzUyE5avI7w==", "license": "MIT", "dependencies": { "@rc-component/input": "~1.3.0", - "@rc-component/menu": "~1.3.0", + "@rc-component/menu": "~1.4.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -1611,9 +1610,9 @@ } }, "node_modules/@rc-component/menu": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.1.tgz", - "integrity": "sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.4.1.tgz", + "integrity": "sha512-3GsVRoQ4cnF/AoIQ4P+Z1haBfgfBPQfLT1RJY3Nu4DzOnheTslfCiGSPj7bv/cLj5sW5pHqN25dDXGP3JELAlQ==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.1.4", @@ -1704,9 +1703,9 @@ } }, "node_modules/@rc-component/pagination": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.3.0.tgz", - "integrity": "sha512-12ahTY+HPITg1L2bjWKXUqBJe/oOnpA2QsChdCjthqLVf/e19StiCsv8OLKpWoHbc+8PFEkNjRqRqrLoRBHjFw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.4.0.tgz", + "integrity": "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.11.1", @@ -1718,15 +1717,15 @@ } }, "node_modules/@rc-component/picker": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.10.0.tgz", - "integrity": "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.11.0.tgz", + "integrity": "sha512-6qXGKtoJvO8sUd17m5cyNEbEJub0zflCHnaZTBBmj63DPRZYc0WEHN8rp6hFSl+yMCJS/dJY5G+1fQ8bLCuD7A==", "license": "MIT", "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "engines": { @@ -1849,9 +1848,9 @@ } }, "node_modules/@rc-component/select": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.7.1.tgz", - "integrity": "sha512-GZ1cMJk2xQh0VHyOQjjG8drYL4iu24NcbkXioUcReQOCUr+ub/3fmRonZe6cRPEZhWMbJdeHsqnEltogDaZ5Tg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.8.2.tgz", + "integrity": "sha512-HQ9zuYqjfZTlcEMWlU1GAPBajd2OHIMVHyjZSGVTCVARwkfCgvXZMTEn0cduy3L+ejAKkaZluOQvxovZoaJaQw==", "license": "MIT", "dependencies": { "@rc-component/overflow": "^1.0.0", @@ -1869,9 +1868,9 @@ } }, "node_modules/@rc-component/slider": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", - "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.1.1.tgz", + "integrity": "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.3.0", @@ -1937,13 +1936,13 @@ } }, "node_modules/@rc-component/tabs": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.1.tgz", - "integrity": "sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.11.0.tgz", + "integrity": "sha512-hA/drZYOVa/MMIb4M2fWf3yaTyTG4qVuIABmghvEhyfw2nBob5VTH69lMCDjSVKmgODjO6nWlCV+gVn3xBrj5Q==", "license": "MIT", "dependencies": { "@rc-component/dropdown": "~1.0.0", - "@rc-component/menu": "~1.3.0", + "@rc-component/menu": "~1.4.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.11.1", @@ -2011,14 +2010,14 @@ } }, "node_modules/@rc-component/tree-select": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.10.0.tgz", - "integrity": "sha512-E1U4pn2LAbXEhLJdzIzid7WYbIuFbkTIctuFoeC6weppf8UbPR3+YYB6/ay0c0ksand4gXMRQpa1Z60Auo7VJA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.11.0.tgz", + "integrity": "sha512-EhS0X0wtUhBfK4S5TlpSY3MR9ndPMGgujtt1PJW3Ej+ToAlnS/6ohYURtCoXBYGqazUwHmgQGVUDsfpVwhWPkg==", "license": "MIT", "dependencies": { - "@rc-component/select": "~1.7.0", - "@rc-component/tree": "~1.3.0", - "@rc-component/util": "^1.4.0", + "@rc-component/select": "~1.8.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -2125,16 +2124,6 @@ } } }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/@renovatebot/pep440": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-4.2.1.tgz", @@ -2264,9 +2253,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], @@ -2284,9 +2273,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], @@ -2406,9 +2395,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" }, @@ -2453,9 +2442,9 @@ "license": "MIT" }, "node_modules/@tanstack/query-core": { - "version": "5.101.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", - "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", "license": "MIT", "funding": { "type": "github", @@ -2463,12 +2452,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.101.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", - "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.101.0" + "@tanstack/query-core": "5.101.2" }, "funding": { "type": "github", @@ -2525,9 +2514,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { @@ -2628,9 +2617,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", - "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", "dev": true, "license": "MIT", "dependencies": { @@ -2665,9 +2654,9 @@ "license": "MIT" }, "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", + "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -2734,13 +2723,14 @@ } }, "node_modules/@vercel/backends": { - "version": "0.8.14", - "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.8.14.tgz", - "integrity": "sha512-jgepdZh7E4ameCUSt/b28ImFd9Bz1VX71lKwD/ThtZ7nGIwi5SJ9hwyURvH60c4rNW6nKxMD/iSOlcgQOfrK3A==", + "version": "0.8.21", + "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.8.21.tgz", + "integrity": "sha512-c0mKXgfxMyb44+kMGbNoQJomf37QAfd6yL/2imhAnAChR1Ddkxk6uwbrOC89LcG+QmN2ZoEFUSJdMInSfreztw==", "license": "Apache-2.0", "dependencies": { - "@vercel/build-utils": "13.30.0", + "@vercel/build-utils": "13.32.2", "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.0", "execa": "3.2.0", "fs-extra": "11.1.0", "get-port": "5.1.1", @@ -2748,7 +2738,8 @@ "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", - "srvx": "0.8.9", + "srvx": "0.11.16", + "ts-morph": "12.0.0", "tsx": "4.21.0", "zod": "3.22.4" } @@ -2779,9 +2770,9 @@ } }, "node_modules/@vercel/build-utils": { - "version": "13.30.0", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.30.0.tgz", - "integrity": "sha512-fLIa8cpELsSoWbcxshaqegwlTCfKnbgsDmA6uIb+oA797xvSmYkPu5a781aRYCRdghgyOZF7NCSVgtZjHjunnQ==", + "version": "13.32.2", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.32.2.tgz", + "integrity": "sha512-XgATgMjt2NHF6HHo1wii6f43qlWjaFx3o+9L7aOUPLQgqwgYp2BGwuuBjg8U6sNgut1QsmFBMvNqTAUBvack9Q==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1", @@ -2790,12 +2781,12 @@ } }, "node_modules/@vercel/cervel": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.22.tgz", - "integrity": "sha512-YbL2AosH3FaBLJZu4GYdVMkFv+5cbWwwSrRBTGN52odynQUudTUQfbc7rCzPyNQLdk2QC4w2IDYRD+nqgojhVg==", + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.29.tgz", + "integrity": "sha512-tjwW6bj1g9qwOO0y/3WTGiarvyWmsWOtLkPB0esFmnscMZCxRqzNh0WJqrbbWxMdC5A2LXzDDHuZ4sXpRg4Tqw==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.8.14" + "@vercel/backends": "0.8.21" }, "bin": { "cervel": "bin/cervel.mjs" @@ -2850,6 +2841,12 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@vercel/container": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@vercel/container/-/container-0.0.4.tgz", + "integrity": "sha512-lbUnpg2Iawoi0oDlFE6Se43FNUd2B2nMnCXD9Af7NsysMENFaNu2ajzgsocTxt+O6djUgW9IdBnIRHgnHs+vZQ==", + "license": "Apache-2.0" + }, "node_modules/@vercel/detect-agent": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@vercel/detect-agent/-/detect-agent-1.2.3.tgz", @@ -2860,12 +2857,12 @@ } }, "node_modules/@vercel/elysia": { - "version": "0.1.93", - "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.93.tgz", - "integrity": "sha512-36yzZJVU9o7/CcH1TbqeGTRwBePDaCEzv1CYfS183HdLXzsD+pOjOkTqZOBJu7h9aHpJFCCau55Ed0xOQC2jhA==", + "version": "0.1.98", + "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.98.tgz", + "integrity": "sha512-VJMPfvslPFP5lg7oNN+BZEXHdaejmI25OEyco5OvZWpR80MYpTPil+2+hbkoR6lN3kVUC9/xahtgsdfouOvNbA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0" } }, @@ -2876,14 +2873,14 @@ "license": "Apache-2.0" }, "node_modules/@vercel/express": { - "version": "0.1.105", - "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.105.tgz", - "integrity": "sha512-F+QRQxWWKdeztW4MKPkzXl/Ajc8j1qOffCp1tt8+YPaNiqZtxpn7FjIYRXHbfR1aepfaEkDRDD7o2qxmmaUU5w==", + "version": "0.1.112", + "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.112.tgz", + "integrity": "sha512-Xpw1/+G/NpWYzBh3HqT+npYKo+yrLhSKC+hekBYCoddfDjSTevF1hQSbdEo7oHbZbgQo7h5Rb2vWVQ404zSmkA==", "license": "Apache-2.0", "dependencies": { - "@vercel/cervel": "0.1.22", + "@vercel/cervel": "0.1.29", "@vercel/nft": "1.10.0", - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -2901,12 +2898,12 @@ } }, "node_modules/@vercel/fastify": { - "version": "0.1.96", - "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.96.tgz", - "integrity": "sha512-4Uq6GVmi0ksQxIHp4FDUhgoz47uacxZm42XT04SK5gzX5v5UmZXWZPprZjkwwk/2gmP/rF2qKqJ0nu6OsI+yDw==", + "version": "0.1.101", + "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.101.tgz", + "integrity": "sha512-+oUCBpSs8ANQmdQB6CTCWTfdxKutCzKEMkMtUJVLVnO0eaiONADkZeaFjjZwMl4Cg6z49ifhJ3QVJD1IWwIwsA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0" } }, @@ -3020,42 +3017,42 @@ } }, "node_modules/@vercel/gatsby-plugin-vercel-builder": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.2.19.tgz", - "integrity": "sha512-dA1ZWZHruRjFbLToqjcPHUyh5J7/Cl3qGFDge87ghAInuy/aXfalApzFbw61OIopGx0SSsIYi/8kbpJ+SbQ6ZQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.2.24.tgz", + "integrity": "sha512-+rqPToquzHOnSsist6FylJY++Z7ScRvl87gJshGhYYXJSCMki5zquX+D8wliOb5H0SW3KGwmTNQ10gUJt52c8w==", "license": "Apache-2.0", "dependencies": { "@sinclair/typebox": "0.25.24", - "@vercel/build-utils": "13.30.0", + "@vercel/build-utils": "13.32.2", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "node_modules/@vercel/go": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.9.0.tgz", - "integrity": "sha512-vGbEwckvtr9UXKDz3qEHUE7VHJGG/9VQLuScgHytksfZyX8dzvNPgEuBKBbhPaMwua8pUCZKQqNxFM3Dul+bLA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.10.2.tgz", + "integrity": "sha512-ZJqx1GzD1qqMkI92jEHp04zCwbZ7tNwcgI58e7t8HiYvILwCZ4Rvq0hfjTjsk+0Zn8A369qs4eAHvCYu38uB5A==", "license": "Apache-2.0" }, "node_modules/@vercel/h3": { - "version": "0.1.102", - "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.102.tgz", - "integrity": "sha512-UnHFLBemE2UfvQ+w2z2zxGcvyiShQ8eMdk3ag2sV71wbyatnXwmio8HOy//B2ZIEQNEid5gW2HjWOXLtrR+E2Q==", + "version": "0.1.107", + "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.107.tgz", + "integrity": "sha512-Vqo5AvE5ku3MIWAi56KlYbLIg/PfX7Xxp94xbNa7J7dbREbO8PZWD0QLKvOw1uGqyoa5YqNZZ3RdtCfDq9Z6oA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/hono": { - "version": "0.2.96", - "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.96.tgz", - "integrity": "sha512-h4G0GSo5omhXS1iqimIcekcW7THEAnUGd6eioDNnSqwS3L0CGEvvTPUekJDyJcybsqnKslH1XTZA40f4b9MPHg==", + "version": "0.2.101", + "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.101.tgz", + "integrity": "sha512-sm24D5OUJEo9cuvPHeVgouuc0zhZdIhX28BdtvIek5b7FGmvOE8VExOhteWU5YufhJR0M1y1zbbVC2ULr/arHw==", "license": "Apache-2.0", "dependencies": { "@vercel/nft": "1.10.0", - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -3083,29 +3080,29 @@ } }, "node_modules/@vercel/koa": { - "version": "0.1.76", - "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.76.tgz", - "integrity": "sha512-6MuBn5RM2nHKfHKeBqbs3aaFJlBFDhOZgpC1ubLQG2B/EvnAfljB7FPIKCl1SwANAHisX7vQACinMedCbFdi+Q==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.81.tgz", + "integrity": "sha512-sAavmnlTM9LhYPWAgIvWuKgpeozk/tB4ZPLe/Fl7qE6EJiN3sCjtaSatXhla5w1Zxy0veBI5u0LCcjref4Oxag==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/nestjs": { - "version": "0.2.97", - "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.97.tgz", - "integrity": "sha512-KM9uBmC+a10EPJgtm0B/vfGsTaUEzxKkihTyaQ4PhSUQHJOABTv6TBpGKtwIM5P1vMLKUg9R0A2SUvkKdaALvA==", + "version": "0.2.102", + "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.102.tgz", + "integrity": "sha512-CMDoq4dFBI1rLmAuya7UPXfcg7h0y1xdNVCeFhzlZOfp7EF1+PFkn3dVF8tIX46yiUFCz94weiLHSAoh+mRD4g==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.17", + "@vercel/node": "5.8.22", "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/next": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.19.0.tgz", - "integrity": "sha512-9B7juRNydQ9MU7xT8OE254ymhVrNdcOKlXrZ4anem8YEa34KvXjLczQhZEPdvTz9JaCXpYiEEGmd/D3t2oJ+Uw==", + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.20.2.tgz", + "integrity": "sha512-mroXgsjkmI6i5m1dMRmFNHO8wExhhgOzFRHT0pg195uOVyuNU2JxBCtU47FowbcnLbrAmIfStWkPSHLwPRh6FA==", "license": "Apache-2.0", "dependencies": { "@vercel/nft": "1.10.0" @@ -3138,16 +3135,16 @@ } }, "node_modules/@vercel/node": { - "version": "5.8.17", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.17.tgz", - "integrity": "sha512-n2DVzblqS43LTs4BV1iLfx8tjjrTegcSsyDgRzn70h6tQePYWjl+h0bU3X1stpITZtaYJSZYwVevuX1BJNZHHg==", + "version": "5.8.22", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.22.tgz", + "integrity": "sha512-WfciIhDVh9RwFmvrWp7FAdYCBPV94bZvIo4JbaHqZbvBJ2Bmnv/Pgj1fTjjLKfnKTbLJy+8HN1FXB8KYDnwGZQ==", "license": "Apache-2.0", "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", - "@vercel/build-utils": "13.30.0", + "@vercel/build-utils": "13.32.2", "@vercel/error-utils": "2.2.0", "@vercel/nft": "1.10.0", "@vercel/static-config": "3.4.0", @@ -3264,9 +3261,9 @@ "license": "MIT" }, "node_modules/@vercel/python": { - "version": "6.45.1", - "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.45.1.tgz", - "integrity": "sha512-2ay12lt3Cu6xS6qiaKNNwALRKxI1AE3rtHDfFOP+BU4qJ2U5k7JmPxlDuU+CXKY+chQClQM2orgThJWeMoeQkw==", + "version": "6.47.3", + "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.47.3.tgz", + "integrity": "sha512-h8j5Fln+GYPkWCIXZdhUq1ibFQofjeiV6LMgeY31NEFn/lolz/UxDwqSZswM9H1aP/5lH54VCe/iFE4dMlSWiQ==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1" @@ -3352,18 +3349,19 @@ "license": "MIT" }, "node_modules/@vercel/ruby": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vercel/ruby/-/ruby-2.4.0.tgz", - "integrity": "sha512-YI7Amyf09hHZWOqDHgJO92XcKh6Pye8rrmJFhlP6euG3o6QjoZzJj7Z2WzjSrDRGMewzEK4uz2+CbNpNS7gLog==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@vercel/ruby/-/ruby-2.5.1.tgz", + "integrity": "sha512-e23M7pfORQyyxSUmeQ8PqEnyLJ4swhtNrnyctU5E02UCaUYFm5YAdNfdPYRP4It874H4Wyvm+LV8u7CPBrfcPA==", "license": "Apache-2.0" }, "node_modules/@vercel/rust": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vercel/rust/-/rust-1.3.0.tgz", - "integrity": "sha512-z1X0z1NM+ISunm/scgRpuENNwcKlh7DjXn0QvKE0n10DcaYqxkBQVK8iRA9X05xSK9AzPlnB9DHdGKiXZO5buw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vercel/rust/-/rust-1.4.0.tgz", + "integrity": "sha512-/zRJtaf3pvdxZ7+tShmIo8StdbkCSBDcY7Gy7LOwNTIxm6GU9vkS+BlCwJHkOu6qNkbcSRH6ZQIeo4vNmqzKdQ==", "license": "Apache-2.0", "dependencies": { "execa": "5", + "get-port": "5.1.1", "smol-toml": "1.5.2" } }, @@ -3470,13 +3468,13 @@ } }, "node_modules/@vercel/static-build": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.10.3.tgz", - "integrity": "sha512-+IipWidX6mL1Zck8Khw4wSXHPSgDfkapvcuWA9wSCmhnk2GL2Iuu+6vab+g8UPahv+QDUYm8Tphhadg6dICZnA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.11.4.tgz", + "integrity": "sha512-Cyjsg4yfiSSwRSq4TaYnxDzh3Cembx6hlUCKD0z3QCvEYFjTR3j1bFKk8haekwSB16qg25k9fiPEZMP7B/Azag==", "license": "Apache-2.0", "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", - "@vercel/gatsby-plugin-vercel-builder": "2.2.19", + "@vercel/gatsby-plugin-vercel-builder": "2.2.24", "@vercel/static-config": "3.4.0", "ts-morph": "12.0.0" } @@ -3493,13 +3491,13 @@ } }, "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.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -3703,51 +3701,51 @@ } }, "node_modules/antd": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.4.tgz", - "integrity": "sha512-lgPz4KhfhiYddV/qPYo0ieqWimCVgV2OQF72mbeGNixE753JWNnmEc7UNGy08wBS/zZ7hxrmX0pc5aX7EUaIIg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.5.0.tgz", + "integrity": "sha512-9zbVc9UukfGuqCvIAov01nlpDQWfARNmZQyt21ZhqLX7ilXmi4cdkp12xA48WEmXRXwZvno8A03qQuGE9JG8fg==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", - "@ant-design/icons": "^6.2.5", + "@ant-design/icons": "^6.3.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", - "@rc-component/cascader": "~1.16.1", + "@rc-component/cascader": "~1.17.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", - "@rc-component/dialog": "~1.9.0", + "@rc-component/dialog": "~1.10.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", - "@rc-component/form": "~1.8.3", + "@rc-component/form": "~1.8.5", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.1", "@rc-component/input-number": "~1.6.2", - "@rc-component/mentions": "~1.9.0", - "@rc-component/menu": "~1.3.1", + "@rc-component/mentions": "~1.10.0", + "@rc-component/menu": "~1.4.1", "@rc-component/motion": "^1.3.3", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.7", - "@rc-component/pagination": "~1.3.0", - "@rc-component/picker": "~1.10.0", + "@rc-component/pagination": "~1.4.0", + "@rc-component/picker": "~1.11.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~2.0.0", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", - "@rc-component/select": "~1.7.1", - "@rc-component/slider": "~1.0.1", + "@rc-component/select": "~1.8.2", + "@rc-component/slider": "~1.1.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.10.2", - "@rc-component/tabs": "~1.9.1", + "@rc-component/tabs": "~1.11.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.2", - "@rc-component/tree-select": "~1.10.0", + "@rc-component/tree-select": "~1.11.0", "@rc-component/trigger": "^3.9.1", "@rc-component/upload": "~1.1.1", "@rc-component/util": "^1.11.1", @@ -3833,9 +3831,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", - "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -4109,12 +4107,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cookie-es": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.1.tgz", - "integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5091,9 +5083,9 @@ } }, "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "version": "11.1.11", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.11.tgz", + "integrity": "sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==", "license": "MIT", "funding": { "type": "opencollective", @@ -5569,9 +5561,9 @@ } }, "node_modules/lucide-react": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", - "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -5763,9 +5755,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -6076,9 +6068,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -6242,9 +6234,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.79.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.79.0.tgz", - "integrity": "sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw==", + "version": "7.81.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz", + "integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6288,9 +6280,9 @@ } }, "node_modules/react-router": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", - "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -6310,12 +6302,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", - "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "license": "MIT", "dependencies": { - "react-router": "7.17.0" + "react-router": "7.18.1" }, "engines": { "node": ">=20.0.0" @@ -6339,9 +6331,9 @@ } }, "node_modules/recharts": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", - "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.2.tgz", + "integrity": "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw==", "license": "MIT", "workspaces": [ "www" @@ -6352,9 +6344,9 @@ "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", - "immer": "^10.1.1", + "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", + "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" @@ -6393,9 +6385,9 @@ } }, "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, "node_modules/resolve-from": { @@ -6676,13 +6668,10 @@ } }, "node_modules/srvx": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.8.9.tgz", - "integrity": "sha512-wYc3VLZHRzwYrWJhkEqkhLb31TI0SOkfYZDkUhXdp3NoCnNS0FqajiQszZZjfow/VYEuc6Q5sZh9nM6kPy2NBQ==", + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.16.tgz", + "integrity": "sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==", "license": "MIT", - "dependencies": { - "cookie-es": "^2.0.0" - }, "bin": { "srvx": "bin/srvx.mjs" }, @@ -6836,9 +6825,9 @@ } }, "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", "license": "MIT" }, "node_modules/throttle-debounce": { @@ -7530,44 +7519,45 @@ } }, "node_modules/vercel": { - "version": "54.14.2", - "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.14.2.tgz", - "integrity": "sha512-XMUBq4mEYniGGIHTAIY7Egop120x6d2NLZ9wezk3032VfKRMsB3q2UYFiZduWETG/TwFSddlNJ6UI8jBskleog==", + "version": "54.20.1", + "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.20.1.tgz", + "integrity": "sha512-Mssq8tAwjTstAi25XkFMG+nCjwCqvVLmRgcGQwSxwpptrLN6KBPDd+hAcaMXZtlxoocqdr3aa+bRqw0P9H2Y3g==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.8.14", + "@vercel/backends": "0.8.21", "@vercel/blob": "2.4.0", - "@vercel/build-utils": "13.30.0", + "@vercel/build-utils": "13.32.2", "@vercel/cli-auth": "0.3.0", "@vercel/cli-config": "0.2.0", + "@vercel/container": "0.0.4", "@vercel/detect-agent": "1.2.3", - "@vercel/elysia": "0.1.93", - "@vercel/express": "0.1.105", - "@vercel/fastify": "0.1.96", + "@vercel/elysia": "0.1.98", + "@vercel/express": "0.1.112", + "@vercel/fastify": "0.1.101", "@vercel/fun": "1.3.0", - "@vercel/go": "3.9.0", - "@vercel/h3": "0.1.102", - "@vercel/hono": "0.2.96", + "@vercel/go": "3.10.2", + "@vercel/h3": "0.1.107", + "@vercel/hono": "0.2.101", "@vercel/hydrogen": "1.4.0", - "@vercel/koa": "0.1.76", - "@vercel/nestjs": "0.2.97", - "@vercel/next": "4.19.0", - "@vercel/node": "5.8.17", + "@vercel/koa": "0.1.81", + "@vercel/nestjs": "0.2.102", + "@vercel/next": "4.20.2", + "@vercel/node": "5.8.22", "@vercel/prepare-flags-definitions": "0.3.0", - "@vercel/python": "6.45.1", + "@vercel/python": "6.47.3", "@vercel/redwood": "2.5.0", "@vercel/remix-builder": "5.9.1", - "@vercel/ruby": "2.4.0", - "@vercel/rust": "1.3.0", - "@vercel/static-build": "2.10.3", + "@vercel/ruby": "2.5.1", + "@vercel/rust": "1.4.0", + "@vercel/static-build": "2.11.4", "chokidar": "4.0.0", "esbuild": "0.27.0", - "form-data": "^4.0.0", "jose": "5.9.6", "luxon": "^3.4.0", "proxy-agent": "6.4.0", "sandbox": "3.1.2", "smol-toml": "1.5.2", + "undici": "5.29.0", "zod": "4.1.11" }, "bin": { @@ -7578,6 +7568,18 @@ "node": ">= 18" } }, + "node_modules/vercel/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/vercel/node_modules/zod": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", @@ -7610,16 +7612,16 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -7636,7 +7638,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -7688,32 +7690,21 @@ } }, "node_modules/vite/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/vite/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/vite/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -7722,9 +7713,9 @@ } }, "node_modules/vite/node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -7732,9 +7723,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -7749,9 +7740,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -7766,9 +7757,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -7783,9 +7774,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -7800,9 +7791,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -7817,9 +7808,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], @@ -7837,9 +7828,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], @@ -7857,9 +7848,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], @@ -7877,9 +7868,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], @@ -7897,9 +7888,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -7914,9 +7905,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -7924,18 +7915,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -7950,9 +7941,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -7966,21 +7957,14 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/@rolldown/pluginutils": { - "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" - }, "node_modules/vite/node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -7990,21 +7974,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/vitest": { diff --git a/ai-service/constraints-ai.txt b/ai-service/constraints-ai.txt index d344bd5f..b5f8ad24 100644 --- a/ai-service/constraints-ai.txt +++ b/ai-service/constraints-ai.txt @@ -1,7 +1,7 @@ # Install with: pip install -c constraints-ai.txt -r requirements.txt # These versions are the verified CPU/macOS x86_64-compatible AI runtime. numpy==1.26.4 -torch==2.12.1 +torch==2.2.2 transformers==4.57.6 sentence-transformers==4.1.0 faster-whisper==1.2.1 diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 8daaab58..d28d1825 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -56,7 +56,7 @@ gTTS>=2.5.4 transformers>=4.41.0,<5.0.0 accelerate>=0.34.2 # NOTE: torch>=2.4.0 has no wheels for macOS x86_64. Max available: 2.2.2. -torch<=2.12.1,>=2.12.1 +torch<=2.2.2,>=2.2.2 peft>=0.19.1 # LoRA fine-tuning and adapter loading # LangGraph + LangChain (TRACECAG Orchestration) diff --git a/backend-service/app/crud/gamification.py b/backend-service/app/crud/gamification.py index 91c80214..b0f8f0fa 100644 --- a/backend-service/app/crud/gamification.py +++ b/backend-service/app/crud/gamification.py @@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import IntegrityError +from app.core.cache import build_cache_key, delete_cached from app.models.gamification import ( Achievement, UserAchievement, UserWallet, WalletTransaction, LeaderboardEntry, UserFollowing, ActivityFeed, ShopItem, UserInventory @@ -207,6 +208,7 @@ async def add_gems( await db.refresh(wallet) else: await db.flush() + await delete_cached(build_cache_key("wallet", user_id=str(user_id))) return wallet, transaction @@ -256,6 +258,7 @@ async def spend_gems( await db.refresh(wallet) else: await db.flush() + await delete_cached(build_cache_key("wallet", user_id=str(user_id))) return wallet, transaction diff --git a/backend-service/app/routes/referral.py b/backend-service/app/routes/referral.py index b7b62f62..a9c0d5d8 100644 --- a/backend-service/app/routes/referral.py +++ b/backend-service/app/routes/referral.py @@ -12,6 +12,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.core.cache import build_cache_key, delete_cached from app.core.database import get_db from app.core.dependencies import get_current_user from app.models.gamification import UserWallet, WalletTransaction @@ -150,6 +151,8 @@ async def claim_referral_code( # Mark referral as claimed — set after wallet checks so the error path is clear locked_user.referred_by = referrer.id await db.commit() + await delete_cached(build_cache_key("wallet", user_id=str(referrer.id))) + await delete_cached(build_cache_key("wallet", user_id=str(locked_user.id))) logger.info("Referral claimed: %s referred %s", referrer.id, locked_user.id) return ClaimResponse( diff --git a/backend-service/requirements.txt b/backend-service/requirements.txt index 7753ee74..783f56ac 100644 --- a/backend-service/requirements.txt +++ b/backend-service/requirements.txt @@ -51,7 +51,7 @@ redis>=8.0.0,<9.0.0 # Background jobs — install without redis extras; redis-py is a direct dep and satisfies the transport at runtime -kombu>=5.6.2,<5.7 +kombu>=5.5.2,<5.6 celery>=5.5.3,<5.6 # System metrics (used by admin monitoring endpoints) diff --git a/backend-service/tests/services/test_starter_reward_service.py b/backend-service/tests/services/test_starter_reward_service.py index 6c424b37..f6eb8b3f 100644 --- a/backend-service/tests/services/test_starter_reward_service.py +++ b/backend-service/tests/services/test_starter_reward_service.py @@ -4,6 +4,8 @@ from sqlalchemy.orm import sessionmaker from app.core.database import Base +from app.core.cache import build_cache_key +from app.crud.gamification import WalletCRUD from app.models.gamification import UserWallet, WalletTransaction from app.models.notification import Notification from app.models.rbac import Role @@ -128,6 +130,29 @@ async def test_mark_seen_is_idempotent(db_session, test_user): assert len(grants) == 1 +async def test_add_gems_invalidates_wallet_cache( + db_session, + test_user, + monkeypatch, +): + deleted = [] + + async def fake_delete_cached(key): + deleted.append(key) + + monkeypatch.setattr("app.crud.gamification.delete_cached", fake_delete_cached) + + await WalletCRUD.add_gems( + db_session, + test_user.id, + 100, + source="test", + description="test grant", + ) + + assert deleted == [build_cache_key("wallet", user_id=str(test_user.id))] + + async def test_first_social_login_receives_reward_only_once(db_session): claims = { "email": "social@example.com", diff --git a/backend-service/tests/test_referral_routes.py b/backend-service/tests/test_referral_routes.py index 1b693b24..f4d5bed6 100644 --- a/backend-service/tests/test_referral_routes.py +++ b/backend-service/tests/test_referral_routes.py @@ -14,6 +14,8 @@ import pytest from httpx import AsyncClient, ASGITransport +from app.core.cache import build_cache_key + BASE = "/api/v1/referral" @@ -154,13 +156,19 @@ async def fake_execute(query): class TestClaimCode: @pytest.mark.asyncio - async def test_successful_claim_awards_gems(self, authed_client): + async def test_successful_claim_awards_gems(self, authed_client, monkeypatch): client, session, claimer = authed_client claimer.referred_by = None referrer = _make_user(referral_code="REFER123") referrer_wallet = _make_wallet(referrer.id, gems=50) claimer_wallet = _make_wallet(claimer.id, gems=0) + deleted = [] + + async def fake_delete_cached(key): + deleted.append(key) + + monkeypatch.setattr("app.routes.referral.delete_cached", fake_delete_cached) # Sequence: with_for_update user, referrer lookup, referrer wallet, claimer wallet results = [ @@ -189,6 +197,10 @@ async def fake_execute(query): # referred_by should be set assert claimer.referred_by == referrer.id session.commit.assert_called_once() + assert deleted == [ + build_cache_key("wallet", user_id=str(referrer.id)), + build_cache_key("wallet", user_id=str(claimer.id)), + ] @pytest.mark.asyncio async def test_self_referral_returns_400(self, authed_client): diff --git a/flutter-app/lib/features/admin/features/auth/presentation/login_screen.dart b/flutter-app/lib/features/admin/features/auth/presentation/login_screen.dart index c4f45785..afd2ad75 100644 --- a/flutter-app/lib/features/admin/features/auth/presentation/login_screen.dart +++ b/flutter-app/lib/features/admin/features/auth/presentation/login_screen.dart @@ -109,24 +109,33 @@ class LoginScreen extends StatelessWidget { ), ), const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/out-app/LexiLingo-logo.png', - height: 30, - fit: BoxFit.contain, - ), - Text( - 'ADMIN CONSOLE', - style: GoogleFonts.spaceGrotesk( - fontSize: 10, - fontWeight: FontWeight.w600, - letterSpacing: 0.18, - color: AppColors.onSurfaceMuted, + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 220, + ), + child: Image.asset( + 'assets/out-app/lexilingo-logo.png', + height: 30, + fit: BoxFit.contain, + alignment: Alignment.centerLeft, + ), ), - ), - ], + Text( + 'ADMIN CONSOLE', + style: GoogleFonts.spaceGrotesk( + fontSize: 10, + fontWeight: FontWeight.w600, + letterSpacing: 0.18, + color: AppColors.onSurfaceMuted, + ), + ), + ], + ), ), ], ), diff --git a/flutter-app/lib/features/auth/data/repositories/auth_repository_impl.dart b/flutter-app/lib/features/auth/data/repositories/auth_repository_impl.dart index f43bb9ac..6cd45482 100644 --- a/flutter-app/lib/features/auth/data/repositories/auth_repository_impl.dart +++ b/flutter-app/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -21,38 +21,22 @@ class AuthRepositoryImpl implements AuthRepository { String? displayName, }) async { try { - // Step 1: Create account on backend - await backendDataSource.register( + final user = await backendDataSource.register( email: email, username: username, password: password, displayName: displayName, ); - // Step 2: Auto-login to obtain JWT tokens, so the user is fully - // authenticated immediately after registration (no second login needed). - final loginResponse = await backendDataSource.login( - email: email, - password: password, - ); - - // Step 3: Fetch full profile (same pattern as login) - UserEntity user; - try { - user = await backendDataSource.getCurrentUser(); - } on ServerException { - user = _buildFallbackUser(loginResponse); - } - return Right(user); } on ApiErrorException catch (e) { return Left(_mapApiErrorToFailure(e)); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on UnauthorizedException catch (_) { - return Left(AuthFailure('Auto-login after registration failed.')); + return Left(AuthFailure('Registration failed.')); } on AuthException catch (_) { - return Left(AuthFailure('Auto-login after registration failed.')); + return Left(AuthFailure('Registration failed.')); } catch (e) { return Left(ServerFailure('Registration failed: $e')); } diff --git a/flutter-app/lib/features/auth/presentation/pages/login_page.dart b/flutter-app/lib/features/auth/presentation/pages/login_page.dart index 8fbae9c1..95c6e9d4 100644 --- a/flutter-app/lib/features/auth/presentation/pages/login_page.dart +++ b/flutter-app/lib/features/auth/presentation/pages/login_page.dart @@ -63,7 +63,8 @@ class _LoginPageState extends State { if (!remember) return; final savedEmail = prefs.getString(_savedEmailKey) ?? ''; - final savedPassword = await _secureStorage.read(key: _savedPasswordKey) ?? ''; + final savedPassword = + await _secureStorage.read(key: _savedPasswordKey) ?? ''; if (!mounted) return; setState(() { @@ -135,7 +136,8 @@ class _LoginPageState extends State { mediaQuery.viewInsets.bottom; final fitT = ((usableHeight - tightHeight) / (roomyHeight - tightHeight)) .clamp(0.0, 1.0); - double gap(double full, double compact) => compact + (full - compact) * fitT; + double gap(double full, double compact) => + compact + (full - compact) * fitT; return Scaffold( key: ValueKey('login-page-$localeCode'), @@ -170,7 +172,7 @@ class _LoginPageState extends State { Expanded( child: Center( child: Image.asset( - 'assets/out-app/LexiLingo-logo.png', + 'assets/out-app/lexilingo-logo.png', height: 40, fit: BoxFit.contain, color: isDark ? Colors.white : null, @@ -263,7 +265,9 @@ class _LoginPageState extends State { if (value == null || value.isEmpty) { return 'auth.pleaseEnterEmail'.tr(); } - if (!RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(value.trim())) { + if (!RegExp( + r'^[^@\s]+@[^@\s]+\.[^@\s]+$', + ).hasMatch(value.trim())) { return 'auth.invalidEmail'.tr(); } return null; diff --git a/flutter-app/lib/features/auth/presentation/pages/welcome_page.dart b/flutter-app/lib/features/auth/presentation/pages/welcome_page.dart index cc9dcfbe..0c145d7e 100644 --- a/flutter-app/lib/features/auth/presentation/pages/welcome_page.dart +++ b/flutter-app/lib/features/auth/presentation/pages/welcome_page.dart @@ -87,7 +87,7 @@ class _WelcomePageState extends State Expanded( child: Center( child: Image.asset( - 'assets/out-app/LexiLingo-logo.png', + 'assets/out-app/lexilingo-logo.png', height: 40, fit: BoxFit.contain, ), diff --git a/flutter-app/lib/features/auth/presentation/providers/auth_provider.dart b/flutter-app/lib/features/auth/presentation/providers/auth_provider.dart index 6b6aa16d..1c2fdb91 100644 --- a/flutter-app/lib/features/auth/presentation/providers/auth_provider.dart +++ b/flutter-app/lib/features/auth/presentation/providers/auth_provider.dart @@ -211,7 +211,9 @@ class AuthProvider extends ChangeNotifier { _user = user; _errorMessage = null; _isJustLoggedIn = true; - FirebaseMessagingService.instance.registerTokenWithBackend(sl()); + FirebaseMessagingService.instance.registerTokenWithBackend( + sl(), + ); unawaited(_claimPendingReferral()); }, ); @@ -247,7 +249,9 @@ class AuthProvider extends ChangeNotifier { _user = user; _errorMessage = null; _isJustLoggedIn = true; - FirebaseMessagingService.instance.registerTokenWithBackend(sl()); + FirebaseMessagingService.instance.registerTokenWithBackend( + sl(), + ); unawaited(_claimPendingReferral()); }, ); @@ -286,7 +290,9 @@ class AuthProvider extends ChangeNotifier { _user = user; _errorMessage = null; _isJustLoggedIn = true; - FirebaseMessagingService.instance.registerTokenWithBackend(sl()); + FirebaseMessagingService.instance.registerTokenWithBackend( + sl(), + ); unawaited(_claimPendingReferral()); }, ); @@ -452,10 +458,10 @@ class AuthProvider extends ChangeNotifier { _user = null; _isJustLoggedIn = false; }, - (user) { - _user = user; + (_) { + _user = null; _errorMessage = null; - _isJustLoggedIn = true; + _isJustLoggedIn = false; }, ); } catch (e) { diff --git a/flutter-app/lib/features/lexi_chat/data/datasources/lexi_chat_data_source.dart b/flutter-app/lib/features/lexi_chat/data/datasources/lexi_chat_data_source.dart index c090951b..9af5f701 100644 --- a/flutter-app/lib/features/lexi_chat/data/datasources/lexi_chat_data_source.dart +++ b/flutter-app/lib/features/lexi_chat/data/datasources/lexi_chat_data_source.dart @@ -41,6 +41,17 @@ class LexiChatDataSource { msg.contains('not found'); } + bool _isForbiddenError(Object error) { + final msg = error.toString().toLowerCase(); + return msg.contains('forbidden') || + msg.contains('status 403') || + msg.contains('403'); + } + + bool _isSessionAccessError(Object error) { + return _isSessionNotFoundError(error) || _isForbiddenError(error); + } + bool _isUnauthorizedError(Object error) { final msg = error.toString().toLowerCase(); return msg.contains('unauthorized') || @@ -288,6 +299,9 @@ class LexiChatDataSource { ); }).toList(); } catch (e) { + if (_isSessionAccessError(e)) { + rethrow; + } logWarn(_tag, 'getMessages failed, return empty history: $e'); return []; } @@ -346,17 +360,12 @@ class LexiChatDataSource { returned: (pagination['returned'] as num?)?.toInt() ?? messages.length, ); } catch (e) { - if (_isSessionNotFoundError(e)) { + if (_isSessionAccessError(e)) { logWarn( _tag, - 'getMessagesPaged session not found, return empty page: $e', - ); - return const LexiMessagesPage( - messages: [], - hasMore: false, - nextCursor: null, - returned: 0, + 'getMessagesPaged session unavailable, hand off to provider: $e', ); + rethrow; } logWarn(_tag, 'getMessagesPaged fallback to full history: $e'); final all = await getMessages(sessionId: sessionId); @@ -405,6 +414,9 @@ class LexiChatDataSource { oldestTs: metadata['oldest_ts']?.toString(), ); } catch (e) { + if (_isSessionAccessError(e)) { + rethrow; + } logWarn(_tag, 'getMessagesMetadata failed, return empty metadata: $e'); return const LexiMessagesMetadata( totalCount: 0, diff --git a/flutter-app/lib/features/lexi_chat/data/repositories/lexi_chat_repository_impl.dart b/flutter-app/lib/features/lexi_chat/data/repositories/lexi_chat_repository_impl.dart index 21139f59..13b6e6ed 100644 --- a/flutter-app/lib/features/lexi_chat/data/repositories/lexi_chat_repository_impl.dart +++ b/flutter-app/lib/features/lexi_chat/data/repositories/lexi_chat_repository_impl.dart @@ -88,8 +88,8 @@ class LexiChatRepositoryImpl implements LexiChatRepository { ); } } catch (e) { - if (_isSessionNotFoundError(e)) { - // Let provider handle stale session cleanup and auto-create a new session. + if (_isSessionAccessError(e)) { + // Let provider handle stale or cross-account session cleanup. rethrow; } // Metadata endpoint is an optimization layer only. @@ -110,6 +110,14 @@ class LexiChatRepositoryImpl implements LexiChatRepository { msg.contains('not found'); } + bool _isSessionAccessError(Object error) { + final msg = error.toString().toLowerCase(); + return _isSessionNotFoundError(error) || + msg.contains('forbidden') || + msg.contains('status 403') || + msg.contains('403'); + } + @override Future> getSessions({required String userId}) { return dataSource.getSessions(userId: userId); diff --git a/flutter-app/lib/features/lexi_chat/presentation/providers/lexi_chat_provider.dart b/flutter-app/lib/features/lexi_chat/presentation/providers/lexi_chat_provider.dart index 7c618abe..f41c3f15 100644 --- a/flutter-app/lib/features/lexi_chat/presentation/providers/lexi_chat_provider.dart +++ b/flutter-app/lib/features/lexi_chat/presentation/providers/lexi_chat_provider.dart @@ -67,6 +67,7 @@ class LexiChatProvider extends ChangeNotifier { bool _isLexiThinking = false; bool _isLexiTyping = false; bool _isRestoringSession = false; + bool _isDisposed = false; String? _error; bool _ttsEnabled = true; double _ttsSpeed = 1.0; @@ -114,6 +115,13 @@ class LexiChatProvider extends ChangeNotifier { normalized.contains('401'); } + bool _isForbiddenSessionError(Object error) { + final normalized = error.toString().toLowerCase(); + return normalized.contains('forbidden') || + normalized.contains('status 403') || + normalized.contains('403'); + } + String get learnerLevel => _learnerLevel; // ── Session ──────────────────────────────────────────────────────────────── @@ -180,6 +188,7 @@ class LexiChatProvider extends ChangeNotifier { try { await syncSessions(userId); + await _removeSessionsForOtherUsers(userId); if (_sessions.isNotEmpty) { await selectSession(_sessions.first); @@ -276,12 +285,6 @@ class LexiChatProvider extends ChangeNotifier { _isLoading = false; notifyListeners(); } catch (e) { - if (restoredFromCache) { - _isLoading = false; - notifyListeners(); - return; - } - final err = e.toString().toLowerCase(); if (err.contains('404') || err.contains('not found')) { _sessions.removeWhere((s) => s.sessionId == summary.sessionId); @@ -291,6 +294,26 @@ class LexiChatProvider extends ChangeNotifier { return; } + if (_isForbiddenSessionError(e)) { + _sessions.removeWhere((s) => s.sessionId == summary.sessionId); + await _saveSessions(); + _session = null; + _messages.clear(); + _isLoadingMoreMessages = false; + _hasMoreMessages = false; + _nextMessageCursor = null; + _error = 'This session belongs to another account. Start a new chat.'; + _isLoading = false; + notifyListeners(); + return; + } + + if (restoredFromCache) { + _isLoading = false; + notifyListeners(); + return; + } + _error = 'Failed to load session: $e'; if (_isUnauthorizedError(e)) { _error = 'Your login session expired. Please sign in again.'; @@ -570,8 +593,8 @@ class LexiChatProvider extends ChangeNotifier { final finalContent = serverText.isNotEmpty ? serverText : (accumulated.isNotEmpty - ? accumulated - : 'Squawk! Something went quiet. Can you ask that again?'); + ? accumulated + : 'Squawk! Something went quiet. Can you ask that again?'); _messages[idx] = LexiMessage( id: messageId.isNotEmpty ? messageId : placeholderId, role: 'assistant', @@ -867,6 +890,7 @@ class LexiChatProvider extends ChangeNotifier { @override void dispose() { + _isDisposed = true; _syncQueueSub?.cancel(); _typingStageTimer?.cancel(); _ttsPlayer.dispose(); @@ -928,13 +952,24 @@ class LexiChatProvider extends ChangeNotifier { } } + Future _removeSessionsForOtherUsers(String userId) async { + final before = _sessions.length; + _sessions.removeWhere((s) => s.userId != userId); + if (_sessions.length != before) { + await _saveSessions(); + notifyListeners(); + } + } + Future _loadSavedSessions() async { + if (_isDisposed) return; _isLoadingSessions = true; notifyListeners(); try { final rawString = await _secureStorage.read(key: _savedSessionsKey); if (rawString == null || rawString.isEmpty) { + if (_isDisposed) return; _isLoadingSessions = false; notifyListeners(); return; @@ -954,6 +989,7 @@ class LexiChatProvider extends ChangeNotifier { logWarn(_tag, 'Failed to load saved Lexi sessions: $e'); } + if (_isDisposed) return; _isLoadingSessions = false; notifyListeners(); } diff --git a/flutter-app/test/features/games/presentation/screens/game_accessibility_test.dart b/flutter-app/test/features/games/presentation/screens/game_accessibility_test.dart index 312c9384..0dfeea6e 100644 --- a/flutter-app/test/features/games/presentation/screens/game_accessibility_test.dart +++ b/flutter-app/test/features/games/presentation/screens/game_accessibility_test.dart @@ -11,11 +11,19 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:dartz/dartz.dart'; +import 'package:lexilingo_app/core/error/failures.dart'; import 'package:lexilingo_app/features/games/domain/entities/game_entities.dart'; import 'package:lexilingo_app/features/games/presentation/screens/game_result_screen.dart'; import 'package:lexilingo_app/features/games/presentation/widgets/game_load_state.dart'; import 'package:lexilingo_app/features/games/data/repositories/games_repository.dart'; import 'package:lexilingo_app/features/games/presentation/providers/games_provider.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/daily_challenge_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/streak_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/user_progress_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/weekly_progress_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/repositories/progress_repository.dart'; +import 'package:lexilingo_app/features/progress/presentation/providers/streak_provider.dart'; import 'package:provider/provider.dart'; // ── Fakes ───────────────────────────────────────────────────────────────────── @@ -27,8 +35,68 @@ class _FakeGamesRepository extends GamesRepository { required List> answers, int? clientDurationSeconds, int hintsUsed = 0, - }) async => + }) async => throw UnimplementedError(); +} + +class _FakeProgressRepository implements ProgressRepository { + @override + Future> updateStreak() async => Right( + StreakUpdateResult( + currentStreak: 1, + longestStreak: 1, + totalDaysActive: 1, + freezeCount: 0, + streakIncreased: false, + streakSaved: false, + ), + ); + + @override + Future> getMyStreak() async => + Right(StreakEntity.empty()); + + @override + Future> getMyProgress() async => throw UnimplementedError(); + + @override + Future> getCourseProgress( + String courseId, + ) async => throw UnimplementedError(); + + @override + Future> completeLesson({ + required String lessonId, + required double score, + }) async => throw UnimplementedError(); + + @override + Future> getTotalXp() async => throw UnimplementedError(); + + @override + Future> getWeeklyProgress() async => + throw UnimplementedError(); + + @override + Future>> useStreakFreeze() async => + throw UnimplementedError(); + + @override + Future> restoreStreak() async => + throw UnimplementedError(); + + @override + Future>> claimDailyReward() async => + throw UnimplementedError(); + + @override + Future> getDailyChallenges() async => + throw UnimplementedError(); + + @override + Future>> claimChallengeReward( + String challengeId, + ) async => throw UnimplementedError(); } // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -43,11 +111,20 @@ Widget _wrapWithSize(Widget child, Size size) { } Widget _wrapWithProvider(Widget child) { - return MaterialApp( - home: ChangeNotifierProvider.value( - value: GamesProvider(repository: _FakeGamesRepository()), - child: child, - ), + return MaterialApp(home: _withGameProviders(child)); +} + +Widget _withGameProviders(Widget child) { + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => GamesProvider(repository: _FakeGamesRepository()), + ), + ChangeNotifierProvider( + create: (_) => StreakProvider(repository: _FakeProgressRepository()), + ), + ], + child: child, ); } @@ -77,10 +154,7 @@ void main() { testWidgets('retry button is semantically a button', (tester) async { await tester.pumpWidget( _wrapWithSize( - GameLoadState( - message: 'Could not load game.', - onRetry: () async {}, - ), + GameLoadState(message: 'Could not load game.', onRetry: () async {}), const Size(390, 844), ), ); @@ -97,10 +171,7 @@ void main() { testWidgets('error icon does not block tappable controls', (tester) async { await tester.pumpWidget( _wrapWithSize( - GameLoadState( - message: 'Error.', - onRetry: () async {}, - ), + GameLoadState(message: 'Error.', onRetry: () async {}), const Size(390, 844), ), ); @@ -110,14 +181,12 @@ void main() { expect(errorIcon, findsOneWidget); }); - testWidgets('retry button has adequate minimum height (≥40px M3 standard)', - (tester) async { + testWidgets('retry button has adequate minimum height (≥40px M3 standard)', ( + tester, + ) async { await tester.pumpWidget( _wrapWithSize( - GameLoadState( - message: 'Error.', - onRetry: () async {}, - ), + GameLoadState(message: 'Error.', onRetry: () async {}), const Size(390, 844), ), ); @@ -127,10 +196,7 @@ void main() { // We verify the button renders at the expected minimum logical height. final retryButton = find.byIcon(Icons.refresh_rounded); final renderBox = tester.renderObject( - find.ancestor( - of: retryButton, - matching: find.byType(InkWell), - ).first, + find.ancestor(of: retryButton, matching: find.byType(InkWell)).first, ); expect( renderBox.size.height, @@ -150,9 +216,7 @@ void main() { final result = _makeResult(xpResult: _award); await tester.pumpWidget( - _wrapWithProvider( - GameResultScreen(result: result, xpResult: _award), - ), + _wrapWithProvider(GameResultScreen(result: result, xpResult: _award)), ); await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 1400)); @@ -163,14 +227,13 @@ void main() { expect(playAgainButtons, findsWidgets); }); - testWidgets('cloud_off error icon has no tap action (decorative)', - (tester) async { + testWidgets('cloud_off error icon has no tap action (decorative)', ( + tester, + ) async { final result = _makeResult(xpResult: null); await tester.pumpWidget( - _wrapWithProvider( - GameResultScreen(result: result, xpResult: null), - ), + _wrapWithProvider(GameResultScreen(result: result, xpResult: null)), ); await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 1400)); @@ -185,9 +248,7 @@ void main() { final result = _makeResult(xpResult: null); await tester.pumpWidget( - _wrapWithProvider( - GameResultScreen(result: result, xpResult: null), - ), + _wrapWithProvider(GameResultScreen(result: result, xpResult: null)), ); await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 1400)); @@ -211,8 +272,9 @@ void main() { (label: 'standard 390px', width: 390.0, height: 844.0), (label: 'tablet 768px', width: 768.0, height: 1024.0), ]) { - testWidgets('renders without overflow at ${testCase.label}', - (tester) async { + testWidgets('renders without overflow at ${testCase.label}', ( + tester, + ) async { await tester.pumpWidget( _wrapWithSize( GameLoadState( @@ -238,19 +300,17 @@ void main() { (label: 'standard 390px', width: 390.0, height: 844.0), (label: 'tablet 768px', width: 768.0, height: 1024.0), ]) { - testWidgets('renders without overflow at ${testCase.label}', - (tester) async { + testWidgets('renders without overflow at ${testCase.label}', ( + tester, + ) async { final result = _makeResult(xpResult: _award); await tester.pumpWidget( - MaterialApp( - home: MediaQuery( - data: MediaQueryData(size: Size(testCase.width, testCase.height)), - child: ChangeNotifierProvider.value( - value: GamesProvider(repository: _FakeGamesRepository()), - child: GameResultScreen(result: result, xpResult: _award), - ), + _wrapWithSize( + _withGameProviders( + GameResultScreen(result: result, xpResult: _award), ), + Size(testCase.width, testCase.height), ), ); await tester.pump(const Duration(milliseconds: 400)); @@ -264,8 +324,9 @@ void main() { }); group('SpellingBee listen button — semantics', () { - testWidgets('listen button has a semantic label via Semantics widget', - (tester) async { + testWidgets('listen button has a semantic label via Semantics widget', ( + tester, + ) async { // Test the Semantics wrapping we added in spelling_bee_screen.dart // by checking that a GestureDetector inside a Semantics(button: true) // is correctly labeled. @@ -285,8 +346,9 @@ void main() { }); group('reduced motion', () { - testWidgets('AnimatedContainer uses Duration.zero when disableAnimations', - (tester) async { + testWidgets('AnimatedContainer uses Duration.zero when disableAnimations', ( + tester, + ) async { // Simulate reduced motion preference await tester.pumpWidget( MaterialApp( diff --git a/flutter-app/test/features/games/presentation/screens/game_completion_test.dart b/flutter-app/test/features/games/presentation/screens/game_completion_test.dart index c2c881d7..879dcb97 100644 --- a/flutter-app/test/features/games/presentation/screens/game_completion_test.dart +++ b/flutter-app/test/features/games/presentation/screens/game_completion_test.dart @@ -9,12 +9,20 @@ library; import 'dart:async'; +import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lexilingo_app/core/error/failures.dart'; import 'package:lexilingo_app/features/games/data/repositories/games_repository.dart'; import 'package:lexilingo_app/features/games/domain/entities/game_entities.dart'; import 'package:lexilingo_app/features/games/presentation/providers/games_provider.dart'; import 'package:lexilingo_app/features/games/presentation/screens/game_result_screen.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/daily_challenge_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/streak_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/user_progress_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/weekly_progress_entity.dart'; +import 'package:lexilingo_app/features/progress/domain/repositories/progress_repository.dart'; +import 'package:lexilingo_app/features/progress/presentation/providers/streak_provider.dart'; import 'package:provider/provider.dart'; // ── Fakes ───────────────────────────────────────────────────────────────────── @@ -42,6 +50,67 @@ class _FakeGamesRepository extends GamesRepository { } } +class _FakeProgressRepository implements ProgressRepository { + @override + Future> updateStreak() async => Right( + StreakUpdateResult( + currentStreak: 1, + longestStreak: 1, + totalDaysActive: 1, + freezeCount: 0, + streakIncreased: false, + streakSaved: false, + ), + ); + + @override + Future> getMyStreak() async => + Right(StreakEntity.empty()); + + @override + Future> getMyProgress() async => + throw UnimplementedError(); + + @override + Future> getCourseProgress( + String courseId, + ) async => throw UnimplementedError(); + + @override + Future> completeLesson({ + required String lessonId, + required double score, + }) async => throw UnimplementedError(); + + @override + Future> getTotalXp() async => throw UnimplementedError(); + + @override + Future> getWeeklyProgress() async => + throw UnimplementedError(); + + @override + Future>> useStreakFreeze() async => + throw UnimplementedError(); + + @override + Future> restoreStreak() async => + throw UnimplementedError(); + + @override + Future>> claimDailyReward() async => + throw UnimplementedError(); + + @override + Future> getDailyChallenges() async => + throw UnimplementedError(); + + @override + Future>> claimChallengeReward( + String challengeId, + ) async => throw UnimplementedError(); +} + // ── Constants ───────────────────────────────────────────────────────────────── const _award = XPAwardResult( @@ -61,8 +130,13 @@ const _answers = [ Widget _wrap(Widget child, GamesProvider provider) { return MaterialApp( - home: ChangeNotifierProvider.value( - value: provider, + home: MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: provider), + ChangeNotifierProvider( + create: (_) => StreakProvider(repository: _FakeProgressRepository()), + ), + ], child: child, ), ); @@ -112,8 +186,9 @@ void main() { expect(find.byIcon(Icons.cloud_off_rounded), findsNothing); }); - testWidgets('does not show error card when xpResult is present', - (tester) async { + testWidgets('does not show error card when xpResult is present', ( + tester, + ) async { final result = _makeResult(xpResult: _award); await tester.pumpWidget( @@ -129,8 +204,9 @@ void main() { }); group('GameResultScreen — award failed', () { - testWidgets('shows error card with cloud_off icon when xpResult is null', - (tester) async { + testWidgets('shows error card with cloud_off icon when xpResult is null', ( + tester, + ) async { final result = _makeResult(xpResult: null); await tester.pumpWidget( @@ -144,8 +220,9 @@ void main() { expect(find.byIcon(Icons.cloud_off_rounded), findsOneWidget); }); - testWidgets('retry button (refresh icon) is visible in error state', - (tester) async { + testWidgets('retry button (refresh icon) is visible in error state', ( + tester, + ) async { final result = _makeResult(xpResult: null); await tester.pumpWidget( @@ -160,8 +237,9 @@ void main() { expect(find.byIcon(Icons.refresh_rounded), findsAtLeastNWidgets(1)); }); - testWidgets('tapping retry triggers second completion and awards XP', - (tester) async { + testWidgets('tapping retry triggers second completion and awards XP', ( + tester, + ) async { final repo = _FakeGamesRepository( outcomes: [Exception('network error'), _award], ); @@ -203,47 +281,45 @@ void main() { group('GameResultScreen — already awarded', () { testWidgets( - 'result screen renders without resubmitting when already awarded', - (tester) async { - final repository = _FakeGamesRepository(outcomes: [_award]); - final provider = GamesProvider(repository: repository); - - // Award once - await provider.completeGame( - gameType: GameType.wordScramble, - score: 10, - totalQuestions: 10, - correctAnswers: 10, - answers: _answers, - sessionId: 'session-z', - ); - // Try to award again — should hit alreadyAwarded path - await provider.completeGame( - gameType: GameType.wordScramble, - score: 10, - totalQuestions: 10, - correctAnswers: 10, - answers: _answers, - sessionId: 'session-z', - ); - expect(provider.awardStatus, GameAwardStatus.alreadyAwarded); - expect(repository.completionCalls, 1); - - final result = _makeResult(xpResult: _award); - - await tester.pumpWidget( - _wrap( - GameResultScreen(result: result, xpResult: _award), - provider, - ), - ); - await _drainTimers(tester); - - // Still only one network call - expect(repository.completionCalls, 1); - // Result screen shows correctly (no error state) - expect(find.byIcon(Icons.cloud_off_rounded), findsNothing); - }); + 'result screen renders without resubmitting when already awarded', + (tester) async { + final repository = _FakeGamesRepository(outcomes: [_award]); + final provider = GamesProvider(repository: repository); + + // Award once + await provider.completeGame( + gameType: GameType.wordScramble, + score: 10, + totalQuestions: 10, + correctAnswers: 10, + answers: _answers, + sessionId: 'session-z', + ); + // Try to award again — should hit alreadyAwarded path + await provider.completeGame( + gameType: GameType.wordScramble, + score: 10, + totalQuestions: 10, + correctAnswers: 10, + answers: _answers, + sessionId: 'session-z', + ); + expect(provider.awardStatus, GameAwardStatus.alreadyAwarded); + expect(repository.completionCalls, 1); + + final result = _makeResult(xpResult: _award); + + await tester.pumpWidget( + _wrap(GameResultScreen(result: result, xpResult: _award), provider), + ); + await _drainTimers(tester); + + // Still only one network call + expect(repository.completionCalls, 1); + // Result screen shows correctly (no error state) + expect(find.byIcon(Icons.cloud_off_rounded), findsNothing); + }, + ); }); // ── Unit tests for provider answer payloads ──────────────────────────────── @@ -299,7 +375,9 @@ void main() { score: 10, totalQuestions: 1, correctAnswers: 1, - answers: [{'id': 'h1', 'answer': 'elephant'}], + answers: [ + {'id': 'h1', 'answer': 'elephant'}, + ], sessionId: 'hangman-session-1', hintsUsed: 2, ); diff --git a/flutter-app/test/features/lexi_chat/data/repositories/lexi_chat_repository_impl_metadata_test.dart b/flutter-app/test/features/lexi_chat/data/repositories/lexi_chat_repository_impl_metadata_test.dart index 24f6a076..b7711f09 100644 --- a/flutter-app/test/features/lexi_chat/data/repositories/lexi_chat_repository_impl_metadata_test.dart +++ b/flutter-app/test/features/lexi_chat/data/repositories/lexi_chat_repository_impl_metadata_test.dart @@ -180,6 +180,20 @@ void main() { expect(dataSource.lastPagedSessionId, 'session_1'); }); + test('rethrows forbidden metadata errors without paged fallback', () async { + dataSource.metadataError = Exception( + 'Request /lexi/sessions/session_1/messages/metadata failed with status 403', + ); + + await expectLater( + repository.getMessagesPaged(sessionId: 'session_1'), + throwsA(isA()), + ); + + expect(dataSource.metadataCalls, 1); + expect(dataSource.pagedCalls, 0); + }); + test('skips metadata check when cursor is provided', () async { dataSource.pagedResult = const LexiMessagesPage( messages: [], diff --git a/flutter-app/test/features/lexi_chat/presentation/providers/lexi_chat_provider_test.dart b/flutter-app/test/features/lexi_chat/presentation/providers/lexi_chat_provider_test.dart index 9585c1be..c3d19126 100644 --- a/flutter-app/test/features/lexi_chat/presentation/providers/lexi_chat_provider_test.dart +++ b/flutter-app/test/features/lexi_chat/presentation/providers/lexi_chat_provider_test.dart @@ -12,6 +12,7 @@ class _FakeLexiChatRepository implements LexiChatRepository { int sendCalls = 0; int streamCalls = 0; String? lastIdempotencyKey; + Object? pagedError; Stream Function()? streamFactory; @override @@ -39,6 +40,9 @@ class _FakeLexiChatRepository implements LexiChatRepository { int limit = 50, String? cursor, }) async { + if (pagedError != null) { + throw pagedError!; + } return const LexiMessagesPage( messages: [], hasMore: false, @@ -112,7 +116,10 @@ void main() { group('LexiChatProvider', () { test('builds web-safe request ids without crypto random', () async { final repo = _FakeLexiChatRepository(); - final provider = LexiChatProvider(repository: repo, aiClient: AiApiClient()); + final provider = LexiChatProvider( + repository: repo, + aiClient: AiApiClient(), + ); addTearDown(provider.dispose); await provider.sendMessage('hello', userId: 'user@example.com'); @@ -132,7 +139,10 @@ void main() { test('falls back to non-streaming send when SSE closes empty', () async { final repo = _FakeLexiChatRepository() ..streamFactory = () => const Stream.empty(); - final provider = LexiChatProvider(repository: repo, aiClient: AiApiClient()); + final provider = LexiChatProvider( + repository: repo, + aiClient: AiApiClient(), + ); addTearDown(provider.dispose); await provider.sendMessageStreaming('hello', userId: 'user-1'); @@ -153,7 +163,10 @@ void main() { const LexiStreamChunk('Hi'), const LexiStreamChunk(' there'), ]); - final provider = LexiChatProvider(repository: repo, aiClient: AiApiClient()); + final provider = LexiChatProvider( + repository: repo, + aiClient: AiApiClient(), + ); addTearDown(provider.dispose); await provider.sendMessageStreaming('hello', userId: 'user-1'); @@ -167,5 +180,32 @@ void main() { expect(provider.messages.last.content, 'Hi there'); expect(provider.messages.last.syncStatus, 'synced'); }); + + test('clears selected session when backend returns forbidden', () async { + final repo = _FakeLexiChatRepository() + ..pagedError = Exception( + 'Request /lexi/sessions/old/messages/metadata failed with status 403', + ); + final provider = LexiChatProvider( + repository: repo, + aiClient: AiApiClient(), + ); + addTearDown(provider.dispose); + + await provider.selectSession( + LexiSessionSummary( + sessionId: 'old', + userId: 'other-user', + title: 'Old session', + createdAt: DateTime.parse('2026-05-30T00:00:00Z'), + updatedAt: DateTime.parse('2026-05-30T00:00:00Z'), + messageCount: 1, + ), + ); + + expect(provider.session, isNull); + expect(provider.messages, isEmpty); + expect(provider.error, contains('another account')); + }); }); }