docs(项目): 添加项目文档并进行代码调整

- 新增 CODEBUDDY.md、GEMINI.md、GEMINI_CN.md 等项目文档
- 更新 Dockerfile 和其他配置文件
- 优化部分代码结构,如 orders.py、tasks.py 等
- 新增 .dockerignore 文件
This commit is contained in:
danial
2025-09-12 19:38:24 +08:00
parent 48cdcb6140
commit 5c486e34d3
63 changed files with 2733 additions and 836 deletions

View File

@@ -13,7 +13,9 @@
"Bash(docker run:*)",
"Bash(curl:*)",
"Bash(docker logs:*)",
"Bash(docker rm:*)"
"Bash(docker rm:*)",
"Bash(docker:*)",
"Bash(openssl:*)"
],
"deny": [],
"ask": []

170
.dockerignore Normal file
View File

@@ -0,0 +1,170 @@
# Git
.git
.gitignore
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
.venv/
.pytest_cache/
.coverage
.tox/
htmlcov/
.mypy_cache/
.dmypy.json
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
uv.lock
poetry.lock
Pipfile.lock
pdm.lock
requirements*.txt
setup.py
setup.cfg
pyproject.toml
*.egg-info/
celerybeat-schedule
celerybeat.pid
*.rdb
test-results/
playwright-report/
playwright/.cache/
.uv/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error-log*
.next/
out/
dist/
build/
*.tsbuildinfo
next-env-d.ts
.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.pnpm-debug.log*
.vercel
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
*.code-workspace
.history/
# OS
.DS_Store
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
*.stackdump
Desktop.ini
$RECYCLE.BIN/
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
# Logs
*.log
logs/
log/
# Environment variables
# Temporary files
tmp/
temp/
*.tmp
*.temp
cache/
.cache/
# Data directories
data/
uploads/
downloads/
apple_data/
exchange_logs/
transaction_logs/
user_uploads/
# Database files
*.db
*.sqlite
*.sqlite3
# SSL certificates
*.pem
*.key
*.crt
*.cert
# Backup files
*.bak
*.backup
*.old
*.orig
# Compressed files
*.zip
*.tar.gz
*.rar
*.7z
# Documentation
*.md
docs/
README*
# Testing and coverage
coverage/
htmlcov/
.tox/
.nox/
# Claude Code
.claude/
# Kubernetes
*.kubeconfig
.kube/
# Terraform
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
# Ansible
*.retry
# Monitoring
*.prof
*.pprof
*.trace

48
CODEBUDDY.md Normal file
View File

@@ -0,0 +1,48 @@
# CODEBUDDY.md
## Development Commands
### Frontend (Next.js)
- `cd frontend && bun install` - Install dependencies
- `cd frontend && bun run dev` - Start development server
- `cd frontend && bun run build` - Build production version
- `cd frontend && bun run lint` - Run ESLint checks
- `cd frontend && bun run test` - Run tests
### Backend (FastAPI)
- `cd backend && uv sync` - Install dependencies
- `cd backend && uv run python app/main.py` - Run development server
- `cd backend && uv run pytest` - Run tests
- `cd backend && uv run black .` - Format code with Black
- `cd backend && uv run ruff check .` - Lint with Ruff
### Docker Development
- `cd backend && docker-compose up -d` - Start all services
- `cd backend && docker-compose down` - Stop all services
## Architecture Overview
### Project Structure
- **Frontend**: Next.js 15 with App Router, React 18, TypeScript
- **Backend**: FastAPI with Python 3.13, async/await architecture
- **Infrastructure**: Docker, Redis, PostgreSQL
### Key Directories
- **Frontend**: `frontend/src/app/` (Next.js pages), `frontend/src/components/` (UI components)
- **Backend**: `backend/app/api/` (FastAPI routes), `backend/app/core/` (core functionality), `backend/app/services/` (business logic)
### Core Features
- **Frontend**: Real-time dashboard, Apple-style UI, theme switching
- **Backend**: Distributed web scraping, Redis-based locking, Celery task queue
### API Integration
- Backend OpenAPI spec at `http://localhost:8000/openapi.json`
- Frontend auto-generates API client code
### State Management
- **Frontend**: React Context + TanStack Query
- **Backend**: PostgreSQL + Redis
### Deployment
- **Development**: Docker Compose for services, Bun/UV for apps
- **Production**: Docker containers with Nginx, Kubernetes support

100
DRAFT_SECURITY_REPORT.md Normal file
View File

@@ -0,0 +1,100 @@
## Vulnerability Report
### 1. Container Runs as Root
* **Vulnerability:** The Docker container runs as the `root` user.
* **Severity:** High
* **Location:** `backend/Dockerfile`
* **Line Content:** Not applicable (absence of `USER` instruction).
* **Description:** Running a container as `root` is a security risk. If an attacker gains control of the application, they will have root privileges inside the container, which can be used to escalate privileges further.
* **Recommendation:** Create a non-root user and switch to it using the `USER` instruction in the Dockerfile.
### 2. Hardcoded Database Credentials
* **Vulnerability:** The `DATABASE_URL` in the `.env.production` file contains a hardcoded password.
* **Severity:** Critical
* **Location:** `backend/.env.production`
* **Line Content:** `DATABASE_URL="postgresql+asyncpg://postgres:password@db:5432/apple_exchange"`
* **Description:** The production database credentials are hardcoded in the `.env.production` file, which is then copied into the Docker image. This makes it easy for an attacker to gain access to the database if they can read the contents of the image.
* **Recommendation:** Use a secrets management solution to inject the database password into the container at runtime. The `DATABASE_URL` should be constructed using an environment variable for the password, similar to how `JWT_SECRET_KEY` and `ENCRYPTION_KEY` are handled.
### 3. Test Container Runs as Root
* **Vulnerability:** The test Docker container runs as the `root` user.
* **Severity:** High
* **Location:** `backend/Dockerfile.test`
* **Line Content:** Not applicable (absence of `USER` instruction).
* **Description:** Running a container as `root` is a security risk, even in a test environment. If an attacker gains control of the application, they will have root privileges inside the container, which can be used to escalate privileges further.
* **Recommendation:** Create a non-root user and switch to it using the `USER` instruction in the Dockerfile.
### 4. Worker Container Runs as Root
* **Vulnerability:** The worker Docker container runs as the `root` user.
* **Severity:** High
* **Location:** `backend/Dockerfile.worker`
* **Line Content:** Not applicable (absence of `USER` instruction).
* **Description:** Running a container as `root` is a security risk. If an attacker gains control of the application, they will have root privileges inside the container, which can be used to escalate privileges further.
* **Recommendation:** Create a non-root user and switch to it using the `USER` instruction in the Dockerfile.
### 5. Potential IDOR in `get_order` endpoint
* **Vulnerability:** Potential Insecure Direct Object Reference (IDOR) in the `get_order` endpoint.
* **Severity:** High
* **Location:** `backend/app/api/v1/orders.py`
* **Line Content:** `async def get_order(order_id: str, db: AsyncSession = Depends(get_async_db)) -> OrderDetailResponse:`
* **Description:** The `get_order` endpoint takes an `order_id` from the URL and retrieves the order details. There are no checks to verify that the authenticated user has permission to access the requested order. An attacker could potentially enumerate order IDs to view orders belonging to other users.
* **Recommendation:** Implement an access control check to ensure that the current user is authorized to view the requested order. This typically involves checking if the `order.user_id` matches the `current_user.id`.
### 6. Missing Access Control on Task Management Endpoints
* **Vulnerability:** Missing access control on task management endpoints.
* **Severity:** High
* **Location:** `backend/app/api/v1/tasks.py`
* **Line Content:** `async def toggle_task_state(request: TaskControlRequest) -> TaskControlResponse:`
* **Description:** The endpoints in `tasks.py` for controlling the task queue (`/toggle`, `/status`, etc.) do not have any authorization checks. This means that any user, including unauthenticated users, could potentially pause, resume, or otherwise interfere with the task queue, leading to a denial of service.
* **Recommendation:** Implement a robust access control mechanism for all task management endpoints. Only authorized users (e.g., administrators) should be able to perform these actions. This can be achieved using FastAPI's dependency injection system to require an authenticated user with the necessary permissions.
### 7. Potential IDOR in `user_data` endpoints
* **Vulnerability:** Potential Insecure Direct Object Reference (IDOR) in the `user_data` endpoints.
* **Severity:** High
* **Location:** `backend/app/api/v1/user_data.py`
* **Line Content:** `async def get_user_data(user_id: str, service: UserDataService = Depends(get_user_data_service)):` and `async def delete_user_data(user_id: str, service: UserDataService = Depends(get_user_data_service)):`
* **Description:** The `get_user_data` and `delete_user_data` endpoints take a `user_id` from the URL and retrieve or delete the user's data. There are no checks to verify that the authenticated user has permission to access or delete the requested user's data. An attacker could potentially enumerate user IDs to view or delete user data belonging to other users.
* **Recommendation:** Implement an access control check to ensure that the current user is authorized to view or delete the requested user data. This typically involves checking if the `user_id` matches the `current_user.id` or if the current user has administrative privileges.
### 8. Potential IDOR in `get_task_status` endpoint
* **Vulnerability:** Potential Insecure Direct Object Reference (IDOR) in the `get_task_status` endpoint.
* **Severity:** Medium
* **Location:** `backend/app/api/v1/user_data.py`
* **Line Content:** `async def get_task_status(task_id: str, service: UserDataService = Depends(get_user_data_service)):`
* **Description:** The `get_task_status` endpoint takes a `task_id` from the URL and retrieves the task status. There are no checks to verify that the authenticated user has permission to access the requested task status. An attacker could potentially enumerate task IDs to view the status of tasks belonging to other users.
* **Recommendation:** Implement an access control check to ensure that the current user is authorized to view the requested task status. This could involve associating tasks with users and checking for ownership.
### 9. Permissive CORS and `ALLOWED_HOSTS` Configuration
* **Vulnerability:** The `ALLOWED_HOSTS` and `CORS_ORIGINS` are set to `["*"]` by default.
* **Severity:** Medium
* **Location:** `backend/app/core/config.py`
* **Line Content:** `ALLOWED_HOSTS: list[str] = Field(default=["*"], description="允许的主机列表")` and `CORS_ORIGINS: list[str] = Field(default=["*"], description="CORS允许的源列表")`
* **Description:** The `ALLOWED_HOSTS` and `CORS_ORIGINS` settings are configured to allow any host. This is a permissive setting that could be a security risk in a production environment. A permissive CORS policy can make the application vulnerable to cross-site request forgery (CSRF) attacks, and a permissive `ALLOWED_HOSTS` setting can make the application vulnerable to host header attacks.
* **Recommendation:** In a production environment, `ALLOWED_HOSTS` and `CORS_ORIGINS` should be set to a specific list of trusted domains.
### 10. Use of `KEYS` command in Redis
* **Vulnerability:** The `list_states` method uses the `KEYS` command to get all keys matching a pattern.
* **Severity:** Low
* **Location:** `backend/app/core/state_manager.py`
* **Line Content:** `keys = await redis_client.keys(search_pattern)`
* **Description:** The `KEYS` command can block the Redis server while it is executing, and can cause performance issues on a Redis instance with a large number of keys. This could be a potential denial of service vector.
* **Recommendation:** Use the `SCAN` command instead of `KEYS` to iterate over the keys in a non-blocking way.
### 11. Storage of PII in Plain Text
* **Vulnerability:** Personally Identifiable Information (PII) is stored in plain text in the database.
* **Severity:** Medium
* **Location:** `backend/app/models/user_data.py`
* **Line Content:** Not applicable (model definition).
* **Description:** The `UserData` model contains sensitive user information such as name, address, email, and phone number. This data is stored in the database without any encryption at the application level. If the database is compromised, all of this sensitive user data will be exposed in plain text.
* **Recommendation:** Encrypt sensitive PII in the database. This can be done at the application level using a library like `sqlalchemy-utils` with the `EncryptedType` or by using database-level encryption features.

88
GEMINI.md Normal file
View File

@@ -0,0 +1,88 @@
# Gemini Project Context: Apple Gift Card Exchange Platform
## Project Overview
This is a full-stack application for an Apple gift card exchange platform. It features a modern, scalable architecture with a real-time monitoring dashboard and an Apple-style user interface.
* **Frontend:** A Next.js application written in TypeScript, using Tailwind CSS for styling and Radix UI for components. It communicates with the backend via a REST API.
* **Backend:** A Python-based microservices architecture using FastAPI. It leverages Celery for asynchronous task processing, Playwright for browser automation, and PostgreSQL for the database.
* **Architecture:** The system is designed as a distributed system with a frontend, a backend API, and a pool of Celery workers. It uses Redis for caching and as a message broker.
## Building and Running
### Prerequisites
* Python 3.13+
* Node.js 18+
* Docker & Docker Compose
* PostgreSQL
* Redis
### Development Mode
1. **Start Dependencies:**
```bash
cd backend
docker-compose up -d redis postgres
```
2. **Start Backend:**
```bash
cd backend
uv sync
uv run python app/main.py
```
3. **Start Frontend:**
```bash
cd frontend
bun install
bun run dev
```
4. **Start Workers:**
```bash
cd backend
uv run celery -A app.core.celery_app worker --loglevel=info
uv run celery -A app.core.celery_app beat --loglevel=info
```
### Docker Development
```bash
cd backend
docker-compose up -d
```
* **Frontend:** `http://localhost:3000`
* **Backend API:** `http://localhost:8000`
* **API Documentation:** `http://localhost:8000/docs`
## Development Conventions
### Code Quality
* **Backend:**
* **Formatting:** Black
* **Import Sorting:** isort
* **Type Checking:** MyPy
* **Linting:** Ruff
* **Testing:** pytest
* **Frontend:**
* **Formatting:** Prettier
* **Linting:** ESLint
* **Type Checking:** TypeScript
* **Testing:** Jest + React Testing Library
### Git Workflow
* Create feature branches from `master`.
* Use conventional commit messages (e.g., `feat:`, `fix:`, `docs:`).
* Submit pull requests for code review.
## Key Files
* `frontend/`: Contains the Next.js frontend application.
* `backend/`: Contains the FastAPI backend application.
* `deploy/`: Contains deployment scripts and configurations.
* `README.md`: The main README file with detailed project information.

88
GEMINI_CN.md Normal file
View File

@@ -0,0 +1,88 @@
# Gemini 项目背景:苹果礼品卡兑换平台
## 项目概述
这是一个用于苹果礼品卡兑換平台的全栈应用程序。它采用现代、可扩展的架构,具有实时监控仪表板和苹果风格的用户界面。
* **前端:** 一个使用 TypeScript 编写的 Next.js 应用程序,使用 Tailwind CSS 进行样式设计,并使用 Radix UI 作为组件。它通过 REST API 与后端通信。
* **后端:** 一个基于 Python 的微服务架构,使用 FastAPI。它利用 Celery 进行异步任务处理,使用 Playwright 进行浏览器自动化,并使用 PostgreSQL 作为数据库。
* **架构:** 该系统设计为一个分布式系统,包括一个前端、一个后端 API 和一个 Celery 工作者池。它使用 Redis 进行缓存和作为消息代理。
## 构建和运行
### 先决条件
* Python 3.13+
* Node.js 18+
* Docker 和 Docker Compose
* PostgreSQL
* Redis
### 开发模式
1. **启动依赖项:**
```bash
cd backend
docker-compose up -d redis postgres
```
2. **启动后端:**
```bash
cd backend
uv sync
uv run python app/main.py
```
3. **启动前端:**
```bash
cd frontend
bun install
bun run dev
```
4. **启动工作者:**
```bash
cd backend
uv run celery -A app.core.celery_app worker --loglevel=info
uv run celery -A app.core.celery_app beat --loglevel=info
```
### Docker 开发
```bash
cd backend
docker-compose up -d
```
* **前端:** `http://localhost:3000`
* **后端 API:** `http://localhost:8000`
* **API 文档:** `http://localhost:8000/docs`
## 开发规范
### 代码质量
* **后端:**
* **格式化:** Black
* **导入排序:** isort
* **类型检查:** MyPy
* **代码检查:** Ruff
* **测试:** pytest
* **前端:**
* **格式化:** Prettier
* **代码检查:** ESLint
* **类型检查:** TypeScript
* **测试:** Jest + React Testing Library
### Git 工作流程
* 从 `master` 创建功能分支。
* 使用常规提交消息 (例如, `feat:`, `fix:`, `docs:`)。
* 提交拉取请求以进行代码审查。
## 关键文件
* `frontend/`: 包含 Next.js 前端应用程序。
* `backend/`: 包含 FastAPI 后端应用程序。
* `deploy/`: 包含部署脚本和配置。
* `README.md`: 包含详细项目信息的主 README 文件。

73
SECURITY_ANALYSIS_TODO.md Normal file
View File

