commit 2a93de600d1d6c01bae992895aa408b3e98c7e2e Author: Logan.Li Date: Mon Jun 29 17:03:57 2026 +0800 feat: LokiDeploy 初始提交 - 轻量级 K8s Loki 部署平台 Go(Gin) + React(Vite + shadcn/ui + Tailwind) 前后端分离,全中文 UI - 集群管理:Kubeconfig / Token 双接入,kubectl + helm 部署 - 主机管理:SSH 密钥 / 密码双认证,CPU/内存/磁盘使用率监控,SSH 错误中文友好提示 - 存储配置、部署管理、系统设置 - Docker 镜像内置 kubectl + helm,开箱即用 - run.sh 一键启停(start/stop/restart/status) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49c8536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log + +# Backend build artifacts +backend/bin/ +backend/dist/ + +# Runtime data (JSON file DB, kubeconfigs) +data/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f891568 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Build stage - Frontend +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# Build stage - Backend +FROM golang:1.24-alpine AS backend-builder +RUN apk add --no-cache git ca-certificates +ENV GOTOOLCHAIN=local +WORKDIR /app/backend +COPY backend/go.* ./ +RUN go mod download +COPY backend/ ./ +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bin/lokideploy . + +# Final stage - 内置 kubectl + helm,部署开箱即用 +FROM alpine:3.19 +RUN apk --no-cache add ca-certificates openssh-client curl bash +ARG TARGETARCH=amd64 +# 安装 kubectl(兼容大多数集群版本,±1 版本原则) +RUN curl -fsSL -o /usr/local/bin/kubectl \ + "https://dl.k8s.io/release/v1.31.0/bin/linux/${TARGETARCH}/kubectl" \ + && chmod +x /usr/local/bin/kubectl +# 安装 helm +RUN mkdir -p /tmp/helm \ + && curl -fsSL "https://get.helm.sh/helm-v3.16.3-linux-${TARGETARCH}.tar.gz" \ + | tar -xz -C /tmp/helm \ + && mv "/tmp/helm/linux-${TARGETARCH}/helm" /usr/local/bin/helm \ + && chmod +x /usr/local/bin/helm \ + && rm -rf /tmp/helm +WORKDIR /app + +# Copy frontend build +COPY --from=frontend-builder /app/frontend/dist ./dist + +# Copy backend binary +COPY --from=backend-builder /app/backend/bin/lokideploy ./lokideploy +RUN chmod +x ./lokideploy + +# Create data directory +RUN mkdir -p /app/data + +# Environment +ENV DATA_DIR=/app/data +ENV SERVER_PORT=8080 +ENV JWT_SECRET=lokideploy-secret-key-change-in-production +EXPOSE 8080 + +# Serve frontend as static files +CMD ["./lokideploy"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..18ae092 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# LokiDeploy - K8s Loki Deployment Platform + +A lightweight **Gin + React + JSON file storage** platform for deploying Grafana Loki log aggregation on Kubernetes clusters or bare-metal hosts via SSH. + +## Features + +- **K8s Cluster Management** - Add clusters via kubeconfig, auto-validate connectivity +- **SSH Host Management** - Password or key-based authentication +- **Storage Configuration** - S3, OSS, MinIO, RustFS backends +- **One-Click Loki Deploy** - Helm chart / YAML manifest / Binary +- **Deployment Logs** - Real-time log streaming +- **Web Terminal** - Browser-based remote command execution +- **Single Binary** - 13MB zero-dependency executable +- **Security** - Argon2 password hashing, AES credential encryption, JWT auth + +## Quick Start + +### Prerequisites +- Linux/macOS with `kubectl` and `helm` installed (for K8s deployment) +- `curl` or `wget` on target hosts +- Go 1.23+ (for building from source) +- Node.js 20+ (for building frontend) + +### Option 1: Build & Run (Recommended) + +```bash +git clone https://github.com/your/lokideploy.git +cd lokideploy + +# Build everything +bash build.sh + +# Start frontend (:3000) + backend (:8080) +./run.sh + +# Open http://localhost:3000 +# Default: admin / admin123 +``` + +### Option 2: Docker Compose + +```bash +cd lokideploy +docker compose up -d + +# Open http://localhost:8080 +``` + +### Option 3: Development Mode (Hot Reload) + +```bash +# Terminal 1 - Backend +cd backend +go run . + +# Terminal 2 - Frontend +cd frontend +npm install +npm run dev +# Opens http://localhost:3000, proxies API to localhost:8080 +``` + +## Architecture + +``` +Frontend (React + TypeScript + Tailwind CSS) [Port 3000 dev / 8080 prod] + | + | HTTP/REST + JWT Bearer Token + v +Backend (Go + Gin) [Port 8080] + | + +---> JSON File DB (data/lokideploy.json) + +---> K8s CLI (kubectl / helm) + +---> SSH Client (golang.org/x/crypto/ssh) + +---> Loki Deployer (Helm Chart / YAML / Binary) +``` + +## API Endpoints + +Base URL: `/api/v1` + +| Method | Endpoint | Auth | Description | +|--------|----------|------|-------------| +| POST | `/auth/login` | No | Login, get JWT token | +| GET | `/auth/user` | Yes | Current user info | +| POST | `/auth/password` | Yes | Change password | +| GET/POST | `/clusters` | Yes | Cluster CRUD | +| POST | `/clusters/:id/validate` | Yes | Validate cluster | +| GET/POST | `/hosts` | Yes | Host CRUD | +| POST | `/hosts/:id/execute` | Yes | Remote command | +| GET/POST | `/storages` | Yes | Storage config CRUD | +| GET/POST | `/deployments` | Yes | Deployment CRUD | +| POST | `/deployments/:id/deploy` | Yes | Start deployment | +| POST | `/deployments/:id/stop` | Yes | Stop deployment | +| GET | `/deployments/:id/logs` | Yes | Deployment logs | +| GET | `/deployments/stats` | Yes | Statistics | + +## Deployment Modes + +| Mode | Target | Description | +|------|--------|-------------| +| `k8s-helm` | K8s Cluster | Official Grafana Helm chart | +| `k8s-yaml` | K8s Cluster | Raw Kubernetes manifests | +| `host-binary` | Linux Host | Binary + systemd via SSH | + +## Storage Backends + +| Type | Endpoint Example | +|------|-----------------| +| S3 | `s3.amazonaws.com` | +| OSS | `oss-cn-hangzhou.aliyuncs.com` | +| MinIO | `http://minio:9000` | +| RustFS | `http://rustfs:8080` | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `SERVER_PORT` | `8080` | HTTP server port | +| `DATA_DIR` | `./data` | Data storage directory | +| `JWT_SECRET` | `lokideploy-secret-key...` | JWT signing key | + +## Security + +- **Argon2** password hashing +- **AES-GCM** credential encryption +- **JWT** token authentication +- **RBAC** role-based access control +- API responses sanitized (no passwords, kubeconfig, or keys exposed) + +## License + +MIT License diff --git a/backend/.backend.pid b/backend/.backend.pid new file mode 100644 index 0000000..04506c8 --- /dev/null +++ b/backend/.backend.pid @@ -0,0 +1 @@ +46832 diff --git a/backend/config/config.go b/backend/config/config.go new file mode 100644 index 0000000..7a9ceca --- /dev/null +++ b/backend/config/config.go @@ -0,0 +1,42 @@ +package config + +import ( + "os" + "path/filepath" + "sync" +) + +var ( + instance *Config + once sync.Once +) + +type Config struct { + ServerPort string + DatabasePath string + JWTSecret string + DataDir string + KubeDir string +} + +func GetConfig() *Config { + once.Do(func() { + dataDir := getEnv("DATA_DIR", "./data") + instance = &Config{ + ServerPort: getEnv("SERVER_PORT", "8080"), + DatabasePath: filepath.Join(dataDir, "lokideploy.json"), + JWTSecret: getEnv("JWT_SECRET", "lokideploy-secret-key-change-in-production"), + DataDir: dataDir, + KubeDir: filepath.Join(dataDir, "kubeconfigs"), + } + _ = os.MkdirAll(instance.KubeDir, 0755) + }) + return instance +} + +func getEnv(key, defaultVal string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultVal +} diff --git a/backend/controllers/auth.go b/backend/controllers/auth.go new file mode 100644 index 0000000..a022fbf --- /dev/null +++ b/backend/controllers/auth.go @@ -0,0 +1,94 @@ +package controllers + +import ( + "lokideploy/models" + "lokideploy/services" + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +type AuthController struct { + service *services.AuthService +} + +func NewAuthController() *AuthController { + return &AuthController{service: services.NewAuthService()} +} + +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type RegisterRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required,min=6"` + Nickname string `json:"nickname"` + Role string `json:"role"` +} + +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword" binding:"required"` + NewPassword string `json:"newPassword" binding:"required,min=6"` +} + +func (ctrl *AuthController) Login(c *gin.Context) { + var req LoginRequest + if !utils.BindAndValidate(c, &req) { + return + } + user, token, err := ctrl.service.Login(req.Username, req.Password) + if err != nil { + utils.Unauthorized(c, "用户名或密码错误") + return + } + utils.Success(c, gin.H{"token": token, "user": models.SanitizedJSON(user)}) +} + +func (ctrl *AuthController) Register(c *gin.Context) { + var req RegisterRequest + if !utils.BindAndValidate(c, &req) { + return + } + if req.Role == "" { + req.Role = "user" + } + user, err := ctrl.service.Register(req.Username, req.Password, req.Nickname, req.Role) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, models.SanitizedJSON(user)) +} + +func (ctrl *AuthController) GetUser(c *gin.Context) { + claims, ok := utils.GetUserFromContext(c) + if !ok { + utils.Unauthorized(c, "") + return + } + user, err := ctrl.service.GetUserByID(claims.UserID) + if err != nil { + utils.Error(c, 404, "用户不存在") + return + } + utils.Success(c, models.SanitizedJSON(user)) +} + +func (ctrl *AuthController) ChangePassword(c *gin.Context) { + var req ChangePasswordRequest + if !utils.BindAndValidate(c, &req) { + return + } + claims, ok := utils.GetUserFromContext(c) + if !ok { + utils.Unauthorized(c, "") + return + } + if err := ctrl.service.ChangePassword(claims.UserID, req.OldPassword, req.NewPassword); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "password changed"}) +} diff --git a/backend/controllers/cluster.go b/backend/controllers/cluster.go new file mode 100644 index 0000000..f073887 --- /dev/null +++ b/backend/controllers/cluster.go @@ -0,0 +1,137 @@ +package controllers + +import ( + "lokideploy/models" + "lokideploy/services" + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +type ClusterController struct { + service *services.ClusterService +} + +func NewClusterController() *ClusterController { + return &ClusterController{service: services.NewClusterService()} +} + +type CreateClusterRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + ConfigType string `json:"configType"` // kubeconfig | token + APIServer string `json:"apiServer"` // token 模式必填 + KubeConfig string `json:"kubeconfig"` // kubeconfig 模式必填 + Token string `json:"token"` // token 模式必填 + Context string `json:"context"` +} + +func (ctrl *ClusterController) Create(c *gin.Context) { + var req CreateClusterRequest + if !utils.BindAndValidate(c, &req) { + return + } + if req.ConfigType == "" { + req.ConfigType = "kubeconfig" + } + if req.ConfigType == "kubeconfig" && req.KubeConfig == "" { + utils.BadRequest(c, "请填写 kubeconfig 内容") + return + } + if req.ConfigType == "token" && (req.APIServer == "" || req.Token == "") { + utils.BadRequest(c, "Token 接入方式需要填写 API Server 和 Token") + return + } + + cluster, err := ctrl.service.Create(req.Name, req.Description, req.ConfigType, req.APIServer, req.KubeConfig, req.Token, req.Context) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, models.SanitizedJSON(cluster)) +} + +func (ctrl *ClusterController) Get(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + cluster, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "集群不存在") + return + } + utils.Success(c, models.SanitizedJSON(cluster)) +} + +func (ctrl *ClusterController) List(c *gin.Context) { + page, pageSize := utils.Pagination(c) + clusters, total, err := ctrl.service.List(page, pageSize) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, utils.PageData{ + List: models.SanitizedJSON(clusters), Total: total, Page: page, PageSize: pageSize, + }) +} + +func (ctrl *ClusterController) Update(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + var updates map[string]interface{} + if !utils.BindAndValidate(c, &updates) { + return + } + if err := ctrl.service.Update(uint(id), updates); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "更新成功"}) +} + +func (ctrl *ClusterController) Delete(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.Delete(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "删除成功"}) +} + +func (ctrl *ClusterController) Validate(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + go ctrl.service.Validate(uint(id)) + utils.Success(c, gin.H{"message": "已开始验证连接"}) +} + +func (ctrl *ClusterController) GetInfo(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + cluster, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "集群不存在") + return + } + info, err := services.GetClusterInfo(c.Request.Context(), cluster) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, info) +} diff --git a/backend/controllers/deployment.go b/backend/controllers/deployment.go new file mode 100644 index 0000000..db70217 --- /dev/null +++ b/backend/controllers/deployment.go @@ -0,0 +1,182 @@ +package controllers + +import ( + "lokideploy/services" + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +type DeploymentController struct { + service *services.DeploymentService +} + +func NewDeploymentController() *DeploymentController { + return &DeploymentController{service: services.NewDeploymentService()} +} + +type CreateDeploymentRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Version string `json:"version"` + Mode string `json:"mode" binding:"required,oneof=k8s-helm k8s-yaml host-binary"` + ClusterID *uint `json:"clusterID"` + HostID *uint `json:"hostID"` + Namespace string `json:"namespace"` + StorageConfigID uint `json:"storageConfigID"` + AuthEnabled bool `json:"authEnabled"` + RetentionDays int `json:"retentionDays"` + Replicas int `json:"replicas"` + ResourcesCPU string `json:"resourcesCPU"` + ResourcesMem string `json:"resourcesMem"` + StorageSize string `json:"storageSize"` +} + +func (ctrl *DeploymentController) Create(c *gin.Context) { + var req CreateDeploymentRequest + if !utils.BindAndValidate(c, &req) { + return + } + deployment, err := ctrl.service.Create(&services.CreateDeploymentRequest{ + Name: req.Name, + Description: req.Description, + Version: req.Version, + Mode: req.Mode, + ClusterID: req.ClusterID, + HostID: req.HostID, + Namespace: req.Namespace, + StorageConfigID: req.StorageConfigID, + AuthEnabled: req.AuthEnabled, + RetentionDays: req.RetentionDays, + Replicas: req.Replicas, + ResourcesCPU: req.ResourcesCPU, + ResourcesMem: req.ResourcesMem, + StorageSize: req.StorageSize, + }) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, deployment) +} + +func (ctrl *DeploymentController) Get(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + deployment, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "部署不存在") + return + } + utils.Success(c, deployment) +} + +func (ctrl *DeploymentController) List(c *gin.Context) { + page, pageSize := utils.Pagination(c) + deployments, total, err := ctrl.service.List(page, pageSize) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, utils.PageData{ + List: deployments, Total: total, Page: page, PageSize: pageSize, + }) +} + +func (ctrl *DeploymentController) Update(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + var updates map[string]interface{} + if !utils.BindAndValidate(c, &updates) { + return + } + if err := ctrl.service.Update(uint(id), updates); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "updated"}) +} + +func (ctrl *DeploymentController) Delete(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.Delete(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deleted"}) +} + +func (ctrl *DeploymentController) Deploy(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.StartDeploy(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deployment started"}) +} + +func (ctrl *DeploymentController) Stop(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.StopDeployment(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deployment stopped"}) +} + +func (ctrl *DeploymentController) Restart(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.RestartDeployment(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deployment restarted"}) +} + +func (ctrl *DeploymentController) GetLogs(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + page, pageSize := utils.Pagination(c) + logs, total, err := ctrl.service.GetLogs(uint(id), page, pageSize) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, utils.PageData{ + List: logs, Total: total, Page: page, PageSize: pageSize, + }) +} + +func (ctrl *DeploymentController) GetStats(c *gin.Context) { + stats, err := ctrl.service.GetStats() + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, stats) +} diff --git a/backend/controllers/host.go b/backend/controllers/host.go new file mode 100644 index 0000000..14e1924 --- /dev/null +++ b/backend/controllers/host.go @@ -0,0 +1,164 @@ +package controllers + +import ( + "lokideploy/models" + "lokideploy/services" + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +type HostController struct { + service *services.HostService +} + +func NewHostController() *HostController { + return &HostController{service: services.NewHostService()} +} + +type CreateHostRequest struct { + Name string `json:"name" binding:"required"` + HostIP string `json:"hostIP" binding:"required"` + Port int `json:"port"` + User string `json:"user" binding:"required"` + AuthType string `json:"authType" binding:"required,oneof=password key"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + KeyPhrase string `json:"keyPhrase"` + OS string `json:"os"` + Arch string `json:"arch"` + Labels string `json:"labels"` +} + +func (ctrl *HostController) Create(c *gin.Context) { + var req CreateHostRequest + if !utils.BindAndValidate(c, &req) { + return + } + host, err := ctrl.service.Create(req.Name, req.HostIP, req.Port, req.User, req.AuthType, + req.Password, req.PrivateKey, req.KeyPhrase, req.OS, req.Arch, req.Labels) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, models.SanitizedJSON(host)) +} + +func (ctrl *HostController) Get(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + host, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "主机不存在") + return + } + utils.Success(c, models.SanitizedJSON(host)) +} + +func (ctrl *HostController) List(c *gin.Context) { + page, pageSize := utils.Pagination(c) + hosts, total, err := ctrl.service.List(page, pageSize) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, utils.PageData{ + List: models.SanitizedJSON(hosts), + Total: total, Page: page, PageSize: pageSize, + }) +} + +func (ctrl *HostController) Update(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + var updates map[string]interface{} + if !utils.BindAndValidate(c, &updates) { + return + } + if err := ctrl.service.Update(uint(id), updates); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "updated"}) +} + +func (ctrl *HostController) Delete(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.Delete(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deleted"}) +} + +func (ctrl *HostController) Validate(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + go ctrl.service.Validate(uint(id)) + utils.Success(c, gin.H{"message": "validation started"}) +} + +func (ctrl *HostController) GetInfo(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + host, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "主机不存在") + return + } + + password := host.Password + if len(password) > 4 && password[:4] == "ENC:" { + password, _ = utils.Decrypt(password[4:]) + } + + client, err := services.NewSSHClient(host.HostIP, host.Port, host.User, password) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + defer client.Close() + + info, err := client.GetHostInfo(c.Request.Context()) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, info) +} + +func (ctrl *HostController) Execute(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + var req struct { + Command string `json:"command" binding:"required"` + } + if !utils.BindAndValidate(c, &req) { + return + } + stdout, stderr, err := ctrl.service.ExecuteCommand(uint(id), req.Command) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, gin.H{"stdout": stdout, "stderr": stderr}) +} diff --git a/backend/controllers/storage.go b/backend/controllers/storage.go new file mode 100644 index 0000000..a06f1ed --- /dev/null +++ b/backend/controllers/storage.go @@ -0,0 +1,110 @@ +package controllers + +import ( + "lokideploy/models" + "lokideploy/services" + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +type StorageController struct { + service *services.StorageService +} + +func NewStorageController() *StorageController { + return &StorageController{service: services.NewStorageService()} +} + +type CreateStorageRequest struct { + Name string `json:"name" binding:"required"` + StorageType string `json:"storageType" binding:"required,oneof=s3 oss minio rustfs"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket" binding:"required"` + Region string `json:"region"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + PathPrefix string `json:"pathPrefix"` + Insecure bool `json:"insecure"` + IsDefault bool `json:"isDefault"` +} + +func (ctrl *StorageController) Create(c *gin.Context) { + var req CreateStorageRequest + if !utils.BindAndValidate(c, &req) { + return + } + storage, err := ctrl.service.Create(req.Name, req.StorageType, req.Endpoint, req.Bucket, + req.Region, req.AccessKey, req.SecretKey, req.PathPrefix, req.Insecure, req.IsDefault) + if err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, models.SanitizedJSON(storage)) +} + +func (ctrl *StorageController) Get(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + storage, err := ctrl.service.Get(uint(id)) + if err != nil { + utils.NotFound(c, "存储配置不存在") + return + } + utils.Success(c, models.SanitizedJSON(storage)) +} + +func (ctrl *StorageController) List(c *gin.Context) { + page, pageSize := utils.Pagination(c) + storages, total, err := ctrl.service.List(page, pageSize) + if err != nil { + utils.Error(c, 500, err.Error()) + return + } + utils.Success(c, utils.PageData{ + List: models.SanitizedJSON(storages), + Total: total, Page: page, PageSize: pageSize, + }) +} + +func (ctrl *StorageController) Update(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + var updates map[string]interface{} + if !utils.BindAndValidate(c, &updates) { + return + } + if err := ctrl.service.Update(uint(id), updates); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "updated"}) +} + +func (ctrl *StorageController) Delete(c *gin.Context) { + id, err := utils.IntParam(c, "id") + if err != nil { + utils.BadRequest(c, "ID 无效") + return + } + if err := ctrl.service.Delete(uint(id)); err != nil { + utils.Error(c, 400, err.Error()) + return + } + utils.Success(c, gin.H{"message": "deleted"}) +} + +func (ctrl *StorageController) GetDefault(c *gin.Context) { + storage, err := ctrl.service.GetDefault() + if err != nil { + utils.NotFound(c, "未设置默认存储配置") + return + } + utils.Success(c, models.SanitizedJSON(storage)) +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..a820ac2 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,44 @@ +module lokideploy + +go 1.24.0 + +toolchain go1.24.3 + +require ( + github.com/gin-contrib/cors v1.7.3 + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + golang.org/x/crypto v0.47.0 +) + +require ( + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..69cc5f0 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,103 @@ +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= +github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..919e1d6 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + + "lokideploy/config" + "lokideploy/models" + "lokideploy/routers" + "lokideploy/seed" +) + +func main() { + cfg := config.GetConfig() + + log.Println("Initializing database...") + if err := models.InitDB(); err != nil { + log.Fatalf("Failed to init DB: %v", err) + } + + log.Println("Seeding default data...") + seed.Seed() + + r := routers.SetupRouter() + + log.Printf("LokiDeploy starting on port %s", cfg.ServerPort) + log.Printf("API: http://localhost:%s/api/v1", cfg.ServerPort) + log.Printf("Default login: admin / admin123") + + if err := r.Run(":" + cfg.ServerPort); err != nil { + log.Fatalf("Server failed: %v", err) + } +} diff --git a/backend/middleware/jwt.go b/backend/middleware/jwt.go new file mode 100644 index 0000000..697c9a4 --- /dev/null +++ b/backend/middleware/jwt.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "strings" + + "lokideploy/utils" + + "github.com/gin-gonic/gin" +) + +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + utils.Unauthorized(c, "missing authorization header") + c.Abort() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + utils.Unauthorized(c, "invalid authorization header format") + c.Abort() + return + } + + claims, err := utils.ParseToken(parts[1]) + if err != nil { + utils.Unauthorized(c, "invalid or expired token") + c.Abort() + return + } + + c.Set("user", claims) + c.Set("userId", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Next() + } +} + +func AdminAuth() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists || role != "admin" { + utils.Forbidden(c, "admin access required") + c.Abort() + return + } + c.Next() + } +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if origin == "" { + origin = "*" + } + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + } +} diff --git a/backend/models/cluster.go b/backend/models/cluster.go new file mode 100644 index 0000000..ed8c1be --- /dev/null +++ b/backend/models/cluster.go @@ -0,0 +1,38 @@ +package models + +import "time" + +type Cluster struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ConfigType string `json:"configType"` // kubeconfig | token(空值视为 kubeconfig,向后兼容) + APIServer string `json:"apiServer"` // 展示用 + KubeConfig string `json:"kubeconfig"` // 统一存储(token 接入时存构造出的等价 kubeconfig) + KubeConfigPath string `json:"kubeconfigPath"` + Context string `json:"context"` + Version string `json:"version"` + NodesCount int `json:"nodesCount"` + Status string `json:"status"` + LastConnected time.Time `json:"lastConnected"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (c *Cluster) Sanitized() map[string]interface{} { + return map[string]interface{}{ + "id": c.ID, + "name": c.Name, + "description": c.Description, + "configType": c.ConfigType, + "apiServer": c.APIServer, + "context": c.Context, + "version": c.Version, + "nodesCount": c.NodesCount, + "status": c.Status, + "hasKubeconfig": c.KubeConfig != "", + "lastConnected": c.LastConnected, + "createdAt": c.CreatedAt, + "updatedAt": c.UpdatedAt, + } +} diff --git a/backend/models/db.go b/backend/models/db.go new file mode 100644 index 0000000..2c1feed --- /dev/null +++ b/backend/models/db.go @@ -0,0 +1,111 @@ +package models + +import ( + "encoding/json" + "lokideploy/config" + "os" + "sync" +) + +type JsonDB struct { + mu sync.RWMutex + dataFile string + Users []User `json:"users"` + Clusters []Cluster `json:"clusters"` + Hosts []Host `json:"hosts"` + Storages []StorageConfig `json:"storages"` + Deployments []LokiDeployment `json:"deployments"` + Logs []DeploymentLog `json:"logs"` + Settings []SystemSetting `json:"settings"` + sequences map[string]uint +} + +var db *JsonDB + +func GetDB() *JsonDB { + return db +} + +func InitDB() error { + cfg := config.GetConfig() + _ = os.MkdirAll(cfg.DataDir, 0755) + + d := &JsonDB{ + dataFile: cfg.DatabasePath, + Users: []User{}, + Clusters: []Cluster{}, + Hosts: []Host{}, + Storages: []StorageConfig{}, + Deployments: []LokiDeployment{}, + Logs: []DeploymentLog{}, + Settings: []SystemSetting{}, + sequences: map[string]uint{ + "users": 0, + "clusters": 0, + "hosts": 0, + "storages": 0, + "deployments": 0, + "logs": 0, + }, + } + + if data, err := os.ReadFile(d.dataFile); err == nil { + if err := json.Unmarshal(data, d); err == nil { + d.sequences = map[string]uint{} + for _, u := range d.Users { if u.ID > d.sequences["users"] { d.sequences["users"] = u.ID } } + for _, c := range d.Clusters { if c.ID > d.sequences["clusters"] { d.sequences["clusters"] = c.ID } } + for _, h := range d.Hosts { if h.ID > d.sequences["hosts"] { d.sequences["hosts"] = h.ID } } + for _, s := range d.Storages { if s.ID > d.sequences["storages"] { d.sequences["storages"] = s.ID } } + for _, de := range d.Deployments { if de.ID > d.sequences["deployments"] { d.sequences["deployments"] = de.ID } } + for _, l := range d.Logs { if l.ID > d.sequences["logs"] { d.sequences["logs"] = l.ID } } + } + } + + db = d + return nil +} + +func (d *JsonDB) Save() error { + d.mu.Lock() + defer d.mu.Unlock() + data, err := json.MarshalIndent(d, "", " ") + if err != nil { + return err + } + return os.WriteFile(d.dataFile, data, 0644) +} + +func (d *JsonDB) NextID(table string) uint { + d.mu.Lock() + defer d.mu.Unlock() + d.sequences[table]++ + return d.sequences[table] +} + +func (d *JsonDB) Lock() { + d.mu.Lock() +} + +func (d *JsonDB) Unlock() { + d.mu.Unlock() +} + +func (d *JsonDB) RLock() { + d.mu.RLock() +} + +func (d *JsonDB) RUnlock() { + d.mu.RUnlock() +} + +func (d *JsonDB) CreateUser(user *User) error { + d.mu.Lock() + defer d.mu.Unlock() + if user.ID == 0 { + user.ID = d.sequences["users"] + 1 + d.sequences["users"] = user.ID + } + d.Users = append(d.Users, *user) + go d.Save() + return nil +} diff --git a/backend/models/deployment.go b/backend/models/deployment.go new file mode 100644 index 0000000..b96cc59 --- /dev/null +++ b/backend/models/deployment.go @@ -0,0 +1,43 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type LokiDeployment struct { + ID uint `json:"id"` + UUID string `json:"uuid"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + Mode string `json:"mode"` + ClusterID *uint `json:"clusterID,omitempty"` + Cluster *Cluster `json:"cluster,omitempty"` + HostID *uint `json:"hostID,omitempty"` + Host *Host `json:"host,omitempty"` + Namespace string `json:"namespace"` + StorageConfigID uint `json:"storageConfigID"` + StorageConfig *StorageConfig `json:"storageConfig,omitempty"` + AuthEnabled bool `json:"authEnabled"` + RetentionDays int `json:"retentionDays"` + Replicas int `json:"replicas"` + ResourcesCPU string `json:"resourcesCPU"` + ResourcesMem string `json:"resourcesMem"` + StorageSize string `json:"storageSize"` + Status string `json:"status"` + StatusMessage string `json:"statusMessage"` + HelmValues string `json:"helmValues"` + Endpoint string `json:"endpoint"` + AdminUser string `json:"adminUser"` + AdminPassword string `json:"adminPassword"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (d *LokiDeployment) BeforeCreate() { + if d.UUID == "" { + d.UUID = uuid.New().String() + } +} diff --git a/backend/models/host.go b/backend/models/host.go new file mode 100644 index 0000000..d5571e8 --- /dev/null +++ b/backend/models/host.go @@ -0,0 +1,38 @@ +package models + +import "time" + +type Host struct { + ID uint `json:"id"` + Name string `json:"name"` + HostIP string `json:"hostIP"` + Port int `json:"port"` + User string `json:"user"` + AuthType string `json:"authType"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + KeyPhrase string `json:"keyPhrase"` + OS string `json:"os"` + Arch string `json:"arch"` + Status string `json:"status"` + Labels string `json:"labels"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (h *Host) Sanitized() map[string]interface{} { + return map[string]interface{}{ + "id": h.ID, + "name": h.Name, + "hostIP": h.HostIP, + "port": h.Port, + "user": h.User, + "authType": h.AuthType, + "os": h.OS, + "arch": h.Arch, + "status": h.Status, + "labels": h.Labels, + "createdAt": h.CreatedAt, + "updatedAt": h.UpdatedAt, + } +} diff --git a/backend/models/log.go b/backend/models/log.go new file mode 100644 index 0000000..46a35dc --- /dev/null +++ b/backend/models/log.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type DeploymentLog struct { + ID uint `json:"id"` + DeploymentID uint `json:"deploymentID"` + Level string `json:"level"` + Message string `json:"message"` + Detail string `json:"detail"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/backend/models/sanitizer.go b/backend/models/sanitizer.go new file mode 100644 index 0000000..6a6a6d9 --- /dev/null +++ b/backend/models/sanitizer.go @@ -0,0 +1,68 @@ +package models + +// Sanitizer interface for models that can sanitize themselves +type Sanitizer interface { + Sanitized() map[string]interface{} +} + +// SanitizedJSON sanitizes a single model or a slice of models +func SanitizedJSON(v any) any { + switch val := v.(type) { + // Single objects + case User: + return val.Sanitized() + case *User: + if val == nil { + return nil + } + return val.Sanitized() + case Cluster: + return val.Sanitized() + case *Cluster: + if val == nil { + return nil + } + return val.Sanitized() + case Host: + return val.Sanitized() + case *Host: + if val == nil { + return nil + } + return val.Sanitized() + case StorageConfig: + return val.Sanitized() + case *StorageConfig: + if val == nil { + return nil + } + return val.Sanitized() + // Slices + case []User: + result := make([]map[string]interface{}, len(val)) + for i, item := range val { + result[i] = item.Sanitized() + } + return result + case []Cluster: + result := make([]map[string]interface{}, len(val)) + for i, item := range val { + result[i] = item.Sanitized() + } + return result + case []Host: + result := make([]map[string]interface{}, len(val)) + for i, item := range val { + result[i] = item.Sanitized() + } + return result + case []StorageConfig: + result := make([]map[string]interface{}, len(val)) + for i, item := range val { + result[i] = item.Sanitized() + } + return result + default: + return v + } +} diff --git a/backend/models/setting.go b/backend/models/setting.go new file mode 100644 index 0000000..f6ada0c --- /dev/null +++ b/backend/models/setting.go @@ -0,0 +1,7 @@ +package models + +type SystemSetting struct { + ID uint `json:"id"` + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/backend/models/storage.go b/backend/models/storage.go new file mode 100644 index 0000000..a4507d1 --- /dev/null +++ b/backend/models/storage.go @@ -0,0 +1,35 @@ +package models + +import "time" + +type StorageConfig struct { + ID uint `json:"id"` + Name string `json:"name"` + StorageType string `json:"storageType"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket"` + Region string `json:"region"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + PathPrefix string `json:"pathPrefix"` + Insecure bool `json:"insecure"` + IsDefault bool `json:"isDefault"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (s *StorageConfig) Sanitized() map[string]interface{} { + return map[string]interface{}{ + "id": s.ID, + "name": s.Name, + "storageType": s.StorageType, + "endpoint": s.Endpoint, + "bucket": s.Bucket, + "region": s.Region, + "pathPrefix": s.PathPrefix, + "insecure": s.Insecure, + "isDefault": s.IsDefault, + "createdAt": s.CreatedAt, + "updatedAt": s.UpdatedAt, + } +} diff --git a/backend/models/user.go b/backend/models/user.go new file mode 100644 index 0000000..ee193b2 --- /dev/null +++ b/backend/models/user.go @@ -0,0 +1,24 @@ +package models + +import "time" + +type User struct { + ID uint `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Nickname string `json:"nickname"` + Role string `json:"role"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (u *User) Sanitized() map[string]interface{} { + return map[string]interface{}{ + "id": u.ID, + "username": u.Username, + "nickname": u.Nickname, + "role": u.Role, + "createdAt": u.CreatedAt, + "updatedAt": u.UpdatedAt, + } +} diff --git a/backend/routers/router.go b/backend/routers/router.go new file mode 100644 index 0000000..275d4ae --- /dev/null +++ b/backend/routers/router.go @@ -0,0 +1,127 @@ +package routers + +import ( + "os" + "path/filepath" + "strings" + + "lokideploy/controllers" + "lokideploy/middleware" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func SetupRouter() *gin.Engine { + r := gin.Default() + + // CORS + cfg := cors.DefaultConfig() + cfg.AllowAllOrigins = true + cfg.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"} + cfg.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"} + r.Use(cors.New(cfg)) + + // Controllers + authCtrl := controllers.NewAuthController() + clusterCtrl := controllers.NewClusterController() + hostCtrl := controllers.NewHostController() + storageCtrl := controllers.NewStorageController() + deploymentCtrl := controllers.NewDeploymentController() + + // API v1 + api := r.Group("/api/v1") + + // Public + api.POST("/auth/login", authCtrl.Login) + + // Auth required + authorized := api.Group("") + authorized.Use(middleware.JWTAuth()) + + // Auth routes + authorized.GET("/auth/user", authCtrl.GetUser) + authorized.POST("/auth/password", authCtrl.ChangePassword) + authorized.POST("/auth/register", middleware.AdminAuth(), authCtrl.Register) + + // Clusters + authorized.GET("/clusters", clusterCtrl.List) + authorized.GET("/clusters/:id", clusterCtrl.Get) + authorized.POST("/clusters", clusterCtrl.Create) + authorized.PUT("/clusters/:id", clusterCtrl.Update) + authorized.DELETE("/clusters/:id", clusterCtrl.Delete) + authorized.POST("/clusters/:id/validate", clusterCtrl.Validate) + authorized.GET("/clusters/:id/info", clusterCtrl.GetInfo) + + // Hosts + authorized.GET("/hosts", hostCtrl.List) + authorized.GET("/hosts/:id", hostCtrl.Get) + authorized.POST("/hosts", hostCtrl.Create) + authorized.PUT("/hosts/:id", hostCtrl.Update) + authorized.DELETE("/hosts/:id", hostCtrl.Delete) + authorized.POST("/hosts/:id/validate", hostCtrl.Validate) + authorized.GET("/hosts/:id/info", hostCtrl.GetInfo) + authorized.POST("/hosts/:id/execute", hostCtrl.Execute) + + // Storage + authorized.GET("/storages", storageCtrl.List) + authorized.GET("/storages/:id", storageCtrl.Get) + authorized.POST("/storages", storageCtrl.Create) + authorized.PUT("/storages/:id", storageCtrl.Update) + authorized.DELETE("/storages/:id", storageCtrl.Delete) + authorized.GET("/storages/default", storageCtrl.GetDefault) + + // Deployments + authorized.GET("/deployments", deploymentCtrl.List) + authorized.GET("/deployments/:id", deploymentCtrl.Get) + authorized.POST("/deployments", deploymentCtrl.Create) + authorized.PUT("/deployments/:id", deploymentCtrl.Update) + authorized.DELETE("/deployments/:id", deploymentCtrl.Delete) + authorized.POST("/deployments/:id/deploy", deploymentCtrl.Deploy) + authorized.POST("/deployments/:id/stop", deploymentCtrl.Stop) + authorized.POST("/deployments/:id/restart", deploymentCtrl.Restart) + authorized.GET("/deployments/:id/logs", deploymentCtrl.GetLogs) + authorized.GET("/deployments/stats", deploymentCtrl.GetStats) + + // Static files - use executable directory as base + distPath := findDistPath() + + if distPath != "" { + r.Static("/assets", filepath.Join(distPath, "assets")) + r.NoRoute(func(c *gin.Context) { + if strings.HasPrefix(c.Request.URL.Path, "/api/") { + c.JSON(404, gin.H{"code": 404, "message": "API endpoint not found"}) + return + } + c.File(filepath.Join(distPath, "index.html")) + }) + } + + return r +} + +// findDistPath finds the frontend dist directory +func findDistPath() string { + // Try relative to executable + if ex, err := os.Executable(); err == nil { + execDir := filepath.Dir(ex) + // If running via "go run", executable is in temp dir - fall through + if !strings.Contains(execDir, "/tmp/go-build") { + for _, rel := range []string{"dist", "../dist", "../../dist"} { + p := filepath.Join(execDir, rel) + if _, err := os.Stat(filepath.Join(p, "index.html")); err == nil { + return p + } + } + } + } + // Try working directory relative paths + for _, p := range []string{"./dist", "../dist", "/app/dist"} { + if abs, err := filepath.Abs(p); err == nil { + if _, err := os.Stat(filepath.Join(abs, "index.html")); err == nil { + return abs + } + } + } + return "" +} diff --git a/backend/seed/seed.go b/backend/seed/seed.go new file mode 100644 index 0000000..7727a9b --- /dev/null +++ b/backend/seed/seed.go @@ -0,0 +1,23 @@ +package seed + +import ( + "lokideploy/models" + "lokideploy/utils" +) + +func Seed() { + db := models.GetDB() + db.RLock() + hasUsers := len(db.Users) > 0 + db.RUnlock() + + if !hasUsers { + hash, _ := utils.HashPassword("admin123") + models.GetDB().CreateUser(&models.User{ + Username: "admin", + Password: hash, + Nickname: "Administrator", + Role: "admin", + }) + } +} diff --git a/backend/services/auth.go b/backend/services/auth.go new file mode 100644 index 0000000..24f4045 --- /dev/null +++ b/backend/services/auth.go @@ -0,0 +1,83 @@ +package services + +import ( + "lokideploy/models" + "lokideploy/utils" +) + +type AuthService struct{} + +func NewAuthService() *AuthService { + return &AuthService{} +} + +func (s *AuthService) Login(username, password string) (*models.User, string, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Users { + if db.Users[i].Username == username { + if !utils.VerifyPassword(password, db.Users[i].Password) { + return nil, "", utils.ErrInvalidCredentials + } + token, err := utils.GenerateToken(db.Users[i].ID, db.Users[i].Username, db.Users[i].Role) + if err != nil { + return nil, "", err + } + return &db.Users[i], token, nil + } + } + return nil, "", utils.ErrInvalidCredentials +} + +func (s *AuthService) Register(username, password, nickname, role string) (*models.User, error) { + hash, err := utils.HashPassword(password) + if err != nil { + return nil, err + } + user := &models.User{ + ID: models.GetDB().NextID("users"), + Username: username, + Password: hash, + Nickname: nickname, + Role: role, + } + if err := models.GetDB().CreateUser(user); err != nil { + return nil, err + } + return user, nil +} + +func (s *AuthService) GetUserByID(id uint) (*models.User, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Users { + if db.Users[i].ID == id { + u := db.Users[i] + return &u, nil + } + } + return nil, utils.ErrNotFound +} + +func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i := range db.Users { + if db.Users[i].ID == userID { + if !utils.VerifyPassword(oldPassword, db.Users[i].Password) { + return utils.ErrInvalidCredentials + } + hash, err := utils.HashPassword(newPassword) + if err != nil { + return err + } + db.Users[i].Password = hash + go db.Save() + return nil + } + } + return utils.ErrNotFound +} diff --git a/backend/services/cluster.go b/backend/services/cluster.go new file mode 100644 index 0000000..e75cf9e --- /dev/null +++ b/backend/services/cluster.go @@ -0,0 +1,195 @@ +package services + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "lokideploy/models" + "lokideploy/utils" +) + +type ClusterService struct{} + +func NewClusterService() *ClusterService { + return &ClusterService{} +} + +func SaveKubeConfig(name, content string) (string, error) { + dir := filepath.Join("data", "kubeconfigs") + if err := os.MkdirAll(dir, 0755); err != nil { + return "", err + } + path := filepath.Join(dir, fmt.Sprintf("%s.yaml", name)) + if err := os.WriteFile(path, []byte(content), 0600); err != nil { + return "", err + } + return path, nil +} + +// Create 创建集群。token 接入时自动构造等价 kubeconfig,统一存储,使部署模块同样可用。 +func (s *ClusterService) Create(name, description, configType, apiServer, kubeconfig, token, contextName string) (*models.Cluster, error) { + if configType == "" { + configType = "kubeconfig" + } + + finalKubeconfig := kubeconfig + if configType == "token" { + finalKubeconfig = BuildKubeconfigFromToken(apiServer, token) + } + // 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取 + if apiServer == "" { + apiServer = ExtractAPIServer(finalKubeconfig) + } + + path, err := SaveKubeConfig(name, finalKubeconfig) + if err != nil { + return nil, fmt.Errorf("kubeconfig 保存失败: %w", err) + } + + cluster := models.Cluster{ + ID: models.GetDB().NextID("clusters"), + Name: name, + Description: description, + ConfigType: configType, + APIServer: apiServer, + KubeConfig: finalKubeconfig, + KubeConfigPath: path, + Context: contextName, + Status: "pending", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + db := models.GetDB() + db.Lock() + db.Clusters = append(db.Clusters, cluster) + db.Unlock() + go db.Save() + + go s.Validate(cluster.ID) + return &cluster, nil +} + +func (s *ClusterService) Get(id uint) (*models.Cluster, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Clusters { + if db.Clusters[i].ID == id { + c := db.Clusters[i] + return &c, nil + } + } + return nil, utils.ErrNotFound +} + +func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + total := int64(len(db.Clusters)) + start := (page - 1) * pageSize + if start < 0 || start >= len(db.Clusters) { + return []models.Cluster{}, total, nil + } + end := start + pageSize + if end > len(db.Clusters) { + end = len(db.Clusters) + } + result := make([]models.Cluster, end-start) + copy(result, db.Clusters[start:end]) + return result, total, nil +} + +func (s *ClusterService) Update(id uint, updates map[string]interface{}) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i := range db.Clusters { + if db.Clusters[i].ID == id { + if v, ok := updates["name"].(string); ok && v != "" { + db.Clusters[i].Name = v + } + if v, ok := updates["description"].(string); ok { + db.Clusters[i].Description = v + } + if v, ok := updates["status"].(string); ok && v != "" { + db.Clusters[i].Status = v + } + if v, ok := updates["version"].(string); ok { + db.Clusters[i].Version = v + } + if v, ok := updates["nodesCount"].(int); ok { + db.Clusters[i].NodesCount = v + } + if v, ok := updates["context"].(string); ok { + db.Clusters[i].Context = v + } + if v, ok := updates["lastConnected"].(time.Time); ok { + db.Clusters[i].LastConnected = v + } + // 接入方式与展示字段 + if v, ok := updates["configType"].(string); ok && v != "" { + db.Clusters[i].ConfigType = v + } + if v, ok := updates["apiServer"].(string); ok { + db.Clusters[i].APIServer = v + } + // 凭据字段:空值不覆盖,避免编辑其它项时清空 + if v, ok := updates["kubeconfig"].(string); ok && v != "" { + db.Clusters[i].KubeConfig = v + if db.Clusters[i].KubeConfigPath != "" { + _ = os.WriteFile(db.Clusters[i].KubeConfigPath, []byte(v), 0600) + } + } + db.Clusters[i].UpdatedAt = time.Now() + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *ClusterService) Delete(id uint) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i, c := range db.Clusters { + if c.ID == id { + if c.KubeConfigPath != "" { + _ = os.Remove(c.KubeConfigPath) + } + db.Clusters = append(db.Clusters[:i], db.Clusters[i+1:]...) + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *ClusterService) Validate(id uint) { + cluster, err := s.Get(id) + if err != nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + info, err := GetClusterInfo(ctx, cluster) + if err != nil { + s.Update(id, map[string]interface{}{"status": "error"}) + return + } + + s.Update(id, map[string]interface{}{ + "status": "active", + "version": info.Version, + "nodesCount": len(info.Nodes), + "lastConnected": time.Now(), + "context": info.CurrentCtx, + }) +} diff --git a/backend/services/cluster_info.go b/backend/services/cluster_info.go new file mode 100644 index 0000000..79fed94 --- /dev/null +++ b/backend/services/cluster_info.go @@ -0,0 +1,164 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + + "lokideploy/models" +) + +type ClusterInfo struct { + Version string `json:"version"` + Nodes []NodeInfo `json:"nodes"` + Namespaces []string `json:"namespaces"` + Contexts []string `json:"contexts"` + CurrentCtx string `json:"currentContext"` +} + +type NodeInfo struct { + Name string `json:"name"` + Status string `json:"status"` + Roles []string `json:"roles"` + Version string `json:"version"` + OS string `json:"os"` + Arch string `json:"arch"` + InternalIP string `json:"internalIP"` + Labels map[string]string `json:"labels"` +} + +// GetClusterInfo 通过 kubectl 查询集群信息。 +// Token 接入的集群在创建时已构造等价 kubeconfig,因此 kubectl 同样适用。 +func GetClusterInfo(ctx context.Context, cluster *models.Cluster) (*ClusterInfo, error) { + kubeconfig, err := resolveKubeconfig(cluster) + if err != nil { + return nil, err + } + + kubeFile, err := os.CreateTemp("", "kube-*.yaml") + if err != nil { + return nil, err + } + defer os.Remove(kubeFile.Name()) + kubeFile.WriteString(kubeconfig) + kubeFile.Close() + + env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name())) + + info := &ClusterInfo{ + Nodes: []NodeInfo{}, + Namespaces: []string{}, + Contexts: []string{}, + } + + // Version(best-effort) + if versionOut, err := runCmd(ctx, env, "kubectl", "version", "-o", "json"); err == nil { + var versionData struct { + ServerVersion struct { + GitVersion string `json:"gitVersion"` + } `json:"serverVersion"` + } + json.Unmarshal(versionOut, &versionData) + info.Version = versionData.ServerVersion.GitVersion + } + + // Nodes + nodesOut, err := runCmd(ctx, env, "kubectl", "get", "nodes", "-o", "json") + if err != nil { + return nil, fmt.Errorf("获取节点列表失败: %v", err) + } + var nodesData struct { + Items []struct { + Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` + } `json:"metadata"` + Status struct { + Conditions []struct { + Type string `json:"type"` + Status string `json:"status"` + } `json:"conditions"` + NodeInfo struct { + KubeletVersion string `json:"kubeletVersion"` + OperatingSystem string `json:"operatingSystem"` + Architecture string `json:"architecture"` + } `json:"nodeInfo"` + Addresses []struct { + Type string `json:"type"` + Address string `json:"address"` + } `json:"addresses"` + } `json:"status"` + } `json:"items"` + } + json.Unmarshal(nodesOut, &nodesData) + + for _, item := range nodesData.Items { + status := "Unknown" + for _, cond := range item.Status.Conditions { + if cond.Type == "Ready" { + if cond.Status == "True" { + status = "Ready" + } else { + status = "NotReady" + } + break + } + } + var roles []string + for label := range item.Metadata.Labels { + if strings.HasPrefix(label, "node-role.kubernetes.io/") { + roles = append(roles, label[len("node-role.kubernetes.io/"):]) + } + } + if len(roles) == 0 { + roles = []string{"worker"} + } + internalIP := "" + for _, addr := range item.Status.Addresses { + if addr.Type == "InternalIP" { + internalIP = addr.Address + break + } + } + info.Nodes = append(info.Nodes, NodeInfo{ + Name: item.Metadata.Name, + Status: status, + Roles: roles, + Version: item.Status.NodeInfo.KubeletVersion, + OS: item.Status.NodeInfo.OperatingSystem, + Arch: item.Status.NodeInfo.Architecture, + InternalIP: internalIP, + Labels: item.Metadata.Labels, + }) + } + + // Namespaces(best-effort) + if nsOut, err := runCmd(ctx, env, "kubectl", "get", "ns", "-o", "jsonpath={.items[*].metadata.name}"); err == nil { + info.Namespaces = strings.Fields(string(nsOut)) + } + + // Contexts(best-effort) + if ctxOut, err := runCmd(ctx, env, "kubectl", "config", "get-contexts", "-o", "name"); err == nil { + for _, line := range strings.Split(strings.TrimSpace(string(ctxOut)), "\n") { + if line = strings.TrimSpace(line); line != "" { + info.Contexts = append(info.Contexts, line) + } + } + } + + // Current context(best-effort) + if currentCtxOut, err := runCmd(ctx, env, "kubectl", "config", "current-context"); err == nil { + info.CurrentCtx = strings.TrimSpace(string(currentCtxOut)) + } + + return info, nil +} + +func runCmd(ctx context.Context, env []string, name string, args ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Env = env + return cmd.CombinedOutput() +} diff --git a/backend/services/deployment.go b/backend/services/deployment.go new file mode 100644 index 0000000..a6da0bf --- /dev/null +++ b/backend/services/deployment.go @@ -0,0 +1,347 @@ +package services + +import ( + "context" + "fmt" + "time" + + "lokideploy/models" + "lokideploy/utils" +) + +type DeploymentService struct{} + +func NewDeploymentService() *DeploymentService { + return &DeploymentService{} +} + +type CreateDeploymentRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Version string `json:"version"` + Mode string `json:"mode" binding:"required"` + ClusterID *uint `json:"clusterID"` + HostID *uint `json:"hostID"` + Namespace string `json:"namespace"` + StorageConfigID uint `json:"storageConfigID"` + AuthEnabled bool `json:"authEnabled"` + RetentionDays int `json:"retentionDays"` + Replicas int `json:"replicas"` + ResourcesCPU string `json:"resourcesCPU"` + ResourcesMem string `json:"resourcesMem"` + StorageSize string `json:"storageSize"` +} + +func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDeployment, error) { + db := models.GetDB() + d := models.LokiDeployment{ + ID: db.NextID("deployments"), + Name: req.Name, + Description: req.Description, + Version: req.Version, + Mode: req.Mode, + ClusterID: req.ClusterID, + HostID: req.HostID, + Namespace: req.Namespace, + StorageConfigID: req.StorageConfigID, + AuthEnabled: req.AuthEnabled, + RetentionDays: req.RetentionDays, + Replicas: req.Replicas, + ResourcesCPU: req.ResourcesCPU, + ResourcesMem: req.ResourcesMem, + StorageSize: req.StorageSize, + Status: "pending", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + d.BeforeCreate() + + // Load relations + if d.ClusterID != nil && *d.ClusterID > 0 { + for i := range db.Clusters { + if db.Clusters[i].ID == *d.ClusterID { + c := db.Clusters[i] + d.Cluster = &c + break + } + } + } + if d.HostID != nil && *d.HostID > 0 { + for i := range db.Hosts { + if db.Hosts[i].ID == *d.HostID { + h := db.Hosts[i] + d.Host = &h + break + } + } + } + if d.StorageConfigID > 0 { + for i := range db.Storages { + if db.Storages[i].ID == d.StorageConfigID { + st := db.Storages[i] + d.StorageConfig = &st + break + } + } + } + + db.Lock() + db.Deployments = append(db.Deployments, d) + db.Unlock() + go db.Save() + + return &d, nil +} + +func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Deployments { + if db.Deployments[i].ID == id { + d := db.Deployments[i] + s.loadRelations(&d) + return &d, nil + } + } + return nil, utils.ErrNotFound +} + +func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, int64, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + total := int64(len(db.Deployments)) + start := (page - 1) * pageSize + if start < 0 || start >= len(db.Deployments) { + return []models.LokiDeployment{}, total, nil + } + end := start + pageSize + if end > len(db.Deployments) { + end = len(db.Deployments) + } + result := make([]models.LokiDeployment, end-start) + copy(result, db.Deployments[start:end]) + for i := range result { + s.loadRelations(&result[i]) + } + return result, total, nil +} + +func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i := range db.Deployments { + if db.Deployments[i].ID == id { + if v, ok := updates["status"].(string); ok { + db.Deployments[i].Status = v + } + if v, ok := updates["statusMessage"].(string); ok { + db.Deployments[i].StatusMessage = v + } + if v, ok := updates["endpoint"].(string); ok { + db.Deployments[i].Endpoint = v + } + db.Deployments[i].UpdatedAt = time.Now() + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *DeploymentService) Delete(id uint) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i, d := range db.Deployments { + if d.ID == id { + db.Deployments = append(db.Deployments[:i], db.Deployments[i+1:]...) + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *DeploymentService) StartDeploy(id uint) error { + deployment, err := s.Get(id) + if err != nil { + return err + } + if deployment.Status == "deploying" { + return fmt.Errorf("部署正在进行中,请勿重复操作") + } + s.Update(id, map[string]interface{}{"status": "deploying"}) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + logFunc := func(level, message, detail string) { + s.AddLog(id, level, message, detail) + } + + deployer := NewLokiDeployer(deployment, logFunc) + go func() { + err := deployer.Deploy(ctx) + if err != nil { + s.Update(id, map[string]interface{}{ + "status": "failed", + "statusMessage": fmt.Sprintf("Deployment failed: %v", err), + }) + s.AddLog(id, "error", fmt.Sprintf("Deployment failed: %v", err), "") + } else { + s.Update(id, map[string]interface{}{ + "status": "running", + "statusMessage": "Deployment completed successfully", + "endpoint": s.getEndpoint(deployment), + }) + s.AddLog(id, "success", "Deployment completed successfully", "") + } + }() + return nil +} + +func (s *DeploymentService) StopDeployment(id uint) error { + deployment, err := s.Get(id) + if err != nil { + return err + } + if deployment.Status != "running" && deployment.Status != "deploying" { + return fmt.Errorf("部署未在运行,无法停止") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + CleanupLoki(ctx, deployment) + return s.Update(id, map[string]interface{}{ + "status": "stopped", + "statusMessage": "Deployment stopped by user", + }) +} + +func (s *DeploymentService) RestartDeployment(id uint) error { + if err := s.StopDeployment(id); err != nil { + return err + } + time.Sleep(2 * time.Second) + return s.StartDeploy(id) +} + +func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail string) { + db := models.GetDB() + log := models.DeploymentLog{ + ID: db.NextID("logs"), + DeploymentID: deploymentID, + Level: level, + Message: message, + Detail: detail, + CreatedAt: time.Now(), + } + db.Lock() + db.Logs = append(db.Logs, log) + db.Unlock() + go db.Save() +} + +func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]models.DeploymentLog, int64, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + var filtered []models.DeploymentLog + for i := len(db.Logs) - 1; i >= 0; i-- { + if db.Logs[i].DeploymentID == deploymentID { + filtered = append(filtered, db.Logs[i]) + } + } + + total := int64(len(filtered)) + start := (page - 1) * pageSize + if start >= len(filtered) { + return []models.DeploymentLog{}, total, nil + } + end := start + pageSize + if end > len(filtered) { + end = len(filtered) + } + return filtered[start:end], total, nil +} + +func (s *DeploymentService) GetStats() (map[string]interface{}, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + var running, failed, pending, stopped int64 + for _, d := range db.Deployments { + switch d.Status { + case "running": + running++ + case "failed": + failed++ + case "pending": + pending++ + case "stopped": + stopped++ + } + } + + return map[string]interface{}{ + "totalDeployments": int64(len(db.Deployments)), + "running": running, + "failed": failed, + "pending": pending, + "stopped": stopped, + "clusterCount": int64(len(db.Clusters)), + "hostCount": int64(len(db.Hosts)), + }, nil +} + +func (s *DeploymentService) loadRelations(d *models.LokiDeployment) { + db := models.GetDB() + if d.ClusterID != nil && *d.ClusterID > 0 { + for i := range db.Clusters { + if db.Clusters[i].ID == *d.ClusterID { + c := db.Clusters[i] + d.Cluster = &c + break + } + } + } + if d.HostID != nil && *d.HostID > 0 { + for i := range db.Hosts { + if db.Hosts[i].ID == *d.HostID { + h := db.Hosts[i] + d.Host = &h + break + } + } + } + if d.StorageConfigID > 0 { + for i := range db.Storages { + if db.Storages[i].ID == d.StorageConfigID { + st := db.Storages[i] + d.StorageConfig = &st + break + } + } + } +} + +func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string { + switch d.Mode { + case "k8s-helm", "k8s-yaml": + ns := d.Namespace + if ns == "" { + ns = "loki" + } + return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns) + case "host-binary": + if d.Host != nil { + return fmt.Sprintf("http://%s:3100", d.Host.HostIP) + } + } + return "" +} diff --git a/backend/services/host.go b/backend/services/host.go new file mode 100644 index 0000000..3e43d9e --- /dev/null +++ b/backend/services/host.go @@ -0,0 +1,177 @@ +package services + +import ( + "context" + "strings" + "time" + + "lokideploy/models" + "lokideploy/utils" +) + +type HostService struct{} + +func NewHostService() *HostService { + return &HostService{} +} + +func (s *HostService) Create(name, hostIP string, port int, user, authType, password, privateKey, keyPhrase, os, arch, labels string) (*models.Host, error) { + if port == 0 { + port = 22 + } + host := models.Host{ + ID: models.GetDB().NextID("hosts"), + Name: name, + HostIP: hostIP, + Port: port, + User: user, + AuthType: authType, + OS: os, + Arch: arch, + Labels: labels, + Status: "pending", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if password != "" { + enc, _ := utils.Encrypt(password) + host.Password = "ENC:" + enc + } + if privateKey != "" { + enc, _ := utils.Encrypt(privateKey) + host.PrivateKey = "ENC:" + enc + } + if keyPhrase != "" { + enc, _ := utils.Encrypt(keyPhrase) + host.KeyPhrase = "ENC:" + enc + } + + db := models.GetDB() + db.Lock() + db.Hosts = append(db.Hosts, host) + db.Unlock() + go db.Save() + + go s.Validate(host.ID) + return &host, nil +} + +func (s *HostService) Get(id uint) (*models.Host, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Hosts { + if db.Hosts[i].ID == id { + h := db.Hosts[i] + return &h, nil + } + } + return nil, utils.ErrNotFound +} + +func (s *HostService) List(page, pageSize int) ([]models.Host, int64, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + total := int64(len(db.Hosts)) + start := (page - 1) * pageSize + if start < 0 || start >= len(db.Hosts) { + return []models.Host{}, total, nil + } + end := start + pageSize + if end > len(db.Hosts) { + end = len(db.Hosts) + } + result := make([]models.Host, end-start) + copy(result, db.Hosts[start:end]) + return result, total, nil +} + +func (s *HostService) Update(id uint, updates map[string]interface{}) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i := range db.Hosts { + if db.Hosts[i].ID == id { + if v, ok := updates["name"].(string); ok { + db.Hosts[i].Name = v + } + if v, ok := updates["status"].(string); ok { + db.Hosts[i].Status = v + } + if v, ok := updates["os"].(string); ok { + db.Hosts[i].OS = v + } + if v, ok := updates["arch"].(string); ok { + db.Hosts[i].Arch = v + } + db.Hosts[i].UpdatedAt = time.Now() + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *HostService) Delete(id uint) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i, h := range db.Hosts { + if h.ID == id { + db.Hosts = append(db.Hosts[:i], db.Hosts[i+1:]...) + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *HostService) Validate(id uint) { + host, err := s.Get(id) + if err != nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + password := host.Password + if strings.HasPrefix(password, "ENC:") { + password, _ = utils.Decrypt(password[4:]) + } + + client, err := NewSSHClient(host.HostIP, host.Port, host.User, password) + if err != nil { + s.Update(id, map[string]interface{}{"status": "error"}) + return + } + defer client.Close() + + if info, err := client.GetHostInfo(ctx); err == nil { + s.Update(id, map[string]interface{}{"status": "active", "os": info.OS, "arch": info.Arch}) + } else { + s.Update(id, map[string]interface{}{"status": "active"}) + } +} + +func (s *HostService) ExecuteCommand(id uint, command string) (string, string, error) { + host, err := s.Get(id) + if err != nil { + return "", "", err + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + password := host.Password + if strings.HasPrefix(password, "ENC:") { + password, _ = utils.Decrypt(password[4:]) + } + + client, err := NewSSHClient(host.HostIP, host.Port, host.User, password) + if err != nil { + return "", "", err + } + defer client.Close() + return client.Execute(ctx, command) +} diff --git a/backend/services/k8s_client.go b/backend/services/k8s_client.go new file mode 100644 index 0000000..a947a89 --- /dev/null +++ b/backend/services/k8s_client.go @@ -0,0 +1,58 @@ +package services + +import ( + "fmt" + "os" + "regexp" + + "lokideploy/models" +) + +// resolveKubeconfig 取集群最终的 kubeconfig 文本(优先内存字段,其次磁盘文件) +func resolveKubeconfig(cluster *models.Cluster) (string, error) { + if cluster.KubeConfig != "" { + return cluster.KubeConfig, nil + } + if cluster.KubeConfigPath != "" { + data, err := os.ReadFile(cluster.KubeConfigPath) + if err != nil { + return "", fmt.Errorf("读取 kubeconfig 文件失败: %v", err) + } + return string(data), nil + } + return "", fmt.Errorf("集群未配置 kubeconfig") +} + +// BuildKubeconfigFromToken 用 API Server + Token 构造最小 kubeconfig +// 采用 insecure-skip-tls-verify,便于内网/自签证书集群接入 +func BuildKubeconfigFromToken(apiServer, token string) string { + return fmt.Sprintf(`apiVersion: v1 +kind: Config +clusters: +- cluster: + insecure-skip-tls-verify: true + server: %s + name: loki-cluster +contexts: +- context: + cluster: loki-cluster + user: loki-user + name: loki-context +current-context: loki-context +users: +- name: loki-user + user: + token: %s +`, apiServer, token) +} + +var apiServerRe = regexp.MustCompile(`(?m)^\s*server:\s*(\S+)`) + +// ExtractAPIServer 从 kubeconfig 文本提取 server 地址(展示用) +func ExtractAPIServer(kubeconfig string) string { + m := apiServerRe.FindStringSubmatch(kubeconfig) + if len(m) >= 2 { + return m[1] + } + return "" +} diff --git a/backend/services/loki_deployer.go b/backend/services/loki_deployer.go new file mode 100644 index 0000000..383b660 --- /dev/null +++ b/backend/services/loki_deployer.go @@ -0,0 +1,287 @@ +package services + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "lokideploy/models" + "lokideploy/utils" +) + +type LokiDeployer struct { + deployment *models.LokiDeployment + logFunc func(level, message, detail string) +} + +func NewLokiDeployer(deployment *models.LokiDeployment, logFunc func(level, message, detail string)) *LokiDeployer { + return &LokiDeployer{deployment: deployment, logFunc: logFunc} +} + +func (d *LokiDeployer) Deploy(ctx context.Context) error { + d.log("info", fmt.Sprintf("Starting Loki deployment: %s (mode: %s)", d.deployment.Name, d.deployment.Mode), "") + switch d.deployment.Mode { + case "k8s-helm": + return d.deployHelm(ctx) + case "k8s-yaml": + return d.deployYaml(ctx) + case "host-binary": + return d.deployBinary(ctx) + default: + return fmt.Errorf("不支持的部署模式: %s", d.deployment.Mode) + } +} + +func (d *LokiDeployer) deployHelm(ctx context.Context) error { + if d.deployment.Cluster == nil { + return fmt.Errorf("请选择目标集群") + } + kubeconfig := d.deployment.Cluster.KubeConfig + if kubeconfig == "" && d.deployment.Cluster.KubeConfigPath != "" { + data, _ := os.ReadFile(d.deployment.Cluster.KubeConfigPath) + kubeconfig = string(data) + } + + kubeFile, _ := os.CreateTemp("", "kube-*.yaml") + defer os.Remove(kubeFile.Name()) + kubeFile.WriteString(kubeconfig) + kubeFile.Close() + + env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name())) + ns := d.deployment.Namespace + if ns == "" { + ns = "loki" + } + + d.log("info", fmt.Sprintf("Creating namespace %s...", ns), "") + exec.CommandContext(ctx, "kubectl", "create", "namespace", ns, "--dry-run=client", "-o", "yaml").CombinedOutput() + + d.log("info", "Adding Grafana Helm repo...", "") + exec.CommandContext(ctx, "helm", "repo", "add", "grafana", "https://grafana.github.io/helm-charts").CombinedOutput() + exec.CommandContext(ctx, "helm", "repo", "update").CombinedOutput() + + d.log("info", fmt.Sprintf("Installing Loki in %s...", ns), "") + releaseName := d.deployment.Name + if releaseName == "" { + releaseName = "loki" + } + + chart := "grafana/loki" + if d.deployment.Replicas > 1 { + chart = "grafana/loki-distributed" + } + + cmd := exec.CommandContext(ctx, "helm", "upgrade", "--install", releaseName, chart, + "--namespace", ns, "--create-namespace", "--wait", "--timeout", "10m") + cmd.Env = env + out, err := cmd.CombinedOutput() + if err != nil { + d.log("error", fmt.Sprintf("Helm install failed: %v", err), string(out)) + return err + } + d.log("success", "Loki installed via Helm", string(out)) + return nil +} + +func (d *LokiDeployer) deployYaml(ctx context.Context) error { + if d.deployment.Cluster == nil { + return fmt.Errorf("请选择目标集群") + } + kubeconfig := d.deployment.Cluster.KubeConfig + if kubeconfig == "" && d.deployment.Cluster.KubeConfigPath != "" { + data, _ := os.ReadFile(d.deployment.Cluster.KubeConfigPath) + kubeconfig = string(data) + } + + kubeFile, _ := os.CreateTemp("", "kube-*.yaml") + defer os.Remove(kubeFile.Name()) + kubeFile.WriteString(kubeconfig) + kubeFile.Close() + + env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name())) + ns := d.deployment.Namespace + if ns == "" { + ns = "loki" + } + + manifest := d.generateManifest(ns) + manifestFile, _ := os.CreateTemp("", "loki-*.yaml") + defer os.Remove(manifestFile.Name()) + manifestFile.WriteString(manifest) + manifestFile.Close() + + cmd := exec.CommandContext(ctx, "kubectl", "apply", "-f", manifestFile.Name(), "-n", ns) + cmd.Env = env + out, err := cmd.CombinedOutput() + if err != nil { + d.log("error", fmt.Sprintf("kubectl apply failed: %v", err), string(out)) + return err + } + d.log("success", "Loki deployed via YAML", string(out)) + return nil +} + +func (d *LokiDeployer) deployBinary(ctx context.Context) error { + if d.deployment.Host == nil { + return fmt.Errorf("请选择目标主机") + } + host := d.deployment.Host + password := host.Password + if strings.HasPrefix(password, "ENC:") { + password, _ = DecryptCred(password) + } + + client, err := NewSSHClient(host.HostIP, host.Port, host.User, password) + if err != nil { + return err + } + defer client.Close() + + d.log("info", "SSH connected, downloading Loki...", "") + arch, _, _ := client.Execute(ctx, "uname -m") + lokiArch := strings.TrimSpace(arch) + switch lokiArch { + case "x86_64": + lokiArch = "amd64" + case "aarch64": + lokiArch = "arm64" + } + + version := d.deployment.Version + if version == "" { + version = "3.0.0" + } + + client.Execute(ctx, "mkdir -p /opt/loki /var/lib/loki /etc/loki") + + downloadURL := fmt.Sprintf("https://github.com/grafana/loki/releases/download/v%s/loki-linux-%s.zip", version, lokiArch) + cmd := fmt.Sprintf("cd /tmp && curl -sL -o loki.zip '%s' && unzip -o loki.zip -d /opt/loki/ && chmod +x /opt/loki/loki-linux-%s", downloadURL, lokiArch) + client.Execute(ctx, cmd) + client.Execute(ctx, "ln -sf /opt/loki/loki-linux-* /usr/local/bin/loki 2>/dev/null || true") + + // Upload config + config := d.generateLokiConfig() + client.Execute(ctx, fmt.Sprintf("cat > /etc/loki/loki.yaml << 'EOF'\n%s\nEOF", config)) + + // Systemd service + service := `[Unit] +Description=Grafana Loki +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/loki -config.file=/etc/loki/loki.yaml +Restart=always + +[Install] +WantedBy=multi-user.target +` + client.Execute(ctx, fmt.Sprintf("cat > /etc/systemd/system/loki.service << 'EOF'\n%s\nEOF", service)) + client.Execute(ctx, "systemctl daemon-reload && systemctl enable loki && systemctl restart loki") + + d.log("success", fmt.Sprintf("Loki deployed on %s", host.HostIP), "") + return nil +} + +func (d *LokiDeployer) generateManifest(ns string) string { + return fmt.Sprintf(`apiVersion: v1 +kind: Namespace +metadata: + name: %s +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: loki + namespace: %s +spec: + replicas: 1 + selector: + matchLabels: + app: loki + template: + metadata: + labels: + app: loki + spec: + containers: + - name: loki + image: grafana/loki:%s + ports: + - containerPort: 3100 +--- +apiVersion: v1 +kind: Service +metadata: + name: loki + namespace: %s +spec: + ports: + - port: 3100 + selector: + app: loki +`, ns, ns, d.deployment.Version, ns) +} + +func (d *LokiDeployer) generateLokiConfig() string { + return fmt.Sprintf(`auth_enabled: false +server: + http_listen_port: 3100 +limits_config: + retention_period: %dh +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h +`, d.deployment.RetentionDays*24) +} + +func (d *LokiDeployer) log(level, message, detail string) { + if d.logFunc != nil { + d.logFunc(level, message, detail) + } +} + +func CleanupLoki(ctx context.Context, deployment *models.LokiDeployment) { + if deployment.Mode == "k8s-helm" && deployment.Cluster != nil { + kubeconfig := deployment.Cluster.KubeConfig + if kubeconfig == "" && deployment.Cluster.KubeConfigPath != "" { + data, _ := os.ReadFile(deployment.Cluster.KubeConfigPath) + kubeconfig = string(data) + } + kubeFile, _ := os.CreateTemp("", "kube-*.yaml") + defer os.Remove(kubeFile.Name()) + os.WriteFile(kubeFile.Name(), []byte(kubeconfig), 0600) + env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name())) + cmd := exec.CommandContext(ctx, "helm", "uninstall", deployment.Name, "--namespace", deployment.Namespace) + cmd.Env = env + cmd.CombinedOutput() + } + if deployment.Mode == "host-binary" && deployment.Host != nil { + host := deployment.Host + password := host.Password + if strings.HasPrefix(password, "ENC:") { + password, _ = DecryptCred(password) + } + client, _ := NewSSHClient(host.HostIP, host.Port, host.User, password) + if client != nil { + defer client.Close() + client.Execute(ctx, "systemctl stop loki && systemctl disable loki && rm -f /etc/systemd/system/loki.service /usr/local/bin/loki") + } + } +} + +func DecryptCred(encrypted string) (string, error) { + if strings.HasPrefix(encrypted, "ENC:") { + return utils.Decrypt(encrypted[4:]) + } + return encrypted, nil +} diff --git a/backend/services/ssh.go b/backend/services/ssh.go new file mode 100644 index 0000000..7011506 --- /dev/null +++ b/backend/services/ssh.go @@ -0,0 +1,168 @@ +package services + +import ( + "bytes" + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "golang.org/x/crypto/ssh" +) + +type SSHClient struct { + client *ssh.Client +} + +type HostInfo struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + Version string `json:"version"` + Arch string `json:"arch"` + Kernel string `json:"kernel"` + CPU string `json:"cpu"` + Memory string `json:"memory"` + Disk string `json:"disk"` + Uptime string `json:"uptime"` + + // 资源使用率(百分比) + CPUUsage float64 `json:"cpuUsage"` + MemUsage float64 `json:"memUsage"` + DiskUsage float64 `json:"diskUsage"` + // 资源已用/总量 + MemTotal string `json:"memTotal"` + MemUsed string `json:"memUsed"` + DiskTotal string `json:"diskTotal"` + DiskUsed string `json:"diskUsed"` +} + +func NewSSHClient(host string, port int, user, password string) (*SSHClient, error) { + if port == 0 { + port = 22 + } + config := &ssh.ClientConfig{ + User: user, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, + Auth: []ssh.AuthMethod{ssh.Password(password)}, + } + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), config) + if err != nil { + return nil, errors.New(FormatSSHError(err)) + } + return &SSHClient{client: client}, nil +} + +func (c *SSHClient) Execute(ctx context.Context, command string) (string, string, error) { + session, err := c.client.NewSession() + if err != nil { + return "", "", err + } + defer session.Close() + + var stdout, stderr bytes.Buffer + session.Stdout = &stdout + session.Stderr = &stderr + + done := make(chan error, 1) + go func() { done <- session.Run(command) }() + + select { + case err := <-done: + return stdout.String(), stderr.String(), err + case <-ctx.Done(): + session.Signal(ssh.SIGKILL) + return "", "", ctx.Err() + } +} + +// GetHostInfo 采集主机静态信息与资源使用率 +func (c *SSHClient) GetHostInfo(ctx context.Context) (*HostInfo, error) { + info := &HostInfo{} + + // 静态信息(逐条,best-effort) + if out, _, err := c.Execute(ctx, "hostname"); err == nil { + info.Hostname = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, `cat /etc/os-release | grep '^ID=' | cut -d= -f2 | tr -d '"'`); err == nil { + info.OS = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, `cat /etc/os-release | grep 'VERSION_ID=' | cut -d= -f2 | tr -d '"'`); err == nil { + info.Version = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, "uname -m"); err == nil { + info.Arch = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, "uname -r"); err == nil { + info.Kernel = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, "nproc"); err == nil { + info.CPU = strings.TrimSpace(out) + " 核" + } + if out, _, err := c.Execute(ctx, "free -h | awk '/^Mem:/ {print $2}'"); err == nil { + info.Memory = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, "df -h / | awk 'NR==2 {print $2}'"); err == nil { + info.Disk = strings.TrimSpace(out) + } + if out, _, err := c.Execute(ctx, "uptime -p 2>/dev/null || uptime"); err == nil { + info.Uptime = strings.TrimSpace(out) + } + + // 资源使用率:一次合并命令(减少 SSH 往返) + // 输出形如:CPUUSAGE:12.3 / MEM:16384:8192 / DISK:51200:25600:50% + metricScript := `top -bn1 2>/dev/null | awk '/Cpu\(s\)/{print "CPUUSAGE:"100-$8; exit}' +free -m | awk '/^Mem:/{print "MEM:"$2":"$3}' +df -m / | awk 'NR==2{print "DISK:"$2":"$3":"$5}'` + if out, _, err := c.Execute(ctx, metricScript); err == nil { + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + switch { + case strings.HasPrefix(line, "CPUUSAGE:"): + info.CPUUsage, _ = strconv.ParseFloat(strings.TrimPrefix(line, "CPUUSAGE:"), 64) + case strings.HasPrefix(line, "MEM:"): + parts := strings.Split(strings.TrimPrefix(line, "MEM:"), ":") + if len(parts) == 2 { + total, _ := strconv.Atoi(parts[0]) + used, _ := strconv.Atoi(parts[1]) + info.MemTotal = fmt.Sprintf("%d MB", total) + info.MemUsed = fmt.Sprintf("%d MB", used) + if total > 0 { + info.MemUsage = float64(used) / float64(total) * 100 + } + } + case strings.HasPrefix(line, "DISK:"): + parts := strings.Split(strings.TrimPrefix(line, "DISK:"), ":") + if len(parts) >= 3 { + info.DiskTotal = parts[0] + " MB" + info.DiskUsed = parts[1] + " MB" + pct := strings.TrimSuffix(strings.TrimSpace(parts[2]), "%") + info.DiskUsage, _ = strconv.ParseFloat(pct, 64) + } + } + } + } + + return info, nil +} + +func (c *SSHClient) Close() error { + if c.client != nil { + return c.client.Close() + } + return nil +} + +func TestSSHConnection(host string, port int, user, password string) error { + client, err := NewSSHClient(host, port, user, password) + if err != nil { + return err + } + defer client.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, _, err = client.Execute(ctx, "echo 'connected'") + return err +} diff --git a/backend/services/ssh_errors.go b/backend/services/ssh_errors.go new file mode 100644 index 0000000..1b8b101 --- /dev/null +++ b/backend/services/ssh_errors.go @@ -0,0 +1,33 @@ +package services + +import "strings" + +// FormatSSHError 将底层 SSH 错误翻译为中文友好提示,便于用户排查 +func FormatSSHError(err error) string { + if err == nil { + return "" + } + msg := err.Error() + lower := strings.ToLower(msg) + + switch { + case strings.Contains(lower, "i/o timeout") || + strings.Contains(lower, "deadline exceeded") || + strings.Contains(lower, "connection timed out"): + return "连接超时,请检查主机网络或 SSH 端口是否可达" + case strings.Contains(lower, "connection refused"): + return "连接被拒绝,目标端口未开放或 SSH 服务未运行" + case strings.Contains(lower, "no such host") || + strings.Contains(lower, "no route to host"): + return "无法解析或到达目标主机,请检查 IP 地址" + case strings.Contains(lower, "permission denied") || + strings.Contains(lower, "unable to authenticate") || + strings.Contains(lower, "handshake") || + strings.Contains(lower, "password"): + return "认证失败,请检查用户名、密码或 SSH 密钥" + case strings.Contains(lower, "no supported methods"): + return "SSH 协商失败,服务端未支持当前认证方式" + default: + return msg + } +} diff --git a/backend/services/storage.go b/backend/services/storage.go new file mode 100644 index 0000000..61bfae0 --- /dev/null +++ b/backend/services/storage.go @@ -0,0 +1,143 @@ +package services + +import ( + "time" + + "lokideploy/models" + "lokideploy/utils" +) + +type StorageService struct{} + +func NewStorageService() *StorageService { + return &StorageService{} +} + +func (s *StorageService) Create(name, storageType, endpoint, bucket, region, accessKey, secretKey, pathPrefix string, insecure, isDefault bool) (*models.StorageConfig, error) { + encAccessKey, _ := utils.Encrypt(accessKey) + encSecretKey, _ := utils.Encrypt(secretKey) + + storage := models.StorageConfig{ + ID: models.GetDB().NextID("storages"), + Name: name, + StorageType: storageType, + Endpoint: endpoint, + Bucket: bucket, + Region: region, + AccessKey: "ENC:" + encAccessKey, + SecretKey: "ENC:" + encSecretKey, + PathPrefix: pathPrefix, + Insecure: insecure, + IsDefault: isDefault, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + db := models.GetDB() + if isDefault { + db.Lock() + for i := range db.Storages { + db.Storages[i].IsDefault = false + } + db.Unlock() + } + db.Lock() + db.Storages = append(db.Storages, storage) + db.Unlock() + go db.Save() + + return &storage, nil +} + +func (s *StorageService) Get(id uint) (*models.StorageConfig, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Storages { + if db.Storages[i].ID == id { + st := db.Storages[i] + return &st, nil + } + } + return nil, utils.ErrNotFound +} + +func (s *StorageService) List(page, pageSize int) ([]models.StorageConfig, int64, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + + total := int64(len(db.Storages)) + start := (page - 1) * pageSize + if start < 0 || start >= len(db.Storages) { + return []models.StorageConfig{}, total, nil + } + end := start + pageSize + if end > len(db.Storages) { + end = len(db.Storages) + } + result := make([]models.StorageConfig, end-start) + copy(result, db.Storages[start:end]) + return result, total, nil +} + +func (s *StorageService) Update(id uint, updates map[string]interface{}) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i := range db.Storages { + if db.Storages[i].ID == id { + if v, ok := updates["name"].(string); ok { + db.Storages[i].Name = v + } + if v, ok := updates["endpoint"].(string); ok { + db.Storages[i].Endpoint = v + } + if v, ok := updates["bucket"].(string); ok { + db.Storages[i].Bucket = v + } + if v, ok := updates["region"].(string); ok { + db.Storages[i].Region = v + } + if v, ok := updates["isDefault"].(bool); ok { + if v { + for j := range db.Storages { + db.Storages[j].IsDefault = false + } + } + db.Storages[i].IsDefault = v + } + db.Storages[i].UpdatedAt = time.Now() + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *StorageService) Delete(id uint) error { + db := models.GetDB() + db.Lock() + defer db.Unlock() + for i, st := range db.Storages { + if st.ID == id { + db.Storages = append(db.Storages[:i], db.Storages[i+1:]...) + go db.Save() + return nil + } + } + return utils.ErrNotFound +} + +func (s *StorageService) GetDefault() (*models.StorageConfig, error) { + db := models.GetDB() + db.RLock() + defer db.RUnlock() + for i := range db.Storages { + if db.Storages[i].IsDefault { + st := db.Storages[i] + return &st, nil + } + } + return nil, utils.ErrNotFound +} diff --git a/backend/utils/crypto.go b/backend/utils/crypto.go new file mode 100644 index 0000000..f5f9acd --- /dev/null +++ b/backend/utils/crypto.go @@ -0,0 +1,79 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "io" + + "lokideploy/config" + + "golang.org/x/crypto/argon2" +) + +func HashPassword(password string) (string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", err + } + hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32) + combined := append(salt, hash...) + return base64.StdEncoding.EncodeToString(combined), nil +} + +func VerifyPassword(password, hashStr string) bool { + combined, err := base64.StdEncoding.DecodeString(hashStr) + if err != nil || len(combined) < 16 { + return false + } + salt := combined[:16] + hash := combined[16:] + newHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32) + return subtle.ConstantTimeCompare(hash, newHash) == 1 +} + +func Encrypt(plaintext string) (string, error) { + key := []byte(config.GetConfig().JWTSecret)[:32] + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func Decrypt(ciphertext string) (string, error) { + key := []byte(config.GetConfig().JWTSecret)[:32] + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + if len(data) < gcm.NonceSize() { + return "", errors.New("ciphertext too short") + } + nonce, ciphertextBytes := data[:gcm.NonceSize()], data[gcm.NonceSize():] + plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) + if err != nil { + return "", err + } + return string(plaintext), nil +} diff --git a/backend/utils/errors.go b/backend/utils/errors.go new file mode 100644 index 0000000..48627d7 --- /dev/null +++ b/backend/utils/errors.go @@ -0,0 +1,11 @@ +package utils + +import "errors" + +var ( + ErrInvalidCredentials = errors.New("用户名或密码错误") + ErrNotFound = errors.New("资源不存在") + ErrAlreadyExists = errors.New("资源已存在") + ErrUnauthorized = errors.New("未登录或登录已过期") + ErrForbidden = errors.New("没有操作权限") +) diff --git a/backend/utils/jwt.go b/backend/utils/jwt.go new file mode 100644 index 0000000..00df823 --- /dev/null +++ b/backend/utils/jwt.go @@ -0,0 +1,62 @@ +package utils + +import ( + "errors" + "time" + + "lokideploy/config" + + "github.com/golang-jwt/jwt/v5" + "github.com/gin-gonic/gin" +) + +type Claims struct { + UserID uint `json:"userId"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func GenerateToken(userID uint, username, role string) (string, error) { + cfg := config.GetConfig() + claims := Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: username, + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(cfg.JWTSecret)) +} + +func ParseToken(tokenString string) (*Claims, error) { + cfg := config.GetConfig() + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(cfg.JWTSecret), nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("invalid token") +} + +func GetUserFromContext(c *gin.Context) (*Claims, bool) { + user, exists := c.Get("user") + if !exists { + return nil, false + } + claims, ok := user.(*Claims) + return claims, ok +} + +func IsAdmin(c *gin.Context) bool { + claims, ok := GetUserFromContext(c) + return ok && claims.Role == "admin" +} diff --git a/backend/utils/response.go b/backend/utils/response.go new file mode 100644 index 0000000..54f4e24 --- /dev/null +++ b/backend/utils/response.go @@ -0,0 +1,81 @@ +package utils + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +type PageData struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{Code: 0, Message: "success", Data: data}) +} + +func Error(c *gin.Context, code int, message string) { + c.JSON(http.StatusOK, Response{Code: code, Message: message}) +} + +func BadRequest(c *gin.Context, message string) { + Error(c, 400, message) +} + +func Unauthorized(c *gin.Context, message string) { + if message == "" { + message = "未登录或登录已过期" + } + c.JSON(http.StatusUnauthorized, Response{Code: 401, Message: message}) +} + +func Forbidden(c *gin.Context, message string) { + if message == "" { + message = "没有操作权限" + } + c.JSON(http.StatusForbidden, Response{Code: 403, Message: message}) +} + +func NotFound(c *gin.Context, message string) { + if message == "" { + message = "资源不存在" + } + Error(c, 404, message) +} + +func Pagination(c *gin.Context) (page, pageSize int) { + page, pageSize = 1, 10 + if p := c.Query("page"); p != "" { + if v, err := strconv.Atoi(p); err == nil && v > 0 { + page = v + } + } + if ps := c.Query("pageSize"); ps != "" { + if v, err := strconv.Atoi(ps); err == nil && v > 0 && v <= 100 { + pageSize = v + } + } + return +} + +func IntParam(c *gin.Context, key string) (int, error) { + return strconv.Atoi(c.Param(key)) +} + +func BindAndValidate(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindJSON(obj); err != nil { + BadRequest(c, "请求参数无效: "+err.Error()) + return false + } + return true +} diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..bbef45d --- /dev/null +++ b/build.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# LokiDeploy Build Script +# Builds frontend and backend into a single deployable package + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo "========================================" +echo " LokiDeploy Build Script" +echo "========================================" + +# 1. Build Frontend +echo "[1/2] Building frontend..." +cd "$SCRIPT_DIR/frontend" +if [ ! -d "node_modules" ]; then + echo " Installing dependencies..." + npm ci +fi +npm run build +echo " Frontend built: frontend/dist/" + +# 2. Build Backend +echo "[2/2] Building backend..." +cd "$SCRIPT_DIR/backend" + +# Detect Go +if command -v go &> /dev/null; then + GO_CMD="go" +elif [ -f "$HOME/go/bin/go" ]; then + GO_CMD="$HOME/go/bin/go" +else + echo "ERROR: Go not found. Please install Go 1.23+" + echo " wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz" + echo " tar -C $HOME -xzf go1.23.4.linux-amd64.tar.gz" + echo " export PATH=\$HOME/go/bin:\$PATH" + exit 1 +fi + +export CGO_ENABLED=0 +export GOPROXY=https://goproxy.cn,direct +$GO_CMD build -ldflags="-s -w" -o bin/lokideploy . + +echo " Backend built: bin/lokideploy" + +# 3. Copy dist to backend for embedded serving +cd "$SCRIPT_DIR" +rm -rf backend/dist +cp -r frontend/dist backend/dist + +# 4. Summary +echo "" +echo "========================================" +echo " Build Complete!" +echo "========================================" +echo " Frontend: backend/dist/" +echo " Backend: backend/bin/lokideploy" +echo " Size: $(du -h backend/bin/lokideploy | cut -f1)" +echo "" +echo " Start with: ./run.sh" +echo "========================================" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..916bd69 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + lokideploy: + build: + context: . + dockerfile: Dockerfile + container_name: lokideploy + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - ./data:/app/data + - ~/.kube:/root/.kube:ro + environment: + - SERVER_PORT=8080 + - DATA_DIR=/app/data + - JWT_SECRET=change-me-in-production + networks: + - lokideploy-net + +networks: + lokideploy-net: + driver: bridge diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..5b391d0 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +# API URL - leave empty for same-origin (proxy mode), or set to backend URL +VITE_API_URL= diff --git a/frontend/.frontend.pid b/frontend/.frontend.pid new file mode 100644 index 0000000..c87407e --- /dev/null +++ b/frontend/.frontend.pid @@ -0,0 +1 @@ +46900 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..1f5d23f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Dependencies +node_modules + +# Build output +dist +dist-ssr + +# Local env overrides +*.local +.env.local +.env.*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..411f770 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "postcss.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..93de5c1 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + LokiDeploy + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9da042c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5866 @@ +{ + "name": "my-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-app", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.562.0", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-day-picker": "^9.13.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.70.0", + "react-resizable-panels": "^4.2.2", + "react-router": "^7.6.1", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "vaul": "^1.1.2", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self/node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source/node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "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.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/type-utils": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.52.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.52.0", + "@typescript-eslint/types": "^8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.52.0", + "@typescript-eslint/tsconfig-utils": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/input-otp": { + "version": "1.4.2", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.13.0", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-hook-form": { + "version": "7.70.0", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "4.2.2", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.52.0", + "@typescript-eslint/parser": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/vaul": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..869ab4c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,79 @@ +{ + "name": "my-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.562.0", + "next-themes": "^0.4.6", + "react": "^19.2.0", + "react-day-picker": "^9.13.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.70.0", + "react-resizable-panels": "^4.2.2", + "react-router": "^7.6.1", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "vaul": "^1.1.2", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..8aa626c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,74 @@ +import { Routes, Route, Navigate } from 'react-router' +import Layout from '@/components/Layout' +import Login from '@/pages/Login' +import Dashboard from '@/pages/Dashboard' +import Clusters from '@/pages/Clusters' +import Hosts from '@/pages/Hosts' +import Storage from '@/pages/Storage' +import Deployments from '@/pages/Deployments' +import Settings from '@/pages/Settings' + +function RequireAuth({ children }: { children: React.ReactNode }) { + const token = localStorage.getItem('token') + if (!token) { + return + } + return {children} +} + +export default function App() { + return ( + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + ) +} diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..083b702 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,58 @@ +import { Component, type ReactNode } from 'react' + +interface State { + hasError: boolean + error?: Error +} + +// 捕获子树渲染错误,避免整页白屏;显示错误信息便于排查 +export default class ErrorBoundary extends Component<{ children: ReactNode }, State> { + state: State = { hasError: false } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: { componentStack: string }) { + console.error('页面渲染错误:', error, info) + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined }) + } + + render() { + if (this.state.hasError) { + return ( +
+
+

