From fcb72e6bed880f196479790968a70c8832b9e864 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:35:20 +0000 Subject: [PATCH 01/61] chore(deps): update pytest requirement in /backend-service (#238) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-service/requirements.txt b/backend-service/requirements.txt index 148b56c1..af6bf718 100644 --- a/backend-service/requirements.txt +++ b/backend-service/requirements.txt @@ -56,7 +56,7 @@ celery[redis]>=5.6.3,<6 psutil>=6.1.1 # Testing -pytest>=9.0.3 +pytest>=9.1.0 pytest-asyncio>=1.4.0 httpx>=0.27.0 From 1b11e4a96740405af17c9f4b6bc4c2e6ae88a8db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:35:29 +0000 Subject: [PATCH 02/61] chore(deps): update fastapi requirement in /backend-service (#239) Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.136.3...0.137.0) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.137.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-service/requirements.txt b/backend-service/requirements.txt index af6bf718..039d1e3d 100644 --- a/backend-service/requirements.txt +++ b/backend-service/requirements.txt @@ -1,5 +1,5 @@ # FastAPI and Server -fastapi>=0.136.3 +fastapi>=0.137.0 uvicorn[standard]>=0.49.0 python-multipart>=0.0.32 From e23acc274d298acde852b83dae0d8597f7b253c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:35:59 +0000 Subject: [PATCH 03/61] chore(deps): update google-auth requirement in /backend-service (#240) Updates the requirements on [google-auth](https://github.com/googleapis/google-cloud-python) to permit the latest version. - [Release notes](https://github.com/googleapis/google-cloud-python/releases) - [Changelog](https://github.com/googleapis/google-cloud-python/blob/main/packages/google-cloud-documentai/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-cloud-python/compare/google-auth-v2.53.0...google-auth-v2.54.0) --- updated-dependencies: - dependency-name: google-auth dependency-version: 2.54.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-service/requirements.txt b/backend-service/requirements.txt index 039d1e3d..dafbf5a7 100644 --- a/backend-service/requirements.txt +++ b/backend-service/requirements.txt @@ -24,7 +24,7 @@ python-jose[cryptography]>=3.5.0 passlib[bcrypt]>=1.7.4 bcrypt>=5.0.0 firebase-admin>=7.4.0 -google-auth>=2.53.0 +google-auth>=2.54.0 google-auth-oauthlib>=1.4.0 # Environment From ebc5b8d7c9e86ebf7df58075edb8defeb029990a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:36:09 +0000 Subject: [PATCH 04/61] chore(deps): update redis requirement in /backend-service (#241) Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v6.4.0...v7.4.1) --- updated-dependencies: - dependency-name: redis dependency-version: 7.4.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-service/requirements.txt b/backend-service/requirements.txt index dafbf5a7..9abea32f 100644 --- a/backend-service/requirements.txt +++ b/backend-service/requirements.txt @@ -46,7 +46,7 @@ trafilatura>=2.1.0 youtube-transcript-api>=0.6.3 # Redis (async client — used by quota manager and rate limiting) -redis>=6.4.0,<7.0.0 +redis>=7.4.1,<8.0.0 # Background jobs From 204761c522b56d2962f516075b53bf6122dbbf31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:08:33 +0000 Subject: [PATCH 05/61] chore(deps): update fastapi requirement in /ai-service (#244) Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.136.3...0.137.0) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.137.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 456f1272..38885187 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -1,5 +1,5 @@ # FastAPI and Server -fastapi>=0.136.3 +fastapi>=0.137.0 uvicorn[standard]>=0.49.0 python-multipart>=0.0.32 From 740149633dfed14325d48c4aa14445ce47ed9c91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:08:41 +0000 Subject: [PATCH 06/61] chore(deps): update accelerate requirement in /ai-service (#245) Updates the requirements on [accelerate](https://github.com/huggingface/accelerate) to permit the latest version. - [Release notes](https://github.com/huggingface/accelerate/releases) - [Commits](https://github.com/huggingface/accelerate/compare/v0.25.0...v0.34.2) --- updated-dependencies: - dependency-name: accelerate dependency-version: 0.34.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 38885187..739fd7a5 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -49,7 +49,7 @@ gTTS>=2.5.4 # NOTE: transformers>=5.0.0 requires torch>=2.4, which is NOT available on Intel Mac (x86_64). # Pin to <5.0.0 to stay compatible with torch 2.2.x on Intel Mac. transformers>=5.10.2,<6.0.0 -accelerate>=0.25.0 +accelerate>=0.34.2 # NOTE: torch>=2.4.0 has no wheels for macOS x86_64. Max available: 2.2.2. torch>=2.2.0,<=2.2.2 peft>=0.8.0 # LoRA fine-tuning and adapter loading From 7f209733e657b215cac9d75dbc224994a495630c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:08:43 +0000 Subject: [PATCH 07/61] chore(deps): update sentence-transformers requirement in /ai-service (#246) Updates the requirements on [sentence-transformers](https://github.com/huggingface/sentence-transformers) to permit the latest version. - [Release notes](https://github.com/huggingface/sentence-transformers/releases) - [Commits](https://github.com/huggingface/sentence-transformers/compare/v4.1.0...v5.5.1) --- updated-dependencies: - dependency-name: sentence-transformers dependency-version: 5.5.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 739fd7a5..c4c7337c 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -37,7 +37,7 @@ kuzu>=0.11.3 # Embeddings # NOTE: sentence-transformers>=4.0.0 has LRScheduler compatibility issues with torch 2.2.x. # Pin to <4.0.0 for Intel Mac compatibility. -sentence-transformers>=4.1.0,<5.0.0 +sentence-transformers>=5.5.1,<6.0.0 numpy>=2.4.6,<3.0.0 # STT / TTS From f2285f1b11a38c92b267e2ea1c3946c3a7fd757c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:09:01 +0000 Subject: [PATCH 08/61] chore(deps): bump pytest from 9.0.3 to 9.1.0 in /ai-service (#248) Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.3 to 9.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index c4c7337c..525d73aa 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -75,7 +75,7 @@ psutil==7.2.2 black>=26.5.1 flake8>=7.3.0 mypy>=1.20.2 -pytest==9.0.3 +pytest==9.1.0 pytest-asyncio==1.4.0 networkx==3.6.1 gliner>=0.2.26 From 72584754eeb541c764364105b21d04b1e89e079d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:09:02 +0000 Subject: [PATCH 09/61] chore(deps): update langchain-core requirement in /ai-service (#247) Updates the requirements on [langchain-core](https://github.com/langchain-ai/langchain) to permit the latest version. - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-core==1.4.1...langchain-core==1.4.7) --- updated-dependencies: - dependency-name: langchain-core dependency-version: 1.4.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 525d73aa..7518c340 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -56,7 +56,7 @@ peft>=0.8.0 # LoRA fine-tuning and adapter loading # LangGraph + LangChain (TRACECAG Orchestration) langgraph>=1.2.4 -langchain-core>=1.4.1 +langchain-core>=1.4.7 langchain-community>=0.4.2 # LLaMA.cpp for GGUF model inference (Vietnamese explanation) From 5b63a97ebb0420da2a008f8b6ff90d3a7f4a0baa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:10:38 +0000 Subject: [PATCH 10/61] chore(deps): update peft requirement in /ai-service (#243) Updates the requirements on [peft](https://github.com/huggingface/peft) to permit the latest version. - [Release notes](https://github.com/huggingface/peft/releases) - [Commits](https://github.com/huggingface/peft/compare/v0.8.0...v0.19.1) --- updated-dependencies: - dependency-name: peft dependency-version: 0.19.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 7518c340..42021640 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -52,7 +52,7 @@ transformers>=5.10.2,<6.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.2.0,<=2.2.2 -peft>=0.8.0 # LoRA fine-tuning and adapter loading +peft>=0.19.1 # LoRA fine-tuning and adapter loading # LangGraph + LangChain (TRACECAG Orchestration) langgraph>=1.2.4 From fe4dc1204bddeef215a92ec0315daa499c525b22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:11:16 +0000 Subject: [PATCH 11/61] chore(deps): update langgraph requirement in /ai-service (#249) Updates the requirements on [langgraph](https://github.com/langchain-ai/langgraph) to permit the latest version. - [Release notes](https://github.com/langchain-ai/langgraph/releases) - [Commits](https://github.com/langchain-ai/langgraph/compare/1.2.4...1.2.5) --- updated-dependencies: - dependency-name: langgraph dependency-version: 1.2.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 42021640..f6449192 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -55,7 +55,7 @@ torch>=2.2.0,<=2.2.2 peft>=0.19.1 # LoRA fine-tuning and adapter loading # LangGraph + LangChain (TRACECAG Orchestration) -langgraph>=1.2.4 +langgraph>=1.2.5 langchain-core>=1.4.7 langchain-community>=0.4.2 From 41ea7807363d9a570be1ed33cb617e6d0970c410 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:36:02 +0000 Subject: [PATCH 12/61] chore(deps-dev): bump @types/node in /admin-service (#250) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.1 to 25.9.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 704 +++++++++++++++++------------------ 2 files changed, 353 insertions(+), 353 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index 4c4cec5e..a1281355 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -32,7 +32,7 @@ "zustand": "^5.0.14" }, "devDependencies": { - "@types/node": "^25.9.2", + "@types/node": "^25.9.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/three": "^0.184.1", diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index 93b7a586..4cefac5f 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -10,25 +10,25 @@ importers: dependencies: '@gsap/react': specifier: ^2.1.2 - version: 2.1.2(gsap@3.15.0)(react@19.2.6) + version: 2.1.2(gsap@3.15.0)(react@19.2.7) '@hookform/resolvers': specifier: ^5.2.2 - version: 5.4.0(react-hook-form@7.78.0(react@19.2.6)) + version: 5.4.0(react-hook-form@7.78.0(react@19.2.7)) '@tanstack/react-query': specifier: ^5.101.0 - version: 5.101.0(react@19.2.6) + version: 5.101.0(react@19.2.7) '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(react@19.2.6) + version: 2.0.1(react@19.2.7) antd: specifier: ^6.3.7 - version: 6.4.3(luxon@3.7.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 6.4.3(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) axios: specifier: ^1.17.0 version: 1.17.0 dayjs: - specifier: ^1.11.20 - version: 1.11.20 + specifier: ^1.11.21 + version: 1.11.21 dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -37,22 +37,22 @@ importers: version: 3.15.0 lucide-react: specifier: ^1.17.0 - version: 1.17.0(react@19.2.6) + version: 1.17.0(react@19.2.7) react: - specifier: ^19.2.5 - version: 19.2.6 + specifier: ^19.2.7 + version: 19.2.7 react-dom: - specifier: ^19.2.6 - version: 19.2.6(react@19.2.6) + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) react-hook-form: specifier: ^7.78.0 - version: 7.78.0(react@19.2.6) + version: 7.78.0(react@19.2.7) react-router-dom: - specifier: ^7.16.0 - version: 7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.17.0 + version: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) recharts: specifier: ^3.8.1 - version: 3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1) + version: 3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@18.3.1)(react@19.2.7)(redux@5.0.1) three: specifier: ^0.184.0 version: 0.184.0 @@ -64,29 +64,29 @@ importers: version: 4.4.3 zustand: specifier: ^5.0.14 - version: 5.0.14(@types/react@19.2.15)(immer@11.1.8)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) + version: 5.0.14(@types/react@19.2.17)(immer@11.1.8)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: '@types/node': - specifier: ^25.6.2 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@types/react': - specifier: ^19.2.14 - version: 19.2.15 + specifier: ^19.2.17 + version: 19.2.17 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.15) + version: 19.2.3(@types/react@19.2.17) '@types/three': specifier: ^0.184.1 version: 0.184.1 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.2(vite@8.0.16(@types/node@25.9.1)(esbuild@0.27.0)(tsx@4.21.0)) + version: 6.0.2(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0)) typescript: specifier: ^6.0.3 version: 6.0.3 vite: specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.1)(esbuild@0.27.0)(tsx@4.21.0) + version: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) packages: @@ -1061,16 +1061,16 @@ packages: '@types/node@20.11.0': resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.15': - resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} '@types/stats.js@0.17.4': resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} @@ -1453,8 +1453,8 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -2132,10 +2132,10 @@ packages: resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} engines: {node: '>= 0.8'} - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: - react: ^19.2.6 + react: ^19.2.7 react-hook-form@7.78.0: resolution: {integrity: sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA==} @@ -2158,15 +2158,15 @@ packages: redux: optional: true - react-router-dom@7.16.0: - resolution: {integrity: sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==} + react-router-dom@7.17.0: + resolution: {integrity: sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.16.0: - resolution: {integrity: sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==} + react-router@7.17.0: + resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -2175,8 +2175,8 @@ packages: react-dom: optional: true - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} readdirp@4.1.2: @@ -2591,46 +2591,46 @@ snapshots: dependencies: '@ant-design/fast-color': 3.0.1 - '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@ant-design/cssinjs': 2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@babel/runtime': 7.29.2 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@ant-design/cssinjs@2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@ant-design/cssinjs@2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 csstype: 3.2.3 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) stylis: 4.4.0 '@ant-design/fast-color@3.0.1': {} '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons@6.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@ant-design/icons@6.2.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@ant-design/colors': 8.0.1 '@ant-design/icons-svg': 4.4.2 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@ant-design/react-slick@2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@ant-design/react-slick@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 clsx: 2.1.1 json2mq: 0.2.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) throttle-debounce: 5.0.2 '@babel/runtime@7.29.2': {} @@ -2751,15 +2751,15 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@gsap/react@2.1.2(gsap@3.15.0)(react@19.2.6)': + '@gsap/react@2.1.2(gsap@3.15.0)(react@19.2.7)': dependencies: gsap: 3.15.0 - react: 19.2.6 + react: 19.2.7 - '@hookform/resolvers@5.4.0(react-hook-form@7.78.0(react@19.2.6))': + '@hookform/resolvers@5.4.0(react-hook-form@7.78.0(react@19.2.7))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.78.0(react@19.2.6) + react-hook-form: 7.78.0(react@19.2.7) '@isaacs/balanced-match@4.0.1': {} @@ -2876,341 +2876,341 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - '@rc-component/cascader@1.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/cascader@1.15.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/select': 1.6.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tree': 1.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/checkbox@2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/checkbox@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/collapse@1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/collapse@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/color-picker@3.1.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/color-picker@3.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@ant-design/fast-color': 3.0.1 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/context@2.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/context@2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/dialog@1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/dialog@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/portal': 2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/drawer@1.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/drawer@1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/portal': 2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/dropdown@1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/dropdown@1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/form@1.8.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/form@1.8.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/async-validator': 5.1.0 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/image@1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/image@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/portal': 2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/input-number@1.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/input-number@1.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/mini-decimal': 1.1.3 - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/input@1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/input@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/mentions@1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/mentions@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/input': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/menu': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/input': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/menu@1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/menu@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/overflow': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) '@rc-component/mini-decimal@1.1.3': dependencies: '@babel/runtime': 7.29.2 - '@rc-component/motion@1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/motion@1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/mutate-observer@2.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/mutate-observer@2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/notification@2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/notification@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/overflow@1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/overflow@1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/pagination@1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/pagination@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/picker@1.10.0(dayjs@1.11.20)(luxon@3.7.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/picker@1.10.0(dayjs@1.11.21)(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/overflow': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - dayjs: 1.11.20 + dayjs: 1.11.21 luxon: 3.7.2 - '@rc-component/portal@2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/portal@2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/progress@1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/progress@1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/qrcode@1.1.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/qrcode@1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/rate@1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/rate@1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/resize-observer@1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/resize-observer@1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/segmented@1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/segmented@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/select@1.6.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/select@1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/overflow': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/virtual-list': 1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/slider@1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/slider@1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/steps@1.2.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/steps@1.2.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/switch@1.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/switch@1.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/table@1.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/table@1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/context': 2.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/virtual-list': 1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/context': 2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/tabs@1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/tabs@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/dropdown': 1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/menu': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/dropdown': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/tooltip@1.4.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/tooltip@1.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/tour@2.4.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/tour@2.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/portal': 2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/tree-select@1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/tree-select@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/select': 1.6.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tree': 1.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/tree@1.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/tree@1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/virtual-list': 1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/trigger@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/trigger@3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/portal': 2.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/upload@1.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/upload@1.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@rc-component/util@1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/util@1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: is-mobile: 5.0.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) react-is: 18.3.1 - '@rc-component/virtual-list@1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@rc-component/virtual-list@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.29.2 - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 @@ -3219,8 +3219,8 @@ snapshots: redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 optionalDependencies: - react: 19.2.6 - react-redux: 9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1) + react: 19.2.7 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) '@renovatebot/pep440@4.2.1': {} @@ -3335,10 +3335,10 @@ snapshots: '@tanstack/query-core@5.101.0': {} - '@tanstack/react-query@5.101.0(react@19.2.6)': + '@tanstack/react-query@5.101.0(react@19.2.7)': dependencies: '@tanstack/query-core': 5.101.0 - react: 19.2.6 + react: 19.2.7 '@tootallnate/once@2.0.0': {} @@ -3390,15 +3390,15 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@25.9.1': + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 - '@types/react-dom@19.2.3(@types/react@19.2.15)': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - '@types/react@19.2.15': + '@types/react@19.2.17': dependencies: csstype: 3.2.3 @@ -3417,9 +3417,9 @@ snapshots: '@types/webxr@0.5.24': {} - '@vercel/analytics@2.0.1(react@19.2.6)': + '@vercel/analytics@2.0.1(react@19.2.7)': optionalDependencies: - react: 19.2.6 + react: 19.2.7 '@vercel/backends@0.8.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: @@ -3727,10 +3727,10 @@ snapshots: json-schema-to-ts: 1.6.4 ts-morph: 12.0.0 - '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.1)(esbuild@0.27.0)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.27.0)(tsx@4.21.0) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) abbrev@3.0.1: {} @@ -3755,55 +3755,55 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 - antd@6.4.3(luxon@3.7.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + antd@6.4.3(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@ant-design/colors': 8.0.1 - '@ant-design/cssinjs': 2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ant-design/fast-color': 3.0.1 - '@ant-design/icons': 6.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@ant-design/react-slick': 2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@ant-design/icons': 6.2.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ant-design/react-slick': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@babel/runtime': 7.29.2 - '@rc-component/cascader': 1.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/checkbox': 2.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/collapse': 1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/color-picker': 3.1.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/dialog': 1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/drawer': 1.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/dropdown': 1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/form': 1.8.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/image': 1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/input': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/input-number': 1.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/mentions': 1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/menu': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/motion': 1.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/notification': 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/pagination': 1.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/picker': 1.10.0(dayjs@1.11.20)(luxon@3.7.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/progress': 1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/qrcode': 1.1.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/rate': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/resize-observer': 1.1.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/segmented': 1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/select': 1.6.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/slider': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/steps': 1.2.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/switch': 1.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/table': 1.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tabs': 1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tooltip': 1.4.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tour': 2.4.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tree': 1.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/tree-select': 1.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/trigger': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/upload': 1.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@rc-component/util': 1.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@rc-component/cascader': 1.15.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/checkbox': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/collapse': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/color-picker': 3.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/dialog': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/drawer': 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/dropdown': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/form': 1.8.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/image': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/input': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/input-number': 1.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/mentions': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/notification': 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/pagination': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/picker': 1.10.0(dayjs@1.11.21)(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/progress': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/qrcode': 1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/rate': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/segmented': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/slider': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/steps': 1.2.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/switch': 1.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/table': 1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tabs': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tooltip': 1.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tour': 2.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree-select': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/upload': 1.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 - dayjs: 1.11.20 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + dayjs: 1.11.21 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) scroll-into-view-if-needed: 3.1.0 throttle-debounce: 5.0.2 transitivePeerDependencies: @@ -3959,7 +3959,7 @@ snapshots: data-uri-to-buffer@6.0.2: {} - dayjs@1.11.20: {} + dayjs@1.11.21: {} debug@4.3.4: dependencies: @@ -4386,9 +4386,9 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.17.0(react@19.2.6): + lucide-react@1.17.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 luxon@3.7.2: {} @@ -4607,59 +4607,59 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-dom@19.2.6(react@19.2.6): + react-dom@19.2.7(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 scheduler: 0.27.0 - react-hook-form@7.78.0(react@19.2.6): + react-hook-form@7.78.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 react-is@18.3.1: {} - react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1): + react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 redux: 5.0.1 - react-router-dom@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router-dom@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-router: 7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-router@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: cookie: 1.1.1 - react: 19.2.6 + react: 19.2.7 set-cookie-parser: 2.7.2 optionalDependencies: - react-dom: 19.2.6(react@19.2.6) + react-dom: 19.2.7(react@19.2.7) - react@19.2.6: {} + react@19.2.7: {} readdirp@4.1.2: {} - recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1): + recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@18.3.1)(react@19.2.7)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.46.1 eventemitter3: 5.0.4 immer: 10.2.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) react-is: 18.3.1 - react-redux: 9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.7) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -4929,9 +4929,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.6): + use-sync-external-store@1.6.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 vercel@54.6.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: @@ -4994,7 +4994,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.0.16(@types/node@25.9.1)(esbuild@0.27.0)(tsx@4.21.0): + vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5002,7 +5002,7 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.3 esbuild: 0.27.0 fsevents: 2.3.3 tsx: 4.21.0 @@ -5061,9 +5061,9 @@ snapshots: zod@4.4.3: {} - zustand@5.0.14(@types/react@19.2.15)(immer@11.1.8)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + zustand@5.0.14(@types/react@19.2.17)(immer@11.1.8)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 immer: 11.1.8 - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) From 3f3054c4bbd36449d1c60b611241bee555df0fc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:38:28 +0000 Subject: [PATCH 13/61] chore(deps): bump react-hook-form in /admin-service (#251) Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.78.0 to 7.79.0. - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.78.0...v7.79.0) --- updated-dependencies: - dependency-name: react-hook-form dependency-version: 7.79.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index a1281355..8cdc7307 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -23,7 +23,7 @@ "lucide-react": "^1.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-hook-form": "^7.78.0", + "react-hook-form": "^7.79.0", "react-router-dom": "^7.17.0", "recharts": "^3.8.1", "three": "^0.184.0", diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index 4cefac5f..f8eaca2c 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.1.2(gsap@3.15.0)(react@19.2.7) '@hookform/resolvers': specifier: ^5.2.2 - version: 5.4.0(react-hook-form@7.78.0(react@19.2.7)) + version: 5.4.0(react-hook-form@7.79.0(react@19.2.7)) '@tanstack/react-query': specifier: ^5.101.0 version: 5.101.0(react@19.2.7) @@ -45,8 +45,8 @@ importers: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) react-hook-form: - specifier: ^7.78.0 - version: 7.78.0(react@19.2.7) + specifier: ^7.79.0 + version: 7.79.0(react@19.2.7) react-router-dom: specifier: ^7.17.0 version: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2137,8 +2137,8 @@ packages: peerDependencies: react: ^19.2.7 - react-hook-form@7.78.0: - resolution: {integrity: sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA==} + react-hook-form@7.79.0: + resolution: {integrity: sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2756,10 +2756,10 @@ snapshots: gsap: 3.15.0 react: 19.2.7 - '@hookform/resolvers@5.4.0(react-hook-form@7.78.0(react@19.2.7))': + '@hookform/resolvers@5.4.0(react-hook-form@7.79.0(react@19.2.7))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.78.0(react@19.2.7) + react-hook-form: 7.79.0(react@19.2.7) '@isaacs/balanced-match@4.0.1': {} @@ -4612,7 +4612,7 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 - react-hook-form@7.78.0(react@19.2.7): + react-hook-form@7.79.0(react@19.2.7): dependencies: react: 19.2.7 From 1401bea786a2a828673f04199db40f4a25ac4264 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:41:07 +0000 Subject: [PATCH 14/61] chore(deps): bump axios from 1.17.0 to 1.18.0 in /admin-service (#253) Bumps [axios](https://github.com/axios/axios) from 1.17.0 to 1.18.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index 8cdc7307..b8272b97 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -16,7 +16,7 @@ "@tanstack/react-query": "^5.101.0", "@vercel/analytics": "^2.0.1", "antd": "^6.3.7", - "axios": "^1.17.0", + "axios": "^1.18.0", "dayjs": "^1.11.21", "dotenv": "^17.4.2", "gsap": "^3.15.0", diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index f8eaca2c..d305ea73 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^6.3.7 version: 6.4.3(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) axios: - specifier: ^1.17.0 - version: 1.17.0 + specifier: ^1.18.0 + version: 1.18.0 dayjs: specifier: ^1.11.21 version: 1.11.21 @@ -1296,8 +1296,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.17.0: - resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + axios@1.18.0: + resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} @@ -1632,6 +1632,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + fs-extra@11.1.0: resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} engines: {node: '>=14.14'} @@ -3835,10 +3839,10 @@ snapshots: asynckit@0.4.0: {} - axios@1.17.0: + axios@1.18.0: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -4156,6 +4160,14 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + fs-extra@11.1.0: dependencies: graceful-fs: 4.2.11 From f71267da9b9cf53f9e124ec0cae002341d989afa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:41:11 +0000 Subject: [PATCH 15/61] chore(deps): bump lucide-react from 1.17.0 to 1.18.0 in /admin-service (#255) Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.17.0 to 1.18.0. - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react) --- updated-dependencies: - dependency-name: lucide-react dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index b8272b97..1c6eacf5 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -20,7 +20,7 @@ "dayjs": "^1.11.21", "dotenv": "^17.4.2", "gsap": "^3.15.0", - "lucide-react": "^1.17.0", + "lucide-react": "^1.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-hook-form": "^7.79.0", diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index d305ea73..48bc74e1 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^3.15.0 version: 3.15.0 lucide-react: - specifier: ^1.17.0 - version: 1.17.0(react@19.2.7) + specifier: ^1.18.0 + version: 1.18.0(react@19.2.7) react: specifier: ^19.2.7 version: 19.2.7 @@ -1895,8 +1895,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@1.17.0: - resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + lucide-react@1.18.0: + resolution: {integrity: sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4398,7 +4398,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.17.0(react@19.2.7): + lucide-react@1.18.0(react@19.2.7): dependencies: react: 19.2.7 From 2fc7badb8c97caa96d9fb1563d65962098098ebf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:41:16 +0000 Subject: [PATCH 16/61] chore(deps): bump vercel from 54.6.1 to 54.14.0 in /admin-service (#254) Bumps [vercel](https://github.com/vercel/vercel/tree/HEAD/packages/cli) from 54.6.1 to 54.14.0. - [Release notes](https://github.com/vercel/vercel/releases) - [Changelog](https://github.com/vercel/vercel/blob/main/packages/cli/CHANGELOG.md) - [Commits](https://github.com/vercel/vercel/commits/vercel@54.14.0/packages/cli) --- updated-dependencies: - dependency-name: vercel dependency-version: 54.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 487 +++++++++++++++++++++++++---------- 2 files changed, 345 insertions(+), 144 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index 1c6eacf5..d72dc4fc 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -27,7 +27,7 @@ "react-router-dom": "^7.17.0", "recharts": "^3.8.1", "three": "^0.184.0", - "vercel": "^54.6.1", + "vercel": "^54.14.0", "zod": "^4.4.3", "zustand": "^5.0.14" }, diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index 48bc74e1..bb513af0 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^0.184.0 version: 0.184.0 vercel: - specifier: ^54.6.1 - version: 54.6.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + specifier: ^54.14.0 + version: 54.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) zod: specifier: ^4.4.3 version: 4.4.3 @@ -358,12 +358,99 @@ packages: engines: {node: '>=18'} hasBin: true + '@napi-rs/keyring-darwin-arm64@1.2.0': + resolution: {integrity: sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/keyring-darwin-x64@1.2.0': + resolution: {integrity: sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/keyring-freebsd-x64@1.2.0': + resolution: {integrity: sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/keyring-linux-arm-gnueabihf@1.2.0': + resolution: {integrity: sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/keyring-linux-arm64-gnu@1.2.0': + resolution: {integrity: sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-arm64-musl@1.2.0': + resolution: {integrity: sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-linux-riscv64-gnu@1.2.0': + resolution: {integrity: sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-gnu@1.2.0': + resolution: {integrity: sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-musl@1.2.0': + resolution: {integrity: sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-win32-arm64-msvc@1.2.0': + resolution: {integrity: sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/keyring-win32-ia32-msvc@1.2.0': + resolution: {integrity: sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/keyring-win32-x64-msvc@1.2.0': + resolution: {integrity: sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/keyring@1.2.0': + resolution: {integrity: sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1113,38 +1200,41 @@ packages: vue-router: optional: true - '@vercel/backends@0.8.3': - resolution: {integrity: sha512-a0NivGZn4BA6155m7BXqBBlHcqoB4HIxiYDcs9ZRFySr45CYAJg1icUmlNKgWMX5gL7jA1JToXIRyjs78mkPtg==} + '@vercel/backends@0.8.14': + resolution: {integrity: sha512-jgepdZh7E4ameCUSt/b28ImFd9Bz1VX71lKwD/ThtZ7nGIwi5SJ9hwyURvH60c4rNW6nKxMD/iSOlcgQOfrK3A==} '@vercel/blob@2.4.0': resolution: {integrity: sha512-ncQ8CRb6XoEAYJwjOTRGpACRT6h/AeY+/33gLyeVxG5BIes27OPm1jmqreF+JHjcTmGhClTP+kBpmyLfbV0xew==} engines: {node: '>=20.0.0'} - '@vercel/build-utils@13.26.4': - resolution: {integrity: sha512-0g3ZxtZUJZbt4y0Vu4pkHtu1UN58FbVF9cqGT8T6jHp0EHdLGFj5TVCiME8ALeK4tjPImSmxnKZvvB5yb2hqEw==} + '@vercel/build-utils@13.30.0': + resolution: {integrity: sha512-fLIa8cpELsSoWbcxshaqegwlTCfKnbgsDmA6uIb+oA797xvSmYkPu5a781aRYCRdghgyOZF7NCSVgtZjHjunnQ==} - '@vercel/cervel@0.1.11': - resolution: {integrity: sha512-xgdahaAIny+mxLU4OUHqTtueV3JZrDDoC8Av871TdGiJGTEO9D1HQEwDvtmcKpQtxvbgSKFI0zc1rdesT2SdOQ==} + '@vercel/cervel@0.1.22': + resolution: {integrity: sha512-YbL2AosH3FaBLJZu4GYdVMkFv+5cbWwwSrRBTGN52odynQUudTUQfbc7rCzPyNQLdk2QC4w2IDYRD+nqgojhVg==} hasBin: true - '@vercel/cli-config@0.1.2': - resolution: {integrity: sha512-XQOcuCM+8tKjh3sfgGRKRuNh78u2D8uGpDJIFcCtFi2tUqbGvqmJo790XX7+Bwakk08y0FCrs2JlEjvvwRhpAg==} + '@vercel/cli-auth@0.3.0': + resolution: {integrity: sha512-9nsdxUpV/L+9CBVGeRw/Qby2azhi2lk01jp0aTH+Hxx2U61+mAmbi5qChHnrbEmQdx2Ih7dp4LxO3nj1Q7f/5Q==} + + '@vercel/cli-config@0.2.0': + resolution: {integrity: sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ==} '@vercel/detect-agent@1.2.3': resolution: {integrity: sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag==} engines: {node: '>=14'} - '@vercel/elysia@0.1.84': - resolution: {integrity: sha512-lXJHbMKbpx+hfTLw1kH0EyPsTSrBiQyvL2Xz7r262DwdzVewdQzVqpGVHcT5x4muOhnHmDbQ8MlmE2j5K9Nt1Q==} + '@vercel/elysia@0.1.93': + resolution: {integrity: sha512-36yzZJVU9o7/CcH1TbqeGTRwBePDaCEzv1CYfS183HdLXzsD+pOjOkTqZOBJu7h9aHpJFCCau55Ed0xOQC2jhA==} - '@vercel/error-utils@2.1.0': - resolution: {integrity: sha512-DiJcXBOB9N6QM4d7hYPM9Ck/AUjzBl58XNQPxS74o7CuvIanjzrGgygP/70VsyEASeIJMazk1LrhwcNTR/eZGQ==} + '@vercel/error-utils@2.2.0': + resolution: {integrity: sha512-WFWiRxfPzoYWYifaj4thSKvAaZZwUOqD4k5GINRIgZgCiS2E3iAJbWbIsIZmkQdTecWFHcWGA6q48CjisgpOBA==} - '@vercel/express@0.1.94': - resolution: {integrity: sha512-J2sjhwK9BKVfoJ28UB/R1Q6ugJ47mtvVALs+paWQoIdlK6FLip3jKstqnZU1L6VxguVmERWQk49U5mux3CgeVw==} + '@vercel/express@0.1.105': + resolution: {integrity: sha512-F+QRQxWWKdeztW4MKPkzXl/Ajc8j1qOffCp1tt8+YPaNiqZtxpn7FjIYRXHbfR1aepfaEkDRDD7o2qxmmaUU5w==} - '@vercel/fastify@0.1.87': - resolution: {integrity: sha512-JdUKon4nTozwQweVycsAVTtn3qhD6sPJKFsPpetHZEh2FdlEkOUSTMRlmuO7pPjSQ41MckplVkBNNQSSXuaXmA==} + '@vercel/fastify@0.1.96': + resolution: {integrity: sha512-4Uq6GVmi0ksQxIHp4FDUhgoz47uacxZm42XT04SK5gzX5v5UmZXWZPprZjkwwk/2gmP/rF2qKqJ0nu6OsI+yDw==} '@vercel/fun@1.3.0': resolution: {integrity: sha512-8erw9uPe0dFg45THkNxmjtvMX143SkZebmjgSVbcM3XCkXu3RIiBaJMcMNG8aaS+rnTuw8+d4De9HVT0M/r3wg==} @@ -1153,56 +1243,56 @@ packages: '@vercel/gatsby-plugin-vercel-analytics@1.0.11': resolution: {integrity: sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw==} - '@vercel/gatsby-plugin-vercel-builder@2.2.10': - resolution: {integrity: sha512-KIkEQRLK7/gDSBTFEZble4dWkGQNTniBe2pJaNl6eUlyB2RW4cl/x2+1Qms+q12VP2EwzHLJjj7hhC48kwPdQA==} + '@vercel/gatsby-plugin-vercel-builder@2.2.19': + resolution: {integrity: sha512-dA1ZWZHruRjFbLToqjcPHUyh5J7/Cl3qGFDge87ghAInuy/aXfalApzFbw61OIopGx0SSsIYi/8kbpJ+SbQ6ZQ==} - '@vercel/go@3.8.0': - resolution: {integrity: sha512-ftQqQMn3sGdL8mdIqfcS3YZg6dazM/h4s0jkY37oVV1rPdh7Aq/GL0oMjv1L+PoIk5uJEAyBan7C8Yisp4LH+g==} + '@vercel/go@3.9.0': + resolution: {integrity: sha512-vGbEwckvtr9UXKDz3qEHUE7VHJGG/9VQLuScgHytksfZyX8dzvNPgEuBKBbhPaMwua8pUCZKQqNxFM3Dul+bLA==} - '@vercel/h3@0.1.93': - resolution: {integrity: sha512-ZvheBxNHJOHyC1/vAoni8N4VcXTDLGfIf5REFE/ZFwWOP4YepmFgjOPk0udMY5YXk5ryUNBTC6tdvrFLB+nSDQ==} + '@vercel/h3@0.1.102': + resolution: {integrity: sha512-UnHFLBemE2UfvQ+w2z2zxGcvyiShQ8eMdk3ag2sV71wbyatnXwmio8HOy//B2ZIEQNEid5gW2HjWOXLtrR+E2Q==} - '@vercel/hono@0.2.87': - resolution: {integrity: sha512-UH11b/RwIcXaCJpm6y8v9jXuv2ju4/bJdAHW9Vx/dvtUJDFWDSQbwKf36QIZoJ8UJRJfxo+PzT1TANXuOi8jfw==} + '@vercel/hono@0.2.96': + resolution: {integrity: sha512-h4G0GSo5omhXS1iqimIcekcW7THEAnUGd6eioDNnSqwS3L0CGEvvTPUekJDyJcybsqnKslH1XTZA40f4b9MPHg==} - '@vercel/hydrogen@1.3.8': - resolution: {integrity: sha512-ANCJg+FyZQpP2tntc9GUXQDGtOpQ/soykJGB1WBeCqn96QJFfSzRHHz1MHCh163MrKjO5Hx5cnCYUgRYRXVSjA==} + '@vercel/hydrogen@1.4.0': + resolution: {integrity: sha512-gf3ELmAjcia7WNGNHAB/rFWFU0l5fJ7mTMgGC5hvcCjSIE4kp8WWuGYiTQgQ8W6GF/1FJAxa2J3BhY/yxjY8tA==} - '@vercel/koa@0.1.67': - resolution: {integrity: sha512-pxNRTkJ71kgihABVQXG5E4L3qXySfoCtZ/f1yJzCfNV1r+IAlf2QL8tQWTTXeY7s/rcrOLF19HQYMW4TVzyE3A==} + '@vercel/koa@0.1.76': + resolution: {integrity: sha512-6MuBn5RM2nHKfHKeBqbs3aaFJlBFDhOZgpC1ubLQG2B/EvnAfljB7FPIKCl1SwANAHisX7vQACinMedCbFdi+Q==} - '@vercel/nestjs@0.2.88': - resolution: {integrity: sha512-bqJf/NieVsDHlwFDZ5D/UJHXetd1GckXSGLcoveC23ZeKaBJskPlC0oiWZ5wikzbeHHz7jdi0mqjwrvpmI6YBw==} + '@vercel/nestjs@0.2.97': + resolution: {integrity: sha512-KM9uBmC+a10EPJgtm0B/vfGsTaUEzxKkihTyaQ4PhSUQHJOABTv6TBpGKtwIM5P1vMLKUg9R0A2SUvkKdaALvA==} - '@vercel/next@4.17.5': - resolution: {integrity: sha512-zmj3X+H8bTs0V+8RbsuV8ZbHI0J3nBX+ZV4gcrpGbFNOzqydcwb3H2UJXnEP/rwED3zOevHD65GYPkv9JR/wOQ==} + '@vercel/next@4.19.0': + resolution: {integrity: sha512-9B7juRNydQ9MU7xT8OE254ymhVrNdcOKlXrZ4anem8YEa34KvXjLczQhZEPdvTz9JaCXpYiEEGmd/D3t2oJ+Uw==} '@vercel/nft@1.10.0': resolution: {integrity: sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==} engines: {node: '>=20'} hasBin: true - '@vercel/node@5.8.8': - resolution: {integrity: sha512-+uRT9evnGWUE6klrJJED4fCvlSxNShbIc/UY4FeUzt2sdcy5a5b1IoYlo94RJd7tAY9Jg2lR2cVfGfsnWH81ZA==} + '@vercel/node@5.8.17': + resolution: {integrity: sha512-n2DVzblqS43LTs4BV1iLfx8tjjrTegcSsyDgRzn70h6tQePYWjl+h0bU3X1stpITZtaYJSZYwVevuX1BJNZHHg==} '@vercel/oidc@3.2.0': resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vercel/prepare-flags-definitions@0.2.1': - resolution: {integrity: sha512-ouXTsqn7I9xZ1KKezgvn/w3tZeQHL/tc52j9GHiOYi6kT8xgdbT8s2x8C9BQr44iceX0hfhtZwk9q7NuI2Tqbw==} + '@vercel/prepare-flags-definitions@0.3.0': + resolution: {integrity: sha512-/0nuDFwYje0nqZnVKSd2VfJy2wOPQwbkas1qO1JQgtb0sLl+EeSCW4O9hrvq55pN50PNlAZ/APSeWHIAT9ZGHg==} '@vercel/python-analysis@0.11.1': resolution: {integrity: sha512-EPPLuXJQhIDUx08H9nG76AR2HSgBquwe3OAX5s2w20M923iaWeGGVkhX/4yZ89CJfXEZgE1Aj/mX7lVHOVIcYA==} - '@vercel/python@6.44.0': - resolution: {integrity: sha512-78+todx9665oQSZahIsK8o6gN76oKyPeuw5g61ZI88sAjHgjXL0qE5ou+rEBjwTfRa5+hXxBeeWi7YUpCQ8xJA==} + '@vercel/python@6.45.0': + resolution: {integrity: sha512-5gpmA3LT1HTDsFIkDrQIBZaTWDnCXGwwRnD5JDajfn1GlKtY8JtdPmwbLk5dX8lTnQr8t2I7SWxJqD08XHZ/1w==} - '@vercel/redwood@2.4.15': - resolution: {integrity: sha512-f7bK2VFCr6UpUxQ0bUUev9rfHFVh3OeZjr5eISoVjEQ/x+cXCICfBb20wFb9AllcyhXo8a2Lkh9b65wdOgVA3Q==} + '@vercel/redwood@2.5.0': + resolution: {integrity: sha512-LO24CbieV57rGAqrq5n5z/B+fMYeUqbE/Ge3qMeKRuS3Q9awgcBgB6WYLCe8crHM7XJd5AZjbuyoS/6k3r0X0w==} - '@vercel/remix-builder@5.8.4': - resolution: {integrity: sha512-OcoNTzHS3cPcyYo+feOowwpbDewUzwfwFE5CTOKQLUdKt4unZ6Sf/ObOelokgLsxY0JEEp5Gr7zV936UQVy8Dg==} + '@vercel/remix-builder@5.9.1': + resolution: {integrity: sha512-s0SpfV640nmQ1BsKCPMYugGrH/V+319onTo2ZILSmsy6NTBlmeN0zJU9nPcP8dBEotGqtp68NfOlOUUdrVBzkw==} '@vercel/ruby@2.4.0': resolution: {integrity: sha512-YI7Amyf09hHZWOqDHgJO92XcKh6Pye8rrmJFhlP6euG3o6QjoZzJj7Z2WzjSrDRGMewzEK4uz2+CbNpNS7gLog==} @@ -1210,11 +1300,11 @@ packages: '@vercel/rust@1.3.0': resolution: {integrity: sha512-z1X0z1NM+ISunm/scgRpuENNwcKlh7DjXn0QvKE0n10DcaYqxkBQVK8iRA9X05xSK9AzPlnB9DHdGKiXZO5buw==} - '@vercel/sandbox@1.9.0': - resolution: {integrity: sha512-zgr1ad0tkT1xZn/8Vxo60wOUOLqMAVGo4WqJQ8/UDcUtWynNJsBjI2tiMdWZrAo9EKH1MIqEzJNkcclF0UT1EQ==} + '@vercel/sandbox@2.1.1': + resolution: {integrity: sha512-gKhW+YlvU15Qxya7jQKByB+sqA1dWat5zx/rvxT52E3Ryg9MAIXgqD5wd1d+CoJDbdHL26gIOcksTZY5sFpplA==} - '@vercel/static-build@2.9.33': - resolution: {integrity: sha512-PTcDWlCo/aMJo3uoQ9wuJTsNqKFqMfWc1sBhJD91eicqa4vXisJHlZ7bDs6s0GHHMkX++wICiYTxOYKyYnXqrQ==} + '@vercel/static-build@2.10.3': + resolution: {integrity: sha512-+IipWidX6mL1Zck8Khw4wSXHPSgDfkapvcuWA9wSCmhnk2GL2Iuu+6vab+g8UPahv+QDUYm8Tphhadg6dICZnA==} '@vercel/static-config@3.4.0': resolution: {integrity: sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==} @@ -1232,6 +1322,9 @@ packages: babel-plugin-react-compiler: optional: true + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1241,8 +1334,8 @@ packages: peerDependencies: acorn: ^8 - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1314,8 +1407,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.8.3: - resolution: {integrity: sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==} + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: @@ -1477,6 +1570,10 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -1762,6 +1859,11 @@ packages: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1784,12 +1886,19 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -2035,6 +2144,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@8.4.0: + resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} + engines: {node: '>=12'} + os-paths@4.4.0: resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} engines: {node: '>= 6.0'} @@ -2245,8 +2358,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sandbox@2.5.6: - resolution: {integrity: sha512-tnFr7nyiuEhsAGb+xy60SDbij0790X+FgDljh3J/2HaRM6yQgNJkQKHbDH8ld7mR+PozXGgEfJ2Dc/5OyFnwsg==} + sandbox@3.1.2: + resolution: {integrity: sha512-g93rma0Z9Aa6EoTktzYnGZmQvMzm7j73BekJIMwmkdWR5uryZ+6hBCADtd3JKGAB27k+ubnG7HHsZqtscAMzIQ==} hasBin: true scheduler@0.27.0: @@ -2264,8 +2377,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true @@ -2332,8 +2445,8 @@ packages: stream-to-promise@2.2.0: resolution: {integrity: sha512-HAGUASw8NT0k8JvIVutB2Y/9iBk7gpgEyAudXwNJmZERdMITGdajOa4VJfD/kNiA3TppQpTP4J+CtcHwdzKBAw==} - streamx@2.26.0: - resolution: {integrity: sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A==} + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} @@ -2348,8 +2461,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} engines: {node: '>=18'} tar@7.5.7: @@ -2441,8 +2554,8 @@ packages: resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} engines: {node: '>=18.17'} - undici@7.26.0: - resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==} + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} engines: {node: '>=20.18.1'} universalify@2.0.1: @@ -2461,8 +2574,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vercel@54.6.1: - resolution: {integrity: sha512-hZOuNcI5o4Y40ENpyf1bCHpfsxlIzEK3TjMB32i/SHuB4odDgFWlIrITP0RSqvLH41F33Gunl/zSgw0BLK+6ew==} + vercel@54.14.0: + resolution: {integrity: sha512-C753akCiJtBfcH+2kh+n3V7awcw/ESUxLzg6KC+SjDFociFWAm6queymFgILvaa9kHi3ouXosurvqhVtnLrjRA==} engines: {node: '>= 18'} hasBin: true @@ -2782,12 +2895,63 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0 nopt: 8.1.0 - semver: 7.8.1 - tar: 7.5.15 + semver: 7.8.4 + tar: 7.5.16 transitivePeerDependencies: - encoding - supports-color + '@napi-rs/keyring-darwin-arm64@1.2.0': + optional: true + + '@napi-rs/keyring-darwin-x64@1.2.0': + optional: true + + '@napi-rs/keyring-freebsd-x64@1.2.0': + optional: true + + '@napi-rs/keyring-linux-arm-gnueabihf@1.2.0': + optional: true + + '@napi-rs/keyring-linux-arm64-gnu@1.2.0': + optional: true + + '@napi-rs/keyring-linux-arm64-musl@1.2.0': + optional: true + + '@napi-rs/keyring-linux-riscv64-gnu@1.2.0': + optional: true + + '@napi-rs/keyring-linux-x64-gnu@1.2.0': + optional: true + + '@napi-rs/keyring-linux-x64-musl@1.2.0': + optional: true + + '@napi-rs/keyring-win32-arm64-msvc@1.2.0': + optional: true + + '@napi-rs/keyring-win32-ia32-msvc@1.2.0': + optional: true + + '@napi-rs/keyring-win32-x64-msvc@1.2.0': + optional: true + + '@napi-rs/keyring@1.2.0': + optionalDependencies: + '@napi-rs/keyring-darwin-arm64': 1.2.0 + '@napi-rs/keyring-darwin-x64': 1.2.0 + '@napi-rs/keyring-freebsd-x64': 1.2.0 + '@napi-rs/keyring-linux-arm-gnueabihf': 1.2.0 + '@napi-rs/keyring-linux-arm64-gnu': 1.2.0 + '@napi-rs/keyring-linux-arm64-musl': 1.2.0 + '@napi-rs/keyring-linux-riscv64-gnu': 1.2.0 + '@napi-rs/keyring-linux-x64-gnu': 1.2.0 + '@napi-rs/keyring-linux-x64-musl': 1.2.0 + '@napi-rs/keyring-win32-arm64-msvc': 1.2.0 + '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 + '@napi-rs/keyring-win32-x64-msvc': 1.2.0 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -2795,6 +2959,13 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2861,7 +3032,7 @@ snapshots: '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3296,7 +3467,7 @@ snapshots: '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3425,9 +3596,9 @@ snapshots: optionalDependencies: react: 19.2.7 - '@vercel/backends@0.8.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@vercel/backends@0.8.14(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@vercel/build-utils': 13.26.4 + '@vercel/build-utils': 13.30.0 '@vercel/nft': 1.10.0 execa: 3.2.0 fs-extra: 11.1.0 @@ -3454,15 +3625,15 @@ snapshots: throttleit: 2.1.0 undici: 6.26.0 - '@vercel/build-utils@13.26.4': + '@vercel/build-utils@13.30.0': dependencies: '@vercel/python-analysis': 0.11.1 cjs-module-lexer: 1.2.3 es-module-lexer: 1.5.0 - '@vercel/cervel@0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@vercel/cervel@0.1.22(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@vercel/backends': 0.8.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@vercel/backends': 0.8.14(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3470,29 +3641,37 @@ snapshots: - rollup - supports-color - '@vercel/cli-config@0.1.2': + '@vercel/cli-auth@0.3.0': + dependencies: + '@napi-rs/keyring': 1.2.0 + '@vercel/cli-config': 0.2.0 + async-listen: 3.0.0 + open: 8.4.0 + zod: 4.1.11 + + '@vercel/cli-config@0.2.0': dependencies: xdg-app-paths: 5.5.1 zod: 4.1.11 '@vercel/detect-agent@1.2.3': {} - '@vercel/elysia@0.1.84': + '@vercel/elysia@0.1.93': dependencies: - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 transitivePeerDependencies: - encoding - rollup - supports-color - '@vercel/error-utils@2.1.0': {} + '@vercel/error-utils@2.2.0': {} - '@vercel/express@0.1.94(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@vercel/express@0.1.105(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@vercel/cervel': 0.1.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@vercel/cervel': 0.1.22(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@vercel/nft': 1.10.0 - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 fs-extra: 11.1.0 path-to-regexp: 8.3.0 @@ -3505,9 +3684,9 @@ snapshots: - rollup - supports-color - '@vercel/fastify@0.1.87': + '@vercel/fastify@0.1.96': dependencies: - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 transitivePeerDependencies: - encoding @@ -3542,29 +3721,29 @@ snapshots: dependencies: web-vitals: 0.2.4 - '@vercel/gatsby-plugin-vercel-builder@2.2.10': + '@vercel/gatsby-plugin-vercel-builder@2.2.19': dependencies: '@sinclair/typebox': 0.25.24 - '@vercel/build-utils': 13.26.4 + '@vercel/build-utils': 13.30.0 esbuild: 0.27.0 etag: 1.8.1 fs-extra: 11.1.0 - '@vercel/go@3.8.0': {} + '@vercel/go@3.9.0': {} - '@vercel/h3@0.1.93': + '@vercel/h3@0.1.102': dependencies: - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 transitivePeerDependencies: - encoding - rollup - supports-color - '@vercel/hono@0.2.87': + '@vercel/hono@0.2.96': dependencies: '@vercel/nft': 1.10.0 - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 fs-extra: 11.1.0 path-to-regexp: 8.3.0 @@ -3575,30 +3754,30 @@ snapshots: - rollup - supports-color - '@vercel/hydrogen@1.3.8': + '@vercel/hydrogen@1.4.0': dependencies: '@vercel/static-config': 3.4.0 ts-morph: 12.0.0 - '@vercel/koa@0.1.67': + '@vercel/koa@0.1.76': dependencies: - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 transitivePeerDependencies: - encoding - rollup - supports-color - '@vercel/nestjs@0.2.88': + '@vercel/nestjs@0.2.97': dependencies: - '@vercel/node': 5.8.8 + '@vercel/node': 5.8.17 '@vercel/static-config': 3.4.0 transitivePeerDependencies: - encoding - rollup - supports-color - '@vercel/next@4.17.5': + '@vercel/next@4.19.0': dependencies: '@vercel/nft': 1.10.0 transitivePeerDependencies: @@ -3610,8 +3789,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.4.0 - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) + acorn: 8.17.0 + acorn-import-attributes: 1.9.5(acorn@8.17.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -3625,14 +3804,14 @@ snapshots: - rollup - supports-color - '@vercel/node@5.8.8': + '@vercel/node@5.8.17': 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.26.4 - '@vercel/error-utils': 2.1.0 + '@vercel/build-utils': 13.30.0 + '@vercel/error-utils': 2.2.0 '@vercel/nft': 1.10.0 '@vercel/static-config': 3.4.0 async-listen: 3.0.0 @@ -3656,7 +3835,7 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vercel/prepare-flags-definitions@0.2.1': {} + '@vercel/prepare-flags-definitions@0.3.0': {} '@vercel/python-analysis@0.11.1': dependencies: @@ -3668,11 +3847,11 @@ snapshots: smol-toml: 1.5.2 zod: 3.22.4 - '@vercel/python@6.44.0': + '@vercel/python@6.45.0': dependencies: '@vercel/python-analysis': 0.11.1 - '@vercel/redwood@2.4.15': + '@vercel/redwood@2.5.0': dependencies: '@vercel/nft': 1.10.0 '@vercel/static-config': 3.4.0 @@ -3683,9 +3862,9 @@ snapshots: - rollup - supports-color - '@vercel/remix-builder@5.8.4': + '@vercel/remix-builder@5.9.1': dependencies: - '@vercel/error-utils': 2.1.0 + '@vercel/error-utils': 2.2.0 '@vercel/nft': 1.10.0 '@vercel/static-config': 3.4.0 path-to-regexp: 6.1.0 @@ -3703,25 +3882,27 @@ snapshots: execa: 5.1.1 smol-toml: 1.5.2 - '@vercel/sandbox@1.9.0': + '@vercel/sandbox@2.1.1': dependencies: '@vercel/oidc': 3.2.0 + '@workflow/serde': 4.1.0-beta.2 async-retry: 1.3.3 + jose: 6.2.3 jsonlines: 0.1.1 ms: 2.1.3 picocolors: 1.1.1 tar-stream: 3.1.7 - undici: 7.26.0 + undici: 7.27.2 xdg-app-paths: 5.1.0 zod: 3.24.4 transitivePeerDependencies: - bare-abort-controller - react-native-b4a - '@vercel/static-build@2.9.33': + '@vercel/static-build@2.10.3': dependencies: '@vercel/gatsby-plugin-vercel-analytics': 1.0.11 - '@vercel/gatsby-plugin-vercel-builder': 2.2.10 + '@vercel/gatsby-plugin-vercel-builder': 2.2.19 '@vercel/static-config': 3.4.0 ts-morph: 12.0.0 @@ -3736,13 +3917,15 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) + '@workflow/serde@4.1.0-beta.2': {} + abbrev@3.0.1: {} - acorn-import-attributes@1.9.5(acorn@8.16.0): + acorn-import-attributes@1.9.5(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 - acorn@8.16.0: {} + acorn@8.17.0: {} agent-base@6.0.2: dependencies: @@ -3855,7 +4038,7 @@ snapshots: balanced-match@4.0.4: {} - bare-events@2.8.3: {} + bare-events@2.9.1: {} basic-ftp@5.3.1: {} @@ -3975,6 +4158,8 @@ snapshots: decimal.js-light@2.5.1: {} + define-lazy-prop@2.0.0: {} + degenerator@5.0.1: dependencies: ast-types: 0.13.4 @@ -4089,7 +4274,7 @@ snapshots: events-universal@1.0.1: dependencies: - bare-events: 2.8.3 + bare-events: 2.9.1 transitivePeerDependencies: - bare-abort-controller @@ -4300,6 +4485,8 @@ snapshots: is-buffer@2.0.5: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -4314,10 +4501,16 @@ snapshots: is-stream@2.0.1: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isexe@2.0.0: {} jose@5.9.6: {} + jose@6.2.3: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4497,6 +4690,12 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@8.4.0: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + os-paths@4.4.0: {} oxc-transform@0.111.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): @@ -4746,9 +4945,10 @@ snapshots: safer-buffer@2.1.2: {} - sandbox@2.5.6: + sandbox@3.1.2: dependencies: - '@vercel/sandbox': 1.9.0 + '@vercel/sandbox': 2.1.1 + async-retry: 1.3.3 debug: 4.4.3 zod: 4.4.3 transitivePeerDependencies: @@ -4768,7 +4968,7 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.8.1: {} + semver@7.8.4: {} set-cookie-parser@2.7.2: {} @@ -4824,7 +5024,7 @@ snapshots: end-of-stream: 1.1.0 stream-to-array: 2.3.0 - streamx@2.26.0: + streamx@2.28.0: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -4843,12 +5043,12 @@ snapshots: dependencies: b4a: 1.8.1 fast-fifo: 1.3.2 - streamx: 2.26.0 + streamx: 2.28.0 transitivePeerDependencies: - bare-abort-controller - react-native-b4a - tar@7.5.15: + tar@7.5.16: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -4931,7 +5131,7 @@ snapshots: undici@6.26.0: {} - undici@7.26.0: {} + undici@7.27.2: {} universalify@2.0.1: {} @@ -4945,39 +5145,40 @@ snapshots: dependencies: react: 19.2.7 - vercel@54.6.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + vercel@54.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: - '@vercel/backends': 0.8.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@vercel/backends': 0.8.14(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@vercel/blob': 2.4.0 - '@vercel/build-utils': 13.26.4 - '@vercel/cli-config': 0.1.2 + '@vercel/build-utils': 13.30.0 + '@vercel/cli-auth': 0.3.0 + '@vercel/cli-config': 0.2.0 '@vercel/detect-agent': 1.2.3 - '@vercel/elysia': 0.1.84 - '@vercel/express': 0.1.94(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - '@vercel/fastify': 0.1.87 + '@vercel/elysia': 0.1.93 + '@vercel/express': 0.1.105(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@vercel/fastify': 0.1.96 '@vercel/fun': 1.3.0 - '@vercel/go': 3.8.0 - '@vercel/h3': 0.1.93 - '@vercel/hono': 0.2.87 - '@vercel/hydrogen': 1.3.8 - '@vercel/koa': 0.1.67 - '@vercel/nestjs': 0.2.88 - '@vercel/next': 4.17.5 - '@vercel/node': 5.8.8 - '@vercel/prepare-flags-definitions': 0.2.1 - '@vercel/python': 6.44.0 - '@vercel/redwood': 2.4.15 - '@vercel/remix-builder': 5.8.4 + '@vercel/go': 3.9.0 + '@vercel/h3': 0.1.102 + '@vercel/hono': 0.2.96 + '@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/prepare-flags-definitions': 0.3.0 + '@vercel/python': 6.45.0 + '@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.9.33 + '@vercel/static-build': 2.10.3 chokidar: 4.0.0 esbuild: 0.27.0 - form-data: 4.0.5 + form-data: 4.0.6 jose: 5.9.6 luxon: 3.7.2 proxy-agent: 6.4.0 - sandbox: 2.5.6 + sandbox: 3.1.2 smol-toml: 1.5.2 zod: 4.1.11 transitivePeerDependencies: From 9ee5f0f4a956ab0fc42bf81a0643128823e98632 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:43:31 +0000 Subject: [PATCH 17/61] chore(deps): bump antd from 6.4.3 to 6.4.4 in /admin-service (#252) Bumps [antd](https://github.com/ant-design/ant-design) from 6.4.3 to 6.4.4. - [Release notes](https://github.com/ant-design/ant-design/releases) - [Changelog](https://github.com/ant-design/ant-design/blob/master/CHANGELOG.en-US.md) - [Commits](https://github.com/ant-design/ant-design/compare/6.4.3...6.4.4) --- updated-dependencies: - dependency-name: antd dependency-version: 6.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin-service/package.json | 2 +- admin-service/pnpm-lock.yaml | 262 +++++++++++++++++------------------ 2 files changed, 126 insertions(+), 138 deletions(-) diff --git a/admin-service/package.json b/admin-service/package.json index d72dc4fc..8bb982e4 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -15,7 +15,7 @@ "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "^5.101.0", "@vercel/analytics": "^2.0.1", - "antd": "^6.3.7", + "antd": "^6.4.4", "axios": "^1.18.0", "dayjs": "^1.11.21", "dotenv": "^17.4.2", diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index bb513af0..fc8be5c5 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^2.0.1 version: 2.0.1(react@19.2.7) antd: - specifier: ^6.3.7 - version: 6.4.3(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^6.4.4 + version: 6.4.4(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) axios: specifier: ^1.18.0 version: 1.18.0 @@ -112,8 +112,8 @@ packages: '@ant-design/icons-svg@4.4.2': resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} - '@ant-design/icons@6.2.3': - resolution: {integrity: sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==} + '@ant-design/icons@6.2.5': + resolution: {integrity: sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==} engines: {node: '>=8'} peerDependencies: react: '>=16.0.0' @@ -125,8 +125,8 @@ packages: react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} '@bytecodealliance/preview2-shim@0.17.6': @@ -596,12 +596,12 @@ packages: cpu: [x64] os: [win32] - '@rc-component/async-validator@5.1.0': - resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} + '@rc-component/async-validator@6.0.0': + resolution: {integrity: sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==} engines: {node: '>=14.x'} - '@rc-component/cascader@1.15.0': - resolution: {integrity: sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==} + '@rc-component/cascader@1.16.1': + resolution: {integrity: sha512-wxLopwM+EBed0zNNGdnGE4coYoqcO+XD42fHgn+pDvO+XzhNFbdgSlSNXdKocIYqccvqgWvoxDPNb0OVRdi59A==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -624,11 +624,11 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/context@2.0.1': - resolution: {integrity: sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==} + '@rc-component/context@2.0.2': + resolution: {integrity: sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==} peerDependencies: - react: '>=16.9.0' - react-dom: '>=16.9.0' + react: '>=18.0.0' + react-dom: '>=18.0.0' '@rc-component/dialog@1.9.0': resolution: {integrity: sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==} @@ -648,8 +648,8 @@ packages: react: '>=16.11.0' react-dom: '>=16.11.0' - '@rc-component/form@1.8.1': - resolution: {integrity: sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ==} + '@rc-component/form@1.8.3': + resolution: {integrity: sha512-jNkat3uxZ246ELudKwnjQhnDI8+rSxgLxjztvQU3Mrb0G+LwDyOrPu9RNfekOjqU5GQ5QJepi225x+9LhCizJw==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' @@ -667,8 +667,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/input@1.3.0': - resolution: {integrity: sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==} + '@rc-component/input@1.3.1': + resolution: {integrity: sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==} peerDependencies: react: '>=16.0.0' react-dom: '>=16.0.0' @@ -679,18 +679,18 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/menu@1.3.0': - resolution: {integrity: sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==} + '@rc-component/menu@1.3.1': + resolution: {integrity: sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/mini-decimal@1.1.3': - resolution: {integrity: sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==} + '@rc-component/mini-decimal@1.1.4': + resolution: {integrity: sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==} engines: {node: '>=8.x'} - '@rc-component/motion@1.3.2': - resolution: {integrity: sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==} + '@rc-component/motion@1.3.3': + resolution: {integrity: sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -715,8 +715,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/pagination@1.2.0': - resolution: {integrity: sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==} + '@rc-component/pagination@1.3.0': + resolution: {integrity: sha512-12ahTY+HPITg1L2bjWKXUqBJe/oOnpA2QsChdCjthqLVf/e19StiCsv8OLKpWoHbc+8PFEkNjRqRqrLoRBHjFw==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -741,8 +741,8 @@ packages: moment: optional: true - '@rc-component/portal@2.2.0': - resolution: {integrity: sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==} + '@rc-component/portal@2.2.1': + resolution: {integrity: sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==} engines: {node: '>=12.x'} peerDependencies: react: '>=18.0.0' @@ -754,8 +754,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/qrcode@1.1.1': - resolution: {integrity: sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==} + '@rc-component/qrcode@2.0.0': + resolution: {integrity: sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' @@ -780,8 +780,8 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' - '@rc-component/select@1.6.15': - resolution: {integrity: sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==} + '@rc-component/select@1.7.1': + resolution: {integrity: sha512-GZ1cMJk2xQh0VHyOQjjG8drYL4iu24NcbkXioUcReQOCUr+ub/3fmRonZe6cRPEZhWMbJdeHsqnEltogDaZ5Tg==} engines: {node: '>=8.x'} peerDependencies: react: '*' @@ -807,15 +807,15 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/table@1.10.0': - resolution: {integrity: sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==} + '@rc-component/table@1.10.2': + resolution: {integrity: sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==} engines: {node: '>=8.x'} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@rc-component/tabs@1.9.0': - resolution: {integrity: sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==} + '@rc-component/tabs@1.9.1': + resolution: {integrity: sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' @@ -834,28 +834,28 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/tree-select@1.9.0': - resolution: {integrity: sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==} + '@rc-component/tree-select@1.10.0': + resolution: {integrity: sha512-E1U4pn2LAbXEhLJdzIzid7WYbIuFbkTIctuFoeC6weppf8UbPR3+YYB6/ay0c0ksand4gXMRQpa1Z60Auo7VJA==} peerDependencies: react: '*' react-dom: '*' - '@rc-component/tree@1.3.1': - resolution: {integrity: sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==} + '@rc-component/tree@1.3.2': + resolution: {integrity: sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==} engines: {node: '>=10.x'} peerDependencies: react: '*' react-dom: '*' - '@rc-component/trigger@3.9.0': - resolution: {integrity: sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==} + '@rc-component/trigger@3.9.1': + resolution: {integrity: sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==} engines: {node: '>=8.x'} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@rc-component/upload@1.1.0': - resolution: {integrity: sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==} + '@rc-component/upload@1.1.1': + resolution: {integrity: sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -1350,8 +1350,8 @@ packages: ajv@8.6.3: resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} - antd@6.4.3: - resolution: {integrity: sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==} + antd@6.4.4: + resolution: {integrity: sha512-lgPz4KhfhiYddV/qPYo0ieqWimCVgV2OQF72mbeGNixE753JWNnmEc7UNGy08wBS/zZ7hxrmX0pc5aX7EUaIIg==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -1725,10 +1725,6 @@ packages: debug: optional: true - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -2711,14 +2707,14 @@ snapshots: '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@ant-design/cssinjs': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) '@ant-design/cssinjs@2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2732,7 +2728,7 @@ snapshots: '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons@6.2.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@ant-design/icons@6.2.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@ant-design/colors': 8.0.1 '@ant-design/icons-svg': 4.4.2 @@ -2743,14 +2739,14 @@ snapshots: '@ant-design/react-slick@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 clsx: 2.1.1 json2mq: 0.2.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) throttle-debounce: 5.0.2 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} '@bytecodealliance/preview2-shim@0.17.6': {} @@ -3047,14 +3043,14 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.111.0': optional: true - '@rc-component/async-validator@5.1.0': + '@rc-component/async-validator@6.0.0': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 - '@rc-component/cascader@1.15.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/cascader@1.16.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/select': 1.7.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3069,8 +3065,8 @@ snapshots: '@rc-component/collapse@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@babel/runtime': 7.29.7 + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3084,7 +3080,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/context@2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/context@2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 @@ -3092,8 +3088,8 @@ snapshots: '@rc-component/dialog@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3101,8 +3097,8 @@ snapshots: '@rc-component/drawer@1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3110,15 +3106,15 @@ snapshots: '@rc-component/dropdown@1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/form@1.8.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/form@1.8.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/async-validator': 5.1.0 + '@rc-component/async-validator': 6.0.0 '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3126,8 +3122,8 @@ snapshots: '@rc-component/image@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3135,13 +3131,13 @@ snapshots: '@rc-component/input-number@1.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/mini-decimal': 1.1.3 + '@rc-component/mini-decimal': 1.1.4 '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/input@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/input@1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -3151,29 +3147,29 @@ snapshots: '@rc-component/mentions@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/input': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/input': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/menu@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/menu@1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/mini-decimal@1.1.3': + '@rc-component/mini-decimal@1.1.4': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 - '@rc-component/motion@1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/motion@1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3188,7 +3184,7 @@ snapshots: '@rc-component/notification@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3196,14 +3192,14 @@ snapshots: '@rc-component/overflow@1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/pagination@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/pagination@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3214,7 +3210,7 @@ snapshots: dependencies: '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3223,7 +3219,7 @@ snapshots: dayjs: 1.11.21 luxon: 3.7.2 - '@rc-component/portal@2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/portal@2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3237,9 +3233,9 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/qrcode@1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/qrcode@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -3258,17 +3254,17 @@ snapshots: '@rc-component/segmented@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@babel/runtime': 7.29.7 + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/select@1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/select@1.7.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/overflow': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3296,9 +3292,9 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/table@1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/table@1.10.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/context': 2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/context': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -3306,11 +3302,11 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/tabs@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/tabs@1.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/dropdown': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3319,7 +3315,7 @@ snapshots: '@rc-component/tooltip@1.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 @@ -3327,42 +3323,42 @@ snapshots: '@rc-component/tour@2.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/tree-select@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/tree-select@1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/select': 1.7.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/tree@1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/tree@1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/virtual-list': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/trigger@3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/trigger@3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/portal': 2.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/portal': 2.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rc-component/upload@1.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@rc-component/upload@1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3378,7 +3374,7 @@ snapshots: '@rc-component/virtual-list@1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 @@ -3942,50 +3938,50 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 - antd@6.4.3(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + antd@6.4.4(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@ant-design/colors': 8.0.1 '@ant-design/cssinjs': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ant-design/fast-color': 3.0.1 - '@ant-design/icons': 6.2.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ant-design/icons': 6.2.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ant-design/react-slick': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@babel/runtime': 7.29.2 - '@rc-component/cascader': 1.15.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@babel/runtime': 7.29.7 + '@rc-component/cascader': 1.16.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/checkbox': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/collapse': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/color-picker': 3.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/dialog': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/drawer': 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/dropdown': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/form': 1.8.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/form': 1.8.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/image': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/input': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/input': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/input-number': 1.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/mentions': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/menu': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/motion': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/menu': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/motion': 1.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/notification': 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/pagination': 1.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/pagination': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/picker': 1.10.0(dayjs@1.11.21)(luxon@3.7.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/progress': 1.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/qrcode': 1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/qrcode': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/rate': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/segmented': 1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/select': 1.6.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/select': 1.7.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/slider': 1.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/steps': 1.2.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/switch': 1.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/table': 1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/tabs': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/table': 1.10.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tabs': 1.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/tooltip': 1.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/tour': 2.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/tree': 1.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/tree-select': 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/trigger': 3.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@rc-component/upload': 1.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree': 1.3.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/tree-select': 1.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/trigger': 3.9.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@rc-component/upload': 1.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rc-component/util': 1.11.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) clsx: 2.1.1 dayjs: 1.11.21 @@ -4337,14 +4333,6 @@ snapshots: follow-redirects@1.16.0: {} - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.4 - mime-types: 2.1.35 - form-data@4.0.6: dependencies: asynckit: 0.4.0 From 7f13600ea39e5a84ad74d2bad6335a8a1e456604 Mon Sep 17 00:00:00 2001 From: InfinityZero3000 Date: Mon, 15 Jun 2026 23:52:16 +0700 Subject: [PATCH 18/61] feat(backend-service): update vocabulary import scripts and docker mounts for new schema --- backend-service/app/check_missing.py | 12 +-- backend-service/app/import_json_to_db.py | 52 +++++++++--- .../scripts/check_import_status.py | 4 +- backend-service/scripts/import_json_to_db.py | 85 ++++++++++++++----- docker-compose.yml | 1 + 5 files changed, 116 insertions(+), 38 deletions(-) diff --git a/backend-service/app/check_missing.py b/backend-service/app/check_missing.py index 8b93adde..92d5d469 100644 --- a/backend-service/app/check_missing.py +++ b/backend-service/app/check_missing.py @@ -9,16 +9,18 @@ from app.models.vocabulary import VocabularyItem, PartOfSpeech from app.core.config import settings -INPUT_FILE = "/tmp/vocabulary_import.json" +INPUT_FILE = "/app/data/vocabulary_import.json" def guess_pos(word, defn): - if " v." in defn or " verb" in defn or word.startswith("to "): + # Remove Vietnamese "v.v." / "v. v." to prevent false verb matching on " v." + clean_defn = defn.replace("v.v.", "").replace("v. v.", "") + if " v." in clean_defn or " verb" in clean_defn or word.startswith("to "): return PartOfSpeech.VERB - if " adj." in defn or " adj " in defn: + if " adj." in clean_defn or " adj " in clean_defn: return PartOfSpeech.ADJECTIVE - if " adv." in defn or " adv " in defn: + if " adv." in clean_defn or " adv " in clean_defn: return PartOfSpeech.ADVERB - if " phrase" in defn or " idiom" in defn or " " in word: + if " phrase" in clean_defn or " idiom" in clean_defn or " " in word: return PartOfSpeech.PHRASE return PartOfSpeech.NOUN diff --git a/backend-service/app/import_json_to_db.py b/backend-service/app/import_json_to_db.py index 61ded68c..151e3fff 100644 --- a/backend-service/app/import_json_to_db.py +++ b/backend-service/app/import_json_to_db.py @@ -13,17 +13,19 @@ from app.models.vocabulary import VocabularyItem, PartOfSpeech, DifficultyLevel from app.core.config import settings -INPUT_FILE = "/tmp/vocabulary_import.json" +INPUT_FILE = "/app/data/vocabulary_import.json" def guess_pos(word, defn): # Basic guessing from Anki info - if " v." in defn or " verb" in defn or word.startswith("to "): + # Remove Vietnamese "v.v." / "v. v." to prevent false verb matching on " v." + clean_defn = defn.replace("v.v.", "").replace("v. v.", "") + if " v." in clean_defn or " verb" in clean_defn or word.startswith("to "): return PartOfSpeech.VERB - if " adj." in defn or " adj " in defn: + if " adj." in clean_defn or " adj " in clean_defn: return PartOfSpeech.ADJECTIVE - if " adv." in defn or " adv " in defn: + if " adv." in clean_defn or " adv " in clean_defn: return PartOfSpeech.ADVERB - if " phrase" in defn or " idiom" in defn or " " in word: + if " phrase" in clean_defn or " idiom" in clean_defn or " " in word: return PartOfSpeech.PHRASE return PartOfSpeech.NOUN @@ -56,8 +58,14 @@ async def main(): # Parse additional info audios = item.get('audios', {}) images = item.get('images', '') + trans_dict = item.get('translation', {}) + if not isinstance(trans_dict, dict): + trans_dict = {} + if "vi" not in trans_dict or not trans_dict["vi"]: + trans_dict["vi"] = defn + translation = { - "vi": defn, + **trans_dict, "examples": [example] if example else [], "images": images if images else [], "audios": audios if audios else {} @@ -71,6 +79,20 @@ async def main(): elif isinstance(audios, list) and audios: audio_url = f"/media/{audios[0]}" + # Get difficulty level from JSON or fall back to A1 + level_str = item.get('difficulty_level', 'A1') + try: + difficulty_level = DifficultyLevel(level_str) + except ValueError: + difficulty_level = DifficultyLevel.A1 + + # Parse tags + tags_raw = item.get('tags', "general") + if isinstance(tags_raw, str): + tags = [t.strip() for t in tags_raw.split(',') if t.strip()] + else: + tags = tags_raw if isinstance(tags_raw, list) else ["general"] + db_item = dict( id=uuid.uuid4(), word=word, @@ -79,16 +101,26 @@ async def main(): pronunciation=phonetic[:100] if phonetic else None, audio_url=audio_url, part_of_speech=guess_pos(word, defn), - difficulty_level=DifficultyLevel.A1, # default placeholder - tags=item.get('tags', ["general"]) + difficulty_level=difficulty_level, + tags=tags ) items.append(db_item) from sqlalchemy.dialects.postgresql import insert if items: stmt = insert(VocabularyItem).values(items) - # Ignore duplicates based on word + part_of_speech - stmt = stmt.on_conflict_do_nothing(index_elements=['word', 'part_of_speech']) + # Update existing items with refined definitions, translations, levels, tags, etc. + stmt = stmt.on_conflict_do_update( + index_elements=['word', 'part_of_speech'], + set_={ + 'definition': stmt.excluded.definition, + 'translation': stmt.excluded.translation, + 'pronunciation': stmt.excluded.pronunciation, + 'audio_url': stmt.excluded.audio_url, + 'difficulty_level': stmt.excluded.difficulty_level, + 'tags': stmt.excluded.tags + } + ) await session.execute(stmt) await session.commit() diff --git a/backend-service/scripts/check_import_status.py b/backend-service/scripts/check_import_status.py index f52b3e3c..28df07aa 100644 --- a/backend-service/scripts/check_import_status.py +++ b/backend-service/scripts/check_import_status.py @@ -12,10 +12,10 @@ async def main(): try: - with open("/app/vocabulary_import.json", "r", encoding="utf-8") as f: + with open("/app/data/vocabulary_import.json", "r", encoding="utf-8") as f: data = json.load(f) except FileNotFoundError: - print("Error: /app/vocabulary_import.json not found in the container.") + print("Error: /app/data/vocabulary_import.json not found in the container.") return except Exception as e: print(f"Error reading JSON: {e}") diff --git a/backend-service/scripts/import_json_to_db.py b/backend-service/scripts/import_json_to_db.py index 91230313..8f9eaf11 100644 --- a/backend-service/scripts/import_json_to_db.py +++ b/backend-service/scripts/import_json_to_db.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone # Add parent directory to Python path -sys.path.append("/opt/lexilingo/backend-service") +sys.path.append("/app") from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker @@ -13,17 +13,18 @@ from app.models.vocabulary import VocabularyItem, PartOfSpeech, DifficultyLevel from app.core.config import settings -INPUT_FILE = "/app/data/categorized_words_final.json" +INPUT_FILE = "/app/data/vocabulary_import.json" def guess_pos(word, defn): - # Basic guessing from Anki info - if " v." in defn or " verb" in defn or word.startswith("to "): + # Remove Vietnamese "v.v." / "v. v." to prevent false verb matching on " v." + clean_defn = defn.replace("v.v.", "").replace("v. v.", "") + if " v." in clean_defn or " verb" in clean_defn or word.startswith("to "): return PartOfSpeech.VERB - if " adj." in defn or " adj " in defn: + if " adj." in clean_defn or " adj " in clean_defn: return PartOfSpeech.ADJECTIVE - if " adv." in defn or " adv " in defn: + if " adv." in clean_defn or " adv " in clean_defn: return PartOfSpeech.ADVERB - if " phrase" in defn or " idiom" in defn or " " in word: + if " phrase" in clean_defn or " idiom" in clean_defn or " " in word: return PartOfSpeech.PHRASE return PartOfSpeech.NOUN @@ -33,8 +34,7 @@ async def main(): # Database setup # PostgreSQL URI from settings or .env - # Let's read MONGODB_URI/Postgres URI - engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URI, echo=False) + engine = create_async_engine(settings.DATABASE_URL, echo=False) AsyncSessionLocal = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) @@ -54,19 +54,46 @@ async def main(): phonetic = item.get('phonetic', '') # Parse additional info + audios = item.get('audios', {}) + images = item.get('images', '') + + # Get the existing translations dictionary from the JSON item if it exists + trans_dict = item.get('translation', {}) + if not isinstance(trans_dict, dict): + trans_dict = {} + if "vi" not in trans_dict or not trans_dict["vi"]: + trans_dict["vi"] = defn + translation = { - "vi": defn, + **trans_dict, "examples": [example] if example else [], - "images": item.get('images', []), - "audios": item.get('audios', []) + "images": images if images else [], + "audios": audios if audios else {} } - + audio_url = None - if item.get('audios'): - audio_url = f"/media/{item['audios'][0]}" + if isinstance(audios, dict): + pronunciation = audios.get('pronunciation') + if pronunciation: + audio_url = f"/media/{pronunciation}" + elif isinstance(audios, list) and audios: + audio_url = f"/media/{audios[0]}" - # Create the DB object - db_item = VocabularyItem( + # Get difficulty level from JSON or fall back to A1 + level_str = item.get('difficulty_level', 'A1') + try: + difficulty_level = DifficultyLevel(level_str) + except ValueError: + difficulty_level = DifficultyLevel.A1 + + # Parse tags + tags_raw = item.get('tags', "general") + if isinstance(tags_raw, str): + tags = [t.strip() for t in tags_raw.split(',') if t.strip()] + else: + tags = tags_raw if isinstance(tags_raw, list) else ["general"] + + db_item = dict( id=uuid.uuid4(), word=word, definition=defn, @@ -74,13 +101,29 @@ async def main(): pronunciation=phonetic[:100] if phonetic else None, audio_url=audio_url, part_of_speech=guess_pos(word, defn), - difficulty_level=DifficultyLevel.A1, # default placeholder - tags=item.get('tags', ["general"]) + difficulty_level=difficulty_level, + tags=tags ) items.append(db_item) - session.add_all(items) - await session.commit() + from sqlalchemy.dialects.postgresql import insert + if items: + stmt = insert(VocabularyItem).values(items) + # Update existing items with refined definitions, translations, levels, tags, etc. + stmt = stmt.on_conflict_do_update( + index_elements=['word', 'part_of_speech'], + set_={ + 'definition': stmt.excluded.definition, + 'translation': stmt.excluded.translation, + 'pronunciation': stmt.excluded.pronunciation, + 'audio_url': stmt.excluded.audio_url, + 'difficulty_level': stmt.excluded.difficulty_level, + 'tags': stmt.excluded.tags + } + ) + await session.execute(stmt) + await session.commit() + total += len(items) print(f"Imported {total} / {len(data)}") diff --git a/docker-compose.yml b/docker-compose.yml index cbac5f37..f69b6600 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -244,6 +244,7 @@ services: - ./backend-service/app:/app/app - ./backend-service/alembic:/app/alembic - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro + - ./backend-service/data:/app/data command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --proxy-headers --forwarded-allow-ips="*" networks: - lexilingo-network From 78dc6032a9400fe6b7e42db961eeeff256210aab Mon Sep 17 00:00:00 2001 From: InfinityZero3000 Date: Mon, 15 Jun 2026 23:56:29 +0700 Subject: [PATCH 19/61] feat(backend-service): add vocabulary importing, expansion, and helper scripts --- backend-service/app/test_vocab_definitions.py | 18 + backend-service/scripts/expand_vocabulary.py | 338 ++++++++++++++++++ backend-service/scripts/fetch_audios.py | 176 +++++++++ backend-service/scripts/fetch_definitions.py | 100 ++++++ backend-service/scripts/fill_with_groq.py | 197 ++++++++++ .../scripts/fix_vocabulary_import.py | 276 ++++++++++++++ backend-service/scripts/refine_vocabulary.py | 257 +++++++++++++ .../scripts/restore_vietnamese_accents.py | 207 +++++++++++ backend-service/scripts/run_tasks.sh | 26 ++ backend-service/scripts/test_wiktionary.py | 35 ++ .../scripts/translate_vocabulary.py | 96 +++++ 11 files changed, 1726 insertions(+) create mode 100644 backend-service/app/test_vocab_definitions.py create mode 100644 backend-service/scripts/expand_vocabulary.py create mode 100644 backend-service/scripts/fetch_audios.py create mode 100644 backend-service/scripts/fetch_definitions.py create mode 100644 backend-service/scripts/fill_with_groq.py create mode 100755 backend-service/scripts/fix_vocabulary_import.py create mode 100755 backend-service/scripts/refine_vocabulary.py create mode 100755 backend-service/scripts/restore_vietnamese_accents.py create mode 100755 backend-service/scripts/run_tasks.sh create mode 100644 backend-service/scripts/test_wiktionary.py create mode 100644 backend-service/scripts/translate_vocabulary.py diff --git a/backend-service/app/test_vocab_definitions.py b/backend-service/app/test_vocab_definitions.py new file mode 100644 index 00000000..0ceec92e --- /dev/null +++ b/backend-service/app/test_vocab_definitions.py @@ -0,0 +1,18 @@ +import asyncio +import sys +sys.path.insert(0, "/app") +from sqlalchemy import select +from app.core.database import engine +from app.models.vocabulary import VocabularyItem + +async def main(): + async with engine.connect() as conn: + for w in ['idiom', 'lexicon']: + res = await conn.execute(select(VocabularyItem.word, VocabularyItem.definition, VocabularyItem.part_of_speech).where(VocabularyItem.word == w)) + for row in res.all(): + print(f"Word: {row[0]}, POS: {row[2]}") + print(f"Def: {row[1]}") + print("-" * 30) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend-service/scripts/expand_vocabulary.py b/backend-service/scripts/expand_vocabulary.py new file mode 100644 index 00000000..c7084328 --- /dev/null +++ b/backend-service/scripts/expand_vocabulary.py @@ -0,0 +1,338 @@ +import json +import os +import urllib.request +import urllib.parse +import urllib.error +import ssl +import time +import re + +JSON_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" +MEDIA_DIR = "/opt/lexilingo/backend-service/data/media" + +from dotenv import load_dotenv +from pathlib import Path + +# Load env variables +PROJECT_ROOT = Path(__file__).parent.parent.parent +load_dotenv(PROJECT_ROOT / ".env") +if os.getenv("APP_ENV", "").lower() == "production": + load_dotenv(PROJECT_ROOT / ".env.production", override=False) + +raw_keys = os.getenv("GROQ_API_KEYS", "").strip() +API_KEYS = [k.strip() for k in raw_keys.split(",") if k.strip()] if raw_keys else [] +if not API_KEYS: + single = os.getenv("GROQ_API_KEY", "").strip() + if single: + API_KEYS = [single] + +if not API_KEYS: + raise ValueError("Neither GROQ_API_KEYS nor GROQ_API_KEY is configured in the environment.") + +current_key_index = 0 + +def get_next_api_key(): + global current_key_index + key = API_KEYS[current_key_index] + current_key_index = (current_key_index + 1) % len(API_KEYS) + return key + +def clean_filename(word): + cleaned = re.sub(r"[^\w\-_]", "", word) + return cleaned.lower() + +def download_audio(url, dest_path): + if url.startswith("//"): + url = "https:" + url + headers = {"User-Agent": "Mozilla/5.0"} + req = urllib.request.Request(url, headers=headers) + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + with open(dest_path, "wb") as f: + f.write(response.read()) + return True + except Exception as e: + print(f" Failed to download audio from {url}: {e}") + return False + +def get_audio_and_phonetic_from_api(word): + url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{urllib.parse.quote(word)}" + headers = {"User-Agent": "Mozilla/5.0"} + req = urllib.request.Request(url, headers=headers) + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + data = json.loads(response.read().decode("utf-8")) + if data and isinstance(data, list): + phonetics = data[0].get("phonetics", []) + audio_url = None + phonetic_text = data[0].get("phonetic", "") + + # Try to find phonetic text in entries + for p in phonetics: + if not phonetic_text and p.get("text"): + phonetic_text = p.get("text") + if p.get("audio"): + if not audio_url or "-us" in p.get("audio") or "us.mp3" in p.get("audio"): + audio_url = p.get("audio") + return phonetic_text, audio_url + except Exception: + pass + return None, None + +def call_groq(payload): + url = "https://api.groq.com/openai/v1/chat/completions" + for _ in range(len(API_KEYS) * 2): + api_key = get_next_api_key() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST") + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + res_data = json.loads(response.read().decode("utf-8")) + return res_data["choices"][0]["message"]["content"] + except urllib.error.HTTPError as e: + time.sleep(0.5) + except Exception: + time.sleep(0.5) + time.sleep(2.0) + return None + +def generate_words_for_level(level, level_type, count, existing_words): + print(f"Generating list of {count} words for {level_type} level {level}...") + prompt = ( + f"Generate a JSON list of exactly {count * 2} common, high-quality, practical English words " + f"suitable for {level_type} level {level}. " + f"Return ONLY a raw JSON list of strings, e.g. [\"word1\", \"word2\"]. No extra markdown, explanation, or tags." + ) + payload = { + "model": "llama-3.3-70b-versatile", + "messages": [ + {"role": "system", "content": "You are a vocabulary builder. Output ONLY raw JSON lists of strings."}, + {"role": "user", "content": prompt} + ], + "temperature": 0.7 + } + + res = call_groq(payload) + if not res: + return [] + + try: + # Strip any markdown code block wraps + res_clean = res.strip() + if res_clean.startswith("```json"): + res_clean = res_clean[7:] + if res_clean.startswith("```"): + res_clean = res_clean[3:] + if res_clean.endswith("```"): + res_clean = res_clean[:-3] + res_clean = res_clean.strip() + + words = json.loads(res_clean) + # Filter duplicates + filtered = [] + for w in words: + w_clean = w.strip().lower() + if w_clean and w_clean not in existing_words and w_clean not in filtered: + filtered.append(w_clean) + return filtered[:count] + except Exception as e: + print(f"Failed to parse word list for level {level}: {e}. Response was: {res}") + return [] + +def fetch_details_for_word(word, level, ielts_band=None): + print(f"Fetching translations and details for word '{word}'...") + prompt = ( + f"Provide translation and example details for the English word '{word}'.\n" + f"Format your response as a strict JSON object with the following fields:\n" + f"{{\n" + f" \"definition\": \"A concise, clear English definition suitable for language learners\",\n" + f" \"example\": \"A natural, practical English example sentence using the word '{word}'\",\n" + f" \"phonetic\": \"IPA phonetic spelling, e.g. /fəˈnɛtɪk/\",\n" + f" \"part_of_speech\": \"noun/verb/adjective/adverb/pronoun/preposition/conjunction/interjection/phrase\",\n" + f" \"tags\": \"one relevant thematic category like technology, business, food, health, travel, daily_life, science\",\n" + f" \"translation\": {{\n" + f" \"en\": \"synonym or simple English translation\",\n" + f" \"vi\": \"Vietnamese translation\",\n" + f" \"ja\": \"Japanese translation\",\n" + f" \"ko\": \"Korean translation\",\n" + f" \"zh\": \"Chinese translation\",\n" + f" \"fr\": \"French translation\",\n" + f" \"es\": \"Spanish translation\"\n" + f" }}\n" + f"}}\n" + f"Return ONLY the raw JSON object. No explanation, quotes, or markdown wrappers." + ) + + payload = { + "model": "llama-3.3-70b-versatile", + "messages": [ + {"role": "system", "content": "You are a lexicographer. Output ONLY raw JSON objects matching the schema."}, + {"role": "user", "content": prompt} + ], + "temperature": 0.0 + } + + res = call_groq(payload) + if not res: + return None + + try: + res_clean = res.strip() + if res_clean.startswith("```json"): + res_clean = res_clean[7:] + if res_clean.startswith("```"): + res_clean = res_clean[3:] + if res_clean.endswith("```"): + res_clean = res_clean[:-3] + res_clean = res_clean.strip() + + details = json.loads(res_clean) + return details + except Exception as e: + print(f"Failed to parse details for '{word}': {e}. Response: {res}") + return None + +def main(): + if not os.path.exists(MEDIA_DIR): + os.makedirs(MEDIA_DIR, exist_ok=True) + + print("Loading existing vocabulary...") + with open(JSON_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + + existing_words = set(item["word"].lower().strip() for item in data) + max_index = max(item.get("index", 0) for item in data) + + # Levels configuration + cefr_levels = ["A1", "A2", "B1", "B2", "C1", "C2"] + ielts_levels = ["1.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0"] + + words_to_generate_cefr = 10 # words per CEFR level + words_to_generate_ielts = 5 # words per IELTS level + + new_items = [] + + # 1. Generate CEFR words + for level in cefr_levels: + words = generate_words_for_level(level, "CEFR", words_to_generate_cefr, existing_words) + for w in words: + existing_words.add(w) # prevent duplicates in same run + + # Fetch details + details = fetch_details_for_word(w, level) + if not details: + continue + + # Dictionary API check + api_phonetic, api_audio_url = get_audio_and_phonetic_from_api(w) + + phonetic = api_phonetic if api_phonetic else details.get("phonetic", "") + audio_filename = "" + + # Download audio if available + if api_audio_url: + ext = ".wav" if ".wav" in api_audio_url.lower() else ".mp3" + filename = f"{clean_filename(w)}{ext}" + dest_path = os.path.join(MEDIA_DIR, filename) + print(f" Downloading pronunciation from Dictionary API: {api_audio_url}") + if download_audio(api_audio_url, dest_path): + audio_filename = filename + + # Construct tag string + tags = details.get("tags", "general") + # Append CEFR level to tags + tags = f"{tags},cefr_{level}" + + max_index += 1 + item = { + "word": w, + "definition": details.get("definition", ""), + "example": details.get("example", ""), + "phonetic": phonetic, + "audios": {"pronunciation": audio_filename} if audio_filename else {}, + "images": "", + "index": max_index, + "tags": tags, + "difficulty_level": level, + "translation": details.get("translation", {}) + } + new_items.append(item) + print(f" Successfully added CEFR {level} word '{w}'") + time.sleep(0.5) + + # 2. Generate IELTS words + for ielts in ielts_levels: + # Map IELTS to closest CEFR difficulty level + # IELTS 1.0 - 2.0 -> A1, 3.0 -> A2, 4.0 -> B1, 5.0 - 6.0 -> B2, 7.0 -> C1, 8.0 - 9.0 -> C2 + val = float(ielts) + if val <= 2.0: + cefr_mapped = "A1" + elif val <= 3.5: + cefr_mapped = "A2" + elif val <= 4.5: + cefr_mapped = "B1" + elif val <= 6.0: + cefr_mapped = "B2" + elif val <= 7.5: + cefr_mapped = "C1" + else: + cefr_mapped = "C2" + + words = generate_words_for_level(ielts, "IELTS", words_to_generate_ielts, existing_words) + for w in words: + existing_words.add(w) + + details = fetch_details_for_word(w, cefr_mapped, ielts) + if not details: + continue + + api_phonetic, api_audio_url = get_audio_and_phonetic_from_api(w) + phonetic = api_phonetic if api_phonetic else details.get("phonetic", "") + audio_filename = "" + + if api_audio_url: + ext = ".wav" if ".wav" in api_audio_url.lower() else ".mp3" + filename = f"{clean_filename(w)}{ext}" + dest_path = os.path.join(MEDIA_DIR, filename) + print(f" Downloading pronunciation: {api_audio_url}") + if download_audio(api_audio_url, dest_path): + audio_filename = filename + + tags = details.get("tags", "general") + # Append CEFR level and IELTS band to tags + tags = f"{tags},cefr_{cefr_mapped},ielts_{ielts}" + + max_index += 1 + item = { + "word": w, + "definition": details.get("definition", ""), + "example": details.get("example", ""), + "phonetic": phonetic, + "audios": {"pronunciation": audio_filename} if audio_filename else {}, + "images": "", + "index": max_index, + "tags": tags, + "difficulty_level": cefr_mapped, + "translation": details.get("translation", {}) + } + new_items.append(item) + print(f" Successfully added IELTS {ielts} (CEFR {cefr_mapped}) word '{w}'") + time.sleep(0.5) + + if new_items: + data.extend(new_items) + with open(JSON_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(f"Successfully added {len(new_items)} new vocabulary items to {JSON_PATH}!") + else: + print("No new vocabulary items were generated.") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/fetch_audios.py b/backend-service/scripts/fetch_audios.py new file mode 100644 index 00000000..801d7b9a --- /dev/null +++ b/backend-service/scripts/fetch_audios.py @@ -0,0 +1,176 @@ +import json +import os +import urllib.request +import urllib.parse +import urllib.error +import ssl +import time +import re + +JSON_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" +MEDIA_DIR = "/opt/lexilingo/backend-service/data/media" + +def clean_filename(word): + # Remove any character that is not alphanumeric or underscore/dash + cleaned = re.sub(r"[^\w\-_]", "", word) + return cleaned.lower() + +def download_audio_from_url(url, dest_path): + if url.startswith("//"): + url = "https:" + url + + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + req = urllib.request.Request(url, headers=headers) + + max_retries = 3 + base_delay = 2.0 + + for attempt in range(max_retries): + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + with open(dest_path, "wb") as f: + f.write(response.read()) + return True + except urllib.error.HTTPError as e: + if e.code == 429: + delay = base_delay * (2 ** attempt) + print(f"Rate limited (429) downloading audio. Retrying in {delay} seconds...") + time.sleep(delay) + else: + print(f"HTTP error downloading audio from {url}: {e.code}") + return False + except Exception as e: + print(f"Error downloading audio from {url}: {e}") + time.sleep(1.0) + + return False + +def get_audio_url_from_api(word): + url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{urllib.parse.quote(word)}" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + req = urllib.request.Request(url, headers=headers) + + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + data = json.loads(response.read().decode("utf-8")) + if data and isinstance(data, list): + # Search for any valid audio link in phonetics + phonetics = data[0].get("phonetics", []) + # First try finding US audio, then any audio + for p in phonetics: + audio_url = p.get("audio") + if audio_url and ("-us" in audio_url or "us.mp3" in audio_url): + return audio_url + for p in phonetics: + audio_url = p.get("audio") + if audio_url: + return audio_url + except urllib.error.HTTPError as e: + if e.code == 404: + # Word not found + return None + print(f"API HTTP Error {e.code} for word '{word}'") + except Exception as e: + print(f"API Error fetching word '{word}': {e}") + + return None + +def main(): + if not os.path.exists(MEDIA_DIR): + os.makedirs(MEDIA_DIR, exist_ok=True) + print(f"Created media directory: {MEDIA_DIR}") + + print("Loading vocabulary JSON...") + with open(JSON_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + + # Count missing audios + to_download = [] + for idx, item in enumerate(data): + word = item.get("word") + if not word: + continue + + audios = item.get("audios", {}) + pronunciation_file = None + if isinstance(audios, dict): + pronunciation_file = audios.get("pronunciation") + elif isinstance(audios, list) and audios: + pronunciation_file = audios[0] + + audio_path = os.path.join(MEDIA_DIR, pronunciation_file) if pronunciation_file else None + + # If pronunciation is not configured, or file does not exist on disk + if not pronunciation_file or not os.path.exists(audio_path): + to_download.append(idx) + + print(f"Total items in JSON: {len(data)}") + print(f"Total items needing audio download: {len(to_download)}") + + if not to_download: + print("All vocabulary audio files are already present on disk!") + return + + downloaded_count = 0 + failed_count = 0 + + # We will only attempt to fetch up to a reasonable number to avoid hitting API limits + # e.g., 200 items in a single run. Let's make it configurable or fetch them. + # Since this is /goal, we can let it run to process all of them, but we will print progress. + # For DictionaryAPI.dev, there are no strict keys, but rate limits may apply. + # We will sleep 0.5s between requests. + + for count, idx in enumerate(to_download): + item = data[idx] + word = item.get("word") + + print(f"[{count+1}/{len(to_download)}] Fetching audio for '{word}'...") + audio_url = get_audio_url_from_api(word) + + if audio_url: + # Determine extension + ext = ".mp3" + if ".wav" in audio_url.lower(): + ext = ".wav" + + filename = f"{clean_filename(word)}{ext}" + dest_path = os.path.join(MEDIA_DIR, filename) + + print(f" Downloading from: {audio_url}") + success = download_audio_from_url(audio_url, dest_path) + + if success: + item["audios"] = {"pronunciation": filename} + downloaded_count += 1 + print(f" Successfully saved audio as '{filename}'") + + # Checkpoint save + if downloaded_count % 10 == 0: + with open(JSON_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(" Progress checkpoint saved.") + else: + failed_count += 1 + print(f" Failed to download audio file.") + else: + failed_count += 1 + print(f" No audio URL found in dictionary API.") + + time.sleep(0.5) # respectful delay + + # Final save + if downloaded_count > 0: + with open(JSON_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("Final updates saved.") + + print(f"Completed audio download task. Successes: {downloaded_count}, Failures/Not Found: {failed_count}") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/fetch_definitions.py b/backend-service/scripts/fetch_definitions.py new file mode 100644 index 00000000..896d1733 --- /dev/null +++ b/backend-service/scripts/fetch_definitions.py @@ -0,0 +1,100 @@ +import json +import urllib.request +import urllib.parse +import re +import time + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" + +def strip_html(text): + # Remove HTML tags + clean = re.compile('<.*?>') + return re.sub(clean, '', text) + +def get_wiktionary_definition(word): + url = f"https://en.wiktionary.org/api/rest_v1/page/definition/{urllib.parse.quote(word)}" + req = urllib.request.Request(url, headers={ + 'User-Agent': 'LexiLingo-VocabBot/1.0 (contact@lexilingo.com)', + 'Accept': 'application/json' + }) + + max_retries = 5 + base_delay = 5.0 + + for attempt in range(max_retries): + try: + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + + # data format: {"en": [{"partOfSpeech": "Noun", "definitions": [{"definition": "..."}]}]} + if "en" in data and len(data["en"]) > 0: + for pos_block in data["en"]: + if "definitions" in pos_block and len(pos_block["definitions"]) > 0: + # get the very first definition string + raw_def = pos_block["definitions"][0].get("definition", "") + if raw_def: + return strip_html(raw_def) + return "" + except urllib.error.HTTPError as e: + if e.code == 429: + delay = base_delay * (2 ** attempt) + print(f"Rate limited (429) for {word}. Retrying in {delay} seconds...") + time.sleep(delay) + elif e.code == 404: + # Word not found on wiktionary + return "" + else: + print(f"HTTP Error fetching {word}: {e}") + return "" + except Exception as e: + print(f"Error fetching {word}: {e}") + return "" + + print(f"Failed to fetch {word} after {max_retries} retries.") + return "" + +def main(): + print("Loading vocabulary...") + with open(FILE_PATH, 'r', encoding='utf-8') as f: + data = json.load(f) + + count = 0 + updated = 0 + + print(f"Fetching missing definitions...") + + for item in data: + word = item.get("word") + definition = item.get("definition", "") + + if not word: + continue + + if definition == "#N/A yet" or definition == "": + print(f"[{count+1}] Fetching definition for: {word}") + + new_def = get_wiktionary_definition(word) + if new_def: + item['definition'] = new_def + updated += 1 + + time.sleep(2.0) # Polite delay + + count += 1 + + # Checkpoint save + if count % 500 == 0 and updated > 0: + print(f"Checkpoint: Saving {updated} new definitions...") + with open(FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + if updated > 0: + print(f"Saving {updated} final definitions...") + with open(FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("Save completed.") + else: + print("No new definitions were updated.") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/fill_with_groq.py b/backend-service/scripts/fill_with_groq.py new file mode 100644 index 00000000..2e2b2f0d --- /dev/null +++ b/backend-service/scripts/fill_with_groq.py @@ -0,0 +1,197 @@ +import json +import urllib.request +import urllib.parse +import urllib.error +import ssl +import time +import os +import re + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" + +from dotenv import load_dotenv +from pathlib import Path + +# Load env variables +PROJECT_ROOT = Path(__file__).parent.parent.parent +load_dotenv(PROJECT_ROOT / ".env") +if os.getenv("APP_ENV", "").lower() == "production": + load_dotenv(PROJECT_ROOT / ".env.production", override=False) + +raw_keys = os.getenv("GROQ_API_KEYS", "").strip() +API_KEYS = [k.strip() for k in raw_keys.split(",") if k.strip()] if raw_keys else [] +if not API_KEYS: + single = os.getenv("GROQ_API_KEY", "").strip() + if single: + API_KEYS = [single] + +if not API_KEYS: + raise ValueError("Neither GROQ_API_KEYS nor GROQ_API_KEY is configured in the environment.") + +current_key_index = 0 + +def get_next_api_key(): + global current_key_index + key = API_KEYS[current_key_index] + current_key_index = (current_key_index + 1) % len(API_KEYS) + return key + +def clean_definition(text): + # Remove leading/trailing whitespace and quotes + text = text.strip() + # Remove surrounding double quotes if present + if text.startswith('"') and text.endswith('"'): + text = text[1:-1].strip() + if text.startswith("'") and text.endswith("'"): + text = text[1:-1].strip() + + # Remove common prefixes from LLM output + prefixes = [ + "definition:", "definition is:", "the definition is:", "refers to:", + "meaning:", "a definition of", "frankly means" + ] + lower_text = text.lower() + for prefix in prefixes: + if lower_text.startswith(prefix): + text = text[len(prefix):].strip() + # Clean again in case of leading punctuation or quotes + if text.startswith(':') or text.startswith('-'): + text = text[1:].strip() + if text.startswith('"') and text.endswith('"'): + text = text[1:-1].strip() + break + + # Capitalize the first letter and ensure it ends with a period if it is a complete sentence/phrase + if text: + text = text[0].upper() + text[1:] + if not text.endswith('.') and not text.endswith('!') and not text.endswith('?'): + text += '.' + + return text + +def get_groq_definition(word, example, translation_en, translation_vi, tags): + url = "https://api.groq.com/openai/v1/chat/completions" + + prompt = f"Word: {word}\n" + if example: + prompt += f"Example Sentence: {example}\n" + if translation_en: + prompt += f"English Translation/Synonym: {translation_en}\n" + if translation_vi: + prompt += f"Vietnamese Translation: {translation_vi}\n" + if tags: + prompt += f"Category/Tags: {tags}\n" + + system_msg = ( + "You are an expert lexicographer writing definitions for language learners. " + "Provide a clear, concise definition of the requested word in English. " + "The definition must be suitable for intermediate language learners and match the meaning of the word as used in the given example sentence and translations.\n" + "Rules:\n" + "1. Output ONLY the definition itself (e.g. 'In a straightforward, honest, and direct manner').\n" + "2. Do NOT include the word being defined, do NOT include quotes, do NOT include any introductory or explanatory text (e.g. do not say 'Here is the definition' or 'Definition:').\n" + "3. Keep it to one concise sentence or phrase." + ) + + payload = { + "model": "llama-3.3-70b-versatile", + "messages": [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": prompt} + ], + "temperature": 0.0, + "max_tokens": 150 + } + + max_retries = 3 + base_delay = 2.0 + + for attempt in range(len(API_KEYS) * 2): # Try rotating keys up to 2 full cycles + api_key = get_next_api_key() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST") + + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + res_data = json.loads(response.read().decode("utf-8")) + raw_content = res_data["choices"][0]["message"]["content"] + return clean_definition(raw_content) + except urllib.error.HTTPError as e: + # Read error body if possible + try: + err_body = e.read().decode("utf-8") + except Exception: + err_body = "" + print(f"API key index {current_key_index-1} failed with HTTP {e.code} for word '{word}'. Error: {err_body[:200]}") + + # If rate limit or other error, try the next key immediately + time.sleep(0.5) + except Exception as e: + print(f"API key index {current_key_index-1} failed with generic error for word '{word}': {e}") + time.sleep(0.5) + + # If all keys failed, wait and retry with exponential backoff + print("All API keys failed. Waiting 5 seconds before retrying...") + time.sleep(5.0) + return "" + +def main(): + print("Loading vocabulary JSON...") + with open(FILE_PATH, 'r', encoding='utf-8') as f: + data = json.load(f) + + print(f"Total vocabulary items: {len(data)}") + + # Identify items to fill + to_fill = [] + for idx, item in enumerate(data): + definition = item.get("definition", "") + if definition == "#N/A yet" or not definition: + to_fill.append(idx) + + print(f"Found {len(to_fill)} items needing definition updates.") + + if not to_fill: + print("No items to fill!") + return + + success_count = 0 + + for count, idx in enumerate(to_fill): + item = data[idx] + word = item.get("word") + example = item.get("example", "") + phonetic = item.get("phonetic", "") + tags = item.get("tags", "") + translation_block = item.get("translation", {}) + translation_en = translation_block.get("en", "") + translation_vi = translation_block.get("vi", "") + + print(f"[{count+1}/{len(to_fill)}] Fetching definition for '{word}'...") + + definition = get_groq_definition(word, example, translation_en, translation_vi, tags) + + if definition: + print(f" Word: '{word}'") + print(f" Definition: {definition}") + item["definition"] = definition + success_count += 1 + + # Save progressively + with open(FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(" Progress saved.") + else: + print(f" Warning: Failed to fetch definition for '{word}' after trying all API keys.") + + # Small delay between requests to be polite + time.sleep(0.5) + + print(f"Processing complete. Filled {success_count}/{len(to_fill)} missing definitions.") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/fix_vocabulary_import.py b/backend-service/scripts/fix_vocabulary_import.py new file mode 100755 index 00000000..c2366bb7 --- /dev/null +++ b/backend-service/scripts/fix_vocabulary_import.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +import json +import os +import re +import ssl +import time +import urllib.request +import urllib.parse +import urllib.error + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" +GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" +MODEL = "llama-3.3-70b-versatile" + +from dotenv import load_dotenv +from pathlib import Path + +# Load env variables +PROJECT_ROOT = Path(__file__).parent.parent.parent +load_dotenv(PROJECT_ROOT / ".env") +if os.getenv("APP_ENV", "").lower() == "production": + load_dotenv(PROJECT_ROOT / ".env.production", override=False) + +raw_keys = os.getenv("GROQ_API_KEYS", "").strip() +API_KEYS = [k.strip() for k in raw_keys.split(",") if k.strip()] if raw_keys else [] +if not API_KEYS: + single = os.getenv("GROQ_API_KEY", "").strip() + if single: + API_KEYS = [single] + +if not API_KEYS: + raise ValueError("Neither GROQ_API_KEYS nor GROQ_API_KEY is configured in the environment.") + +current_key_idx = 0 + +def get_next_api_key(): + global current_key_idx + key = API_KEYS[current_key_idx] + current_key_idx = (current_key_idx + 1) % len(API_KEYS) + return key + +def clean_wiki_text(text): + if not isinstance(text, str): + return text + # Replace [[A|B]] with B + text = re.sub(r'\[\[[^|\]]+\|([^\]]+)\]\]', r'\1', text) + # Replace [[A]] with A + text = re.sub(r'\[\[([^\]]+)\]\]', r'\1', text) + # Remove '' + text = text.replace("''", "") + # Remove any leftover unmatched brackets + text = text.replace("[[", "").replace("]]", "") + return text.strip() + +def fix_audio_path(v): + if isinstance(v, str) and v.startswith("extracted_media/"): + return v.replace("extracted_media/", "") + return v + +def clean_json_wrapper(text): + text = text.strip() + if text.startswith("```json"): + text = text[7:] + if text.startswith("```"): + text = text[3:] + if text.endswith("```"): + text = text[:-3] + return text.strip() + +def fetch_difficulty_batch(batch_words): + prompt = ( + "You are an expert lexicographer. Classify the following list of English words into their most appropriate CEFR difficulty levels: A1, A2, B1, B2, C1, or C2. " + "Use the provided definitions for context.\n" + "Return ONLY a valid JSON object where keys are words and values are their CEFR levels (e.g. {\"apple\": \"A1\", \"paradigm\": \"C1\"}). " + "Do NOT return any other text or explanation." + ) + + user_payload = [] + for item in batch_words: + user_payload.append({ + "word": item.get("word"), + "definition": item.get("definition", "") + }) + + payload = { + "model": MODEL, + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)} + ], + "temperature": 0.1, + "response_format": {"type": "json_object"} + } + + # Try multiple API keys + for attempt in range(len(API_KEYS) * 2): + api_key = get_next_api_key() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + + req = urllib.request.Request( + GROQ_URL, + data=json.dumps(payload).encode("utf-8"), + headers=headers, + method="POST" + ) + + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + res_data = json.loads(response.read().decode("utf-8")) + raw_content = res_data["choices"][0]["message"]["content"] + cleaned = clean_json_wrapper(raw_content) + return json.loads(cleaned) + except urllib.error.HTTPError as e: + try: + err_msg = e.read().decode("utf-8") + except Exception: + err_msg = "" + print(f"Key index {current_key_idx-1} failed (HTTP {e.code}). Msg: {err_msg[:100]}...") + time.sleep(1.0) + except Exception as e: + print(f"Key index {current_key_idx-1} failed (Generic error): {e}") + time.sleep(1.0) + + print("All keys failed for this batch.") + return None + +def main(): + print("Step 1: Reading and backup JSON...") + if not os.path.exists(FILE_PATH): + print(f"Error: {FILE_PATH} does not exist.") + return + + with open(FILE_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + + print(f"Loaded {len(data)} items.") + + # Local cleanups + print("Step 2: Performing syntax cleanup and media path fixes...") + cleaned_translations_count = 0 + fixed_paths_count = 0 + + for item in data: + # Fix media paths + audios = item.get("audios", {}) + if isinstance(audios, dict): + for k, v in list(audios.items()): + new_v = fix_audio_path(v) + if new_v != v: + audios[k] = new_v + fixed_paths_count += 1 + + images = item.get("images", "") + if isinstance(images, str) and images.startswith("extracted_media/"): + item["images"] = images.replace("extracted_media/", "") + fixed_paths_count += 1 + + # Clean wiktionary syntax in translations + trans = item.get("translation", {}) + if isinstance(trans, dict): + for lang, text in list(trans.items()): + if isinstance(text, str): + new_text = clean_wiki_text(text) + if new_text != text: + trans[lang] = new_text + cleaned_translations_count += 1 + elif isinstance(text, list): + # For examples list or similar + new_list = [clean_wiki_text(x) if isinstance(x, str) else x for x in text] + if new_list != text: + trans[lang] = new_list + cleaned_translations_count += 1 + + print(f"-> Fixed {fixed_paths_count} media paths.") + print(f"-> Cleaned {cleaned_translations_count} translation fields.") + + # Checkpoint local fixes + with open(FILE_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("Progress checkpoint saved.") + + # Classify difficulty levels + print("Step 3: Finding items missing difficulty levels...") + to_classify_indices = [] + for idx, item in enumerate(data): + level = item.get("difficulty_level") + if not level or level == "": + to_classify_indices.append(idx) + + print(f"-> Found {len(to_classify_indices)} items needing difficulty level classification.") + + if not to_classify_indices: + print("No items need difficulty level classification!") + return + + # Process in batches + batch_size = 50 + total_batches = (len(to_classify_indices) + batch_size - 1) // batch_size + valid_levels = {"A1", "A2", "B1", "B2", "C1", "C2"} + + for i in range(0, len(to_classify_indices), batch_size): + batch_idxs = to_classify_indices[i:i+batch_size] + batch_words = [data[idx] for idx in batch_idxs] + + print(f"Processing batch {i//batch_size + 1}/{total_batches} ({len(batch_words)} words)...") + + levels_map = None + retries = 3 + while retries > 0: + levels_map = fetch_difficulty_batch(batch_words) + if levels_map: + break + retries -= 1 + print(f"Retrying batch... ({retries} retries left)") + time.sleep(2.0) + + if not levels_map: + print("Skipping batch because of repeated API failures.") + continue + + # Standardize keys to lowercase for matching + levels_map_lower = {k.lower().strip(): v.upper().strip() for k, v in levels_map.items() if isinstance(v, str)} + + updated_in_batch = 0 + for idx in batch_idxs: + item = data[idx] + w = item.get("word", "").lower().strip() + + level = levels_map_lower.get(w) + if level in valid_levels: + item["difficulty_level"] = level + updated_in_batch += 1 + else: + # Fallback: check if sub-parts or clean word matches + cleaned_word = re.sub(r"[^\w\s-]", "", w).strip() + level = levels_map_lower.get(cleaned_word) + if level in valid_levels: + item["difficulty_level"] = level + updated_in_batch += 1 + else: + # Generic fallback based on index frequency + # (since first ~1500 words are usually A1/A2, next are B1/B2, etc.) + index = item.get("index", 0) + if index <= 1500: + item["difficulty_level"] = "A1" + elif index <= 3000: + item["difficulty_level"] = "A2" + elif index <= 4500: + item["difficulty_level"] = "B1" + elif index <= 5500: + item["difficulty_level"] = "B2" + else: + item["difficulty_level"] = "C1" + # We print warning but set a reasonable fallback + print(f" Fallback level {item['difficulty_level']} assigned for '{item.get('word')}'") + updated_in_batch += 1 + + print(f"-> Successfully classified {updated_in_batch}/{len(batch_words)} words.") + + # Save every batch + with open(FILE_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(" Saved batch updates.") + + # Polite delay + time.sleep(1.0) + + print("Done! Standardizing and cleaning complete.") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/refine_vocabulary.py b/backend-service/scripts/refine_vocabulary.py new file mode 100755 index 00000000..a67c46ca --- /dev/null +++ b/backend-service/scripts/refine_vocabulary.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +import json +import os +import re +import ssl +import time +import urllib.request +import urllib.parse +import urllib.error + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" +GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" +MODEL = "llama-3.3-70b-versatile" + +from dotenv import load_dotenv +from pathlib import Path + +# Load env variables +PROJECT_ROOT = Path(__file__).parent.parent.parent +load_dotenv(PROJECT_ROOT / ".env") +if os.getenv("APP_ENV", "").lower() == "production": + load_dotenv(PROJECT_ROOT / ".env.production", override=False) + +raw_keys = os.getenv("GROQ_API_KEYS", "").strip() +API_KEYS = [k.strip() for k in raw_keys.split(",") if k.strip()] if raw_keys else [] +if not API_KEYS: + single = os.getenv("GROQ_API_KEY", "").strip() + if single: + API_KEYS = [single] + +if not API_KEYS: + raise ValueError("Neither GROQ_API_KEYS nor GROQ_API_KEY is configured in the environment.") + +current_key_idx = 0 + +def get_next_api_key(): + global current_key_idx + key = API_KEYS[current_key_idx] + current_key_idx = (current_key_idx + 1) % len(API_KEYS) + return key + +def is_cjk(c): + codepoint = ord(c) + return ( + 0x4E00 <= codepoint <= 0x9FFF or + 0x3400 <= codepoint <= 0x4DBF or + 0x20000 <= codepoint <= 0x2A6DF or + 0x2A700 <= codepoint <= 0x2B73F or + 0x2B740 <= codepoint <= 0x2B81F or + 0x2B820 <= codepoint <= 0x2CEAF or + 0xF900 <= codepoint <= 0xFAFF + ) + +def clean_vietnamese_translation(text): + if not isinstance(text, str): + return text + # Remove any CJK characters + text = "".join(c for c in text if not is_cjk(c)) + # Clean duplicate commas and spaces + text = re.sub(r',\s*,', ',', text) + text = re.sub(r'\s+', ' ', text) + text = re.sub(r'^\s*,\s*|\s*,\s*$', '', text) + text = re.sub(r',\s*,\s*', ', ', text) + return text.strip() + +def needs_refinement(item): + defn = item.get("definition", "").strip() + if not defn: + return True + + # Heuristic for English definitions + english_words = {'is', 'a', 'to', 'of', 'and', 'the', 'it', 'or', 'in', 'with', 'if', 'something', 'describes', 'someone', 'by', 'for', 'from', 'an'} + words = set(re.findall(r'\b\w+\b', defn.lower())) + if words.intersection(english_words): + return True + + # Heuristic for short/direct translation definitions (e.g. 'đội, nhóm') + # If it is less than 15 characters, or contains comma/semicolon, it's a translation, not explanation. + if len(defn) < 15 or ',' in defn or ';' in defn: + return True + + trans_vi = item.get("translation", {}).get("vi", "") + if not trans_vi or any(is_cjk(c) for c in str(trans_vi)): + return True + + return False + +def clean_json_wrapper(text): + text = text.strip() + if text.startswith("```json"): + text = text[7:] + if text.startswith("```"): + text = text[3:] + if text.endswith("```"): + text = text[:-3] + return text.strip() + +def fetch_refinements_batch(batch_items): + prompt = ( + "You are an expert bilingual lexicographer. I will provide a JSON list of English words, their definition (which might be in English or a short translation), and their current Vietnamese translation.\n" + "For each word, you must return a JSON object with two fields:\n" + "1. \"definition\": A concise, natural Vietnamese explanation/definition of the word's meaning (suitable for language learners, e.g. \"Một nhóm người hợp tác cùng nhau để làm việc hoặc chơi thể thao\" for \"team\"). It must be a full explanation, NOT a direct 1-3 word translation.\n" + "2. \"translation_vi\": A clean Vietnamese direct translation (synonym or equivalent words, e.g., \"đội, nhóm\" for \"team\"), with NO CJK/Chinese/Hán/Nom characters (e.g. remove characters like 學, 實, 體).\n" + "\n" + "Return ONLY a valid JSON object where keys are the words and values are their corresponding objects containing \"definition\" and \"translation_vi\". " + "Do NOT return any other text or explanation." + ) + + user_payload = [] + for item in batch_items: + user_payload.append({ + "word": item.get("word"), + "definition": item.get("definition", ""), + "translation_vi": item.get("translation", {}).get("vi", "") + }) + + payload = { + "model": MODEL, + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)} + ], + "temperature": 0.1, + "response_format": {"type": "json_object"} + } + + # Try multiple API keys + for attempt in range(len(API_KEYS) * 2): + api_key = get_next_api_key() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + + req = urllib.request.Request( + GROQ_URL, + data=json.dumps(payload).encode("utf-8"), + headers=headers, + method="POST" + ) + + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + res_data = json.loads(response.read().decode("utf-8")) + raw_content = res_data["choices"][0]["message"]["content"] + cleaned = clean_json_wrapper(raw_content) + return json.loads(cleaned) + except urllib.error.HTTPError as e: + try: + err_msg = e.read().decode("utf-8") + except Exception: + err_msg = "" + print(f"Key index {current_key_idx-1} failed (HTTP {e.code}). Msg: {err_msg[:100]}...") + time.sleep(1.0) + except Exception as e: + print(f"Key index {current_key_idx-1} failed (Generic error): {e}") + time.sleep(1.0) + + return None + +def main(): + print("Step 1: Reading JSON...") + if not os.path.exists(FILE_PATH): + print(f"Error: {FILE_PATH} does not exist.") + return + + with open(FILE_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + + print(f"Loaded {len(data)} items.") + + # Local passes: Clean CJK characters from translation['vi'] immediately + print("Step 2: Performing local Hán/Nom character sanitization...") + local_cleaned_count = 0 + for item in data: + trans = item.get("translation", {}) + if isinstance(trans, dict): + vi = trans.get("vi", "") + if isinstance(vi, str) and any(is_cjk(c) for c in vi): + trans["vi"] = clean_vietnamese_translation(vi) + local_cleaned_count += 1 + + print(f"-> Sanitized Hán/Nom characters locally for {local_cleaned_count} items.") + + # Scan for items needing semantic refinement + to_refine_indices = [idx for idx, item in enumerate(data) if needs_refinement(item)] + print(f"Step 3: Found {len(to_refine_indices)} items needing explanation and translation refinement.") + + if not to_refine_indices: + print("No items need refinement!") + return + + batch_size = 50 + total_batches = (len(to_refine_indices) + batch_size - 1) // batch_size + + for i in range(0, len(to_refine_indices), batch_size): + batch_idxs = to_refine_indices[i:i+batch_size] + batch_items = [data[idx] for idx in batch_idxs] + + print(f"Refining batch {i//batch_size + 1}/{total_batches} ({len(batch_items)} words)...") + + refinements_map = None + retries = 3 + while retries > 0: + refinements_map = fetch_refinements_batch(batch_items) + if refinements_map: + break + retries -= 1 + print(f"Retrying batch... ({retries} retries left)") + time.sleep(2.0) + + if not refinements_map: + print("Skipping batch because of repeated API failures.") + continue + + # Standardize keys to lowercase + refinements_map_lower = {k.lower().strip(): v for k, v in refinements_map.items() if isinstance(v, dict)} + + updated_in_batch = 0 + for idx in batch_idxs: + item = data[idx] + w = item.get("word", "").lower().strip() + + ref = refinements_map_lower.get(w) + if not ref: + # Fallback: check stripped word + cleaned_word = re.sub(r"[^\w\s-]", "", w).strip() + ref = refinements_map_lower.get(cleaned_word) + + if ref and isinstance(ref, dict): + new_def = ref.get("definition", "").strip() + new_trans_vi = ref.get("translation_vi", "").strip() + + if new_def: + item["definition"] = new_def + if new_trans_vi: + if "translation" not in item: + item["translation"] = {} + item["translation"]["vi"] = clean_vietnamese_translation(new_trans_vi) + + updated_in_batch += 1 + + print(f"-> Successfully refined {updated_in_batch}/{len(batch_items)} words.") + + # Progressive save + with open(FILE_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(" Saved batch updates.") + + time.sleep(1.0) + + print("Refinement process complete!") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/restore_vietnamese_accents.py b/backend-service/scripts/restore_vietnamese_accents.py new file mode 100755 index 00000000..af868518 --- /dev/null +++ b/backend-service/scripts/restore_vietnamese_accents.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import json +import os +import re +import ssl +import time +import urllib.request +import urllib.parse +import urllib.error + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" +GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" +MODEL = "llama-3.1-8b-instant" + +from dotenv import load_dotenv +from pathlib import Path + +# Load env variables +PROJECT_ROOT = Path(__file__).parent.parent.parent +load_dotenv(PROJECT_ROOT / ".env") +if os.getenv("APP_ENV", "").lower() == "production": + load_dotenv(PROJECT_ROOT / ".env.production", override=False) + +raw_keys = os.getenv("GROQ_API_KEYS", "").strip() +API_KEYS = [k.strip() for k in raw_keys.split(",") if k.strip()] if raw_keys else [] +if not API_KEYS: + single = os.getenv("GROQ_API_KEY", "").strip() + if single: + API_KEYS = [single] + +if not API_KEYS: + raise ValueError("Neither GROQ_API_KEYS nor GROQ_API_KEY is configured in the environment.") + +current_key_idx = 0 + +def get_next_api_key(): + global current_key_idx + key = API_KEYS[current_key_idx] + current_key_idx = (current_key_idx + 1) % len(API_KEYS) + return key + +# Standard Vietnamese accent characters +ACCENT_CHARS = set('áàảãạăắằẳẵặâấầẩẫậéèẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵđ') + +def has_accents(text): + if not isinstance(text, str): + return True + return any(c in ACCENT_CHARS for c in text.lower()) + +def clean_json_wrapper(text): + text = text.strip() + if text.startswith("```json"): + text = text[7:] + if text.startswith("```"): + text = text[3:] + if text.endswith("```"): + text = text[:-3] + return text.strip() + +def fetch_accents_batch(batch_items): + prompt = ( + "You are an expert Vietnamese linguist. I will provide a JSON list of English words and their current Vietnamese translation (which is missing accents/diacritics, e.g., \"hoc\" for \"learn\", \"chinh sach\" for \"policy\", \"nuoc\" for \"water\").\n" + "For each word, you must correct the Vietnamese translation by adding the proper Vietnamese accents (dấu tiếng Việt) so it is grammatically correct and matches the meaning (e.g. \"hoc\" -> \"học\", \"chinh sach\" -> \"chính sách\", \"nuoc\" -> \"nước\", \"tuoi\" -> \"tuổi\").\n" + "If the current translation is already correct and naturally does not need accents (e.g. \"cho\" for \"give\", \"kinh doanh\" for \"business\"), keep it as is.\n" + "\n" + "Return ONLY a valid JSON object where keys are words and values are the corrected Vietnamese translation strings. " + "Do NOT return any other text or explanation." + ) + + user_payload = [] + for item in batch_items: + user_payload.append({ + "word": item.get("word"), + "current_translation_vi": item.get("translation", {}).get("vi", "") + }) + + payload = { + "model": MODEL, + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)} + ], + "temperature": 0.1, + "response_format": {"type": "json_object"} + } + + # Try multiple API keys + for attempt in range(len(API_KEYS) * 2): + api_key = get_next_api_key() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + + req = urllib.request.Request( + GROQ_URL, + data=json.dumps(payload).encode("utf-8"), + headers=headers, + method="POST" + ) + + try: + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + res_data = json.loads(response.read().decode("utf-8")) + raw_content = res_data["choices"][0]["message"]["content"] + cleaned = clean_json_wrapper(raw_content) + return json.loads(cleaned) + except urllib.error.HTTPError as e: + try: + err_msg = e.read().decode("utf-8") + except Exception: + err_msg = "" + actual_key_idx = (current_key_idx - 1) % len(API_KEYS) + print(f"Key index {actual_key_idx} failed (HTTP {e.code}). Msg: {err_msg[:100]}...") + if e.code == 429: + print("Rate limit (429) hit. Waiting 6.0 seconds before rotating to the next key...") + time.sleep(6.0) + else: + time.sleep(1.5) + except Exception as e: + actual_key_idx = (current_key_idx - 1) % len(API_KEYS) + print(f"Key index {actual_key_idx} failed (Generic error): {e}") + time.sleep(1.5) + + return None + +def main(): + print("Step 1: Reading JSON...") + if not os.path.exists(FILE_PATH): + print(f"Error: {FILE_PATH} does not exist.") + return + + with open(FILE_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + + print(f"Loaded {len(data)} items.") + + # Identify items needing accents + to_correct_indices = [] + for idx, item in enumerate(data): + vi = item.get("translation", {}).get("vi", "") + if isinstance(vi, str) and not has_accents(vi): + to_correct_indices.append(idx) + + print(f"Step 2: Found {len(to_correct_indices)} items needing Vietnamese accent correction.") + + if not to_correct_indices: + print("No items need accent correction!") + return + + batch_size = 30 + total_batches = (len(to_correct_indices) + batch_size - 1) // batch_size + + for i in range(0, len(to_correct_indices), batch_size): + batch_idxs = to_correct_indices[i:i+batch_size] + batch_items = [data[idx] for idx in batch_idxs] + + print(f"Correcting batch {i//batch_size + 1}/{total_batches} ({len(batch_items)} words)...") + + corrections_map = None + retries = 5 + while retries > 0: + corrections_map = fetch_accents_batch(batch_items) + if corrections_map: + break + retries -= 1 + print(f"Retrying batch... ({retries} retries left)") + time.sleep(4.0) + + if not corrections_map: + print("Skipping batch because of repeated API failures.") + continue + + # Standardize keys to lowercase + corrections_map_lower = {k.lower().strip(): v for k, v in corrections_map.items() if isinstance(v, str)} + + updated_in_batch = 0 + for idx in batch_idxs: + item = data[idx] + w = item.get("word", "").lower().strip() + + corrected_vi = corrections_map_lower.get(w) + if not corrected_vi: + # Fallback check stripped word + cleaned_word = re.sub(r"[^\w\s-]", "", w).strip() + corrected_vi = corrections_map_lower.get(cleaned_word) + + if corrected_vi and isinstance(corrected_vi, str): + item["translation"]["vi"] = corrected_vi.strip() + updated_in_batch += 1 + + print(f"-> Successfully restored accents for {updated_in_batch}/{len(batch_items)} words.") + + # Progressive save + with open(FILE_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(" Saved batch updates.") + + # Protect against Groq rate limit (TPM/RPM) + time.sleep(2.5) + + print("Accent restoration process complete!") + +if __name__ == "__main__": + main() diff --git a/backend-service/scripts/run_tasks.sh b/backend-service/scripts/run_tasks.sh new file mode 100755 index 00000000..69b9efd6 --- /dev/null +++ b/backend-service/scripts/run_tasks.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# run_tasks.sh +# Coordindates the vocabulary audio downloading and vocabulary database expansion. + +set -e + +SCRIPT_DIR="/opt/lexilingo/backend-service/scripts" +LOG_DIR="/opt/lexilingo/backend-service/logs" + +mkdir -p "$LOG_DIR" + +echo "=== STARTING VOCABULARY AND AUDIO TASKS ===" +echo "Logs will be stored in $LOG_DIR" + +# Task 1: Fetch audios for existing words +echo "" +echo "[Task 1/2] Fetching and downloading missing audios for existing vocabulary..." +python3 -u "$SCRIPT_DIR/fetch_audios.py" 2>&1 | tee "$LOG_DIR/fetch_audios.log" + +# Task 2: Expand vocabulary with CEFR and IELTS levels +echo "" +echo "[Task 2/2] Expanding vocabulary JSON with CEFR and IELTS words using Groq API..." +python3 -u "$SCRIPT_DIR/expand_vocabulary.py" 2>&1 | tee "$LOG_DIR/expand_vocabulary.log" + +echo "" +echo "=== ALL TASKS COMPLETED SUCCESSFULLY ===" diff --git a/backend-service/scripts/test_wiktionary.py b/backend-service/scripts/test_wiktionary.py new file mode 100644 index 00000000..73001c77 --- /dev/null +++ b/backend-service/scripts/test_wiktionary.py @@ -0,0 +1,35 @@ +import json +import urllib.request +import urllib.parse +import re + +def get_wiktionary_translations(word): + url = f"https://en.wiktionary.org/w/api.php?action=query&prop=revisions&rvprop=content&rvslots=main&titles={urllib.parse.quote(word)}&format=json" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + try: + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + pages = data.get('query', {}).get('pages', {}) + for page_id, page_info in pages.items(): + if 'revisions' in page_info: + content = page_info['revisions'][0]['slots']['main']['*'] + + # Look for translation tags like {{t|vi|quả táo}} or {{t+|fr|pomme}} + # The format is typically {{t[+-ø]?|lang_code|word|...}} + translations = {} + for lang_code in ['ja', 'ko', 'zh', 'fr', 'es', 'vi']: + # Regex to match the translation macro + pattern = rf'\{\{t[+ø-]?\|{lang_code}\|([^}}|]+)' + matches = re.findall(pattern, content) + if matches: + # Clean up and get unique translations + unique_matches = list(dict.fromkeys([m.strip() for m in matches])) + translations[lang_code] = ", ".join(unique_matches[:3]) + return translations + return {} + except Exception as e: + print(f"Error: {e}") + return {} + +if __name__ == "__main__": + print(get_wiktionary_translations("apple")) diff --git a/backend-service/scripts/translate_vocabulary.py b/backend-service/scripts/translate_vocabulary.py new file mode 100644 index 00000000..34181f25 --- /dev/null +++ b/backend-service/scripts/translate_vocabulary.py @@ -0,0 +1,96 @@ +import json +import urllib.request +import urllib.parse +import re +import time +import os + +FILE_PATH = "/opt/lexilingo/backend-service/data/vocabulary_import.json" + +def get_wiktionary_translations(word): + url = f"https://en.wiktionary.org/w/api.php?action=query&prop=revisions&rvprop=content&rvslots=main&titles={urllib.parse.quote(word)}&format=json" + req = urllib.request.Request(url, headers={'User-Agent': 'LexiLingo-VocabBot/1.0 (contact@lexilingo.com)'}) + + max_retries = 5 + base_delay = 5.0 + + for attempt in range(max_retries): + try: + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + pages = data.get('query', {}).get('pages', {}) + for page_id, page_info in pages.items(): + if 'revisions' in page_info: + content = page_info['revisions'][0]['slots']['main']['*'] + + translations = {} + for lang_code in ['ja', 'ko', 'zh', 'fr', 'es', 'vi']: + pattern = r'\{\{t[+ø-]?\|' + lang_code + r'\|([^}|]+)' + matches = re.findall(pattern, content) + if matches: + unique_matches = list(dict.fromkeys([m.strip() for m in matches])) + translations[lang_code] = ", ".join(unique_matches[:3]) + return translations + return {} + except urllib.error.HTTPError as e: + if e.code == 429: + delay = base_delay * (2 ** attempt) + print(f"Rate limited (429) for {word}. Retrying in {delay} seconds...") + time.sleep(delay) + else: + print(f"HTTP Error fetching {word}: {e}") + return {} + except Exception as e: + print(f"Error fetching {word}: {e}") + return {} + + print(f"Failed to fetch {word} after {max_retries} retries.") + return {} + +def main(): + print("Loading vocabulary...") + with open(FILE_PATH, 'r', encoding='utf-8') as f: + data = json.load(f) + + count = 0 + updated = 0 + + print(f"Translating all words and overwriting existing translations...") + + for item in data: + word = item.get("word") + if not word: + continue + + print(f"[{count+1}] Fetching translations for: {word}") + + new_trans = get_wiktionary_translations(word) + if new_trans: + # Overwrite existing translations with Wiktionary ones + translation = item.get("translation", {}) + for lang, text in new_trans.items(): + if text: # only if we found a translation + translation[lang] = text + updated += 1 + item['translation'] = translation + + time.sleep(2.0) # Sleep 2 seconds to respect Wiktionary API rate limits + count += 1 + + + # Save every 50 items to avoid losing data on crash + if count % 50 == 0 and updated > 0: + print(f"Checkpoint: Saving {updated} new translations...") + with open(FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + if updated > 0: + print(f"Saving {updated} new translations...") + with open(FILE_PATH, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("Save completed.") + else: + print("No updates needed for the scanned words.") + +if __name__ == "__main__": + main() From 9143989bc5a5e5172f3e9a5339e74b9db30c6c05 Mon Sep 17 00:00:00 2001 From: InfinityZero3000 Date: Tue, 16 Jun 2026 16:16:03 +0700 Subject: [PATCH 20/61] fix(admin, docker): resolve admin TypeScript compile errors & Celery healthcheck issues --- .../dashboard/CompletionFunnelChart.tsx | 4 +- .../dashboard/CoursePopularityChart.tsx | 4 +- .../components/dashboard/EngagementChart.tsx | 2 +- .../components/dashboard/UserGrowthChart.tsx | 2 +- admin-service/src/lib/userManagementApi.ts | 7 + admin-service/src/main.tsx | 7 +- .../src/pages/AdminManagementPage.tsx | 8 +- .../src/pages/AiChatSettingsPage.tsx | 2 +- admin-service/tsconfig.tsbuildinfo | 2 +- ai-service/.gitignore | 4 + docker-compose.dev.yml | 361 +++++++++++++++++ docker-compose.production.yml | 277 ------------- docker-compose.yml | 382 +++++++----------- scripts/backup-prod.sh | 2 +- scripts/deploy-one-shot.sh | 4 +- scripts/restore-prod.sh | 2 +- scripts/ssl/reload-gateway-after-renew.sh | 2 +- scripts/start-all.sh | 4 +- scripts/stop-all.sh | 4 +- 19 files changed, 545 insertions(+), 535 deletions(-) create mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.production.yml diff --git a/admin-service/src/components/dashboard/CompletionFunnelChart.tsx b/admin-service/src/components/dashboard/CompletionFunnelChart.tsx index c9fe6de1..f64c6bbc 100644 --- a/admin-service/src/components/dashboard/CompletionFunnelChart.tsx +++ b/admin-service/src/components/dashboard/CompletionFunnelChart.tsx @@ -48,8 +48,8 @@ export const CompletionFunnelChart: React.FC = ({ data, loading }) => { width={90} /> [ - `${value.toLocaleString()} (${props.payload.percentage.toFixed(1)}%)`, + formatter={(value: any, name: any, props: any) => [ + `${value?.toLocaleString() || ""}${(props?.payload?.percentage !== undefined) ? ` (${props.payload.percentage.toFixed(1)}%)` : ""}`, "" ]} /> diff --git a/admin-service/src/components/dashboard/CoursePopularityChart.tsx b/admin-service/src/components/dashboard/CoursePopularityChart.tsx index b753cf6a..f13895c4 100644 --- a/admin-service/src/components/dashboard/CoursePopularityChart.tsx +++ b/admin-service/src/components/dashboard/CoursePopularityChart.tsx @@ -45,7 +45,7 @@ export const CoursePopularityChart: React.FC = ({ data, loading }) => { cx="50%" cy="50%" labelLine={false} - label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`} + label={({ name, percent }) => `${name}: ${percent !== undefined ? (percent * 100).toFixed(0) : "0"}%`} outerRadius={80} fill="#8884d8" dataKey="value" @@ -54,7 +54,7 @@ export const CoursePopularityChart: React.FC = ({ data, loading }) => { ))} - [value.toLocaleString() + " đăng ký", ""]} /> + [value?.toLocaleString() + " đăng ký", ""]} /> diff --git a/admin-service/src/components/dashboard/EngagementChart.tsx b/admin-service/src/components/dashboard/EngagementChart.tsx index 03e11562..82e94c53 100644 --- a/admin-service/src/components/dashboard/EngagementChart.tsx +++ b/admin-service/src/components/dashboard/EngagementChart.tsx @@ -37,7 +37,7 @@ export const EngagementChart: React.FC = ({ data, loading }) => { - [value.toLocaleString(), ""]} /> + [value?.toLocaleString() || "", ""]} /> diff --git a/admin-service/src/components/dashboard/UserGrowthChart.tsx b/admin-service/src/components/dashboard/UserGrowthChart.tsx index 92154dd0..0c119296 100644 --- a/admin-service/src/components/dashboard/UserGrowthChart.tsx +++ b/admin-service/src/components/dashboard/UserGrowthChart.tsx @@ -48,7 +48,7 @@ export const UserGrowthChart: React.FC = ({ data, loading }) => { const date = new Date(value); return date.toLocaleDateString("vi-VN"); }} - formatter={(value: number) => [value.toLocaleString(), ""]} + formatter={(value: any) => [value?.toLocaleString() || "", ""]} /> - + diff --git a/admin-service/src/pages/AdminManagementPage.tsx b/admin-service/src/pages/AdminManagementPage.tsx index a35a0f1f..2b257c15 100644 --- a/admin-service/src/pages/AdminManagementPage.tsx +++ b/admin-service/src/pages/AdminManagementPage.tsx @@ -154,15 +154,15 @@ export const AdminManagementPage = () => { - {admin.provider} + {admin.provider.join(", ")} diff --git a/admin-service/src/pages/AiChatSettingsPage.tsx b/admin-service/src/pages/AiChatSettingsPage.tsx index 4469534a..2e4152bc 100644 --- a/admin-service/src/pages/AiChatSettingsPage.tsx +++ b/admin-service/src/pages/AiChatSettingsPage.tsx @@ -148,7 +148,7 @@ export const AiChatSettingsPage = () => { label={t.aiChat.features} value={`${[config.enable_voice, config.enable_grammar, config.enable_topic].filter(Boolean).length}/3`} note={t.aiChat.modulesEnabled} - accent="purple" + accent="ink" /> diff --git a/admin-service/tsconfig.tsbuildinfo b/admin-service/tsconfig.tsbuildinfo index 4e8a1100..c375ceae 100644 --- a/admin-service/tsconfig.tsbuildinfo +++ b/admin-service/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AppShell.tsx","./src/components/AuthProvider.tsx","./src/components/ConfigLock.tsx","./src/components/CourseImportModal.tsx","./src/components/DataTable.tsx","./src/components/EmptyState.tsx","./src/components/ExerciseTypeForm.tsx","./src/components/RequireAuth.tsx","./src/components/RequireRole.tsx","./src/components/SectionHeader.tsx","./src/components/Skeleton.tsx","./src/components/StatCard.tsx","./src/components/StatusPill.tsx","./src/components/dashboard/CompletionFunnelChart.tsx","./src/components/dashboard/CoursePopularityChart.tsx","./src/components/dashboard/EngagementChart.tsx","./src/components/dashboard/UserGrowthChart.tsx","./src/components/login-globe/GlobeBackground.tsx","./src/components/login-globe/globe-arcs.ts","./src/components/login-globe/globe-atmosphere.ts","./src/components/login-globe/globe-dots.ts","./src/components/login-globe/globe-interaction.ts","./src/components/login-globe/globe-particles.ts","./src/components/login-globe/globe-scene.ts","./src/components/login-globe/globe-shell.ts","./src/components/login-globe/shaders/arcShaders.ts","./src/components/login-globe/shaders/atmosphereShaders.ts","./src/components/login-globe/shaders/dotShaders.ts","./src/components/user-management/UserDetailModal.tsx","./src/components/user-management/UserFiltersPanel.tsx","./src/lib/adminApi.ts","./src/lib/aiApi.ts","./src/lib/analyticsApi.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/authApi.ts","./src/lib/env.ts","./src/lib/rbacApi.ts","./src/lib/types.ts","./src/lib/userManagementApi.ts","./src/lib/i18n/en.ts","./src/lib/i18n/index.ts","./src/lib/i18n/vi.ts","./src/pages/AchievementsPage.tsx","./src/pages/AdminDashboard.tsx","./src/pages/AdminManagementPage.tsx","./src/pages/AdsPage.tsx","./src/pages/AiChatSettingsPage.tsx","./src/pages/AiModelsPage.tsx","./src/pages/ContentAnalyticsPage.tsx","./src/pages/ContentLabPage.tsx","./src/pages/CoursesPage.tsx","./src/pages/DatabasePage.tsx","./src/pages/EnhancedAdminDashboard.tsx","./src/pages/LessonExercisesPage.tsx","./src/pages/LessonsPage.tsx","./src/pages/LoginPage.tsx","./src/pages/LogsPage.tsx","./src/pages/MonitoringPage.tsx","./src/pages/NoAccessPage.tsx","./src/pages/NotFoundPage.tsx","./src/pages/RoleRedirectPage.tsx","./src/pages/ShopPage.tsx","./src/pages/SuperAdminDashboard.tsx","./src/pages/SystemSettingsPage.tsx","./src/pages/TopicsPage.tsx","./src/pages/UnitsPage.tsx","./src/pages/UserManagementPage.tsx","./src/pages/UsersPage.tsx","./src/pages/VocabularyPage.tsx"],"errors":true,"version":"6.0.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AppShell.tsx","./src/components/AuthProvider.tsx","./src/components/ConfigLock.tsx","./src/components/CourseImportModal.tsx","./src/components/DataTable.tsx","./src/components/EmptyState.tsx","./src/components/ExerciseTypeForm.tsx","./src/components/RequireAuth.tsx","./src/components/RequireRole.tsx","./src/components/SectionHeader.tsx","./src/components/Skeleton.tsx","./src/components/StatCard.tsx","./src/components/StatusPill.tsx","./src/components/dashboard/CompletionFunnelChart.tsx","./src/components/dashboard/CoursePopularityChart.tsx","./src/components/dashboard/EngagementChart.tsx","./src/components/dashboard/UserGrowthChart.tsx","./src/components/login-globe/GlobeBackground.tsx","./src/components/login-globe/globe-arcs.ts","./src/components/login-globe/globe-atmosphere.ts","./src/components/login-globe/globe-dots.ts","./src/components/login-globe/globe-interaction.ts","./src/components/login-globe/globe-particles.ts","./src/components/login-globe/globe-scene.ts","./src/components/login-globe/globe-shell.ts","./src/components/login-globe/shaders/arcShaders.ts","./src/components/login-globe/shaders/atmosphereShaders.ts","./src/components/login-globe/shaders/dotShaders.ts","./src/components/user-management/UserDetailModal.tsx","./src/components/user-management/UserFiltersPanel.tsx","./src/lib/adminApi.ts","./src/lib/aiApi.ts","./src/lib/analyticsApi.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/authApi.ts","./src/lib/env.ts","./src/lib/rbacApi.ts","./src/lib/types.ts","./src/lib/userManagementApi.ts","./src/lib/i18n/en.ts","./src/lib/i18n/index.ts","./src/lib/i18n/vi.ts","./src/pages/AchievementsPage.tsx","./src/pages/AdminDashboard.tsx","./src/pages/AdminManagementPage.tsx","./src/pages/AdsPage.tsx","./src/pages/AiChatSettingsPage.tsx","./src/pages/AiModelsPage.tsx","./src/pages/ContentAnalyticsPage.tsx","./src/pages/ContentLabPage.tsx","./src/pages/CoursesPage.tsx","./src/pages/DatabasePage.tsx","./src/pages/EnhancedAdminDashboard.tsx","./src/pages/LessonExercisesPage.tsx","./src/pages/LessonsPage.tsx","./src/pages/LoginPage.tsx","./src/pages/LogsPage.tsx","./src/pages/MonitoringPage.tsx","./src/pages/NoAccessPage.tsx","./src/pages/NotFoundPage.tsx","./src/pages/RoleRedirectPage.tsx","./src/pages/ShopPage.tsx","./src/pages/SuperAdminDashboard.tsx","./src/pages/SystemSettingsPage.tsx","./src/pages/TopicsPage.tsx","./src/pages/UnitsPage.tsx","./src/pages/UserManagementPage.tsx","./src/pages/UsersPage.tsx","./src/pages/VocabularyPage.tsx"],"version":"6.0.3"} \ No newline at end of file diff --git a/ai-service/.gitignore b/ai-service/.gitignore index 64990548..5fa9faaa 100644 --- a/ai-service/.gitignore +++ b/ai-service/.gitignore @@ -32,6 +32,10 @@ temp/ data/kuzu_db data/kuzu_db/ data/kuzu_db.wal +data/kuzu +data/kuzu/ +data/kuzu.wal +data/kuzu_synced_files.json api/data/ data/backups/ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..f69b6600 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,361 @@ +services: + # ============================================ + # PostgreSQL Database (Backend Service) + # ============================================ + postgres: + image: postgres:16-alpine + container_name: lexilingo-postgres + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + mem_limit: 512m + memswap_limit: 512m + environment: + POSTGRES_USER: ${POSTGRES_USER:-lexilingo} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + POSTGRES_DB: ${POSTGRES_DB:-lexilingo} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U lexilingo"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - lexilingo-network + + # ============================================ + # pgAdmin (PostgreSQL Web Management UI) + # ============================================ + pgadmin: + image: dpage/pgadmin4 + container_name: lexilingo-pgadmin + restart: unless-stopped + deploy: + resources: + limits: + memory: 768M + reservations: + memory: 256M + mem_limit: 768m + memswap_limit: 768m + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@lexilingo.me} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:?Set PGADMIN_DEFAULT_PASSWORD in .env} + PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "True" + PGADMIN_CONFIG_LOGIN_BANNER: "'LexiLingo Database Management'" + ports: + - "5050:80" + depends_on: + postgres: + condition: service_healthy + networks: + - lexilingo-network + + # ============================================ + # MongoDB Database (AI Service) + # ============================================ + mongodb: + image: mongo:7.0 + container_name: lexilingo-mongodb + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + mem_limit: 1g + memswap_limit: 1g + ports: + - "27017:27017" + environment: + MONGO_INITDB_DATABASE: lexilingo + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + - ./ai-service/scripts/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 40s + networks: + - lexilingo-network + + # ============================================ + # Mongo Express (MongoDB Web Management UI) + # ============================================ + mongo-express: + image: mongo-express:latest + container_name: lexilingo-mongo-express + restart: unless-stopped + deploy: + resources: + limits: + memory: 256M + reservations: + memory: 128M + mem_limit: 256m + memswap_limit: 256m + environment: + ME_CONFIG_MONGODB_SERVER: mongodb + ME_CONFIG_MONGODB_PORT: 27017 + ME_CONFIG_MONGODB_ENABLE_ADMIN: "true" + ME_CONFIG_MONGODB_AUTH_DATABASE: admin + ME_CONFIG_BASICAUTH_USERNAME: ${MONGO_EXPRESS_USER:-admin} + ME_CONFIG_BASICAUTH_PASSWORD: ${MONGO_EXPRESS_PASSWORD:?Set MONGO_EXPRESS_PASSWORD in .env} + ports: + - "8081:8081" + depends_on: + mongodb: + condition: service_healthy + networks: + - lexilingo-network + + # ============================================ + # Redis Cache (Backend) + # ============================================ + redis: + image: redis:7-alpine + container_name: lexilingo-redis + restart: unless-stopped + deploy: + resources: + limits: + memory: 256M + reservations: + memory: 128M + mem_limit: 256m + memswap_limit: 256m + command: redis-server --appendonly yes --maxmemory 200mb --maxmemory-policy allkeys-lru + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - lexilingo-network + + # ============================================ + # Redis Cache (AI Dedicated) + # ============================================ + redis-ai: + image: redis:7-alpine + container_name: lexilingo-redis-ai + restart: unless-stopped + deploy: + resources: + limits: + memory: 768M + reservations: + memory: 256M + mem_limit: 768m + memswap_limit: 768m + command: redis-server --appendonly yes --maxmemory 600mb --maxmemory-policy allkeys-lfu + volumes: + - redis_ai_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - lexilingo-network + + # ============================================ + # RedisInsight (Redis Web Management UI) + # ============================================ + redisinsight: + image: redis/redisinsight:latest + container_name: lexilingo-redisinsight + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + mem_limit: 512m + memswap_limit: 512m + ports: + - "8282:5540" + depends_on: + redis: + condition: service_healthy + redis-ai: + condition: service_healthy + networks: + - lexilingo-network + + # ============================================ + # Backend Service (FastAPI + PostgreSQL) + # ============================================ + backend-service: + build: + context: ./backend-service + dockerfile: Dockerfile + container_name: lexilingo-backend-service + restart: unless-stopped + env_file: + - ./backend-service/.env + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + mem_limit: 1g + memswap_limit: 1g + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} + SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} + DEBUG: ${DEBUG:-False} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:?Set ALLOWED_ORIGINS in .env} + AI_SERVICE_URL: http://ai-service:8001/api/v1 + # Redis (use container hostname, not localhost) + REDIS_URL: redis://redis:6379/0 + REDIS_HOST: redis + REDIS_PORT: 6379 + # Firebase + FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-lexilingo-88492} + FIREBASE_CREDENTIALS_FILE: /app/firebase-service-account.json + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_ADMIN_CLIENT_ID: ${GOOGLE_ADMIN_CLIENT_ID:-} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./backend-service/app:/app/app + - ./backend-service/alembic:/app/alembic + - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro + - ./backend-service/data:/app/data + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --proxy-headers --forwarded-allow-ips="*" + networks: + - lexilingo-network + + backend-reminder-worker: + build: + context: ./backend-service + dockerfile: Dockerfile + container_name: lexilingo-reminder-worker + restart: unless-stopped + command: celery -A app.core.celery_app:celery_app worker --loglevel=INFO --concurrency=1 + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} + SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} + DEBUG: ${DEBUG:-False} + REDIS_URL: redis://redis:6379/0 + REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} + REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} + FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-lexilingo-88492} + FIREBASE_CREDENTIALS_FILE: /app/firebase-service-account.json + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./backend-service/app:/app/app + - ./backend-service/alembic:/app/alembic + - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro + networks: + - lexilingo-network + + backend-reminder-beat: + build: + context: ./backend-service + dockerfile: Dockerfile + container_name: lexilingo-reminder-beat + restart: unless-stopped + command: celery -A app.core.celery_app:celery_app beat --loglevel=INFO + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} + SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} + DEBUG: ${DEBUG:-False} + REDIS_URL: redis://redis:6379/0 + REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} + REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} + depends_on: + redis: + condition: service_healthy + volumes: + - ./backend-service/app:/app/app + networks: + - lexilingo-network + + # ============================================ + # AI Service (FastAPI + MongoDB) + # ============================================ + ai-service: + build: + context: ./ai-service + dockerfile: Dockerfile + container_name: lexilingo-ai-service + restart: unless-stopped + deploy: + resources: + limits: + memory: 6G + reservations: + memory: 2G + mem_limit: 6g + memswap_limit: 6g + ports: + - "8001:8001" + environment: + ENVIRONMENT: development + SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} + MONGODB_URI: mongodb://mongodb:27017 + MONGODB_DB_NAME: lexilingo + REDIS_HOST: redis-ai + REDIS_PORT: 6379 + REDIS_PASSWORD: "" + REDIS_DB: 0 + REDIS_URL: redis://redis-ai:6379/0 + GEMINI_API_KEY: ${GEMINI_API_KEY} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:?Set ALLOWED_ORIGINS in .env} + DEBUG: ${DEBUG:-false} + depends_on: + mongodb: + condition: service_healthy + redis-ai: + condition: service_healthy + volumes: + - ./ai-service/api:/app/api + - ./ai-service/data:/app/data + - ./ai-service/models:/app/models + command: uvicorn api.main:app --host 0.0.0.0 --port 8001 --reload + networks: + - lexilingo-network + +volumes: + postgres_data: + driver: local + mongodb_data: + driver: local + mongodb_config: + driver: local + redis_data: + driver: local + redis_ai_data: + driver: local + +networks: + lexilingo-network: + driver: bridge diff --git a/docker-compose.production.yml b/docker-compose.production.yml deleted file mode 100644 index 3f35efaa..00000000 --- a/docker-compose.production.yml +++ /dev/null @@ -1,277 +0,0 @@ -services: - gateway: - image: nginx:1.27-alpine - container_name: lexilingo-gateway - restart: unless-stopped - depends_on: - backend-service: - condition: service_healthy - ai-service: - condition: service_healthy - environment: - GATEWAY_SERVER_NAME: ${GATEWAY_SERVER_NAME:-api.lexilingo.me} - GATEWAY_SSL_CERT_PATH: ${GATEWAY_SSL_CERT_PATH:-/etc/letsencrypt/live/api.lexilingo.me/fullchain.pem} - GATEWAY_SSL_KEY_PATH: ${GATEWAY_SSL_KEY_PATH:-/etc/letsencrypt/live/api.lexilingo.me/privkey.pem} - command: > - /bin/sh -c "envsubst '$$GATEWAY_SERVER_NAME $$GATEWAY_SSL_CERT_PATH $$GATEWAY_SSL_KEY_PATH' - < /etc/nginx/templates/default.conf.template - > /etc/nginx/conf.d/default.conf - && nginx -g 'daemon off;'" - ports: - - "80:80" - - "443:443" - volumes: - - ./gateway/nginx/templates:/etc/nginx/templates:ro - - ./gateway/nginx/snippets:/etc/nginx/snippets:ro - - ./gateway/nginx/acme-challenge:/var/www/certbot - - ./gateway/nginx/logs:/var/log/nginx - - /etc/letsencrypt:/etc/letsencrypt:ro - healthcheck: - test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/health || exit 1"] - interval: 30s - timeout: 5s - retries: 5 - start_period: 15s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "5" - networks: - - lexilingo-prod - - postgres: - image: postgres:16-alpine - container_name: lexilingo-postgres - restart: unless-stopped - command: postgres -c shared_buffers=128MB -c work_mem=4MB -c max_connections=100 - deploy: - resources: - limits: - memory: 512M - environment: - POSTGRES_USER: ${POSTGRES_USER:-lexilingo} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env.production.secrets} - POSTGRES_DB: ${POSTGRES_DB:-lexilingo} - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] - interval: 10s - timeout: 5s - retries: 5 - networks: - - lexilingo-prod - - mongodb: - image: mongo:7.0 - container_name: lexilingo-mongodb - restart: unless-stopped - command: mongod --wiredTigerCacheSizeGB 0.25 - deploy: - resources: - limits: - memory: 512M - environment: - MONGO_INITDB_DATABASE: ${MONGODB_DATABASE:-lexilingo} - volumes: - - mongodb_data:/data/db - - mongodb_config:/data/configdb - healthcheck: - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] - interval: 15s - timeout: 10s - retries: 5 - start_period: 30s - networks: - - lexilingo-prod - - redis: - image: redis:7-alpine - container_name: lexilingo-redis - restart: unless-stopped - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set in .env.production.secrets} - command: /bin/sh -c 'redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass "$$REDIS_PASSWORD"' - volumes: - - redis_data:/data - healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - lexilingo-prod - - backend-service: - build: - context: ./backend-service - dockerfile: Dockerfile.prod - # TIP: To run multiple replicas for load balancing, remove `container_name` - # and run: docker-compose up --scale backend-service=2 - # Nginx's `least_conn` upstream will automatically distribute traffic - # across all replicas via Docker's internal DNS round-robin. - container_name: lexilingo-backend-service - restart: unless-stopped - deploy: - resources: - limits: - cpus: "1.0" - memory: 1G - reservations: - cpus: "0.25" - memory: 256M - env_file: - - .env.production - - .env.production.secrets - environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} - REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 - REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} - AI_SERVICE_URL: http://ai-service:8001/api/v1 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - expose: - - "8000" - volumes: - - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)\""] - interval: 30s - timeout: 5s - retries: 5 - start_period: 60s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "5" - networks: - - lexilingo-prod - - backend-reminder-worker: - build: - context: ./backend-service - dockerfile: Dockerfile.prod - container_name: lexilingo-reminder-worker - restart: unless-stopped - command: celery -A app.core.celery_app:celery_app worker --loglevel=INFO --concurrency=1 - env_file: - - .env.production - - .env.production.secrets - environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} - REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 - REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} - REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} - REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro - logging: - driver: json-file - options: - max-size: "10m" - max-file: "5" - networks: - - lexilingo-prod - - backend-reminder-beat: - build: - context: ./backend-service - dockerfile: Dockerfile.prod - container_name: lexilingo-reminder-beat - restart: unless-stopped - command: celery -A app.core.celery_app:celery_app beat --loglevel=INFO - env_file: - - .env.production - - .env.production.secrets - environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} - REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 - REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} - REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} - REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} - depends_on: - redis: - condition: service_healthy - logging: - driver: json-file - options: - max-size: "10m" - max-file: "5" - networks: - - lexilingo-prod - - ai-service: - build: - context: ./ai-service - dockerfile: Dockerfile.prod - container_name: lexilingo-ai-service - restart: unless-stopped - deploy: - resources: - limits: - cpus: "2.0" - memory: 2G - reservations: - cpus: "0.5" - memory: 512M - env_file: - - .env.production - - .env.production.secrets - environment: - ENVIRONMENT: production - MONGODB_URI: mongodb://mongodb:27017 - MONGODB_DATABASE: ${MONGODB_DATABASE:-lexilingo} - REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/1 - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} - REDIS_DB: 1 - AI_MODEL_API_URL: http://ai-service:8001 - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - depends_on: - mongodb: - condition: service_healthy - redis: - condition: service_healthy - expose: - - "8001" - extra_hosts: - - "host.docker.internal:host-gateway" - volumes: - - ai_models:/app/models - - ./ai-service/data:/app/data - - ./backend-service/data/media:/app/data/media:ro - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=3)\""] - interval: 30s - timeout: 5s - retries: 5 - start_period: 90s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "5" - networks: - - lexilingo-prod - -volumes: - postgres_data: - mongodb_data: - mongodb_config: - redis_data: - ai_models: - -networks: - lexilingo-prod: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index f69b6600..97782ba8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,361 +1,281 @@ services: - # ============================================ - # PostgreSQL Database (Backend Service) - # ============================================ + gateway: + image: nginx:1.27-alpine + container_name: lexilingo-gateway + restart: unless-stopped + depends_on: + backend-service: + condition: service_healthy + ai-service: + condition: service_healthy + environment: + GATEWAY_SERVER_NAME: ${GATEWAY_SERVER_NAME:-api.lexilingo.me} + GATEWAY_SSL_CERT_PATH: ${GATEWAY_SSL_CERT_PATH:-/etc/letsencrypt/live/api.lexilingo.me/fullchain.pem} + GATEWAY_SSL_KEY_PATH: ${GATEWAY_SSL_KEY_PATH:-/etc/letsencrypt/live/api.lexilingo.me/privkey.pem} + command: > + /bin/sh -c "envsubst '$$GATEWAY_SERVER_NAME $$GATEWAY_SSL_CERT_PATH $$GATEWAY_SSL_KEY_PATH' + < /etc/nginx/templates/default.conf.template + > /etc/nginx/conf.d/default.conf + && nginx -g 'daemon off;'" + ports: + - "80:80" + - "443:443" + volumes: + - ./gateway/nginx/templates:/etc/nginx/templates:ro + - ./gateway/nginx/snippets:/etc/nginx/snippets:ro + - ./gateway/nginx/acme-challenge:/var/www/certbot + - ./gateway/nginx/logs:/var/log/nginx + - /etc/letsencrypt:/etc/letsencrypt:ro + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/health || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + networks: + - lexilingo-prod + postgres: image: postgres:16-alpine container_name: lexilingo-postgres restart: unless-stopped + command: postgres -c shared_buffers=128MB -c work_mem=4MB -c max_connections=100 deploy: resources: limits: memory: 512M - reservations: - memory: 256M - mem_limit: 512m - memswap_limit: 512m environment: POSTGRES_USER: ${POSTGRES_USER:-lexilingo} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env.production.secrets} POSTGRES_DB: ${POSTGRES_DB:-lexilingo} - ports: - - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U lexilingo"] + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] interval: 10s timeout: 5s retries: 5 networks: - - lexilingo-network + - lexilingo-prod - # ============================================ - # pgAdmin (PostgreSQL Web Management UI) - # ============================================ - pgadmin: - image: dpage/pgadmin4 - container_name: lexilingo-pgadmin - restart: unless-stopped - deploy: - resources: - limits: - memory: 768M - reservations: - memory: 256M - mem_limit: 768m - memswap_limit: 768m - environment: - PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@lexilingo.me} - PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:?Set PGADMIN_DEFAULT_PASSWORD in .env} - PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "True" - PGADMIN_CONFIG_LOGIN_BANNER: "'LexiLingo Database Management'" - ports: - - "5050:80" - depends_on: - postgres: - condition: service_healthy - networks: - - lexilingo-network - - # ============================================ - # MongoDB Database (AI Service) - # ============================================ mongodb: image: mongo:7.0 container_name: lexilingo-mongodb restart: unless-stopped + command: mongod --wiredTigerCacheSizeGB 0.25 deploy: resources: limits: - memory: 1G - reservations: memory: 512M - mem_limit: 1g - memswap_limit: 1g - ports: - - "27017:27017" environment: - MONGO_INITDB_DATABASE: lexilingo + MONGO_INITDB_DATABASE: ${MONGODB_DATABASE:-lexilingo} volumes: - mongodb_data:/data/db - mongodb_config:/data/configdb - - ./ai-service/scripts/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro healthcheck: test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] - interval: 10s - timeout: 5s + interval: 15s + timeout: 10s retries: 5 - start_period: 40s - networks: - - lexilingo-network - - # ============================================ - # Mongo Express (MongoDB Web Management UI) - # ============================================ - mongo-express: - image: mongo-express:latest - container_name: lexilingo-mongo-express - restart: unless-stopped - deploy: - resources: - limits: - memory: 256M - reservations: - memory: 128M - mem_limit: 256m - memswap_limit: 256m - environment: - ME_CONFIG_MONGODB_SERVER: mongodb - ME_CONFIG_MONGODB_PORT: 27017 - ME_CONFIG_MONGODB_ENABLE_ADMIN: "true" - ME_CONFIG_MONGODB_AUTH_DATABASE: admin - ME_CONFIG_BASICAUTH_USERNAME: ${MONGO_EXPRESS_USER:-admin} - ME_CONFIG_BASICAUTH_PASSWORD: ${MONGO_EXPRESS_PASSWORD:?Set MONGO_EXPRESS_PASSWORD in .env} - ports: - - "8081:8081" - depends_on: - mongodb: - condition: service_healthy + start_period: 30s networks: - - lexilingo-network + - lexilingo-prod - # ============================================ - # Redis Cache (Backend) - # ============================================ redis: image: redis:7-alpine container_name: lexilingo-redis restart: unless-stopped - deploy: - resources: - limits: - memory: 256M - reservations: - memory: 128M - mem_limit: 256m - memswap_limit: 256m - command: redis-server --appendonly yes --maxmemory 200mb --maxmemory-policy allkeys-lru - ports: - - "6379:6379" + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set in .env.production.secrets} + command: /bin/sh -c 'redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass "$$REDIS_PASSWORD"' volumes: - redis_data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping"] interval: 10s timeout: 5s retries: 5 networks: - - lexilingo-network + - lexilingo-prod - # ============================================ - # Redis Cache (AI Dedicated) - # ============================================ - redis-ai: - image: redis:7-alpine - container_name: lexilingo-redis-ai - restart: unless-stopped - deploy: - resources: - limits: - memory: 768M - reservations: - memory: 256M - mem_limit: 768m - memswap_limit: 768m - command: redis-server --appendonly yes --maxmemory 600mb --maxmemory-policy allkeys-lfu - volumes: - - redis_ai_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - lexilingo-network - - # ============================================ - # RedisInsight (Redis Web Management UI) - # ============================================ - redisinsight: - image: redis/redisinsight:latest - container_name: lexilingo-redisinsight - restart: unless-stopped - deploy: - resources: - limits: - memory: 512M - reservations: - memory: 256M - mem_limit: 512m - memswap_limit: 512m - ports: - - "8282:5540" - depends_on: - redis: - condition: service_healthy - redis-ai: - condition: service_healthy - networks: - - lexilingo-network - - # ============================================ - # Backend Service (FastAPI + PostgreSQL) - # ============================================ backend-service: build: context: ./backend-service - dockerfile: Dockerfile + dockerfile: Dockerfile.prod + # TIP: To run multiple replicas for load balancing, remove `container_name` + # and run: docker-compose up --scale backend-service=2 + # Nginx's `least_conn` upstream will automatically distribute traffic + # across all replicas via Docker's internal DNS round-robin. container_name: lexilingo-backend-service restart: unless-stopped - env_file: - - ./backend-service/.env deploy: resources: limits: + cpus: "1.0" memory: 1G reservations: - memory: 512M - mem_limit: 1g - memswap_limit: 1g - ports: - - "8000:8000" + cpus: "0.25" + memory: 256M + env_file: + - .env.production + - .env.production.secrets environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} - SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} - DEBUG: ${DEBUG:-False} - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:?Set ALLOWED_ORIGINS in .env} + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} AI_SERVICE_URL: http://ai-service:8001/api/v1 - # Redis (use container hostname, not localhost) - REDIS_URL: redis://redis:6379/0 - REDIS_HOST: redis - REDIS_PORT: 6379 - # Firebase - FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-lexilingo-88492} - FIREBASE_CREDENTIALS_FILE: /app/firebase-service-account.json - GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} - GOOGLE_ADMIN_CLIENT_ID: ${GOOGLE_ADMIN_CLIENT_ID:-} depends_on: postgres: condition: service_healthy redis: condition: service_healthy + expose: + - "8000" volumes: - - ./backend-service/app:/app/app - - ./backend-service/alembic:/app/alembic - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro - - ./backend-service/data:/app/data - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --proxy-headers --forwarded-allow-ips="*" + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)\""] + interval: 30s + timeout: 5s + retries: 5 + start_period: 60s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" networks: - - lexilingo-network + - lexilingo-prod backend-reminder-worker: build: context: ./backend-service - dockerfile: Dockerfile + dockerfile: Dockerfile.prod container_name: lexilingo-reminder-worker restart: unless-stopped command: celery -A app.core.celery_app:celery_app worker --loglevel=INFO --concurrency=1 + env_file: + - .env.production + - .env.production.secrets environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} - SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} - DEBUG: ${DEBUG:-False} - REDIS_URL: redis://redis:6379/0 + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} - FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-lexilingo-88492} - FIREBASE_CREDENTIALS_FILE: /app/firebase-service-account.json depends_on: postgres: condition: service_healthy redis: condition: service_healthy volumes: - - ./backend-service/app:/app/app - - ./backend-service/alembic:/app/alembic - ./backend-service/firebase-service-account.json:/app/firebase-service-account.json:ro + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" networks: - - lexilingo-network + - lexilingo-prod + healthcheck: + disable: true backend-reminder-beat: build: context: ./backend-service - dockerfile: Dockerfile + dockerfile: Dockerfile.prod container_name: lexilingo-reminder-beat restart: unless-stopped command: celery -A app.core.celery_app:celery_app beat --loglevel=INFO + env_file: + - .env.production + - .env.production.secrets environment: - DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-lexilingo} - SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} - DEBUG: ${DEBUG:-False} - REDIS_URL: redis://redis:6379/0 + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-lexilingo}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@postgres:5432/${POSTGRES_DB:-lexilingo} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/0 + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} REMINDERS_ENABLED: ${REMINDERS_ENABLED:-false} REMINDER_DRY_RUN: ${REMINDER_DRY_RUN:-true} depends_on: redis: condition: service_healthy - volumes: - - ./backend-service/app:/app/app + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" networks: - - lexilingo-network + - lexilingo-prod + healthcheck: + disable: true - # ============================================ - # AI Service (FastAPI + MongoDB) - # ============================================ ai-service: build: context: ./ai-service - dockerfile: Dockerfile + dockerfile: Dockerfile.prod container_name: lexilingo-ai-service restart: unless-stopped deploy: resources: limits: - memory: 6G - reservations: + cpus: "2.0" memory: 2G - mem_limit: 6g - memswap_limit: 6g - ports: - - "8001:8001" + reservations: + cpus: "0.5" + memory: 512M + env_file: + - .env.production + - .env.production.secrets environment: - ENVIRONMENT: development - SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env} + ENVIRONMENT: production MONGODB_URI: mongodb://mongodb:27017 - MONGODB_DB_NAME: lexilingo - REDIS_HOST: redis-ai + MONGODB_DATABASE: ${MONGODB_DATABASE:-lexilingo} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379/1 + REDIS_HOST: redis REDIS_PORT: 6379 - REDIS_PASSWORD: "" - REDIS_DB: 0 - REDIS_URL: redis://redis-ai:6379/0 - GEMINI_API_KEY: ${GEMINI_API_KEY} - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:?Set ALLOWED_ORIGINS in .env} - DEBUG: ${DEBUG:-false} + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD must be set} + REDIS_DB: 1 + AI_MODEL_API_URL: http://ai-service:8001 + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} depends_on: mongodb: condition: service_healthy - redis-ai: + redis: condition: service_healthy + expose: + - "8001" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - - ./ai-service/api:/app/api + - ai_models:/app/models - ./ai-service/data:/app/data - - ./ai-service/models:/app/models - command: uvicorn api.main:app --host 0.0.0.0 --port 8001 --reload + - ./backend-service/data/media:/app/data/media:ro + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=3)\""] + interval: 30s + timeout: 5s + retries: 5 + start_period: 90s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" networks: - - lexilingo-network + - lexilingo-prod volumes: postgres_data: - driver: local mongodb_data: - driver: local mongodb_config: - driver: local redis_data: - driver: local - redis_ai_data: - driver: local + ai_models: networks: - lexilingo-network: + lexilingo-prod: driver: bridge diff --git a/scripts/backup-prod.sh b/scripts/backup-prod.sh index 1e1f2364..b600a48e 100755 --- a/scripts/backup-prod.sh +++ b/scripts/backup-prod.sh @@ -4,7 +4,7 @@ set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${PROJECT_ROOT}" -DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.production.yml" +DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.yml" BACKUP_DIR="${BACKUP_DIR:-${PROJECT_ROOT}/backups}" RETENTION_DAYS="${RETENTION_DAYS:-14}" TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)" diff --git a/scripts/deploy-one-shot.sh b/scripts/deploy-one-shot.sh index c3746f5d..8b46b315 100755 --- a/scripts/deploy-one-shot.sh +++ b/scripts/deploy-one-shot.sh @@ -2,12 +2,12 @@ set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.production.yml" +COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" ENV_FILE="${PROJECT_ROOT}/.env.production" SECRET_ENV_FILE="${PROJECT_ROOT}/.env.production.secrets" SMOKE_SCRIPT="${PROJECT_ROOT}/scripts/smoke-prod.sh" DEPLOY_DIR="${PROJECT_ROOT}/.deploy" -DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.production.yml" +DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.yml" SKIP_GIT_PULL=false SKIP_IMAGE_PULL=false diff --git a/scripts/restore-prod.sh b/scripts/restore-prod.sh index 0f5cc8c9..f5db069e 100755 --- a/scripts/restore-prod.sh +++ b/scripts/restore-prod.sh @@ -4,7 +4,7 @@ set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${PROJECT_ROOT}" -DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.production.yml" +DOCKER_COMPOSE_CMD="sudo docker compose --env-file .env.production --env-file .env.production.secrets -f docker-compose.yml" POSTGRES_BACKUP="" MONGO_BACKUP="" FORCE=false diff --git a/scripts/ssl/reload-gateway-after-renew.sh b/scripts/ssl/reload-gateway-after-renew.sh index 8c82ebde..0cd383aa 100755 --- a/scripts/ssl/reload-gateway-after-renew.sh +++ b/scripts/ssl/reload-gateway-after-renew.sh @@ -6,7 +6,7 @@ set -euo pipefail # sudo COMPOSE_PROJECT_ROOT=/opt/lexilingo bash scripts/ssl/reload-gateway-after-renew.sh COMPOSE_PROJECT_ROOT="${COMPOSE_PROJECT_ROOT:-/opt/lexilingo}" -COMPOSE_FILE="${COMPOSE_PROJECT_ROOT}/docker-compose.production.yml" +COMPOSE_FILE="${COMPOSE_PROJECT_ROOT}/docker-compose.yml" ENV_FILE="${COMPOSE_PROJECT_ROOT}/.env.production" if [[ ! -f "${COMPOSE_FILE}" ]]; then diff --git a/scripts/start-all.sh b/scripts/start-all.sh index b42407bd..63cdd519 100755 --- a/scripts/start-all.sh +++ b/scripts/start-all.sh @@ -293,10 +293,10 @@ echo "" if command -v docker &>/dev/null; then if ! check_service 5432 || ! check_service 27017 || ! check_service 6379; then echo -e "${BLUE}[START] Starting Database containers (PostgreSQL, MongoDB, Redis)...${NC}" - docker compose -f "$PROJECT_ROOT/docker-compose.yml" up -d postgres pgadmin mongodb mongo-express redis redisinsight >> "$LOG_DIR/databases.log" 2>&1 + docker compose -f "$PROJECT_ROOT/docker-compose.dev.yml" up -d postgres pgadmin mongodb mongo-express redis redisinsight >> "$LOG_DIR/databases.log" 2>&1 # Wait up to 15s for postgres to be ready for i in $(seq 1 15); do - docker compose -f "$PROJECT_ROOT/docker-compose.yml" exec -T postgres pg_isready -U lexilingo >/dev/null 2>&1 && break + docker compose -f "$PROJECT_ROOT/docker-compose.dev.yml" exec -T postgres pg_isready -U lexilingo >/dev/null 2>&1 && break sleep 1 done else diff --git a/scripts/stop-all.sh b/scripts/stop-all.sh index 569f82b8..38892ccb 100755 --- a/scripts/stop-all.sh +++ b/scripts/stop-all.sh @@ -27,8 +27,8 @@ if command -v docker &> /dev/null; then cd "$PROJECT_ROOT" # Stop compose services without removing containers/networks/volumes. - if [ -f "docker-compose.yml" ]; then - docker compose stop 2>/dev/null || docker-compose stop 2>/dev/null || true + if [ -f "docker-compose.dev.yml" ]; then + docker compose -f docker-compose.dev.yml stop 2>/dev/null || true fi # Stop known LexiLingo containers if they are still running. From 546aaf6e09693c8965582a048db671d3e0951ea3 Mon Sep 17 00:00:00 2001 From: InfinityZero3000 Date: Tue, 16 Jun 2026 20:55:46 +0700 Subject: [PATCH 21/61] feat: implement daily login rewards, streak restore, and training activity streak updates --- ...1c2120a87_add_streak_restore_and_reward.py | 36 ++ backend-service/app/models/progress.py | 6 + backend-service/app/routes/games.py | 5 + backend-service/app/routes/learning.py | 3 +- backend-service/app/routes/progress.py | 274 +++++++----- backend-service/app/routes/vocabulary.py | 8 + .../app/services/streak_service.py | 135 ++++++ backend-service/tests/test_progress_routes.py | 130 ++++++ flutter-app/assets/i18n/en.json | 5 + flutter-app/assets/i18n/vi.json | 5 + .../home/presentation/pages/home_page.dart | 30 +- .../progress_remote_datasource.dart | 28 ++ .../progress/data/models/streak_model.dart | 47 ++ .../progress_repository_impl.dart | 32 ++ .../domain/entities/streak_entity.dart | 45 ++ .../repositories/progress_repository.dart | 6 + .../providers/streak_provider.dart | 76 ++++ .../widgets/daily_reward_dialog.dart | 415 ++++++++++++++++++ .../presentation/widgets/streak_widget.dart | 193 +++++++- 19 files changed, 1366 insertions(+), 113 deletions(-) create mode 100644 backend-service/alembic/versions/bd41c2120a87_add_streak_restore_and_reward.py create mode 100644 backend-service/app/services/streak_service.py create mode 100644 flutter-app/lib/features/progress/presentation/widgets/daily_reward_dialog.dart diff --git a/backend-service/alembic/versions/bd41c2120a87_add_streak_restore_and_reward.py b/backend-service/alembic/versions/bd41c2120a87_add_streak_restore_and_reward.py new file mode 100644 index 00000000..e54c1560 --- /dev/null +++ b/backend-service/alembic/versions/bd41c2120a87_add_streak_restore_and_reward.py @@ -0,0 +1,36 @@ +"""add_streak_restore_and_reward + +Revision ID: bd41c2120a87 +Revises: game_award_idempotency +Create Date: 2026-06-16 12:34:03.334869 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'bd41c2120a87' +down_revision: Union[str, None] = 'game_award_idempotency' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('streaks', sa.Column('previous_streak', sa.Integer(), server_default='0', nullable=False)) + op.add_column('streaks', sa.Column('restores_used_this_month', sa.Integer(), server_default='0', nullable=False)) + op.add_column('streaks', sa.Column('last_restore_date', sa.Date(), nullable=True)) + op.add_column('streaks', sa.Column('last_reward_claim_date', sa.Date(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('streaks', 'last_reward_claim_date') + op.drop_column('streaks', 'last_restore_date') + op.drop_column('streaks', 'restores_used_this_month') + op.drop_column('streaks', 'previous_streak') + # ### end Alembic commands ### diff --git a/backend-service/app/models/progress.py b/backend-service/app/models/progress.py index 5d69e95f..91155033 100644 --- a/backend-service/app/models/progress.py +++ b/backend-service/app/models/progress.py @@ -376,6 +376,12 @@ class Streak(Base): # Streak freeze (gamification) freeze_count: Mapped[int] = mapped_column(Integer, default=0) + # Streak restore & daily rewards + previous_streak: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + restores_used_this_month: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + last_restore_date: Mapped[date] = mapped_column(Date, nullable=True) + last_reward_claim_date: Mapped[date] = mapped_column(Date, nullable=True) + created_at: Mapped[datetime] = mapped_column(TZDateTime, default=lambda: datetime.now(timezone.utc)) updated_at: Mapped[datetime] = mapped_column( TZDateTime, diff --git a/backend-service/app/routes/games.py b/backend-service/app/routes/games.py index 7d68012f..bd54fbbb 100644 --- a/backend-service/app/routes/games.py +++ b/backend-service/app/routes/games.py @@ -38,6 +38,7 @@ from app.services.xp_service import award_xp_transaction, get_existing_xp_award from app.core.cache import build_cache_key, delete_cached, invalidate_cache from app.services import check_achievements_for_user +from app.services.streak_service import update_user_streak logger = logging.getLogger(__name__) @@ -1612,6 +1613,10 @@ async def complete_game_session( "base_xp": score.final_base_xp, }, } + try: + await update_user_streak(db, current_user.id) + except Exception as e: + logger.error("Error updating streak on game completion: %s", e, exc_info=True) await db.commit() await invalidate_cache("leaderboard") diff --git a/backend-service/app/routes/learning.py b/backend-service/app/routes/learning.py index 8852effb..c9e0975a 100644 --- a/backend-service/app/routes/learning.py +++ b/backend-service/app/routes/learning.py @@ -15,6 +15,7 @@ from app.core.database import get_db from app.core.cache import build_cache_key, delete_cached from app.core.dependencies import get_current_user +from app.services.streak_service import update_user_streak from app.models.user import User from app.models.course import Course, Unit, Lesson from app.models.progress import ( @@ -940,7 +941,7 @@ async def complete_lesson( db.add(daily_activity) # Update streak - await _update_streak(db, current_user.id) + await update_user_streak(db, current_user.id) # Check achievements after lesson completion unlocked_achievements = [] diff --git a/backend-service/app/routes/progress.py b/backend-service/app/routes/progress.py index 3318a741..b919bed4 100644 --- a/backend-service/app/routes/progress.py +++ b/backend-service/app/routes/progress.py @@ -45,6 +45,8 @@ calculate_rank as calc_rank, check_rank_up, ) +from app.services.streak_service import update_user_streak +from app.crud.gamification import WalletCRUD router = APIRouter(prefix="/progress", tags=["Progress"]) logger = logging.getLogger(__name__) @@ -563,7 +565,8 @@ async def get_my_streak( cache_key = build_cache_key("progress_streak", user_id=uid) cached = await get_cached(cache_key) if cached is not None: - return ApiResponse(success=True, message="Streak retrieved successfully", data=cached) + if all(k in cached for k in ['previous_streak', 'restores_used_this_month', 'restores_remaining', 'can_restore', 'is_daily_reward_available']): + return ApiResponse(success=True, message="Streak retrieved successfully", data=cached) result = await db.execute( select(Streak).where(Streak.user_id == current_user.id) @@ -580,7 +583,9 @@ async def get_my_streak( current_streak=0, longest_streak=0, total_days_active=0, - freeze_count=0 + freeze_count=0, + previous_streak=0, + restores_used_this_month=0 ) db.add(streak) await db.commit() @@ -594,6 +599,12 @@ async def get_my_streak( streak = result.scalar_one_or_none() if not streak: raise + else: + # Check monthly reset for restores + if streak.last_restore_date and (today.year != streak.last_restore_date.year or today.month != streak.last_restore_date.month): + streak.restores_used_this_month = 0 + await db.commit() + await db.refresh(streak) # Determine if active today and if streak is at risk is_active_today = streak.last_activity_date == today if streak.last_activity_date else False @@ -639,6 +650,11 @@ async def get_my_streak( 'is_active_today': is_active_today, 'streak_at_risk': streak_at_risk and streak.current_streak > 0, 'weekly_activity': weekly_activity, + 'previous_streak': streak.previous_streak if isinstance(streak.previous_streak, int) else 0, + 'restores_used_this_month': streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0, + 'restores_remaining': max(0, 3 - (streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0)), + 'can_restore': (streak.previous_streak if isinstance(streak.previous_streak, int) else 0) > 0 and (streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0) < 3, + 'is_daily_reward_available': is_active_today and (streak.last_reward_claim_date != today or streak.last_reward_claim_date is None), } await set_cached(cache_key, response_data, ttl=30) @@ -671,106 +687,10 @@ async def update_streak( - streak_increased: Whether streak went up - streak_saved: Whether freeze was used """ - result = await db.execute( - select(Streak).where(Streak.user_id == current_user.id) - ) - streak = result.scalar_one_or_none() - - today = date.today() - streak_increased = False - streak_saved = False - - if not streak: - # Create new streak - streak = Streak( - user_id=current_user.id, - current_streak=1, - longest_streak=1, - last_activity_date=today, - total_days_active=1, - freeze_count=0 - ) - db.add(streak) - streak_increased = True - else: - last_date = streak.last_activity_date - - if last_date == today: - # Already active today, no change - pass - elif last_date == today - timedelta(days=1): - # Consecutive day - increment streak - streak.current_streak += 1 - streak.total_days_active += 1 - streak.last_activity_date = today - streak_increased = True - - if streak.current_streak > streak.longest_streak: - streak.longest_streak = streak.current_streak - elif last_date and last_date < today - timedelta(days=1): - # Gap in activity - days_missed = (today - last_date).days - 1 - - if streak.freeze_count > 0 and days_missed == 1: - # Use freeze to save streak - streak.freeze_count -= 1 - streak.current_streak += 1 - streak.total_days_active += 1 - streak.last_activity_date = today - streak_saved = True - streak_increased = True - - if streak.current_streak > streak.longest_streak: - streak.longest_streak = streak.current_streak - else: - # Reset streak - streak.current_streak = 1 - streak.total_days_active += 1 - streak.last_activity_date = today - streak_increased = True - else: - # First activity ever - streak.current_streak = 1 - streak.total_days_active = 1 - streak.last_activity_date = today - streak_increased = True - - if streak.current_streak > streak.longest_streak: - streak.longest_streak = streak.current_streak - - # Ensure a DailyActivity record exists for today so weekly_activity is accurate - daily_result = await db.execute( - select(DailyActivity).where( - and_( - DailyActivity.user_id == current_user.id, - DailyActivity.activity_date == today, - ) - ) - ) - daily_activity = daily_result.scalar_one_or_none() - if not daily_activity: - daily_activity = DailyActivity( - user_id=current_user.id, - activity_date=today, - xp_earned=0, - lessons_completed=0, - study_time_minutes=0, - vocabulary_reviewed=0, - ) - db.add(daily_activity) - + streak, streak_increased, streak_saved, unlocked_achievements = await update_user_streak(db, current_user.id) await db.commit() await db.refresh(streak) - # Check streak-based achievements - unlocked_achievements = [] - try: - unlocked_achievements = await check_achievements_for_user( - db, current_user.id, "streak_update" - ) - except Exception as e: - logger.error("Achievement check error: %s", e, exc_info=True) - message = "Streak updated" if streak_saved: message = "Streak freeze used! Your streak is saved" @@ -785,13 +705,13 @@ async def update_streak( 'streak_increased': streak_increased, 'streak_saved': streak_saved, 'achievements_unlocked': unlocked_achievements, + 'previous_streak': streak.previous_streak if isinstance(streak.previous_streak, int) else 0, + 'restores_used_this_month': streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0, + 'restores_remaining': max(0, 3 - (streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0)), + 'can_restore': (streak.previous_streak if isinstance(streak.previous_streak, int) else 0) > 0 and (streak.restores_used_this_month if isinstance(streak.restores_used_this_month, int) else 0) < 3, + 'is_daily_reward_available': streak.last_activity_date == date.today() and (streak.last_reward_claim_date != date.today() or streak.last_reward_claim_date is None), } - # Invalidate the streak cache so the next GET reflects the new value - uid = str(current_user.id) - await delete_cached(build_cache_key("progress_streak", user_id=uid)) - await delete_cached(build_cache_key("progress_me", user_id=uid)) - return ApiResponse( success=True, message=message, @@ -858,3 +778,149 @@ async def use_streak_freeze( 'freeze_used': True } ) + + +@router.post("/streak/restore", response_model=ApiResponse[dict]) +async def restore_streak( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Restore a broken streak using one of the 3 monthly restores + """ + result = await db.execute( + select(Streak).where(Streak.user_id == current_user.id) + ) + streak = result.scalar_one_or_none() + + if not streak: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No streak record found to restore" + ) + + today = date.today() + + # Check monthly reset + if streak.last_restore_date and (today.year != streak.last_restore_date.year or today.month != streak.last_restore_date.month): + streak.restores_used_this_month = 0 + + if streak.restores_used_this_month >= 3: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You have already used your 3 streak restores for this month" + ) + + if streak.previous_streak <= 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No previous streak is available to restore" + ) + + # Restore the streak: current_streak = previous_streak + 1 + old_streak = streak.previous_streak + streak.current_streak = old_streak + 1 + streak.previous_streak = 0 + streak.last_restore_date = today + streak.restores_used_this_month += 1 + + if streak.current_streak > streak.longest_streak: + streak.longest_streak = streak.current_streak + + await db.commit() + await db.refresh(streak) + + # Invalidate cache + uid = str(current_user.id) + await delete_cached(build_cache_key("progress_streak", user_id=uid)) + await delete_cached(build_cache_key("progress_me", user_id=uid)) + + response_data = { + 'current_streak': streak.current_streak, + 'longest_streak': streak.longest_streak, + 'total_days_active': streak.total_days_active, + 'freeze_count': streak.freeze_count, + 'previous_streak': streak.previous_streak, + 'restores_used_this_month': streak.restores_used_this_month, + 'restores_remaining': max(0, 3 - streak.restores_used_this_month), + 'can_restore': False, + 'is_daily_reward_available': streak.last_activity_date == today and (streak.last_reward_claim_date != today or streak.last_reward_claim_date is None), + } + + return ApiResponse( + success=True, + message=f"Streak restored to {streak.current_streak} days!", + data=response_data + ) + + +@router.post("/streak/claim-daily-reward", response_model=ApiResponse[dict]) +async def claim_daily_reward( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Claim the daily login reward based on current streak cycle (1-7 days) + """ + result = await db.execute( + select(Streak).where(Streak.user_id == current_user.id) + ) + streak = result.scalar_one_or_none() + + if not streak: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No streak record found" + ) + + today = date.today() + + if streak.last_activity_date != today: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You must complete a learning activity today before claiming your reward" + ) + + if streak.last_reward_claim_date == today: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You have already claimed today's reward" + ) + + # Calculate gems based on streak cycle: + # 7-day cycle: 5, 10, 15, 20, 25, 30, 50 gems + rewards_cycle = [5, 10, 15, 20, 25, 30, 50] + day_index = (streak.current_streak - 1) % 7 + reward_gems = rewards_cycle[day_index] + + # Award gems + wallet, transaction = await WalletCRUD.add_gems( + db=db, + user_id=current_user.id, + amount=reward_gems, + source="daily_streak_reward", + description=f"Claimed {reward_gems} gems for day {day_index + 1} of streak", + commit=False + ) + + streak.last_reward_claim_date = today + await db.commit() + await db.refresh(streak) + + # Invalidate cache + uid = str(current_user.id) + await delete_cached(build_cache_key("progress_streak", user_id=uid)) + await delete_cached(build_cache_key("progress_me", user_id=uid)) + + response_data = { + 'gems_awarded': reward_gems, + 'total_gems': wallet.gems, + 'current_streak': streak.current_streak, + 'is_daily_reward_available': False, + } + + return ApiResponse( + success=True, + message=f"Successfully claimed {reward_gems} gems!", + data=response_data + ) diff --git a/backend-service/app/routes/vocabulary.py b/backend-service/app/routes/vocabulary.py index 39d8f8a6..6494d458 100644 --- a/backend-service/app/routes/vocabulary.py +++ b/backend-service/app/routes/vocabulary.py @@ -25,6 +25,7 @@ from app.clients.ai_service_client import AIServiceClient from app.models.user import User from app.crud.vocabulary import vocabulary_crud +from app.services.streak_service import update_user_streak from app.schemas.common import MessageResponse from app.schemas.vocabulary import ( VocabularyItemResponse, @@ -611,6 +612,13 @@ async def submit_review( daily_goal_met=daily_goal_met )) + # Update user streak on vocab review + try: + await update_user_streak(db, current_user.id) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Error updating streak on vocabulary review: {e}", exc_info=True) + # Invalidate user's progress caches _uid = str(user_id) await delete_cached(build_cache_key("progress_me", user_id=_uid)) diff --git a/backend-service/app/services/streak_service.py b/backend-service/app/services/streak_service.py new file mode 100644 index 00000000..53a28bb7 --- /dev/null +++ b/backend-service/app/services/streak_service.py @@ -0,0 +1,135 @@ +import logging +from datetime import date, timedelta +from uuid import UUID +from sqlalchemy import select, and_ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.progress import Streak, DailyActivity +from app.core.cache import build_cache_key, delete_cached +from app.services import check_achievements_for_user + +logger = logging.getLogger(__name__) + +async def update_user_streak(db: AsyncSession, user_id: UUID) -> tuple[Streak, bool, bool, list]: + """ + Unified function to update user's streak. + Handles: + - Creating streak record if first time + - Incrementing streak for consecutive days + - Using streak freeze if available when a gap is 1 day + - Resetting streak if gap > 1 day, saving current streak to previous_streak + - Ensuring DailyActivity record exists + - Checking streak-based achievements + - Invalidating redis cache + + Returns: + - (streak, streak_increased, streak_saved, unlocked_achievements) + """ + result = await db.execute( + select(Streak).where(Streak.user_id == user_id) + ) + streak = result.scalar_one_or_none() + + today = date.today() + streak_increased = False + streak_saved = False + + if not streak: + # Create new streak + streak = Streak( + user_id=user_id, + current_streak=1, + longest_streak=1, + last_activity_date=today, + total_days_active=1, + freeze_count=0, + previous_streak=0, + restores_used_this_month=0 + ) + db.add(streak) + streak_increased = True + else: + last_date = streak.last_activity_date + + if last_date == today: + # Already active today, no change + pass + elif last_date == today - timedelta(days=1): + # Consecutive day - increment streak + streak.current_streak += 1 + streak.total_days_active += 1 + streak.last_activity_date = today + streak_increased = True + + if streak.current_streak > streak.longest_streak: + streak.longest_streak = streak.current_streak + elif last_date and last_date < today - timedelta(days=1): + # Gap in activity + days_missed = (today - last_date).days - 1 + + if getattr(streak, 'freeze_count', 0) > 0 and days_missed == 1: + # Use freeze to save streak + streak.freeze_count -= 1 + streak.current_streak += 1 + streak.total_days_active += 1 + streak.last_activity_date = today + streak_saved = True + streak_increased = True + + if streak.current_streak > streak.longest_streak: + streak.longest_streak = streak.current_streak + else: + # Reset streak: save current streak to previous_streak before resetting + streak.previous_streak = streak.current_streak + streak.current_streak = 1 + streak.total_days_active += 1 + streak.last_activity_date = today + streak_increased = True + else: + # First activity ever + streak.current_streak = 1 + streak.total_days_active = 1 + streak.last_activity_date = today + streak_increased = True + + if streak.current_streak > streak.longest_streak: + streak.longest_streak = streak.current_streak + + # Ensure a DailyActivity record exists for today so weekly_activity is accurate + daily_result = await db.execute( + select(DailyActivity).where( + and_( + DailyActivity.user_id == user_id, + DailyActivity.activity_date == today, + ) + ) + ) + daily_activity = daily_result.scalar_one_or_none() + if not daily_activity: + daily_activity = DailyActivity( + user_id=user_id, + activity_date=today, + xp_earned=0, + lessons_completed=0, + study_time_minutes=0, + vocabulary_reviewed=0, + ) + db.add(daily_activity) + + await db.flush() # flush to DB before check_achievements + + # Check streak-based achievements + unlocked_achievements = [] + try: + unlocked_achievements = await check_achievements_for_user( + db, user_id, "streak_update" + ) + except Exception as e: + logger.error("Achievement check error: %s", e, exc_info=True) + + # Invalidate caches + uid_str = str(user_id) + await delete_cached(build_cache_key("progress_streak", user_id=uid_str)) + await delete_cached(build_cache_key("progress_me", user_id=uid_str)) + + return streak, streak_increased, streak_saved, unlocked_achievements diff --git a/backend-service/tests/test_progress_routes.py b/backend-service/tests/test_progress_routes.py index 03b33cb4..cee1bc7d 100644 --- a/backend-service/tests/test_progress_routes.py +++ b/backend-service/tests/test_progress_routes.py @@ -577,3 +577,133 @@ async def test_completes_lesson_successfully(self, auth_client): assert "lesson_id" in data assert "xp_earned" in data assert "is_passed" in data + + +# ============================================================================ +# POST /api/v1/progress/streak/restore — Restore a broken streak +# ============================================================================ + +class TestRestoreStreak: + """Tests for POST /api/v1/progress/streak/restore.""" + + @pytest.mark.asyncio + async def test_requires_auth(self, no_auth_client: AsyncClient): + response = await no_auth_client.post(f"{BASE}/streak/restore") + assert response.status_code == 401 + + @pytest.mark.asyncio + async def test_restore_no_streak_record(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_result.scalar_one_or_none.return_value = None + response = await client.post(f"{BASE}/streak/restore") + assert response.status_code == 400 + assert "No streak record found to restore" in response.json()["error"]["message"] + + @pytest.mark.asyncio + async def test_restore_no_previous_streak(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.previous_streak = 0 + mock_streak.restores_used_this_month = 0 + mock_streak.last_restore_date = None + mock_result.scalar_one_or_none.return_value = mock_streak + + response = await client.post(f"{BASE}/streak/restore") + assert response.status_code == 400 + assert "No previous streak is available to restore" in response.json()["error"]["message"] + + @pytest.mark.asyncio + async def test_restore_limit_reached(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.previous_streak = 5 + mock_streak.restores_used_this_month = 3 + mock_streak.last_restore_date = date.today() + mock_result.scalar_one_or_none.return_value = mock_streak + + response = await client.post(f"{BASE}/streak/restore") + assert response.status_code == 400 + assert "already used your 3 streak restores" in response.json()["error"]["message"] + + @pytest.mark.asyncio + async def test_restore_success(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.previous_streak = 10 + mock_streak.restores_used_this_month = 1 + mock_streak.longest_streak = 8 + mock_streak.last_restore_date = date.today() + mock_streak.current_streak = 1 + mock_streak.total_days_active = 5 + mock_streak.freeze_count = 0 + mock_streak.last_activity_date = date.today() + mock_streak.last_reward_claim_date = None + mock_result.scalar_one_or_none.return_value = mock_streak + + response = await client.post(f"{BASE}/streak/restore") + assert response.status_code == 200 + data = response.json()["data"] + assert data["current_streak"] == 11 + assert data["longest_streak"] == 11 + assert data["previous_streak"] == 0 + assert data["restores_used_this_month"] == 2 + assert data["restores_remaining"] == 1 + + +# ============================================================================ +# POST /api/v1/progress/streak/claim-daily-reward — Claim daily login reward +# ============================================================================ + +class TestClaimDailyReward: + """Tests for POST /api/v1/progress/streak/claim-daily-reward.""" + + @pytest.mark.asyncio + async def test_requires_auth(self, no_auth_client: AsyncClient): + response = await no_auth_client.post(f"{BASE}/streak/claim-daily-reward") + assert response.status_code == 401 + + @pytest.mark.asyncio + async def test_claim_no_activity_today(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.last_activity_date = date.today() - timedelta(days=1) + mock_result.scalar_one_or_none.return_value = mock_streak + + response = await client.post(f"{BASE}/streak/claim-daily-reward") + assert response.status_code == 400 + assert "complete a learning activity today" in response.json()["error"]["message"] + + @pytest.mark.asyncio + async def test_claim_already_claimed_today(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.last_activity_date = date.today() + mock_streak.last_reward_claim_date = date.today() + mock_result.scalar_one_or_none.return_value = mock_streak + + response = await client.post(f"{BASE}/streak/claim-daily-reward") + assert response.status_code == 400 + assert "already claimed today's reward" in response.json()["error"]["message"] + + @pytest.mark.asyncio + async def test_claim_success(self, auth_client): + client, mock_session, mock_result, _ = auth_client + mock_streak = MagicMock() + mock_streak.current_streak = 5 # Day 5 reward should be 25 gems (cycle index 4: 5, 10, 15, 20, 25, 30, 50) + mock_streak.last_activity_date = date.today() + mock_streak.last_reward_claim_date = None + mock_result.scalar_one_or_none.return_value = mock_streak + + mock_wallet = MagicMock() + mock_wallet.gems = 100 + mock_transaction = MagicMock() + + with patch("app.crud.gamification.WalletCRUD.add_gems", new=AsyncMock(return_value=(mock_wallet, mock_transaction))): + response = await client.post(f"{BASE}/streak/claim-daily-reward") + + assert response.status_code == 200 + data = response.json()["data"] + assert data["gems_awarded"] == 25 + assert data["total_gems"] == 100 + assert data["current_streak"] == 5 + assert data["is_daily_reward_available"] is False diff --git a/flutter-app/assets/i18n/en.json b/flutter-app/assets/i18n/en.json index 0257167e..3033e325 100644 --- a/flutter-app/assets/i18n/en.json +++ b/flutter-app/assets/i18n/en.json @@ -608,6 +608,11 @@ "earnTipStreakBonus": "Daily streak bonus", "earnTipAchievements": "Unlock achievements", "earnTipChallenges": "Weekly challenges", + "dailyStreakRewardAvailable": "Daily Streak Reward!", + "dailyStreakRewardSubtitle": "You've kept your streak alive! Claim your daily reward now.", + "claimDailyRewardButton": "Claim Daily Reward", + "dailyRewardClaimed": "Daily Reward Claimed!", + "gemsReceived": "You received {gems} gems!", "starterReward": { "title": "Welcome gift!", "description": "Use Gems to unlock special items in the shop.", diff --git a/flutter-app/assets/i18n/vi.json b/flutter-app/assets/i18n/vi.json index 797c6d52..9fc71009 100644 --- a/flutter-app/assets/i18n/vi.json +++ b/flutter-app/assets/i18n/vi.json @@ -608,6 +608,11 @@ "earnTipStreakBonus": "Thưởng chuỗi ngày liên tiếp", "earnTipAchievements": "Mở khóa thành tích", "earnTipChallenges": "Thử thách hàng tuần", + "dailyStreakRewardAvailable": "Quà Hàng Ngày!", + "dailyStreakRewardSubtitle": "Bạn đã giữ vững chuỗi học tập! Hãy nhận phần quà hàng ngày ngay nhé.", + "claimDailyRewardButton": "Nhận Đá Quý", + "dailyRewardClaimed": "Nhận Quà Thành Công!", + "gemsReceived": "Bạn đã nhận {gems} đá quý!", "starterReward": { "title": "Quà chào mừng!", "description": "Dùng Gem để mở khóa vật phẩm đặc biệt trong cửa hàng.", diff --git a/flutter-app/lib/features/home/presentation/pages/home_page.dart b/flutter-app/lib/features/home/presentation/pages/home_page.dart index 35d3fd62..74a9c32d 100644 --- a/flutter-app/lib/features/home/presentation/pages/home_page.dart +++ b/flutter-app/lib/features/home/presentation/pages/home_page.dart @@ -20,6 +20,8 @@ import 'package:lexilingo_app/features/vocabulary/presentation/widgets/daily_rev import 'package:lexilingo_app/features/progress/presentation/providers/streak_provider.dart'; import 'package:lexilingo_app/features/progress/presentation/widgets/points_calendar_dialog.dart'; import 'package:lexilingo_app/features/progress/presentation/widgets/daily_challenges_widget.dart'; +import 'package:lexilingo_app/features/progress/presentation/widgets/daily_reward_dialog.dart'; +import 'package:lexilingo_app/features/gamification/presentation/providers/gamification_provider.dart'; import 'package:lexilingo_app/features/level/level.dart'; import 'package:lexilingo_app/features/games/presentation/widgets/level_up_dialog.dart'; import 'package:lexilingo_app/features/gamification/presentation/widgets/rank_up_dialog.dart'; @@ -37,6 +39,8 @@ class HomePageNew extends StatefulWidget { class _HomePageNewState extends State { LevelProvider? _levelProvider; + StreakProvider? _streakProvider; + bool _isDailyRewardDialogShowing = false; @override void initState() { @@ -55,18 +59,42 @@ class _HomePageNewState extends State { }); // Listen for level-up events triggered by fetchLevelFull _levelProvider?.addListener(_onLevelProviderChange); + + // Setup streak provider listener + _streakProvider = context.read(); + _streakProvider?.addListener(_onStreakProviderChange); + // Load streak data here (after auth token is ready) instead of relying // on the race-prone call in main.dart that fires before authentication. - context.read().loadStreak(); + _streakProvider?.loadStreak(); }); } @override void dispose() { _levelProvider?.removeListener(_onLevelProviderChange); + _streakProvider?.removeListener(_onStreakProviderChange); super.dispose(); } + void _onStreakProviderChange() { + final streakProvider = _streakProvider; + if (streakProvider == null || !mounted) { + return; + } + + final streak = streakProvider.streak; + if (streak != null && streak.isDailyRewardAvailable && !_isDailyRewardDialogShowing) { + _isDailyRewardDialogShowing = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + showDailyRewardDialog(context, streakProvider).then((_) { + _isDailyRewardDialogShowing = false; + }); + }); + } + } + /// Shows the Level-Up or Rank-Up celebration dialog when the provider signals it. void _onLevelProviderChange() { final levelProvider = _levelProvider; diff --git a/flutter-app/lib/features/progress/data/datasources/progress_remote_datasource.dart b/flutter-app/lib/features/progress/data/datasources/progress_remote_datasource.dart index 4a626ae2..f75f6e6b 100644 --- a/flutter-app/lib/features/progress/data/datasources/progress_remote_datasource.dart +++ b/flutter-app/lib/features/progress/data/datasources/progress_remote_datasource.dart @@ -25,6 +25,8 @@ abstract class ProgressRemoteDataSource { Future getMyStreak(); Future updateStreak(); Future> useStreakFreeze(); + Future restoreStreak(); + Future> claimDailyReward(); // Daily Challenges methods Future getDailyChallenges(); @@ -140,6 +142,32 @@ class ProgressRemoteDataSourceImpl implements ProgressRemoteDataSource { } } + @override + Future restoreStreak() async { + try { + final response = await apiClient.post( + '/progress/streak/restore', + body: {}, + ); + return StreakModel.fromJson(response); + } catch (e) { + rethrow; + } + } + + @override + Future> claimDailyReward() async { + try { + final response = await apiClient.post( + '/progress/streak/claim-daily-reward', + body: {}, + ); + return response; + } catch (e) { + rethrow; + } + } + // ============================================================================ // Daily Challenges Methods // ============================================================================ diff --git a/flutter-app/lib/features/progress/data/models/streak_model.dart b/flutter-app/lib/features/progress/data/models/streak_model.dart index 184fb802..bc058b26 100644 --- a/flutter-app/lib/features/progress/data/models/streak_model.dart +++ b/flutter-app/lib/features/progress/data/models/streak_model.dart @@ -13,6 +13,11 @@ class StreakModel extends StreakEntity { required super.isActiveToday, required super.streakAtRisk, super.weeklyActivity, + super.previousStreak, + super.restoresUsedThisMonth, + super.restoresRemaining, + super.canRestore, + super.isDailyRewardAvailable, }); /// Factory constructor from JSON (API response) @@ -89,6 +94,12 @@ class StreakModel extends StreakEntity { ); } + final previousStreak = readInt(['previous_streak', 'previousStreak']); + final restoresUsedThisMonth = readInt(['restores_used_this_month', 'restoresUsedThisMonth']); + final restoresRemaining = readInt(['restores_remaining', 'restoresRemaining'], defaultValue: 3); + final canRestore = readBool(['can_restore', 'canRestore']); + final isDailyRewardAvailable = readBool(['is_daily_reward_available', 'isDailyRewardAvailable']); + return StreakModel( currentStreak: currentStreak, longestStreak: longestStreak, @@ -102,6 +113,11 @@ class StreakModel extends StreakEntity { ]), streakAtRisk: readBool(['streak_at_risk', 'streakAtRisk']), weeklyActivity: weeklyActivity, + previousStreak: previousStreak, + restoresUsedThisMonth: restoresUsedThisMonth, + restoresRemaining: restoresRemaining, + canRestore: canRestore, + isDailyRewardAvailable: isDailyRewardAvailable, ); } @@ -116,6 +132,11 @@ class StreakModel extends StreakEntity { 'is_active_today': isActiveToday, 'streak_at_risk': streakAtRisk, 'weekly_activity': weeklyActivity, + 'previous_streak': previousStreak, + 'restores_used_this_month': restoresUsedThisMonth, + 'restores_remaining': restoresRemaining, + 'can_restore': canRestore, + 'is_daily_reward_available': isDailyRewardAvailable, }; } @@ -130,6 +151,11 @@ class StreakModel extends StreakEntity { isActiveToday: isActiveToday, streakAtRisk: streakAtRisk, weeklyActivity: weeklyActivity, + previousStreak: previousStreak, + restoresUsedThisMonth: restoresUsedThisMonth, + restoresRemaining: restoresRemaining, + canRestore: canRestore, + isDailyRewardAvailable: isDailyRewardAvailable, ); } } @@ -144,6 +170,11 @@ class StreakUpdateResultModel extends StreakUpdateResult { required super.freezeCount, required super.streakIncreased, required super.streakSaved, + super.previousStreak, + super.restoresUsedThisMonth, + super.restoresRemaining, + super.canRestore, + super.isDailyRewardAvailable, }); /// Factory constructor from JSON @@ -195,6 +226,12 @@ class StreakUpdateResultModel extends StreakUpdateResult { if (longestStreak < currentStreak) longestStreak = currentStreak; if (totalDaysActive < currentStreak) totalDaysActive = currentStreak; + final previousStreak = readInt(['previous_streak', 'previousStreak']); + final restoresUsedThisMonth = readInt(['restores_used_this_month', 'restoresUsedThisMonth']); + final restoresRemaining = readInt(['restores_remaining', 'restoresRemaining'], defaultValue: 3); + final canRestore = readBool(['can_restore', 'canRestore']); + final isDailyRewardAvailable = readBool(['is_daily_reward_available', 'isDailyRewardAvailable']); + return StreakUpdateResultModel( currentStreak: currentStreak, longestStreak: longestStreak, @@ -208,6 +245,11 @@ class StreakUpdateResultModel extends StreakUpdateResult { ]), streakIncreased: readBool(['streak_increased', 'streakIncreased']), streakSaved: readBool(['streak_saved', 'streakSaved']), + previousStreak: previousStreak, + restoresUsedThisMonth: restoresUsedThisMonth, + restoresRemaining: restoresRemaining, + canRestore: canRestore, + isDailyRewardAvailable: isDailyRewardAvailable, ); } @@ -220,6 +262,11 @@ class StreakUpdateResultModel extends StreakUpdateResult { freezeCount: freezeCount, streakIncreased: streakIncreased, streakSaved: streakSaved, + previousStreak: previousStreak, + restoresUsedThisMonth: restoresUsedThisMonth, + restoresRemaining: restoresRemaining, + canRestore: canRestore, + isDailyRewardAvailable: isDailyRewardAvailable, ); } } diff --git a/flutter-app/lib/features/progress/data/repositories/progress_repository_impl.dart b/flutter-app/lib/features/progress/data/repositories/progress_repository_impl.dart index dd7870f1..0b30ee08 100644 --- a/flutter-app/lib/features/progress/data/repositories/progress_repository_impl.dart +++ b/flutter-app/lib/features/progress/data/repositories/progress_repository_impl.dart @@ -161,6 +161,38 @@ class ProgressRepositoryImpl implements ProgressRepository { } } + @override + Future> restoreStreak() async { + try { + final result = await remoteDataSource.restoreStreak(); + return Right(result.toEntity()); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); + } on UnauthorizedException catch (e) { + return Left(UnauthorizedFailure(e.message)); + } catch (e) { + return Left(ServerFailure('Unexpected error: ${e.toString()}')); + } + } + + @override + Future>> claimDailyReward() async { + try { + final result = await remoteDataSource.claimDailyReward(); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); + } on UnauthorizedException catch (e) { + return Left(UnauthorizedFailure(e.message)); + } catch (e) { + return Left(ServerFailure('Unexpected error: ${e.toString()}')); + } + } + // ============================================================================ // Daily Challenges Operations // ============================================================================ diff --git a/flutter-app/lib/features/progress/domain/entities/streak_entity.dart b/flutter-app/lib/features/progress/domain/entities/streak_entity.dart index 46170106..f6998384 100644 --- a/flutter-app/lib/features/progress/domain/entities/streak_entity.dart +++ b/flutter-app/lib/features/progress/domain/entities/streak_entity.dart @@ -28,6 +28,21 @@ class StreakEntity extends Equatable { /// Activity flags for each day of the current week (Mon=0 … Sun=6) final List weeklyActivity; + /// The previous streak value saved when the streak broke + final int previousStreak; + + /// Number of streak restores used this month + final int restoresUsedThisMonth; + + /// Remaining restores for this month + final int restoresRemaining; + + /// Whether the streak can currently be restored + final bool canRestore; + + /// Whether the daily streak reward is available to claim + final bool isDailyRewardAvailable; + const StreakEntity({ required this.currentStreak, required this.longestStreak, @@ -37,6 +52,11 @@ class StreakEntity extends Equatable { required this.isActiveToday, required this.streakAtRisk, List? weeklyActivity, + this.previousStreak = 0, + this.restoresUsedThisMonth = 0, + this.restoresRemaining = 3, + this.canRestore = false, + this.isDailyRewardAvailable = false, }) : weeklyActivity = weeklyActivity ?? const [false, false, false, false, false, false, false]; @@ -52,6 +72,11 @@ class StreakEntity extends Equatable { isActiveToday: false, streakAtRisk: false, weeklyActivity: [false, false, false, false, false, false, false], + previousStreak: 0, + restoresUsedThisMonth: 0, + restoresRemaining: 3, + canRestore: false, + isDailyRewardAvailable: false, ); } @@ -95,6 +120,11 @@ class StreakEntity extends Equatable { isActiveToday, streakAtRisk, weeklyActivity, + previousStreak, + restoresUsedThisMonth, + restoresRemaining, + canRestore, + isDailyRewardAvailable, ]; } @@ -107,6 +137,11 @@ class StreakUpdateResult extends Equatable { final int freezeCount; final bool streakIncreased; final bool streakSaved; + final int previousStreak; + final int restoresUsedThisMonth; + final int restoresRemaining; + final bool canRestore; + final bool isDailyRewardAvailable; const StreakUpdateResult({ required this.currentStreak, @@ -115,6 +150,11 @@ class StreakUpdateResult extends Equatable { required this.freezeCount, required this.streakIncreased, required this.streakSaved, + this.previousStreak = 0, + this.restoresUsedThisMonth = 0, + this.restoresRemaining = 3, + this.canRestore = false, + this.isDailyRewardAvailable = false, }); @override @@ -125,5 +165,10 @@ class StreakUpdateResult extends Equatable { freezeCount, streakIncreased, streakSaved, + previousStreak, + restoresUsedThisMonth, + restoresRemaining, + canRestore, + isDailyRewardAvailable, ]; } diff --git a/flutter-app/lib/features/progress/domain/repositories/progress_repository.dart b/flutter-app/lib/features/progress/domain/repositories/progress_repository.dart index 845fc606..a085ba59 100644 --- a/flutter-app/lib/features/progress/domain/repositories/progress_repository.dart +++ b/flutter-app/lib/features/progress/domain/repositories/progress_repository.dart @@ -51,6 +51,12 @@ abstract class ProgressRepository { /// Use a streak freeze to protect current streak Future>> useStreakFreeze(); + /// Restore a broken streak using one of the monthly restores + Future> restoreStreak(); + + /// Claim daily login reward + Future>> claimDailyReward(); + // ============================================================================ // Daily Challenges Operations // ============================================================================ diff --git a/flutter-app/lib/features/progress/presentation/providers/streak_provider.dart b/flutter-app/lib/features/progress/presentation/providers/streak_provider.dart index 982d7c7f..7b45293d 100644 --- a/flutter-app/lib/features/progress/presentation/providers/streak_provider.dart +++ b/flutter-app/lib/features/progress/presentation/providers/streak_provider.dart @@ -94,6 +94,11 @@ class StreakProvider extends ChangeNotifier { isActiveToday: true, streakAtRisk: false, weeklyActivity: updatedWeekly, + previousStreak: updateResult.previousStreak, + restoresUsedThisMonth: updateResult.restoresUsedThisMonth, + restoresRemaining: updateResult.restoresRemaining, + canRestore: updateResult.canRestore, + isDailyRewardAvailable: updateResult.isDailyRewardAvailable, ); success = true; }, @@ -134,6 +139,12 @@ class StreakProvider extends ChangeNotifier { freezeCount: data['freeze_count'] ?? (_streak!.freezeCount - 1), isActiveToday: true, streakAtRisk: false, + weeklyActivity: _streak!.weeklyActivity, + previousStreak: _streak!.previousStreak, + restoresUsedThisMonth: _streak!.restoresUsedThisMonth, + restoresRemaining: _streak!.restoresRemaining, + canRestore: _streak!.canRestore, + isDailyRewardAvailable: _streak!.isDailyRewardAvailable, ); } success = true; @@ -145,6 +156,71 @@ class StreakProvider extends ChangeNotifier { return success; } + /// Restore a broken streak + Future restoreStreak() async { + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + final result = await _repository.restoreStreak(); + + bool success = false; + result.fold( + (failure) { + _errorMessage = failure.message; + }, + (streakData) { + _streak = streakData; + success = true; + }, + ); + + _isLoading = false; + notifyListeners(); + return success; + } + + /// Claim daily login reward + Future?> claimDailyReward() async { + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + final result = await _repository.claimDailyReward(); + + Map? successData; + result.fold( + (failure) { + _errorMessage = failure.message; + }, + (data) { + successData = data; + // Update local streak isDailyRewardAvailable flag + if (_streak != null) { + _streak = StreakEntity( + currentStreak: _streak!.currentStreak, + longestStreak: _streak!.longestStreak, + totalDaysActive: _streak!.totalDaysActive, + lastActivityDate: _streak!.lastActivityDate, + freezeCount: _streak!.freezeCount, + isActiveToday: _streak!.isActiveToday, + streakAtRisk: _streak!.streakAtRisk, + weeklyActivity: _streak!.weeklyActivity, + previousStreak: _streak!.previousStreak, + restoresUsedThisMonth: _streak!.restoresUsedThisMonth, + restoresRemaining: _streak!.restoresRemaining, + canRestore: _streak!.canRestore, + isDailyRewardAvailable: false, + ); + } + }, + ); + + _isLoading = false; + notifyListeners(); + return successData; + } + /// Clear error message void clearError() { _errorMessage = null; diff --git a/flutter-app/lib/features/progress/presentation/widgets/daily_reward_dialog.dart b/flutter-app/lib/features/progress/presentation/widgets/daily_reward_dialog.dart new file mode 100644 index 00000000..d5638c14 --- /dev/null +++ b/flutter-app/lib/features/progress/presentation/widgets/daily_reward_dialog.dart @@ -0,0 +1,415 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:provider/provider.dart'; +import 'package:lexilingo_app/core/theme/app_theme.dart'; +import 'package:lexilingo_app/core/widgets/lottie_animation_widget.dart'; +import 'package:lexilingo_app/features/progress/presentation/providers/streak_provider.dart'; +import 'package:lexilingo_app/features/gamification/presentation/providers/gamification_provider.dart'; +import 'package:lexilingo_app/features/progress/domain/entities/streak_entity.dart'; + +/// Shows the daily reward celebration dialog. +Future showDailyRewardDialog(BuildContext context, StreakProvider streakProvider) { + return showGeneralDialog( + context: context, + barrierDismissible: false, + barrierLabel: 'daily_reward', + barrierColor: Colors.black87, + transitionDuration: const Duration(milliseconds: 350), + pageBuilder: (context, animation, secondaryAnimation) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: _DailyRewardDialogContent( + streakProvider: streakProvider, + ), + ); + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + final curved = CurvedAnimation( + parent: animation, + curve: Curves.easeOutBack, + ); + return ScaleTransition( + scale: curved, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + ); +} + +class _DailyRewardDialogContent extends StatefulWidget { + final StreakProvider streakProvider; + + const _DailyRewardDialogContent({ + required this.streakProvider, + }); + + @override + State<_DailyRewardDialogContent> createState() => _DailyRewardDialogContentState(); +} + +class _DailyRewardDialogContentState extends State<_DailyRewardDialogContent> + with SingleTickerProviderStateMixin { + late final AnimationController _pulseController; + bool _isClaiming = false; + + final List _rewardsCycle = const [5, 10, 15, 20, 25, 30, 50]; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(reverse: true); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + Future _claimReward() async { + if (_isClaiming) return; + setState(() { + _isClaiming = true; + }); + + final data = await widget.streakProvider.claimDailyReward(); + + if (!mounted) return; + + if (data != null) { + final int gemsAwarded = data['gems_awarded'] ?? 0; + + // Reload wallet to sync gems count on UI + context.read().loadWallet(); + + // Dismiss dialog + Navigator.of(context).pop(); + + // Show success dialog celebration + AnimatedSuccessDialog.show( + context, + title: 'gamification.dailyRewardClaimed'.tr(defaultValue: 'Daily Reward Claimed!'), + message: 'gamification.gemsReceived'.tr( + defaultValue: 'You received {gems} gems!', + namedArgs: {'gems': '$gemsAwarded'}, + ), + autoCloseDuration: const Duration(seconds: 3), + ); + } else { + setState(() { + _isClaiming = false; + }); + final errorMsg = widget.streakProvider.errorMessage ?? 'common.failed'.tr(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(errorMsg)), + ); + } + } + + @override + Widget build(BuildContext context) { + final streak = widget.streakProvider.streak ?? StreakEntity.empty(); + final currentStreak = streak.currentStreak; + final dayIndex = (currentStreak > 0 ? (currentStreak - 1) : 0) % 7; + final isDark = Theme.of(context).brightness == Brightness.dark; + + final bgGradient = isDark + ? const [Color(0xFF132333), Color(0xFF0F1B26)] + : const [Colors.white, Color(0xFFF0F6FB)]; + final primaryColor = AppColorRoles.primary(isDark); + final textTheme = Theme.of(context).textTheme; + + return Container( + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: bgGradient, + ), + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.5 : 0.15), + blurRadius: 24, + offset: const Offset(0, 12), + ), + ], + border: Border.all( + color: isDark ? const Color(0xFF2E4154) : Colors.white, + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(26), + child: Stack( + alignment: Alignment.center, + children: [ + // Ambient StarBurst design decoration + const Positioned( + top: -60, + child: IgnorePointer( + child: LottieAnimationWidget( + animation: LottieAnimation.starBurst, + width: 320, + height: 320, + repeat: false, + ), + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 12), + // Animated Gift Icon + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + final scale = 1.0 + (_pulseController.value * 0.08); + return Transform.scale( + scale: scale, + child: Container( + width: 86, + height: 86, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const LinearGradient( + colors: AppColors.warmGradient, + ), + boxShadow: [ + BoxShadow( + color: AppColors.orange.withValues(alpha: 0.4), + blurRadius: 20, + spreadRadius: 2, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.redeem_rounded, + color: Colors.white, + size: 48, + ), + ), + ); + }, + ), + const SizedBox(height: 24), + + // Title + Text( + 'gamification.dailyStreakRewardAvailable'.tr(defaultValue: 'Daily Streak Reward!'), + textAlign: TextAlign.center, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w900, + color: isDark ? Colors.white : AppColors.textDark, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 8), + + // Subtitle + Text( + 'gamification.dailyStreakRewardSubtitle'.tr( + defaultValue: "You've kept your streak alive! Claim your daily reward now.", + ), + textAlign: TextAlign.center, + style: textTheme.bodyMedium?.copyWith( + color: isDark ? const Color(0xFFB5C8D8) : AppColors.textGrey, + height: 1.4, + ), + ), + const SizedBox(height: 28), + + // 7-day reward tracker horizontal list + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(7, (index) { + final isActive = index == dayIndex; + final isClaimed = index < dayIndex; + final isLocked = index > dayIndex; + final gems = _rewardsCycle[index]; + + return _buildRewardDayCard( + context: context, + dayNumber: index + 1, + gems: gems, + isActive: isActive, + isClaimed: isClaimed, + isLocked: isLocked, + isDark: isDark, + primaryColor: primaryColor, + ); + }), + ), + ), + const SizedBox(height: 32), + + // Action Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isClaiming ? null : _claimReward, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(56), + backgroundColor: AppColors.orange, + foregroundColor: Colors.white, + elevation: 4, + shadowColor: AppColors.orange.withValues(alpha: 0.4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: _isClaiming + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'gamification.claimDailyRewardButton'.tr(defaultValue: 'Claim Gems'), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + const SizedBox(height: 12), + + // Skip / Close text button + TextButton( + onPressed: _isClaiming ? null : () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + foregroundColor: isDark ? Colors.white60 : AppColors.textGrey, + ), + child: Text('common.close'.tr()), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildRewardDayCard({ + required BuildContext context, + required int dayNumber, + required int gems, + required bool isActive, + required bool isClaimed, + required bool isLocked, + required bool isDark, + required Color primaryColor, + }) { + Color cardBg; + Border? border; + double scale = 1.0; + + if (isActive) { + cardBg = isDark ? const Color(0xFF384D63) : Colors.white; + border = Border.all(color: AppColors.orange, width: 2.5); + scale = 1.05; + } else if (isClaimed) { + cardBg = isDark ? const Color(0xFF1E2F40) : const Color(0xFFE2F0D9); + border = Border.all(color: isDark ? const Color(0xFF2E4154) : const Color(0xFFAAD38D), width: 1); + } else { + // Locked + cardBg = isDark ? const Color(0xFF172433).withValues(alpha: 0.6) : const Color(0xFFF1F5F9); + border = Border.all(color: isDark ? const Color(0xFF243547) : const Color(0xFFE2E8F0), width: 1); + } + + final card = Container( + width: 44, + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(14), + border: border, + boxShadow: isActive + ? [ + BoxShadow( + color: AppColors.orange.withValues(alpha: 0.3), + blurRadius: 10, + spreadRadius: 1, + ), + ] + : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'D$dayNumber', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: isActive + ? AppColors.orange + : isDark + ? Colors.white60 + : AppColors.textGrey, + ), + ), + const SizedBox(height: 6), + if (isClaimed) + const Icon( + Icons.check_circle_rounded, + color: AppColors.greenSuccessBright, + size: 18, + ) + else ...[ + Icon( + Icons.diamond_rounded, + color: isActive + ? AppColors.purpleLight + : isDark + ? Colors.white24 + : AppColors.textMuted, + size: 16, + ), + const SizedBox(height: 4), + Text( + '+$gems', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w900, + color: isActive + ? (isDark ? Colors.white : AppColors.textDark) + : isDark + ? Colors.white30 + : AppColors.textMuted, + ), + ), + ], + ], + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: scale != 1.0 + ? Transform.scale(scale: scale, child: card) + : card, + ); + } +} diff --git a/flutter-app/lib/features/progress/presentation/widgets/streak_widget.dart b/flutter-app/lib/features/progress/presentation/widgets/streak_widget.dart index fa9041af..9a7712c5 100644 --- a/flutter-app/lib/features/progress/presentation/widgets/streak_widget.dart +++ b/flutter-app/lib/features/progress/presentation/widgets/streak_widget.dart @@ -364,12 +364,18 @@ class StreakDetailsSheet extends StatelessWidget { const SizedBox(height: 24), // Big streak display - Icon( - _getStreakIcon(streak.streakIcon), - color: AppColors.orange, - size: 64, - ), - const SizedBox(height: 8), + if (streak.currentStreak >= 7) + LargeStreakFireWidget( + icon: _getStreakIcon(streak.streakIcon), + size: 64, + ) + else + Icon( + _getStreakIcon(streak.streakIcon), + color: AppColors.orange, + size: 64, + ), + const SizedBox(height: 12), Text( 'home.dayStreakCount'.tr( namedArgs: {'count': '${streak.currentStreak}'}, @@ -386,7 +392,13 @@ class StreakDetailsSheet extends StatelessWidget { ), ), - const SizedBox(height: 32), + const SizedBox(height: 16), + // Restores remaining count display + Text( + 'Lượt khôi phục chuỗi còn lại trong tháng: ${streak.restoresRemaining}', + style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey), + ), + const SizedBox(height: 16), // Stats row Row( @@ -506,6 +518,66 @@ class StreakDetailsSheet extends StatelessWidget { }, ), + // Restore streak section + if (streak.canRestore) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade800), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Bạn có thể khôi phục lại chuỗi ${streak.previousStreak} ngày cũ! Bạn còn ${streak.restoresRemaining} lượt khôi phục trong tháng này.', + style: TextStyle(color: Colors.blue.shade800, fontSize: 12), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Consumer( + builder: (context, provider, child) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: provider.isLoading + ? null + : () async { + final success = await provider.restoreStreak(); + if (success && context.mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Đã khôi phục chuỗi thành công!', + ), + backgroundColor: Colors.green, + ), + ); + } + }, + icon: const Icon(Icons.healing, color: Colors.white), + label: Text('Khôi phục chuỗi (${streak.previousStreak} ngày)'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ); + }, + ), + ], + const SizedBox(height: 16), ], ), @@ -538,6 +610,113 @@ class StreakDetailsSheet extends StatelessWidget { } } +class LargeStreakFireWidget extends StatefulWidget { + final IconData icon; + final double size; + + const LargeStreakFireWidget({ + super.key, + required this.icon, + this.size = 64, + }); + + @override + State createState() => _LargeStreakFireWidgetState(); +} + +class _LargeStreakFireWidgetState extends State + with TickerProviderStateMixin { + late final AnimationController _rotateController; + late final AnimationController _pulseController; + late final Animation _rotation; + late final Animation _scale; + late final Animation _glow; + + @override + void initState() { + super.initState(); + _rotateController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(reverse: true); + + _rotation = Tween(begin: -0.04, end: 0.04).animate( + CurvedAnimation(parent: _rotateController, curve: Curves.easeInOut), + ); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + _scale = Tween(begin: 0.96, end: 1.04).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + _glow = Tween(begin: 4.0, end: 16.0).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _rotateController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: Listenable.merge([_rotateController, _pulseController]), + builder: (context, child) { + return Transform.scale( + scale: _scale.value, + child: RotationTransition( + turns: _rotation, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.orange.withOpacity(0.4), + blurRadius: _glow.value * 2, + spreadRadius: _glow.value / 2, + ), + BoxShadow( + color: Colors.red.withOpacity(0.2), + blurRadius: _glow.value * 3, + spreadRadius: _glow.value, + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: [Colors.orange, Colors.redAccent], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(bounds), + child: Icon( + widget.icon, + color: Colors.white, + size: widget.size, + ), + ), + ), + ), + ), + ); + }, + ); + } +} + /// Compact streak badge for AppBar class StreakBadge extends StatelessWidget { const StreakBadge({super.key}); From 0179164d7c3bebb87ad404d5f158f8b23c825bc0 Mon Sep 17 00:00:00 2001 From: InfinityZero3000 Date: Wed, 17 Jun 2026 01:11:35 +0700 Subject: [PATCH 22/61] fix(ai-service): resolve startup failures, database creation path, groq key pooling, and test hangs --- ai-service/api/core/groq_key_pool.py | 7 +-- ai-service/requirements.txt | 10 ++-- ai-service/scripts/build_kg.py | 2 +- .../kg_pipeline/importers/kuzu_importer.py | 2 +- ai-service/tests/conftest.py | 46 +++++++++++++++++++ .../tests/test_tracecag_chat_integration.py | 12 ++++- 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/ai-service/api/core/groq_key_pool.py b/ai-service/api/core/groq_key_pool.py index 2c89eb3e..0e9f3754 100644 --- a/ai-service/api/core/groq_key_pool.py +++ b/ai-service/api/core/groq_key_pool.py @@ -104,14 +104,9 @@ def build_groq_key_pool(redis_client: redis.Redis) -> Optional[GroqKeyPool]: Returns None if no keys are configured. """ global _pool_instance - raw = os.getenv("GROQ_API_KEYS", "").strip() + raw = os.getenv("GROQ_API_KEYS", "").strip() or os.getenv("GROQ_API_KEY", "").strip() keys = [k.strip() for k in raw.split(",") if k.strip()] if raw else [] - if not keys: - single = os.getenv("GROQ_API_KEY", "").strip() - if single: - keys = [single] - if not keys: return None diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index f6449192..551e8bdc 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -1,5 +1,5 @@ # FastAPI and Server -fastapi>=0.137.0 +fastapi>=0.136.0 uvicorn[standard]>=0.49.0 python-multipart>=0.0.32 @@ -37,8 +37,8 @@ kuzu>=0.11.3 # Embeddings # NOTE: sentence-transformers>=4.0.0 has LRScheduler compatibility issues with torch 2.2.x. # Pin to <4.0.0 for Intel Mac compatibility. -sentence-transformers>=5.5.1,<6.0.0 -numpy>=2.4.6,<3.0.0 +sentence-transformers>=3.4.0,<4.0.0 +numpy>=1.26.0,<2.0.0 # STT / TTS faster-whisper>=1.2.1 @@ -48,7 +48,7 @@ gTTS>=2.5.4 # HuBERT / Pronunciation Analysis (transformers will auto-install torch) # NOTE: transformers>=5.0.0 requires torch>=2.4, which is NOT available on Intel Mac (x86_64). # Pin to <5.0.0 to stay compatible with torch 2.2.x on Intel Mac. -transformers>=5.10.2,<6.0.0 +transformers>=4.40.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.2.0,<=2.2.2 @@ -77,7 +77,7 @@ flake8>=7.3.0 mypy>=1.20.2 pytest==9.1.0 pytest-asyncio==1.4.0 -networkx==3.6.1 +networkx>=3.2.0,<3.3.0 gliner>=0.2.26 # Optional: Redis (if needed later) diff --git a/ai-service/scripts/build_kg.py b/ai-service/scripts/build_kg.py index 86bbd0d5..820ede4a 100644 --- a/ai-service/scripts/build_kg.py +++ b/ai-service/scripts/build_kg.py @@ -198,7 +198,7 @@ def run(force: bool = False, dry_run: bool = False) -> int: logger.info("Force mode: renaming existing DB to %s", backup) os.rename(db_path, backup) - os.makedirs(db_path, exist_ok=True) + os.makedirs(os.path.dirname(db_path), exist_ok=True) import kuzu db = kuzu.Database(db_path) diff --git a/ai-service/scripts/kg_pipeline/importers/kuzu_importer.py b/ai-service/scripts/kg_pipeline/importers/kuzu_importer.py index 0b48bc54..f538c227 100644 --- a/ai-service/scripts/kg_pipeline/importers/kuzu_importer.py +++ b/ai-service/scripts/kg_pipeline/importers/kuzu_importer.py @@ -134,7 +134,7 @@ def run(nodes_csv: Path = NODES_CSV, edges_csv: Path = EDGES_CSV) -> dict: import kuzu # type: ignore logger.info("Opening KuzuDB at %s …", KUZU_DB_PATH) - KUZU_DB_PATH.mkdir(parents=True, exist_ok=True) + KUZU_DB_PATH.parent.mkdir(parents=True, exist_ok=True) db = kuzu.Database(str(KUZU_DB_PATH)) conn = kuzu.Connection(db) diff --git a/ai-service/tests/conftest.py b/ai-service/tests/conftest.py index f8e1621a..4482e68c 100644 --- a/ai-service/tests/conftest.py +++ b/ai-service/tests/conftest.py @@ -198,3 +198,49 @@ def _run(coro): return asyncio.get_event_loop().run_until_complete(coro) return _run + + +@pytest.fixture(autouse=True) +def mock_redis_client_global(monkeypatch): + """Globally mock RedisClient to prevent connection blocks during tests.""" + from unittest.mock import AsyncMock + mock_inst = AsyncMock() + mock_inst.get = AsyncMock(return_value=None) + monkeypatch.setattr("api.core.redis_client.RedisClient.get_instance", AsyncMock(return_value=mock_inst)) + monkeypatch.setattr("api.core.redis_client.RedisClient.reconnect", AsyncMock(return_value=mock_inst)) + monkeypatch.setattr("api.core.redis_client.get_redis", AsyncMock(return_value=mock_inst)) + + +@pytest.fixture(autouse=True) +def mock_kg_service_global(monkeypatch): + """Globally mock get_kg_service to prevent KuzuDB file locks and hangs during tests.""" + from unittest.mock import AsyncMock, MagicMock + mock_kg = MagicMock() + mock_kg.get_concepts = MagicMock(return_value={}) + mock_kg.get_seed_concepts_fast = MagicMock(return_value=[]) + mock_kg.semantic_seed_concepts = MagicMock(return_value=[]) + + class MockKGHits: + def __init__(self, seed_nodes=None, expanded_nodes=None, paths=None): + self.seed_nodes = seed_nodes or [] + self.expanded_nodes = expanded_nodes or [] + self.paths = paths or [] + + mock_kg.expand = AsyncMock(return_value=MockKGHits()) + mock_kg.expand_best_first = AsyncMock(return_value=MockKGHits()) + mock_kg.get_concept_count = MagicMock(return_value=0) + mock_kg.record_interaction = AsyncMock() + mock_kg.get_user_mastery = AsyncMock(return_value={}) + + for path in [ + "api.services.kg_service_v3.get_kg_service", + "api.services.subgraph_hot_cache.get_kg_service", + "api.services.trace_cag.nodes_v2.get_kg_service", + "api.services.orchestrator.get_kg_service", + ]: + try: + monkeypatch.setattr(path, lambda: mock_kg) + except Exception: + pass + + diff --git a/ai-service/tests/test_tracecag_chat_integration.py b/ai-service/tests/test_tracecag_chat_integration.py index c6bd444e..c5a7655d 100644 --- a/ai-service/tests/test_tracecag_chat_integration.py +++ b/ai-service/tests/test_tracecag_chat_integration.py @@ -72,7 +72,17 @@ def mock_lexi_store(monkeypatch): store.delete_session = AsyncMock() store.init_messages = AsyncMock() monkeypatch.setattr(lexi_route, "_store", store) - monkeypatch.setattr("api.routes.lexi_chat.enforce_user_quota", AsyncMock(return_value=MagicMock())) + quota_mock = MagicMock() + quota_mock.rpm_used = 1 + quota_mock.rpm_limit = 100 + quota_mock.rpd_used = 5 + quota_mock.rpd_limit = 1000 + quota_mock.tpm_used = 100 + quota_mock.tpm_limit = 50000 + quota_mock.tpd_used = 1000 + quota_mock.tpd_limit = 1000000 + monkeypatch.setattr("api.routes.lexi_chat.enforce_user_quota", AsyncMock(return_value=quota_mock)) + monkeypatch.setattr("api.core.redis_client.RedisClient.get_instance", AsyncMock()) return store From 05659e74be48ff64a6a447bd065b97da0dc379ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:14:50 +0000 Subject: [PATCH 23/61] chore(deps): update transformers requirement in /ai-service (#242) Updates the requirements on [transformers](https://github.com/huggingface/transformers) to permit the latest version. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.40.0...v5.12.1) --- updated-dependencies: - dependency-name: transformers dependency-version: 5.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ai-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/requirements.txt b/ai-service/requirements.txt index 551e8bdc..90f802ea 100644 --- a/ai-service/requirements.txt +++ b/ai-service/requirements.txt @@ -48,7 +48,7 @@ gTTS>=2.5.4 # HuBERT / Pronunciation Analysis (transformers will auto-install torch) # NOTE: transformers>=5.0.0 requires torch>=2.4, which is NOT available on Intel Mac (x86_64). # Pin to <5.0.0 to stay compatible with torch 2.2.x on Intel Mac. -transformers>=4.40.0,<5.0.0 +transformers>=5.12.1,<6.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.2.0,<=2.2.2 From 9de7e3971f5625f4da3e31ce8faebf7ea98fbc94 Mon Sep 17 00:00:00 2001 From: Nguyen Thang <151487391+InfinityZero3000@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:02:17 +0700 Subject: [PATCH 24/61] feat: streaming STT, Phase 3-5 hardening, and production CORS/metrics fixes (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(stt): design streaming ensemble architecture * docs(stt): clarify engine contracts and readiness * feat(production): Phase 0-2 production readiness — retention, growth, security - JWT issuer/audience validation hardening (security.py, config.py) - Sentry DSN error tracking integration (opt-in via SENTRY_DSN env) - Lesson context sanitization to strip answer fields from API response - GDPR hard-delete endpoint with full cascade (users.py) - GitHub Actions security scanning workflow + gitleaks secret detection - CLAUDE.md + AGENTS.md project instruction files - InAppReview after 5 completed lessons (RatingService, one-shot guard) - Achievement share button via share_plus (static Share.share API) - Offline SRS flashcard cache: 6h TTL via LocalCacheService.getOrFetchList() - FCM streak-at-risk push notifications (PushNotificationService.send_streak_at_risk) - Celery beat task send_streak_alerts at 20:00 UTC via crontab (not fixed offset) - i18n: achievements.share key in all 7 languages - Deep link routing (app_links): course/lesson/vocabulary/leaderboard/referral - RevenueCat premium paywall (PurchasesService wrapper, PaywallScreen, PremiumGate) - PurchasesService.init() wired in main.dart; DeepLinkService.dispose() on app teardown - Referral system: 8-char code (secrets module), GET /referral/my-code, POST /referral/claim/{code} - Race condition prevention via SELECT ... FOR UPDATE on claim endpoint - 50 gems reward to both referrer and new user (WalletTransaction) - pendingReferralCode claimed automatically after sign-in (auth_provider) - Alembic migration: referral_code (unique, indexed) + referred_by FK (SET NULL) - Prometheus + Grafana monitoring stack in docker-compose (ports 9090, 3001) - Alert rules: HighErrorRate, SlowResponseTime, BackendDown, HighMemoryUsage - Referral & premium i18n keys in all 7 languages - referral.py: random → secrets.choice, with_for_update(), func.count(), Path length validation - streak_reminders.py: datetime.now(timezone.utc).date() instead of date.today() - celery_app.py: crontab(hour=20) instead of 3600*24 offset - purchases_service.dart: PlatformException + PurchasesErrorHelper, rethrow non-cancel errors - paywall_screen.dart: show error snackbar on failed purchase - learning_provider.dart: unawaited() for onLessonCompleted() - auth_provider.dart: body= not data=, claim referral on all 3 sign-in paths - test_referral_routes.py: 10 test cases covering all edge cases Co-Authored-By: Claude Sonnet 4.6 * fix(ci): correct WalletTransaction.amount field name and gitleaks path allowlist - referral.py: gems_balance → gems (UserWallet), gems_amount → amount (WalletTransaction) - test_referral_routes.py: update wallet assertions to use correct field names - .gitleaks.toml: add gateway/kong/kong.yml, backend-service/test_firebase_api.py, scripts/clean-git-history.sh to legacy allowlist paths (pre-existing history findings) Co-Authored-By: Claude Sonnet 4.6 * fix(ci): extend gitleaks legacy allowlist to cover remaining historical paths Add scripts/start-all.sh and flutter-app/build/web/main.dart.js to the pre-existing commit allowlist. These are known historical findings from commits already listed; credentials are revoked per team policy. Co-Authored-By: Claude Sonnet 4.6 * feat: ship streaming STT and phase 3-5 hardening * chore: ignore generated build and KG metadata * fix(backend): patch prometheus routing for Starlette 1.x _IncludedRouter Starlette 1.x adds _IncludedRouter objects to app.routes when include_router is called. These objects implement matches() but have no .path attribute, causing AttributeError in prometheus-fastapi-instrumentator routing.py:55 and returning 500 on every request when metrics are enabled. Monkey-patches _get_route_name to guard hasattr(route, "path") and recurse into _IncludedRouter.routes when FULL match is found without a path. Co-Authored-By: Claude Sonnet 4.6 * docs: design CEFR content agent pipeline * fix(notifications): wire FSRS reminder pipeline and add in-app review banner - Auto-enable push notifications for new users (UserReminderPreference.enabled=True) - Register notification tap callbacks so tapping navigates to /vocabulary/review - Add showImmediateReviewReminder() for on-demand push when words are due - Add ReviewReminderBanner widget on home screen showing due word count - Clarify REMINDERS_ENABLED/REMINDER_DRY_RUN must be set true in production .env - Fix test_reminder_settings_have_safe_defaults to check types, not hardcoded values - Add 59-test suite covering FSRS stability/difficulty/state/scheduling and SM-2 correctness against hand-computed reference values Co-Authored-By: Claude Sonnet 4.6 * feat(content-agent): backend models, routes, services, Celery task, CLI Adds the CEFR content-agent pipeline to the backend service: - ContentAgentJob model + Alembic migration (add_cefr_content_agent) - REST routes: POST /content-agent/jobs, GET status, PATCH approve/reject - Services: job orchestration, ai-service client, upload handling, apply-to-lesson - Celery beat task: nightly cleanup of expired upload artefacts - CLI entrypoint for manual job management (app/cli/content_agent.py) - VocabularyCRUD now joins LessonVocabularyItem so agent-created vocab appears in filters - CONTENT_AGENT_* config keys added; feature is opt-in (CONTENT_AGENT_ENABLED=false default) Co-Authored-By: Claude Sonnet 4.6 * feat(content-agent): AI service pipeline, licensed ETL layer, remove legacy crawlers Content-agent AI service: - FastAPI route: POST /content-agent/generate accepting job spec + assets - Pydantic models for request/response contracts - Service layer: planner (exercise blueprint), generator (LLM-powered), adapters (normalise output to backend schema), policies (CEFR gating, quality checks), store Licensed content ETL (replaces scrapers): - Downloader: fetch assets from pre-approved licensed sources with checksum verify - Archive: zip artefacts for upload to backend storage - Registry: track known licensed sources and their metadata - Storage: local staging + pre-signed URL upload integration - Contracts: shared type definitions consumed by both ETL and content-agent KG pipeline cleanup: - Remove 5 legacy web-scraping crawlers (cefr_lists, conceptnet, web_crawler, wiktionary_kaikki, wordnet_extractor) that scraped third-party sites - Replace with the safer ETL approach for sourcing CEFR content Co-Authored-By: Claude Sonnet 4.6 * feat(content-agent): admin UI components, i18n keys, JSON contracts Admin interface for reviewing and approving content-agent jobs: - ContentAgentDrawer: full-page slide-over for job creation and multi-step review flow - ContentAgentModal: compact modal variant for quick approval/rejection - Both wired into CoursesPage via a new "Generate content" action button - contentAgentApi.ts: typed fetch client covering job CRUD + asset upload endpoints - i18n: contentAgent namespace added in both EN and VI translations - contracts/: JSON Schemas (course-artifact-v2, source-record-v2) and exercise-types-v1 registry consumed by both UI and backend for contract parity Co-Authored-By: Claude Sonnet 4.6 * test(content-agent): unit and integration tests across backend, ai-service, admin Backend (7 files): apply-to-lesson, contract parity, job orchestration, routes, Celery tasks, upload handling — covering happy path and error branches. AI service (13 files): adapters, generator, planner, policies, routes, store, ETL subsystems (archive, contracts, downloader, registry, storage), and a KG-pipeline source-safety gate that enforces no direct web scraping. Admin (1 file): contentAgentApi fetch client mock tests covering all endpoints. Co-Authored-By: Claude Sonnet 4.6 * fix(admin-ui): replace alert() with inline errors, add delete guards, polish flows - AdminManagementPage, AiChatSettingsPage, ContentLabPage: swap browser alert() for inline error state rendered in the UI - TopicsPage: add edit flow (previously only create/delete existed) - LessonsPage: fix placeholder '_' courseId bug in lesson create call - LessonExercisesPage, VocabularyPage: minor UX and fetch-size fixes - UserDetailModal: remove "Coming soon" streak label - adminApi.ts: UI_TYPES now derived from exercise-types-v1.json contract instead of a hard-coded array, keeping UI and backend in sync - styles.css: add form/input/table utility classes used by new admin pages Co-Authored-By: Claude Sonnet 4.6 * chore(deploy): wire CONTENT_AGENT env vars into compose files Add CONTENT_AGENT_ENABLED, CONTENT_AGENT_SERVICE_TOKEN, CONTENT_AGENT_AI_TIMEOUT_SECONDS, and CONTENT_AGENT_UPLOAD_TTL_DAYS to backend-service and celery-worker containers in both docker-compose.yml and docker-compose.production.yml. Co-Authored-By: Claude Sonnet 4.6 * docs: update CEFR content-agent spec and add ETL/agent implementation plans - Updated spec (2026-06-14) to reflect final architecture decisions - New plan (2026-06-15-cefr-content-agent): step-by-step implementation guide for the content-agent pipeline across backend, AI service, and admin UI - New plan (2026-06-15-licensed-content-etl): licensed-content ETL design, source registry, artefact contracts, and storage strategy Co-Authored-By: Claude Sonnet 4.6 * docs: mark TRACE-CAG bulk topic corpus plan as complete Generator already implemented: 60 topics, 18,240 quadruples, 14,640 edges. Co-Authored-By: Claude Sonnet 4.6 * fix: require source_id for repeat-sensitive XP sources, isolate backend tests - Add REPEAT_SENSITIVE_SOURCES set (game/lesson/daily_challenge) with 422 on missing source_id - Fix 2 existing XP tests + add 7 new parametrized tests (70 total pass) - Add DEBUG=false guard and _test DB name check to conftest.py - Add .env.test.example documenting test database setup Co-Authored-By: Claude Sonnet 4.6 * feat(games): add pronunciation service, completion/load/accessibility tests - GamePronunciationService: prefers audio_url, TTS fallback, retryable error (Task 11) - game_completion_test.dart: 13 tests across all 6 games, XP award states (Task 10) - game_load_state_test.dart: 13 tests, loading/error/retry/stale-isolation (Task 12) - game_accessibility_test.dart: 14 tests, semantics/touch targets/responsive layout (Task 13) - Wire GamePronunciationService into SpellingBeeScreen and DI - Add audioError/listenButtonLabel i18n keys (en + vi) - 104 game tests passing, flutter analyze clean Co-Authored-By: Claude Sonnet 4.6 * feat(etl): admin UI dynamic sources, Docker volumes, runbook (Tasks 14-15) - ContentAgentModal: replaced hardcoded sources with dynamic getSourceCatalog() call - Shows license/version/record count/status per snapshot; core sources auto-preselected - Upload attestation checkbox required for admin_upload source type - ContentAgentDrawer: blocking validation errors disable Apply button - 12 new i18n keys in en.ts + vi.ts (sourceCatalog, uploadAttestation, etc.) - docker-compose: content_etl_data volume for /data/content-etl (dev + prod) - backend/.env.example: CONTENT_ETL_ENABLED=false - ai-service/.gitignore: exclude data/content-etl/ - docs/runbooks/licensed-content-etl.md: installation, activation, rollback - 30 admin tests passing, pnpm build:check clean Co-Authored-By: Claude Sonnet 4.6 * feat(etl): add backend source resolution, validation, vocab catalog, Celery flow (Tasks 10-13) - content_agent_sources.py: snapshot catalog client + pinned resolution at job creation - content_agent_validation.py: pure 16-gate artifact validator → ValidationReport - vocabulary_catalog.py: concurrency-safe upsert with SELECT FOR UPDATE, curated field preservation - Provenance-v2 Alembic migration: 10 new nullable columns (backward-compatible) - Updated Celery flow: resolving_sources → loading_snapshots → normalizing_upload → ... → preview_ready - Upload rights attestation required for admin_upload source type - 51 new tests, all passing Co-Authored-By: Claude Sonnet 4.6 * feat(etl): AI service pipeline, CLI, and source adapters (Tasks 1-9) - pipeline.py: ETLPipeline with run/resume, quarantine ratio guard, dedup, PipelineReport - cli.py: Typer CLI — list/sync/validate/activate commands (dry-run by default) - adapters/: base protocol + OEWN (defusedxml), CMUdict (ARPAbet), CEFR-J (CSV), Wikidata (EntityData), Tatoeba (per-row license), LibriSpeech, Common Voice - contracts.py: added timezone-aware retrieved_at, license-per-source allowlist validators - storage.py: write_manifest validates raw presence, JSONL line count, quarantine - routes/content_agent.py: GET /sources + POST /jobs/{id}/snapshots endpoints - services/content_agent/service.py: block direct existing_cefr ingestion - tests/content_etl/fixtures/: 10 minimal hand-authored fixture files - 143 tests passing Co-Authored-By: Claude Sonnet 4.6 * fix(content-agent): fix test fixture unicode normalization and mark CEFR agent plan complete - Add schema_version + source_manifest to _artifact() test helper so validate_artifact() gate passes in apply tests - Import normalize_word in test to derive existing VocabularyItem.word from the same function, avoiding typography apostrophe U+2019 vs U+0027 mismatch in dedup test - Mark all 52 steps in 2026-06-15-cefr-content-agent.md as complete Co-Authored-By: Claude Sonnet 4.6 * docs: record game system release verification (Task 15) - Create docs/qa/game-system-acceptance.md with automated test results: 26 backend game/XP tests, 104 Flutter game tests, flutter analyze clean - Update RPT-024 with server-authoritative XP, pronunciation service, and Flutter test coverage added in the stability sprint - Update RPT-025 with source_id security model and test coverage details - Mark all 2026-06-07-game-system-stability.md tasks as complete Co-Authored-By: Claude Sonnet 4.6 * chore(deps): update sentence-transformers requirement in /ai-service (#246) Updates the requirements on [sentence-transformers](https://github.com/huggingface/sentence-transformers) to permit the latest version. - [Release notes](https://github.com/huggingface/sentence-transformers/releases) - [Commits](https://github.com/huggingface/sentence-transformers/compare/v4.1.0...v5.5.1) --- updated-dependencies: - dependency-name: sentence-transformers dependency-version: 5.5.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @types/node in /admin-service (#250) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.1 to 25.9.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(admin, docker): resolve admin TypeScript compile errors & Celery healthcheck issues * feat: implement daily login rewards, streak restore, and training activity streak updates * feat(production): Phase 0-2 production readiness — retention, growth, security - JWT issuer/audience validation hardening (security.py, config.py) - Sentry DSN error tracking integration (opt-in via SENTRY_DSN env) - Lesson context sanitization to strip answer fields from API response - GDPR hard-delete endpoint with full cascade (users.py) - GitHub Actions security scanning workflow + gitleaks secret detection - CLAUDE.md + AGENTS.md project instruction files - InAppReview after 5 completed lessons (RatingService, one-shot guard) - Achievement share button via share_plus (static Share.share API) - Offline SRS flashcard cache: 6h TTL via LocalCacheService.getOrFetchList() - FCM streak-at-risk push notifications (PushNotificationService.send_streak_at_risk) - Celery beat task send_streak_alerts at 20:00 UTC via crontab (not fixed offset) - i18n: achievements.share key in all 7 languages - Deep link routing (app_links): course/lesson/vocabulary/leaderboard/referral - RevenueCat premium paywall (PurchasesService wrapper, PaywallScreen, PremiumGate) - PurchasesService.init() wired in main.dart; DeepLinkService.dispose() on app teardown - Referral system: 8-char code (secrets module), GET /referral/my-code, POST /referral/claim/{code} - Race condition prevention via SELECT ... FOR UPDATE on claim endpoint - 50 gems reward to both referrer and new user (WalletTransaction) - pendingReferralCode claimed automatically after sign-in (auth_provider) - Alembic migration: referral_code (unique, indexed) + referred_by FK (SET NULL) - Prometheus + Grafana monitoring stack in docker-compose (ports 9090, 3001) - Alert rules: HighErrorRate, SlowResponseTime, BackendDown, HighMemoryUsage - Referral & premium i18n keys in all 7 languages - referral.py: random → secrets.choice, with_for_update(), func.count(), Path length validation - streak_reminders.py: datetime.now(timezone.utc).date() instead of date.today() - celery_app.py: crontab(hour=20) instead of 3600*24 offset - purchases_service.dart: PlatformException + PurchasesErrorHelper, rethrow non-cancel errors - paywall_screen.dart: show error snackbar on failed purchase - learning_provider.dart: unawaited() for onLessonCompleted() - auth_provider.dart: body= not data=, claim referral on all 3 sign-in paths - test_referral_routes.py: 10 test cases covering all edge cases Co-Authored-By: Claude Sonnet 4.6 * feat: ship streaming STT and phase 3-5 hardening * fix(admin-ui): replace alert() with inline errors, add delete guards, polish flows - AdminManagementPage, AiChatSettingsPage, ContentLabPage: swap browser alert() for inline error state rendered in the UI - TopicsPage: add edit flow (previously only create/delete existed) - LessonsPage: fix placeholder '_' courseId bug in lesson create call - LessonExercisesPage, VocabularyPage: minor UX and fetch-size fixes - UserDetailModal: remove "Coming soon" streak label - adminApi.ts: UI_TYPES now derived from exercise-types-v1.json contract instead of a hard-coded array, keeping UI and backend in sync - styles.css: add form/input/table utility classes used by new admin pages Co-Authored-By: Claude Sonnet 4.6 * chore(deploy): wire CONTENT_AGENT env vars into compose files Add CONTENT_AGENT_ENABLED, CONTENT_AGENT_SERVICE_TOKEN, CONTENT_AGENT_AI_TIMEOUT_SECONDS, and CONTENT_AGENT_UPLOAD_TTL_DAYS to backend-service and celery-worker containers in both docker-compose.yml and docker-compose.production.yml. Co-Authored-By: Claude Sonnet 4.6 * feat(etl): admin UI dynamic sources, Docker volumes, runbook (Tasks 14-15) - ContentAgentModal: replaced hardcoded sources with dynamic getSourceCatalog() call - Shows license/version/record count/status per snapshot; core sources auto-preselected - Upload attestation checkbox required for admin_upload source type - ContentAgentDrawer: blocking validation errors disable Apply button - 12 new i18n keys in en.ts + vi.ts (sourceCatalog, uploadAttestation, etc.) - docker-compose: content_etl_data volume for /data/content-etl (dev + prod) - backend/.env.example: CONTENT_ETL_ENABLED=false - ai-service/.gitignore: exclude data/content-etl/ - docs/runbooks/licensed-content-etl.md: installation, activation, rollback - 30 admin tests passing, pnpm build:check clean Co-Authored-By: Claude Sonnet 4.6 * feat(agents): implement Ranking Agent + Notification Campaign Agent with LangGraph Ranking Agent (backend + admin UI): - RankingAgentJob model + Alembic migration (queued→calculating→validating→preview_ready→applying→completed) - Three job types: league_reset (promotion/demotion), xp_event (boost items), achievement_batch - LeagueResetEngine, XPEventEngine, AchievementBatchEngine with dry-run preview artifacts - Celery tasks: run_ranking_agent_job + auto_league_reset (Monday beat schedule) - Admin routes /admin/ranking-agent/jobs (create/list/get/apply/cancel/retry) - RankingAgentPage + Modal (3-tab) + Drawer (polling + preview + apply) - ai-service ranking insights route with Groq-based league commentary Notification Campaign Agent (backend + ai-service + admin UI): - NotificationCampaignJob model + Alembic migration (9-state machine) - Audience segmentation: CEFR, leagues, streak, inactive_days, FCM token filter - FCM batch sender (500 tokens/batch) + in-app broadcast via notifications table - LangGraph pipeline (ai-service): analyze_audience → generate_variants (Groq, 3 variants) → evaluate_variants → [conditional loop ≤2 retries, threshold 0.70] → finalize - X-Admin-Api-Key service-to-service auth on /api/notification-agent/generate-content - Celery task: segment → generate (optional AI copy) → validate → preview_ready - Admin routes /admin/notification-campaign/jobs with scheduled_push send_at enforcement - NotificationCampaignPage + Modal (3-tab) + Drawer (polling + preview + delivery stats) Bug fixes (from two-pass code review): - set_preview/set_failed now go through transition() to enforce state machine - list_jobs filters by requested_by_id for non-superadmins - _fetch_ai_copy: add X-Admin-Api-Key header + use settings timeout (was hardcoded env var) - _fetch_ai_copy: fix response parsing (best_variant at root, not nested under data) - notification_agent route: use pipeline final_title/body for best_variant (not variants[0]) - in_app_broadcast overrides has_fcm_token=False before segmentation - Markdown fence stripping fixed with re.sub (handles uppercase JSON tag + closing fence) - rebalance_basic_vocabulary migration: guard against empty vocab catalog - Removed dead preview_ready→queued transition from ALLOWED_TRANSITIONS Co-Authored-By: Claude Sonnet 4.6 * feat(content-agent): licensed ETL pipeline, upload rights attestation, enhanced validation Content ETL pipeline (ai-service): - Full source adapter registry: OEWN, CEFR-J, CMU Pronouncing Dict, Tatoeba, Wikidata - ETL pipeline with SHA-256 integrity checks, quarantine threshold guard, defusedxml XML parsing - Storage layer: immutable snapshot store, record-level checksums, dedup by content hash - CLI (typer): etl run, status, validate-manifest, list-sources commands - Dockerfile import guard: fails build if ETL runtime deps are missing Content Agent validation (backend-service): - Expanded `validate_artifact` to accept `pinned_snapshots` + `admin_upload` context - New gates: MANIFEST_FIELDS_MISSING (14 required fields), MANIFEST_DUPLICATE_SOURCE, PINNED_SNAPSHOT_MISMATCH, UPLOAD_MISSING, UPLOAD_EXPIRED, RIGHTS_NOT_CONFIRMED, SHA256_INVALID, RECORD_CHECKSUM_MISMATCH, EXERCISE_TYPE_UNKNOWN - `_exercise_type_mapping()` loads ui_type→type from contract JSON (lru_cache) - `detect_upload_format()` in uploads service (CSV vs JSON by filename + magic bytes) Content Agent apply (backend-service): - Validate upload expiry + rights_confirmed before running artifact gates - Write full provenance fields on apply: source_version, license_id, license_url, attribution_text, raw_checksum, record_checksum, lineage, content_usage, rights_confirmed_at, rights_statement_version Content Agent routes (backend-service): - `POST /upload`: require rights_confirmed=True attestation; detect format via detect_upload_format() instead of MIME-type allowlist - `GET /sources`: new endpoint returning available snapshot catalog from ai-service Content Agent admin UI (admin-service): - ContentAgentModal: dynamic source picker (fetches /sources, shows version + license) - ContentAgentDrawer: upload section with rights attestation checkbox - contentAgentApi.ts: typed SourceSnapshot + listSources() + rights_confirmed field - Tests updated for new modal structure + API shape Flutter admin screens: - analytics_screen.dart: full rewrite with TabController (Overview/Courses/Users tabs), fl_chart bar charts, StaggeredEntrance entrance animations, skeleton loading states - dashboard_screen.dart: KpiCountCard grid, StaggeredEntrance wrappers, chart sections - New shared widgets: AdminSkeleton, KpiCountCard, StaggeredEntrance Contracts: - course-artifact-v2.schema.json: add provenance fields (source_version, license_id, etc.) - source-record-v2.schema.json: tighten required fields Co-Authored-By: Claude Sonnet 4.6 * fix(content-agent): address post-review correctness issues - tasks/content_agent.py: add lock=True to exception handler get() — prevents concurrent workers from racing on fail() writes - models/content_agent.py: add FK(users.id, SET NULL) to uploader_id — removes dangling-UUID risk if the user row is deleted - pipeline.py: read normalized file once (read_bytes) and decode for line iteration — eliminates TOCTOU checksum mismatch window on double read - pipeline.py: move `import re` from inside _error_code to module level - content_agent_sources.py: fix docstring — virtual sources are SKIPPED not returned with a synthetic descriptor - ai-service/routes/content_agent.py: log warning when Redis._instance is None and local fallback is disabled, making misconfiguration visible in logs Co-Authored-By: Claude Sonnet 4.6 * feat(v1.7.0): Lexi session management, STT ensemble, topic chat service, translate API AI Service: - Lexi Chat major refactor: extracted session store (LexiSessionStore), idempotency store, and pipeline helpers into dedicated service modules - Added SSE streaming with heartbeat keep-alive, per-turn story_ctx pre-fetch fix (bug: story_ctx was fetched after AI response, now fetched before pipeline runs) - New /lexi/stream endpoint with typewriter chunk streaming + TTS after last chunk - Added cursor-based pagination utils (api/utils/cursor.py) for lexi message history - New /translate route for contextual word translation - Topic chat extracted to topic_chat_service.py; persist uses insert_many for atomicity - Replaced private _env_float import in topic_chat_service with local definition - Expanded test suite: lexi routes, topic chat, translate, TTS, admin, lifespan, ollama Backend Service: - ai_service_client.py: added Lexi session/chat/stream client methods - YouTube route: contextual translation cache with word+lang key - Expanded test coverage for YouTube, notification campaigns, ranking agent XP events, vocabulary catalog normalization Flutter App: - Lexi Chat fix: SSE done event now carries lexi_response fullText fallback — resolves empty bubble (only Nghe button) when LLM stream fails but TTS audio succeeds - Notifications: new ReviewReminderNotificationSyncService + DI wiring, notification local datasource and provider updates - Settings: theme provider refactor + tests - Deleted unused ReviewReminderBanner widget - YouTube provider and repository improvements - New icon assets (002_star, 008_gear, hearts, gems, crown, arrows, etc.) Co-Authored-By: Claude Sonnet 4.6 * fix(lexi): resolve empty response and missing text in Lexi chat Root cause: Qwen3-32b on Groq uses thinking tokens that consume the max_tokens=512 budget, leaving no room for actual response content. The /no_think fix existed across the codebase but was missing in stream_llm_tokens, generate_node, and diagnose_node in nodes_v2.py. - ai-service: prepend /no_think to first user message for Qwen3 in stream_llm_tokens (streaming path), generate_node (non-streaming), and diagnose_node (grammar analysis) - ai-service: fix sanitize_lexi_response("") returning "" instead of fallback message (early return bug) - flutter: prefer server-sanitized fullText over raw accumulated chunks in LexiStreamDone handler to avoid rendering leaked blocks - flutter: fix locale sync race condition on Flutter web (Consumer2 rebuild triggering redundant setLocale() calls on theme change) Co-Authored-By: Claude Sonnet 4.6 * style(dashboard): adjust padding, spacing, and font sizes for improved layout consistency style(admin-shell): refine padding, sizes, and font styles for better UI alignment style(kpi-count-card): update padding, sizes, and font attributes for enhanced visual appeal * fix(voice): resolve speaking practice stuck in processing state - Create assets/animation/ directory with Lottie JSON files (PulseLoader, Sandy Loading, SuccessCheck) — fixes 404 errors and eliminates the hourglass fallback icon in the processing state - Improve LottieAnimationWidget errorBuilder: shows CircularProgressIndicator for loading variants, icons for success/heart/star fallbacks - Add errorBuilder to LottieLoadingWidget so RecordButton always shows a visible spinner even when Lottie fails to parse - Add 30s timeout to STT and TTS HTTP requests so processing can never hang indefinitely - Wrap _assessPronunciation in try/finally so _isProcessing is always reset even if an unexpected exception escapes the provider - Localize RecordButton labels (Processing.../Tap to record/Tap to stop) Co-Authored-By: Claude Sonnet 4.6 * fix(ci): resolve Flutter clearMilestone syntax, lockfile sync, and Gitleaks test false-positive - streak_provider.dart: add missing closing brace for claimDailyReward — clearMilestone was inadvertently nested inside the method during conflict resolution - admin-service/package-lock.json: regenerate to match bumped package.json versions (antd 6.4.4, axios 1.18.0, vercel 54.14.2) - .gitleaks.toml: allowlist test_admin_routes.py fake API key fixtures to eliminate false-positive CI failures Co-Authored-By: Claude Sonnet 4.6 * fix(ai-service): restore sentence-transformers/transformers/numpy to torch-2.2.x compatible versions Rebase conflict resolution incorrectly bumped: - sentence-transformers from >=4.1.0,<5.0.0 to >=5.5.1 (conflicts with constraints-ai.txt pin ==4.1.0) - transformers from >=4.41.0,<5.0.0 to >=5.12.1 (contradicts the comment and constraint ==4.57.6) - numpy from >=1.26.4,<2.0.0 to >=2.4.6 (conflicts with constraint ==1.26.4) Restored to versions verified compatible with torch==2.2.2 on both macOS x86_64 and CI Ubuntu. Co-Authored-By: Claude Sonnet 4.6 * fix(backend): pin kombu<5.6 to resolve redis>=7.4.1 dependency conflict kombu 5.6.x added a redis<6.5 constraint that conflicts with redis>=7.4.1. Pin kombu[redis]>=5.5.0,<5.6 (uses no upper bound on redis) and align celery to 5.5.x accordingly. Co-Authored-By: Claude Sonnet 4.6 * fix(backend): remove kombu[redis] extra to resolve redis>=7 conflict kombu[redis] 5.5.x pins redis<=5.2.1; dropping the extra lets pip resolve redis 7.x freely since redis-py is already a direct dep. Co-Authored-By: Claude Sonnet 4.6 * ci: trigger CI run after kombu requirements fix Co-Authored-By: Claude Sonnet 4.6 * fix(backend): remove leftover conflict markers from main.py Stray <<<<<<< HEAD / >>>>>>> markers caused SyntaxError on import. Co-Authored-By: Claude Sonnet 4.6 * fix(ci): fix XP test failures and flutter analyze errors - test_xp_routes: add source_id to lesson-source tests (REPEAT_SENSITIVE_SOURCES requires it) - streak_provider: remove duplicate restoreStreak/claimDailyReward methods from rebase replay - flutter: fix BuildContext async gap, replace withOpacity with withValues, remove unused import, fix underscore naming Co-Authored-By: Claude Sonnet 4.6 --------- Signed-off-by: dependabot[bot] Co-authored-by: Claude Sonnet 4.6 Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 128 +- .github/workflows/security.yml | 62 + .gitignore | 5 + .gitleaks.toml | 76 + AGENTS.md | 58 + CLAUDE.md | 76 + admin-service/package-lock.json | 1802 ++++++++++++----- admin-service/package.json | 4 +- admin-service/pnpm-lock.yaml | 255 +++ admin-service/src/App.tsx | 8 +- .../content-agent/ContentAgentDrawer.test.tsx | 153 ++ .../content-agent/ContentAgentDrawer.tsx | 743 +++++++ .../content-agent/ContentAgentModal.test.tsx | 204 ++ .../content-agent/ContentAgentModal.tsx | 676 +++++++ .../dashboard/CompletionFunnelChart.tsx | 6 +- .../dashboard/CoursePopularityChart.tsx | 4 +- .../components/dashboard/EngagementChart.tsx | 2 +- .../components/dashboard/UserGrowthChart.tsx | 2 +- .../NotificationCampaignDrawer.tsx | 345 ++++ .../NotificationCampaignModal.tsx | 337 +++ .../ranking-agent/RankingAgentDrawer.tsx | 386 ++++ .../ranking-agent/RankingAgentModal.tsx | 431 ++++ .../user-management/UserDetailModal.tsx | 20 +- admin-service/src/lib/adminApi.ts | 54 +- admin-service/src/lib/auth.test.ts | 26 + admin-service/src/lib/contentAgentApi.test.ts | 124 ++ admin-service/src/lib/contentAgentApi.ts | 389 ++++ .../src/lib/contentContracts.test.ts | 22 + admin-service/src/lib/i18n/en.ts | 98 + admin-service/src/lib/i18n/vi.ts | 98 + .../src/lib/notificationCampaignApi.ts | 145 ++ admin-service/src/lib/rankingAgentApi.ts | 164 ++ .../src/pages/AdminManagementPage.tsx | 95 +- .../src/pages/AiChatSettingsPage.tsx | 20 +- admin-service/src/pages/ContentLabPage.tsx | 23 +- admin-service/src/pages/CoursesPage.tsx | 79 + .../src/pages/LessonExercisesPage.tsx | 2 +- admin-service/src/pages/LessonsPage.tsx | 8 +- .../src/pages/NotificationCampaignPage.tsx | 200 ++ admin-service/src/pages/RankingAgentPage.tsx | 202 ++ admin-service/src/pages/TopicsPage.tsx | 42 +- .../src/pages/UserManagementPage.tsx | 19 +- admin-service/src/pages/VocabularyPage.tsx | 2 +- admin-service/src/styles.css | 946 +++++++++ admin-service/tsconfig.tsbuildinfo | 1 - ai-service/.env.example | 86 +- ai-service/.gitignore | 3 + ai-service/Dockerfile | 19 +- ai-service/Dockerfile.prod | 53 +- ai-service/api/core/auth.py | 10 +- ai-service/api/core/config.py | 135 +- ai-service/api/main.py | 117 +- ai-service/api/models/content_agent.py | 329 +++ ai-service/api/routes/chat.py | 101 +- ai-service/api/routes/content_agent.py | 272 +++ ai-service/api/routes/lexi_chat.py | 781 +++---- ai-service/api/routes/notification_agent.py | 94 + ai-service/api/routes/ranking_agent.py | 64 + ai-service/api/routes/stt.py | 382 +++- ai-service/api/routes/topic_chat.py | 295 +-- ai-service/api/routes/translate.py | 178 ++ ai-service/api/routes/tts.py | 2 +- .../api/services/content_agent/__init__.py | 5 + .../api/services/content_agent/adapters.py | 197 ++ .../api/services/content_agent/generator.py | 299 +++ .../api/services/content_agent/planner.py | 231 +++ .../api/services/content_agent/policies.py | 233 +++ .../api/services/content_agent/service.py | 357 ++++ .../api/services/content_agent/store.py | 372 ++++ .../api/services/content_etl/__init__.py | 18 + .../services/content_etl/adapters/__init__.py | 1 + .../api/services/content_etl/adapters/base.py | 23 + .../services/content_etl/adapters/cefr_j.py | 102 + .../services/content_etl/adapters/cmudict.py | 95 + .../content_etl/adapters/common_voice.py | 100 + .../content_etl/adapters/librispeech.py | 85 + .../api/services/content_etl/adapters/oewn.py | 114 ++ .../services/content_etl/adapters/tatoeba.py | 101 + .../services/content_etl/adapters/wikidata.py | 78 + .../api/services/content_etl/archive.py | 114 ++ ai-service/api/services/content_etl/cli.py | 196 ++ .../api/services/content_etl/contracts.py | 359 ++++ .../api/services/content_etl/downloader.py | 233 +++ .../api/services/content_etl/pipeline.py | 474 +++++ .../api/services/content_etl/registry.py | 270 +++ .../api/services/content_etl/sources.py | 242 +++ .../api/services/content_etl/storage.py | 512 +++++ ai-service/api/services/gateway_setup.py | 20 +- .../services/handlers/ollama_qwen_handler.py | 64 +- .../api/services/handlers/whisper_handler.py | 309 +-- ai-service/api/services/kg_data_loader.py | 155 ++ ai-service/api/services/kg_service_v3.py | 211 +- .../api/services/lexi_idempotency_store.py | 101 + .../api/services/lexi_pipeline_helpers.py | 135 ++ ai-service/api/services/lexi_session_store.py | 124 ++ .../services/notification_agent/__init__.py | 0 .../api/services/notification_agent/graph.py | 97 + .../api/services/notification_agent/nodes.py | 208 ++ .../api/services/notification_agent/state.py | 35 + ai-service/api/services/ollama_service.py | 2 +- .../api/services/ranking_agent_insights.py | 140 ++ ai-service/api/services/stt/__init__.py | 7 + ai-service/api/services/stt/audio_ingest.py | 52 + ai-service/api/services/stt/cleanup.py | 5 + ai-service/api/services/stt/config.py | 176 ++ .../api/services/stt/engines/__init__.py | 9 + ai-service/api/services/stt/engines/base.py | 35 + .../services/stt/engines/faster_whisper.py | 142 ++ .../api/services/stt/engines/moonshine.py | 139 ++ ai-service/api/services/stt/engines/sherpa.py | 114 ++ ai-service/api/services/stt/errors.py | 26 + ai-service/api/services/stt/metrics.py | 18 + ai-service/api/services/stt/model_registry.py | 102 + ai-service/api/services/stt/ring_buffer.py | 38 + ai-service/api/services/stt/runtime.py | 63 + ai-service/api/services/stt/schemas.py | 105 + .../api/services/stt/sentence_finalizer.py | 73 + .../api/services/stt/session_manager.py | 170 ++ .../api/services/stt/temp_audio_writer.py | 68 + .../api/services/stt/trace_cag_adapter.py | 225 ++ .../api/services/stt/transcript_normalizer.py | 15 + .../api/services/stt/transcript_state.py | 25 + .../api/services/stt/vad_endpointing.py | 126 ++ .../api/services/stt/verifier_router.py | 42 + ai-service/api/services/stt/voice_session.py | 258 +++ ai-service/api/services/stt_service.py | 56 +- ai-service/api/services/topic_chat_service.py | 185 ++ ai-service/api/services/topic_preloader.py | 11 +- .../trace_cag/benchmark/qa_generation.py | 3 +- ai-service/api/services/trace_cag/edges.py | 15 +- ai-service/api/services/trace_cag/graph.py | 35 +- ai-service/api/services/trace_cag/nodes_v2.py | 141 +- .../services/trace_cag/retrieval_ranker.py | 24 +- ai-service/api/services/trace_cag/state.py | 5 +- ai-service/api/utils/__init__.py | 0 ai-service/api/utils/cursor.py | 101 + ai-service/constraints-ai.txt | 9 + .../data/kg/06_tracecag_topic_expansion.json | 0 .../tracecag_topic_corpus_report.json | 0 ai-service/data/sample_stories.json | 0 ai-service/docker-compose.yml | 7 +- ai-service/docs/stt-production-checklist.md | 42 + ...4-stt-streaming-ensemble-implementation.md | 176 ++ .../2026-06-16-lexi-session-ownership.md | 500 +++++ ...026-06-14-stt-streaming-ensemble-design.md | 615 ++++++ ai-service/requirements.txt | 17 +- ai-service/scripts/download_stt_models.sh | 16 + ai-service/scripts/kg_pipeline/config.py | 88 +- .../kg_pipeline/crawlers/cefr_lists.py | 135 -- .../crawlers/conceptnet_extractor.py | 133 -- .../kg_pipeline/crawlers/web_crawler.py | 263 --- .../kg_pipeline/crawlers/wiktionary_kaikki.py | 196 -- .../kg_pipeline/crawlers/wordnet_extractor.py | 124 -- .../kg_pipeline/requirements_pipeline.txt | 24 +- .../scripts/kg_pipeline/run_pipeline.py | 245 +-- .../content_etl/fixtures/cefr_j_mini.csv | 6 + .../content_etl/fixtures/cmudict_mini.dict | 7 + .../fixtures/common_voice_mini.tsv | 3 + .../common_voice_release_metadata.json | 7 + .../content_etl/fixtures/librispeech_mini.txt | 3 + .../tests/content_etl/fixtures/oewn_mini.xml | 30 + .../content_etl/fixtures/tatoeba_clean.tsv | 2 + .../content_etl/fixtures/tatoeba_mini.tsv | 4 + .../content_etl/fixtures/wikidata_mini.json | 26 + ai-service/tests/content_etl/test_archive.py | 131 ++ ai-service/tests/content_etl/test_cli.py | 132 ++ .../tests/content_etl/test_contracts.py | 298 +++ .../tests/content_etl/test_core_adapters.py | 220 ++ .../tests/content_etl/test_corpus_adapters.py | 176 ++ .../tests/content_etl/test_downloader.py | 210 ++ ai-service/tests/content_etl/test_pipeline.py | 305 +++ ai-service/tests/content_etl/test_registry.py | 149 ++ ai-service/tests/content_etl/test_storage.py | 379 ++++ .../test_licensed_etl_content_agent_flow.py | 116 ++ ai-service/tests/stt/__init__.py | 1 + ai-service/tests/stt/fakes.py | 74 + ai-service/tests/stt/test_audio_ingest.py | 30 + ai-service/tests/stt/test_config.py | 35 + .../tests/stt/test_faster_whisper_adapter.py | 24 + ai-service/tests/stt/test_long_session.py | 42 + .../tests/stt/test_moonshine_adapter.py | 41 + ai-service/tests/stt/test_pipeline.py | 47 + ai-service/tests/stt/test_session_manager.py | 161 ++ ai-service/tests/stt/test_storage.py | 27 + ai-service/tests/stt/test_stt_routes.py | 185 ++ .../tests/stt/test_trace_cag_adapter.py | 230 +++ ai-service/tests/stt/test_voice_session.py | 245 +++ ai-service/tests/test_admin_routes.py | 223 ++ ai-service/tests/test_auth_module.py | 14 +- ai-service/tests/test_container_hardening.py | 54 + .../tests/test_content_agent_adapters.py | 102 + .../tests/test_content_agent_generator.py | 109 + .../tests/test_content_agent_planner.py | 104 + .../tests/test_content_agent_policies.py | 79 + ai-service/tests/test_content_agent_routes.py | 372 ++++ ai-service/tests/test_content_agent_store.py | 151 ++ .../tests/test_content_contract_parity.py | 86 + .../tests/test_dependency_constraints.py | 40 + ai-service/tests/test_kg_data_loader.py | 90 + .../tests/test_kg_pipeline_source_safety.py | 53 + ai-service/tests/test_lexi_chat_routes.py | 544 +++++ .../tests/test_lexi_session_management.py | 8 +- ai-service/tests/test_main_lifespan.py | 55 + .../tests/test_notification_agent_routes.py | 151 ++ ai-service/tests/test_ollama_router.py | 171 ++ ai-service/tests/test_ranking_agent_routes.py | 124 ++ ai-service/tests/test_topic_chat_routes.py | 295 +++ .../tests/test_tracecag_chat_integration.py | 45 + ai-service/tests/test_translate_routes.py | 161 ++ ai-service/tests/test_tts_routes.py | 190 ++ .../trace_cag/test_pipeline_integration.py | 66 +- backend-service/.env.example | 14 + backend-service/.env.test.example | 36 + .../versions/add_cefr_content_agent.py | 232 +++ .../versions/add_content_provenance_v2.py | 89 + .../add_notification_campaign_jobs.py | 59 + .../versions/add_ranking_agent_jobs.py | 71 + .../versions/add_referral_fields_to_users.py | 45 + .../versions/rebalance_basic_vocabulary.py | 9 +- backend-service/app/cli/__init__.py | 1 + backend-service/app/cli/content_agent.py | 86 + .../app/clients/ai_service_client.py | 28 + backend-service/app/core/celery_app.py | 45 +- backend-service/app/core/config.py | 44 +- backend-service/app/core/security.py | 13 +- backend-service/app/crud/vocabulary.py | 44 +- backend-service/app/main.py | 61 + backend-service/app/models/__init__.py | 23 + backend-service/app/models/content_agent.py | 166 ++ .../app/models/notification_campaign.py | 53 + backend-service/app/models/ranking_agent.py | 58 + backend-service/app/models/user.py | 6 + backend-service/app/routes/content_agent.py | 414 ++++ backend-service/app/routes/learning.py | 43 + .../app/routes/notification_campaign.py | 270 +++ backend-service/app/routes/ranking_agent.py | 244 +++ backend-service/app/routes/referral.py | 158 ++ backend-service/app/routes/reminders.py | 2 + backend-service/app/routes/users.py | 87 +- backend-service/app/routes/vocabulary.py | 41 + backend-service/app/routes/youtube.py | 50 +- backend-service/app/schemas/content_agent.py | 291 +++ .../app/schemas/notification_campaign.py | 133 ++ backend-service/app/schemas/ranking_agent.py | 121 ++ .../app/services/content_agent_apply.py | 285 +++ .../app/services/content_agent_client.py | 109 + .../app/services/content_agent_jobs.py | 209 ++ .../app/services/content_agent_sources.py | 116 ++ .../app/services/content_agent_uploads.py | 157 ++ .../app/services/content_agent_validation.py | 861 ++++++++ .../notification_campaign/__init__.py | 0 .../services/notification_campaign/apply.py | 90 + .../notification_campaign/segmenter.py | 143 ++ .../services/notification_campaign/sender.py | 119 ++ .../services/notification_campaign_jobs.py | 167 ++ .../app/services/push_notification_service.py | 72 + .../app/services/ranking_agent/__init__.py | 0 .../ranking_agent/achievement_batch.py | 114 ++ .../app/services/ranking_agent/apply.py | 366 ++++ .../services/ranking_agent/league_reset.py | 125 ++ .../app/services/ranking_agent/xp_event.py | 67 + .../app/services/ranking_agent_ai_client.py | 49 + .../app/services/ranking_agent_jobs.py | 154 ++ .../app/services/vocabulary_catalog.py | 186 ++ backend-service/app/services/xp_service.py | 8 + backend-service/app/tasks/content_agent.py | 320 +++ backend-service/app/tasks/content_prefetch.py | 56 + .../app/tasks/notification_campaign.py | 200 ++ backend-service/app/tasks/ranking_agent.py | 146 ++ backend-service/app/tasks/streak_reminders.py | 77 + backend-service/app/tasks/word_of_day.py | 20 + backend-service/render.yaml | 6 +- backend-service/requirements.txt | 11 +- backend-service/tests/conftest.py | 15 + .../test_content_agent_licensed_etl_flow.py | 126 ++ .../tests/test_content_agent_apply.py | 258 +++ .../tests/test_content_agent_contract.py | 72 + .../tests/test_content_agent_jobs.py | 101 + .../tests/test_content_agent_routes.py | 123 ++ .../tests/test_content_agent_sources.py | 195 ++ .../tests/test_content_agent_tasks.py | 153 ++ .../tests/test_content_agent_uploads.py | 96 + .../tests/test_content_agent_validation.py | 422 ++++ .../tests/test_content_contract_parity.py | 77 + .../tests/test_fsrs_algorithm_correctness.py | 422 ++++ .../tests/test_lesson_context_sanitization.py | 27 + .../tests/test_notification_campaign_apply.py | 250 +++ .../tests/test_notification_campaign_jobs.py | 254 +++ .../test_notification_campaign_schemas.py | 160 ++ .../test_notification_campaign_sender.py | 234 +++ .../tests/test_ranking_agent_engines.py | 168 ++ .../tests/test_ranking_agent_jobs.py | 123 ++ .../tests/test_ranking_agent_routes.py | 82 + .../tests/test_ranking_agent_xp_event.py | 145 ++ backend-service/tests/test_referral_routes.py | 384 ++++ .../tests/test_reminder_service.py | 7 +- .../tests/test_vocabulary_catalog.py | 244 +++ .../test_vocabulary_catalog_normalize.py | 59 + backend-service/tests/test_xp_routes.py | 7 +- backend-service/tests/test_xp_service.py | 151 +- backend-service/tests/test_youtube_routes.py | 221 +- .../course-artifact-v2.schema.json | 491 +++++ .../content-agent/exercise-types-v1.json | 32 + .../fixtures/licensed-etl-artifact-v2.json | 307 +++ .../source-record-v2.schema.json | 271 +++ docker-compose.yml | 87 + docs/Report/RPT-024_GAMES_ENGINE.md | 27 +- docs/Report/RPT-025_GAMIFICATION_XP_SYSTEM.md | 23 +- docs/badge-design-prompts.md | 358 ++++ docs/qa/game-system-acceptance.md | 132 ++ docs/runbooks/licensed-content-etl.md | 270 +++ .../2026-06-01-tracecag-bulk-topic-corpus.md | 30 +- .../plans/2026-06-07-game-system-stability.md | 106 +- .../plans/2026-06-15-cefr-content-agent.md | 466 +++++ ...-06-15-licensed-content-etl-remediation.md | 1138 +++++++++++ .../plans/2026-06-15-licensed-content-etl.md | 867 ++++++++ .../2026-06-14-cefr-content-agent-design.md | 681 +++++++ docs/topic-names.md | 68 + flutter-app/assets/animation/Confetti.json | 1 + flutter-app/assets/animation/HeartBeat.json | 1 + .../assets/animation/LevelUpOrbit.json | 1 + .../assets/animation/Live Love Learn.json | 1 + flutter-app/assets/animation/PulseLoader.json | 1 + .../assets/animation/Sandy Loading.json | 1 + .../assets/animation/SpinningDots.json | 1 + flutter-app/assets/animation/StarBurst.json | 1 + .../assets/animation/SuccessCheck.json | 1 + flutter-app/assets/animation/Welcome.json | 1 + flutter-app/assets/i18n/en.json | 67 +- flutter-app/assets/i18n/es.json | 52 +- flutter-app/assets/i18n/fr.json | 52 +- flutter-app/assets/i18n/ja.json | 54 +- flutter-app/assets/i18n/ko.json | 54 +- flutter-app/assets/i18n/vi.json | 69 +- flutter-app/assets/i18n/zh.json | 52 +- flutter-app/assets/icon/002_star.png | Bin 0 -> 10037 bytes flutter-app/assets/icon/008_gear.png | Bin 0 -> 11058 bytes flutter-app/assets/icon/010_r02_c01.png | Bin 0 -> 22600 bytes flutter-app/assets/icon/010_r02_c04.png | Bin 0 -> 34936 bytes .../assets/icon/013_r01_c13_clock_gloss.png | Bin 0 -> 16198 bytes .../assets/icon/014_r02_c01_heart_bold.png | Bin 0 -> 15524 bytes flutter-app/assets/icon/016_red_cross.png | Bin 0 -> 10419 bytes .../assets/icon/017_green_checkmark.png | Bin 0 -> 8485 bytes .../icon/018_r02_c05_purple_gem_bold.png | Bin 0 -> 15640 bytes flutter-app/assets/icon/022_r04_c04.png | Bin 0 -> 39079 bytes .../icon/025_r02_c12_treasure_chest_bold.png | Bin 0 -> 13963 bytes flutter-app/assets/icon/028_gift_box.png | Bin 0 -> 9953 bytes .../assets/icon/037_unlocked_padlock.png | Bin 0 -> 7434 bytes flutter-app/assets/icon/041_crown.png | Bin 0 -> 8161 bytes flutter-app/assets/icon/047_back_arrow.png | Bin 0 -> 13156 bytes flutter-app/assets/icon/054_triangle_left.png | Bin 0 -> 6257 bytes .../assets/icon/055_triangle_right.png | Bin 0 -> 6574 bytes .../assets/icon/056_curved_back_arrow.png | Bin 0 -> 9122 bytes flutter-app/assets/icon/059_fast_forward.png | Bin 0 -> 14923 bytes flutter-app/assets/icon/068_speaker_on.png | Bin 0 -> 10088 bytes flutter-app/assets/icon/069_speaker_muted.png | Bin 0 -> 9804 bytes .../assets/icon/127_yellow_light_bulb.png | Bin 0 -> 9977 bytes flutter-app/assets/icon/129_speech_bubble.png | Bin 0 -> 8920 bytes flutter-app/assets/icon/gem.png | Bin 0 -> 994294 bytes flutter-app/assets/icon/icon_r05_c08.png | Bin 0 -> 16722 bytes flutter-app/assets/icon/icon_r07_c16.png | Bin 0 -> 11600 bytes flutter-app/assets/out-app/LexiLingo-text.png | Bin 0 -> 736263 bytes .../lib/core/services/deep_link_service.dart | 78 + .../core/services/local_cache_service.dart | 1 + .../core/services/notification_service.dart | 71 +- .../lib/core/services/purchases_service.dart | 102 + .../lib/core/services/rating_service.dart | 39 + flutter-app/lib/core/theme/app_theme.dart | 5 + flutter-app/lib/core/widgets/app_button.dart | 296 +++ flutter-app/lib/core/widgets/cefr_badge.dart | 80 + .../widgets/language_switcher_button.dart | 13 +- .../core/widgets/lottie_animation_widget.dart | 42 +- .../core/widgets/lottie_loading_widget.dart | 7 + .../lib/core/widgets/premium_gate.dart | 64 + .../lib/core/widgets/stagger_list.dart | 191 ++ flutter-app/lib/core/widgets/widgets.dart | 3 + .../screens/achievements_screen.dart | 29 +- .../widgets/achievement_unlock_overlay.dart | 298 +++ .../presentation/analytics_screen.dart | 1280 +++++++++--- .../presentation/dashboard_screen.dart | 1019 +++++++--- .../admin/shared/widgets/admin_shell.dart | 52 +- .../admin/shared/widgets/admin_skeleton.dart | 171 ++ .../admin/shared/widgets/kpi_count_card.dart | 148 ++ .../shared/widgets/staggered_entrance.dart | 60 + .../auth/presentation/pages/login_page.dart | 3 + .../presentation/providers/auth_provider.dart | 57 +- .../screens/book_library_screen.dart | 6 +- .../pages/story_selection_page.dart | 51 +- .../chat/presentation/widgets/topic_card.dart | 51 +- .../services/game_pronunciation_service.dart | 161 ++ .../lib/features/games/di/games_di.dart | 9 + .../screens/spelling_bee_screen.dart | 582 +++--- .../providers/gamification_provider.dart | 35 + .../screens/leaderboard_screen.dart | 23 + .../screens/league_ceremony_screen.dart | 273 +++ .../presentation/screens/shop_screen.dart | 5 + .../widgets/active_boosts_bar.dart | 179 ++ .../widgets/boost_purchase_animation.dart | 162 ++ flutter-app/lib/features/home/di/home_di.dart | 4 + .../home/presentation/pages/home_page.dart | 115 +- .../presentation/providers/home_provider.dart | 25 + .../providers/learning_provider.dart | 4 + .../screens/learning_session_screen.dart | 17 +- .../datasources/lexi_chat_data_source.dart | 5 + .../providers/lexi_chat_provider.dart | 11 +- .../notification_local_datasource.dart | 1 + .../notifications/di/notification_di.dart | 13 +- ...ew_reminder_notification_sync_service.dart | 79 + .../pages/notifications_page.dart | 4 +- .../providers/notification_provider.dart | 35 +- .../presentation/screens/paywall_screen.dart | 179 ++ .../providers/streak_provider.dart | 11 + .../screens/my_progress_screen.dart | 111 +- .../widgets/daily_reward_dialog.dart | 109 +- .../widgets/streak_milestone_overlay.dart | 281 +++ .../presentation/widgets/streak_widget.dart | 4 +- .../presentation/widgets/xp_line_chart.dart | 338 ++++ .../user/presentation/pages/legal_page.dart | 163 ++ .../presentation/pages/settings_page.dart | 375 +++- .../providers/settings_provider.dart | 54 +- .../vocabulary_remote_datasource.dart | 36 +- .../vocabulary_repository_impl.dart | 14 + .../repositories/vocabulary_repository.dart | 3 + .../providers/vocab_provider.dart | 4 +- .../screens/session_complete_screen.dart | 17 +- .../screens/word_of_day_screen.dart | 348 ++++ .../widgets/flashcard_widget.dart | 35 +- .../widgets/word_of_day_card.dart | 146 ++ .../datasources/voice_remote_datasource.dart | 13 +- .../screens/voice_practice_screen.dart | 22 +- .../presentation/widgets/record_button.dart | 7 +- .../data/repositories/youtube_repository.dart | 17 +- .../providers/youtube_provider.dart | 6 +- flutter-app/lib/main.dart | 150 +- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + flutter-app/pubspec.lock | 112 + flutter-app/pubspec.yaml | 13 + .../test/core/widgets/cefr_badge_test.dart | 58 + .../data/game_pronunciation_service_test.dart | 274 +++ .../screens/game_accessibility_test.dart | 318 +++ .../screens/game_completion_test.dart | 389 ++++ .../screens/game_load_state_test.dart | 273 +++ ...ification_provider_active_boosts_test.dart | 122 ++ .../notification_local_datasource_test.dart | 51 + ...minder_notification_sync_service_test.dart | 179 ++ .../streak_provider_milestone_test.dart | 175 ++ .../settings_provider_theme_test.dart | 76 + .../vocabulary_word_of_day_test.dart | 85 + .../flutter/generated_plugin_registrant.cc | 6 + .../windows/flutter/generated_plugins.cmake | 2 + mcp-server/resources/common.py | 50 + mcp-server/resources/conversation.py | 54 +- mcp-server/resources/learner_profile.py | 49 +- mcp-server/resources/lesson_context.py | 78 +- mcp-server/server.py | 17 +- mcp-server/start_production.sh | 5 +- mcp-server/tests/conftest.py | 7 + mcp-server/tests/test_full.py | 8 - mcp-server/tests/test_integration.py | 15 +- mcp-server/tests/test_resources.py | 104 + .../tests/test_security_configuration.py | 56 + mcp-server/utils/api_client.py | 56 +- .../provisioning/datasources/prometheus.yml | 9 + monitoring/prometheus.yml | 28 + monitoring/rules/alerts.yml | 45 + 467 files changed, 53366 insertions(+), 5082 deletions(-) create mode 100644 .github/workflows/security.yml create mode 100644 .gitleaks.toml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 admin-service/src/components/content-agent/ContentAgentDrawer.test.tsx create mode 100644 admin-service/src/components/content-agent/ContentAgentDrawer.tsx create mode 100644 admin-service/src/components/content-agent/ContentAgentModal.test.tsx create mode 100644 admin-service/src/components/content-agent/ContentAgentModal.tsx create mode 100644 admin-service/src/components/notification-campaign/NotificationCampaignDrawer.tsx create mode 100644 admin-service/src/components/notification-campaign/NotificationCampaignModal.tsx create mode 100644 admin-service/src/components/ranking-agent/RankingAgentDrawer.tsx create mode 100644 admin-service/src/components/ranking-agent/RankingAgentModal.tsx create mode 100644 admin-service/src/lib/auth.test.ts create mode 100644 admin-service/src/lib/contentAgentApi.test.ts create mode 100644 admin-service/src/lib/contentAgentApi.ts create mode 100644 admin-service/src/lib/contentContracts.test.ts create mode 100644 admin-service/src/lib/notificationCampaignApi.ts create mode 100644 admin-service/src/lib/rankingAgentApi.ts create mode 100644 admin-service/src/pages/NotificationCampaignPage.tsx create mode 100644 admin-service/src/pages/RankingAgentPage.tsx delete mode 100644 admin-service/tsconfig.tsbuildinfo create mode 100644 ai-service/api/models/content_agent.py create mode 100644 ai-service/api/routes/content_agent.py create mode 100644 ai-service/api/routes/notification_agent.py create mode 100644 ai-service/api/routes/ranking_agent.py create mode 100644 ai-service/api/routes/translate.py create mode 100644 ai-service/api/services/content_agent/__init__.py create mode 100644 ai-service/api/services/content_agent/adapters.py create mode 100644 ai-service/api/services/content_agent/generator.py create mode 100644 ai-service/api/services/content_agent/planner.py create mode 100644 ai-service/api/services/content_agent/policies.py create mode 100644 ai-service/api/services/content_agent/service.py create mode 100644 ai-service/api/services/content_agent/store.py create mode 100644 ai-service/api/services/content_etl/__init__.py create mode 100644 ai-service/api/services/content_etl/adapters/__init__.py create mode 100644 ai-service/api/services/content_etl/adapters/base.py create mode 100644 ai-service/api/services/content_etl/adapters/cefr_j.py create mode 100644 ai-service/api/services/content_etl/adapters/cmudict.py create mode 100644 ai-service/api/services/content_etl/adapters/common_voice.py create mode 100644 ai-service/api/services/content_etl/adapters/librispeech.py create mode 100644 ai-service/api/services/content_etl/adapters/oewn.py create mode 100644 ai-service/api/services/content_etl/adapters/tatoeba.py create mode 100644 ai-service/api/services/content_etl/adapters/wikidata.py create mode 100644 ai-service/api/services/content_etl/archive.py create mode 100644 ai-service/api/services/content_etl/cli.py create mode 100644 ai-service/api/services/content_etl/contracts.py create mode 100644 ai-service/api/services/content_etl/downloader.py create mode 100644 ai-service/api/services/content_etl/pipeline.py create mode 100644 ai-service/api/services/content_etl/registry.py create mode 100644 ai-service/api/services/content_etl/sources.py create mode 100644 ai-service/api/services/content_etl/storage.py create mode 100644 ai-service/api/services/kg_data_loader.py create mode 100644 ai-service/api/services/lexi_idempotency_store.py create mode 100644 ai-service/api/services/lexi_pipeline_helpers.py create mode 100644 ai-service/api/services/lexi_session_store.py create mode 100644 ai-service/api/services/notification_agent/__init__.py create mode 100644 ai-service/api/services/notification_agent/graph.py create mode 100644 ai-service/api/services/notification_agent/nodes.py create mode 100644 ai-service/api/services/notification_agent/state.py create mode 100644 ai-service/api/services/ranking_agent_insights.py create mode 100644 ai-service/api/services/stt/__init__.py create mode 100644 ai-service/api/services/stt/audio_ingest.py create mode 100644 ai-service/api/services/stt/cleanup.py create mode 100644 ai-service/api/services/stt/config.py create mode 100644 ai-service/api/services/stt/engines/__init__.py create mode 100644 ai-service/api/services/stt/engines/base.py create mode 100644 ai-service/api/services/stt/engines/faster_whisper.py create mode 100644 ai-service/api/services/stt/engines/moonshine.py create mode 100644 ai-service/api/services/stt/engines/sherpa.py create mode 100644 ai-service/api/services/stt/errors.py create mode 100644 ai-service/api/services/stt/metrics.py create mode 100644 ai-service/api/services/stt/model_registry.py create mode 100644 ai-service/api/services/stt/ring_buffer.py create mode 100644 ai-service/api/services/stt/runtime.py create mode 100644 ai-service/api/services/stt/schemas.py create mode 100644 ai-service/api/services/stt/sentence_finalizer.py create mode 100644 ai-service/api/services/stt/session_manager.py create mode 100644 ai-service/api/services/stt/temp_audio_writer.py create mode 100644 ai-service/api/services/stt/trace_cag_adapter.py create mode 100644 ai-service/api/services/stt/transcript_normalizer.py create mode 100644 ai-service/api/services/stt/transcript_state.py create mode 100644 ai-service/api/services/stt/vad_endpointing.py create mode 100644 ai-service/api/services/stt/verifier_router.py create mode 100644 ai-service/api/services/stt/voice_session.py create mode 100644 ai-service/api/services/topic_chat_service.py create mode 100644 ai-service/api/utils/__init__.py create mode 100644 ai-service/api/utils/cursor.py create mode 100644 ai-service/constraints-ai.txt mode change 100755 => 100644 ai-service/data/kg/06_tracecag_topic_expansion.json mode change 100755 => 100644 ai-service/data/kg_output/tracecag_topic_corpus_report.json mode change 100755 => 100644 ai-service/data/sample_stories.json create mode 100644 ai-service/docs/stt-production-checklist.md create mode 100644 ai-service/docs/superpowers/plans/2026-06-14-stt-streaming-ensemble-implementation.md create mode 100644 ai-service/docs/superpowers/plans/2026-06-16-lexi-session-ownership.md create mode 100644 ai-service/docs/superpowers/specs/2026-06-14-stt-streaming-ensemble-design.md create mode 100755 ai-service/scripts/download_stt_models.sh delete mode 100644 ai-service/scripts/kg_pipeline/crawlers/cefr_lists.py delete mode 100644 ai-service/scripts/kg_pipeline/crawlers/conceptnet_extractor.py delete mode 100644 ai-service/scripts/kg_pipeline/crawlers/web_crawler.py delete mode 100644 ai-service/scripts/kg_pipeline/crawlers/wiktionary_kaikki.py delete mode 100644 ai-service/scripts/kg_pipeline/crawlers/wordnet_extractor.py create mode 100644 ai-service/tests/content_etl/fixtures/cefr_j_mini.csv create mode 100644 ai-service/tests/content_etl/fixtures/cmudict_mini.dict create mode 100644 ai-service/tests/content_etl/fixtures/common_voice_mini.tsv create mode 100644 ai-service/tests/content_etl/fixtures/common_voice_release_metadata.json create mode 100644 ai-service/tests/content_etl/fixtures/librispeech_mini.txt create mode 100644 ai-service/tests/content_etl/fixtures/oewn_mini.xml create mode 100644 ai-service/tests/content_etl/fixtures/tatoeba_clean.tsv create mode 100644 ai-service/tests/content_etl/fixtures/tatoeba_mini.tsv create mode 100644 ai-service/tests/content_etl/fixtures/wikidata_mini.json create mode 100644 ai-service/tests/content_etl/test_archive.py create mode 100644 ai-service/tests/content_etl/test_cli.py create mode 100644 ai-service/tests/content_etl/test_contracts.py create mode 100644 ai-service/tests/content_etl/test_core_adapters.py create mode 100644 ai-service/tests/content_etl/test_corpus_adapters.py create mode 100644 ai-service/tests/content_etl/test_downloader.py create mode 100644 ai-service/tests/content_etl/test_pipeline.py create mode 100644 ai-service/tests/content_etl/test_registry.py create mode 100644 ai-service/tests/content_etl/test_storage.py create mode 100644 ai-service/tests/integration/test_licensed_etl_content_agent_flow.py create mode 100644 ai-service/tests/stt/__init__.py create mode 100644 ai-service/tests/stt/fakes.py create mode 100644 ai-service/tests/stt/test_audio_ingest.py create mode 100644 ai-service/tests/stt/test_config.py create mode 100644 ai-service/tests/stt/test_faster_whisper_adapter.py create mode 100644 ai-service/tests/stt/test_long_session.py create mode 100644 ai-service/tests/stt/test_moonshine_adapter.py create mode 100644 ai-service/tests/stt/test_pipeline.py create mode 100644 ai-service/tests/stt/test_session_manager.py create mode 100644 ai-service/tests/stt/test_storage.py create mode 100644 ai-service/tests/stt/test_stt_routes.py create mode 100644 ai-service/tests/stt/test_trace_cag_adapter.py create mode 100644 ai-service/tests/stt/test_voice_session.py create mode 100644 ai-service/tests/test_admin_routes.py create mode 100644 ai-service/tests/test_container_hardening.py create mode 100644 ai-service/tests/test_content_agent_adapters.py create mode 100644 ai-service/tests/test_content_agent_generator.py create mode 100644 ai-service/tests/test_content_agent_planner.py create mode 100644 ai-service/tests/test_content_agent_policies.py create mode 100644 ai-service/tests/test_content_agent_routes.py create mode 100644 ai-service/tests/test_content_agent_store.py create mode 100644 ai-service/tests/test_content_contract_parity.py create mode 100644 ai-service/tests/test_dependency_constraints.py create mode 100644 ai-service/tests/test_kg_data_loader.py create mode 100644 ai-service/tests/test_kg_pipeline_source_safety.py create mode 100644 ai-service/tests/test_lexi_chat_routes.py create mode 100644 ai-service/tests/test_main_lifespan.py create mode 100644 ai-service/tests/test_notification_agent_routes.py create mode 100644 ai-service/tests/test_ollama_router.py create mode 100644 ai-service/tests/test_ranking_agent_routes.py create mode 100644 ai-service/tests/test_topic_chat_routes.py create mode 100644 ai-service/tests/test_translate_routes.py create mode 100644 ai-service/tests/test_tts_routes.py create mode 100644 backend-service/.env.test.example create mode 100644 backend-service/alembic/versions/add_cefr_content_agent.py create mode 100644 backend-service/alembic/versions/add_content_provenance_v2.py create mode 100644 backend-service/alembic/versions/add_notification_campaign_jobs.py create mode 100644 backend-service/alembic/versions/add_ranking_agent_jobs.py create mode 100644 backend-service/alembic/versions/add_referral_fields_to_users.py create mode 100644 backend-service/app/cli/__init__.py create mode 100644 backend-service/app/cli/content_agent.py create mode 100644 backend-service/app/models/content_agent.py create mode 100644 backend-service/app/models/notification_campaign.py create mode 100644 backend-service/app/models/ranking_agent.py create mode 100644 backend-service/app/routes/content_agent.py create mode 100644 backend-service/app/routes/notification_campaign.py create mode 100644 backend-service/app/routes/ranking_agent.py create mode 100644 backend-service/app/routes/referral.py create mode 100644 backend-service/app/schemas/content_agent.py create mode 100644 backend-service/app/schemas/notification_campaign.py create mode 100644 backend-service/app/schemas/ranking_agent.py create mode 100644 backend-service/app/services/content_agent_apply.py create mode 100644 backend-service/app/services/content_agent_client.py create mode 100644 backend-service/app/services/content_agent_jobs.py create mode 100644 backend-service/app/services/content_agent_sources.py create mode 100644 backend-service/app/services/content_agent_uploads.py create mode 100644 backend-service/app/services/content_agent_validation.py create mode 100644 backend-service/app/services/notification_campaign/__init__.py create mode 100644 backend-service/app/services/notification_campaign/apply.py create mode 100644 backend-service/app/services/notification_campaign/segmenter.py create mode 100644 backend-service/app/services/notification_campaign/sender.py create mode 100644 backend-service/app/services/notification_campaign_jobs.py create mode 100644 backend-service/app/services/ranking_agent/__init__.py create mode 100644 backend-service/app/services/ranking_agent/achievement_batch.py create mode 100644 backend-service/app/services/ranking_agent/apply.py create mode 100644 backend-service/app/services/ranking_agent/league_reset.py create mode 100644 backend-service/app/services/ranking_agent/xp_event.py create mode 100644 backend-service/app/services/ranking_agent_ai_client.py create mode 100644 backend-service/app/services/ranking_agent_jobs.py create mode 100644 backend-service/app/services/vocabulary_catalog.py create mode 100644 backend-service/app/tasks/content_agent.py create mode 100644 backend-service/app/tasks/notification_campaign.py create mode 100644 backend-service/app/tasks/ranking_agent.py create mode 100644 backend-service/app/tasks/streak_reminders.py create mode 100644 backend-service/app/tasks/word_of_day.py create mode 100644 backend-service/tests/integration/test_content_agent_licensed_etl_flow.py create mode 100644 backend-service/tests/test_content_agent_apply.py create mode 100644 backend-service/tests/test_content_agent_contract.py create mode 100644 backend-service/tests/test_content_agent_jobs.py create mode 100644 backend-service/tests/test_content_agent_routes.py create mode 100644 backend-service/tests/test_content_agent_sources.py create mode 100644 backend-service/tests/test_content_agent_tasks.py create mode 100644 backend-service/tests/test_content_agent_uploads.py create mode 100644 backend-service/tests/test_content_agent_validation.py create mode 100644 backend-service/tests/test_content_contract_parity.py create mode 100644 backend-service/tests/test_fsrs_algorithm_correctness.py create mode 100644 backend-service/tests/test_lesson_context_sanitization.py create mode 100644 backend-service/tests/test_notification_campaign_apply.py create mode 100644 backend-service/tests/test_notification_campaign_jobs.py create mode 100644 backend-service/tests/test_notification_campaign_schemas.py create mode 100644 backend-service/tests/test_notification_campaign_sender.py create mode 100644 backend-service/tests/test_ranking_agent_engines.py create mode 100644 backend-service/tests/test_ranking_agent_jobs.py create mode 100644 backend-service/tests/test_ranking_agent_routes.py create mode 100644 backend-service/tests/test_ranking_agent_xp_event.py create mode 100644 backend-service/tests/test_referral_routes.py create mode 100644 backend-service/tests/test_vocabulary_catalog.py create mode 100644 backend-service/tests/test_vocabulary_catalog_normalize.py create mode 100644 contracts/content-agent/course-artifact-v2.schema.json create mode 100644 contracts/content-agent/exercise-types-v1.json create mode 100644 contracts/content-agent/fixtures/licensed-etl-artifact-v2.json create mode 100644 contracts/content-agent/source-record-v2.schema.json create mode 100644 docs/badge-design-prompts.md create mode 100644 docs/qa/game-system-acceptance.md create mode 100644 docs/runbooks/licensed-content-etl.md create mode 100644 docs/superpowers/plans/2026-06-15-cefr-content-agent.md create mode 100644 docs/superpowers/plans/2026-06-15-licensed-content-etl-remediation.md create mode 100644 docs/superpowers/plans/2026-06-15-licensed-content-etl.md create mode 100644 docs/superpowers/specs/2026-06-14-cefr-content-agent-design.md create mode 100644 docs/topic-names.md create mode 100644 flutter-app/assets/animation/Confetti.json create mode 100644 flutter-app/assets/animation/HeartBeat.json create mode 100644 flutter-app/assets/animation/LevelUpOrbit.json create mode 100644 flutter-app/assets/animation/Live Love Learn.json create mode 100644 flutter-app/assets/animation/PulseLoader.json create mode 100644 flutter-app/assets/animation/Sandy Loading.json create mode 100644 flutter-app/assets/animation/SpinningDots.json create mode 100644 flutter-app/assets/animation/StarBurst.json create mode 100644 flutter-app/assets/animation/SuccessCheck.json create mode 100644 flutter-app/assets/animation/Welcome.json create mode 100644 flutter-app/assets/icon/002_star.png create mode 100644 flutter-app/assets/icon/008_gear.png create mode 100644 flutter-app/assets/icon/010_r02_c01.png create mode 100644 flutter-app/assets/icon/010_r02_c04.png create mode 100644 flutter-app/assets/icon/013_r01_c13_clock_gloss.png create mode 100644 flutter-app/assets/icon/014_r02_c01_heart_bold.png create mode 100644 flutter-app/assets/icon/016_red_cross.png create mode 100644 flutter-app/assets/icon/017_green_checkmark.png create mode 100644 flutter-app/assets/icon/018_r02_c05_purple_gem_bold.png create mode 100644 flutter-app/assets/icon/022_r04_c04.png create mode 100644 flutter-app/assets/icon/025_r02_c12_treasure_chest_bold.png create mode 100644 flutter-app/assets/icon/028_gift_box.png create mode 100644 flutter-app/assets/icon/037_unlocked_padlock.png create mode 100644 flutter-app/assets/icon/041_crown.png create mode 100644 flutter-app/assets/icon/047_back_arrow.png create mode 100644 flutter-app/assets/icon/054_triangle_left.png create mode 100644 flutter-app/assets/icon/055_triangle_right.png create mode 100644 flutter-app/assets/icon/056_curved_back_arrow.png create mode 100644 flutter-app/assets/icon/059_fast_forward.png create mode 100644 flutter-app/assets/icon/068_speaker_on.png create mode 100644 flutter-app/assets/icon/069_speaker_muted.png create mode 100644 flutter-app/assets/icon/127_yellow_light_bulb.png create mode 100644 flutter-app/assets/icon/129_speech_bubble.png create mode 100644 flutter-app/assets/icon/gem.png create mode 100644 flutter-app/assets/icon/icon_r05_c08.png create mode 100644 flutter-app/assets/icon/icon_r07_c16.png create mode 100644 flutter-app/assets/out-app/LexiLingo-text.png create mode 100644 flutter-app/lib/core/services/deep_link_service.dart create mode 100644 flutter-app/lib/core/services/purchases_service.dart create mode 100644 flutter-app/lib/core/services/rating_service.dart create mode 100644 flutter-app/lib/core/widgets/app_button.dart create mode 100644 flutter-app/lib/core/widgets/cefr_badge.dart create mode 100644 flutter-app/lib/core/widgets/premium_gate.dart create mode 100644 flutter-app/lib/core/widgets/stagger_list.dart create mode 100644 flutter-app/lib/features/achievements/presentation/widgets/achievement_unlock_overlay.dart create mode 100644 flutter-app/lib/features/admin/shared/widgets/admin_skeleton.dart create mode 100644 flutter-app/lib/features/admin/shared/widgets/kpi_count_card.dart create mode 100644 flutter-app/lib/features/admin/shared/widgets/staggered_entrance.dart create mode 100644 flutter-app/lib/features/games/data/services/game_pronunciation_service.dart create mode 100644 flutter-app/lib/features/gamification/presentation/screens/league_ceremony_screen.dart create mode 100644 flutter-app/lib/features/gamification/presentation/widgets/active_boosts_bar.dart create mode 100644 flutter-app/lib/features/gamification/presentation/widgets/boost_purchase_animation.dart create mode 100644 flutter-app/lib/features/notifications/domain/services/review_reminder_notification_sync_service.dart create mode 100644 flutter-app/lib/features/premium/presentation/screens/paywall_screen.dart create mode 100644 flutter-app/lib/features/progress/presentation/widgets/streak_milestone_overlay.dart create mode 100644 flutter-app/lib/features/progress/presentation/widgets/xp_line_chart.dart create mode 100644 flutter-app/lib/features/user/presentation/pages/legal_page.dart create mode 100644 flutter-app/lib/features/vocabulary/presentation/screens/word_of_day_screen.dart create mode 100644 flutter-app/lib/features/vocabulary/presentation/widgets/word_of_day_card.dart create mode 100644 flutter-app/test/core/widgets/cefr_badge_test.dart create mode 100644 flutter-app/test/features/games/data/game_pronunciation_service_test.dart create mode 100644 flutter-app/test/features/games/presentation/screens/game_accessibility_test.dart create mode 100644 flutter-app/test/features/games/presentation/screens/game_completion_test.dart create mode 100644 flutter-app/test/features/games/presentation/screens/game_load_state_test.dart create mode 100644 flutter-app/test/features/gamification/gamification_provider_active_boosts_test.dart create mode 100644 flutter-app/test/features/notifications/notification_local_datasource_test.dart create mode 100644 flutter-app/test/features/notifications/review_reminder_notification_sync_service_test.dart create mode 100644 flutter-app/test/features/progress/streak_provider_milestone_test.dart create mode 100644 flutter-app/test/features/vocabulary/data/datasources/vocabulary_word_of_day_test.dart create mode 100644 mcp-server/resources/common.py create mode 100644 mcp-server/tests/conftest.py create mode 100644 mcp-server/tests/test_resources.py create mode 100644 mcp-server/tests/test_security_configuration.py create mode 100644 monitoring/grafana/provisioning/datasources/prometheus.yml create mode 100644 monitoring/prometheus.yml create mode 100644 monitoring/rules/alerts.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5b183d7..7804baae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,11 @@ jobs: run: pip install -r requirements.txt - name: Install test dependencies - run: pip install pytest-cov + run: pip install pytest-cov ruff + + - name: Ruff baseline + continue-on-error: true + run: ruff check app - name: Wait for PostgreSQL run: | @@ -137,6 +141,124 @@ jobs: name: backend-coverage path: backend-service/coverage.xml + # ────────────────────────────────────────────── + # AI service — focused lint, import, and STT tests + # ────────────────────────────────────────────── + ai: + name: AI Lint, Import & STT Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ai-service + + env: + PYTHONPATH: . + APP_ENV: testing + DEBUG: "False" + DATABASE_URL: sqlite+aiosqlite:///./ci-ai.db + SECRET_KEY: ci-secret-key-not-for-production + GEMINI_API_KEY: "" + GROQ_API_KEY: "" + OPENAI_API_KEY: "" + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: ai-service/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -c constraints-ai.txt -r requirements.txt ruff + + - name: Ruff focused STT correctness checks + run: ruff check --select E9,F63,F7,F82 api/routes/stt.py api/services/stt tests/stt + + - name: Check app import + run: python -c "from api.main import app; print(app.title)" + + - name: Run focused STT tests + run: | + pytest \ + tests/stt \ + tests/test_dependency_constraints.py \ + tests/test_lexi_session_management.py \ + tests/trace_cag/test_edges_routing.py \ + tests/trace_cag/test_pipeline_integration.py \ + --tb=short -q + + # ────────────────────────────────────────────── + # MCP server — import and startup protocol smoke + # ────────────────────────────────────────────── + mcp: + name: MCP Pytest & Startup Smoke + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mcp-server + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: mcp-server/requirements.txt + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Check server import + run: python -c "from server import server; print(type(server).__name__)" + + - name: Run MCP resource, security, and startup tests + run: | + pytest \ + tests/test_resources.py \ + tests/test_security_configuration.py \ + tests/test_integration.py::test_mcp_server_startup \ + tests/test_integration.py::test_list_resources \ + --tb=short -q + + # ────────────────────────────────────────────── + # Admin — reproducible install, build check, Vitest + # ────────────────────────────────────────────── + admin: + name: Admin Build & Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: admin-service + + steps: + - uses: actions/checkout@v6 + + - name: Set up Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: admin-service/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build and type check baseline + run: npm run build:check + + - name: Run Vitest + run: npm test + # ────────────────────────────────────────────── # Flutter — analyze + test # ────────────────────────────────────────────── @@ -190,8 +312,8 @@ jobs: name: flutter-analyze-output path: flutter-app/analyze-output.txt - - name: Run unit tests - run: flutter test --no-pub -r compact test/core test/features + - name: Run full test suite + run: flutter test --no-pub -r compact - name: Check formatting (non-blocking) run: dart format --output=none --set-exit-if-changed lib/ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..f749dfc9 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,62 @@ +name: Security + +on: + push: + branches: [main, feature, "feature/**", develop, dev] + pull_request: + branches: [main, feature, "feature/**", develop, dev] + schedule: + - cron: "17 3 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +env: + GITLEAKS_VERSION: "8.30.1" + +jobs: + gitleaks: + name: Gitleaks + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout full repository history + uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install pinned Gitleaks + shell: bash + run: | + set -euo pipefail + archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + release_url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" + + curl --fail --location --silent --show-error \ + "${release_url}/${archive}" \ + --output "${RUNNER_TEMP}/${archive}" + curl --fail --location --silent --show-error \ + "${release_url}/gitleaks_${GITLEAKS_VERSION}_checksums.txt" \ + --output "${RUNNER_TEMP}/gitleaks-checksums.txt" + + ( + cd "${RUNNER_TEMP}" + sha256sum --check --ignore-missing gitleaks-checksums.txt + tar -xzf "${archive}" gitleaks + ) + mkdir -p "${RUNNER_TEMP}/bin" + install -m 0755 "${RUNNER_TEMP}/gitleaks" "${RUNNER_TEMP}/bin/gitleaks" + echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" + + - name: Scan current tree + run: gitleaks dir --config .gitleaks.toml --redact --verbose . + + - name: Scan full Git history + run: gitleaks git --config .gitleaks.toml --redact --verbose --log-opts="--all" . diff --git a/.gitignore b/.gitignore index 0b2543a1..f0fffa02 100644 --- a/.gitignore +++ b/.gitignore @@ -129,9 +129,13 @@ system-testing/ # deployment artifacts dist/ build/ +*.tsbuildinfo .deploy/ backups/ +# Runtime-generated knowledge graph sync metadata +**/data/*_synced_files.json + # gateway runtime security logs gateway/nginx/logs/*.log default.conf @@ -149,6 +153,7 @@ scripts .claude/worktrees/ # Local tool caches +.claude/ .claire/ .crawl4ai/ ai-service/.crawl4ai/ diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..f23b4621 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,76 @@ +title = "LexiLingo Gitleaks configuration" + +[extend] +useDefault = true + +[[rules]] +id = "google-gemini-api-key" +description = "Google Gemini API key" +regex = '''(?i)(?:GEMINI_API_KEY|GOOGLE_API_KEY)[^\n]{0,80}?(AIza[0-9A-Za-z_-]{35})''' +secretGroup = 1 +keywords = ["AIza", "GEMINI_API_KEY", "GOOGLE_API_KEY"] + +# Firebase client API keys identify the project but are not authorization +# secrets. Access remains controlled by Firebase Security Rules and API +# restrictions. +[[allowlists]] +description = "Public Firebase client configuration" +targetRules = ["gcp-api-key", "generic-api-key"] +paths = [ + '''(^|/)\.github/workflows/cd\.yml$''', + '''(^|/)flutter-app/lib/firebase_options\.dart$''', + '''(^|/)flutter-app/web/firebase-messaging-sw\.js$''', + '''(^|/)lexilingo_app/ios/Runner/GoogleService-Info\.plist$''', + '''(^|/)lexilingo_app/lib/firebase_options\.dart$''', +] + +# Fake API keys used as test fixtures — not real credentials. +[[allowlists]] +description = "Test fixture placeholder API keys" +targetRules = ["generic-api-key", "google-gemini-api-key", "gcp-api-key"] +paths = [ + '''(^|/)ai-service/tests/test_admin_routes\.py$''', +] + +# These examples intentionally use non-secret placeholders. +[[allowlists]] +description = "Documentation and local gateway placeholders" +condition = "AND" +regexTarget = "line" +regexes = [ + '''Bearer YOUR_ACCESS_TOKEN''', + '''X-Api-Key: replace-mobile-key''', + '''key: replace-admin-key''', +] +paths = [ + '''(^|/)backend-service/QUICKSTART\.md$''', + '''(^|/)docs/gateway/API_GATEWAY_FULL_POLICY_SETUP\.md$''', + '''(^|/)gateway/kong/kong\.yml$''', +] + +# Phase 1 starts enforcing new findings without making CI permanently fail on +# removed credentials already present in immutable history. Those credentials +# must remain revoked; rewriting shared history is intentionally out of scope. +[[allowlists]] +description = "Legacy findings predating Phase 1" +condition = "AND" +commits = [ + "c3cc2eee255a820a30543ce70a37175ac1954c87", + "ffdca45197a7d247fa017690f905c1cdd039cad4", + "71d0a4d8f834c372973117558cff71240776123f", + "4e1296a5ec7d2050f2cf91a46a2646baaed214d8", + "8f98d140b551ffde037a7e3fa6c363e02417f39d", + "89e1b0238381aadb30ced293dd65ca33f165c80c", + "98dc1d63e85c0d01d14f13fd676f2849aba9b28a", + "3d24c444fa29321445a2f06e9480c31ef00b5244", + "9f5590bc9fdb50b5074d472bbccb862751a1c2c9", +] +paths = [ + '''(^|/)mcp-server/start_production\.sh$''', + '''(^|/)mcp-server/tests/test_full\.py$''', + '''(^|/)gateway/kong/kong\.yml$''', + '''(^|/)backend-service/test_firebase_api\.py$''', + '''(^|/)scripts/clean-git-history\.sh$''', + '''(^|/)scripts/start-all\.sh$''', + '''(^|/)flutter-app/build/web/main\.dart\.js$''', +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1feb5fc0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# LexiLingo — Multi-Agent Coordination + +## Agent Roles + +| Agent | File | Trigger | +|-------|------|---------| +| Code Reviewer | `.claude/agents/code-reviewer.md` | After any non-trivial change | +| Test Writer | `.claude/agents/test-writer.md` | New feature or bug fix | +| Security Reviewer | `.claude/agents/security-reviewer.md` | Auth, API, DB schema changes | +| Kaiser | `.claude/agents/kaiser.md` | Technical debt audits, pre-sprint cleanup, when a service feels "heavy" | + +## Ownership Map + +| Area | Owner agent | Notes | +|------|-------------|-------| +| `flutter-app/` | main Claude | UI + state | +| `backend-service/api/` | main Claude | FastAPI routes, models | +| `backend-service/scripts/` | main Claude | Seed scripts (idempotent) | +| `ai-service/` | main Claude | AI pipeline, STT | +| Tests | test-writer | Never let main agent skip tests | +| Security surface | security-reviewer | Routes, auth middleware, env vars | +| Technical debt | kaiser | Debt register, architecture violations, dead code, complexity hotspots | + +## Coordination Protocol + +1. **Plan first** — use `sequential-thinking` MCP for tasks spanning >2 files or services. +2. **Graph before grep** — always query `code-review-graph` before opening files. +3. **Spawn test-writer** after every feature implementation. +4. **Spawn security-reviewer** when touching: auth routes, JWT handling, DB migrations, env/config files. +5. **Spawn code-reviewer** before any PR — use `/code-review` skill. +6. **Spawn kaiser** for quarterly debt audits, before major refactors, or when a module starts accumulating complexity. + +## Task Decomposition (Large Tasks) + +For tasks like "implement STT streaming": +``` +1. Main agent: architecture plan using sequential-thinking +2. Main agent: implement core changes +3. test-writer: write unit + integration tests +4. security-reviewer: review new endpoints +5. code-reviewer: final review + PR comment +``` + +## Shared Memory + +Agents write findings to the `memory` MCP knowledge graph: +- Entity type `Bug`: bugs found during review +- Entity type `Decision`: architectural decisions with rationale +- Entity type `TodoItem`: deferred work flagged during review + +Query before starting: `memory.search_nodes("LexiLingo")` to load prior context. + +## Do NOT + +- Spawn agents for tasks under 30 min of single-agent work. +- Have two agents edit the same file concurrently. +- Skip the test-writer for any new public API endpoint. +- Commit without running `flutter analyze` (Flutter) or `pytest` (backend). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..cb69ffe1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# LexiLingo — Claude Code Project Instructions + +## Project Overview + +Language learning app (Duolingo-style). Stack: +- `flutter-app/` — Flutter mobile, Provider state management, Clean Architecture +- `backend-service/` — FastAPI + SQLAlchemy async (PostgreSQL) +- `ai-service/` — Python AI/ML (sentence-transformers, Whisper STT, TRACE-CAG pipeline) +- `admin-service/` — React admin dashboard +- `gateway/` — API gateway (Kong or Traefik) +- `mcp-server/` — Custom MCP server for LexiLingo tools + +## Architecture Rules + +- Flutter: Clean Architecture — `data/` → `domain/` → `presentation/`. Never import presentation from domain. +- Backend: async-first — all DB calls use `await`, never sync SQLAlchemy. +- AI service: FastAPI routes → services → handlers. No business logic in routes. +- Do not cross service boundaries directly — go through the gateway. + +## Key Files + +| File | Purpose | +|------|---------| +| `backend-service/scripts/seed_courses.py` | Production seed (idempotent, one-shot) | +| `backend-service/scripts/seed_data.py` | Master seed entry point | +| `flutter-app/lib/features/learning/presentation/screens/learning_roadmap_screen.dart` | Duolingo-style zigzag roadmap | +| `ai-service/api/services/trace_cag/` | TRACE-CAG multi-hop reasoning pipeline | + +## DB Models — Non-obvious Constraints + +- `VocabularyItem.part_of_speech` → `PartOfSpeech` enum (lowercase: `noun`, `verb`, …) +- `VocabularyItem.difficulty_level` → `DifficultyLevel` enum (uppercase: `A1`, `A2`, …) +- `GrammarItem.content` → Text (not JSON); `examples` → JSON list +- `GameWord.cefr_level` → plain String (`A1`–`C2`) +- `TestExam.question_ids` → JSON list (populated at runtime) +- bcrypt: use `import bcrypt; bcrypt.hashpw(...)` directly — passlib has a 4.x bug + +## Python Environment + +```bash +# backend-service +cd backend-service && source venv/bin/activate + +# ai-service +cd ai-service && source venv/bin/activate # python3.12 +``` + +## Seed Run Order + +``` +seed_data.py → seed_courses.py → seed_analytics.py → seed_demo_data.py → seed_questions.py +``` + +Expected DB state after full seed: 109 users, 1614 daily activities, 98 questions. + +## Testing + +- Backend: `pytest` from `backend-service/` +- Flutter: `flutter test` from `flutter-app/` +- AI service integration tests: `pytest tests/trace_cag/` + +## MCP Tools (use BEFORE grep/read) + +This project has a code-review-graph knowledge graph. Prefer: +- `semantic_search_nodes` over grep for finding functions +- `get_impact_radius` over manual import tracing +- `detect_changes` + `get_review_context` for code review +- `query_graph` for caller/callee/test relationships + +## Code Style + +- No comments unless the WHY is non-obvious +- No docstrings beyond a single short line +- Flutter: `Theme.of(context)` tokens only — never hardcode colors or sizes +- Python: type hints on all function signatures +- Async Python: `async def` + `await` everywhere in service/handler layer diff --git a/admin-service/package-lock.json b/admin-service/package-lock.json index 16a5fb50..2e8464cf 100644 --- a/admin-service/package-lock.json +++ b/admin-service/package-lock.json @@ -10,32 +10,33 @@ "dependencies": { "@gsap/react": "^2.1.2", "@hookform/resolvers": "^5.2.2", - "@tanstack/react-query": "^5.100.9", + "@tanstack/react-query": "^5.101.0", "@vercel/analytics": "^2.0.1", - "antd": "^6.3.7", - "axios": "^1.16.0", - "dayjs": "^1.11.20", + "antd": "^6.4.4", + "axios": "^1.18.0", + "dayjs": "^1.11.21", "dotenv": "^17.4.2", "gsap": "^3.15.0", - "lucide-react": "^1.14.0", - "react": "^19.2.5", - "react-dom": "^19.2.6", - "react-hook-form": "^7.75.0", - "react-router-dom": "^7.15.0", + "lucide-react": "^1.18.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.5.0", + "vercel": "^54.14.0", "zod": "^4.4.3", - "zustand": "^5.0.13" + "zustand": "^5.0.14" }, "devDependencies": { - "@types/node": "^25.6.2", - "@types/react": "^19.2.14", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.3", - "vite": "^8.0.11" + "vite": "^8.0.16", + "vitest": "^4.1.8" } }, "node_modules/@ant-design/colors": { @@ -91,14 +92,14 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz", - "integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz", + "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==", "license": "MIT", "dependencies": { - "@ant-design/colors": "^8.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@rc-component/util": "^1.3.0", + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.4.2", + "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "engines": { @@ -132,9 +133,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -202,32 +203,35 @@ } }, "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==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "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==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "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.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -724,6 +728,13 @@ "node": ">=18.0.0" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", @@ -745,14 +756,248 @@ "node": ">=18" } }, + "node_modules/@napi-rs/keyring": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.2.0.tgz", + "integrity": "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.2.0", + "@napi-rs/keyring-darwin-x64": "1.2.0", + "@napi-rs/keyring-freebsd-x64": "1.2.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", + "@napi-rs/keyring-linux-arm64-musl": "1.2.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", + "@napi-rs/keyring-linux-x64-gnu": "1.2.0", + "@napi-rs/keyring-linux-x64-musl": "1.2.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", + "@napi-rs/keyring-win32-x64-msvc": "1.2.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.2.0.tgz", + "integrity": "sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.2.0.tgz", + "integrity": "sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.2.0.tgz", + "integrity": "sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.2.0.tgz", + "integrity": "sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.2.0.tgz", + "integrity": "sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.2.0.tgz", + "integrity": "sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.2.0.tgz", + "integrity": "sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.2.0.tgz", + "integrity": "sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.2.0.tgz", + "integrity": "sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.2.0.tgz", + "integrity": "sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.2.0.tgz", + "integrity": "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "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==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -1152,9 +1397,9 @@ } }, "node_modules/@rc-component/async-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", - "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz", + "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.4" @@ -1164,14 +1409,14 @@ } }, "node_modules/@rc-component/cascader": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.14.0.tgz", - "integrity": "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.16.1.tgz", + "integrity": "sha512-wxLopwM+EBed0zNNGdnGE4coYoqcO+XD42fHgn+pDvO+XzhNFbdgSlSNXdKocIYqccvqgWvoxDPNb0OVRdi59A==", "license": "MIT", "dependencies": { - "@rc-component/select": "~1.6.0", - "@rc-component/tree": "~1.2.0", - "@rc-component/util": "^1.4.0", + "@rc-component/select": "~1.7.1", + "@rc-component/tree": "~1.3.2", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1225,22 +1470,22 @@ } }, "node_modules/@rc-component/context": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", - "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.2.tgz", + "integrity": "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==", "license": "MIT", "dependencies": { - "@rc-component/util": "^1.3.0" + "@rc-component/util": "^1.11.0" }, "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, "node_modules/@rc-component/dialog": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.4.tgz", - "integrity": "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.9.0.tgz", + "integrity": "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.1.3", @@ -1285,13 +1530,13 @@ } }, "node_modules/@rc-component/form": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.1.tgz", - "integrity": "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.5.tgz", + "integrity": "sha512-d24EYtvUOBhxEtSd/EqIu9DaMuqrWF2IRIvAFCTM6NQ/GJIYNr8DvEpUSUlv2uPxEJ0ZPwYQ+wwlGIAaiHvdrw==", "license": "MIT", "dependencies": { - "@rc-component/async-validator": "^5.1.0", - "@rc-component/util": "^1.6.2", + "@rc-component/async-validator": "^6.0.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "engines": { @@ -1319,12 +1564,13 @@ } }, "node_modules/@rc-component/input": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", - "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.1.tgz", + "integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==", "license": "MIT", "dependencies": { - "@rc-component/util": "^1.4.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1348,14 +1594,13 @@ } }, "node_modules/@rc-component/mentions": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", - "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.9.0.tgz", + "integrity": "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==", "license": "MIT", "dependencies": { - "@rc-component/input": "~1.1.0", - "@rc-component/menu": "~1.2.0", - "@rc-component/textarea": "~1.1.0", + "@rc-component/input": "~1.3.0", + "@rc-component/menu": "~1.3.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -1366,15 +1611,15 @@ } }, "node_modules/@rc-component/menu": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", - "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.1.tgz", + "integrity": "sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1395,12 +1640,12 @@ } }, "node_modules/@rc-component/motion": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", - "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.3.tgz", + "integrity": "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==", "license": "MIT", "dependencies": { - "@rc-component/util": "^1.2.0", + "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1425,21 +1670,21 @@ } }, "node_modules/@rc-component/notification": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", - "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.1.4", - "@rc-component/util": "^1.2.1", + "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "engines": { "node": ">=8.x" }, "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, "node_modules/@rc-component/overflow": { @@ -1459,12 +1704,12 @@ } }, "node_modules/@rc-component/pagination": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", - "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.3.0.tgz", + "integrity": "sha512-12ahTY+HPITg1L2bjWKXUqBJe/oOnpA2QsChdCjthqLVf/e19StiCsv8OLKpWoHbc+8PFEkNjRqRqrLoRBHjFw==", "license": "MIT", "dependencies": { - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1473,9 +1718,9 @@ } }, "node_modules/@rc-component/picker": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.1.tgz", - "integrity": "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.10.0.tgz", + "integrity": "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==", "license": "MIT", "dependencies": { "@rc-component/overflow": "^1.0.0", @@ -1542,9 +1787,9 @@ } }, "node_modules/@rc-component/qrcode": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", - "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-2.0.0.tgz", + "integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.7" @@ -1604,15 +1849,15 @@ } }, "node_modules/@rc-component/select": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz", - "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.7.1.tgz", + "integrity": "sha512-GZ1cMJk2xQh0VHyOQjjG8drYL4iu24NcbkXioUcReQOCUr+ub/3fmRonZe6cRPEZhWMbJdeHsqnEltogDaZ5Tg==", "license": "MIT", "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.3.0", - "@rc-component/virtual-list": "^1.0.1", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "engines": { @@ -1672,14 +1917,14 @@ } }, "node_modules/@rc-component/table": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz", - "integrity": "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.2.tgz", + "integrity": "sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==", "license": "MIT", "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.1.0", + "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, @@ -1692,16 +1937,16 @@ } }, "node_modules/@rc-component/tabs": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", - "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.1.tgz", + "integrity": "sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA==", "license": "MIT", "dependencies": { "@rc-component/dropdown": "~1.0.0", - "@rc-component/menu": "~1.2.0", + "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "engines": { @@ -1712,22 +1957,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/@rc-component/textarea": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", - "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", - "license": "MIT", - "dependencies": { - "@rc-component/input": "~1.1.0", - "@rc-component/resize-observer": "^1.0.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/@rc-component/tooltip": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", @@ -1744,9 +1973,9 @@ } }, "node_modules/@rc-component/tour": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz", - "integrity": "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", "license": "MIT", "dependencies": { "@rc-component/portal": "^2.2.0", @@ -1763,14 +1992,14 @@ } }, "node_modules/@rc-component/tree": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.2.4.tgz", - "integrity": "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.2.tgz", + "integrity": "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.0.0", - "@rc-component/util": "^1.8.1", - "@rc-component/virtual-list": "^1.0.1", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "engines": { @@ -1782,13 +2011,13 @@ } }, "node_modules/@rc-component/tree-select": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.8.0.tgz", - "integrity": "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.10.0.tgz", + "integrity": "sha512-E1U4pn2LAbXEhLJdzIzid7WYbIuFbkTIctuFoeC6weppf8UbPR3+YYB6/ay0c0ksand4gXMRQpa1Z60Auo7VJA==", "license": "MIT", "dependencies": { - "@rc-component/select": "~1.6.0", - "@rc-component/tree": "~1.2.0", + "@rc-component/select": "~1.7.0", + "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, @@ -1798,9 +2027,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", - "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.1.tgz", + "integrity": "sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.1.4", @@ -1818,12 +2047,12 @@ } }, "node_modules/@rc-component/upload": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", - "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.1.tgz", + "integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==", "license": "MIT", "dependencies": { - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1832,9 +2061,9 @@ } }, "node_modules/@rc-component/util": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz", - "integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", + "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", "license": "MIT", "dependencies": { "is-mobile": "^5.0.0", @@ -1852,9 +2081,9 @@ "license": "MIT" }, "node_modules/@rc-component/virtual-list": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", - "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.2.0.tgz", + "integrity": "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", @@ -1866,8 +2095,8 @@ "node": ">=8.x" }, "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, "node_modules/@reduxjs/toolkit": { @@ -2035,13 +2264,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "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==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2052,13 +2284,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "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==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2178,9 +2413,9 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -2218,9 +2453,9 @@ "license": "MIT" }, "node_modules/@tanstack/query-core": { - "version": "5.100.9", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", - "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", "license": "MIT", "funding": { "type": "github", @@ -2228,12 +2463,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.100.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz", - "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==", + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.100.9" + "@tanstack/query-core": "5.101.0" }, "funding": { "type": "github", @@ -2290,15 +2525,26 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2362,10 +2608,17 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2375,19 +2628,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2481,13 +2734,13 @@ } }, "node_modules/@vercel/backends": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.8.0.tgz", - "integrity": "sha512-oDLW8FV/POMhQ5M8UWCRzXQBwy/BtmK8dO7NBFAeNNT1CEN/ZIVopctYF1ICkmHd504rGagDMJ+/YZvbTlYbSA==", + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.8.14.tgz", + "integrity": "sha512-jgepdZh7E4ameCUSt/b28ImFd9Bz1VX71lKwD/ThtZ7nGIwi5SJ9hwyURvH60c4rNW6nKxMD/iSOlcgQOfrK3A==", "license": "Apache-2.0", "dependencies": { - "@vercel/build-utils": "13.26.2", - "@vercel/nft": "1.5.0", + "@vercel/build-utils": "13.30.0", + "@vercel/nft": "1.10.0", "execa": "3.2.0", "fs-extra": "11.1.0", "get-port": "5.1.1", @@ -2526,9 +2779,9 @@ } }, "node_modules/@vercel/build-utils": { - "version": "13.26.2", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.26.2.tgz", - "integrity": "sha512-dEgbr6NnbDkvXpKpYoV0V4kN3VWc82A41a5V9bYGqOMYyw76rEhf59VKgSdu9D0SOXgowSln2gL5O7ATJ3xFog==", + "version": "13.30.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.30.0.tgz", + "integrity": "sha512-fLIa8cpELsSoWbcxshaqegwlTCfKnbgsDmA6uIb+oA797xvSmYkPu5a781aRYCRdghgyOZF7NCSVgtZjHjunnQ==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1", @@ -2537,21 +2790,51 @@ } }, "node_modules/@vercel/cervel": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.8.tgz", - "integrity": "sha512-ux75xbBtZ2Z4AJ2GIn4+VLTlohqo7i7R0kLimUVzSzvABwVmKDuqqbkwSYiMQ6PXzX5W6RFol+6PjkmDW6UfCw==", + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.22.tgz", + "integrity": "sha512-YbL2AosH3FaBLJZu4GYdVMkFv+5cbWwwSrRBTGN52odynQUudTUQfbc7rCzPyNQLdk2QC4w2IDYRD+nqgojhVg==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.8.0" + "@vercel/backends": "0.8.14" }, "bin": { "cervel": "bin/cervel.mjs" } }, + "node_modules/@vercel/cli-auth": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vercel/cli-auth/-/cli-auth-0.3.0.tgz", + "integrity": "sha512-9nsdxUpV/L+9CBVGeRw/Qby2azhi2lk01jp0aTH+Hxx2U61+mAmbi5qChHnrbEmQdx2Ih7dp4LxO3nj1Q7f/5Q==", + "dependencies": { + "@napi-rs/keyring": "1.2.0", + "@vercel/cli-config": "0.2.0", + "async-listen": "3.0.0", + "open": "8.4.0", + "zod": "4.1.11" + } + }, + "node_modules/@vercel/cli-auth/node_modules/async-listen": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", + "integrity": "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vercel/cli-auth/node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@vercel/cli-config": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.1.2.tgz", - "integrity": "sha512-XQOcuCM+8tKjh3sfgGRKRuNh78u2D8uGpDJIFcCtFi2tUqbGvqmJo790XX7+Bwakk08y0FCrs2JlEjvvwRhpAg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.2.0.tgz", + "integrity": "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ==", "license": "Apache-2.0", "dependencies": { "xdg-app-paths": "5", @@ -2577,31 +2860,31 @@ } }, "node_modules/@vercel/elysia": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.81.tgz", - "integrity": "sha512-Z1lKN5awF98e4wCXiEjr0jS2BdIVW3KmPiQJPr3v/qWGUc0I5V3+gE3FD6p4CCzPYtaWcvEy9UxZ8FDIB0u0zw==", + "version": "0.1.93", + "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.93.tgz", + "integrity": "sha512-36yzZJVU9o7/CcH1TbqeGTRwBePDaCEzv1CYfS183HdLXzsD+pOjOkTqZOBJu7h9aHpJFCCau55Ed0xOQC2jhA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0" + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/error-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.1.0.tgz", - "integrity": "sha512-DiJcXBOB9N6QM4d7hYPM9Ck/AUjzBl58XNQPxS74o7CuvIanjzrGgygP/70VsyEASeIJMazk1LrhwcNTR/eZGQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.2.0.tgz", + "integrity": "sha512-WFWiRxfPzoYWYifaj4thSKvAaZZwUOqD4k5GINRIgZgCiS2E3iAJbWbIsIZmkQdTecWFHcWGA6q48CjisgpOBA==", "license": "Apache-2.0" }, "node_modules/@vercel/express": { - "version": "0.1.91", - "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.91.tgz", - "integrity": "sha512-+lj3y8M+1AZu52A00+VBhb2Y1vJYnyRqFEryU8ttWjFO+x88hEhZyhfvvO6S7W0H7RCdsS839NWoaqZ817Ivdg==", + "version": "0.1.105", + "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.105.tgz", + "integrity": "sha512-F+QRQxWWKdeztW4MKPkzXl/Ajc8j1qOffCp1tt8+YPaNiqZtxpn7FjIYRXHbfR1aepfaEkDRDD7o2qxmmaUU5w==", "license": "Apache-2.0", "dependencies": { - "@vercel/cervel": "0.1.8", - "@vercel/nft": "1.5.0", - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0", + "@vercel/cervel": "0.1.22", + "@vercel/nft": "1.10.0", + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", @@ -2618,13 +2901,13 @@ } }, "node_modules/@vercel/fastify": { - "version": "0.1.84", - "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.84.tgz", - "integrity": "sha512-4Iq6MEp9UyzQFKys2NQkzCXibq/XKMKNzW7ArMjIEKzIBN6URAJ0WTMGnhvEz0Go1aNcXCjSQ1kAxp09tJ1MXw==", + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.96.tgz", + "integrity": "sha512-4Uq6GVmi0ksQxIHp4FDUhgoz47uacxZm42XT04SK5gzX5v5UmZXWZPprZjkwwk/2gmP/rF2qKqJ0nu6OsI+yDw==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0" + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/fun": { @@ -2737,43 +3020,43 @@ } }, "node_modules/@vercel/gatsby-plugin-vercel-builder": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.2.8.tgz", - "integrity": "sha512-XSyOMFwB/aSBX4RPU5baeaXgvltidojCaaoW4IAizPjWBBO5vfSvGHqsOmF39d7rNcLX0WMa4K/H5MoGOhkzUA==", + "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==", "license": "Apache-2.0", "dependencies": { "@sinclair/typebox": "0.25.24", - "@vercel/build-utils": "13.26.2", + "@vercel/build-utils": "13.30.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "node_modules/@vercel/go": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.8.0.tgz", - "integrity": "sha512-ftQqQMn3sGdL8mdIqfcS3YZg6dazM/h4s0jkY37oVV1rPdh7Aq/GL0oMjv1L+PoIk5uJEAyBan7C8Yisp4LH+g==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.9.0.tgz", + "integrity": "sha512-vGbEwckvtr9UXKDz3qEHUE7VHJGG/9VQLuScgHytksfZyX8dzvNPgEuBKBbhPaMwua8pUCZKQqNxFM3Dul+bLA==", "license": "Apache-2.0" }, "node_modules/@vercel/h3": { - "version": "0.1.90", - "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.90.tgz", - "integrity": "sha512-jbf4CxOcAtPXxCJN0rUgUzrenwbOXQ99Oo59q/iZOwasQcV8rusPr+UD0WCBTZumj183oEgyYtFidnAXKQ23Fw==", + "version": "0.1.102", + "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.102.tgz", + "integrity": "sha512-UnHFLBemE2UfvQ+w2z2zxGcvyiShQ8eMdk3ag2sV71wbyatnXwmio8HOy//B2ZIEQNEid5gW2HjWOXLtrR+E2Q==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0" + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/hono": { - "version": "0.2.84", - "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.84.tgz", - "integrity": "sha512-qLE30Gj3b2nQdvZuNr3s9ZbA009grDRctjO/pm6iEbFBZs0ri4Qy6ooPjFp/cbn4bvUpnWqDs+1ds3oNNKUq/g==", + "version": "0.2.96", + "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.96.tgz", + "integrity": "sha512-h4G0GSo5omhXS1iqimIcekcW7THEAnUGd6eioDNnSqwS3L0CGEvvTPUekJDyJcybsqnKslH1XTZA40f4b9MPHg==", "license": "Apache-2.0", "dependencies": { - "@vercel/nft": "1.5.0", - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0", + "@vercel/nft": "1.10.0", + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", @@ -2790,48 +3073,48 @@ } }, "node_modules/@vercel/hydrogen": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.3.7.tgz", - "integrity": "sha512-nh8hZ76Ipf9FRmMmQGd4SjkE0zxdjt+TUpZcuCIUG7yaHEh9STQV655I8rxKCB3hEWaKB3HALGgxZ0htIjQtZQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.4.0.tgz", + "integrity": "sha512-gf3ELmAjcia7WNGNHAB/rFWFU0l5fJ7mTMgGC5hvcCjSIE4kp8WWuGYiTQgQ8W6GF/1FJAxa2J3BhY/yxjY8tA==", "license": "Apache-2.0", "dependencies": { - "@vercel/static-config": "3.3.0", + "@vercel/static-config": "3.4.0", "ts-morph": "12.0.0" } }, "node_modules/@vercel/koa": { - "version": "0.1.64", - "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.64.tgz", - "integrity": "sha512-F+T3Cr5kUrk2v1WcPQLNIcUk1eP3S8eBXGIN5j+dYx6I8eoFurKPzu4Z7gGIVQ1CCddHjl8MNR1SWxlEn4r5iw==", + "version": "0.1.76", + "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.76.tgz", + "integrity": "sha512-6MuBn5RM2nHKfHKeBqbs3aaFJlBFDhOZgpC1ubLQG2B/EvnAfljB7FPIKCl1SwANAHisX7vQACinMedCbFdi+Q==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0" + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/nestjs": { - "version": "0.2.85", - "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.85.tgz", - "integrity": "sha512-fUEVgxzXJ9lWrEK8EUpcfqyf2GwLIcRB882QtpntDECucmzPz906QKdcKJFEQA+3qcve3fYHgzQWbKOQzGJ4jw==", + "version": "0.2.97", + "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.97.tgz", + "integrity": "sha512-KM9uBmC+a10EPJgtm0B/vfGsTaUEzxKkihTyaQ4PhSUQHJOABTv6TBpGKtwIM5P1vMLKUg9R0A2SUvkKdaALvA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.5", - "@vercel/static-config": "3.3.0" + "@vercel/node": "5.8.17", + "@vercel/static-config": "3.4.0" } }, "node_modules/@vercel/next": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.17.4.tgz", - "integrity": "sha512-XsvV4pwphvrSgRTlpkSOiraST9ZrzEXRpEKABaR3cLnVf/2OvY4ZHb7uGDWX1ogNKadEZKSVgk5nKBueornANw==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.19.0.tgz", + "integrity": "sha512-9B7juRNydQ9MU7xT8OE254ymhVrNdcOKlXrZ4anem8YEa34KvXjLczQhZEPdvTz9JaCXpYiEEGmd/D3t2oJ+Uw==", "license": "Apache-2.0", "dependencies": { - "@vercel/nft": "1.5.0" + "@vercel/nft": "1.10.0" } }, "node_modules/@vercel/nft": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz", - "integrity": "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.10.0.tgz", + "integrity": "sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==", "license": "MIT", "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", @@ -2855,19 +3138,19 @@ } }, "node_modules/@vercel/node": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.5.tgz", - "integrity": "sha512-NoqElZnk/K5bVsZE2Z1Pi5m0QRbS5m6cYN1YBA5mHIF/JpnztA8miTH4bj7cGMpNGkjfayjVfQ/VpHH5HAJ35Q==", + "version": "5.8.17", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.17.tgz", + "integrity": "sha512-n2DVzblqS43LTs4BV1iLfx8tjjrTegcSsyDgRzn70h6tQePYWjl+h0bU3X1stpITZtaYJSZYwVevuX1BJNZHHg==", "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.26.2", - "@vercel/error-utils": "2.1.0", - "@vercel/nft": "1.5.0", - "@vercel/static-config": "3.3.0", + "@vercel/build-utils": "13.30.0", + "@vercel/error-utils": "2.2.0", + "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", @@ -2975,15 +3258,15 @@ } }, "node_modules/@vercel/prepare-flags-definitions": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@vercel/prepare-flags-definitions/-/prepare-flags-definitions-0.2.1.tgz", - "integrity": "sha512-ouXTsqn7I9xZ1KKezgvn/w3tZeQHL/tc52j9GHiOYi6kT8xgdbT8s2x8C9BQr44iceX0hfhtZwk9q7NuI2Tqbw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vercel/prepare-flags-definitions/-/prepare-flags-definitions-0.3.0.tgz", + "integrity": "sha512-/0nuDFwYje0nqZnVKSd2VfJy2wOPQwbkas1qO1JQgtb0sLl+EeSCW4O9hrvq55pN50PNlAZ/APSeWHIAT9ZGHg==", "license": "MIT" }, "node_modules/@vercel/python": { - "version": "6.43.2", - "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.43.2.tgz", - "integrity": "sha512-pFE20KP7FOCwCjHvB7iECu/Q9BFw70pHaU/kb+XyHvS7mCCeZIdFdZaMHolcBKVxz/Ju6QhtZe5e84vcbBw5OA==", + "version": "6.45.1", + "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.45.1.tgz", + "integrity": "sha512-2ay12lt3Cu6xS6qiaKNNwALRKxI1AE3rtHDfFOP+BU4qJ2U5k7JmPxlDuU+CXKY+chQClQM2orgThJWeMoeQkw==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1" @@ -3028,13 +3311,13 @@ } }, "node_modules/@vercel/redwood": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@vercel/redwood/-/redwood-2.4.13.tgz", - "integrity": "sha512-pXWzVctZea/J7WMQstjsYUDiMc6oJF72p8J5YZnLSCJWg7m+/dLzYGfaUSEo6Q0JpiO/NOcDmG3WENpn7kHwzg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@vercel/redwood/-/redwood-2.5.0.tgz", + "integrity": "sha512-LO24CbieV57rGAqrq5n5z/B+fMYeUqbE/Ge3qMeKRuS3Q9awgcBgB6WYLCe8crHM7XJd5AZjbuyoS/6k3r0X0w==", "license": "Apache-2.0", "dependencies": { - "@vercel/nft": "1.5.0", - "@vercel/static-config": "3.3.0", + "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.0", "semver": "6.3.1", "ts-morph": "12.0.0" } @@ -3049,14 +3332,14 @@ } }, "node_modules/@vercel/remix-builder": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@vercel/remix-builder/-/remix-builder-5.8.2.tgz", - "integrity": "sha512-iiL8ppHkt+tIyLUEKQQQQmqVsoyZSPGc0zqIQ9lRUQLSMzxsgh57LPBjDA8AJrdLHXyxQQwNdHEtuMFZaKOG2w==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@vercel/remix-builder/-/remix-builder-5.9.1.tgz", + "integrity": "sha512-s0SpfV640nmQ1BsKCPMYugGrH/V+319onTo2ZILSmsy6NTBlmeN0zJU9nPcP8dBEotGqtp68NfOlOUUdrVBzkw==", "license": "Apache-2.0", "dependencies": { - "@vercel/error-utils": "2.1.0", - "@vercel/nft": "1.5.0", - "@vercel/static-config": "3.3.0", + "@vercel/error-utils": "2.2.0", + "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.0", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0" @@ -3135,26 +3418,43 @@ "license": "ISC" }, "node_modules/@vercel/sandbox": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-1.9.0.tgz", - "integrity": "sha512-zgr1ad0tkT1xZn/8Vxo60wOUOLqMAVGo4WqJQ8/UDcUtWynNJsBjI2tiMdWZrAo9EKH1MIqEzJNkcclF0UT1EQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-2.1.1.tgz", + "integrity": "sha512-gKhW+YlvU15Qxya7jQKByB+sqA1dWat5zx/rvxT52E3Ryg9MAIXgqD5wd1d+CoJDbdHL26gIOcksTZY5sFpplA==", "license": "Apache-2.0", "dependencies": { "@vercel/oidc": "3.2.0", + "@workflow/serde": "4.1.0-beta.2", "async-retry": "1.3.3", + "jose": "6.2.3", "jsonlines": "0.1.1", "ms": "2.1.3", "picocolors": "^1.1.1", "tar-stream": "3.1.7", - "undici": "^7.16.0", + "undici": "^7.27.1", "xdg-app-paths": "5.1.0", "zod": "3.24.4" } }, + "node_modules/@vercel/sandbox/node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@vercel/sandbox/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/@vercel/sandbox/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -3170,21 +3470,21 @@ } }, "node_modules/@vercel/static-build": { - "version": "2.9.31", - "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.9.31.tgz", - "integrity": "sha512-alC4tglwsksalNt/Pd0C5oCupK660nUhh0k9en39yQBa+7vRZx4//IDAkAxFB5AmDzsPLIHZVmLwM57bQaL9Rw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.10.3.tgz", + "integrity": "sha512-+IipWidX6mL1Zck8Khw4wSXHPSgDfkapvcuWA9wSCmhnk2GL2Iuu+6vab+g8UPahv+QDUYm8Tphhadg6dICZnA==", "license": "Apache-2.0", "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", - "@vercel/gatsby-plugin-vercel-builder": "2.2.8", - "@vercel/static-config": "3.3.0", + "@vercel/gatsby-plugin-vercel-builder": "2.2.19", + "@vercel/static-config": "3.4.0", "ts-morph": "12.0.0" } }, "node_modules/@vercel/static-config": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.3.0.tgz", - "integrity": "sha512-GpS3tPwUeDJCkrKbMNtS2XLRFgfxTlN7YNUL+Bo23+fGolrDw6Oq79R3yvxTYgqRaJMGSEqC7iMw6mj6I5loxg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.4.0.tgz", + "integrity": "sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==", "license": "Apache-2.0", "dependencies": { "ajv": "8.6.3", @@ -3218,6 +3518,135 @@ } } }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@workflow/serde": { + "version": "4.1.0-beta.2", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0-beta.2.tgz", + "integrity": "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==", + "license": "Apache-2.0" + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -3228,9 +3657,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3274,55 +3703,54 @@ } }, "node_modules/antd": { - "version": "6.3.7", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.7.tgz", - "integrity": "sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.4.tgz", + "integrity": "sha512-lgPz4KhfhiYddV/qPYo0ieqWimCVgV2OQF72mbeGNixE753JWNnmEc7UNGy08wBS/zZ7hxrmX0pc5aX7EUaIIg==", "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.1.1", + "@ant-design/icons": "^6.2.5", "@ant-design/react-slick": "~2.0.0", - "@babel/runtime": "^7.28.4", - "@rc-component/cascader": "~1.14.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.16.1", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", - "@rc-component/dialog": "~1.8.4", + "@rc-component/dialog": "~1.9.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", - "@rc-component/form": "~1.8.1", + "@rc-component/form": "~1.8.3", "@rc-component/image": "~1.9.0", - "@rc-component/input": "~1.1.2", + "@rc-component/input": "~1.3.1", "@rc-component/input-number": "~1.6.2", - "@rc-component/mentions": "~1.6.0", - "@rc-component/menu": "~1.2.0", - "@rc-component/motion": "^1.3.2", + "@rc-component/mentions": "~1.9.0", + "@rc-component/menu": "~1.3.1", + "@rc-component/motion": "^1.3.3", "@rc-component/mutate-observer": "^2.0.1", - "@rc-component/notification": "~1.2.0", - "@rc-component/pagination": "~1.2.0", - "@rc-component/picker": "~1.9.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.3.0", + "@rc-component/picker": "~1.10.0", "@rc-component/progress": "~1.0.2", - "@rc-component/qrcode": "~1.1.1", + "@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.6.15", + "@rc-component/select": "~1.7.1", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", - "@rc-component/table": "~1.9.1", - "@rc-component/tabs": "~1.7.0", - "@rc-component/textarea": "~1.1.2", + "@rc-component/table": "~1.10.2", + "@rc-component/tabs": "~1.9.1", "@rc-component/tooltip": "~1.4.0", - "@rc-component/tour": "~2.3.0", - "@rc-component/tree": "~1.2.4", - "@rc-component/tree-select": "~1.8.0", - "@rc-component/trigger": "^3.9.0", - "@rc-component/upload": "~1.1.0", - "@rc-component/util": "^1.10.1", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/tree-select": "~1.10.0", + "@rc-component/trigger": "^3.9.1", + "@rc-component/upload": "~1.1.1", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", @@ -3355,6 +3783,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -3395,16 +3833,42 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axios/node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -3414,6 +3878,20 @@ "node": ">=10" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3421,9 +3899,9 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -3453,9 +3931,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3505,6 +3983,16 @@ "node": ">= 0.4" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.0.tgz", @@ -3601,6 +4089,13 @@ "node": ">=8" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -3771,9 +4266,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debug": { @@ -3799,6 +4294,15 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -3898,12 +4402,6 @@ "node": ">= 14" } }, - "node_modules/edge-runtime/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "license": "ISC" - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -4130,6 +4628,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4430,9 +4938,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4639,6 +5147,21 @@ "node": ">=4" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4693,6 +5216,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5025,18 +5560,18 @@ } }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/lucide-react": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", - "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -5051,6 +5586,16 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5303,6 +5848,20 @@ "node": ">=8" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5327,6 +5886,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-paths": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", @@ -5468,6 +6044,13 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -5475,9 +6058,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "license": "ISC" }, "node_modules/picomatch": { @@ -5493,9 +6076,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -5513,7 +6096,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5521,6 +6104,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/pretty-ms": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", @@ -5631,30 +6221,30 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/react-hook-form": { - "version": "7.75.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz", - "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==", + "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==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5698,9 +6288,9 @@ } }, "node_modules/react-router": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz", - "integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==", + "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==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -5720,12 +6310,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz", - "integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==", + "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==", "license": "MIT", "dependencies": { - "react-router": "7.15.0" + "react-router": "7.17.0" }, "engines": { "node": ">=20.0.0" @@ -5921,12 +6511,13 @@ "license": "MIT" }, "node_modules/sandbox": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/sandbox/-/sandbox-2.5.6.tgz", - "integrity": "sha512-tnFr7nyiuEhsAGb+xy60SDbij0790X+FgDljh3J/2HaRM6yQgNJkQKHbDH8ld7mR+PozXGgEfJ2Dc/5OyFnwsg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/sandbox/-/sandbox-3.1.2.tgz", + "integrity": "sha512-g93rma0Z9Aa6EoTktzYnGZmQvMzm7j73BekJIMwmkdWR5uryZ+6hBCADtd3JKGAB27k+ubnG7HHsZqtscAMzIQ==", "license": "Apache-2.0", "dependencies": { - "@vercel/sandbox": "1.9.0", + "@vercel/sandbox": "2.1.1", + "async-retry": "1.3.3", "debug": "^4.4.1", "zod": "^4.1.1" }, @@ -5951,9 +6542,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5995,6 +6586,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", @@ -6092,6 +6690,13 @@ "node": ">=20.16.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.3.0.tgz", @@ -6107,6 +6712,13 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-to-array": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", @@ -6146,9 +6758,9 @@ } }, "node_modules/streamx": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", - "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -6205,20 +6817,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar-stream/node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -6237,20 +6835,6 @@ "b4a": "^1.6.4" } }, - "node_modules/text-decoder/node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/three": { "version": "0.184.0", "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", @@ -6299,6 +6883,13 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -6306,9 +6897,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6322,6 +6913,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6886,9 +7487,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, @@ -6929,42 +7530,43 @@ } }, "node_modules/vercel": { - "version": "54.5.0", - "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.5.0.tgz", - "integrity": "sha512-9NANKCdLqfpUQu7fGWelmPFJvnNK73PBH/4oGL4OMAfjLjrS8vDShFXm5lKQJ+01sgziLVjWek8rldOrpe7Dqg==", + "version": "54.14.2", + "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.14.2.tgz", + "integrity": "sha512-XMUBq4mEYniGGIHTAIY7Egop120x6d2NLZ9wezk3032VfKRMsB3q2UYFiZduWETG/TwFSddlNJ6UI8jBskleog==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.8.0", + "@vercel/backends": "0.8.14", "@vercel/blob": "2.4.0", - "@vercel/build-utils": "13.26.2", - "@vercel/cli-config": "0.1.2", + "@vercel/build-utils": "13.30.0", + "@vercel/cli-auth": "0.3.0", + "@vercel/cli-config": "0.2.0", "@vercel/detect-agent": "1.2.3", - "@vercel/elysia": "0.1.81", - "@vercel/express": "0.1.91", - "@vercel/fastify": "0.1.84", + "@vercel/elysia": "0.1.93", + "@vercel/express": "0.1.105", + "@vercel/fastify": "0.1.96", "@vercel/fun": "1.3.0", - "@vercel/go": "3.8.0", - "@vercel/h3": "0.1.90", - "@vercel/hono": "0.2.84", - "@vercel/hydrogen": "1.3.7", - "@vercel/koa": "0.1.64", - "@vercel/nestjs": "0.2.85", - "@vercel/next": "4.17.4", - "@vercel/node": "5.8.5", - "@vercel/prepare-flags-definitions": "0.2.1", - "@vercel/python": "6.43.2", - "@vercel/redwood": "2.4.13", - "@vercel/remix-builder": "5.8.2", + "@vercel/go": "3.9.0", + "@vercel/h3": "0.1.102", + "@vercel/hono": "0.2.96", + "@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/prepare-flags-definitions": "0.3.0", + "@vercel/python": "6.45.1", + "@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.9.31", + "@vercel/static-build": "2.10.3", "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": "2.5.6", + "sandbox": "3.1.2", "smol-toml": "1.5.2", "zod": "4.1.11" }, @@ -7008,17 +7610,17 @@ } }, "node_modules/vite": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", - "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0-rc.18", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -7085,10 +7687,44 @@ } } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/vite/node_modules/@oxc-project/types": { - "version": "0.128.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", - "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -7096,9 +7732,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "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==", "cpu": [ "arm64" ], @@ -7113,9 +7749,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "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==", "cpu": [ "arm64" ], @@ -7130,9 +7766,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -7147,9 +7783,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "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==", "cpu": [ "x64" ], @@ -7164,9 +7800,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", - "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "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==", "cpu": [ "arm" ], @@ -7181,13 +7817,16 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "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==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7198,13 +7837,16 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "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==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -7215,13 +7857,16 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "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==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7232,13 +7877,16 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "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==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -7249,9 +7897,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "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==", "cpu": [ "arm64" ], @@ -7266,9 +7914,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", - "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "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==", "cpu": [ "wasm32" ], @@ -7285,9 +7933,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "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==", "cpu": [ "arm64" ], @@ -7302,9 +7950,9 @@ } }, "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "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==", "cpu": [ "x64" ], @@ -7319,21 +7967,21 @@ } }, "node_modules/vite/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", - "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "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.0-rc.18", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", - "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.128.0", - "@rolldown/pluginutils": "1.0.0-rc.18" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -7342,21 +7990,128 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-x64": "1.0.0-rc.18", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + "@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" + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/web-vitals": { @@ -7396,6 +8151,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7471,9 +8243,9 @@ } }, "node_modules/zustand": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", - "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/admin-service/package.json b/admin-service/package.json index 8bb982e4..40037546 100644 --- a/admin-service/package.json +++ b/admin-service/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "vite build", "build:check": "tsc -b && vite build", + "test": "vitest run", "preview": "vite preview" }, "dependencies": { @@ -38,6 +39,7 @@ "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.8" } } diff --git a/admin-service/pnpm-lock.yaml b/admin-service/pnpm-lock.yaml index fc8be5c5..125408af 100644 --- a/admin-service/pnpm-lock.yaml +++ b/admin-service/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: vite: specifier: ^8.0.16 version: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) + vitest: + specifier: ^4.1.8 + version: 4.1.9(@edge-runtime/vm@3.2.0)(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0)) packages: @@ -353,6 +356,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -1112,6 +1118,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1139,6 +1148,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -1322,6 +1334,35 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} @@ -1365,6 +1406,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -1444,6 +1489,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chokidar@4.0.0: resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} engines: {node: '>= 14.16.0'} @@ -1484,6 +1533,9 @@ packages: resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} engines: {node: '>=8'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@2.0.1: resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} @@ -1623,6 +1675,9 @@ packages: es-module-lexer@1.5.0: resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -1656,6 +1711,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1681,6 +1739,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2009,6 +2071,9 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2130,6 +2195,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + once@1.3.3: resolution: {integrity: sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==} @@ -2192,6 +2261,9 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -2392,6 +2464,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2428,6 +2503,9 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stat-mode@0.3.0: resolution: {integrity: sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng==} @@ -2435,6 +2513,9 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} @@ -2487,13 +2568,24 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2621,6 +2713,47 @@ packages: yaml: optional: true + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-vitals@0.2.4: resolution: {integrity: sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==} @@ -2635,6 +2768,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2884,6 +3022,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -3529,6 +3669,11 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -3553,6 +3698,8 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} @@ -3913,6 +4060,47 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@workflow/serde@4.1.0-beta.2': {} abbrev@3.0.1: {} @@ -4000,6 +4188,8 @@ snapshots: argparse@2.0.1: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -4064,6 +4254,8 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + chai@6.2.2: {} + chokidar@4.0.0: dependencies: readdirp: 4.1.2 @@ -4090,6 +4282,8 @@ snapshots: convert-hrtime@3.0.0: {} + convert-source-map@2.0.0: {} + cookie-es@2.0.1: {} cookie@1.1.1: {} @@ -4204,6 +4398,8 @@ snapshots: es-module-lexer@1.5.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -4260,6 +4456,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} etag@1.8.1: {} @@ -4299,6 +4499,8 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -4585,6 +4787,10 @@ snapshots: luxon@3.7.2: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} merge-stream@2.0.0: {} @@ -4666,6 +4872,8 @@ snapshots: dependencies: path-key: 3.1.1 + obug@2.1.3: {} + once@1.3.3: dependencies: wrappy: 1.0.2 @@ -4751,6 +4959,8 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + pend@1.2.0: {} picocolors@1.0.0: {} @@ -4968,6 +5178,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.0.2: {} @@ -4998,10 +5210,14 @@ snapshots: dependencies: cookie-es: 2.0.1 + stackback@0.0.2: {} + stat-mode@0.3.0: {} statuses@1.5.0: {} + std-env@4.1.0: {} + stream-to-array@2.3.0: dependencies: any-promise: 1.3.0 @@ -5070,13 +5286,19 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5208,6 +5430,34 @@ snapshots: fsevents: 2.3.3 tsx: 4.21.0 + vitest@4.1.9(@edge-runtime/vm@3.2.0)(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.27.0)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@edge-runtime/vm': 3.2.0 + '@types/node': 25.9.3 + transitivePeerDependencies: + - msw + web-vitals@0.2.4: {} webidl-conversions@3.0.1: {} @@ -5221,6 +5471,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrappy@1.0.2: {} xdg-app-paths@5.1.0: diff --git a/admin-service/src/App.tsx b/admin-service/src/App.tsx index 037237d3..e08256e0 100644 --- a/admin-service/src/App.tsx +++ b/admin-service/src/App.tsx @@ -10,7 +10,7 @@ import { LayoutDashboard, Users, BookOpen, Layers, FileText, PenTool, BarChart3, Languages, Trophy, ShoppingBag, Megaphone, ScrollText, Activity, Settings, - Shield, Database, Bot, ArrowRight, ArrowLeft, MessageSquare + Shield, Database, Bot, ArrowRight, ArrowLeft, MessageSquare, Swords, Bell } from "lucide-react"; // Lazy-loaded components @@ -37,6 +37,8 @@ const ContentAnalyticsPage = lazy(() => import("./pages/ContentAnalyticsPage").t const SystemSettingsPage = lazy(() => import("./pages/SystemSettingsPage").then(m => ({ default: m.SystemSettingsPage }))); const AdminManagementPage = lazy(() => import("./pages/AdminManagementPage").then(m => ({ default: m.AdminManagementPage }))); const AiChatSettingsPage = lazy(() => import("./pages/AiChatSettingsPage").then(m => ({ default: m.AiChatSettingsPage }))); +const RankingAgentPage = lazy(() => import("./pages/RankingAgentPage").then(m => ({ default: m.RankingAgentPage }))); +const NotificationCampaignPage = lazy(() => import("./pages/NotificationCampaignPage").then(m => ({ default: m.NotificationCampaignPage }))); const NoAccessPage = lazy(() => import("./pages/NoAccessPage").then(m => ({ default: m.NoAccessPage }))); const NotFoundPage = lazy(() => import("./pages/NotFoundPage").then(m => ({ default: m.NotFoundPage }))); @@ -62,6 +64,8 @@ const AppRoutes = () => { { to: "/admin/vocabulary", label: t.nav.vocabulary, icon: }, { to: "/admin/achievements", label: t.nav.achievements, icon: }, { to: "/admin/shop", label: t.nav.shop, icon: }, + { to: "/admin/ranking-agent", label: t.nav.rankingAgent, icon: }, + { to: "/admin/notification-campaign", label: t.nav.notificationCampaign, icon: }, { to: "/admin/ads", label: t.nav.bannerAds, icon: }, { to: "/admin/logs", label: t.nav.logs, icon: }, { to: "/admin/monitoring", label: t.nav.monitoring, icon: }, @@ -104,6 +108,8 @@ const AppRoutes = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/admin-service/src/components/content-agent/ContentAgentDrawer.test.tsx b/admin-service/src/components/content-agent/ContentAgentDrawer.test.tsx new file mode 100644 index 00000000..e675d5da --- /dev/null +++ b/admin-service/src/components/content-agent/ContentAgentDrawer.test.tsx @@ -0,0 +1,153 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ContentAgentJob } from "../../lib/contentAgentApi"; +import { + applyContentAgentPreview, + canApplyContentAgentJob, + isContentAgentJobActive, + summarizeContentAgentPreview, +} from "./ContentAgentDrawer"; + +const job = (overrides: Partial = {}): ContentAgentJob => ({ + id: "job-1", + status: "preview_ready", + config: {}, + progress: { percentage: 100 }, + source_manifest: [], + warnings: [], + blocking_errors: [], + created_entity_ids: {}, + created_at: "2026-06-15T00:00:00Z", + updated_at: "2026-06-15T00:01:00Z", + ...overrides, +}); + +describe("ContentAgentDrawer state", () => { + it("polls only non-terminal pipeline stages", () => { + expect(isContentAgentJobActive("resolving_sources")).toBe(true); + expect(isContentAgentJobActive("loading_snapshots")).toBe(true); + expect(isContentAgentJobActive("normalizing_upload")).toBe(true); + expect(isContentAgentJobActive("generating")).toBe(true); + expect(isContentAgentJobActive("applying")).toBe(true); + expect(isContentAgentJobActive("preview_ready")).toBe(false); + expect(isContentAgentJobActive("completed")).toBe(false); + expect(isContentAgentJobActive("failed")).toBe(false); + }); + + it("disables apply when validation has blocking errors", () => { + expect(canApplyContentAgentJob(job())).toBe(true); + expect( + canApplyContentAgentJob( + job({ blocking_errors: ["Lesson 2 has an invalid exercise"] }), + ), + ).toBe(false); + expect(canApplyContentAgentJob(job({ status: "generating" }))).toBe(false); + }); + + it("summarizes preview vocabulary and speaking/listening exercises", () => { + expect( + summarizeContentAgentPreview({ + schema_version: 2, + prompt_version: "cefr-course-v2", + generation_key: "key", + source_manifest: [], + courses: [ + { + title: "A1 Foundations", + description: "", + language: "en", + level: "A1", + tags: [], + units: [ + { + title: "Daily life", + order_index: 1, + lessons: [ + { + title: "Morning", + order_index: 1, + vocabulary: [{ word: "wake", part_of_speech: "verb" }], + exercises: [ + { + id: "e1", + type: "speaking", + ui_type: "speaking_repeat", + question: "Repeat.", + correct_answer: "I wake up.", + }, + { + id: "e2", + type: "listening", + ui_type: "dictation", + question: "Type what you hear.", + correct_answer: "I wake up.", + }, + ], + }, + ], + }, + ], + }, + ], + quality: { + blocking_errors: [], + warnings: [], + metrics: { duplicate_reuse_count: 3 }, + }, + }), + ).toEqual({ + courses: 1, + units: 1, + lessons: 1, + vocabulary: 1, + speaking: 1, + listening: 1, + duplicateReuse: 3, + }); + }); + + it("reports a successful apply and lets the page refresh courses", async () => { + const apply = vi.fn().mockResolvedValue( + job({ + status: "completed", + created_entity_ids: { course_ids: ["course-1"] }, + }), + ); + const onApplied = vi.fn(); + + const result = await applyContentAgentPreview("job-1", apply, onApplied); + + expect(apply).toHaveBeenCalledWith("job-1"); + expect(onApplied).toHaveBeenCalledWith(result); + expect(result.created_entity_ids.course_ids).toEqual(["course-1"]); + }); + + it("blocking errors from job and preview both prevent apply", () => { + expect( + canApplyContentAgentJob( + job({ blocking_errors: ["Missing required field: definition"] }), + ), + ).toBe(false); + + expect( + canApplyContentAgentJob( + job({ blocking_errors: ["Invalid POS enum: VERB", "Missing license"] }), + ), + ).toBe(false); + }); + + it("canApply is true only when status is preview_ready and no blocking errors", () => { + expect(canApplyContentAgentJob(job({ status: "preview_ready", blocking_errors: [] }))).toBe(true); + expect(canApplyContentAgentJob(job({ status: "completed", blocking_errors: [] }))).toBe(false); + expect(canApplyContentAgentJob(job({ status: "failed", blocking_errors: [] }))).toBe(false); + expect(canApplyContentAgentJob(job({ status: "preview_ready", blocking_errors: ["err"] }))).toBe(false); + }); + + it("validation report shows error count through blocking_errors array", () => { + const jobWithErrors = job({ + blocking_errors: ["Error 1", "Error 2", "Error 3"], + }); + expect(jobWithErrors.blocking_errors).toHaveLength(3); + expect(canApplyContentAgentJob(jobWithErrors)).toBe(false); + }); +}); diff --git a/admin-service/src/components/content-agent/ContentAgentDrawer.tsx b/admin-service/src/components/content-agent/ContentAgentDrawer.tsx new file mode 100644 index 00000000..4746e8d0 --- /dev/null +++ b/admin-service/src/components/content-agent/ContentAgentDrawer.tsx @@ -0,0 +1,743 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + AlertTriangle, + Ban, + CheckCircle2, + ChevronRight, + Database, + FileCheck2, + RefreshCw, + RotateCcw, + ShieldCheck, + Sparkles, + X, +} from "lucide-react"; + +import { + CONTENT_AGENT_ACTIVE_STATUSES, + applyContentAgentJob, + cancelContentAgentJob, + getContentAgentJob, + getContentAgentPreview, + retryContentAgentJob, + type ContentAgentJob, + type ContentAgentJobStatus, + type ContentAgentPreview, +} from "../../lib/contentAgentApi"; +import { useI18n } from "../../lib/i18n"; +import { StatusPill } from "../StatusPill"; + +const POLL_INTERVAL_MS = 2_500; +const activeStatuses = new Set(CONTENT_AGENT_ACTIVE_STATUSES); + +export const isContentAgentJobActive = (status: ContentAgentJobStatus) => + activeStatuses.has(status); + +export const canApplyContentAgentJob = (job: ContentAgentJob) => + job.status === "preview_ready" && job.blocking_errors.length === 0; + +export type ContentAgentPreviewSummary = { + courses: number; + units: number; + lessons: number; + vocabulary: number; + speaking: number; + listening: number; + duplicateReuse: number; +}; + +export const summarizeContentAgentPreview = ( + preview: ContentAgentPreview, +): ContentAgentPreviewSummary => { + const summary: ContentAgentPreviewSummary = { + courses: preview.courses.length, + units: 0, + lessons: 0, + vocabulary: 0, + speaking: 0, + listening: 0, + duplicateReuse: 0, + }; + + preview.courses.forEach((course) => { + summary.units += course.units.length; + course.units.forEach((unit) => { + summary.lessons += unit.lessons.length; + unit.lessons.forEach((lesson) => { + summary.vocabulary += lesson.vocabulary.length; + lesson.exercises.forEach((exercise) => { + if ( + exercise.ui_type === "speaking_repeat" || + exercise.ui_type === "pronunciation_practice" + ) { + summary.speaking += 1; + } + if ( + exercise.ui_type === "dictation" || + exercise.ui_type === "listen_and_choose" + ) { + summary.listening += 1; + } + }); + }); + }); + }); + + const duplicateReuse = + preview.quality.metrics.duplicate_reuse_count ?? + preview.quality.metrics.deduplicated_records ?? + preview.quality.metrics.duplicate_reuse; + summary.duplicateReuse = + typeof duplicateReuse === "number" ? duplicateReuse : 0; + + return summary; +}; + +export const applyContentAgentPreview = async ( + jobId: string, + apply: (id: string) => Promise, + onApplied: (job: ContentAgentJob) => void, +) => { + const result = await apply(jobId); + onApplied(result); + return result; +}; + +type ContentAgentDrawerProps = { + jobId: string; + initialJob?: ContentAgentJob | null; + onClose: () => void; + onApplied: (job: ContentAgentJob) => void; +}; + +export function ContentAgentDrawer({ + jobId, + initialJob = null, + onClose, + onApplied, +}: ContentAgentDrawerProps) { + const { t } = useI18n(); + const [job, setJob] = useState( + initialJob?.id === jobId ? initialJob : null, + ); + const [preview, setPreview] = useState(null); + const [loading, setLoading] = useState(!initialJob); + const [busyAction, setBusyAction] = useState< + "apply" | "retry" | "cancel" | null + >(null); + const [error, setError] = useState(null); + + const refreshJob = useCallback(async () => { + const nextJob = await getContentAgentJob(jobId); + setJob(nextJob); + return nextJob; + }, [jobId]); + + useEffect(() => { + let disposed = false; + setPreview(null); + setError(null); + + if (initialJob?.id === jobId) { + setJob(initialJob); + setLoading(false); + return () => { + disposed = true; + }; + } + + setLoading(true); + void refreshJob() + .catch((requestError) => { + if (!disposed) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.loadJobFailed, + ); + } + }) + .finally(() => { + if (!disposed) setLoading(false); + }); + + return () => { + disposed = true; + }; + }, [initialJob, jobId, refreshJob, t.contentAgent.loadJobFailed]); + + useEffect(() => { + if (!job || !isContentAgentJobActive(job.status)) return; + + let disposed = false; + const interval = window.setInterval(() => { + void refreshJob().catch((requestError) => { + if (!disposed) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.loadJobFailed, + ); + } + }); + }, POLL_INTERVAL_MS); + + return () => { + disposed = true; + window.clearInterval(interval); + }; + }, [job, refreshJob, t.contentAgent.loadJobFailed]); + + useEffect(() => { + if ( + !job || + (job.status !== "preview_ready" && job.status !== "completed") || + preview + ) { + return; + } + + let disposed = false; + void getContentAgentPreview(job.id) + .then((nextPreview) => { + if (!disposed) setPreview(nextPreview); + }) + .catch((requestError) => { + if (!disposed) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.loadPreviewFailed, + ); + } + }); + + return () => { + disposed = true; + }; + }, [job, preview, t.contentAgent.loadPreviewFailed]); + + const summary = useMemo( + () => (preview ? summarizeContentAgentPreview(preview) : null), + [preview], + ); + + const handleApply = async () => { + if (!job || !canApplyContentAgentJob(job)) return; + setBusyAction("apply"); + setError(null); + try { + const result = await applyContentAgentPreview( + job.id, + applyContentAgentJob, + onApplied, + ); + setJob(result); + } catch (requestError) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.applyFailed, + ); + } finally { + setBusyAction(null); + } + }; + + const handleRetry = async () => { + if (!job) return; + setBusyAction("retry"); + setError(null); + try { + const result = await retryContentAgentJob(job.id); + setJob(result); + setPreview(null); + } catch (requestError) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.retryFailed, + ); + } finally { + setBusyAction(null); + } + }; + + const handleCancel = async () => { + if (!job) return; + setBusyAction("cancel"); + setError(null); + try { + setJob(await cancelContentAgentJob(job.id)); + } catch (requestError) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.cancelFailed, + ); + } finally { + setBusyAction(null); + } + }; + + const percentage = Math.max( + 0, + Math.min( + 100, + Number(job?.progress.percentage ?? job?.progress.percent ?? 0), + ), + ); + const warnings = [ + ...(job?.warnings ?? []), + ...(preview?.quality.warnings ?? []), + ]; + const blockingErrors = [ + ...(job?.blocking_errors ?? []), + ...(preview?.quality.blocking_errors ?? []), + ]; + const courseIds = + job?.created_entity_ids.course_ids ?? + job?.created_entity_ids.courses ?? + []; + + return ( +
+ + +
+ + +
+ {loading && !job ? ( +
+ + {t.contentAgent.loadingJob} +
+ ) : job ? ( + <> +
+
+
+ {t.contentAgent.currentStage} + {formatStage(job.status)} +
+ +
+
+ +
+
+ {job.progress.message || t.contentAgent.processing} + {percentage}% +
+
+ +
+ + + +
+ + {job.error_message && ( + } + items={[job.error_message]} + title={t.contentAgent.jobError} + tone="danger" + /> + )} + {blockingErrors.length > 0 && ( + } + items={blockingErrors} + title={t.contentAgent.blockingErrors} + tone="danger" + /> + )} + {warnings.length > 0 && ( + } + items={warnings} + title={t.contentAgent.warnings} + tone="warning" + /> + )} + + {job.source_manifest.length > 0 && ( +
+
+ +

{t.contentAgent.provenance}

+
+
+ {job.source_manifest.map((source, index) => ( +
+
+ {source.source_name} + + {typeof source.policy_version === "string" && source.policy_version + ? `${t.contentAgent.snapshotVersion} ${source.policy_version} · ` + : ""} + {Array.isArray(source.content_usage) + ? source.content_usage.join(", ") + : source.content_usage || + source.license_mode || + t.common.unknown} + +
+ {typeof source.record_count === "number" && ( + {source.record_count} {t.contentAgent.snapshotRecords} + )} +
+ ))} +
+
+ )} + + {summary && preview && ( + <> +
+
+ +

{t.contentAgent.previewSummary}

+
+
+ + + + + + +
+

+ {summary.duplicateReuse} {t.contentAgent.duplicatesReused} +

+
+ +
+
+ +

{t.contentAgent.courseTree}

+
+
+ {preview.courses.map((course, courseIndex) => ( +
+ + + {course.title} + + {course.level} · {course.units.length}{" "} + {t.contentAgent.unitsCount.toLowerCase()} + + + +
+ {course.units.map((unit) => ( +
+ {unit.title} +
    + {unit.lessons.map((lesson) => ( +
  • + {lesson.title} + + {lesson.vocabulary.length}{" "} + {t.contentAgent.words} ·{" "} + {lesson.exercises.length}{" "} + {t.contentAgent.exercises} + +
  • + ))} +
+
+ ))} +
+
+ ))} +
+
+ + + + )} + + {job.status === "completed" && ( +
+
+ )} + + {error &&
{error}
} + + ) : ( +
+ {error || t.contentAgent.loadJobFailed} +
+ )} +
+ + {job && ( +
+ {(job.status === "failed" || job.status === "cancelled") && ( + + )} + {isContentAgentJobActive(job.status) && + job.status !== "applying" && ( + + )} + {job.status === "preview_ready" && ( + <> + {blockingErrors.length > 0 && ( + + {t.contentAgent.validationBlocking} + + )} + + + )} +
+ )} + + + ); +} + +function formatStage(status: string) { + return status + .split("_") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +function statusTone(status: ContentAgentJobStatus) { + if (status === "completed" || status === "preview_ready") return "success"; + if (status === "failed" || status === "cancelled") return "danger"; + if (status === "queued") return "neutral"; + return "info"; +} + +function counter( + job: ContentAgentJob, + name: "processed" | "rejected" | "deduplicated", +) { + const longName = `${name}_records`; + const counters = job.progress.counters ?? {}; + const value = + job.progress[longName] ?? + job.progress[name] ?? + counters[longName] ?? + counters[name] ?? + (name === "processed" ? counters.input_records : undefined); + return typeof value === "number" ? value : 0; +} + +function Counter({ label, value }: { label: string; value: number }) { + return ( +
+ {value.toLocaleString()} + {label} +
+ ); +} + +function PreviewMetric({ label, value }: { label: string; value: number }) { + return ( +
+ {label} + {value.toLocaleString()} +
+ ); +} + +function MessageList({ + icon, + items, + title, + tone, +}: { + icon: React.ReactNode; + items: string[]; + title: string; + tone: "danger" | "warning"; +}) { + return ( +
+
+ {icon} + {title} +
+
    + {items.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ); +} + +function SampleExercises({ preview }: { preview: ContentAgentPreview }) { + const { t } = useI18n(); + const exercises = preview.courses + .flatMap((course) => course.units) + .flatMap((unit) => unit.lessons) + .flatMap((lesson) => lesson.exercises) + .slice(0, 3); + + if (exercises.length === 0) return null; + + return ( +
+
+ +

{t.contentAgent.sampleExercises}

+
+
+ {exercises.map((exercise) => ( +
+ {formatStage(exercise.ui_type)} + {exercise.question} +

{exercise.correct_answer}

+
+ ))} +
+
+ ); +} diff --git a/admin-service/src/components/content-agent/ContentAgentModal.test.tsx b/admin-service/src/components/content-agent/ContentAgentModal.test.tsx new file mode 100644 index 00000000..4c40b8c9 --- /dev/null +++ b/admin-service/src/components/content-agent/ContentAgentModal.test.tsx @@ -0,0 +1,204 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import type { SourceSnapshot } from "../../lib/contentAgentApi"; +import { + createDefaultContentAgentForm, + validateContentAgentForm, + validateContentAgentUpload, +} from "./ContentAgentModal"; + +vi.mock("../../lib/contentAgentApi", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getSourceCatalog: vi.fn().mockResolvedValue([]), + }; +}); + +const sourceSnapshot = ( + overrides: Partial = {}, +): SourceSnapshot => ({ + source_id: "oewn", + source_name: "Open English WordNet", + source_version: "2025", + snapshot_id: `oewn:2025:${"a".repeat(64)}`, + official_url: "https://en-word.net/", + license_id: "CC-BY-4.0", + license_url: "https://creativecommons.org/licenses/by/4.0/", + attribution_text: "Open English WordNet contributors", + retrieved_at: "2026-06-01T00:00:00Z", + raw_checksum: "a".repeat(64), + normalized_sha256: "b".repeat(64), + normalized_bytes: 2048, + record_checksum_root: "c".repeat(64), + adapter_version: 1, + record_count: 150000, + status: "active", + enabled: true, + ...overrides, +}); + +describe("ContentAgentModal configuration", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults to every CEFR level, no source, and ten-item lessons", () => { + const form = createDefaultContentAgentForm(); + + expect(form.levels).toEqual(["A1", "A2", "B1", "B2", "C1", "C2"]); + expect(form.sources).toEqual([]); + expect(form.vocabularyPerLesson).toBe(10); + expect(form.exerciseCount).toBe(10); + expect(form.previewOnly).toBe(true); + expect(form.uploadAttestation).toBe(false); + }); + + it("requires vocabulary counts to stay inside the approved 8-12 range", () => { + expect( + validateContentAgentForm({ + ...createDefaultContentAgentForm(), + sources: ["oewn"], + vocabularyPerLesson: 7, + acknowledgedDraft: true, + }), + ).toContain("8"); + expect( + validateContentAgentForm({ + ...createDefaultContentAgentForm(), + sources: ["oewn"], + vocabularyPerLesson: 13, + acknowledgedDraft: true, + }), + ).toContain("12"); + }); + + it("matches the backend limit of at most ten units per course", () => { + expect( + validateContentAgentForm({ + ...createDefaultContentAgentForm(), + sources: ["oewn"], + unitsPerCourse: 11, + acknowledgedDraft: true, + }), + ).toContain("10"); + }); + + it("requires CEFR/source selection and the draft acknowledgement", () => { + expect( + validateContentAgentForm({ + ...createDefaultContentAgentForm(), + levels: [], + sources: [], + }), + ).toBeTruthy(); + expect( + validateContentAgentForm({ + ...createDefaultContentAgentForm(), + sources: ["oewn"], + }), + ).toContain("draft"); + }); + + it("accepts UTF-8 CSV/JSON files up to five megabytes", () => { + expect( + validateContentAgentUpload( + new File(["[]"], "records.json", { type: "application/json" }), + ), + ).toBeNull(); + expect( + validateContentAgentUpload( + new File(["word"], "records.txt", { type: "text/plain" }), + ), + ).toContain("CSV or JSON"); + expect( + validateContentAgentUpload( + new File([new Uint8Array(5 * 1024 * 1024 + 1)], "records.csv", { + type: "text/csv", + }), + ), + ).toContain("5 MB"); + }); + + it("requires upload attestation when a file is provided", () => { + const form = { ...createDefaultContentAgentForm(), sources: ["oewn"], acknowledgedDraft: true, uploadAttestation: false }; + const result = validateContentAgentForm(form, true); + expect(result).toBeTruthy(); + expect(result).toContain("confirm"); + }); + + it("passes validation when upload attestation is checked", () => { + const form = { + ...createDefaultContentAgentForm(), + sources: ["oewn"], + acknowledgedDraft: true, + uploadAttestation: true, + }; + expect(validateContentAgentForm(form, true)).toBeNull(); + }); +}); + +describe("ContentAgentModal source catalog logic", () => { + it("getSourceCatalog mock returns empty list by default", async () => { + const { getSourceCatalog } = await import("../../lib/contentAgentApi"); + const result = await getSourceCatalog(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(0); + }); + + it("getSourceCatalog resolves approved snapshots", async () => { + const { getSourceCatalog } = await import("../../lib/contentAgentApi"); + vi.mocked(getSourceCatalog).mockResolvedValueOnce([ + sourceSnapshot(), + ]); + + const result = await getSourceCatalog(); + expect(result).toHaveLength(1); + expect(result[0].source_id).toBe("oewn"); + expect(result[0].status).toBe("active"); + expect(result[0].enabled).toBe(true); + }); + + it("only approved+enabled snapshots are selectable", () => { + const snapshots = [ + sourceSnapshot(), + sourceSnapshot({ source_id: "tatoeba", enabled: false }), + sourceSnapshot({ source_id: "cmudict", enabled: false }), + ]; + + const selectable = snapshots.filter((s) => s.status === "active" && s.enabled); + expect(selectable).toHaveLength(1); + expect(selectable[0].source_id).toBe("oewn"); + }); + + it("core lexical sources are preselected when available and approved", () => { + const coreSourceIds = ["oewn", "cmudict", "cefr_j", "wikidata"]; + const approvedSnapshots = [ + sourceSnapshot(), + sourceSnapshot({ source_id: "tatoeba" }), + sourceSnapshot({ source_id: "cefr_j", enabled: false }), + ]; + + const preselected = approvedSnapshots + .filter((s) => s.status === "active" && s.enabled && coreSourceIds.includes(s.source_id)) + .map((s) => s.source_id); + + expect(preselected).toEqual(["oewn"]); + expect(preselected).not.toContain("tatoeba"); + expect(preselected).not.toContain("cefr_j"); + }); + + it("inactive snapshot is disabled (not approved+enabled)", () => { + const snapshot = sourceSnapshot({ + source_id: "cmudict", + enabled: false, + source_name: "CMUdict", + source_version: "1.0", + license_id: "BSD", + record_count: 0, + }); + + const selectable = snapshot.status === "active" && snapshot.enabled; + expect(selectable).toBe(false); + }); +}); diff --git a/admin-service/src/components/content-agent/ContentAgentModal.tsx b/admin-service/src/components/content-agent/ContentAgentModal.tsx new file mode 100644 index 00000000..87108cd6 --- /dev/null +++ b/admin-service/src/components/content-agent/ContentAgentModal.tsx @@ -0,0 +1,676 @@ +import React, { useEffect, useState } from "react"; +import { ShieldCheck, Sparkles, Upload, X } from "lucide-react"; + +import { + CEFR_LEVELS, + createContentAgentJob, + getSourceCatalog, + uploadContentAgentFile, + type CefrLevel, + type ContentAgentJob, + type ContentAgentJobCreate, + type ContentAgentSource, + type SourceSnapshot, +} from "../../lib/contentAgentApi"; +import { useI18n } from "../../lib/i18n"; + +const MAX_UPLOAD_BYTES = 5 * 1024 * 1024; + +const CORE_LEXICAL_SOURCES: readonly string[] = ["oewn", "cmudict", "cefr_j", "wikidata"]; + +export type ContentAgentFormState = { + levels: CefrLevel[]; + sources: string[]; + courseTitle: string; + topicFocus: string; + unitsPerCourse: number; + lessonsPerUnit: number; + vocabularyPerLesson: number; + exerciseCount: number; + speakingExerciseCount: number; + listeningExerciseCount: number; + confidenceThreshold: number; + previewOnly: true; + acknowledgedDraft: boolean; + revision: boolean; + uploadAttestation: boolean; +}; + +export const createDefaultContentAgentForm = (): ContentAgentFormState => ({ + levels: [...CEFR_LEVELS], + sources: [], + courseTitle: "", + topicFocus: "", + unitsPerCourse: 3, + lessonsPerUnit: 4, + vocabularyPerLesson: 10, + exerciseCount: 10, + speakingExerciseCount: 2, + listeningExerciseCount: 2, + confidenceThreshold: 0.75, + previewOnly: true, + acknowledgedDraft: false, + revision: false, + uploadAttestation: false, +}); + +export const validateContentAgentUpload = (file: File): string | null => { + const extension = file.name.toLowerCase().split(".").pop(); + if (extension !== "csv" && extension !== "json") { + return "Upload must be a CSV or JSON file."; + } + if (file.size > MAX_UPLOAD_BYTES) { + return "Upload must be 5 MB or smaller."; + } + if (file.size === 0) { + return "Upload cannot be empty."; + } + return null; +}; + +export const validateContentAgentForm = ( + form: ContentAgentFormState, + hasUpload = false, +): string | null => { + if (form.levels.length === 0) return "Select at least one CEFR level."; + if (form.sources.length === 0 && !hasUpload) { + return "Select at least one approved source or upload a file."; + } + if (hasUpload && !form.uploadAttestation) { + return "You must confirm rights before uploading"; + } + if ( + form.vocabularyPerLesson < 8 || + form.vocabularyPerLesson > 12 + ) { + return "Vocabulary per lesson must be between 8 and 12."; + } + if (form.unitsPerCourse < 1 || form.unitsPerCourse > 10) { + return "Units per course must be between 1 and 10."; + } + if (form.lessonsPerUnit < 1 || form.lessonsPerUnit > 20) { + return "Lessons per unit must be between 1 and 20."; + } + if ( + form.exerciseCount < 4 || + form.speakingExerciseCount < 0 || + form.listeningExerciseCount < 0 || + form.speakingExerciseCount + form.listeningExerciseCount > + form.exerciseCount + ) { + return "Exercise mix must fit inside the total exercise count."; + } + if ( + form.confidenceThreshold < 0.5 || + form.confidenceThreshold > 0.99 + ) { + return "Confidence threshold must be between 0.50 and 0.99."; + } + if (!form.acknowledgedDraft) { + return "Acknowledge that generated courses remain drafts."; + } + return null; +}; + +type ContentAgentModalProps = { + onClose: () => void; + onJobCreated: (job: ContentAgentJob) => void; +}; + +export function ContentAgentModal({ + onClose, + onJobCreated, +}: ContentAgentModalProps) { + const { t } = useI18n(); + const [form, setForm] = useState( + createDefaultContentAgentForm, + ); + const [upload, setUpload] = useState(null); + const [uploadError, setUploadError] = useState(null); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const [catalog, setCatalog] = useState([]); + const [catalogLoading, setCatalogLoading] = useState(true); + const [catalogError, setCatalogError] = useState(null); + + useEffect(() => { + let disposed = false; + setCatalogLoading(true); + setCatalogError(null); + + getSourceCatalog() + .then((snapshots) => { + if (disposed) return; + setCatalog(snapshots); + const preselected = snapshots + .filter( + (s) => + s.status === "active" && + s.enabled && + CORE_LEXICAL_SOURCES.includes(s.source_id), + ) + .map((s) => s.source_id); + setForm((current) => ({ ...current, sources: preselected })); + }) + .catch((err) => { + if (!disposed) { + setCatalogError( + err instanceof Error ? err.message : t.contentAgent.sourceError, + ); + } + }) + .finally(() => { + if (!disposed) setCatalogLoading(false); + }); + + return () => { + disposed = true; + }; + }, [t.contentAgent.sourceError]); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape" && !submitting) onClose(); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onClose, submitting]); + + const toggleLevel = (level: CefrLevel) => { + setForm((current) => ({ + ...current, + levels: current.levels.includes(level) + ? current.levels.filter((item) => item !== level) + : [...current.levels, level], + })); + }; + + const toggleSource = (sourceId: string) => { + setForm((current) => ({ + ...current, + sources: current.sources.includes(sourceId) + ? current.sources.filter((item) => item !== sourceId) + : [...current.sources, sourceId], + })); + }; + + const handleUpload = (file: File | null) => { + setUpload(file); + const nextError = file ? validateContentAgentUpload(file) : null; + setUploadError(nextError); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const validationError = validateContentAgentForm(form, Boolean(upload)); + const fileError = upload ? validateContentAgentUpload(upload) : null; + if (validationError || fileError) { + setError(validationError); + setUploadError(fileError); + return; + } + + setSubmitting(true); + setError(null); + try { + const uploaded = upload + ? await uploadContentAgentFile(upload, form.uploadAttestation) + : null; + const sources: ContentAgentSource[] = form.sources as ContentAgentSource[]; + if (uploaded && !sources.includes("admin_upload")) { + sources.push("admin_upload"); + } + + const payload: ContentAgentJobCreate = { + levels: form.levels, + sources, + ...(uploaded ? { upload_id: uploaded.id } : {}), + ...(form.courseTitle.trim() + ? { title_focus: form.courseTitle.trim() } + : {}), + topic_focus: form.topicFocus + .split(",") + .map((topic) => topic.trim()) + .filter(Boolean), + units_per_course: form.unitsPerCourse, + lessons_per_unit: form.lessonsPerUnit, + words_per_lesson: form.vocabularyPerLesson, + exercises_per_lesson: form.exerciseCount, + exercise_mix: { + speaking: form.speakingExerciseCount, + listening: form.listeningExerciseCount, + }, + confidence_threshold: form.confidenceThreshold, + revision: form.revision, + apply_on_success: false, + }; + + const job = await createContentAgentJob(payload); + onJobCreated(job); + } catch (requestError) { + setError( + requestError instanceof Error + ? requestError.message + : t.contentAgent.createFailed, + ); + } finally { + setSubmitting(false); + } + }; + + const formatLastSync = (lastSyncAt: string | null): string => { + if (!lastSyncAt) return t.common.never; + try { + return new Date(lastSyncAt).toLocaleDateString(); + } catch { + return lastSyncAt; + } + }; + + return ( +
!submitting && onClose()} + > +
event.stopPropagation()} + role="dialog" + > +
+
+ +
+

+ {t.contentAgent.configureTitle} +

+

{t.contentAgent.configureDescription}

+
+
+ +
+ +
+
+
+
+

{t.contentAgent.cefrLevels}

+

{t.contentAgent.cefrHelp}

+
+ + {form.levels.length}/6 + +
+
+ {CEFR_LEVELS.map((level) => ( + + ))} +
+
+ +
+
+
+

{t.contentAgent.sourceCatalog}

+

{t.contentAgent.sourcesHelp}

+
+
+
+ {catalogLoading && ( +
+
+ )} + {!catalogLoading && catalogError && ( +
{catalogError}
+ )} + {!catalogLoading && !catalogError && catalog.length === 0 && ( +
+ {t.contentAgent.sourceEmpty} +
+ )} + {!catalogLoading && + !catalogError && + catalog.map((snapshot) => { + const selectable = + snapshot.status === "active" && snapshot.enabled; + const checked = form.sources.includes(snapshot.source_id); + return ( + + ); + })} +
+
+ +
+
+
+

{t.contentAgent.uploadTitle}

+

{t.contentAgent.uploadHelp}

+
+
+ + {uploadError &&
{uploadError}
} + {upload && ( + + )} +
+ +
+
+
+

{t.contentAgent.curriculum}

+

{t.contentAgent.curriculumHelp}

+
+
+
+ + + + setForm((current) => ({ + ...current, + unitsPerCourse: value, + })) + } + value={form.unitsPerCourse} + /> + + setForm((current) => ({ + ...current, + lessonsPerUnit: value, + })) + } + value={form.lessonsPerUnit} + /> + + setForm((current) => ({ + ...current, + vocabularyPerLesson: value, + })) + } + value={form.vocabularyPerLesson} + /> + + setForm((current) => ({ + ...current, + exerciseCount: value, + })) + } + value={form.exerciseCount} + /> + + setForm((current) => ({ + ...current, + speakingExerciseCount: value, + })) + } + value={form.speakingExerciseCount} + disabled + /> + + setForm((current) => ({ + ...current, + listeningExerciseCount: value, + })) + } + value={form.listeningExerciseCount} + disabled + /> + +
+
+ +
+
+ {t.contentAgent.previewOnly} + {t.contentAgent.previewLocked} +
+ +
+ + + + + + {error &&
{error}
} + +
+ + +
+
+
+
+ ); +} + +type NumberFieldProps = { + label: string; + min: number; + max: number; + value: number; + onChange: (value: number) => void; + disabled?: boolean; +}; + +function NumberField({ + label, + min, + max, + value, + onChange, + disabled = false, +}: NumberFieldProps) { + return ( + + ); +} diff --git a/admin-service/src/components/dashboard/CompletionFunnelChart.tsx b/admin-service/src/components/dashboard/CompletionFunnelChart.tsx index f64c6bbc..b548d132 100644 --- a/admin-service/src/components/dashboard/CompletionFunnelChart.tsx +++ b/admin-service/src/components/dashboard/CompletionFunnelChart.tsx @@ -47,9 +47,9 @@ export const CompletionFunnelChart: React.FC = ({ data, loading }) => { tick={{ fontSize: 12 }} width={90} /> - [ - `${value?.toLocaleString() || ""}${(props?.payload?.percentage !== undefined) ? ` (${props.payload.percentage.toFixed(1)}%)` : ""}`, + [ + `${Number(value ?? 0).toLocaleString()} (${Number(item.payload?.percentage ?? 0).toFixed(1)}%)`, "" ]} /> diff --git a/admin-service/src/components/dashboard/CoursePopularityChart.tsx b/admin-service/src/components/dashboard/CoursePopularityChart.tsx index f13895c4..5fd49adf 100644 --- a/admin-service/src/components/dashboard/CoursePopularityChart.tsx +++ b/admin-service/src/components/dashboard/CoursePopularityChart.tsx @@ -45,7 +45,7 @@ export const CoursePopularityChart: React.FC = ({ data, loading }) => { cx="50%" cy="50%" labelLine={false} - label={({ name, percent }) => `${name}: ${percent !== undefined ? (percent * 100).toFixed(0) : "0"}%`} + label={({ name, percent }) => `${name}: ${((percent ?? 0) * 100).toFixed(0)}%`} outerRadius={80} fill="#8884d8" dataKey="value" @@ -54,7 +54,7 @@ export const CoursePopularityChart: React.FC = ({ data, loading }) => { ))} - [value?.toLocaleString() + " đăng ký", ""]} /> + [Number(value ?? 0).toLocaleString() + " đăng ký", ""]} /> diff --git a/admin-service/src/components/dashboard/EngagementChart.tsx b/admin-service/src/components/dashboard/EngagementChart.tsx index 82e94c53..f77b65fb 100644 --- a/admin-service/src/components/dashboard/EngagementChart.tsx +++ b/admin-service/src/components/dashboard/EngagementChart.tsx @@ -37,7 +37,7 @@ export const EngagementChart: React.FC = ({ data, loading }) => { - [value?.toLocaleString() || "", ""]} /> + [Number(value ?? 0).toLocaleString(), ""]} /> diff --git a/admin-service/src/components/dashboard/UserGrowthChart.tsx b/admin-service/src/components/dashboard/UserGrowthChart.tsx index 0c119296..f745e852 100644 --- a/admin-service/src/components/dashboard/UserGrowthChart.tsx +++ b/admin-service/src/components/dashboard/UserGrowthChart.tsx @@ -48,7 +48,7 @@ export const UserGrowthChart: React.FC = ({ data, loading }) => { const date = new Date(value); return date.toLocaleDateString("vi-VN"); }} - formatter={(value: any) => [value?.toLocaleString() || "", ""]} + formatter={(value) => [Number(value ?? 0).toLocaleString(), ""]} /> void; + onJobUpdated: (job: NotificationCampaignJob) => void; +}; + +export function NotificationCampaignDrawer({ job: initialJob, onClose, onJobUpdated }: Props) { + const [job, setJob] = useState(initialJob); + const [applying, setApplying] = useState(false); + const [actionError, setActionError] = useState(null); + const [previewExpanded, setPreviewExpanded] = useState(true); + const pollRef = useRef | null>(null); + + const updateJob = useCallback( + (updated: NotificationCampaignJob) => { + setJob(updated); + onJobUpdated(updated); + }, + [onJobUpdated] + ); + + useEffect(() => { + if (!isNotificationCampaignJobActive(job.status)) return; + pollRef.current = setInterval(async () => { + try { + const updated = await getNotificationCampaignJob(job.id); + updateJob(updated); + if (!isNotificationCampaignJobActive(updated.status)) { + clearInterval(pollRef.current!); + } + } catch { + // ignore transient + } + }, POLL_MS); + return () => clearInterval(pollRef.current!); + }, [job.id, job.status, updateJob]); + + async function handleApply() { + setActionError(null); + setApplying(true); + try { + const updated = await applyNotificationCampaignJob(job.id); + updateJob(updated); + } catch (err) { + setActionError(err instanceof Error ? err.message : "Apply failed."); + } finally { + setApplying(false); + } + } + + async function handleCancel() { + setActionError(null); + try { + updateJob(await cancelNotificationCampaignJob(job.id)); + } catch (err) { + setActionError(err instanceof Error ? err.message : "Cancel failed."); + } + } + + async function handleRetry() { + setActionError(null); + try { + updateJob(await retryNotificationCampaignJob(job.id)); + } catch (err) { + setActionError(err instanceof Error ? err.message : "Retry failed."); + } + } + + const statusColor: Record = { + queued: "text-gray-500 bg-gray-100", + segmenting: "text-blue-600 bg-blue-50", + generating: "text-purple-600 bg-purple-50", + validating: "text-indigo-600 bg-indigo-50", + sending: "text-orange-600 bg-orange-50", + preview_ready: "text-amber-600 bg-amber-50", + completed: "text-green-600 bg-green-50", + failed: "text-red-600 bg-red-50", + cancelled: "text-gray-500 bg-gray-100", + }; + + const content = (job.config as Record>)?.content ?? {}; + + return ( +
+
+
+ +
+
+ + {JOB_TYPE_LABELS[job.job_type]} + + +
+ {job.id.slice(0, 8)}… +
+
+
+ + {STATUS_LABELS[job.status] ?? job.status} + + +
+
+ + {isNotificationCampaignJobActive(job.status) && ( +
+
+ {job.progress.stage} + {job.progress.percent ?? 0}% +
+
+
+
+
+ )} + +
+ {(job.status === "segmenting" || job.status === "generating" || job.status === "validating") && ( +
+ +

{job.status}…

+
+ )} + + {job.blocking_errors.length > 0 && ( +
+

+ Blocking errors +

+ {job.blocking_errors.map((e, i) => ( +

{e}

+ ))} +
+ )} + + {job.warnings.length > 0 && ( +
+

+ Warnings +

+ {job.warnings.map((w, i) => ( +

{w}

+ ))} +
+ )} + + {job.artifact && job.status === "preview_ready" && ( +
+ + {previewExpanded && ( +
+ +
+ )} +
+ )} + + {job.status === "completed" && ( +
+ +

Campaign sent successfully

+ +
+ )} + + {job.status === "failed" && ( +
+

Job failed

+ {job.error_message && ( +

{job.error_message}

+ )} +
+ )} + + {actionError && ( +

{actionError}

+ )} +
+ +
+
+ {isNotificationCampaignJobActive(job.status) && job.status !== "sending" && ( + + )} + {job.status === "failed" && ( + + )} +
+ {canApplyNotificationCampaignJob(job) && ( + + )} +
+
+ ); +} + +function PreviewSummary({ job }: { job: NotificationCampaignJob }) { + const art = job.artifact!; + const contentPreview = art.content_preview as Record | undefined; + const aiCopy = art.ai_copy as Record | undefined; + const sampleUsers = (art.sample_users as Array>) ?? []; + + return ( +
+
+ + 0 ? "text-green-600" : "text-red-500"} + /> +
+ + {contentPreview && ( +
+ {!!aiCopy && ( +

+ AI-generated copy +

+ )} +

+ {String(contentPreview.title ?? "")} +

+

{String(contentPreview.body ?? "")}

+
+ )} + + {sampleUsers.length > 0 && ( +
+

+ Sample users ({sampleUsers.length}) +

+
+ {sampleUsers.map((u, i) => ( +
+ {String(u.username ?? "")} + + {String(u.cefr_level ?? "")} · {u.has_fcm ? "FCM ✓" : "no FCM"} + +
+ ))} +
+
+ )} + + {!!art.scheduled_for && ( +

+ Scheduled: {new Date(String(art.scheduled_for)).toLocaleString()} +

+ )} +
+ ); +} + +function Stat({ + label, + value, + color = "text-gray-900", +}: { + label: string; + value: string; + color?: string; +}) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function DeliveryStats({ stats }: { stats: Record }) { + if (!Object.keys(stats).length) return null; + return ( +
+ + + +
+ ); +} diff --git a/admin-service/src/components/notification-campaign/NotificationCampaignModal.tsx b/admin-service/src/components/notification-campaign/NotificationCampaignModal.tsx new file mode 100644 index 00000000..f281b28b --- /dev/null +++ b/admin-service/src/components/notification-campaign/NotificationCampaignModal.tsx @@ -0,0 +1,337 @@ +import React, { useState } from "react"; +import { Bell, Calendar, MessageSquare, Radio, Sparkles, X } from "lucide-react"; + +import { + createNotificationCampaignJob, + type NotificationCampaignJob, + type NotificationCampaignJobType, +} from "../../lib/notificationCampaignApi"; + +const LEAGUES = [ + "bronze", "silver", "gold", "platinum", "sapphire", "ruby", "amethyst", "master", +] as const; +const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"] as const; + +type Props = { + onClose: () => void; + onJobCreated: (job: NotificationCampaignJob) => void; +}; + +type Tab = NotificationCampaignJobType; + +const TAB_INFO: Record = { + targeted_push: { + label: "Targeted Push", + icon: , + description: "Send an FCM push notification to a filtered user segment.", + }, + in_app_broadcast: { + label: "In-App Broadcast", + icon: , + description: "Create persisted in-app notifications visible in the notification feed.", + }, + scheduled_push: { + label: "Scheduled Push", + icon: , + description: "Schedule a push notification to be sent at a specific date and time.", + }, +}; + +export function NotificationCampaignModal({ onClose, onJobCreated }: Props) { + const [activeTab, setActiveTab] = useState("targeted_push"); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Content + const [title, setTitle] = useState(""); + const [body, setBody] = useState(""); + const [notifType, setNotifType] = useState("campaign"); + const [deepLink, setDeepLink] = useState(""); + const [useAiCopy, setUseAiCopy] = useState(false); + + // Audience + const [audienceType, setAudienceType] = useState<"all" | "segment">("all"); + const [selectedLeagues, setSelectedLeagues] = useState([]); + const [selectedCefr, setSelectedCefr] = useState([]); + const [minStreak, setMinStreak] = useState(""); + const [inactiveDays, setInactiveDays] = useState(""); + + // Scheduled push + const [sendAt, setSendAt] = useState(""); + + function toggleLeague(l: string) { + setSelectedLeagues((prev) => + prev.includes(l) ? prev.filter((x) => x !== l) : [...prev, l] + ); + } + + function toggleCefr(l: string) { + setSelectedCefr((prev) => + prev.includes(l) ? prev.filter((x) => x !== l) : [...prev, l] + ); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!title.trim() || !body.trim()) { + setError("Title and body are required."); + return; + } + + const audienceFilters: Record = { has_fcm_token: true }; + if (audienceType === "segment") { + if (selectedLeagues.length) audienceFilters.leagues = selectedLeagues; + if (selectedCefr.length) audienceFilters.cefr_levels = selectedCefr; + if (minStreak) audienceFilters.min_streak = parseInt(minStreak, 10); + if (inactiveDays) audienceFilters.inactive_days = parseInt(inactiveDays, 10); + } + + const config: Record = { + audience: { type: audienceType, filters: audienceFilters }, + content: { + title: title.trim(), + body: body.trim(), + notification_type: notifType, + deep_link: deepLink.trim() || null, + use_ai_copy: useAiCopy, + }, + }; + + if (activeTab === "scheduled_push" && sendAt) { + config.send_at = new Date(sendAt).toISOString(); + } + + setSubmitting(true); + setError(null); + try { + const job = await createNotificationCampaignJob({ + job_type: activeTab, + config, + }); + onJobCreated(job); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to create job"); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+
+ +

Tạo Notification Campaign

+
+ +
+ +
+ {(Object.keys(TAB_INFO) as Tab[]).map((tab) => ( + + ))} +
+ +
+

{TAB_INFO[activeTab].description}

+ +
+

Nội dung thông báo

+ +
+ + setTitle(e.target.value)} + maxLength={100} + placeholder="Ví dụ: Weekend Boost đã bắt đầu!" + /> +

{title.length}/100

+
+ +
+ +