diff --git a/.gitignore b/.gitignore index a9dfd43f3..01b0f24e1 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,18 @@ results/ .augment # Claude Code configuration (should not be committed) -CLAUDE.md \ No newline at end of file +CLAUDE.md + +# Dashboard frontend (React + TypeScript + Vite) +dashboard/frontend/node_modules/ +dashboard/frontend/dist/ +dashboard/frontend/build/ +dashboard/frontend/.vite/ +dashboard/frontend/*.local + +# Dashboard backend build artifacts +dashboard/backend/dashboard-backend +dashboard/backend/dashboard-backend.exe + +# Keep old HTML backup for reference +dashboard/frontend/index.html.old \ No newline at end of file diff --git a/Dockerfile.extproc b/Dockerfile.extproc index 4b019d15d..90e39dc78 100644 --- a/Dockerfile.extproc +++ b/Dockerfile.extproc @@ -57,13 +57,16 @@ FROM golang:1.24 AS go-builder WORKDIR /app +ENV GOPROXY=https://goproxy.cn,direct +ENV GOSUMDB=sum.golang.google.cn + # Copy Go module files first for better layer caching RUN mkdir -p src/semantic-router COPY src/semantic-router/go.mod src/semantic-router/go.sum src/semantic-router/ COPY candle-binding/go.mod candle-binding/semantic-router.go candle-binding/ -# Download Go dependencies (cached layer) -RUN cd src/semantic-router && go mod download +RUN cd src/semantic-router && go mod download && \ + cd /app/candle-binding && go mod download # Copy semantic-router source code COPY src/semantic-router/ src/semantic-router/ diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore new file mode 100644 index 000000000..e64bc867d --- /dev/null +++ b/dashboard/.dockerignore @@ -0,0 +1,33 @@ +# Node +node_modules/ +npm-debug.log +yarn-error.log +package-lock.json +yarn.lock + +# Build outputs +dist/ +build/ +.vite/ +.docusaurus/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temp +*.log +*.tmp +.cache/ + +# Keep necessary files +!dashboard/frontend/package.json +!dashboard/frontend/tsconfig*.json +!dashboard/frontend/vite.config.ts diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 000000000..46c05fe47 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,302 @@ +# Semantic Router Modern Dashboard + +Unified dashboard that brings together Configuration Management, an Interactive Playground, and Real-time Monitoring & Observability. It provides a single entry point across local, Docker Compose, and Kubernetes deployments. + +## Goals + +- Single landing page for new/existing users +- Embed Observability (Grafana/Prometheus) and Playground (Open WebUI) via iframes behind a single backend proxy for auth and CORS/CSP control +- Read-only configuration viewer powered by the existing Semantic Router Classification API +- Environment-agnostic: consistent URLs and behavior for local dev, Compose, and K8s + +## Whatโ€™s already in this repo (reused) + +- Prometheus + Grafana + - Docker Compose services in `docker-compose.yml` (ports: Prometheus 9090, Grafana 3000) + - Local observability in `docker-compose.obs.yml` (host network) + - K8s manifests under `deploy/kubernetes/observability/{prometheus,grafana}` + - Provisioned datasource and dashboard in `tools/observability/` +- Router metrics and API + - Metrics at `:9190/metrics` (Prometheus format) + - Classification API on `:8080` with endpoints like `GET /api/v1`, `GET /config/classification` +- Open WebUI integration + - Pipe in `tools/openwebui-pipe/vllm_semantic_router_pipe.py` + - Doc in `website/docs/tutorials/observability/open-webui-integration.md` + +These are sufficient to embed and proxyโ€”no need to duplicate core functionality. + +## Architecture + +### Frontend (React + TypeScript + Vite) + +Modern SPA built with: + +- **React 18** with TypeScript for type safety +- **Vite 5** for fast development and optimized builds +- **React Router v6** for client-side routing +- **CSS Modules** for scoped styling with theme support (dark/light mode) + +Pages: + +- **Monitoring** (`/monitoring`): Grafana dashboard embedding with custom path input +- **Config** (`/config`): Real-time configuration viewer with multiple endpoints +- **Playground** (`/playground`): Open WebUI interface for testing + +Features: + +- ๐ŸŒ“ Dark/Light theme toggle with localStorage persistence +- ๐Ÿ“ฑ Responsive design +- โšก Fast navigation with React Router +- ๐ŸŽจ Modern UI inspired by vLLM website design + +### Backend (Go HTTP Server) + +- Serves static frontend (Vite production build) +- Reverse proxy with auth/cors/csp controls: + - `GET /embedded/grafana/*` โ†’ Grafana + - `GET /embedded/prometheus/*` โ†’ Prometheus (optional link-outs) + - `GET /embedded/openwebui/*` โ†’ Open WebUI (optional) + - `GET /api/router/*` โ†’ Router Classification API (`:8080`) + - `GET /metrics/router` โ†’ Router `/metrics` (optional aggregation later) + - `GET /healthz` โ†’ Health check endpoint +- Normalizes headers for iframe embedding: strips/overrides `X-Frame-Options` and `Content-Security-Policy` frame-ancestors as needed +- SPA routing support: serves `index.html` for all non-asset routes +- Central point for JWT/OIDC in the future (forward or exchange tokens to upstreams) + +## Directory Layout + +``` +dashboard/ +โ”œโ”€โ”€ frontend/ # React + TypeScript SPA +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Reusable components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Layout.tsx # Main layout with header/nav +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Layout.module.css +โ”‚ โ”‚ โ”œโ”€โ”€ pages/ # Page components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ MonitoringPage.tsx # Grafana iframe with path control +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ConfigPage.tsx # Config viewer with API fetch +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ PlaygroundPage.tsx # Open WebUI iframe +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ *.module.css # Scoped styles per page +โ”‚ โ”‚ โ”œโ”€โ”€ App.tsx # Root component with routing +โ”‚ โ”‚ โ”œโ”€โ”€ main.tsx # Entry point +โ”‚ โ”‚ โ””โ”€โ”€ index.css # Global styles & CSS variables +โ”‚ โ”œโ”€โ”€ public/ # Static assets (vllm.png) +โ”‚ โ”œโ”€โ”€ package.json # Node dependencies +โ”‚ โ”œโ”€โ”€ tsconfig.json # TypeScript configuration +โ”‚ โ”œโ”€โ”€ vite.config.ts # Vite build configuration +โ”‚ โ””โ”€โ”€ index.html # SPA shell +โ”œโ”€โ”€ backend/ # Go reverse proxy server +โ”‚ โ”œโ”€โ”€ main.go # Proxy routes & static file server +โ”‚ โ”œโ”€โ”€ go.mod # Go module (minimal dependencies) +โ”‚ โ””โ”€โ”€ Dockerfile # Multi-stage build (Node + Go + Alpine) +โ”œโ”€โ”€ deploy/ +โ”‚ โ”œโ”€โ”€ docker/ # Docker Compose overlay (deprecated) +โ”‚ โ””โ”€โ”€ kubernetes/ # K8s manifests (Service/Ingress/ConfigMap) +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ RISKS.md # Security considerations +``` + +## Environment-agnostic configuration + +The backend exposes a single port (default 8700) and proxies to targets defined via environment variables. This keeps frontend URLs stable and avoids CORS by same-origining everything under the dashboard host. + +Required env vars (with sensible defaults per environment): + +- `DASHBOARD_PORT` (default: 8700) +- `TARGET_GRAFANA_URL` +- `TARGET_PROMETHEUS_URL` +- `TARGET_ROUTER_API_URL` (router `:8080`) +- `TARGET_ROUTER_METRICS_URL` (router `:9190/metrics`) +- `TARGET_OPENWEBUI_URL` (optional; enable playground tab only if present) +- `ALLOW_IFRAME_EMBED` (default: true; backend will remove/override frame-busting headers) + +Recommended upstream settings for embedding: + +- Grafana: set `GF_SECURITY_ALLOW_EMBEDDING=true` and prefer `access: proxy` datasource (already configured) +- Open WebUI: ensure CSP/frame-ancestors allows embedding, or rely on dashboard proxy to strip/override; configure Open WebUI auth/session to work under proxied path + +## URL strategy (stable, user-facing) + +- Dashboard Home: `http://:8700/` +- Monitoring tab: iframe `src="/embedded/grafana/d/?kiosk&theme=light"` +- Config tab: frontend fetch `GET /api/router/config/classification` +- Playground tab: iframe `src="/embedded/openwebui/"` (rendered only if `TARGET_OPENWEBUI_URL` is set) + +## Deployment matrix + +1) Local dev (router and observability on host) + +- Use `docker-compose.obs.yml` to start Prometheus (9090) and Grafana (3000) on host network +- Start dashboard backend locally (port 8700) +- Env examples: + - `TARGET_GRAFANA_URL=http://localhost:3000` + - `TARGET_PROMETHEUS_URL=http://localhost:9090` + - `TARGET_ROUTER_API_URL=http://localhost:8080` + - `TARGET_ROUTER_METRICS_URL=http://localhost:9190/metrics` + - `TARGET_OPENWEBUI_URL=http://localhost:3001` (if running) + +2) Docker Compose (all-in-one) + +- Reuse services defined in root `docker-compose.yml` +- Add dashboard and optional Open WebUI services in `dashboard/deploy/docker/compose.yml` +- Env examples (inside compose network): + - `TARGET_GRAFANA_URL=http://grafana:3000` + - `TARGET_PROMETHEUS_URL=http://prometheus:9090` + - `TARGET_ROUTER_API_URL=http://semantic-router:8080` + - `TARGET_ROUTER_METRICS_URL=http://semantic-router:9190/metrics` + - `TARGET_OPENWEBUI_URL=http://openwebui:8080` (if included) + +3) Kubernetes + +- Install/confirm Prometheus and Grafana via existing manifests in `deploy/kubernetes/observability` +- Deploy dashboard in `dashboard/deploy/kubernetes/` +- Configure the dashboard Deployment with in-cluster URLs: + - `TARGET_GRAFANA_URL=http://grafana..svc.cluster.local:3000` + - `TARGET_PROMETHEUS_URL=http://prometheus..svc.cluster.local:9090` + - `TARGET_ROUTER_API_URL=http://semantic-router..svc.cluster.local:8080` + - `TARGET_ROUTER_METRICS_URL=http://semantic-router..svc.cluster.local:9190/metrics` + - `TARGET_OPENWEBUI_URL=http://openwebui..svc.cluster.local:8080` (if installed) +- Expose the dashboard via Ingress/Gateway to the outside; upstreams remain ClusterIP + +## Security & access control + +- MVP: bearer token/JWT support via `Authorization: Bearer ` in requests to `/api/router/*` (forwarded to router API) +- Frame embedding: backend strips/overrides `X-Frame-Options` and `Content-Security-Policy` headers from upstreams to permit `frame-ancestors 'self'` only +- Future: OIDC login on dashboard, session cookie, and per-route RBAC; signed proxy sessions to Grafana/Open WebUI + +## Extensibility + +- New panels: add tabs/components to `frontend/` +- New integrations: add target env vars and a new `/embedded/` route in backend proxy +- Metrics aggregation: add `/api/metrics` in backend to produce derived KPIs from Prometheus + +## Implementation milestones + +1) MVP (this PR) + +- Scaffold `dashboard/` (this README) +- Backend: Go server with reverse proxies for `/embedded/*` and `/api/router/*` +- Frontend: minimal SPA with three tabs and iframes + JSON viewer +- Compose overlay: `dashboard/deploy/docker/compose.yml` to launch dashboard with existing stack + +2) K8s manifests + +- Deployment + Service + ConfigMap with env vars; optional Ingress +- Document `kubectl port-forward` for dev + +3) Auth hardening and polish + +- Env toggles for anonymous/off +- OIDC enablement behind a flag +- Metrics summary endpoint + +## Quick Start + +### Method 1: One-click Start with Docker Compose (Recommended) + +The Dashboard is integrated into the main Compose stack, requiring no extra configuration: + +```bash +# Run from the project root directory +make docker-compose-up + +# Or use docker compose directly +docker compose -f deploy/docker-compose/docker-compose.yml up -d --build +``` + +After startup, access: + +- **Dashboard**: http://localhost:8700 +- **Grafana** (direct access): http://localhost:3000 (admin/admin) +- **Prometheus** (direct access): http://localhost:9090 + +### Method 2: Local Development Mode + +When developing the Dashboard code locally: + +```bash +# 1. Start the local Observability stack +make o11y-local +# Or +docker compose -f tools/observability/docker-compose.obs.yml up -d + +# 2. Start the Router (in another terminal) +cd src/semantic-router +go run cmd/main.go -config ../../config/config.yaml + +# 3. Install frontend dependencies +cd dashboard/frontend +npm install + +# 4. Start the frontend dev server (with HMR) +npm run dev +# Vite will start on http://localhost:3001 with proxy to backend + +# 5. Start the Dashboard backend (in another terminal) +cd dashboard/backend +export TARGET_GRAFANA_URL=http://localhost:3000 +export TARGET_PROMETHEUS_URL=http://localhost:9090 +export TARGET_ROUTER_API_URL=http://localhost:8080 +export TARGET_ROUTER_METRICS_URL=http://localhost:9190/metrics +go run main.go -port=8700 -static=../frontend/dist + +# For development, use the Vite dev server at http://localhost:3001 +# For production preview, build first: cd frontend && npm run build +``` + +### Method 3: Rebuild Dashboard Only + +For a quick rebuild after code changes: + +```bash +# Rebuild the dashboard service +docker compose -f deploy/docker-compose/docker-compose.yml build dashboard + +# Restart the dashboard +docker compose -f deploy/docker-compose/docker-compose.yml up -d dashboard + +# View logs +docker logs -f semantic-router-dashboard +``` + +## Deployment Details + +### Docker Compose Integration Notes + +- The Dashboard service is integrated as a **default service** in `deploy/docker-compose/docker-compose.yml`. +- No additional overlay files are needed; `make docker-compose-up` will automatically start all services. +- The Dashboard depends on the `semantic-router` (for health checks), `grafana`, and `prometheus` services. + +### Dockerfile Build + +- A **3-stage multi-stage build** is defined in `dashboard/backend/Dockerfile`: + 1. **Node.js stage**: Builds the React frontend with Vite (`npm run build` โ†’ `dist/`) + 2. **Go builder stage**: Compiles the backend binary + 3. **Alpine runtime stage**: Combines backend + frontend dist in minimal image +- An independent Go module `dashboard/backend/go.mod` isolates backend dependencies. +- Frontend production build (`dist/`) is packaged into the image at `/app/frontend`. + +### Grafana Embedding Support + +Grafana is already configured for embedding in `deploy/docker-compose/docker-compose.yml`: + +```yaml +- GF_SECURITY_ALLOW_EMBEDDING=true +- GF_SECURITY_COOKIE_SAMESITE=lax +``` + +The Dashboard reverse proxy will automatically clean up `X-Frame-Options` and adjust CSP headers to ensure the iframe loads correctly. + +### Health Check + +The Dashboard provides a `/healthz` endpoint for container health checks: + +```bash +curl http://localhost:8700/healthz +# Returns: {"status":"healthy","service":"semantic-router-dashboard"} +``` + +## Notes + +- The website/ (Docusaurus) remains for documentation. The dashboard is a runtime operator/try-it surface, not docs. +- Weโ€™ll keep upstream services untouched and do all UX unification at the proxy + SPA layer. diff --git a/dashboard/backend/.gitkeep b/dashboard/backend/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile new file mode 100644 index 000000000..f0741cb4e --- /dev/null +++ b/dashboard/backend/Dockerfile @@ -0,0 +1,39 @@ +# Stage 1: Build frontend with Node.js +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY dashboard/frontend/package.json dashboard/frontend/tsconfig.json dashboard/frontend/tsconfig.node.json dashboard/frontend/vite.config.ts ./ +COPY dashboard/frontend/src ./src +COPY dashboard/frontend/public ./public +COPY dashboard/frontend/index.html ./ +RUN npm install +RUN npm run build + +# Stage 2: Build backend with Go +FROM golang:1.24 AS backend-builder +WORKDIR /app + +# Use Chinese Go proxy to avoid network timeout issues +ENV GOPROXY=https://goproxy.cn,direct +ENV GOSUMDB=sum.golang.google.cn + +# Copy go.mod and go.sum first for better caching +COPY dashboard/backend/go.mod dashboard/backend/go.sum /app/dashboard/backend/ +WORKDIR /app/dashboard/backend +RUN go mod download + +# Copy source code and build +COPY dashboard/backend/ /app/dashboard/backend/ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/dashboard-backend main.go + +# Stage 3: Final runtime image +FROM alpine:3.19 +RUN apk --no-cache add ca-certificates wget +WORKDIR /app +COPY --from=backend-builder /app/dashboard-backend /app/dashboard-backend +COPY --from=frontend-builder /app/frontend/dist /app/frontend +RUN addgroup -g 65532 nonroot && \ + adduser -D -u 65532 -G nonroot nonroot && \ + chown -R nonroot:nonroot /app +EXPOSE 8700 +USER nonroot:nonroot +ENTRYPOINT ["/app/dashboard-backend", "-port=8700", "-static=/app/frontend"] diff --git a/dashboard/backend/go.mod b/dashboard/backend/go.mod new file mode 100644 index 000000000..a240d517d --- /dev/null +++ b/dashboard/backend/go.mod @@ -0,0 +1,5 @@ +module github.com/vllm-project/semantic-router/dashboard/backend + +go 1.21 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/dashboard/backend/go.sum b/dashboard/backend/go.sum new file mode 100644 index 000000000..a62c313c5 --- /dev/null +++ b/dashboard/backend/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dashboard/backend/main.go b/dashboard/backend/main.go new file mode 100644 index 000000000..9abd7384b --- /dev/null +++ b/dashboard/backend/main.go @@ -0,0 +1,331 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + yaml "gopkg.in/yaml.v3" +) + +// env returns the env var or default +func env(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// configHandler reads and serves the config.yaml file as JSON +func configHandler(configPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only allow GET requests + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read the config file + data, err := os.ReadFile(configPath) + if err != nil { + log.Printf("Error reading config file: %v", err) + http.Error(w, fmt.Sprintf("Failed to read config file: %v", err), http.StatusInternalServerError) + return + } + + // Parse YAML + var config interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + log.Printf("Error parsing config YAML: %v", err) + http.Error(w, fmt.Sprintf("Failed to parse config: %v", err), http.StatusInternalServerError) + return + } + + // Convert to JSON and send response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(config); err != nil { + log.Printf("Error encoding config to JSON: %v", err) + http.Error(w, fmt.Sprintf("Failed to encode config: %v", err), http.StatusInternalServerError) + return + } + } +} + +// newReverseProxy creates a reverse proxy to targetBase and strips the given prefix from the incoming path +func newReverseProxy(targetBase, stripPrefix string, forwardAuth bool) (*httputil.ReverseProxy, error) { + targetURL, err := url.Parse(targetBase) + if err != nil { + return nil, fmt.Errorf("invalid target URL %q: %w", targetBase, err) + } + + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + // Customize the director to rewrite the request + origDirector := proxy.Director + proxy.Director = func(r *http.Request) { + origDirector(r) + // Preserve original path then strip prefix + p := r.URL.Path + if strings.HasPrefix(p, stripPrefix) { + p = strings.TrimPrefix(p, stripPrefix) + } + // Ensure leading slash + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + r.URL.Path = p + r.Host = targetURL.Host + + // Optionally forward Authorization header + if !forwardAuth { + r.Header.Del("Authorization") + } + + // Log the proxied request for debugging + log.Printf("Proxying: %s %s -> %s://%s%s", r.Method, stripPrefix, targetURL.Scheme, targetURL.Host, p) + } + + // Add error handler for proxy failures + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("Proxy error for %s: %v", r.URL.Path, err) + http.Error(w, fmt.Sprintf("Bad Gateway: %v", err), http.StatusBadGateway) + } + + // Sanitize response headers for iframe embedding + proxy.ModifyResponse = func(resp *http.Response) error { + // Remove frame-busting headers + resp.Header.Del("X-Frame-Options") + // Allow iframe from self (dashboard origin) + // If CSP exists, adjust frame-ancestors; otherwise set a permissive one for self + csp := resp.Header.Get("Content-Security-Policy") + if csp == "" { + resp.Header.Set("Content-Security-Policy", "frame-ancestors 'self'") + } else { + // Naive replacement of frame-ancestors directive + // If frame-ancestors exists, replace its value with 'self' + // Otherwise append directive + lower := strings.ToLower(csp) + if strings.Contains(lower, "frame-ancestors") { + // Split directives by ';' + parts := strings.Split(csp, ";") + for i, d := range parts { + if strings.Contains(strings.ToLower(d), "frame-ancestors") { + parts[i] = "frame-ancestors 'self'" + } + } + resp.Header.Set("Content-Security-Policy", strings.Join(parts, ";")) + } else { + resp.Header.Set("Content-Security-Policy", csp+"; frame-ancestors 'self'") + } + } + return nil + } + + return proxy, nil +} + +func staticFileServer(staticDir string) http.Handler { + fs := http.FileServer(http.Dir(staticDir)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Never serve index.html for API or embedded proxy routes + // These should be handled by their respective handlers + p := r.URL.Path + if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/embedded/") || + strings.HasPrefix(p, "/metrics/") || strings.HasPrefix(p, "/public/") || + strings.HasPrefix(p, "/avatar/") { + // These paths should have been handled by other handlers + // If we reach here, it means the proxy failed or route not found + http.Error(w, "Service not available", http.StatusBadGateway) + return + } + + full := path.Join(staticDir, path.Clean(p)) + + // Check if file exists + info, err := os.Stat(full) + if err == nil { + // File exists + if !info.IsDir() { + // It's a file, serve it + fs.ServeHTTP(w, r) + return + } + // It's a directory, try index.html + indexPath := path.Join(full, "index.html") + if _, err := os.Stat(indexPath); err == nil { + http.ServeFile(w, r, indexPath) + return + } + } + + // File doesn't exist or is directory without index.html + // For SPA routing: serve index.html for routes without file extension + if !strings.Contains(path.Base(p), ".") { + http.ServeFile(w, r, path.Join(staticDir, "index.html")) + return + } + + // Otherwise let the file server handle it (will return 404) + fs.ServeHTTP(w, r) + }) +} + +func main() { + // Flags/env for configuration + port := flag.String("port", env("DASHBOARD_PORT", "8700"), "dashboard port") + staticDir := flag.String("static", env("DASHBOARD_STATIC_DIR", "../frontend"), "static assets directory") + configFile := flag.String("config", env("ROUTER_CONFIG_PATH", "../../config/config.yaml"), "path to config.yaml") + + // Upstream targets + grafanaURL := flag.String("grafana", env("TARGET_GRAFANA_URL", ""), "Grafana base URL") + promURL := flag.String("prometheus", env("TARGET_PROMETHEUS_URL", ""), "Prometheus base URL") + routerAPI := flag.String("router_api", env("TARGET_ROUTER_API_URL", "http://localhost:8080"), "Router API base URL") + routerMetrics := flag.String("router_metrics", env("TARGET_ROUTER_METRICS_URL", "http://localhost:9190/metrics"), "Router metrics URL") + openwebuiURL := flag.String("openwebui", env("TARGET_OPENWEBUI_URL", ""), "Open WebUI base URL") + + flag.Parse() + + // Resolve config file path to absolute path + absConfigPath, err := filepath.Abs(*configFile) + if err != nil { + log.Fatalf("Failed to resolve config path: %v", err) + } + log.Printf("Config file path: %s", absConfigPath) + + mux := http.NewServeMux() + + // Health check endpoint + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"healthy","service":"semantic-router-dashboard"}`)) + }) + + // Config endpoint - serve the config.yaml as JSON + mux.HandleFunc("/api/router/config/all", configHandler(absConfigPath)) + log.Printf("Config API endpoint registered: /api/router/config/all") + + // Router API proxy (forward Authorization) - MUST be registered before Grafana + var routerAPIProxy *httputil.ReverseProxy + if *routerAPI != "" { + rp, err := newReverseProxy(*routerAPI, "/api/router", true) + if err != nil { + log.Fatalf("router API proxy error: %v", err) + } + routerAPIProxy = rp + mux.Handle("/api/router/", rp) + log.Printf("Router API proxy configured: %s", *routerAPI) + } + + // Grafana proxy and static assets + var grafanaStaticProxy *httputil.ReverseProxy + if *grafanaURL != "" { + gp, err := newReverseProxy(*grafanaURL, "/embedded/grafana", false) + if err != nil { + log.Fatalf("grafana proxy error: %v", err) + } + mux.Handle("/embedded/grafana/", gp) + + // Proxy for Grafana static assets (no prefix stripping) + grafanaStaticProxy, _ = newReverseProxy(*grafanaURL, "", false) + mux.Handle("/public/", grafanaStaticProxy) + mux.Handle("/avatar/", grafanaStaticProxy) + + log.Printf("Grafana proxy configured: %s", *grafanaURL) + log.Printf("Grafana static assets proxied: /public/, /avatar/") + } else { + mux.HandleFunc("/embedded/grafana/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"error":"Grafana not configured","message":"TARGET_GRAFANA_URL environment variable is not set"}`)) + }) + log.Printf("Warning: Grafana URL not configured") + } + + // Smart /api/ router: route to Router API or Grafana API based on path + mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { + // If path starts with /api/router/, use Router API proxy + if strings.HasPrefix(r.URL.Path, "/api/router/") && routerAPIProxy != nil { + routerAPIProxy.ServeHTTP(w, r) + return + } + // Otherwise, if Grafana is configured, proxy to Grafana API + if grafanaStaticProxy != nil { + grafanaStaticProxy.ServeHTTP(w, r) + return + } + // No handler available + http.Error(w, "Service not available", http.StatusBadGateway) + }) + + // Router metrics passthrough + mux.HandleFunc("/metrics/router", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, *routerMetrics, http.StatusTemporaryRedirect) + }) + + // Static frontend - MUST be registered last + mux.Handle("/", staticFileServer(*staticDir)) + + // Prometheus proxy (optional) + if *promURL != "" { + pp, err := newReverseProxy(*promURL, "/embedded/prometheus", false) + if err != nil { + log.Fatalf("prometheus proxy error: %v", err) + } + mux.Handle("/embedded/prometheus", pp) + mux.Handle("/embedded/prometheus/", pp) + log.Printf("Prometheus proxy configured: %s", *promURL) + } else { + mux.HandleFunc("/embedded/prometheus/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"error":"Prometheus not configured","message":"TARGET_PROMETHEUS_URL environment variable is not set"}`)) + }) + log.Printf("Warning: Prometheus URL not configured") + } + + // Open WebUI proxy (optional) + if *openwebuiURL != "" { + op, err := newReverseProxy(*openwebuiURL, "/embedded/openwebui", true) + if err != nil { + log.Fatalf("openwebui proxy error: %v", err) + } + mux.Handle("/embedded/openwebui", op) + mux.Handle("/embedded/openwebui/", op) + log.Printf("Open WebUI proxy configured: %s", *openwebuiURL) + } else { + mux.HandleFunc("/embedded/openwebui/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"error":"Open WebUI not configured","message":"TARGET_OPENWEBUI_URL environment variable is not set or empty"}`)) + }) + log.Printf("Info: Open WebUI not configured (optional)") + } + + addr := ":" + *port + log.Printf("Semantic Router Dashboard listening on %s", addr) + log.Printf("Static dir: %s", *staticDir) + if *grafanaURL != "" { + log.Printf("Grafana: %s โ†’ /embedded/grafana/", *grafanaURL) + } + if *promURL != "" { + log.Printf("Prometheus: %s โ†’ /embedded/prometheus/", *promURL) + } + if *openwebuiURL != "" { + log.Printf("OpenWebUI: %s โ†’ /embedded/openwebui/", *openwebuiURL) + } + log.Printf("Router API: %s โ†’ /api/router/*", *routerAPI) + log.Printf("Router Metrics: %s โ†’ /metrics/router", *routerMetrics) + + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("server error: %v", err) + } +} diff --git a/dashboard/deploy/kubernetes/.gitkeep b/dashboard/deploy/kubernetes/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/deploy/kubernetes/deployment.yaml b/dashboard/deploy/kubernetes/deployment.yaml new file mode 100644 index 000000000..435b08281 --- /dev/null +++ b/dashboard/deploy/kubernetes/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: semantic-router-dashboard + labels: + app: semantic-router-dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: semantic-router-dashboard + template: + metadata: + labels: + app: semantic-router-dashboard + spec: + containers: + - name: dashboard + image: semantic-router-dashboard:latest + imagePullPolicy: IfNotPresent + args: ["-port=8700", "-static=/app/frontend"] + env: + - name: TARGET_GRAFANA_URL + value: http://grafana.vllm-semantic-router-system.svc.cluster.local:3000 + - name: TARGET_PROMETHEUS_URL + value: http://prometheus.vllm-semantic-router-system.svc.cluster.local:9090 + - name: TARGET_ROUTER_API_URL + value: http://semantic-router.vllm-semantic-router-system.svc.cluster.local:8080 + - name: TARGET_ROUTER_METRICS_URL + value: http://semantic-router.vllm-semantic-router-system.svc.cluster.local:9190/metrics + - name: TARGET_OPENWEBUI_URL + value: "" + ports: + - name: http + containerPort: 8700 + volumeMounts: + - name: frontend + mountPath: /app/frontend + volumes: + - name: frontend + configMap: + name: semantic-router-dashboard-frontend +--- +apiVersion: v1 +kind: Service +metadata: + name: semantic-router-dashboard + labels: + app: semantic-router-dashboard +spec: + type: ClusterIP + selector: + app: semantic-router-dashboard + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: semantic-router-dashboard-frontend +data: + index.html: | + + Semantic Router Dashboard + + + diff --git a/dashboard/deploy/local/.gitkeep b/dashboard/deploy/local/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/frontend/index.html b/dashboard/frontend/index.html new file mode 100644 index 000000000..866e5745f --- /dev/null +++ b/dashboard/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + vLLM Semantic Router Dashboard + + +
+ + + diff --git a/dashboard/frontend/package-lock.json b/dashboard/frontend/package-lock.json new file mode 100644 index 000000000..32c5e08c8 --- /dev/null +++ b/dashboard/frontend/package-lock.json @@ -0,0 +1,3384 @@ +{ + "name": "semantic-router-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "semantic-router-dashboard", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.18.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "typescript": "^5.9.3", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@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.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "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.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "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": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "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/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "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.0", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "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.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "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.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "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", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "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", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "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", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "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", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "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", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "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", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "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", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "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", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", + "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/type-utils": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.46.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4" + }, + "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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", + "debug": "^4.3.4" + }, + "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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", + "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "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/utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", + "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.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.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.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/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", + "integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "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.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "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", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "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": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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.23", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", + "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "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": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "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/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "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/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "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", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "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", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "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.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dashboard/frontend/package.json b/dashboard/frontend/package.json new file mode 100644 index 000000000..d40dc03b7 --- /dev/null +++ b/dashboard/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "semantic-router-dashboard", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.18.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "typescript": "^5.9.3", + "vite": "^5.4.11" + } +} \ No newline at end of file diff --git a/dashboard/frontend/public/vllm.png b/dashboard/frontend/public/vllm.png new file mode 100644 index 000000000..9656c3386 Binary files /dev/null and b/dashboard/frontend/public/vllm.png differ diff --git a/dashboard/frontend/src/App.tsx b/dashboard/frontend/src/App.tsx new file mode 100644 index 000000000..9e41561d4 --- /dev/null +++ b/dashboard/frontend/src/App.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import Layout from './components/Layout' +import MonitoringPage from './pages/MonitoringPage' +import ConfigPage from './pages/ConfigPage' +import PlaygroundPage from './pages/PlaygroundPage' + +const App: React.FC = () => { + const [isInIframe, setIsInIframe] = useState(false) + + useEffect(() => { + // Detect if we're running inside an iframe (potential loop) + if (window.self !== window.top) { + setIsInIframe(true) + console.warn('Dashboard detected it is running inside an iframe - this may indicate a loop') + } + }, []) + + // If we're in an iframe, show a warning instead of rendering the full app + if (isInIframe) { + return ( +
+
โš ๏ธ
+

+ Nested Dashboard Detected +

+

+ The dashboard has detected that it is running inside an iframe. This usually indicates a + configuration error where the dashboard is trying to embed itself. +

+

+ Please check your Grafana dashboard path and backend proxy configuration. +

+ +
+ ) + } + + return ( + + + + } /> + } /> + } /> + } /> + + + + ) +} + +export default App diff --git a/dashboard/frontend/src/components/ConfigNav.module.css b/dashboard/frontend/src/components/ConfigNav.module.css new file mode 100644 index 000000000..88921767f --- /dev/null +++ b/dashboard/frontend/src/components/ConfigNav.module.css @@ -0,0 +1,99 @@ +.nav { + width: 280px; + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + height: fit-content; + flex-shrink: 0; +} + +.navHeader { + padding: 1.25rem 1.5rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%); + border-bottom: 1px solid var(--color-border); +} + +.navTitle { + font-size: 1.125rem; + font-weight: 700; + margin: 0; + color: var(--color-text); + letter-spacing: -0.01em; +} + +.navList { + list-style: none; + padding: 0.5rem; + margin: 0; +} + +.navItem { + display: flex; + align-items: flex-start; + gap: 0.875rem; + width: 100%; + padding: 0.875rem 1rem; + background: transparent; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + text-align: left; + margin-bottom: 0.25rem; +} + +.navItem:hover { + background-color: rgba(99, 102, 241, 0.05); +} + +.navItem.active { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%); + border-left: 3px solid var(--color-primary); + padding-left: calc(1rem - 3px); +} + +.navIcon { + font-size: 1.5rem; + line-height: 1; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.navContent { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + min-width: 0; +} + +.navItemTitle { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text); + line-height: 1.3; +} + +.navItem.active .navItemTitle { + color: var(--color-primary); +} + +.navItemDesc { + font-size: 0.75rem; + color: var(--color-text-secondary); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.navItem:hover .navItemDesc { + color: var(--color-text); +} + +.navItem.active .navItemDesc { + color: rgba(99, 102, 241, 0.8); +} \ No newline at end of file diff --git a/dashboard/frontend/src/components/ConfigNav.tsx b/dashboard/frontend/src/components/ConfigNav.tsx new file mode 100644 index 000000000..5144b3728 --- /dev/null +++ b/dashboard/frontend/src/components/ConfigNav.tsx @@ -0,0 +1,82 @@ +import React from 'react' +import styles from './ConfigNav.module.css' + +export type ConfigSection = + | 'models' + | 'prompt-guard' + | 'similarity-cache' + | 'intelligent-routing' + | 'tools-selection' + | 'observability' + +interface ConfigNavProps { + activeSection: ConfigSection + onSectionChange: (section: ConfigSection) => void +} + +const ConfigNav: React.FC = ({ activeSection, onSectionChange }) => { + const sections = [ + { + id: 'models' as ConfigSection, + icon: '๐Ÿ”Œ', + title: 'Models & Endpoints', + description: 'Model configurations and backend endpoints' + }, + { + id: 'prompt-guard' as ConfigSection, + icon: '๐Ÿ›ก๏ธ', + title: 'Prompt Guard', + description: 'PII and jailbreak detection' + }, + { + id: 'similarity-cache' as ConfigSection, + icon: 'โšก', + title: 'Similarity Cache', + description: 'Semantic caching configuration' + }, + { + id: 'intelligent-routing' as ConfigSection, + icon: '๐Ÿ“Š', + title: 'Intelligent Routing', + description: 'Categories and reasoning configuration' + }, + { + id: 'tools-selection' as ConfigSection, + icon: '๐Ÿ”ง', + title: 'Tools Selection', + description: 'Tool auto-selection settings' + }, + { + id: 'observability' as ConfigSection, + icon: '๐Ÿ“ˆ', + title: 'Observability', + description: 'Metrics and monitoring' + } + ] + + return ( + + ) +} + +export default ConfigNav diff --git a/dashboard/frontend/src/components/Layout.module.css b/dashboard/frontend/src/components/Layout.module.css new file mode 100644 index 000000000..6975dd812 --- /dev/null +++ b/dashboard/frontend/src/components/Layout.module.css @@ -0,0 +1,103 @@ +.container { + display: flex; + height: 100vh; + overflow: hidden; + background-color: var(--color-bg); +} + +.sidebar { + width: 240px; + background-color: var(--color-bg-secondary); + border-right: 1px solid var(--color-border); + display: flex; + flex-direction: column; + padding: 1rem 0.75rem; + gap: 1rem; +} + +.brand { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0 0.5rem; +} + +.logo { + width: 28px; + height: 28px; + object-fit: contain; +} + +.brandText { + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text); +} + +.nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.navLink { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.625rem; + border-radius: var(--radius-md); + color: var(--color-text-secondary); + font-size: 0.9rem; + font-weight: 500; + transition: all var(--transition-fast); +} + +.navLink:hover { + background-color: var(--color-bg-tertiary); + color: var(--color-text); +} + +.navLinkActive { + background-color: var(--color-primary); + color: white; +} + +.navLinkActive:hover { + background-color: var(--color-primary-dark); + color: white; +} + +.navIcon { + font-size: 1rem; + line-height: 1; + width: 1.25rem; + text-align: center; +} + +.navText { + white-space: nowrap; +} + +.sidebarFooter { + margin-top: auto; + padding: 0 0.5rem; +} + +.themeToggle { + padding: 0.5rem; + font-size: 1.25rem; + border-radius: var(--radius-md); + transition: background-color var(--transition-fast); +} + +.themeToggle:hover { + background-color: var(--color-bg-tertiary); +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; +} \ No newline at end of file diff --git a/dashboard/frontend/src/components/Layout.tsx b/dashboard/frontend/src/components/Layout.tsx new file mode 100644 index 000000000..15b97a690 --- /dev/null +++ b/dashboard/frontend/src/components/Layout.tsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect, ReactNode } from 'react' +import { NavLink } from 'react-router-dom' +import styles from './Layout.module.css' + +interface LayoutProps { + children: ReactNode +} + +const Layout: React.FC = ({ children }) => { + const [theme, setTheme] = useState<'light' | 'dark'>('dark') + + useEffect(() => { + // Check system preference or stored preference + const stored = localStorage.getItem('theme') as 'light' | 'dark' | null + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const initialTheme = stored || (prefersDark ? 'dark' : 'light') + setTheme(initialTheme) + document.documentElement.setAttribute('data-theme', initialTheme) + }, []) + + const toggleTheme = () => { + const newTheme = theme === 'light' ? 'dark' : 'light' + setTheme(newTheme) + localStorage.setItem('theme', newTheme) + document.documentElement.setAttribute('data-theme', newTheme) + } + + return ( +
+ +
{children}
+
+ ) +} + +export default Layout diff --git a/dashboard/frontend/src/index.css b/dashboard/frontend/src/index.css new file mode 100644 index 000000000..31166b826 --- /dev/null +++ b/dashboard/frontend/src/index.css @@ -0,0 +1,120 @@ +:root { + /* Colors - inspired by vLLM theme */ + --color-primary: #3b82f6; + --color-primary-dark: #2563eb; + --color-secondary: #8b5cf6; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + + /* Light theme */ + --color-bg: #ffffff; + --color-bg-secondary: #f9fafb; + --color-bg-tertiary: #f3f4f6; + --color-text: #111827; + --color-text-secondary: #6b7280; + --color-border: #e5e7eb; + --color-shadow: rgba(0, 0, 0, 0.1); + + /* Typography */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Fira Code', 'JetBrains Mono', Consolas, Monaco, 'Courier New', monospace; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + + /* Border radius */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 300ms ease; +} + +[data-theme='dark'] { + --color-bg: #0f172a; + --color-bg-secondary: #1e293b; + --color-bg-tertiary: #334155; + --color-text: #f1f5f9; + --color-text-secondary: #94a3b8; + --color-border: #334155; + --color-shadow: rgba(0, 0, 0, 0.5); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-sans); + background-color: var(--color-bg); + color: var(--color-text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +code, +pre { + font-family: var(--font-mono); +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--color-primary-dark); +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + outline: none; + background: none; +} + +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + outline: none; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: var(--radius-sm); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); +} \ No newline at end of file diff --git a/dashboard/frontend/src/main.tsx b/dashboard/frontend/src/main.tsx new file mode 100644 index 000000000..964aeb4c7 --- /dev/null +++ b/dashboard/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/dashboard/frontend/src/pages/ConfigPage.module.css b/dashboard/frontend/src/pages/ConfigPage.module.css new file mode 100644 index 000000000..3bdd59d6a --- /dev/null +++ b/dashboard/frontend/src/pages/ConfigPage.module.css @@ -0,0 +1,811 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; + padding: 1rem; + gap: 1rem; + overflow: hidden; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.headerLeft { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.title { + font-size: 1.5rem; + font-weight: 700; + margin: 0; + color: var(--color-text); + letter-spacing: -0.02em; +} + +.viewToggle { + display: flex; + gap: 0.5rem; + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 0.25rem; +} + +.toggleButton { + padding: 0.5rem 1rem; + background: transparent; + color: var(--color-text-secondary); + border: none; + border-radius: calc(var(--radius-md) - 2px); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); +} + +.toggleButton:hover { + background-color: rgba(99, 102, 241, 0.1); + color: var(--color-text); +} + +.toggleButton.active { + background-color: var(--color-primary); + color: white; + box-shadow: 0 2px 4px rgba(99, 102, 241, 0.3); +} + +.select { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background-color: var(--color-bg); + color: var(--color-text); + font-size: 0.875rem; + cursor: pointer; + transition: border-color var(--transition-fast); +} + +.select:focus { + border-color: var(--color-primary); + outline: none; +} + +.button { + padding: 0.5rem 1.25rem; + background-color: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); +} + +.button:hover:not(:disabled) { + background-color: var(--color-primary-dark); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.content { + flex: 1; + background-color: var(--color-bg); + border-radius: var(--radius-lg); + overflow: auto; + min-height: 0; +} + +.mainLayout { + display: flex; + gap: 1.5rem; + padding: 0.5rem; + height: 100%; +} + +.contentArea { + flex: 1; + min-width: 0; + overflow-y: auto; +} + +.sectionPanel { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.observabilityInfo { + padding: 1.5rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: var(--radius-md); + text-align: center; +} + +.observabilityInfo p { + margin: 0.5rem 0; + color: var(--color-text-secondary); + font-size: 0.9rem; +} + +.observabilityInfo code { + padding: 0.25rem 0.5rem; + background-color: rgba(99, 102, 241, 0.1); + border-radius: var(--radius-sm); + font-family: var(--font-mono); + color: var(--color-primary); + font-size: 0.85rem; +} + +.observabilityInfo strong { + color: var(--color-text); +} + +/* Make Categories section span all columns when in full view */ +.categoriesSection { + grid-column: 1 / -1; +} + +/* Responsive layout */ +@media (max-width: 1199px) { + .mainLayout { + flex-direction: column; + } +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 1rem; + color: var(--color-text-secondary); +} + +.spinner { + width: 48px; + height: 48px; + border: 4px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.error { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1.5rem; + background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%); + border: 1px solid var(--color-danger); + border-radius: var(--radius-lg); + color: var(--color-danger); + margin: 1rem; +} + +.errorIcon { + font-size: 1.5rem; + line-height: 1; +} + +.error h3 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + font-weight: 600; +} + +.error p { + margin: 0; + font-size: 0.875rem; + font-family: var(--font-mono); +} + +.codeBlock { + margin: 1rem; + padding: 1.5rem; + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow-x: auto; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-text); +} + +.codeBlock code { + display: block; +} + +/* Section Styles */ +.section { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + transition: box-shadow var(--transition-fast); + display: flex; + flex-direction: column; + height: fit-content; +} + +.section:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1.25rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +.sectionIcon { + font-size: 1.5rem; + line-height: 1; +} + +.sectionTitle { + font-size: 1rem; + font-weight: 600; + margin: 0; + color: var(--color-text); + flex: 1; +} + +.sectionContent { + padding: 1.25rem; + flex: 1; +} + +/* Config Row */ +.configRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.configRow:last-child { + border-bottom: none; +} + +.configLabel { + font-size: 0.875rem; + color: var(--color-text-secondary); + font-weight: 500; +} + +.configValue { + font-size: 0.875rem; + color: var(--color-text); + font-family: var(--font-mono); + font-weight: 500; +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + background-color: rgba(99, 102, 241, 0.1); + color: var(--color-primary); + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.badgelow { + background-color: rgba(34, 197, 94, 0.1); + color: #16a34a; +} + +.badgemedium { + background-color: rgba(234, 179, 8, 0.1); + color: #ca8a04; +} + +.badgehigh { + background-color: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.badgedevelopment { + background-color: rgba(234, 179, 8, 0.1); + color: #ca8a04; +} + +.badgeproduction { + background-color: rgba(239, 68, 68, 0.1); + color: #dc2626; +} + +.badgetesting { + background-color: rgba(59, 130, 246, 0.1); + color: #2563eb; +} + +.statusBadge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.375rem 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; +} + +.statusActive { + background-color: rgba(34, 197, 94, 0.1); + color: #16a34a; +} + +.statusInactive { + background-color: rgba(156, 163, 175, 0.1); + color: #6b7280; +} + +/* Endpoint Card */ +.endpointCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 0.875rem; + margin-bottom: 0.875rem; + transition: all var(--transition-fast); +} + +.endpointCard:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); +} + +.endpointCard:last-child { + margin-bottom: 0; +} + +.endpointHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--color-border); +} + +.endpointName { + font-size: 1rem; + font-weight: 600; + color: var(--color-text); +} + +.endpointDetails { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.modelTags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.modelTag { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); + border: 1px solid rgba(99, 102, 241, 0.2); + color: var(--color-primary); + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; + font-family: var(--font-mono); +} + +/* Model Card */ +.modelCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: 0.875rem; + transition: all var(--transition-fast); +} + +.modelCard:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); +} + +.modelCard:last-child { + margin-bottom: 0; +} + +.modelCardHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0.875rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); + border-bottom: 1px solid var(--color-border); +} + +.modelCardTitle { + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.modelCardBody { + padding: 0.875rem; +} + +/* Core Settings Inline */ +.coreSettingsInline { + display: flex; + gap: 2rem; + padding: 1rem 1.25rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: var(--radius-md); + margin-bottom: 1.5rem; + flex-wrap: wrap; + align-items: center; +} + +.inlineConfigRow { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.inlineConfigLabel { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text); +} + +.inlineConfigValue { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-primary); + font-family: var(--font-mono); + padding: 0.25rem 0.75rem; + background-color: rgba(99, 102, 241, 0.1); + border-radius: var(--radius-sm); +} + +/* Category Grid */ +.categoryGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +/* 4 columns on extra large screens */ +@media (min-width: 1800px) { + .categoryGrid { + grid-template-columns: repeat(4, 1fr); + } +} + +/* 3 columns on large screens */ +@media (min-width: 1400px) and (max-width: 1799px) { + .categoryGrid { + grid-template-columns: repeat(3, 1fr); + } +} + +/* 2 columns on medium screens */ +@media (min-width: 900px) and (max-width: 1399px) { + .categoryGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +.categoryCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 1.25rem; + transition: all var(--transition-fast); +} + +.categoryCard:hover { + border-color: var(--color-primary); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); + transform: translateY(-2px); +} + +.categoryHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.categoryName { + font-size: 1rem; + font-weight: 700; + color: var(--color-text); + text-transform: capitalize; +} + +.reasoningBadge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.625rem; + border-radius: var(--radius-sm); + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.reasoninglow { + background-color: rgba(34, 197, 94, 0.15); + color: #16a34a; +} + +.reasoningmedium { + background-color: rgba(234, 179, 8, 0.15); + color: #ca8a04; +} + +.reasoninghigh { + background-color: rgba(239, 68, 68, 0.15); + color: #dc2626; +} + +.categoryDescription { + font-size: 0.8rem; + color: var(--color-text-secondary); + line-height: 1.5; + margin: 0 0 1rem 0; + min-height: 2.4em; +} + +.categoryModels { + background-color: rgba(99, 102, 241, 0.03); + border: 1px solid rgba(99, 102, 241, 0.1); + border-radius: var(--radius-sm); + padding: 0.75rem; +} + +.categoryModelsHeader { + font-size: 0.7rem; + font-weight: 700; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.75rem; +} + +.modelScoreRow { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.modelScoreRow:last-child { + margin-bottom: 0; +} + +.modelScoreName { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text); + font-family: var(--font-mono); + min-width: 80px; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.reasoningIcon { + font-size: 0.8rem; +} + +.scoreBar { + flex: 1; + height: 20px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: var(--radius-sm); + overflow: hidden; + position: relative; +} + +.scoreBarFill { + height: 100%; + background: linear-gradient(90deg, var(--color-primary) 0%, rgba(139, 92, 246, 0.8) 100%); + transition: width 0.3s ease; + border-radius: var(--radius-sm); +} + +.scoreText { + position: absolute; + top: 50%; + right: 0.5rem; + transform: translateY(-50%); + font-size: 0.7rem; + font-weight: 700; + color: var(--color-text); + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); +} + +/* Feature Card */ +.featureCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: 1rem; + transition: all var(--transition-fast); +} + +.featureCard:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); +} + +.featureCard:last-child { + margin-bottom: 0; +} + +.featureHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); + border-bottom: 1px solid var(--color-border); +} + +.featureTitle { + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text); +} + +.featureBody { + padding: 1rem 1.25rem; +} + +/* Model Config Grid */ +.modelConfigGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.modelConfigCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + transition: all var(--transition-fast); +} + +.modelConfigCard:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); + transform: translateY(-2px); +} + +.modelConfigHeader { + padding: 1rem 1.25rem; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%); + border-bottom: 1px solid var(--color-border); +} + +.modelConfigName { + font-size: 1rem; + font-weight: 700; + color: var(--color-text); + font-family: var(--font-mono); +} + +.modelConfigBody { + padding: 1rem 1.25rem; +} + +.endpointTags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.endpointTag { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + background-color: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.2); + color: var(--color-primary); + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; + font-family: var(--font-mono); +} + +.badgeInfo { + background-color: rgba(59, 130, 246, 0.1); + color: #2563eb; +} + +/* Reasoning Families Grid */ +.reasoningFamiliesGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; +} + +.reasoningFamilyCard { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + transition: all var(--transition-fast); +} + +.reasoningFamilyCard:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1); + transform: translateY(-2px); +} + +.reasoningFamilyHeader { + padding: 0.875rem 1.25rem; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.08) 0%, rgba(168, 85, 247, 0.08) 100%); + border-bottom: 1px solid var(--color-border); +} + +.reasoningFamilyName { + font-size: 0.95rem; + font-weight: 700; + color: var(--color-text); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.reasoningFamilyBody { + padding: 1rem 1.25rem; +} \ No newline at end of file diff --git a/dashboard/frontend/src/pages/ConfigPage.tsx b/dashboard/frontend/src/pages/ConfigPage.tsx new file mode 100644 index 000000000..98cd35dbd --- /dev/null +++ b/dashboard/frontend/src/pages/ConfigPage.tsx @@ -0,0 +1,827 @@ +import React, { useState, useEffect } from 'react' +import styles from './ConfigPage.module.css' +import ConfigNav, { ConfigSection } from '../components/ConfigNav' + +interface VLLMEndpoint { + name: string + address: string + port: number + models: string[] + weight: number + health_check_path: string +} + +interface ModelConfig { + model_id: string + use_modernbert?: boolean + threshold: number + use_cpu: boolean + category_mapping_path?: string + pii_mapping_path?: string + jailbreak_mapping_path?: string +} + +interface ModelScore { + model: string + score: number + use_reasoning: boolean +} + +interface Category { + name: string + use_reasoning: boolean + reasoning_description: string + reasoning_effort: string + model_scores: ModelScore[] +} + +interface ReasoningFamily { + type: string + parameter: string +} + +interface ModelConfigEntry { + reasoning_family?: string + preferred_endpoints?: string[] + pii_policy?: { + allow_by_default: boolean + } +} + +interface TracingConfig { + enabled: boolean + provider: string + exporter: { + type: string + endpoint?: string + insecure?: boolean + } + sampling: { + type: string + rate?: number + } + resource: { + service_name: string + service_version: string + deployment_environment: string + } +} + +interface APIConfig { + batch_classification?: { + max_batch_size: number + concurrency_threshold: number + max_concurrency: number + metrics?: { + enabled: boolean + detailed_goroutine_tracking?: boolean + high_resolution_timing?: boolean + sample_rate?: number + duration_buckets?: number[] + size_buckets?: number[] + } + } +} + +interface ConfigData { + bert_model?: ModelConfig + semantic_cache?: { + enabled: boolean + backend_type?: string + similarity_threshold: number + max_entries: number + ttl_seconds: number + eviction_policy?: string + } + tools?: { + enabled: boolean + top_k: number + similarity_threshold: number + tools_db_path: string + fallback_to_empty: boolean + } + prompt_guard?: ModelConfig & { enabled: boolean } + vllm_endpoints?: VLLMEndpoint[] + classifier?: { + category_model?: ModelConfig + pii_model?: ModelConfig + } + categories?: Category[] + default_reasoning_effort?: string + default_model?: string + model_config?: Record + reasoning_families?: Record + api?: APIConfig + observability?: { + tracing?: TracingConfig + } + [key: string]: unknown +} + +const ConfigPage: React.FC = () => { + const [config, setConfig] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedView, setSelectedView] = useState<'structured' | 'raw'>('structured') + const [activeSection, setActiveSection] = useState('models') + + useEffect(() => { + fetchConfig() + }, []) + + const fetchConfig = async () => { + setLoading(true) + setError(null) + try { + const response = await fetch('/api/router/config/all') + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + const data = await response.json() + setConfig(data) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch config') + setConfig(null) + } finally { + setLoading(false) + } + } + + const handleRefresh = () => { + fetchConfig() + } + + const renderBackendEndpoints = () => ( +
+
+ ๐Ÿ”Œ +

Backend Endpoints

+ {config?.vllm_endpoints?.length || 0} endpoints +
+
+ {config?.vllm_endpoints?.map((endpoint, index) => ( +
+
+ {endpoint.name} + {endpoint.address}:{endpoint.port} +
+
+
+ Models +
+ {endpoint.models.map((model, idx) => ( + {model} + ))} +
+
+
+ Weight + {endpoint.weight} +
+
+ Health Check + {endpoint.health_check_path} +
+
+
+ ))} +
+
+ ) + + const renderAIModels = () => ( +
+
+ ๐Ÿค– +

AI Models Configuration

+
+
+ {config?.bert_model && ( +
+
+ BERT Model (Semantic Similarity) + + {config.bert_model.use_cpu ? '๐Ÿ’ป CPU' : '๐ŸŽฎ GPU'} + +
+
+
+ Model ID + {config.bert_model.model_id} +
+
+ Threshold + {config.bert_model.threshold} +
+
+
+ )} + + {config?.classifier?.category_model && ( +
+
+ Category Classifier + + {config.classifier.category_model.use_cpu ? '๐Ÿ’ป CPU' : '๐ŸŽฎ GPU'} + +
+
+
+ Model ID + {config.classifier.category_model.model_id} +
+
+ Threshold + {config.classifier.category_model.threshold} +
+
+ ModernBERT + + {config.classifier.category_model.use_modernbert ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+
+
+ )} + + {config?.classifier?.pii_model && ( +
+
+ PII Detector + + {config.classifier.pii_model.use_cpu ? '๐Ÿ’ป CPU' : '๐ŸŽฎ GPU'} + +
+
+
+ Model ID + {config.classifier.pii_model.model_id} +
+
+ Threshold + {config.classifier.pii_model.threshold} +
+
+
+ )} +
+
+ ) + + const renderCategories = () => ( +
+
+ ๐Ÿ“Š +

Categories & Routing

+ {config?.categories?.length || 0} categories +
+
+ {/* Core Settings at the top */} +
+
+ ๐ŸŽฏ Default Model: + {config?.default_model || 'N/A'} +
+
+ โšก Default Reasoning Effort: + + {config?.default_reasoning_effort || 'medium'} + +
+
+ +
+ {config?.categories?.map((category, index) => ( +
+
+ {category.name} + {category.use_reasoning && ( + + โšก {category.reasoning_effort} + + )} +
+

{category.reasoning_description}

+
+
Top Models
+ {category.model_scores.slice(0, 3).map((modelScore, idx) => ( +
+ + {modelScore.model} + {modelScore.use_reasoning && ๐Ÿง } + +
+
+ {(modelScore.score * 100).toFixed(0)}% +
+
+ ))} +
+
+ ))} +
+
+
+ ) + + const renderSecurity = () => ( +
+
+ ๐Ÿ›ก๏ธ +

Security Features

+
+
+ {config?.prompt_guard && ( +
+
+ Jailbreak Protection + + {config.prompt_guard.enabled ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+ {config.prompt_guard.enabled && ( +
+
+ Model + {config.prompt_guard.model_id} +
+
+ Threshold + {config.prompt_guard.threshold} +
+
+ ModernBERT + {config.prompt_guard.use_modernbert ? 'Yes' : 'No'} +
+
+ )} +
+ )} + + {config?.classifier?.pii_model && ( +
+
+ PII Detection + โœ“ Configured +
+
+
+ Threshold + {config.classifier.pii_model.threshold} +
+
+
+ )} +
+
+ ) + + // Section-specific renders + const renderModelsSection = () => ( +
+ {renderBackendEndpoints()} + {renderAIModels()} +
+ ) + + const renderPromptGuardSection = () => ( +
+ {renderSecurity()} +
+ ) + + const renderSimilarityCacheSection = () => ( +
+ {config?.semantic_cache && ( +
+
+ โšก +

Semantic Cache Configuration

+
+
+
+
+ Status + + {config.semantic_cache.enabled ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+ {config.semantic_cache.enabled && ( +
+
+ Similarity Threshold + {config.semantic_cache.similarity_threshold} +
+
+ Max Entries + {config.semantic_cache.max_entries} +
+
+ TTL + {config.semantic_cache.ttl_seconds}s +
+
+ )} +
+ + {config?.bert_model && ( +
+
+ BERT Model (Similarity) + + {config.bert_model.use_cpu ? '๐Ÿ’ป CPU' : '๐ŸŽฎ GPU'} + +
+
+
+ Model ID + {config.bert_model.model_id} +
+
+ Threshold + {config.bert_model.threshold} +
+
+
+ )} +
+
+ )} +
+ ) + + const renderIntelligentRoutingSection = () => ( +
+ {/* Model Configuration */} + {config?.model_config && Object.keys(config.model_config).length > 0 && ( +
+
+ โš™๏ธ +

Model Configuration

+ {Object.keys(config.model_config).length} models +
+
+
+ {Object.entries(config.model_config).map(([modelName, modelConfig]) => ( +
+
+ {modelName} +
+
+ {modelConfig.reasoning_family && ( +
+ ๐Ÿง  Reasoning Family + + {modelConfig.reasoning_family} + +
+ )} + {modelConfig.preferred_endpoints && modelConfig.preferred_endpoints.length > 0 && ( +
+ ๐Ÿ”Œ Preferred Endpoints +
+ {modelConfig.preferred_endpoints.map((endpoint, idx) => ( + {endpoint} + ))} +
+
+ )} + {modelConfig.pii_policy && ( +
+ ๐Ÿ”’ PII Policy + + {modelConfig.pii_policy.allow_by_default ? 'Allow by default' : 'Block by default'} + +
+ )} +
+
+ ))} +
+
+
+ )} + + {/* Reasoning Families */} + {config?.reasoning_families && Object.keys(config.reasoning_families).length > 0 && ( +
+
+ ๐Ÿง  +

Reasoning Families

+ {Object.keys(config.reasoning_families).length} families +
+
+
+ {Object.entries(config.reasoning_families).map(([familyName, familyConfig]) => ( +
+
+ {familyName} +
+
+
+ Type + {familyConfig.type} +
+
+ Parameter + {familyConfig.parameter} +
+
+
+ ))} +
+
+
+ )} + + {/* Categories */} + {renderCategories()} +
+ ) + + const renderToolsSelectionSection = () => ( +
+ {config?.tools && ( +
+
+ ๐Ÿ”ง +

Tool Auto-Selection

+
+
+
+
+ Status + + {config.tools.enabled ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+ {config.tools.enabled && ( +
+
+ Top K + {config.tools.top_k} +
+
+ Similarity Threshold + {config.tools.similarity_threshold} +
+
+ Tools Database Path + {config.tools.tools_db_path} +
+
+ Fallback to Empty + {config.tools.fallback_to_empty ? 'Yes' : 'No'} +
+
+ )} +
+
+
+ )} +
+ ) + + const renderObservabilitySection = () => ( +
+ {/* Distributed Tracing */} + {config?.observability?.tracing && ( +
+
+ ๏ฟฝ +

Distributed Tracing

+
+
+
+
+ Tracing Status + + {config.observability.tracing.enabled ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+ {config.observability.tracing.enabled && ( +
+
+ Provider + {config.observability.tracing.provider} +
+
+ Exporter Type + {config.observability.tracing.exporter.type} +
+ {config.observability.tracing.exporter.endpoint && ( +
+ Endpoint + {config.observability.tracing.exporter.endpoint} +
+ )} +
+ Sampling Type + {config.observability.tracing.sampling.type} +
+ {config.observability.tracing.sampling.rate !== undefined && ( +
+ Sampling Rate + {(config.observability.tracing.sampling.rate * 100).toFixed(0)}% +
+ )} +
+ Service Name + {config.observability.tracing.resource.service_name} +
+
+ Service Version + {config.observability.tracing.resource.service_version} +
+
+ Environment + + {config.observability.tracing.resource.deployment_environment} + +
+
+ )} +
+
+
+ )} + + {/* API & Metrics */} + {config?.api?.batch_classification && ( +
+
+ ๐Ÿ“Š +

API & Performance Metrics

+
+
+
+
+ Batch Classification +
+
+
+ Max Batch Size + {config.api.batch_classification.max_batch_size} +
+ {config.api.batch_classification.concurrency_threshold !== undefined && ( +
+ Concurrency Threshold + {config.api.batch_classification.concurrency_threshold} +
+ )} + {config.api.batch_classification.max_concurrency !== undefined && ( +
+ Max Concurrency + {config.api.batch_classification.max_concurrency} +
+ )} +
+
+ + {config.api.batch_classification.metrics && ( +
+
+ Metrics Collection + + {config.api.batch_classification.metrics.enabled ? 'โœ“ Enabled' : 'โœ— Disabled'} + +
+ {config.api.batch_classification.metrics.enabled && ( +
+ {config.api.batch_classification.metrics.sample_rate !== undefined && ( +
+ Sample Rate + {(config.api.batch_classification.metrics.sample_rate * 100).toFixed(0)}% +
+ )} + {config.api.batch_classification.metrics.detailed_goroutine_tracking !== undefined && ( +
+ Goroutine Tracking + {config.api.batch_classification.metrics.detailed_goroutine_tracking ? 'Yes' : 'No'} +
+ )} + {config.api.batch_classification.metrics.high_resolution_timing !== undefined && ( +
+ High Resolution Timing + {config.api.batch_classification.metrics.high_resolution_timing ? 'Yes' : 'No'} +
+ )} +
+ Metrics Endpoint + /metrics +
+
+ )} +
+ )} +
+
+ )} + + {/* Link to monitoring dashboard */} +
+
+ ๐Ÿ“ˆ +

Monitoring Dashboard

+
+
+
+

๐Ÿ“Š View real-time metrics and performance data in the Monitoring tab

+

๐Ÿ” Distributed traces help diagnose latency and errors across services

+

โšก Performance metrics are collected for optimization insights

+
+
+
+
+ ) + + const renderActiveSection = () => { + switch (activeSection) { + case 'models': + return renderModelsSection() + case 'prompt-guard': + return renderPromptGuardSection() + case 'similarity-cache': + return renderSimilarityCacheSection() + case 'intelligent-routing': + return renderIntelligentRoutingSection() + case 'tools-selection': + return renderToolsSelectionSection() + case 'observability': + return renderObservabilitySection() + default: + return renderModelsSection() + } + } + + return ( +
+
+
+

โš™๏ธ Configuration

+
+ + +
+
+ +
+ +
+ {loading && ( +
+
+

Loading configuration...

+
+ )} + + {error && !loading && ( +
+ โš ๏ธ +
+

Error Loading Config

+

{error}

+
+
+ )} + + {config && !loading && !error && ( + <> + {selectedView === 'structured' ? ( +
+ +
+ {renderActiveSection()} +
+
+ ) : ( +
+                {JSON.stringify(config, null, 2)}
+              
+ )} + + )} +
+
+ ) +} + +export default ConfigPage diff --git a/dashboard/frontend/src/pages/MonitoringPage.module.css b/dashboard/frontend/src/pages/MonitoringPage.module.css new file mode 100644 index 000000000..7d12d0528 --- /dev/null +++ b/dashboard/frontend/src/pages/MonitoringPage.module.css @@ -0,0 +1,168 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; + padding: 1rem; + gap: 1rem; + overflow: hidden; +} + +.controls { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1rem; + flex-shrink: 0; +} + +.controlGroup { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-secondary); + white-space: nowrap; +} + +.input { + flex: 1; + min-width: 300px; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background-color: var(--color-bg); + color: var(--color-text); + font-family: var(--font-mono); + font-size: 0.875rem; + transition: border-color var(--transition-fast); +} + +.input:focus { + border-color: var(--color-primary); + outline: none; +} + +.button { + padding: 0.5rem 1rem; + background-color: var(--color-primary); + color: white; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + transition: background-color var(--transition-fast); +} + +.button:hover { + background-color: var(--color-primary-dark); +} + +.hints { + display: flex; + gap: 1rem; + margin-top: 0.5rem; + flex-wrap: wrap; +} + +.hint { + font-size: 0.75rem; + color: var(--color-text-secondary); +} + +.errorBanner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background-color: var(--color-danger); + color: white; + border-radius: var(--radius-md); + font-size: 0.875rem; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.errorIcon { + font-size: 1.25rem; +} + +.retryButton { + margin-left: auto; + padding: 0.375rem 0.75rem; + background-color: rgba(255, 255, 255, 0.2); + color: white; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 500; + transition: background-color var(--transition-fast); +} + +.retryButton:hover { + background-color: rgba(255, 255, 255, 0.3); +} + +.iframeContainer { + position: relative; + flex: 1; + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + min-height: 0; +} + +.loadingOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: var(--color-bg); + z-index: 10; + gap: 1rem; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loadingOverlay p { + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.iframe { + width: 100%; + height: 100%; + border: none; +} \ No newline at end of file diff --git a/dashboard/frontend/src/pages/MonitoringPage.tsx b/dashboard/frontend/src/pages/MonitoringPage.tsx new file mode 100644 index 000000000..e29ee65b5 --- /dev/null +++ b/dashboard/frontend/src/pages/MonitoringPage.tsx @@ -0,0 +1,145 @@ +import React, { useState, useEffect } from 'react' +import styles from './MonitoringPage.module.css' + +const MonitoringPage: React.FC = () => { + // Get theme from document attribute + const getTheme = () => { + return document.documentElement.getAttribute('data-theme') || 'dark' + } + + const [grafanaPath, setGrafanaPath] = useState('/d/semantic-router-dashboard/semantic-router') + const [currentPath, setCurrentPath] = useState(grafanaPath) + const [theme, setTheme] = useState(getTheme()) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [loadTimeout, setLoadTimeout] = useState | null>(null) + + // Listen to theme changes + useEffect(() => { + const observer = new MutationObserver(() => { + const newTheme = getTheme() + if (newTheme !== theme) { + setTheme(newTheme) + // Reload iframe with new theme + if (currentPath) { + setCurrentPath(currentPath) // Trigger re-render + setLoading(true) + } + } + }) + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }) + + return () => observer.disconnect() + }, [theme, currentPath]) + + // Build complete Grafana URL with necessary parameters + const buildGrafanaUrl = (path: string) => { + const cleanPath = path.startsWith('/') ? path : `/${path}` + // Add kiosk mode, theme, and other necessary parameters + const params = new URLSearchParams({ + kiosk: 'tv', // tv mode hides some UI elements but keeps time range picker + theme: theme, + refresh: '30s', + }) + return `/embedded/grafana${cleanPath}?${params.toString()}` + } + + const handlePathChange = (e: React.ChangeEvent) => { + setGrafanaPath(e.target.value) + } + + const handleApply = () => { + setError(null) + setLoading(true) + setCurrentPath(grafanaPath) + + // Set timeout for loading + if (loadTimeout) clearTimeout(loadTimeout) + const timeout = setTimeout(() => { + setLoading(false) + setError('Dashboard loading timeout. Please check if the dashboard path is correct.') + }, 15000) // 15 second timeout + setLoadTimeout(timeout) + } + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleApply() + } + } + + const handleIframeLoad = () => { + if (loadTimeout) clearTimeout(loadTimeout) + setLoading(false) + setError(null) + } + + const handleIframeError = () => { + if (loadTimeout) clearTimeout(loadTimeout) + setLoading(false) + setError('Failed to load Grafana dashboard. Please check the dashboard path and try again.') + } + + return ( +
+
+
+ + + +
+
+ ๐Ÿ’ก Tip: Press Enter to apply changes + + ๐ŸŽจ Theme: {theme} (synced with dashboard) + +
+
+ + {error && ( +
+ โš ๏ธ + {error} + +
+ )} + +
+ {loading && ( +
+
+

Loading Grafana dashboard...

+
+ )} +