页面渲染出错

+

+ 请把下方错误信息反馈给开发者,或尝试刷新页面: +

+
+              {this.state.error?.message || '未知错误'}
+              {'\n\n'}
+              {this.state.error?.stack}
+            
+
+ + +
+
+
+ ) + } + return this.props.children + } +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..14df5f7 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,143 @@ +import { Link, useLocation, useNavigate } from 'react-router' +import { + LayoutDashboard, + Server, + Monitor, + HardDrive, + Rocket, + Settings, + LogOut, + PanelLeftClose, + PanelLeftOpen, + Shield, + ChevronRight, + Activity, +} from 'lucide-react' +import { useState, useEffect } from 'react' + + +const menuItems = [ + { path: '/', icon: LayoutDashboard, label: '仪表盘' }, + { path: '/clusters', icon: Server, label: '集群管理' }, + { path: '/hosts', icon: Monitor, label: '主机管理' }, + { path: '/storage', icon: HardDrive, label: '存储配置' }, + { path: '/deployments', icon: Rocket, label: '部署管理' }, + { path: '/settings', icon: Settings, label: '系统设置' }, +] + +export default function Layout({ children }: { children: React.ReactNode }) { + const [sidebarOpen, setSidebarOpen] = useState(true) + const [user, setUser] = useState<{ username: string; role: string } | null>(null) + const location = useLocation() + const navigate = useNavigate() + + useEffect(() => { + const stored = localStorage.getItem('user') + if (stored) { + setUser(JSON.parse(stored)) + } + }, []) + + const handleLogout = () => { + localStorage.removeItem('token') + localStorage.removeItem('user') + navigate('/login') + } + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Top Bar */} +
+

+ {menuItems.find((item) => item.path === location.pathname)?.label || '仪表盘'} +

+
+ {user && ( +
+
+ + {user.username} + + {user.role === 'admin' ? '管理员' : user.role} + +
+ +
+ )} +
+
+ + {/* Page Content */} +
+ {children} +
+
+
+ ) +} diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx new file mode 100644 index 0000000..d21b65f --- /dev/null +++ b/frontend/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..935eecf --- /dev/null +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/frontend/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/src/components/ui/aspect-ratio.tsx b/frontend/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..3df3fd0 --- /dev/null +++ b/frontend/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..b7224f0 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..fd3a406 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return