@@ -0,0 +1,73 @@
- [x] Define the audit scope.
- [x] SAST Recon on backend/Dockerfile
- [x] Investigate content of `.env.production`
- [x] SAST Recon on backend/Dockerfile.test
- [x] SAST Recon on backend/Dockerfile.worker
- [x] SAST Recon on backend/app/api/v1/links.py
- [x] SAST Recon on backend/app/api/v1/orders.py
- [x] SAST Recon on backend/app/api/v1/tasks.py
- [x] SAST Recon on backend/app/api/v1/user_data.py
- [x] SAST Recon on backend/app/core/celery_app.py
- [x] SAST Recon on backend/app/core/config.py
- [x] SAST Recon on backend/app/core/exceptions.py
- [x] SAST Recon on backend/app/core/redis_manager.py
- [x] SAST Recon on backend/app/core/state_manager.py
- [x] SAST Recon on backend/app/models/giftcards.py
- [x] SAST Recon on backend/app/models/links.py
- [x] SAST Recon on backend/app/models/orders.py
- [x] SAST Recon on backend/app/models/user_data.py
- [x] SAST Recon on backend/app/repositories/order_repository.py
- [x] Investigate `BaseRepository`
- [x] SAST Recon on backend/app/repositories/user_data_repository.py
- [x] SAST Recon on backend/app/schemas/user_data.py
- [ ] SAST Recon on backend/app/services/link_service.py
- [ ] SAST Recon on backend/app/services/order_business_service.py
- [ ] SAST Recon on backend/app/services/playwright_service.py
- [ ] SAST Recon on backend/app/services/task_service.py
- [ ] SAST Recon on backend/app/services/user_data_service.py
- [ ] SAST Recon on backend/app/tasks/__init__.py
- [ ] SAST Recon on backend/app/tasks/crawler_tasks.py
- [ ] SAST Recon on backend/docker-entrypoint-simple.sh
- [ ] SAST Recon on backend/docker-entrypoint.sh
- [ ] SAST Recon on backend/run.py
- [ ] SAST Recon on backend/test_gunicorn.py
- [ ] SAST Recon on backend/test_tasks.py
- [ ] SAST Recon on deploy/deploy.sh
- [ ] SAST Recon on deploy/docker-compose.combined.yml
- [ ] SAST Recon on deploy/redis.conf
- [ ] SAST Recon on frontend/Dockerfile
- [ ] SAST Recon on frontend/build-docker.sh
- [ ] SAST Recon on frontend/next.config.js
- [ ] SAST Recon on frontend/orval.config.ts
- [ ] SAST Recon on frontend/postcss.config.js
- [ ] SAST Recon on frontend/postcss.config.mjs
- [ ] SAST Recon on frontend/src/app/layout.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/gift-card-input.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/link-item.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/link-management.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/order-stats-card.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/task-control.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/task-list.tsx
- [ ] SAST Recon on frontend/src/components/dashboard/uploaded-data-display.tsx
- [ ] SAST Recon on frontend/src/components/layout/loading-screen.tsx
- [ ] SAST Recon on frontend/src/components/ui/apple-button.tsx
- [ ] SAST Recon on frontend/src/lib/api/generated/schemas/index.ts
- [ ] SAST Recon on frontend/src/lib/api/generated/schemas/userDataUploadResponse.ts
- [ ] SAST Recon on frontend/src/styles/globals.css
- [ ] SAST Recon on frontend/src/styles/sass/animations/apple-animations.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/components/apple-components-extended.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/components/apple-components.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/components/apple-system-components.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/components/dynamic-backgrounds.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/components/glass-components.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/main.scss
- [ ] SAST Recon on frontend/src/styles/sass/tailwind.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/themes/apple-design-tokens.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/themes/glass.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/utils/apple-mixins.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/utils/grid-system.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/utils/mixins.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/utils/variables.module.scss
- [ ] SAST Recon on frontend/src/styles/sass/utils/visual-effects.module.scss
- [ ] SAST Recon on frontend/tailwind.config.js
- [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report.

View File

@@ -9,12 +9,13 @@ DEBUG=true
# 服务配置
HOST=0.0.0.0
PORT=3001
PORT=8000
WORKERS=1
# 数据库配置
# DATABASE_URL=sqlite:///./data/kami_data.db
DATABASE_URL=postgresql+asyncpg://postgres:123456@localhost:5432/apple_exchange
# DATABASE_URL=postgresql+asyncpg://postgres:123456@localhost:5432/apple_exchange
DATABASE_URL=postgresql+asyncpg://postgres:password@localhost:5444/apple_exchange
DATABASE_POOL_SIZE=10
DATABASE_MAX_OVERFLOW=20
DATABASE_TIMEOUT=30

View File

@@ -31,9 +31,6 @@ COPY app ./app
COPY run.py ./
COPY .env.production .env
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/api/v1/health/liveness || exit 1
EXPOSE 8000
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@@ -1,11 +0,0 @@
FROM python:3.13-slim
WORKDIR /app
COPY docker-entrypoint-simple.sh ./
RUN chmod +x docker-entrypoint-simple.sh
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh
CMD ["./docker-entrypoint-simple.sh"]

View File

@@ -23,7 +23,6 @@ def get_link_service(db: AsyncSession = Depends(get_async_db)) -> LinksService:
return LinksService(db)
@router.post("/", response_model=LinkResponse)
async def create_link(
link_data: LinkCreate, link_service: LinksService = Depends(get_link_service)
@@ -65,8 +64,11 @@ async def get_links(
logger.error(f"获取链接列表失败: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="获取链接列表失败")
@router.get("/{link_id}", response_model=LinkResponse)
async def get_link(link_id: int, link_service: LinksService = Depends(get_link_service)):
async def get_link(
link_id: int, link_service: LinksService = Depends(get_link_service)
):
"""获取单个链接详情"""
try:
link = await link_service.get_link(str(link_id))

View File

@@ -9,6 +9,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_async_db
from app.core.log import get_logger
from app.models.orders import OrderResultStatus
from app.schemas.link import LinkResponse
from app.schemas.task import GiftCardInfoResponse
from app.schemas.user_data import UserInfoResponse
from app.services.order_business_service import OrderService
from app.schemas.order import (
OrderStatsResponse,
@@ -36,7 +39,7 @@ async def get_order_stats(
)
@router.get("/list", summary="获取订单列表", response_model=list[OrderDetailResponse])
@router.get("/list", summary="获取订单列表")
async def get_orders(
skip: int = 0,
limit: int = 100,
@@ -45,16 +48,68 @@ async def get_orders(
) -> list[OrderDetailResponse]:
"""获取订单列表"""
try:
result = await OrderService(db).get_order_list( skip, limit, status_filter)
logger.info(
f"获取订单列表成功: skip={skip}, limit={limit}, status_filter={status_filter}"
)
orders = await OrderService(db).get_order_list(skip, limit, status_filter)
# Convert SQLAlchemy models to Pydantic response schemas
result = []
for order in orders:
link_response = LinkResponse(
id=order.links.id,
url=order.links.url,
amount=order.links.amount,
created_at=order.links.created_at.isoformat(),
updated_at=order.links.updated_at.isoformat(),
)
gift_cards = []
if order.gift_cards:
for gc in order.gift_cards:
gift_cards.append(GiftCardInfoResponse(
id=gc.id,
card_code=gc.card_code,
status=gc.status.value,
failure_reason=gc.failure_reason,
order_result_id=order.id,
created_at=gc.created_at.isoformat(),
updated_at=gc.updated_at.isoformat()
))
user_data = UserInfoResponse(
id=order.user_data.id,
email=order.user_data.email,
phone=order.user_data.phone,
created_at=order.user_data.created_at.isoformat(),
updated_at=order.user_data.updated_at.isoformat(),
full_name=order.user_data.full_name,
full_address=order.user_data.full_address,
first_name=order.user_data.first_name,
last_name=order.user_data.last_name,
street_address=order.user_data.street_address,
city=order.user_data.city,
state=order.user_data.state,
zip_code=order.user_data.zip_code
)
# Handle gift_cards being None by converting to empty list
gift_cards = order.gift_cards if order.gift_cards is not None else []
order_detail = OrderDetailResponse(
id=order.id,
status=order.status.value,
created_at=order.created_at.isoformat(),
updated_at=order.updated_at.isoformat(),
final_order_url=order.final_order_url,
failure_reason=order.failure_reason,
user_data_id=order.user_data_id,
links_id=order.links_id,
user_data=user_data,
links=link_response,
gift_cards=gift_cards
)
result.append(order_detail)
return result
except ValueError as e:
logger.warning(f"获取订单列表失败 - 参数验证错误: {str(e)}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"获取订单列表失败: {e}", exc_info=True)
logger.error(f"获取订单列表失败: {traceback.format_exc()}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"获取订单列表失败: {str(e)}",
@@ -67,9 +122,9 @@ async def export_orders(
):
"""导出订单数据为Excel文件"""
try:
excel_content, filename = await OrderService(db, ).export_orders_to_excel(
status_filter
)
excel_content, filename = await OrderService(
db,
).export_orders_to_excel(status_filter)
logger.info(f"导出订单数据成功: filename={filename}")
return Response(
@@ -90,9 +145,7 @@ async def export_orders(
@router.get("/{order_id}", summary="获取订单详情", response_model=OrderDetailResponse)
async def get_order(
order_id: str, db: AsyncSession = Depends(get_async_db)
) -> OrderDetailResponse:
async def get_order(order_id: str, db: AsyncSession = Depends(get_async_db)) -> OrderDetailResponse:
"""获取订单详情"""
try:
order = await OrderService(db).get_order_detail(order_id)
@@ -103,8 +156,25 @@ async def get_order(
status_code=status.HTTP_404_NOT_FOUND, detail="订单不存在"
)
# Convert SQLAlchemy model to Pydantic response schema
# Handle gift_cards being None by converting to empty list
gift_cards = order.gift_cards if order.gift_cards is not None else []
order_detail = OrderDetailResponse(
id=order.id,
status=order.status.value,
created_at=order.created_at.isoformat(),
updated_at=order.updated_at.isoformat(),
final_order_url=order.final_order_url,
failure_reason=order.failure_reason,
user_data_id=order.user_data_id,
links_id=order.links_id,
user_data=order.user_data,
links=order.links,
gift_cards=gift_cards
)
logger.info(f"获取订单详情成功: {order_id}")
return order
return order_detail
except HTTPException:
raise

View File

@@ -14,7 +14,15 @@ from app.core.redis_manager import redis_manager
from app.core.log import get_logger
from app.core.state_manager import task_state_manager
from app.services.task_service import TaskService
from app.schemas.task import GiftCardSubmissionRequest, GiftCardSubmissionResponse, QueueStatsResponse, TaskListResponse, TaskControlRequest, TaskControlResponse, TaskStateResponse
from app.schemas.task import (
GiftCardSubmissionRequest,
GiftCardSubmissionResponse,
QueueStatsResponse,
TaskListResponse,
TaskControlRequest,
TaskControlResponse,
TaskStateResponse,
)
logger = get_logger(__name__)
router = APIRouter()
@@ -85,8 +93,9 @@ async def get_task_status() -> TaskStateResponse:
detail=f"获取任务状态失败: {str(e)}",
)
@router.get("/list", response_model=TaskListResponse)
async def get_task_list( db: AsyncSession = Depends(get_async_db)) -> TaskListResponse:
async def get_task_list(db: AsyncSession = Depends(get_async_db)) -> TaskListResponse:
"""
获取任务列表
@@ -105,6 +114,7 @@ async def get_task_list( db: AsyncSession = Depends(get_async_db)) -> TaskListRe
detail=f"获取任务列表失败: {str(e)}",
)
@router.get("/queue/stats", summary="获取队列统计", response_model=QueueStatsResponse)
async def get_queue_stats() -> QueueStatsResponse:
"""获取Celery队列统计信息"""
@@ -146,7 +156,9 @@ async def get_queue_stats() -> QueueStatsResponse:
@router.post("/submit", response_model=GiftCardSubmissionResponse)
async def submit_gift_card(request: GiftCardSubmissionRequest, db: AsyncSession = Depends(get_async_db)):
async def submit_gift_card(
request: GiftCardSubmissionRequest, db: AsyncSession = Depends(get_async_db)
):
"""
提交礼品卡信息并更新任务状态
@@ -162,14 +174,10 @@ async def submit_gift_card(request: GiftCardSubmissionRequest, db: AsyncSession
return await task_service.submit_gift_card(request)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except RuntimeError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
except Exception as e:
logger.error(f"提交礼品卡失败: {e}")
@@ -177,4 +185,3 @@ async def submit_gift_card(request: GiftCardSubmissionRequest, db: AsyncSession
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"提交礼品卡失败: {str(e)}",
)

View File

@@ -42,6 +42,7 @@ celery_app.autodiscover_tasks(["app.tasks"])
# 显式导入任务模块以确保任务被注册
try:
from app.tasks import crawler_tasks
logger.info("成功导入 crawler_tasks 模块")
except ImportError as e:
logger.error(f"导入 crawler_tasks 模块失败: {e}")

View File

@@ -72,8 +72,6 @@ class LogContext:
if exc_type:
get_logger().bind(**self.context).exception(
f"Context exception: {exc_val}",
exc_type=exc_type.__name__,
exc_message=str(exc_val),
)
def get_enriched_context(self) -> Dict[str, Any]:

View File

@@ -4,6 +4,7 @@ FastAPI中间件模块
"""
import time
import traceback
import uuid
from typing import Callable, Awaitable, Optional
@@ -40,13 +41,13 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
# 使用日志上下文
with LogContext(request_id=request_id):
# 记录请求开始
logger.info(
"请求开始",
method=request.method,
url=str(request.url),
client_ip=request.client.host if request.client else "unknown",
user_agent=request.headers.get("user-agent", "unknown"),
)
# logger.info(
# "请求开始",
# method=str(request.method),
# url=str(request.url),
# client_ip=str(request.client.host) if request.client else "unknown",
# user_agent=str(request.headers.get("user-agent", "unknown")),
# )
try:
# 执行请求
@@ -72,7 +73,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
# 记录请求失败
logger.error(
"请求失败",
error=str(e),
error=traceback.format_exc(),
process_time=process_time,
exc_info=True,
)

View File

@@ -259,7 +259,7 @@ class RedisManager:
is_paused, _ = await self.get_task_pause_state()
return is_paused
# 保存user_data_id
# 保存user_data_id
async def save_user_data_id(self, user_data_id: str) -> bool:
"""保存user_data_id"""
try:

View File

@@ -131,14 +131,6 @@ def create_app() -> FastAPI:
redoc_url="/redoc" if settings.DEBUG else None,
openapi_url="/openapi.json" if settings.DEBUG else None,
lifespan=lifespan,
contact={
"name": "Apple Exchange Team",
"email": "team@apple-exchange.com",
},
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT",
},
servers=[
{
"url": f"http://{settings.HOST}:{settings.PORT}",
@@ -238,8 +230,8 @@ def setup_middleware(app: FastAPI):
# 设置自定义中间件(日志、安全头等)
setup_custom_middleware(app)
# 设置 OpenTelemetry 中间件
add_metrics_middleware(app)
# # 设置 OpenTelemetry 中间件
# add_metrics_middleware(app)
# 添加API日志记录中间件
add_api_logging_middleware(app)

View File

@@ -33,9 +33,7 @@ class GiftCards(BaseModel):
card_code: Mapped[str] = mapped_column(
String(255), nullable=False, comment="礼品卡号码"
)
card_value: Mapped[float] = mapped_column(
nullable=False, comment="卡片面额"
)
card_value: Mapped[float] = mapped_column(nullable=False, comment="卡片面额")
status: Mapped[GiftCardStatus] = mapped_column(
Enum(GiftCardStatus),
default=GiftCardStatus.PENDING,

View File

@@ -25,9 +25,7 @@ class Links(BaseModel):
amount: Mapped[float] = mapped_column(Float, nullable=False, comment="金额")
# 关系映射
orders: Mapped[list["Orders"]] = relationship(
"Orders", back_populates="links"
)
orders: Mapped[list["Orders"]] = relationship("Orders", back_populates="links")
def __repr__(self) -> str:
return f"<Links(id={self.id}, url={self.url}, amount={self.amount})>"

View File

@@ -54,9 +54,7 @@ class Orders(BaseModel):
)
# 关系映射
user_data: Mapped["UserData"] = relationship(
"UserData", back_populates="orders"
)
user_data: Mapped["UserData"] = relationship("UserData", back_populates="orders")
links: Mapped["Links"] = relationship("Links", back_populates="orders")
gift_cards: Mapped["list[GiftCards]"] = relationship(
"GiftCards", back_populates="order", cascade="all, delete-orphan"

View File

@@ -35,9 +35,7 @@ class UserData(BaseModel):
phone: Mapped[str] = mapped_column(String(50), nullable=False, comment="电话号码")
# 关系映射
orders: Mapped[list["Orders"]] = relationship(
"Orders", back_populates="user_data"
)
orders: Mapped[list["Orders"]] = relationship("Orders", back_populates="user_data")
def __repr__(self) -> str:
return (

View File

@@ -56,7 +56,10 @@ class BaseRepository(Generic[ModelType]):
return instance
async def get_by_id(
self, id_: str, relations: list[str] | None = None, include_deleted: bool = False
self,
id_: str,
relations: list[str] | None = None,
include_deleted: bool = False,
) -> ModelType | None:
"""
根据ID获取记录
@@ -186,7 +189,10 @@ class BaseRepository(Generic[ModelType]):
return list(result.scalars().all())
async def get_one_by_filter(
self, relations: list[str] | None = None, include_deleted: bool = False, **filters
self,
relations: list[str] | None = None,
include_deleted: bool = False,
**filters,
) -> ModelType | None:
"""
根据条件获取单条记录
@@ -484,7 +490,11 @@ class BaseRepository(Generic[ModelType]):
# 获取数据
items = await self.get_by_filter(
relations=relations, order_by=order_by, desc=desc, include_deleted=include_deleted, **filters
relations=relations,
order_by=order_by,
desc=desc,
include_deleted=include_deleted,
**filters,
)
# 应用分页
@@ -551,7 +561,9 @@ class BaseRepository(Generic[ModelType]):
await self.db_session.commit()
return result.rowcount > 0
else:
raise ValueError(f"Model {self.model.__name__} does not support soft delete")
raise ValueError(
f"Model {self.model.__name__} does not support soft delete"
)
async def restore_by_id(self, id: str) -> bool:
"""
@@ -572,7 +584,9 @@ class BaseRepository(Generic[ModelType]):
await self.db_session.commit()
return result.rowcount > 0
else:
raise ValueError(f"Model {self.model.__name__} does not support soft delete")
raise ValueError(
f"Model {self.model.__name__} does not support soft delete"
)
async def get_deleted(
self,
@@ -600,10 +614,12 @@ class BaseRepository(Generic[ModelType]):
skip=skip,
limit=limit,
relations=relations,
**filters
**filters,
)
else:
raise ValueError(f"Model {self.model.__name__} does not support soft delete")
raise ValueError(
f"Model {self.model.__name__} does not support soft delete"
)
# ==================== 自定义查询构建器 ====================

View File

@@ -48,7 +48,9 @@ class TaskRepository(BaseRepository):
if order:
# 手动加载关联数据(如果需要)
if order.user_data_id:
user_query = select(UserData).where(UserData.id == order.user_data_id)
user_query = select(UserData).where(
UserData.id == order.user_data_id
)
user_result = await self.db_session.execute(user_query)
order.user_data = user_result.scalar_one_or_none()
@@ -58,7 +60,9 @@ class TaskRepository(BaseRepository):
order.links = link_result.scalar_one_or_none()
# 获取礼品卡信息
gift_card_query = select(GiftCards).where(GiftCards.order_result_id == order_id)
gift_card_query = select(GiftCards).where(
GiftCards.order_result_id == order_id
)
gift_card_result = await self.db_session.execute(gift_card_query)
order.gift_cards = gift_card_result.scalars().all()

View File

@@ -8,7 +8,6 @@ from .order import (
UploadUrlRequest,
UploadUrlResponse,
UserInfoResponse,
GiftCardResponse,
)
from .task import (
ProcessOrderRequest,
@@ -16,6 +15,7 @@ from .task import (
TaskStatusResponse,
WorkerStatusResponse,
QueueStatsResponse,
GiftCardResponse,
)
from .link import (
LinkCreate,

View File

@@ -2,6 +2,7 @@
链接相关的Pydantic模型
"""
from datetime import datetime
from pydantic import BaseModel, Field, ConfigDict
@@ -34,6 +35,19 @@ class LinkResponse(LinkBase):
model_config = ConfigDict(from_attributes=True)
@classmethod
def from_orm(cls, obj):
"""Custom ORM conversion to handle datetime serialization"""
data = {}
for field in cls.model_fields:
value = getattr(obj, field, None)
if isinstance(value, datetime):
data[field] = value.isoformat()
else:
data[field] = value
return cls(**data)
class LinkListResponse(BaseModel):
"""链接列表响应模型"""

View File

@@ -3,7 +3,10 @@
"""
from pydantic import BaseModel, Field, ConfigDict
from app.models.giftcards import GiftCardStatus
from app.models.orders import OrderResultStatus
from app.schemas.link import LinkResponse
from app.schemas.gift_card import GiftCardInfoResponse
from app.schemas.user_data import UserInfoResponse
class OrderStatsResponse(BaseModel):
@@ -17,49 +20,28 @@ class OrderStatsResponse(BaseModel):
last_update: str
class UserInfoResponse(BaseModel):
"""用户信息响应 - 与数据表字段完全一致"""
class OrderDetailResponse(BaseModel):
"""订单详情响应 - 与数据库结构完全一致"""
id: str = Field(..., description="用户数据唯一标识")
first_name: str = Field(..., description="名字")
last_name: str = Field(..., description="姓氏")
email: str = Field(..., description="邮箱地址")
phone: str = Field(..., description="电话号码")
street_address: str = Field(..., description="街道地址")
city: str = Field(..., description="城市")
state: str = Field(..., description="州/省")
zip_code: str = Field(..., description="邮政编码")
id: str = Field(..., description="订单ID")
status: OrderResultStatus = Field(..., description="订单状态")
created_at: str = Field(..., description="创建时间")
updated_at: str = Field(..., description="更新时间")
final_order_url: str | None = Field(None, description="最终订单URL")
failure_reason: str | None = Field(None, description="失败原因")
user_data_id: str = Field(..., description="用户数据ID")
links_id: str = Field(..., description="链接ID")
# 关联关系
user_data: UserInfoResponse = Field(description="用户数据")
links: LinkResponse = Field(description="链接信息")
gift_cards: list[GiftCardInfoResponse] = Field(
default_factory=list, description="礼品卡列表"
)
model_config = ConfigDict(from_attributes=True)
class GiftCardResponse(BaseModel):
"""礼品卡响应模型 - 与数据表字段完全一致"""
id: str = Field(..., description="礼品卡ID")
card_code: str = Field(..., description="礼品卡号码")
status: GiftCardStatus = Field(..., description="礼品卡状态")
failure_reason: str | None = Field(None, description="失败原因")
order_result_id: str = Field(..., description="订单结果ID")
created_at: str = Field(..., description="创建时间")
updated_at: str = Field(..., description="更新时间")
class OrderDetailResponse(BaseModel):
"""订单详情响应"""
id: str
status: str
order_number: str | None
final_order_url: str | None
failure_reason: str | None
created_at: str
user_info: UserInfoResponse
gift_cards: list[GiftCardResponse]
class UploadUrlRequest(BaseModel):
"""上传URL请求"""

View File

@@ -2,6 +2,7 @@
用户数据相关的Pydantic模型
"""
from datetime import datetime
from pydantic import BaseModel, Field, field_validator, ConfigDict
@@ -96,6 +97,25 @@ class UserInfoResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
@classmethod
def from_orm(cls, obj):
"""Custom ORM conversion to handle datetime serialization"""
data = {}
for field in cls.model_fields:
value = getattr(obj, field, None)
if isinstance(value, datetime):
data[field] = value.isoformat()
else:
data[field] = value
# Add computed fields
if hasattr(obj, 'first_name') and hasattr(obj, 'last_name'):
data['full_name'] = f"{obj.first_name} {obj.last_name}".strip()
if hasattr(obj, 'street_address') and hasattr(obj, 'city') and hasattr(obj, 'state') and hasattr(obj, 'zip_code'):
data['full_address'] = f"{obj.street_address}, {obj.city}, {obj.state} {obj.zip_code}".strip(", ")
return cls(**data)
class UserDataStatsResponse(BaseModel):
"""用户数据统计响应模型"""

View File

@@ -15,9 +15,6 @@ from app.repositories.order_repository import OrderRepository
from app.repositories.link_repository import LinkRepository
from app.schemas.order import (
OrderStatsResponse,
OrderDetailResponse,
UserInfoResponse,
GiftCardResponse,
)
logger = get_logger(__name__)
@@ -51,19 +48,15 @@ class OrderService:
skip: int = 0,
limit: int = 100,
status_filter: OrderResultStatus | None = None,
) -> list[OrderDetailResponse]:
"""获取订单列表"""
orders = await self.order_repo.get_orders_with_relations(
) -> list[Orders]:
"""获取订单列表(包含所有关联数据)"""
return await self.order_repo.get_orders_with_relations(
skip, limit, status_filter
)
return [self._convert_order_to_response(order) for order in orders]
async def get_order_detail(self, order_id: str) -> OrderDetailResponse | None:
"""获取订单详情"""
order = await self.order_repo.get_order_with_full_details(order_id)
if not order:
return None
return self._convert_order_to_response(order)
async def get_order_detail(self, order_id: str) -> Orders | None:
"""获取订单详情(包含所有关联数据)"""
return await self.order_repo.get_order_with_full_details(order_id)
async def export_orders_to_excel(
self, status_filter: str | None = None
@@ -151,108 +144,12 @@ class OrderService:
return excel_content, filename
@staticmethod
def _convert_order_to_response(order: Orders) -> OrderDetailResponse:
"""将订单模型转换为响应模型"""
# 处理用户信息
if hasattr(order, "user_data") and order.user_data:
user_data = order.user_data
address_parts = []
if hasattr(user_data, "street_address") and user_data.street_address:
address_parts.append(user_data.street_address)
if hasattr(user_data, "city") and user_data.city:
address_parts.append(user_data.city)
if hasattr(user_data, "state") and user_data.state:
address_parts.append(user_data.state)
if hasattr(user_data, "zip_code") and user_data.zip_code:
address_parts.append(user_data.zip_code)
user_info = UserInfoResponse(
id=str(getattr(user_data, "id", 0)),
first_name=getattr(user_data, "first_name", ""),
last_name=getattr(user_data, "last_name", ""),
email=getattr(user_data, "email", ""),
phone=getattr(user_data, "phone", ""),
street_address=getattr(user_data, "street_address", ""),
city=getattr(user_data, "city", ""),
state=getattr(user_data, "state", ""),
zip_code=getattr(user_data, "zip_code", ""),
created_at=(
getattr(user_data, "created_at", datetime.now()).isoformat()
if hasattr(user_data, "created_at") and user_data.created_at
else datetime.now().isoformat()
),
updated_at=(
getattr(user_data, "updated_at", datetime.now()).isoformat()
if hasattr(user_data, "updated_at") and user_data.updated_at
else datetime.now().isoformat()
),
)
else:
# 当没有用户数据时创建空的UserInfoResponse对象
user_info = UserInfoResponse(
id="0",
first_name="",
last_name="",
email="",
phone="",
street_address="",
city="",
state="",
zip_code="",
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat(),
)
# 处理礼品卡信息
gift_cards = []
if hasattr(order, "gift_cards") and order.gift_cards:
for gc in order.gift_cards:
gc_status = gc.status
status_value = (
gc_status.value if hasattr(gc_status, "value") else "unknown"
)
gift_cards.append(
GiftCardResponse(
id=str(getattr(gc, "id", 0)),
card_code=getattr(gc, "card_code", ""),
status=status_value,
failure_reason=getattr(gc, "failure_reason", ""),
order_result_id=getattr(gc, "order_result_id", ""),
created_at=(
getattr(gc, "created_at", "").isoformat()
if hasattr(getattr(gc, "created_at", ""), "isoformat")
else str(getattr(gc, "created_at", ""))
),
updated_at=(
getattr(gc, "updated_at", "").isoformat()
if hasattr(getattr(gc, "updated_at", ""), "isoformat")
else str(getattr(gc, "updated_at", ""))
),
)
)
return OrderDetailResponse(
id=order.id,
status=order.status.value,
order_number=getattr(order, "order_number", ""),
final_order_url=getattr(order, "final_order_url", ""),
failure_reason=getattr(order, "failure_reason", ""),
created_at=order.created_at.isoformat(),
user_info=user_info,
gift_cards=gift_cards,
)
# 订单处理相关方法
async def get_pending_orders(self) -> list[Orders]:
"""获取pending状态的订单"""
from sqlalchemy import select
query = (
select(Orders)
.where(Orders.status == OrderResultStatus.PENDING)
)
query = select(Orders).where(Orders.status == OrderResultStatus.PENDING)
result = await self.db.execute(query)
return list(result.scalars().all())
@@ -290,7 +187,7 @@ class OrderService:
order = await self.order_repo.create(
user_data_id=user_data_id,
links_id=links_id,
status=OrderResultStatus.PENDING
status=OrderResultStatus.PENDING,
)
logger.info(f"成功创建订单: {order.id}, 用户数据ID: {user_data_id}")
return order.id

View File

@@ -407,9 +407,7 @@ class AppleOrderProcessor:
# 获取礼品卡信息
async with db_manager.get_async_session() as db:
gift_card_service = GiftCardService(db)
card_code = await gift_card_service.get_card_code(
self.order_id
)
card_code = await gift_card_service.get_card_code(self.order_id)
if card_code:
return card_code

View File

@@ -17,7 +17,7 @@ from app.schemas.task import (
TaskLinkInfo,
TaskCardInfo,
GiftCardSubmissionRequest,
GiftCardSubmissionResponse
GiftCardSubmissionResponse,
)
from app.repositories.task_repository import TaskRepository
from app.enums.task import OrderTaskStatus
@@ -41,8 +41,8 @@ class TaskService:
"""
try:
# 获取所有任务状态
all_tasks: list[TaskState] = await task_state_manager.state_manager.list_states(
StateType.TASK
all_tasks: list[TaskState] = (
await task_state_manager.state_manager.list_states(StateType.TASK)
)
task_list = []
@@ -60,7 +60,7 @@ class TaskService:
updated_at=task.updated_at,
completed_at=task.completed_at,
failed_at=task.failed_at,
error_message=task.error_message
error_message=task.error_message,
)
# 如果有订单ID获取关联的用户、链接和礼品卡信息
@@ -75,14 +75,16 @@ class TaskService:
success=True,
tasks=task_list,
total=len(task_list),
message="获取任务列表成功"
message="获取任务列表成功",
)
except Exception as e:
logger.error(f"获取任务列表失败: {traceback.format_exc()}")
raise
async def _enrich_task_with_order_data(self, task_item: TaskListItem, order_id: str) -> None:
async def _enrich_task_with_order_data(
self, task_item: TaskListItem, order_id: str
) -> None:
"""
使用订单数据丰富任务信息
@@ -107,16 +109,14 @@ class TaskService:
street_address=user_data.street_address,
city=user_data.city,
state=user_data.state,
zip_code=user_data.zip_code
zip_code=user_data.zip_code,
)
# 获取链接信息
if order.links:
link = order.links
task_item.link_info = TaskLinkInfo(
url=link.url,
amount=link.amount,
code=link.id # 使用链接ID作为代码
url=link.url, amount=link.amount, code=link.id # 使用链接ID作为代码
)
# 获取礼品卡信息
@@ -125,13 +125,15 @@ class TaskService:
task_item.card_info = TaskCardInfo(
card_number=gift_card.card_code,
status=gift_card.status.value,
failure_reason=gift_card.failure_reason
failure_reason=gift_card.failure_reason,
)
except Exception as e:
logger.error(f"获取订单关联数据失败: {e}")
async def submit_gift_card(self, request: GiftCardSubmissionRequest) -> GiftCardSubmissionResponse:
async def submit_gift_card(
self, request: GiftCardSubmissionRequest
) -> GiftCardSubmissionResponse:
"""
提交礼品卡信息并更新任务状态
@@ -149,25 +151,25 @@ class TaskService:
# 更新任务结果,包含礼品卡信息
updated_result = task.result.copy() if task.result else {}
updated_result.update({
"card_code": request.card_code,
"card_value": request.card_value,
"submitted_at": datetime.now().isoformat()
})
updated_result.update(
{
"card_code": request.card_code,
"card_value": request.card_value,
"submitted_at": datetime.now().isoformat(),
}
)
# 更新任务状态为已接收礼品卡
success = await task_state_manager.update_task_state(
task_id=request.task_id,
status=OrderTaskStatus.GIFT_CARD_RECEIVED,
result=updated_result
result=updated_result,
)
if success:
logger.info(f"礼品卡提交成功任务ID: {request.task_id}")
return GiftCardSubmissionResponse(
success=True,
task_id=request.task_id,
message="礼品卡提交成功"
success=True, task_id=request.task_id, message="礼品卡提交成功"
)
else:
raise RuntimeError("更新任务状态失败")
@@ -192,20 +194,28 @@ class TaskService:
return None
# 获取任务基础信息
task_dict = task.model_dump() if hasattr(task, 'model_dump') else task.dict() if hasattr(task, 'dict') else vars(task)
task_dict = (
task.model_dump()
if hasattr(task, "model_dump")
else task.dict() if hasattr(task, "dict") else vars(task)
)
# 创建任务列表项
task_item = TaskListItem(
task_id=task_dict.get('identifier', ''),
worker_id=task_dict.get('worker_id'),
order_id=task_dict.get('result', {}).get('order_id') if task_dict.get('result') else None,
status=task_dict.get('status', ''),
progress=task_dict.get('progress', 0.0),
created_at=task_dict.get('created_at', ''),
updated_at=task_dict.get('updated_at', ''),
completed_at=task_dict.get('completed_at'),
failed_at=task_dict.get('failed_at'),
error_message=task_dict.get('error_message')
task_id=task_dict.get("identifier", ""),
worker_id=task_dict.get("worker_id"),
order_id=(
task_dict.get("result", {}).get("order_id")
if task_dict.get("result")
else None
),
status=task_dict.get("status", ""),
progress=task_dict.get("progress", 0.0),
created_at=task_dict.get("created_at", ""),
updated_at=task_dict.get("updated_at", ""),
completed_at=task_dict.get("completed_at"),
failed_at=task_dict.get("failed_at"),
error_message=task_dict.get("error_message"),
)
# 如果有订单ID获取关联的用户、链接和礼品卡信息
@@ -236,20 +246,28 @@ class TaskService:
filtered_tasks = []
for task in all_tasks:
task_dict = task.model_dump() if hasattr(task, 'model_dump') else task.dict() if hasattr(task, 'dict') else vars(task)
task_dict = (
task.model_dump()
if hasattr(task, "model_dump")
else task.dict() if hasattr(task, "dict") else vars(task)
)
if task_dict.get('status') == status:
if task_dict.get("status") == status:
task_item = TaskListItem(
task_id=task_dict.get('identifier', ''),
worker_id=task_dict.get('worker_id'),
order_id=task_dict.get('result', {}).get('order_id') if task_dict.get('result') else None,
status=task_dict.get('status', ''),
progress=task_dict.get('progress', 0.0),
created_at=task_dict.get('created_at', ''),
updated_at=task_dict.get('updated_at', ''),
completed_at=task_dict.get('completed_at'),
failed_at=task_dict.get('failed_at'),
error_message=task_dict.get('error_message')
task_id=task_dict.get("identifier", ""),
worker_id=task_dict.get("worker_id"),
order_id=(
task_dict.get("result", {}).get("order_id")
if task_dict.get("result")
else None
),
status=task_dict.get("status", ""),
progress=task_dict.get("progress", 0.0),
created_at=task_dict.get("created_at", ""),
updated_at=task_dict.get("updated_at", ""),
completed_at=task_dict.get("completed_at"),
failed_at=task_dict.get("failed_at"),
error_message=task_dict.get("error_message"),
)
# 如果有订单ID获取关联的用户、链接和礼品卡信息

View File

@@ -16,7 +16,8 @@ from app.schemas.user_data import (
UserDataListResponse,
UserDataUploadResponse,
UserDataStatsResponse,
UserInfoResponse, UserDataBase,
UserInfoResponse,
UserDataBase,
)
from app.models.user_data import UserData
from app.models.orders import OrderResultStatus

View File

@@ -5,6 +5,7 @@
"""
import asyncio
import time
from typing import Any
from datetime import datetime
@@ -45,9 +46,7 @@ def process_apple_order(
task_id = getattr(current_task.request, "id", "unknown")
worker_id = order_id
logger.info(
f"开始处理Apple订单: task_id={task_id}, worker_id={worker_id}"
)
logger.info(f"开始处理Apple订单: task_id={task_id}, worker_id={worker_id}")
# 使用asyncio运行异步任务
loop = asyncio.new_event_loop()
@@ -71,6 +70,8 @@ async def _process_apple_order_async(
if await redis_manager.is_task_paused():
is_paused, reason = await redis_manager.get_task_pause_state()
logger.info(f"任务已暂停,跳过订单处理: {order_id}, 原因: {reason}")
time.sleep(10) # 等待一段时间以防止频繁检查
process_apple_order.retry(countdown=60) # 1分钟后重试
return {
"success": False,
"is_paused": True,
@@ -173,15 +174,12 @@ def batch_process_orders():
loop.close()
async def _batch_process_orders_async():
"""异步批量处理订单"""
# 检查任务是否暂停
if await redis_manager.is_task_paused():
is_paused, reason = await redis_manager.get_task_pause_state()
logger.info(f"任务已暂停,跳过批量处理: {reason}")
return None
# 获取数据库会话
@@ -208,14 +206,14 @@ async def _batch_process_orders_async():
try:
# 开始创建订单
order_id = await order_service.create_order(user_data_id, link_info.link.id)
order_id = await order_service.create_order(
user_data_id, link_info.link.id
)
process_apple_order.delay(order_id)
except Exception as e:
logger.error(f"创建订单失败: {e}")
# @celery_app.task(name="app.tasks.crawler_tasks.recover_stalled_orders")
# def recover_stalled_orders() -> dict[str, Any]:
# """

View File

@@ -1,5 +0,0 @@
#!/bin/bash
echo "Starting service..."
pwd
ls -la
exec "$@"

View File

@@ -55,56 +55,69 @@ reload = False
statsd_host = None
statsd_prefix = ""
# 工作进程启动时的钩子函数
def on_starting(server):
"""工作进程启动前的钩子函数"""
pass
def on_reload(server):
"""重载时的钩子函数"""
pass
def when_ready(server):
"""服务器准备就绪时的钩子函数"""
pass
def on_exit(server):
"""服务器退出时的钩子函数"""
pass
# 工作进程相关的钩子函数
def pre_fork(server, worker):
"""工作进程fork前的钩子函数"""
pass
def post_fork(server, worker):
"""工作进程fork后的钩子函数"""
pass
def post_worker_init(worker):
"""工作进程初始化后的钩子函数"""
pass
def worker_int(worker):
"""工作进程收到中断信号时的钩子函数"""
pass
def worker_abort(worker):
"""工作进程异常终止时的钩子函数"""
pass
def child_exit(server, worker):
"""子进程退出时的钩子函数"""
pass
def pre_exec(server):
"""工作进程执行前的钩子函数"""
pass
def pre_request(worker, req):
"""请求处理前的钩子函数"""
pass
def post_request(worker, req, environ, resp):
"""请求处理后的钩子函数"""
pass

View File

@@ -19,21 +19,25 @@ worker_connections = 1000
timeout = 30
keepalive = 2
def test_imports():
"""测试必要的模块导入"""
try:
import uvicorn
import gunicorn
print("✅ Uvicorn和Gunicorn导入成功")
return True
except ImportError as e:
print(f"❌ 模块导入失败: {e}")
return False
def test_celery():
"""测试Celery应用"""
try:
from app.core.celery_app import get_celery_app
app = get_celery_app()
print("✅ Celery应用创建成功")
return True
@@ -41,10 +45,12 @@ def test_celery():
print(f"❌ Celery应用创建失败: {e}")
return False
def test_database():
"""测试数据库连接"""
try:
from app.core.database import get_database
db = get_database()
print("✅ 数据库连接配置成功")
return True
@@ -52,6 +58,7 @@ def test_database():
print(f"❌ 数据库配置失败: {e}")
return False
def main():
"""主测试函数"""
print("🚀 开始测试Docker容器环境...")
@@ -84,5 +91,6 @@ def main():
print("⚠️ 有测试失败,请检查配置。")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -10,6 +10,7 @@ from pathlib import Path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
def test_task_registration():
"""测试任务注册"""
try:
@@ -25,13 +26,13 @@ def test_task_registration():
print(f"已注册的任务数量: {len(registered_tasks)}")
print("已注册的任务:")
for task_name in sorted(registered_tasks):
if not task_name.startswith('celery.'):
if not task_name.startswith("celery."):
print(f"{task_name}")
# 检查特定任务
target_tasks = [
'app.tasks.crawler_tasks.batch_process_orders',
'app.tasks.crawler_tasks.process_apple_order'
"app.tasks.crawler_tasks.batch_process_orders",
"app.tasks.crawler_tasks.process_apple_order",
]
print("\n检查目标任务:")
@@ -48,9 +49,11 @@ def test_task_registration():
except Exception as e:
print(f"❌ 任务注册测试失败: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = test_task_registration()
sys.exit(0 if success else 1)

329
deploy/SWARM_GUIDE.md Normal file
View File

@@ -0,0 +1,329 @@
# Docker Swarm 部署指南
## 概述
本指南说明如何使用 Docker Swarm 部署 Apple Gift Card Exchange Platform。Docker Swarm 是 Docker 原生的容器编排工具,适用于中小规模的生产环境部署。
## 快速开始
### 1. 环境准备
确保系统已安装以下软件:
- Docker (版本 20.10+)
- Docker Compose (版本 1.29+)
### 2. 初始化 Swarm 集群
#### Linux/macOS
```bash
cd deploy
chmod +x swarm-init.sh
./swarm-init.sh
```
#### Windows
```cmd
cd deploy
swarm-init.bat
```
### 3. 配置环境变量
编辑 `deploy/.env` 文件:
```env
POSTGRES_PASSWORD=your_secure_password_here
LETSENCRYPT_EMAIL=your_email@example.com
FLOWER_AUTH=admin:$(openssl passwd -crypt admin)
```
## 详细部署步骤
### 步骤 1: 初始化 Docker Swarm
如果 Docker Swarm 未激活,运行初始化脚本:
```bash
# Linux/macOS
./swarm-init.sh init
# Windows
swarm-init.bat init
```
### 步骤 2: 构建镜像
构建所有必要的 Docker 镜像:
```bash
# Linux/macOS
./swarm-init.sh build
# Windows
swarm-init.bat build
```
### 步骤 3: 部署应用
部署整个应用到 Swarm 集群:
```bash
# Linux/macOS
./swarm-init.sh deploy
# Windows
swarm-init.bat deploy
```
## 服务管理
### 查看服务状态
```bash
# 查看所有服务
docker stack services apple-exchange
# 查看特定服务
docker service ps apple-exchange_frontend
docker service ps apple-exchange_api
docker service ps apple-exchange_worker
```
### 查看日志
```bash
# 查看服务日志
docker service logs apple-exchange_api
docker service logs apple-exchange_frontend
docker service logs apple-exchange_worker
# 使用脚本
./swarm-init.sh logs api
./swarm-init.sh logs frontend
```
### 扩展服务
```bash
# 扩展前端服务
docker service scale apple-exchange_frontend=3
# 扩展 API 服务
docker service scale apple-exchange_api=2
# 扩展 Worker 服务
docker service scale apple-exchange_worker=5
```
### 更新服务
```bash
# 重新部署所有服务
docker stack deploy -c deploy/docker-compose.swarm.yml apple-exchange
# 更新单个服务
docker service update --image apple-exchange-api:latest apple-exchange_api
```
### 移除部署
```bash
# 移除整个 stack
docker stack rm apple-exchange
# 使用脚本
./swarm-init.sh remove
```
## 服务架构
### 核心服务
1. **frontend** - Next.js 前端应用
- 副本数: 2
- 端口: 8080 (内部)
- 限制: 0.5 CPU, 512MB 内存
2. **api** - FastAPI 后端服务
- 副本数: 2
- 端口: 8000 (内部)
- 限制: 1 CPU, 1GB 内存
3. **worker** - Celery 工作进程
- 副本数: 3
- 限制: 2 CPU, 2GB 内存
- 部署约束: 仅在 worker 节点运行
4. **beat** - Celery 定时任务调度器
- 副本数: 1
- 部署约束: 仅在 manager 节点运行
### 基础设施服务
5. **db** - PostgreSQL 数据库
- 副本数: 1
- 部署约束: 仅在 manager 节点运行
- 数据持久化
6. **redis** - Redis 缓存和消息代理
- 副本数: 1
- 部署约束: 仅在 manager 节点运行
- 数据持久化
7. **traefik** - 反向代理和负载均衡器
- 副本数: 1
- 部署约束: 仅在 manager 节点运行
- 自动 HTTPS 证书管理
### 监控服务
8. **flower** - Celery 任务监控
- 副本数: 1
- 基本认证保护
- 可选服务
## 网络和存储
### 网络配置
- **app-network**: Overlay 网络,用于服务间通信
- **端口映射**:
- 80, 443: Traefik 入口
- 8080: Traefik Dashboard
- 5432: PostgreSQL (可选)
- 6379: Redis (可选)
### 存储卷
- **postgres_data**: PostgreSQL 数据
- **redis_data**: Redis 数据
- **shared_storage**: 共享存储
- **playwright_browsers**: Playwright 浏览器缓存
- **logs**: 应用日志
- **data**: 应用数据
- **screenshots**: 截图存储
- **traefik_letsencrypt**: Let's Encrypt 证书
## 访问地址
部署完成后,可以通过以下地址访问服务:
- **主应用**: https://apple-exchange.com
- **API**: https://api.apple-exchange.com
- **Flower 监控**: https://flower.apple-exchange.com
- **Traefik Dashboard**: https://traefik.apple-exchange.com
## 高级配置
### 节点标签
为优化部署,可以为节点添加标签:
```bash
# 为数据库节点添加标签
docker node update --label-add db=true node-name
# 为缓存节点添加标签
docker node update --label-add redis=true node-name
# 为工作节点添加标签
docker node update --label-add worker=true node-name
```
### 资源限制
根据服务器配置调整资源限制:
```yaml
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 2G
```
### 健康检查
所有服务都配置了健康检查,确保服务正常运行:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
## 故障排除
### 常见问题
1. **Docker Swarm 未激活**
```bash
docker swarm init
```
2. **镜像构建失败**
```bash
docker build -t apple-exchange-api:latest ../backend
```
3. **服务启动失败**
```bash
docker service logs apple-exchange_api
```
4. **网络问题**
```bash
docker network ls
docker network inspect apple-exchange_app-network
```
### 调试命令
```bash
# 查看集群状态
docker info
# 查看节点状态
docker node ls
# 查看服务详情
docker service inspect apple-exchange_api
# 进入容器调试
docker exec -it $(docker ps -q -f "name=apple-exchange-api") bash
```
## 生产环境建议
### 安全性
1. 使用强密码和环境变量
2. 启用 HTTPS 和 Let's Encrypt
3. 限制 Traefik Dashboard 访问
4. 定期更新基础镜像
### 监控
1. 启用 Flower 监控
2. 配置日志收集
3. 设置告警规则
4. 监控资源使用情况
### 备份
1. 定期备份数据库
2. 备份 Redis 数据
3. 备份配置文件
4. 测试恢复流程
### 扩展性
1. 水平扩展服务副本
2. 使用多节点集群
3. 配置负载均衡
4. 优化资源使用

View File

@@ -1,5 +1,5 @@
# Apple Gift Card Exchange Platform - Combined Docker Compose
# 包含前端和后端的完整部署配置
# Apple Gift Card Exchange Platform - Docker Swarm Stack Configuration
# 适用于 Docker Swarm 生产环境部署
services:
# ===== 前端服务 =====
@@ -7,14 +7,10 @@ services:
build:
context: ../frontend
dockerfile: Dockerfile
container_name: apple-exchange-frontend
ports:
- "3000:8080"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_ENV=production
- NEXT_PUBLIC_API_URL=http://api:8000/api
- NEXT_PUBLIC_TELEMETRY_ENDPOINT=http://otel-collector:4318/v1/traces
networks:
- app-network
depends_on:
@@ -27,11 +23,14 @@ services:
start_period: 40s
deploy:
mode: replicated
replicas: 1
replicas: 2
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
rollback_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
@@ -44,17 +43,18 @@ services:
reservations:
cpus: '0.2'
memory: 256M
ports:
- "8080:8080"
# ===== 后端 API 服务 =====
api:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-api
environment:
- SERVICE_TYPE=api
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- DATABASE_URL=postgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
@@ -66,8 +66,6 @@ services:
- data:/app/data
- screenshots:/app/screenshots
- shared_storage:/app/shared
ports:
- "8000:8000"
networks:
- app-network
depends_on:
@@ -81,7 +79,7 @@ services:
start_period: 40s
deploy:
mode: replicated
replicas: 1
replicas: 2
update_config:
parallelism: 1
delay: 10s
@@ -101,17 +99,18 @@ services:
reservations:
cpus: '0.5'
memory: 512M
ports:
- "8000:8000"
# ===== Celery Worker 服务 =====
worker:
build:
context: ../backend
dockerfile: Dockerfile.worker
container_name: apple-exchange-worker
environment:
- SERVICE_TYPE=worker
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- DATABASE_URL=postgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
@@ -140,7 +139,7 @@ services:
start_period: 60s
deploy:
mode: replicated
replicas: 1
replicas: 3
update_config:
parallelism: 2
delay: 10s
@@ -160,17 +159,19 @@ services:
reservations:
cpus: '1.0'
memory: 1G
placement:
constraints:
- node.role == worker
# ===== Celery Beat 调度服务 =====
beat:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-beat
environment:
- SERVICE_TYPE=beat
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- DATABASE_URL=postgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
@@ -207,19 +208,19 @@ services:
reservations:
cpus: '0.2'
memory: 256M
placement:
constraints:
- node.role == manager
# ===== PostgreSQL 数据库 =====
db:
image: postgres:17-alpine
container_name: apple-exchange-db
environment:
- POSTGRES_DB=apple_exchange
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_PASSWORD=ajNHYrqSNaTdPFq3
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- app-network
healthcheck:
@@ -246,17 +247,16 @@ services:
reservations:
cpus: '0.5'
memory: 512M
placement:
constraints:
- node.role == manager
- node.labels.db == true
# ===== Redis 缓存和消息代理 =====
redis:
image: redis:7-alpine
container_name: apple-exchange-redis
volumes:
- redis_data:/data
- ./deploy/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
ports:
- "6379:6379"
networks:
- app-network
healthcheck:
@@ -283,39 +283,10 @@ services:
reservations:
cpus: '0.2'
memory: 256M
# ===== 可选Celery Flower 监控服务 =====
flower:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-flower
environment:
- SERVICE_TYPE=flower
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
ports:
- "5555:5555"
networks:
- app-network
depends_on:
- db
- redis
profiles:
- monitoring
deploy:
mode: replicated
replicas: 1
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.2'
memory: 256M
placement:
constraints:
- node.role == manager
- node.labels.redis == true
# ===== 数据卷 =====
@@ -345,8 +316,6 @@ volumes:
# ===== 网络 =====
networks:
app-network:
driver: bridge
driver: overlay
name: apple-exchange-network
ipam:
config:
- subnet: 172.20.0.0/16
attachable: true

218
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,218 @@
# Apple Gift Card Exchange Platform - Combined Docker Compose
# 包含前端和后端的完整部署配置
services:
# ===== 前端服务 =====
frontend:
build:
context: ../frontend
dockerfile: Dockerfile
container_name: apple-exchange-frontend
ports:
- "3000:8080"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_ENV=production
networks:
- app-network
depends_on:
- api
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# ===== 后端 API 服务 =====
api:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-api
environment:
- SERVICE_TYPE=api
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
- WORKERS=4
- SCREENSHOT_DIR=/app/screenshots
- LOG_DIR=/app/logs
volumes:
- logs:/app/logs
- data:/app/data
- screenshots:/app/screenshots
- shared_storage:/app/shared
networks:
- app-network
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health/liveness"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# ===== Celery Worker 服务 =====
worker:
build:
context: ../backend
dockerfile: Dockerfile.worker
container_name: apple-exchange-worker
environment:
- SERVICE_TYPE=worker
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
- CELERY_CONCURRENCY=2
- CELERY_MAX_TASKS_PER_CHILD=1000
- CELERY_PREFETCH_MULTIPLIER=1
- SCREENSHOT_DIR=/app/screenshots
- LOG_DIR=/app/logs
- PLAYWRIGHT_BROWSERS_PATH=/app/playwright-browsers
volumes:
- logs:/app/logs
- data:/app/data
- screenshots:/app/screenshots
- shared_storage:/app/shared
- playwright_browsers:/app/playwright-browsers
networks:
- app-network
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "python", "-c", "from app.core.celery_app import get_celery_app; app = get_celery_app(); print('Worker healthy')"]
interval: 60s
timeout: 30s
retries: 3
start_period: 60s
# ===== Celery Beat 调度服务 =====
beat:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-beat
environment:
- SERVICE_TYPE=beat
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
volumes:
- logs:/app/logs
- data:/app/data
networks:
- app-network
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ===== PostgreSQL 数据库 =====
db:
image: postgres:17-alpine
container_name: apple-exchange-db
environment:
- POSTGRES_DB=apple_exchange
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5444:5432"
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# ===== Redis 缓存和消息代理 =====
redis:
image: redis:7-alpine
container_name: apple-exchange-redis
volumes:
- redis_data:/data
- ./deploy/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
ports:
- "6379:6379"
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ===== 可选Celery Flower 监控服务 =====
flower:
build:
context: ../backend
dockerfile: Dockerfile
container_name: apple-exchange-flower
environment:
- SERVICE_TYPE=flower
- ENVIRONMENT=production
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/apple_exchange
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
ports:
- "5555:5555"
networks:
- app-network
depends_on:
- db
- redis
profiles:
- monitoring
# ===== 数据卷 =====
volumes:
postgres_data:
driver: local
name: apple-exchange-postgres-data
redis_data:
driver: local
name: apple-exchange-redis-data
shared_storage:
driver: local
name: apple-exchange-shared-storage
playwright_browsers:
driver: local
name: apple-exchange-playwright-browsers
logs:
driver: local
name: apple-exchange-logs
data:
driver: local
name: apple-exchange-data
screenshots:
driver: local
name: apple-exchange-screenshots
# ===== 网络 =====
networks:
app-network:
driver: bridge
name: apple-exchange-network
ipam:
config:
- subnet: 172.20.0.0/16

View File

@@ -1,42 +0,0 @@
# Redis configuration for Apple Exchange Platform
# Basic settings
port 6379
bind 0.0.0.0
# Memory management
maxmemory 512mb
maxmemory-policy allkeys-lru
# Persistence
save 900 1
save 300 10
save 60 10000
# Security
protected-mode no
# Logging
loglevel notice
# Network
timeout 0
tcp-keepalive 300
# Clients
maxclients 10000
# Advanced config
rdbcompression yes
rdbchecksum yes
stop-writes-on-bgsave-error yes
# Lua scripting
lua-time-limit 5000
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Latency monitoring
latency-monitor-threshold 100

352
deploy/swarm-init.bat Normal file
View File

@@ -0,0 +1,352 @@
@echo off
REM Apple Gift Card Exchange Platform - Docker Swarm 初始化脚本 (Windows)
REM 用于初始化 Docker Swarm 集群和部署应用
setlocal enabledelayedexpansion
REM 配置变量
set STACK_NAME=apple-exchange
set DEPLOY_DIR=%~dp0
set COMPOSE_FILE=%DEPLOY_DIR%docker-compose.swarm.yml
REM 颜色定义 (Windows 10+)
for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j
if "%VERSION%" geq "10.0" (
set "RED=[91m"
set "GREEN=[92m"
set "YELLOW=[93m"
set "BLUE=[94m"
set "NC=[0m"
) else (
set "RED="
set "GREEN="
set "YELLOW="
set "BLUE="
set "NC="
)
REM 日志函数
:log_info
echo %BLUE%[INFO]%NC% %~1
goto :eof
:log_success
echo %GREEN%[SUCCESS]%NC% %~1
goto :eof
:log_warning
echo %YELLOW%[WARNING]%NC% %~1
goto :eof
:log_error
echo %RED%[ERROR]%NC% %~1
goto :eof
REM 检查 Docker 是否运行
:check_docker
call :log_info "检查 Docker 运行状态..."
docker info >nul 2>&1
if errorlevel 1 (
call :log_error "Docker 未运行,请启动 Docker 后重试"
exit /b 1
)
call :log_success "Docker 运行正常"
goto :eof
REM 检查 Docker Swarm 是否已初始化
:check_swarm
call :log_info "检查 Docker Swarm 状态..."
docker info | findstr /C:"Swarm: active" >nul
if errorlevel 1 (
call :log_warning "Docker Swarm 未激活"
exit /b 1
) else (
call :log_success "Docker Swarm 已激活"
exit /b 0
)
goto :eof
REM 初始化 Docker Swarm
:init_swarm
call :log_info "初始化 Docker Swarm..."
docker swarm init
if errorlevel 1 (
call :log_error "Docker Swarm 初始化失败"
exit /b 1
)
call :log_success "Docker Swarm 初始化成功"
REM 添加节点标签
call :log_info "添加节点标签..."
for /f "delims=" %%i in ('docker node ls --filter role=manager -q') do (
docker node update --label-add db=true %%i >nul 2>&1
docker node update --label-add redis=true %%i >nul 2>&1
)
call :log_success "节点标签设置完成"
goto :eof
REM 创建必要的配置文件
:create_configs
call :log_info "创建 Docker 配置..."
REM 创建 redis.conf
echo # Redis 配置文件> "%DEPLOY_DIR%redis.conf"
echo bind 0.0.0.0>> "%DEPLOY_DIR%redis.conf"
echo port 6379>> "%DEPLOY_DIR%redis.conf"
echo protected-mode no>> "%DEPLOY_DIR%redis.conf"
echo timeout 0>> "%DEPLOY_DIR%redis.conf"
echo tcp-keepalive 300>> "%DEPLOY_DIR%redis.conf"
echo daemonize no>> "%DEPLOY_DIR%redis.conf"
echo supervised no>> "%DEPLOY_DIR%redis.conf"
echo loglevel notice>> "%DEPLOY_DIR%redis.conf"
echo logfile "">> "%DEPLOY_DIR%redis.conf"
echo databases 16>> "%DEPLOY_DIR%redis.conf"
echo always-show-logo yes>> "%DEPLOY_DIR%redis.conf"
echo save 900 1>> "%DEPLOY_DIR%redis.conf"
echo save 300 10>> "%DEPLOY_DIR%redis.conf"
echo save 60 10000>> "%DEPLOY_DIR%redis.conf"
echo stop-writes-on-bgsave-error yes>> "%DEPLOY_DIR%redis.conf"
echo rdbcompression yes>> "%DEPLOY_DIR%redis.conf"
echo rdbchecksum yes>> "%DEPLOY_DIR%redis.conf"
echo dbfilename dump.rdb>> "%DEPLOY_DIR%redis.conf"
echo dir /data>> "%DEPLOY_DIR%redis.conf"
echo replica-serve-stale-data yes>> "%DEPLOY_DIR%redis.conf"
echo replica-read-only yes>> "%DEPLOY_DIR%redis.conf"
echo repl-diskless-sync no>> "%DEPLOY_DIR%redis.conf"
echo repl-diskless-sync-delay 5>> "%DEPLOY_DIR%redis.conf"
echo repl-ping-replica-period 10>> "%DEPLOY_DIR%redis.conf"
echo repl-timeout 60>> "%DEPLOY_DIR%redis.conf"
echo repl-disable-tcp-nodelay no>> "%DEPLOY_DIR%redis.conf"
echo repl-backlog-size 1mb>> "%DEPLOY_DIR%redis.conf"
echo repl-backlog-ttl 3600>> "%DEPLOY_DIR%redis.conf"
echo replica-priority 100>> "%DEPLOY_DIR%redis.conf"
echo maxmemory-policy allkeys-lru>> "%DEPLOY_DIR%redis.conf"
echo lazyfree-lazy-eviction no>> "%DEPLOY_DIR%redis.conf"
echo lazyfree-lazy-expire no>> "%DEPLOY_DIR%redis.conf"
echo lazyfree-lazy-server-del no>> "%DEPLOY_DIR%redis.conf"
echo replica-lazy-flush no>> "%DEPLOY_DIR%redis.conf"
echo appendonly yes>> "%DEPLOY_DIR%redis.conf"
echo appendfilename "appendonly.aof">> "%DEPLOY_DIR%redis.conf"
echo appendfsync everysec>> "%DEPLOY_DIR%redis.conf"
echo no-appendfsync-on-rewrite no>> "%DEPLOY_DIR%redis.conf"
echo auto-aof-rewrite-percentage 100>> "%DEPLOY_DIR%redis.conf"
echo auto-aof-rewrite-min-size 64mb>> "%DEPLOY_DIR%redis.conf"
echo aof-load-truncated yes>> "%DEPLOY_DIR%redis.conf"
echo aof-use-rdb-preamble yes>> "%DEPLOY_DIR%redis.conf"
echo lua-time-limit 5000>> "%DEPLOY_DIR%redis.conf"
echo slowlog-log-slower-than 10000>> "%DEPLOY_DIR%redis.conf"
echo slowlog-max-len 128>> "%DEPLOY_DIR%redis.conf"
echo latency-monitor-threshold 0>> "%DEPLOY_DIR%redis.conf"
echo notify-keyspace-events "">> "%DEPLOY_DIR%redis.conf"
echo hash-max-ziplist-entries 512>> "%DEPLOY_DIR%redis.conf"
echo hash-max-ziplist-value 64>> "%DEPLOY_DIR%redis.conf"
echo list-max-ziplist-size -2>> "%DEPLOY_DIR%redis.conf"
echo list-compress-depth 0>> "%DEPLOY_DIR%redis.conf"
echo set-max-intset-entries 512>> "%DEPLOY_DIR%redis.conf"
echo zset-max-ziplist-entries 128>> "%DEPLOY_DIR%redis.conf"
echo zset-max-ziplist-value 64>> "%DEPLOY_DIR%redis.conf"
echo hll-sparse-max-bytes 3000>> "%DEPLOY_DIR%redis.conf"
echo stream-node-max-bytes 4096>> "%DEPLOY_DIR%redis.conf"
echo stream-node-max-entries 100>> "%DEPLOY_DIR%redis.conf"
echo activerehashing yes>> "%DEPLOY_DIR%redis.conf"
echo client-output-buffer-limit normal 0 0 0>> "%DEPLOY_DIR%redis.conf"
echo client-output-buffer-limit replica 256mb 64mb 60>> "%DEPLOY_DIR%redis.conf"
echo client-output-buffer-limit pubsub 32mb 8mb 60>> "%DEPLOY_DIR%redis.conf"
echo hz 10>> "%DEPLOY_DIR%redis.conf"
echo dynamic-hz yes>> "%DEPLOY_DIR%redis.conf"
echo aof-rewrite-incremental-fsync yes>> "%DEPLOY_DIR%redis.conf"
echo rdb-save-incremental-fsync yes>> "%DEPLOY_DIR%redis.conf"
REM 创建 postgresql.conf
echo # PostgreSQL 配置文件> "%DEPLOY_DIR%postgresql.conf"
echo listen_addresses = '*'>> "%DEPLOY_DIR%postgresql.conf"
echo port = 5432>> "%DEPLOY_DIR%postgresql.conf"
echo max_connections = 200>> "%DEPLOY_DIR%postgresql.conf"
echo shared_buffers = 256MB>> "%DEPLOY_DIR%postgresql.conf"
echo effective_cache_size = 1GB>> "%DEPLOY_DIR%postgresql.conf"
echo maintenance_work_mem = 64MB>> "%DEPLOY_DIR%postgresql.conf"
echo checkpoint_completion_target = 0.9>> "%DEPLOY_DIR%postgresql.conf"
echo wal_buffers = 16MB>> "%DEPLOY_DIR%postgresql.conf"
echo default_statistics_target = 100>> "%DEPLOY_DIR%postgresql.conf"
echo random_page_cost = 1.1>> "%DEPLOY_DIR%postgresql.conf"
echo effective_io_concurrency = 200>> "%DEPLOY_DIR%postgresql.conf"
echo work_mem = 4MB>> "%DEPLOY_DIR%postgresql.conf"
echo min_wal_size = 1GB>> "%DEPLOY_DIR%postgresql.conf"
echo max_wal_size = 4GB>> "%DEPLOY_DIR%postgresql.conf"
echo max_worker_processes = 4>> "%DEPLOY_DIR%postgresql.conf"
echo max_parallel_workers_per_gather = 2>> "%DEPLOY_DIR%postgresql.conf"
echo max_parallel_workers = 4>> "%DEPLOY_DIR%postgresql.conf"
echo max_parallel_maintenance_workers = 2>> "%DEPLOY_DIR%postgresql.conf"
echo log_line_prefix = '%%t [%%p]: [%%l-1] user=%%u,db=%%d,app=%%a,client=%%h '>> "%DEPLOY_DIR%postgresql.conf"
echo log_timezone = 'UTC'>> "%DEPLOY_DIR%postgresql.conf"
echo datestyle = 'iso, mdy'>> "%DEPLOY_DIR%postgresql.conf"
echo timezone = 'UTC'>> "%DEPLOY_DIR%postgresql.conf"
echo default_text_search_config = 'pg_catalog.english'>> "%DEPLOY_DIR%postgresql.conf"
REM 创建 .env 文件模板
echo # 环境变量配置> "%DEPLOY_DIR%.env.template"
echo POSTGRES_PASSWORD=your_secure_password_here>> "%DEPLOY_DIR%.env.template"
echo LETSENCRYPT_EMAIL=your_email@example.com>> "%DEPLOY_DIR%.env.template"
echo FLOWER_AUTH=admin:$(openssl passwd -crypt admin)>> "%DEPLOY_DIR%.env.template"
call :log_success "配置文件创建完成"
goto :eof
REM 构建镜像
:build_images
call :log_info "构建 Docker 镜像..."
REM 构建前端镜像
if exist "..\frontend" (
call :log_info "构建前端镜像..."
docker build -t apple-exchange-frontend:latest ..\frontend
if errorlevel 1 (
call :log_error "前端镜像构建失败"
exit /b 1
)
call :log_success "前端镜像构建完成"
)
REM 构建后端镜像
if exist "..\backend" (
call :log_info "构建后端镜像..."
docker build -t apple-exchange-api:latest ..\backend
if errorlevel 1 (
call :log_error "后端镜像构建失败"
exit /b 1
)
REM 构建 Worker 镜像
if exist "..\backend\Dockerfile.worker" (
docker build -f ..\backend\Dockerfile.worker -t apple-exchange-worker:latest ..\backend
if errorlevel 1 (
call :log_error "Worker 镜像构建失败"
exit /b 1
)
) else (
REM 如果没有专门的 Dockerfile.worker使用 API 镜像
docker tag apple-exchange-api:latest apple-exchange-worker:latest
)
call :log_success "后端镜像构建完成"
)
goto :eof
REM 部署 Stack
:deploy_stack
call :log_info "部署 Docker Stack: %STACK_NAME%"
REM 检查环境变量文件
if not exist "%DEPLOY_DIR%.env" (
call :log_warning "环境变量文件 .env 不存在,使用模板创建"
copy "%DEPLOY_DIR%.env.template" "%DEPLOY_DIR%.env" >nul
call :log_warning "请编辑 .env 文件设置正确的环境变量"
exit /b 1
)
REM 部署 Stack
docker stack deploy -c "%COMPOSE_FILE%" %STACK_NAME%
if errorlevel 1 (
call :log_error "Stack 部署失败"
exit /b 1
)
call :log_success "Stack 部署成功"
goto :eof
REM 显示部署状态
:show_status
call :log_info "部署状态:"
echo ----------------------------------------
docker stack services %STACK_NAME%
echo ----------------------------------------
call :log_info "查看服务状态:"
echo docker stack services %STACK_NAME%
echo docker service ps %STACK_NAME%_frontend
echo docker service ps %STACK_NAME%_api
echo docker service ps %STACK_NAME%_worker
echo docker service ps %STACK_NAME%_db
echo docker service ps %STACK_NAME%_redis
call :log_info "查看日志:"
echo docker service logs %STACK_NAME%_frontend
echo docker service logs %STACK_NAME%_api
echo docker service logs %STACK_NAME%_worker
call :log_info "访问地址:"
echo 应用: https://apple-exchange.com
echo API: https://api.apple-exchange.com
echo Flower 监控: https://flower.apple-exchange.com
echo Traefik Dashboard: https://traefik.apple-exchange.com
goto :eof
REM 主函数
:main
call :log_info "开始 Docker Swarm 部署..."
REM 检查必要的文件
if not exist "%COMPOSE_FILE%" (
call :log_error "Docker Compose 文件不存在: %COMPOSE_FILE%"
exit /b 1
)
REM 检查 Docker
call :check_docker
REM 检查 Swarm
call :check_swarm
if errorlevel 1 (
call :init_swarm
)
REM 创建配置文件
call :create_configs
REM 构建镜像
call :build_images
REM 部署 Stack
call :deploy_stack
REM 显示状态
call :show_status
call :log_success "Docker Swarm 部署完成!"
goto :eof
REM 脚本选项
if "%1"=="" goto main
if "%1%"=="init" (
call :check_docker
call :check_swarm
if errorlevel 1 (
call :init_swarm
)
goto :eof
)
if "%1%"=="build" (
call :build_images
goto :eof
)
if "%1%"=="deploy" (
call :deploy_stack
goto :eof
)
if "%1%"=="status" (
call :show_status
goto :eof
)
if "%1%"=="remove" (
call :log_info "移除 Stack: %STACK_NAME%"
docker stack rm %STACK_NAME%
goto :eof
)
if "%1%"=="logs" (
if "%2%"=="" (
docker service logs %STACK_NAME%_api
) else (
docker service logs %STACK_NAME%_%2%
)
goto :eof
)
goto main

346
deploy/swarm-init.sh Normal file
View File

@@ -0,0 +1,346 @@
#!/bin/bash
# Apple Gift Card Exchange Platform - Docker Swarm 初始化脚本
# 用于初始化 Docker Swarm 集群和部署应用
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 配置变量
STACK_NAME="apple-exchange"
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.swarm.yml"
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查 Docker 是否运行
check_docker() {
log_info "检查 Docker 运行状态..."
if ! docker info >/dev/null 2>&1; then
log_error "Docker 未运行,请启动 Docker 后重试"
exit 1
fi
log_success "Docker 运行正常"
}
# 检查 Docker Swarm 是否已初始化
check_swarm() {
log_info "检查 Docker Swarm 状态..."
if docker info | grep -q "Swarm: active"; then
log_success "Docker Swarm 已激活"
return 0
else
log_warning "Docker Swarm 未激活"
return 1
fi
}
# 初始化 Docker Swarm
init_swarm() {
log_info "初始化 Docker Swarm..."
# 获取当前主机 IP
local advertise_addr=$(ip route get 8.8.8.8 | awk '{print $7; exit}')
if docker swarm init --advertise-addr "${advertise_addr}"; then
log_success "Docker Swarm 初始化成功"
# 添加节点标签
log_info "添加节点标签..."
docker node update --label-add db=true $(docker node ls --filter role=manager -q) >/dev/null 2>&1 || true
docker node update --label-add redis=true $(docker node ls --filter role=manager -q) >/dev/null 2>&1 || true
log_success "节点标签设置完成"
else
log_error "Docker Swarm 初始化失败"
exit 1
fi
}
# 创建必要的配置文件
create_configs() {
log_info "创建 Docker 配置..."
# 创建 redis.conf
cat > "${DEPLOY_DIR}/redis.conf" << 'EOF'
# Redis 配置文件
bind 0.0.0.0
port 6379
protected-mode no
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-ping-replica-period 10
repl-timeout 60
repl-disable-tcp-nodelay no
repl-backlog-size 1mb
repl-backlog-ttl 3600
replica-priority 100
maxmemory-policy allkeys-lru
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
EOF
# 创建 postgresql.conf
cat > "${DEPLOY_DIR}/postgresql.conf" << 'EOF'
# PostgreSQL 配置文件
listen_addresses = '*'
port = 5432
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 1GB
max_wal_size = 4GB
max_worker_processes = 4
max_parallel_workers_per_gather = 2
max_parallel_workers = 4
max_parallel_maintenance_workers = 2
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_timezone = 'UTC'
datestyle = 'iso, mdy'
timezone = 'UTC'
default_text_search_config = 'pg_catalog.english'
EOF
# 创建 .env 文件模板
cat > "${DEPLOY_DIR}/.env.template" << 'EOF'
# 环境变量配置
POSTGRES_PASSWORD=your_secure_password_here
LETSENCRYPT_EMAIL=your_email@example.com
FLOWER_AUTH=admin:$(openssl passwd -crypt admin)
EOF
log_success "配置文件创建完成"
}
# 构建镜像
build_images() {
log_info "构建 Docker 镜像..."
# 构建前端镜像
if [ -d "../frontend" ]; then
log_info "构建前端镜像..."
docker build -t apple-exchange-frontend:latest ../frontend || {
log_error "前端镜像构建失败"
exit 1
}
log_success "前端镜像构建完成"
fi
# 构建后端镜像
if [ -d "../backend" ]; then
log_info "构建后端镜像..."
docker build -t apple-exchange-api:latest ../backend || {
log_error "后端镜像构建失败"
exit 1
}
# 构建 Worker 镜像
if [ -f "../backend/Dockerfile.worker" ]; then
docker build -f ../backend/Dockerfile.worker -t apple-exchange-worker:latest ../backend || {
log_error "Worker 镜像构建失败"
exit 1
}
else
# 如果没有专门的 Dockerfile.worker使用 API 镜像
docker tag apple-exchange-api:latest apple-exchange-worker:latest
fi
log_success "后端镜像构建完成"
fi
}
# 部署 Stack
deploy_stack() {
log_info "部署 Docker Stack: ${STACK_NAME}"
# 检查环境变量文件
if [ ! -f "${DEPLOY_DIR}/.env" ]; then
log_warning "环境变量文件 .env 不存在,使用模板创建"
cp "${DEPLOY_DIR}/.env.template" "${DEPLOY_DIR}/.env"
log_warning "请编辑 .env 文件设置正确的环境变量"
exit 1
fi
# 部署 Stack
if docker stack deploy -c "${COMPOSE_FILE}" "${STACK_NAME}"; then
log_success "Stack 部署成功"
else
log_error "Stack 部署失败"
exit 1
fi
}
# 显示部署状态
show_status() {
log_info "部署状态:"
echo "----------------------------------------"
docker stack services "${STACK_NAME}"
echo "----------------------------------------"
log_info "查看服务状态:"
echo " docker stack services ${STACK_NAME}"
echo " docker service ps ${STACK_NAME}_frontend"
echo " docker service ps ${STACK_NAME}_api"
echo " docker service ps ${STACK_NAME}_worker"
echo " docker service ps ${STACK_NAME}_db"
echo " docker service ps ${STACK_NAME}_redis"
log_info "查看日志:"
echo " docker service logs ${STACK_NAME}_frontend"
echo " docker service logs ${STACK_NAME}_api"
echo " docker service logs ${STACK_NAME}_worker"
log_info "访问地址:"
echo " 应用: https://apple-exchange.com"
echo " API: https://api.apple-exchange.com"
echo " Flower 监控: https://flower.apple-exchange.com"
echo " Traefik Dashboard: https://traefik.apple-exchange.com"
}
# 主函数
main() {
log_info "开始 Docker Swarm 部署..."
# 检查必要的文件
if [ ! -f "${COMPOSE_FILE}" ]; then
log_error "Docker Compose 文件不存在: ${COMPOSE_FILE}"
exit 1
fi
# 检查 Docker
check_docker
# 检查 Swarm
if ! check_swarm; then
init_swarm
fi
# 创建配置文件
create_configs
# 构建镜像
build_images
# 部署 Stack
deploy_stack
# 显示状态
show_status
log_success "Docker Swarm 部署完成!"
}
# 脚本选项
case "${1:-}" in
"init")
check_docker
if ! check_swarm; then
init_swarm
fi
;;
"build")
build_images
;;
"deploy")
deploy_stack
;;
"status")
show_status
;;
"remove")
log_info "移除 Stack: ${STACK_NAME}"
docker stack rm "${STACK_NAME}"
;;
"logs")
shift
if [ $# -eq 0 ]; then
docker service logs "${STACK_NAME}_api"
else
docker service logs "${STACK_NAME}_${1}"
fi
;;
*)
main
;;
esac

View File

@@ -14,7 +14,8 @@
"Bash(docker rm:*)",
"Bash(docker stop:*)",
"Bash(rm:*)",
"Bash(docker:*)"
"Bash(docker:*)",
"Read(//e/**)"
],
"deny": [],
"ask": []

View File

@@ -31,11 +31,10 @@ tsconfig.tsbuildinfo
# Development files
.env
.env.*
!.env.example
.env.local
.env.development
.env.test
.env.production
!.env.production
# Docker and deployment files
deploy/

View File

@@ -20,6 +20,7 @@ RUN bun install
# Copy source code
COPY . .
COPY .env.production .env
# Build static export
RUN bun run build
@@ -27,6 +28,7 @@ RUN bun run build
# Stage 2: Nginx production stage
FROM nginx:alpine AS runner
# Install required system dependencies
RUN apk add --no-cache bash
@@ -65,7 +67,7 @@ server {
image/svg+xml;
# Static files caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)\$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
@@ -82,6 +84,14 @@ server {
add_header Cache-Control "public, immutable";
}
# 需要配置反向代理
location /api/ {
proxy_pass http://apple-exchange-api:8000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
# Handle static assets
location /static/ {
alias /usr/share/nginx/html/static/;

78
frontend/GEMINI.md Normal file
View File

@@ -0,0 +1,78 @@
# Gemini Project Context: Frontend for Apple Gift Card Exchange
## Project Overview
This is the frontend for the Apple Gift Card Exchange platform, also referred to as a "Crawler Monitoring System." It's a modern, responsive web application built with Next.js and TypeScript, designed to provide a real-time monitoring dashboard for backend processes. The UI is heavily inspired by Apple's Human Interface Guidelines (HIG), utilizing a custom-themed shadcn/ui component library, Tailwind CSS, and glass morphism effects.
* **Framework:** Next.js 14+ (with Turbopack)
* **Language:** TypeScript
* **Styling:** Tailwind CSS, Sass, and a custom Apple-style design system.
* **UI Components:** shadcn/ui, Radix UI, Headless UI
* **State Management:** TanStack Query (React Query) for server state, React Hooks for local state.
* **API Communication:** Axios client with API types and hooks generated by `orval` from an OpenAPI specification.
* **Code Quality:** ESLint for linting, Prettier for formatting.
## Building and Running
The project uses `bun` as the package manager.
1. **Install Dependencies:**
```bash
bun install
```
2. **Run Development Server:**
The development server runs on `http://localhost:3000`.
```bash
bun run dev
```
3. **Build for Production:**
This command builds the application for production usage.
```bash
bun run build
```
4. **Run Production Server:**
Starts the production server.
```bash
bun run start
```
5. **Linting:**
Run the linter to check for code quality issues.
```bash
bun run lint
```
6. **Generate API Client:**
The frontend's API client is generated from the backend's OpenAPI schema. If the backend API changes, run this command to update the client.
```bash
bun run generate:api
```
## Development Conventions
* **Styling:** Follow the Apple HIG design principles. Use Tailwind CSS utility classes and the existing component styles in `src/styles` and `src/components/ui`.
* **API Interaction:** Do not write API fetching code manually. Use the generated hooks from `src/lib/api/generated/`. If an endpoint is missing, regenerate the API client.
* **State Management:** Use TanStack Query for all data fetched from the server. Use component-level state (e.g., `useState`) for UI-related state.
* **Environment Variables:** Create a `.env.local` file from `env.example` for local development settings.
* **Components:** Reusable, low-level components are in `src/components/ui`. Feature-specific components are in `src/components/dashboard`, `src/components/forms`, etc.
## Key Files and Directories
* `src/app/`: Contains the pages and layouts for the application (App Router).
* `src/components/`: Contains all React components.
* `ui/`: General-purpose, reusable UI components (Button, Card, etc.).
* `dashboard/`: Components specific to the main monitoring dashboard.
* `layout/`: Components that define the page structure (Header, Footer, etc.).
* `src/lib/`: Core logic, utilities, and API communication.
* `api/generated/`: API client code generated by `orval`. **Do not edit manually.**
* `contexts/`: React contexts for shared state (e.g., theme).
* `hooks/`: Custom React hooks.
* `utils.ts`: General utility functions.
* `src/styles/`: Global styles and Sass files.
* `tailwind.config.js`: Configuration for the Tailwind CSS design system, including custom colors and effects.
* `next.config.js`: Configuration for the Next.js framework.
* `orval.config.ts`: Configuration for the `orval` API client generator.
* `GEMINI.md`: This file, containing the project context.

View File

@@ -80,15 +80,15 @@
"@asyncapi/specs": ["@asyncapi/specs@6.10.0", "https://registry.npmmirror.com/@asyncapi/specs/-/specs-6.10.0.tgz", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-vB5oKLsdrLUORIZ5BXortZTlVyGWWMC1Nud/0LtgxQ3Yn2738HigAD6EVqScvpPsDUI/bcLVsYEXN4dtXQHVng=="],
"@babel/runtime": ["@babel/runtime@7.28.2", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="],
"@babel/runtime": ["@babel/runtime@7.28.4", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@base-ui-components/react": ["@base-ui-components/react@1.0.0-beta.1", "https://registry.npmmirror.com/@base-ui-components/react/-/react-1.0.0-beta.1.tgz", { "dependencies": { "@babel/runtime": "^7.27.6", "@floating-ui/react-dom": "^2.1.3", "@floating-ui/utils": "^0.2.9", "reselect": "^5.1.1", "tabbable": "^6.2.0", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-7zmGiz4/+HKnv99lWftItoSMqnj2PdSvt2krh0/GP+Rj0xK0NMnFI/gIVvP7CB2G+k0JPUrRWXjXa3y08oiakg=="],
"@emnapi/core": ["@emnapi/core@1.4.5", "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.5.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
"@emnapi/core": ["@emnapi/core@1.5.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.5.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.5.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.5.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
@@ -142,7 +142,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
@@ -154,7 +154,7 @@
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.33.0", "https://registry.npmmirror.com/@eslint/js/-/js-9.33.0.tgz", {}, "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="],
"@eslint/js": ["@eslint/js@9.35.0", "https://registry.npmmirror.com/@eslint/js/-/js-9.35.0.tgz", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.6.tgz", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
@@ -164,23 +164,23 @@
"@floating-ui/core": ["@floating-ui/core@1.7.3", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.3", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.3.tgz", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/react": ["@floating-ui/react@0.26.28", "https://registry.npmmirror.com/@floating-ui/react/-/react-0.26.28.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.5", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.3" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.12.1", "https://registry.npmmirror.com/@gerrit0/mini-shiki/-/mini-shiki-3.12.1.tgz", { "dependencies": { "@shikijs/engine-oniguruma": "^3.12.1", "@shikijs/langs": "^3.12.1", "@shikijs/themes": "^3.12.1", "@shikijs/types": "^3.12.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-qA9/VGm7No0kxb7k0oKFT0DSJ6BtuMMtC7JQdZn9ElMALE9hjbyoaS13Y8OWr0qHwzh07KHt3Wbw34az/FLsrg=="],
"@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.12.2", "https://registry.npmmirror.com/@gerrit0/mini-shiki/-/mini-shiki-3.12.2.tgz", { "dependencies": { "@shikijs/engine-oniguruma": "^3.12.2", "@shikijs/langs": "^3.12.2", "@shikijs/themes": "^3.12.2", "@shikijs/types": "^3.12.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA=="],
"@headlessui/react": ["@headlessui/react@2.2.6", "https://registry.npmmirror.com/@headlessui/react/-/react-2.2.6.tgz", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-gN5CT8Kf4IWwL04GQOjZ/ZnHMFoeFHZmVSFoDKeTmbtmy9oFqQqJMthdBiO3Pl5LXk2w03fGFLpQV6EW84vjjQ=="],
"@headlessui/react": ["@headlessui/react@2.2.7", "https://registry.npmmirror.com/@headlessui/react/-/react-2.2.7.tgz", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg=="],
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "https://registry.npmmirror.com/@hookform/resolvers/-/resolvers-3.10.0.tgz", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.6.tgz", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
@@ -236,15 +236,15 @@
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "https://registry.npmmirror.com/@jsdevtools/ono/-/ono-7.1.3.tgz", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
@@ -256,25 +256,25 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@next/env": ["@next/env@15.4.6", "https://registry.npmmirror.com/@next/env/-/env-15.4.6.tgz", {}, "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ=="],
"@next/env": ["@next/env@15.5.3", "https://registry.npmmirror.com/@next/env/-/env-15.5.3.tgz", {}, "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.6", "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.3", "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.6", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.3", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.6", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.3", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.3", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.3", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.6", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.3", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.6", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.3", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.6", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.3", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.6", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", { "os": "win32", "cpu": "x64" }, "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.3", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -336,13 +336,13 @@
"@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.14", "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.14", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
@@ -350,9 +350,9 @@
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
@@ -360,77 +360,77 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.15", "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw=="],
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.7", "https://registry.npmmirror.com/@radix-ui/react-form/-/react-form-0.1.7.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IXLKFnaYvFg/KkeV5QfOX7tRnwHXp127koOFUjLWMTrRv5Rny3DQcAtIFFeA/Cli4HHM8DuJCXAUsgnFVJndlw=="],
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-form/-/react-form-0.1.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.14", "https://registry.npmmirror.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q=="],
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-menubar/-/react-menubar-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A=="],
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "https://registry.npmmirror.com/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.13", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g=="],
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.7", "https://registry.npmmirror.com/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.7.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-w1vm7AGI8tNXVovOK7TYQHrAGpRF7qQL+ENpT1a743De5Zmay2RbWGKAiYDKIyIuqptns+znCKwNztE2xl1n0Q=="],
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.2", "https://registry.npmmirror.com/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.2.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F90uYnlBsLPU1UbSLciLsWQmk8+hdWa6SFw4GXaIdNWxFxI5ITKVdAG64f+Twaa9ic6xE7pqxPyUmodrGjT4pQ=="],
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "https://registry.npmmirror.com/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.14", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.7", "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g=="],
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.9", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.5.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.5", "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw=="],
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.12", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg=="],
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.9", "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-toggle": "1.1.9", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ=="],
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jiwQsduEL++M4YBIurjSa+voD86OIytCod0/dbIxFZDLD8NfO1//keXYMfsW8BPcfqwoNjt+y06XcJqAb4KR7A=="],
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.7", "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@@ -454,19 +454,19 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@react-aria/focus": ["@react-aria/focus@3.21.0", "https://registry.npmmirror.com/@react-aria/focus/-/focus-3.21.0.tgz", { "dependencies": { "@react-aria/interactions": "^3.25.4", "@react-aria/utils": "^3.30.0", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA=="],
"@react-aria/focus": ["@react-aria/focus@3.21.1", "https://registry.npmmirror.com/@react-aria/focus/-/focus-3.21.1.tgz", { "dependencies": { "@react-aria/interactions": "^3.25.5", "@react-aria/utils": "^3.30.1", "@react-types/shared": "^3.32.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ=="],
"@react-aria/interactions": ["@react-aria/interactions@3.25.4", "https://registry.npmmirror.com/@react-aria/interactions/-/interactions-3.25.4.tgz", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.30.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg=="],
"@react-aria/interactions": ["@react-aria/interactions@3.25.5", "https://registry.npmmirror.com/@react-aria/interactions/-/interactions-3.25.5.tgz", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.30.1", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.32.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA=="],
"@react-aria/ssr": ["@react-aria/ssr@3.9.10", "https://registry.npmmirror.com/@react-aria/ssr/-/ssr-3.9.10.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="],
"@react-aria/utils": ["@react-aria/utils@3.30.0", "https://registry.npmmirror.com/@react-aria/utils/-/utils-3.30.0.tgz", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.10.8", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw=="],
"@react-aria/utils": ["@react-aria/utils@3.30.1", "https://registry.npmmirror.com/@react-aria/utils/-/utils-3.30.1.tgz", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.10.8", "@react-types/shared": "^3.32.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A=="],
"@react-stately/flags": ["@react-stately/flags@3.1.2", "https://registry.npmmirror.com/@react-stately/flags/-/flags-3.1.2.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="],
"@react-stately/utils": ["@react-stately/utils@3.10.8", "https://registry.npmmirror.com/@react-stately/utils/-/utils-3.10.8.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g=="],
"@react-types/shared": ["@react-types/shared@3.31.0", "https://registry.npmmirror.com/@react-types/shared/-/shared-3.31.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg=="],
"@react-types/shared": ["@react-types/shared@3.32.0", "https://registry.npmmirror.com/@react-types/shared/-/shared-3.32.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
@@ -516,13 +516,13 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tanstack/query-core": ["@tanstack/query-core@5.85.5", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.85.5.tgz", {}, "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w=="],
"@tanstack/query-core": ["@tanstack/query-core@5.87.4", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.87.4.tgz", {}, "sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.84.0", "https://registry.npmmirror.com/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", {}, "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.87.3", "https://registry.npmmirror.com/@tanstack/query-devtools/-/query-devtools-5.87.3.tgz", {}, "sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.85.5", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.85.5.tgz", { "dependencies": { "@tanstack/query-core": "5.85.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A=="],
"@tanstack/react-query": ["@tanstack/react-query@5.87.4", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.87.4.tgz", { "dependencies": { "@tanstack/query-core": "5.87.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.85.5", "https://registry.npmmirror.com/@tanstack/react-query-devtools/-/react-query-devtools-5.85.5.tgz", { "dependencies": { "@tanstack/query-devtools": "5.84.0" }, "peerDependencies": { "@tanstack/react-query": "^5.85.5", "react": "^18 || ^19" } }, "sha512-6Ol6Q+LxrCZlQR4NoI5181r+ptTwnlPG2t7H9Sp3klxTBhYGunONqcgBn2YKRPsaKiYM8pItpKMdMXMEINntMQ=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.87.4", "https://registry.npmmirror.com/@tanstack/react-query-devtools/-/react-query-devtools-5.87.4.tgz", { "dependencies": { "@tanstack/query-devtools": "5.87.3" }, "peerDependencies": { "@tanstack/react-query": "^5.87.4", "react": "^18 || ^19" } }, "sha512-JYcnVJBBW1DCPyNGM0S2CyrLpe6KFiL2gpYd/k9tAp62Du7+Y27zkzd+dKFyxpFadYaTxsx4kUA7YvnkMLVUoQ=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "https://registry.npmmirror.com/@tanstack/react-table/-/react-table-8.21.3.tgz", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
@@ -532,7 +532,7 @@
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.1.tgz", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
@@ -566,11 +566,11 @@
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@24.2.1", "https://registry.npmmirror.com/@types/node/-/node-24.2.1.tgz", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"@types/node": ["@types/node@24.3.1", "https://registry.npmmirror.com/@types/node/-/node-24.3.1.tgz", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
"@types/react": ["@types/react@18.3.23", "https://registry.npmmirror.com/@types/react/-/react-18.3.23.tgz", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="],
"@types/react": ["@types/react@18.3.24", "https://registry.npmmirror.com/@types/react/-/react-18.3.24.tgz", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A=="],
"@types/react-dom": ["@types/react-dom@18.3.7", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
@@ -578,25 +578,25 @@
"@types/urijs": ["@types/urijs@1.19.25", "https://registry.npmmirror.com/@types/urijs/-/urijs-1.19.25.tgz", {}, "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.39.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.43.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0" } }, "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.39.0.tgz", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.43.0.tgz", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.39.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.43.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -738,7 +738,7 @@
"axe-core": ["axe-core@4.10.3", "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
"axios": ["axios@1.11.0", "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="],
"axios": ["axios@1.12.0", "https://registry.npmmirror.com/axios/-/axios-1.12.0.tgz", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="],
"axobject-query": ["axobject-query@4.1.0", "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -750,7 +750,7 @@
"braces": ["braces@3.0.3", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.25.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.2.tgz", { "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA=="],
"browserslist": ["browserslist@4.25.4", "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.4.tgz", { "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg=="],
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
@@ -768,7 +768,7 @@
"camelcase-css": ["camelcase-css@2.0.1", "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001733", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", {}, "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q=="],
"caniuse-lite": ["caniuse-lite@1.0.30001741", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", {}, "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw=="],
"chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -872,7 +872,7 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.199", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", {}, "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.218", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", {}, "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg=="],
"emoji-regex": ["emoji-regex@9.2.2", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
@@ -912,9 +912,9 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.33.0", "https://registry.npmmirror.com/eslint/-/eslint-9.33.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.33.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA=="],
"eslint": ["eslint@9.35.0", "https://registry.npmmirror.com/eslint/-/eslint-9.35.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="],
"eslint-config-next": ["eslint-config-next@15.4.6", "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.4.6.tgz", { "dependencies": { "@next/eslint-plugin-next": "15.4.6", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA=="],
"eslint-config-next": ["eslint-config-next@15.5.3", "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.5.3.tgz", { "dependencies": { "@next/eslint-plugin-next": "15.5.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
@@ -972,7 +972,7 @@
"fastq": ["fastq@1.19.1", "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fdir": ["fdir@6.4.6", "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -1268,7 +1268,7 @@
"neo-async": ["neo-async@2.6.2", "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"next": ["next@15.4.6", "https://registry.npmmirror.com/next/-/next-15.4.6.tgz", { "dependencies": { "@next/env": "15.4.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.6", "@next/swc-darwin-x64": "15.4.6", "@next/swc-linux-arm64-gnu": "15.4.6", "@next/swc-linux-arm64-musl": "15.4.6", "@next/swc-linux-x64-gnu": "15.4.6", "@next/swc-linux-x64-musl": "15.4.6", "@next/swc-win32-arm64-msvc": "15.4.6", "@next/swc-win32-x64-msvc": "15.4.6", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ=="],
"next": ["next@15.5.3", "https://registry.npmmirror.com/next/-/next-15.5.3.tgz", { "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.3", "@next/swc-darwin-x64": "15.5.3", "@next/swc-linux-arm64-gnu": "15.5.3", "@next/swc-linux-arm64-musl": "15.5.3", "@next/swc-linux-x64-gnu": "15.5.3", "@next/swc-linux-x64-musl": "15.5.3", "@next/swc-win32-arm64-msvc": "15.5.3", "@next/swc-win32-x64-msvc": "15.5.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw=="],
"next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -1282,7 +1282,7 @@
"node-readfiles": ["node-readfiles@0.2.0", "https://registry.npmmirror.com/node-readfiles/-/node-readfiles-0.2.0.tgz", { "dependencies": { "es6-promise": "^3.2.1" } }, "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA=="],
"node-releases": ["node-releases@2.0.19", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"node-releases": ["node-releases@2.0.20", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.20.tgz", {}, "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA=="],
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@@ -1400,7 +1400,7 @@
"queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"radix-ui": ["radix-ui@1.4.2", "https://registry.npmmirror.com/radix-ui/-/radix-ui-1.4.2.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.11", "@radix-ui/react-alert-dialog": "1.1.14", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.15", "@radix-ui/react-dialog": "1.1.14", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-dropdown-menu": "2.1.15", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.7", "@radix-ui/react-hover-card": "1.1.14", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-menubar": "1.1.15", "@radix-ui/react-navigation-menu": "1.2.13", "@radix-ui/react-one-time-password-field": "0.1.7", "@radix-ui/react-password-toggle-field": "0.1.2", "@radix-ui/react-popover": "1.1.14", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.7", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-scroll-area": "1.2.9", "@radix-ui/react-select": "2.2.5", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.5", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.5", "@radix-ui/react-tabs": "1.1.12", "@radix-ui/react-toast": "1.2.14", "@radix-ui/react-toggle": "1.1.9", "@radix-ui/react-toggle-group": "1.1.10", "@radix-ui/react-toolbar": "1.1.10", "@radix-ui/react-tooltip": "1.2.7", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fT/3YFPJzf2WUpqDoQi005GS8EpCi+53VhcLaHUj5fwkPYiZAjk1mSxFvbMA8Uq71L03n+WysuYC+mlKkXxt/Q=="],
"radix-ui": ["radix-ui@1.4.3", "https://registry.npmmirror.com/radix-ui/-/radix-ui-1.4.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
"randombytes": ["randombytes@2.1.0", "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
@@ -1588,7 +1588,7 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.14", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -1692,13 +1692,11 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.3.1.tgz", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@ibm-cloud/openapi-ruleset/minimatch": ["minimatch@6.2.0", "https://registry.npmmirror.com/minimatch/-/minimatch-6.2.0.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
@@ -1802,9 +1800,9 @@
"@ibm-cloud/openapi-ruleset/minimatch/brace-expansion": ["brace-expansion@2.0.2", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

View File

@@ -5,8 +5,6 @@ NEXT_PUBLIC_USE_PROXY=true
# 目标API URL当使用代理时这是实际的目标服务器
NEXT_PUBLIC_TARGET_API_URL=http://your-test-api-server.com
# 直接API URL当不使用代理时
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
# 遥测端点
NEXT_PUBLIC_TELEMETRY_ENDPOINT=http://localhost:4318/v1/traces

View File

@@ -1,4 +1,4 @@
const ovalCOnfig = {
const orvalConfig = {
// 爬虫监控系统 API 配置
petstore: {
prettier: true,
@@ -66,4 +66,4 @@ const ovalCOnfig = {
},
};
export default ovalCOnfig;
export default orvalConfig;

View File

@@ -1,4 +1,5 @@
ignoredBuiltDependencies:
- '@parcel/watcher'
- sharp
- unrs-resolver

View File

@@ -1,9 +1,6 @@
"use client";
import { useState } from "react";
import { Trash2, ExternalLink, Calendar, DollarSign, Copy, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
// 导入 Tooltip 组件
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
@@ -77,10 +74,8 @@ export function LinkItem({ link, onDelete, isDeleting = false }: LinkItemProps)
{link.url}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-md break-all text-sm">
{link.url}
</div>
<TooltipContent className="max-w-md break-all rounded-2xl shadow-2xl border-0 bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl px-4 py-3 text-gray-900 dark:text-gray-100">
{link.url}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -1,15 +1,16 @@
"use client";
import { useState, useMemo } from "react";
import { RefreshCw, ShoppingCart, Calendar, User, Filter, ExternalLink } from "lucide-react";
import { RefreshCw, ShoppingCart, Calendar, User, Filter, ExternalLink, Info, Link as LinkIcon, CreditCard, AlertTriangle, Hash, Clock, CheckCircle, XCircle, Loader, Package } from "lucide-react";
import {
useGetOrdersApiV1OrdersListGet,
} from "@/lib/api/generated/order-management.gen";
import { OrderResultStatus, OrderDetailResponse } from "@/lib/api/generated/schemas";
import { AppleButton } from "@/components/ui/apple-button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
interface OrderListProps {
@@ -23,29 +24,179 @@ const statusConfig = {
pending: {
label: "待处理",
variant: "secondary" as const,
bgColor: "bg-gray-100 dark:bg-gray-800",
textColor: "text-gray-700 dark:text-gray-300",
icon: Clock,
color: "text-amber-600 dark:text-amber-400",
bgColor: "bg-amber-50 dark:bg-amber-900/20",
borderColor: "border-amber-200 dark:border-amber-700"
},
processing: {
label: "处理中",
variant: "processing" as const,
bgColor: "bg-yellow-100 dark:bg-yellow-900/30",
textColor: "text-yellow-700 dark:text-yellow-300",
icon: Loader,
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
borderColor: "border-blue-200 dark:border-blue-700"
},
success: {
label: "成功",
variant: "success" as const,
bgColor: "bg-green-100 dark:bg-green-900/30",
textColor: "text-green-700 dark:text-green-300",
icon: CheckCircle,
color: "text-green-600 dark:text-green-400",
bgColor: "bg-green-50 dark:bg-green-900/20",
borderColor: "border-green-200 dark:border-green-700"
},
failure: {
label: "失败",
variant: "failure" as const,
bgColor: "bg-red-100 dark:bg-red-900/30",
textColor: "text-red-700 dark:text-red-300",
icon: XCircle,
color: "text-red-600 dark:text-red-400",
bgColor: "bg-red-50 dark:bg-red-900/20",
borderColor: "border-red-200 dark:border-red-700"
},
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
};
const getStatusBadge = (status: string, className?: string) => {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.pending;
const Icon = config.icon;
const isProcessing = status === 'processing';
return (
<Badge
variant={config.variant}
className={cn(
"px-3 py-1.5 text-xs font-medium flex items-center gap-1.5 border transition-all duration-200 hover:shadow-sm",
config.color,
config.bgColor,
config.borderColor,
className
)}
>
<Icon className={cn("w-3 h-3", isProcessing && "animate-spin")} />
{config.label}
</Badge>
);
};
const getOrderItemStyle = (status: string) => {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.pending;
return cn(
"group relative p-4 pl-12 rounded-xl bg-white/50 dark:bg-gray-800/50 border-l-4 hover:bg-white/80 dark:hover:bg-gray-800/80 transition-all duration-200 cursor-pointer",
config.borderColor
);
};
// 模态框内容组件
const OrderDetailModal = ({ order, isOpen, onOpenChange }: { order: OrderDetailResponse | null, isOpen: boolean, onOpenChange: (open: boolean) => void }) => {
if (!order) return null;
const renderDetailItem = (label: string, value: React.ReactNode, fullWidth = false) => (
<div className={cn("flex flex-col gap-1", fullWidth ? "col-span-2" : "col-span-1")}>
<p className="text-xs text-gray-500 dark:text-gray-400">{label}</p>
<div className="text-sm font-medium text-gray-800 dark:text-gray-200 break-words">{value || "N/A"}</div>
</div>
);
const renderSection = (title: string, icon: React.ReactNode, children: React.ReactNode) => (
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-200/50 dark:border-gray-700/50">
<div className="flex items-center gap-2 mb-3">
{icon}
<h4 className="text-md font-semibold text-gray-800 dark:text-gray-200">{title}</h4>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{children}
</div>
</div>
);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="apple-glass-card max-w-4xl max-h-[90vh] overflow-y-auto rounded-2xl shadow-2xl border-0 bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
<DialogHeader className="p-6 pb-4">
<DialogTitle className="text-xl font-bold text-gray-900 dark:text-gray-100"></DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[calc(90vh-120px)] px-6 pb-6">
<div className="space-y-4">
{renderSection("基本信息", <Info className="w-5 h-5 text-blue-500" />,
<>
{renderDetailItem("状态", getStatusBadge(order.status))}
{renderDetailItem("创建时间", formatDate(order.created_at))}
{renderDetailItem("更新时间", formatDate(order.updated_at))}
{order.final_order_url && renderDetailItem("最终订单URL", order.final_order_url.toString(), true)}
</>
)}
{order.user_data && renderSection("用户信息", <User className="w-5 h-5 text-purple-500" />,
<>
{renderDetailItem("姓名", order.user_data.full_name)}
{renderDetailItem("邮箱", order.user_data.email)}
{renderDetailItem("电话", order.user_data.phone)}
{renderDetailItem("地址", order.user_data.full_address, true)}
{renderDetailItem("名字", order.user_data.first_name)}
{renderDetailItem("姓氏", order.user_data.last_name)}
{renderDetailItem("街道地址", order.user_data.street_address)}
{renderDetailItem("城市", order.user_data.city)}
{renderDetailItem("州/省", order.user_data.state)}
{renderDetailItem("邮政编码", order.user_data.zip_code)}
</>
)}
{order.links && renderSection("链接信息", <LinkIcon className="w-5 h-5 text-green-500" />,
<>
{renderDetailItem("链接URL", order.links.url, true)}
{renderDetailItem("金额", `¥${order.links.amount.toFixed(2)}`)}
{renderDetailItem("链接创建时间", formatDate(order.links.created_at))}
{renderDetailItem("链接更新时间", formatDate(order.links.updated_at))}
<div className="col-span-2">
<AppleButton onClick={() => window.open(order.links.url, '_blank')} size="sm">
<ExternalLink className="w-4 h-4 mr-2" />
</AppleButton>
</div>
</>
)}
{order.gift_cards && order.gift_cards.length > 0 && (
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-200/50 dark:border-gray-700/50">
<div className="flex items-center gap-2 mb-3">
<CreditCard className="w-5 h-5 text-orange-500" />
<h4 className="text-md font-semibold text-gray-800 dark:text-gray-200"> ({order.gift_cards.length})</h4>
</div>
<div className="space-y-3">
{order.gift_cards.map((card, index) => (
<div key={index} className="p-3 rounded-md bg-white dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600/50">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
<div className="flex items-center gap-2">
<Hash className="w-3 h-3 text-gray-400" />
<span className="font-mono text-gray-800 dark:text-gray-200">{card.card_code}</span>
</div>
<div>{getStatusBadge(card.status)}</div>
{card.failure_reason && <p className="text-red-500 text-xs col-span-full">: {card.failure_reason.toString()}</p>}
</div>
</div>
))}
</div>
</div>
)}
{order.failure_reason && renderSection("失败信息", <AlertTriangle className="w-5 h-5 text-red-500" />,
<div className="col-span-2 text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
{order.failure_reason.toString()}
</div>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
};
export function OrderList({
refreshEnabled = false,
refreshInterval = 5000,
@@ -53,74 +204,28 @@ export function OrderList({
}: OrderListProps) {
const [statusFilter, setStatusFilter] = useState<OrderResultStatus | "all">("all");
const [page, setPage] = useState(1);
const [selectedOrder, setSelectedOrder] = useState<OrderDetailResponse | null>(null);
const pageSize = 20;
// 获取订单列表
const {
data: ordersData,
isLoading,
error,
refetch,
isRefetching
} = useGetOrdersApiV1OrdersListGet(
{
skip: (page - 1) * pageSize,
limit: pageSize,
status_filter: statusFilter === "all" ? undefined : statusFilter,
},
{
query: {
enabled: true,
refetchInterval: refreshEnabled ? refreshInterval : false,
retry: 2,
staleTime: 30000,
},
}
const { data: ordersData, isLoading, error, refetch, isRefetching } = useGetOrdersApiV1OrdersListGet(
{ skip: (page - 1) * pageSize, limit: pageSize, status_filter: statusFilter === "all" ? undefined : statusFilter },
{ query: { enabled: true, refetchInterval: refreshEnabled ? refreshInterval : false, retry: 2, staleTime: 30000 } }
);
// 过滤数据
const filteredOrders = useMemo(() => {
if (!ordersData) return [];
return [...ordersData];
}, [ordersData]);
const handleRefresh = () => {
refetch();
};
const filteredOrders = useMemo(() => ordersData ? [...ordersData] : [], [ordersData]);
const handleRefresh = () => refetch();
const handleStatusFilterChange = (value: string) => {
setStatusFilter(value as OrderResultStatus | "all");
setPage(1); // 重置页码
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const getStatusBadge = (status: string) => {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.pending;
return (
<Badge variant={config.variant} className="px-2 py-1">
{config.label}
</Badge>
);
setPage(1);
};
const handleOrderClick = (order: OrderDetailResponse) => {
if (order.final_order_url) {
window.open(order.final_order_url.toString(), '_blank');
}
setSelectedOrder(order);
};
// 计算总页数
const totalPages = Math.max(1, Math.ceil(filteredOrders.length / pageSize));
const hasNextPage = filteredOrders.length === pageSize; // 如果返回数据等于页大小,可能还有下一页
const hasNextPage = filteredOrders.length === pageSize;
const hasPreviousPage = page > 1;
if (error) {
return (
@@ -129,16 +234,9 @@ export function OrderList({
<div className="w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-4">
<ShoppingCart className="w-8 h-8 text-red-600 dark:text-red-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{(error as any)?.message || "请检查网络连接或稍后重试"}
</p>
<AppleButton onClick={handleRefresh} variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</AppleButton>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"></h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">{error instanceof Error ? error.message : "请检查网络连接或稍后重试"}</p>
<AppleButton onClick={handleRefresh} variant="outline"><RefreshCw className="w-4 h-4 mr-2" /></AppleButton>
</div>
</div>
);
@@ -146,110 +244,76 @@ export function OrderList({
return (
<div className={cn("apple-glass-card rounded-2xl p-6 transition-all duration-300", className)}>
{/* 头部 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-600">
<ShoppingCart className="w-5 h-5 text-white" />
</div>
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-600"><ShoppingCart className="w-5 h-5 text-white" /></div>
<div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{filteredOrders.length}
</p>
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100"></h3>
<p className="text-sm text-gray-600 dark:text-gray-400"> {filteredOrders.length} </p>
</div>
</div>
<AppleButton
variant="outline"
size="icon"
onClick={handleRefresh}
disabled={isLoading || isRefetching}
className="w-10 h-10 rounded-xl"
>
<RefreshCw
className={cn("h-4 w-4", isRefetching && "animate-spin")}
/>
<AppleButton variant="outline" size="icon" onClick={handleRefresh} disabled={isLoading || isRefetching} className="w-10 h-10 rounded-xl">
<RefreshCw className={cn("h-4 w-4", isRefetching && "animate-spin")} />
</AppleButton>
</div>
{/* 状态筛选按钮 */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<Filter className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">:</span>
</div>
<div className="flex flex-wrap gap-2">
<AppleButton
variant={statusFilter === "all" ? "default" : "outline"}
size="sm"
onClick={() => handleStatusFilterChange("all")}
className={cn(
"transition-all duration-200",
statusFilter === "all"
? "bg-blue-600 text-white shadow-md"
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80"
)}
>
</AppleButton>
<AppleButton
variant={statusFilter === "pending" ? "default" : "outline"}
size="sm"
onClick={() => handleStatusFilterChange("pending")}
className={cn(
"transition-all duration-200",
statusFilter === "pending"
? "bg-gray-600 text-white shadow-md"
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80"
)}
>
</AppleButton>
<AppleButton
variant={statusFilter === "processing" ? "default" : "outline"}
size="sm"
onClick={() => handleStatusFilterChange("processing")}
className={cn(
"transition-all duration-200",
statusFilter === "processing"
? "bg-yellow-600 text-white shadow-md"
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80"
)}
>
</AppleButton>
<AppleButton
variant={statusFilter === "success" ? "default" : "outline"}
size="sm"
onClick={() => handleStatusFilterChange("success")}
className={cn(
"transition-all duration-200",
statusFilter === "success"
? "bg-green-600 text-white shadow-md"
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80"
)}
>
</AppleButton>
<AppleButton
variant={statusFilter === "failure" ? "default" : "outline"}
size="sm"
onClick={() => handleStatusFilterChange("failure")}
className={cn(
"transition-all duration-200",
statusFilter === "failure"
? "bg-red-600 text-white shadow-md"
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80"
)}
>
</AppleButton>
{(["all", "pending", "processing", "success", "failure"] as const).map(status => {
const isActive = statusFilter === status;
return (
<AppleButton
key={status}
variant="outline"
size="sm"
onClick={() => handleStatusFilterChange(status)}
className={cn(
"transition-all duration-200 border",
isActive
? (status === "all"
? "bg-blue-600 text-white shadow-md border-blue-600"
: (() => {
const config = statusConfig[status as keyof typeof statusConfig];
return cn(
"text-white shadow-md border",
config.bgColor.replace("/20", ""),
config.borderColor
);
})()
)
: "bg-white/50 dark:bg-gray-800/50 hover:bg-white/80 dark:hover:bg-gray-800/80 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600"
)}
>
<div className="flex items-center gap-1.5">
{status !== "all" && (
(() => {
const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config.icon;
const isProcessing = status === 'processing';
return (
<Icon
className={cn(
"w-3 h-3",
isActive ? "text-white" : config.color,
isProcessing && "animate-spin"
)}
/>
);
})()
)}
{status === "all" ? "全部状态" : statusConfig[status as keyof typeof statusConfig].label}
</div>
</AppleButton>
);
})}
</div>
</div>
{/* 订单列表 */}
{isLoading ? (
<div className="flex flex-col items-center justify-center py-12">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mb-4"></div>
@@ -260,47 +324,60 @@ export function OrderList({
<div className="w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
<ShoppingCart className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h3>
<p className="text-gray-600 dark:text-gray-400">
{statusFilter !== "all" ? "没有找到符合条件的订单" : "还没有任何订单记录"}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"></h3>
<p className="text-gray-600 dark:text-gray-400">{statusFilter !== "all" ? "没有找到符合条件的订单" : "还没有任何订单记录"}</p>
</div>
) : (
<div className="space-y-3">
{filteredOrders.map((order) => (
<div
key={order.id}
className="group p-4 rounded-xl bg-white/50 dark:bg-gray-800/50 border border-gray-200/50 dark:border-gray-700/50 hover:bg-white/80 dark:hover:bg-gray-800/80 transition-all duration-200 cursor-pointer"
className={getOrderItemStyle(order.status)}
onClick={() => handleOrderClick(order)}
>
{/* 左侧状态图标装饰 */}
<div className="absolute left-4 top-1/2 transform -translate-y-1/2">
{(() => {
const config = statusConfig[order.status as keyof typeof statusConfig] || statusConfig.pending;
const Icon = config.icon;
const isProcessing = order.status === 'processing';
return (
<Icon
className={cn(
"w-5 h-5",
config.color,
isProcessing && "animate-spin"
)}
/>
);
})()}
</div>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
{getStatusBadge(order.status)}
{order.final_order_url && (
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
)}
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
<Info className="w-3 h-3" />
<span></span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
{order.order_number && (
<div className="flex items-center gap-2">
<ShoppingCart className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">
{order.order_number.toString()}
</span>
{order.user_data && (
<div className="flex items-center gap-2" title={order.user_data.full_name}>
<User className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">{order.user_data.full_name}</span>
</div>
)}
{order.user_info && (
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
{order.links && (
<div className="flex items-center gap-2" title={order.links.url}>
<LinkIcon className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">
{`${order.user_info.first_name || ''} ${order.user_info.last_name || ''}`.trim() || order.user_info.email}
¥{order.links.amount.toFixed(2)}
</span>
</div>
)}
@@ -308,17 +385,14 @@ export function OrderList({
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{formatDate(order.created_at)}
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{formatDate(order.created_at)}</span>
</div>
{order.gift_cards && order.gift_cards.length > 0 && (
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-gray-400" />
<span className="text-gray-600 dark:text-gray-400">:</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{order.gift_cards.length}
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{order.gift_cards.length} </span>
</div>
)}
</div>
@@ -337,11 +411,13 @@ export function OrderList({
</div>
)}
{/* 分页控件 */}
{filteredOrders.length > 0 && (
<div className="mt-6 flex justify-between items-center">
<div className="mt-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
{page} {filteredOrders.length}
<div> {page} {filteredOrders.length} </div>
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{hasNextPage ? "可能还有更多数据" : "已到达最后一页"}
</div>
</div>
<Pagination>
<PaginationContent>
@@ -350,26 +426,19 @@ export function OrderList({
href="#"
onClick={(e) => {
e.preventDefault();
if (page > 1) setPage(page - 1);
if (hasPreviousPage) setPage(page - 1);
}}
className={cn(
"cursor-pointer transition-colors",
page <= 1 && "pointer-events-none opacity-50"
!hasPreviousPage && "pointer-events-none opacity-50"
)}
/>
</PaginationItem>
{/* 页码显示 - 简化版本 */}
<PaginationItem>
<PaginationLink
href="#"
isActive={true}
className="cursor-default"
>
<PaginationLink href="#" isActive={true} className="cursor-default">
{page}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
@@ -387,6 +456,8 @@ export function OrderList({
</Pagination>
</div>
)}
<OrderDetailModal order={selectedOrder} isOpen={!!selectedOrder} onOpenChange={(open) => !open && setSelectedOrder(null)} />
</div>
);
}

View File

@@ -5,20 +5,20 @@
* Apple礼品卡兑换服务后端API - FastAPI的现代异步微服务架构
* OpenAPI spec version: 2.0.0
*/
import type { GiftCardStatus } from "./giftCardStatus";
import type { GiftCardResponseFailureReason } from "./giftCardResponseFailureReason";
import type { GiftCardInfoResponseFailureReason } from "./giftCardInfoResponseFailureReason";
/**
* -
* -
*/
export interface GiftCardResponse {
export interface GiftCardInfoResponse {
/** 礼品卡ID */
id: string;
/** 礼品卡号码 */
card_code: string;
status: GiftCardStatus;
/** 礼品卡状态 */
status: string;
/** 失败原因 */
failure_reason?: GiftCardResponseFailureReason;
failure_reason?: GiftCardInfoResponseFailureReason;
/** 订单结果ID */
order_result_id: string;
/** 创建时间 */

View File

@@ -9,4 +9,4 @@
/**
*
*/
export type GiftCardResponseFailureReason = string | null;
export type GiftCardInfoResponseFailureReason = string | null;

View File

@@ -1,20 +0,0 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Apple Gift Card Exchange
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
* OpenAPI spec version: 2.0.0
*/
/**
* 礼品卡状态
*/
export type GiftCardStatus =
(typeof GiftCardStatus)[keyof typeof GiftCardStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const GiftCardStatus = {
pending: "pending",
success: "success",
failure: "failure",
} as const;

View File

@@ -10,9 +10,8 @@ export * from "./exportOrdersApiV1OrdersExportGetParams";
export * from "./getLinksApiV1LinksGetParams";
export * from "./getOrdersApiV1OrdersListGetParams";
export * from "./getUserDataListApiV1UserDataGetParams";
export * from "./giftCardResponse";
export * from "./giftCardResponseFailureReason";
export * from "./giftCardStatus";
export * from "./giftCardInfoResponse";
export * from "./giftCardInfoResponseFailureReason";
export * from "./giftCardSubmissionRequest";
export * from "./giftCardSubmissionResponse";
export * from "./hTTPValidationError";
@@ -22,7 +21,6 @@ export * from "./linkResponse";
export * from "./orderDetailResponse";
export * from "./orderDetailResponseFailureReason";
export * from "./orderDetailResponseFinalOrderUrl";
export * from "./orderDetailResponseOrderNumber";
export * from "./orderResultStatus";
export * from "./orderStatsResponse";
export * from "./orderTaskStatus";

View File

@@ -5,22 +5,37 @@
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
* OpenAPI spec version: 2.0.0
*/
import type { OrderDetailResponseOrderNumber } from "./orderDetailResponseOrderNumber";
import type { OrderResultStatus } from "./orderResultStatus";
import type { OrderDetailResponseFinalOrderUrl } from "./orderDetailResponseFinalOrderUrl";
import type { OrderDetailResponseFailureReason } from "./orderDetailResponseFailureReason";
import type { UserInfoResponse } from "./userInfoResponse";
import type { GiftCardResponse } from "./giftCardResponse";
import type { LinkResponse } from "./linkResponse";
import type { GiftCardInfoResponse } from "./giftCardInfoResponse";
/**
* 订单详情响应
* 订单详情响应 - 与数据库结构完全一致
*/
export interface OrderDetailResponse {
/** 订单ID */
id: string;
status: string;
order_number: OrderDetailResponseOrderNumber;
final_order_url: OrderDetailResponseFinalOrderUrl;
failure_reason: OrderDetailResponseFailureReason;
/** 订单状态 */
status: OrderResultStatus;
/** 创建时间 */
created_at: string;
user_info: UserInfoResponse;
gift_cards: GiftCardResponse[];
/** 更新时间 */
updated_at: string;
/** 最终订单URL */
final_order_url?: OrderDetailResponseFinalOrderUrl;
/** 失败原因 */
failure_reason?: OrderDetailResponseFailureReason;
/** 用户数据ID */
user_data_id: string;
/** 链接ID */
links_id: string;
/** 用户数据 */
user_data: UserInfoResponse;
/** 链接信息 */
links: LinkResponse;
/** 礼品卡列表 */
gift_cards?: GiftCardInfoResponse[];
}

View File

@@ -6,4 +6,7 @@
* OpenAPI spec version: 2.0.0
*/
/**
* 失败原因
*/
export type OrderDetailResponseFailureReason = string | null;

View File

@@ -6,4 +6,7 @@
* OpenAPI spec version: 2.0.0
*/
/**
* 最终订单URL
*/
export type OrderDetailResponseFinalOrderUrl = string | null;

View File

@@ -1,9 +0,0 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Apple Gift Card Exchange
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
* OpenAPI spec version: 2.0.0
*/
export type OrderDetailResponseOrderNumber = string | null;

View File

@@ -7,7 +7,7 @@
*/
/**
* 用户信息响应 - 与数据表字段完全一致
* 用户信息响应模型 - 与数据表字段完全一致
*/
export interface UserInfoResponse {
/** 用户数据唯一标识 */
@@ -32,4 +32,8 @@ export interface UserInfoResponse {
created_at: string;
/** 更新时间 */
updated_at: string;
/** 完整姓名 */
full_name: string;
/** 完整地址 */
full_address: string;
}

View File

@@ -10,13 +10,7 @@ export const config = {
// API基础URL - 支持代理模式
apiBaseUrl: process.env.NEXT_PUBLIC_USE_PROXY === 'true'
? '/'
: (process.env.NEXT_PUBLIC_TARGET_API_URL || 'http://localhost:3001'),
// 目标API URL用于代理
targetApiUrl: process.env.NEXT_PUBLIC_TARGET_API_URL || 'http://localhost:3001',
// 是否使用代理模式
useProxy: process.env.NEXT_PUBLIC_USE_PROXY === 'true',
: (process.env.NEXT_PUBLIC_TARGET_API_URL || 'http://localhost:3000'),
// 环境
env,