diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c73fba59..2fe42bec 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Install Node.js run: | - apt-get update && apt-get install -y nodejs + apt-get update && apt-get install -y nodejs npm - uses: actions/checkout@v4 @@ -44,6 +44,12 @@ jobs: key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-maven- + - name: Build UI + working-directory: ui + run: | + npm ci + npm run build + - name: Build and Test run: mvn clean verify -DskipITs --batch-mode @@ -66,7 +72,7 @@ jobs: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Set up QEMU for cross-platform builds run: docker run --rm --privileged tonistiigi/binfmt --install all - - name: Build and push + - name: Build and push server run: | docker buildx create --use --name cibuilder docker buildx build --platform linux/amd64 \ @@ -79,6 +85,18 @@ jobs: --push . env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + - name: Build and push UI + run: | + docker buildx build --platform linux/amd64 \ + -f ui/Dockerfile \ + -t gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }} \ + -t gitea.siegeln.net/cameleer/cameleer3-server-ui:latest \ + --cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache \ + --cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer3-server-ui:buildcache,mode=max \ + --provenance=false \ + --push ui/ + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - name: Cleanup local Docker run: docker system prune -af --filter "until=24h" if: always() @@ -88,14 +106,16 @@ jobs: API="https://gitea.siegeln.net/api/v1" AUTH="Authorization: token ${REGISTRY_TOKEN}" CURRENT_SHA="${{ github.sha }}" - curl -sf -H "$AUTH" "$API/packages/cameleer/container/cameleer3-server" | \ - jq -r '.[] | "\(.id) \(.version)"' | \ - while read id version; do - if [ "$version" != "latest" ] && [ "$version" != "$CURRENT_SHA" ]; then - echo "Deleting old image tag: $version" - curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/cameleer3-server/$version" - fi - done + for PKG in cameleer3-server cameleer3-server-ui; do + curl -sf -H "$AUTH" "$API/packages/cameleer/container/$PKG" | \ + jq -r '.[] | "\(.id) \(.version)"' | \ + while read id version; do + if [ "$version" != "latest" ] && [ "$version" != "$CURRENT_SHA" ]; then + echo "Deleting old image tag: $PKG:$version" + curl -sf -X DELETE -H "$AUTH" "$API/packages/cameleer/container/$PKG/$version" + fi + done + done env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} if: always() @@ -132,6 +152,8 @@ jobs: kubectl create secret generic cameleer-auth \ --namespace=cameleer \ --from-literal=CAMELEER_AUTH_TOKEN="$CAMELEER_AUTH_TOKEN" \ + --from-literal=CAMELEER_UI_USER="${CAMELEER_UI_USER:-admin}" \ + --from-literal=CAMELEER_UI_PASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic clickhouse-credentials \ @@ -147,8 +169,15 @@ jobs: kubectl -n cameleer set image deployment/cameleer3-server \ server=gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }} kubectl -n cameleer rollout status deployment/cameleer3-server --timeout=120s + + kubectl apply -f deploy/ui.yaml + kubectl -n cameleer set image deployment/cameleer3-ui \ + ui=gitea.siegeln.net/cameleer/cameleer3-server-ui:${{ github.sha }} + kubectl -n cameleer rollout status deployment/cameleer3-ui --timeout=120s env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} CAMELEER_AUTH_TOKEN: ${{ secrets.CAMELEER_AUTH_TOKEN }} + CAMELEER_UI_USER: ${{ secrets.CAMELEER_UI_USER }} + CAMELEER_UI_PASSWORD: ${{ secrets.CAMELEER_UI_PASSWORD }} CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_USER }} CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }} diff --git a/HOWTO.md b/HOWTO.md index f28822cb..67215df8 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -4,12 +4,17 @@ - Java 17+ - Maven 3.9+ +- Node.js 22+ and npm - Docker & Docker Compose - Access to the Gitea Maven registry (for `cameleer3-common` dependency) ## Build ```bash +# Build UI first (required for embedded mode) +cd ui && npm ci && npm run build && cd .. + +# Backend mvn clean compile # compile only mvn clean verify # compile + run all tests (needs Docker for integration tests) ``` @@ -65,7 +70,23 @@ curl -s -X POST http://localhost:8081/api/v1/agents/agent-1/refresh \ # Response: { "accessToken": "new-jwt" } ``` -**Public endpoints (no JWT required):** `GET /api/v1/health`, `POST /api/v1/agents/register` (uses bootstrap token), OpenAPI/Swagger docs. +**UI Login (for browser access):** +```bash +# Login with UI credentials (returns JWT tokens) +curl -s -X POST http://localhost:8081/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' +# Response: { "accessToken": "...", "refreshToken": "..." } + +# Refresh UI token +curl -s -X POST http://localhost:8081/api/v1/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{"refreshToken":""}' +``` + +UI credentials are configured via `CAMELEER_UI_USER` / `CAMELEER_UI_PASSWORD` env vars (default: `admin` / `admin`). + +**Public endpoints (no JWT required):** `GET /api/v1/health`, `POST /api/v1/agents/register` (uses bootstrap token), `POST /api/v1/auth/**`, OpenAPI/Swagger docs. **Protected endpoints (JWT required):** All other endpoints including ingestion, search, agent management, commands. @@ -228,6 +249,30 @@ Key settings in `cameleer3-server-app/src/main/resources/application.yml`: | `security.refresh-token-expiry-ms` | 604800000 | Refresh token lifetime (7d) | | `security.bootstrap-token` | `${CAMELEER_AUTH_TOKEN}` | Bootstrap token for agent registration (required) | | `security.bootstrap-token-previous` | `${CAMELEER_AUTH_TOKEN_PREVIOUS}` | Previous bootstrap token for rotation (optional) | +| `security.ui-user` | `admin` | UI login username (`CAMELEER_UI_USER` env var) | +| `security.ui-password` | `admin` | UI login password (`CAMELEER_UI_PASSWORD` env var) | +| `security.ui-origin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_UI_ORIGIN` env var) | + +## Web UI Development + +```bash +cd ui +npm install +npm run dev # Vite dev server on http://localhost:5173 (proxies /api to :8081) +npm run build # Production build to ui/dist/ +``` + +Login with `admin` / `admin` (or whatever `CAMELEER_UI_USER` / `CAMELEER_UI_PASSWORD` are set to). + +The UI uses runtime configuration via `public/config.js`. In Kubernetes, a ConfigMap overrides this file to set the correct API base URL. + +### Regenerate API Types + +When the backend OpenAPI spec changes: +```bash +cd ui +npm run generate-api # Requires backend running on :8081 +``` ## Running Tests @@ -264,22 +309,22 @@ The full stack is deployed to k3s via CI/CD on push to `main`. K8s manifests are cameleer namespace: ClickHouse (StatefulSet, 2Gi PVC) ← clickhouse:8123 (ClusterIP) cameleer3-server (Deployment) ← NodePort 30081 - cameleer3-sample (Deployment) ← NodePort 30080 (from cameleer3 repo) + cameleer3-ui (Deployment, Nginx) ← NodePort 30080 ``` ### Access (from your network) | Service | URL | |---------|-----| +| Web UI | `http://192.168.50.86:30080` | | Server API | `http://192.168.50.86:30081/api/v1/health` | | Swagger UI | `http://192.168.50.86:30081/api/v1/swagger-ui.html` | -| Sample App | `http://192.168.50.86:30080/api/orders` | ### CI/CD Pipeline -Push to `main` triggers: **build** (Maven, unit tests) → **docker** (buildx cross-compile amd64, push to Gitea registry) → **deploy** (kubectl apply + rolling update). +Push to `main` triggers: **build** (UI npm + Maven, unit tests) → **docker** (buildx amd64 for server + UI, push to Gitea registry) → **deploy** (kubectl apply + rolling update). -Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_AUTH_TOKEN`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`. +Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_AUTH_TOKEN`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CAMELEER_UI_USER` (optional), `CAMELEER_UI_PASSWORD` (optional). ### Manual K8s Commands diff --git a/cameleer3-server-app/pom.xml b/cameleer3-server-app/pom.xml index 6c8bef54..2e5aedc7 100644 --- a/cameleer3-server-app/pom.xml +++ b/cameleer3-server-app/pom.xml @@ -104,6 +104,28 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-resources-plugin + + + copy-ui-dist + generate-resources + + copy-resources + + + ${project.build.directory}/classes/static + + + ${project.basedir}/../ui/dist + false + + + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java index 6a7dc49d..777885ff 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java @@ -43,13 +43,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (token != null) { try { - String agentId = jwtService.validateAndExtractAgentId(token); - if (agentRegistryService.findById(agentId) != null) { + String subject = jwtService.validateAndExtractAgentId(token); + if (subject.startsWith("ui:")) { + // UI user token — authenticate directly without agent registry lookup UsernamePasswordAuthenticationToken auth = - new UsernamePasswordAuthenticationToken(agentId, null, List.of()); + new UsernamePasswordAuthenticationToken(subject, null, List.of()); + SecurityContextHolder.getContext().setAuthentication(auth); + } else if (agentRegistryService.findById(subject) != null) { + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(subject, null, List.of()); SecurityContextHolder.getContext().setAuthentication(auth); } else { - log.debug("JWT valid but agent not found: {}", agentId); + log.debug("JWT valid but agent not found: {}", subject); } } catch (Exception e) { log.debug("JWT validation failed: {}", e.getMessage()); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java index 2c4bdd77..4a05c19d 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java @@ -10,11 +10,16 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; /** * Spring Security configuration for JWT-based stateless authentication. *

- * Public endpoints: health, agent registration, refresh, API docs, Swagger UI. + * Public endpoints: health, agent registration, refresh, auth, API docs, Swagger UI, static resources. * All other endpoints require a valid JWT access token. */ @Configuration @@ -24,8 +29,10 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService, - AgentRegistryService registryService) throws Exception { + AgentRegistryService registryService, + CorsConfigurationSource corsConfigurationSource) throws Exception { http + .cors(cors -> cors.configurationSource(corsConfigurationSource)) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) @@ -35,12 +42,18 @@ public class SecurityConfig { "/api/v1/health", "/api/v1/agents/register", "/api/v1/agents/*/refresh", + "/api/v1/auth/**", "/api/v1/api-docs/**", "/api/v1/swagger-ui/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", - "/error" + "/error", + "/", + "/index.html", + "/config.js", + "/favicon.svg", + "/assets/**" ).permitAll() .anyRequest().authenticated() ) @@ -51,4 +64,23 @@ public class SecurityConfig { return http.build(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource(SecurityProperties properties) { + CorsConfiguration config = new CorsConfiguration(); + String origin = properties.getUiOrigin(); + if (origin != null && !origin.isBlank()) { + config.setAllowedOrigins(List.of(origin)); + } else { + config.setAllowedOrigins(List.of("http://localhost:5173")); + } + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java index 4926cdbc..caad27ea 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java @@ -13,6 +13,9 @@ public class SecurityProperties { private long refreshTokenExpiryMs = 604_800_000; private String bootstrapToken; private String bootstrapTokenPrevious; + private String uiUser; + private String uiPassword; + private String uiOrigin; public long getAccessTokenExpiryMs() { return accessTokenExpiryMs; @@ -45,4 +48,28 @@ public class SecurityProperties { public void setBootstrapTokenPrevious(String bootstrapTokenPrevious) { this.bootstrapTokenPrevious = bootstrapTokenPrevious; } + + public String getUiUser() { + return uiUser; + } + + public void setUiUser(String uiUser) { + this.uiUser = uiUser; + } + + public String getUiPassword() { + return uiPassword; + } + + public void setUiPassword(String uiPassword) { + this.uiPassword = uiPassword; + } + + public String getUiOrigin() { + return uiOrigin; + } + + public void setUiOrigin(String uiOrigin) { + this.uiOrigin = uiOrigin; + } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java new file mode 100644 index 00000000..7012009e --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java @@ -0,0 +1,86 @@ +package com.cameleer3.server.app.security; + +import com.cameleer3.server.core.security.JwtService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * Authentication endpoints for the UI. + *

+ * Validates credentials against environment-configured username/password, + * then issues JWTs with {@code ui:} prefixed subjects to distinguish + * UI users from agent tokens in {@link JwtAuthenticationFilter}. + */ +@RestController +@RequestMapping("/api/v1/auth") +public class UiAuthController { + + private static final Logger log = LoggerFactory.getLogger(UiAuthController.class); + + private final JwtService jwtService; + private final SecurityProperties properties; + + public UiAuthController(JwtService jwtService, SecurityProperties properties) { + this.jwtService = jwtService; + this.properties = properties; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + String configuredUser = properties.getUiUser(); + String configuredPassword = properties.getUiPassword(); + + if (configuredUser == null || configuredUser.isBlank() + || configuredPassword == null || configuredPassword.isBlank()) { + log.warn("UI authentication attempted but CAMELEER_UI_USER / CAMELEER_UI_PASSWORD not configured"); + return ResponseEntity.status(401).body(Map.of("message", "UI authentication not configured")); + } + + if (!configuredUser.equals(request.username()) + || !configuredPassword.equals(request.password())) { + log.debug("UI login failed for user: {}", request.username()); + return ResponseEntity.status(401).body(Map.of("message", "Invalid credentials")); + } + + String subject = "ui:" + request.username(); + String accessToken = jwtService.createAccessToken(subject, "ui"); + String refreshToken = jwtService.createRefreshToken(subject, "ui"); + + log.info("UI user logged in: {}", request.username()); + return ResponseEntity.ok(Map.of( + "accessToken", accessToken, + "refreshToken", refreshToken + )); + } + + @PostMapping("/refresh") + public ResponseEntity refresh(@RequestBody RefreshRequest request) { + try { + String subject = jwtService.validateRefreshToken(request.refreshToken()); + if (!subject.startsWith("ui:")) { + return ResponseEntity.status(401).body(Map.of("message", "Not a UI token")); + } + + String accessToken = jwtService.createAccessToken(subject, "ui"); + String refreshToken = jwtService.createRefreshToken(subject, "ui"); + + return ResponseEntity.ok(Map.of( + "accessToken", accessToken, + "refreshToken", refreshToken + )); + } catch (Exception e) { + log.debug("UI token refresh failed: {}", e.getMessage()); + return ResponseEntity.status(401).body(Map.of("message", "Invalid refresh token")); + } + } + + public record LoginRequest(String username, String password) {} + public record RefreshRequest(String refreshToken) {} +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/web/SpaForwardController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/web/SpaForwardController.java new file mode 100644 index 00000000..9e07cc8f --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/web/SpaForwardController.java @@ -0,0 +1,24 @@ +package com.cameleer3.server.app.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * SPA catch-all: forwards non-API, non-static requests to {@code index.html} + * so that React Router can handle client-side routing. + *

+ * Only active when the UI is embedded (i.e., {@code static/index.html} exists + * in the classpath). When running standalone via Nginx, this is not needed. + */ +@Controller +public class SpaForwardController { + + @GetMapping(value = { + "/login", + "/executions", + "/executions/{path:[^\\.]*}" + }) + public String forward() { + return "forward:/index.html"; + } +} diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index 13b155d0..de2b76bf 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -37,6 +37,9 @@ security: refresh-token-expiry-ms: 604800000 bootstrap-token: ${CAMELEER_AUTH_TOKEN:} bootstrap-token-previous: ${CAMELEER_AUTH_TOKEN_PREVIOUS:} + ui-user: ${CAMELEER_UI_USER:admin} + ui-password: ${CAMELEER_UI_PASSWORD:admin} + ui-origin: ${CAMELEER_UI_ORIGIN:http://localhost:5173} springdoc: api-docs: diff --git a/deploy/server.yaml b/deploy/server.yaml index 53bf751e..92b203e8 100644 --- a/deploy/server.yaml +++ b/deploy/server.yaml @@ -38,6 +38,20 @@ spec: secretKeyRef: name: cameleer-auth key: CAMELEER_AUTH_TOKEN + - name: CAMELEER_UI_USER + valueFrom: + secretKeyRef: + name: cameleer-auth + key: CAMELEER_UI_USER + optional: true + - name: CAMELEER_UI_PASSWORD + valueFrom: + secretKeyRef: + name: cameleer-auth + key: CAMELEER_UI_PASSWORD + optional: true + - name: CAMELEER_UI_ORIGIN + value: "http://192.168.50.86:30080" resources: requests: memory: "256Mi" diff --git a/deploy/ui.yaml b/deploy/ui.yaml new file mode 100644 index 00000000..13f76437 --- /dev/null +++ b/deploy/ui.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: cameleer3-ui-config + namespace: cameleer +data: + config.js: | + window.__CAMELEER_CONFIG__ = { + apiBaseUrl: 'http://192.168.50.86:30081/api/v1', + }; +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cameleer3-ui + namespace: cameleer +spec: + replicas: 1 + selector: + matchLabels: + app: cameleer3-ui + template: + metadata: + labels: + app: cameleer3-ui + spec: + imagePullSecrets: + - name: gitea-registry + containers: + - name: ui + image: gitea.siegeln.net/cameleer/cameleer3-server-ui:latest + ports: + - containerPort: 80 + env: + - name: CAMELEER_API_URL + value: "http://cameleer3-server:8081" + volumeMounts: + - name: config + mountPath: /usr/share/nginx/html/config.js + subPath: config.js + resources: + requests: + memory: "32Mi" + cpu: "10m" + limits: + memory: "64Mi" + cpu: "100m" + livenessProbe: + httpGet: + path: /healthz + port: 80 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: 80 + periodSeconds: 5 + volumes: + - name: config + configMap: + name: cameleer3-ui-config +--- +apiVersion: v1 +kind: Service +metadata: + name: cameleer3-ui + namespace: cameleer +spec: + type: NodePort + selector: + app: cameleer3-ui + ports: + - port: 80 + targetPort: 80 + nodePort: 30080 diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 00000000..a21f178d --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.git diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/ui/Dockerfile b/ui/Dockerfile new file mode 100644 index 00000000..a4448d7c --- /dev/null +++ b/ui/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/templates/default.conf.template + +# Default API URL — override via K8s env or docker run -e +ENV CAMELEER_API_URL=http://cameleer3-server:8081 + +EXPOSE 80 diff --git a/ui/eslint.config.js b/ui/eslint.config.js new file mode 100644 index 00000000..5e6b472f --- /dev/null +++ b/ui/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 00000000..0aa40574 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,14 @@ + + + + + + + Cameleer3 + + + +

+ + + diff --git a/ui/nginx.conf b/ui/nginx.conf new file mode 100644 index 00000000..c330bc08 --- /dev/null +++ b/ui/nginx.conf @@ -0,0 +1,38 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA fallback: serve index.html for all non-file routes + location / { + try_files $uri $uri/ /index.html; + } + + # API proxy — target set via CAMELEER_API_URL env var (default: http://cameleer3-server:8081) + location /api/ { + proxy_pass ${CAMELEER_API_URL}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE support + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 86400s; + } + + # Caching for static assets + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Health check + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 00000000..24299bd2 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,3348 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "openapi-fetch": "^0.17.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "openapi-typescript": "^7.13.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "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-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "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/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@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/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.10", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.10.tgz", + "integrity": "sha512-XCBR/9WHJ0cpezuunHMZjuFMl4KqUo7eiFwzrQrvm7lTXt0EBd3No8UY+9OyzXpDfreGEMMtxmaLZ+ksVw378g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/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/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.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.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "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.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "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.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "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.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "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/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": 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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "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.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "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.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "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": "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/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/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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-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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "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/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.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "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": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/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/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "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/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-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "license": "MIT" + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "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/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "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/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "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/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 00000000..45cfdbd9 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,36 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "generate-api": "openapi-typescript http://localhost:8081/api/v1/api-docs -o src/api/schema.d.ts" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "openapi-fetch": "^0.17.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "openapi-typescript": "^7.13.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } +} diff --git a/ui/public/config.js b/ui/public/config.js new file mode 100644 index 00000000..0a14b981 --- /dev/null +++ b/ui/public/config.js @@ -0,0 +1,3 @@ +window.__CAMELEER_CONFIG__ = { + apiBaseUrl: '/api/v1', +}; diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 00000000..4e92fe1e --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/public/icons.svg b/ui/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/ui/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/api/client.ts b/ui/src/api/client.ts new file mode 100644 index 00000000..94918dfd --- /dev/null +++ b/ui/src/api/client.ts @@ -0,0 +1,33 @@ +import createClient, { type Middleware } from 'openapi-fetch'; +import type { paths } from './schema'; +import { config } from '../config'; + +let getAccessToken: () => string | null = () => null; +let onUnauthorized: () => void = () => {}; + +export function configureAuth(opts: { + getAccessToken: () => string | null; + onUnauthorized: () => void; +}) { + getAccessToken = opts.getAccessToken; + onUnauthorized = opts.onUnauthorized; +} + +const authMiddleware: Middleware = { + async onRequest({ request }) { + const token = getAccessToken(); + if (token) { + request.headers.set('Authorization', `Bearer ${token}`); + } + return request; + }, + async onResponse({ response }) { + if (response.status === 401) { + onUnauthorized(); + } + return response; + }, +}; + +export const api = createClient({ baseUrl: config.apiBaseUrl }); +api.use(authMiddleware); diff --git a/ui/src/api/queries/agents.ts b/ui/src/api/queries/agents.ts new file mode 100644 index 00000000..0f8136ad --- /dev/null +++ b/ui/src/api/queries/agents.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../client'; + +export function useAgents(status?: string) { + return useQuery({ + queryKey: ['agents', status], + queryFn: async () => { + const { data, error } = await api.GET('/agents', { + params: { query: status ? { status } : {} }, + }); + if (error) throw new Error('Failed to load agents'); + return data!; + }, + }); +} diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts new file mode 100644 index 00000000..1a1443f8 --- /dev/null +++ b/ui/src/api/queries/executions.ts @@ -0,0 +1,53 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../client'; +import type { SearchRequest } from '../schema'; + +export function useSearchExecutions(filters: SearchRequest) { + return useQuery({ + queryKey: ['executions', 'search', filters], + queryFn: async () => { + const { data, error } = await api.POST('/search/executions', { + body: filters, + }); + if (error) throw new Error('Search failed'); + return data!; + }, + placeholderData: (prev) => prev, + }); +} + +export function useExecutionDetail(executionId: string | null) { + return useQuery({ + queryKey: ['executions', 'detail', executionId], + queryFn: async () => { + const { data, error } = await api.GET('/executions/{executionId}', { + params: { path: { executionId: executionId! } }, + }); + if (error) throw new Error('Failed to load execution detail'); + return data!; + }, + enabled: !!executionId, + }); +} + +export function useProcessorSnapshot( + executionId: string | null, + index: number | null, +) { + return useQuery({ + queryKey: ['executions', 'snapshot', executionId, index], + queryFn: async () => { + const { data, error } = await api.GET( + '/executions/{executionId}/processors/{index}/snapshot', + { + params: { + path: { executionId: executionId!, index: index! }, + }, + }, + ); + if (error) throw new Error('Failed to load snapshot'); + return data!; + }, + enabled: !!executionId && index !== null, + }); +} diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts new file mode 100644 index 00000000..00293c0d --- /dev/null +++ b/ui/src/api/schema.d.ts @@ -0,0 +1,186 @@ +/** + * Hand-written OpenAPI types matching the cameleer3 server REST API. + * Will be replaced by openapi-typescript codegen once backend is running. + */ + +export interface paths { + '/auth/login': { + post: { + requestBody: { + content: { + 'application/json': { + username: string; + password: string; + }; + }; + }; + responses: { + 200: { + content: { + 'application/json': { + accessToken: string; + refreshToken: string; + }; + }; + }; + 401: { content: { 'application/json': { message: string } } }; + }; + }; + }; + '/auth/refresh': { + post: { + requestBody: { + content: { + 'application/json': { + refreshToken: string; + }; + }; + }; + responses: { + 200: { + content: { + 'application/json': { + accessToken: string; + refreshToken: string; + }; + }; + }; + 401: { content: { 'application/json': { message: string } } }; + }; + }; + }; + '/search/executions': { + post: { + requestBody: { + content: { + 'application/json': SearchRequest; + }; + }; + responses: { + 200: { + content: { + 'application/json': SearchResponse; + }; + }; + }; + }; + }; + '/executions/{executionId}': { + get: { + parameters: { + path: { executionId: string }; + }; + responses: { + 200: { + content: { + 'application/json': ExecutionDetail; + }; + }; + 404: { content: { 'application/json': { message: string } } }; + }; + }; + }; + '/executions/{executionId}/processors/{index}/snapshot': { + get: { + parameters: { + path: { executionId: string; index: number }; + }; + responses: { + 200: { + content: { + 'application/json': ExchangeSnapshot; + }; + }; + 404: { content: { 'application/json': { message: string } } }; + }; + }; + }; + '/agents': { + get: { + parameters: { + query?: { status?: string }; + }; + responses: { + 200: { + content: { + 'application/json': AgentInstance[]; + }; + }; + }; + }; + }; +} + +export interface SearchRequest { + status?: string | null; + timeFrom?: string | null; + timeTo?: string | null; + durationMin?: number | null; + durationMax?: number | null; + correlationId?: string | null; + text?: string | null; + textInBody?: string | null; + textInHeaders?: string | null; + textInErrors?: string | null; + offset?: number; + limit?: number; +} + +export interface SearchResponse { + results: ExecutionSummary[]; + total: number; + offset: number; + limit: number; +} + +export interface ExecutionSummary { + executionId: string; + routeId: string; + agentId: string; + status: 'COMPLETED' | 'FAILED' | 'RUNNING'; + startTime: string; + duration: number; + processorCount: number; + correlationId: string | null; + errorMessage: string | null; +} + +export interface ExecutionDetail { + executionId: string; + routeId: string; + agentId: string; + status: 'COMPLETED' | 'FAILED' | 'RUNNING'; + startTime: string; + duration: number; + correlationId: string | null; + errorMessage: string | null; + processors: ProcessorNode[]; +} + +export interface ProcessorNode { + index: number; + processorId: string; + processorType: string; + uri: string | null; + status: 'COMPLETED' | 'FAILED' | 'RUNNING'; + duration: number; + errorMessage: string | null; + children: ProcessorNode[]; +} + +export interface ExchangeSnapshot { + exchangeId: string; + correlationId: string | null; + bodyType: string | null; + body: string | null; + headers: Record | null; + properties: Record | null; +} + +export interface AgentInstance { + agentId: string; + group: string; + state: 'LIVE' | 'STALE' | 'DEAD'; + lastHeartbeat: string; + registeredAt: string; +} diff --git a/ui/src/auth/LoginPage.module.css b/ui/src/auth/LoginPage.module.css new file mode 100644 index 00000000..51c863b8 --- /dev/null +++ b/ui/src/auth/LoginPage.module.css @@ -0,0 +1,103 @@ +.page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + position: relative; + z-index: 1; +} + +.card { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + padding: 40px; + width: 100%; + max-width: 400px; + animation: fadeIn 0.3s ease-out both; +} + +.logo { + font-family: var(--font-mono); + font-weight: 600; + font-size: 20px; + color: var(--amber); + letter-spacing: -0.5px; + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.subtitle { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 28px; +} + +.field { + margin-bottom: 16px; +} + +.label { + display: block; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + margin-bottom: 6px; +} + +.input { + width: 100%; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 14px; + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 13px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.input:focus { + border-color: var(--amber-dim); + box-shadow: 0 0 0 3px var(--amber-glow); +} + +.submit { + width: 100%; + margin-top: 8px; + padding: 10px 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--amber); + background: var(--amber); + color: #0a0e17; + font-family: var(--font-body); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.submit:hover { + background: var(--amber-hover); + border-color: var(--amber-hover); +} + +.submit:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.error { + margin-top: 12px; + padding: 10px 12px; + background: var(--rose-glow); + border: 1px solid rgba(244, 63, 94, 0.2); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--rose); +} diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx new file mode 100644 index 00000000..24c755fd --- /dev/null +++ b/ui/src/auth/LoginPage.tsx @@ -0,0 +1,61 @@ +import { type FormEvent, useState } from 'react'; +import { Navigate } from 'react-router'; +import { useAuthStore } from './auth-store'; +import styles from './LoginPage.module.css'; + +export function LoginPage() { + const { isAuthenticated, login, loading, error } = useAuthStore(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + if (isAuthenticated) return ; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + login(username, password); + }; + + return ( +
+
+
+ + + + + cameleer3 +
+
Sign in to access the observability dashboard
+ +
+ + setUsername(e.target.value)} + autoFocus + autoComplete="username" + /> +
+ +
+ + setPassword(e.target.value)} + autoComplete="current-password" + /> +
+ + + + {error &&
{error}
} +
+
+ ); +} diff --git a/ui/src/auth/ProtectedRoute.tsx b/ui/src/auth/ProtectedRoute.tsx new file mode 100644 index 00000000..62ba72e7 --- /dev/null +++ b/ui/src/auth/ProtectedRoute.tsx @@ -0,0 +1,12 @@ +import { Navigate, Outlet } from 'react-router'; +import { useAuthStore } from './auth-store'; +import { useAuth } from './use-auth'; + +export function ProtectedRoute() { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); + // Initialize auth hooks (auto-refresh, API client wiring) + useAuth(); + + if (!isAuthenticated) return ; + return ; +} diff --git a/ui/src/auth/auth-store.ts b/ui/src/auth/auth-store.ts new file mode 100644 index 00000000..fb302f64 --- /dev/null +++ b/ui/src/auth/auth-store.ts @@ -0,0 +1,109 @@ +import { create } from 'zustand'; +import { config } from '../config'; + +interface AuthState { + accessToken: string | null; + refreshToken: string | null; + username: string | null; + isAuthenticated: boolean; + error: string | null; + loading: boolean; + login: (username: string, password: string) => Promise; + refresh: () => Promise; + logout: () => void; +} + +function loadTokens() { + return { + accessToken: localStorage.getItem('cameleer-access-token'), + refreshToken: localStorage.getItem('cameleer-refresh-token'), + username: localStorage.getItem('cameleer-username'), + }; +} + +function persistTokens(access: string, refresh: string, username: string) { + localStorage.setItem('cameleer-access-token', access); + localStorage.setItem('cameleer-refresh-token', refresh); + localStorage.setItem('cameleer-username', username); +} + +function clearTokens() { + localStorage.removeItem('cameleer-access-token'); + localStorage.removeItem('cameleer-refresh-token'); + localStorage.removeItem('cameleer-username'); +} + +const initial = loadTokens(); + +export const useAuthStore = create((set, get) => ({ + accessToken: initial.accessToken, + refreshToken: initial.refreshToken, + username: initial.username, + isAuthenticated: !!initial.accessToken, + error: null, + loading: false, + + login: async (username, password) => { + set({ loading: true, error: null }); + try { + const res = await fetch(`${config.apiBaseUrl}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.message || 'Invalid credentials'); + } + const { accessToken, refreshToken } = await res.json(); + persistTokens(accessToken, refreshToken, username); + set({ + accessToken, + refreshToken, + username, + isAuthenticated: true, + loading: false, + }); + } catch (e: unknown) { + set({ + error: e instanceof Error ? e.message : 'Login failed', + loading: false, + }); + } + }, + + refresh: async () => { + const { refreshToken } = get(); + if (!refreshToken) return false; + try { + const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }), + }); + if (!res.ok) return false; + const data = await res.json(); + const username = get().username ?? ''; + persistTokens(data.accessToken, data.refreshToken, username); + set({ + accessToken: data.accessToken, + refreshToken: data.refreshToken, + isAuthenticated: true, + }); + return true; + } catch { + return false; + } + }, + + logout: () => { + clearTokens(); + set({ + accessToken: null, + refreshToken: null, + username: null, + isAuthenticated: false, + error: null, + }); + }, +})); diff --git a/ui/src/auth/use-auth.ts b/ui/src/auth/use-auth.ts new file mode 100644 index 00000000..90331bfd --- /dev/null +++ b/ui/src/auth/use-auth.ts @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import { useAuthStore } from './auth-store'; +import { configureAuth } from '../api/client'; +import { useNavigate } from 'react-router'; + +export function useAuth() { + const { accessToken, isAuthenticated, refresh, logout } = useAuthStore(); + const navigate = useNavigate(); + + // Wire API client to auth store + useEffect(() => { + configureAuth({ + getAccessToken: () => useAuthStore.getState().accessToken, + onUnauthorized: async () => { + const ok = await useAuthStore.getState().refresh(); + if (!ok) { + useAuthStore.getState().logout(); + navigate('/login', { replace: true }); + } + }, + }); + }, [navigate]); + + // Auto-refresh: check token expiry every 30s + useEffect(() => { + if (!isAuthenticated) return; + const interval = setInterval(async () => { + const token = useAuthStore.getState().accessToken; + if (!token) return; + try { + const payload = JSON.parse(atob(token.split('.')[1])); + const expiresIn = payload.exp * 1000 - Date.now(); + // Refresh when less than 5 minutes remaining + if (expiresIn < 5 * 60 * 1000) { + await refresh(); + } + } catch { + // Token parse failure — ignore, will fail on next API call + } + }, 30_000); + return () => clearInterval(interval); + }, [isAuthenticated, refresh]); + + return { accessToken, isAuthenticated, logout }; +} diff --git a/ui/src/components/layout/AppShell.module.css b/ui/src/components/layout/AppShell.module.css new file mode 100644 index 00000000..d66608c0 --- /dev/null +++ b/ui/src/components/layout/AppShell.module.css @@ -0,0 +1,7 @@ +.main { + position: relative; + z-index: 1; + max-width: 1440px; + margin: 0 auto; + padding: 24px; +} diff --git a/ui/src/components/layout/AppShell.tsx b/ui/src/components/layout/AppShell.tsx new file mode 100644 index 00000000..7fdd9514 --- /dev/null +++ b/ui/src/components/layout/AppShell.tsx @@ -0,0 +1,14 @@ +import { Outlet } from 'react-router'; +import { TopNav } from './TopNav'; +import styles from './AppShell.module.css'; + +export function AppShell() { + return ( + <> + +
+ +
+ + ); +} diff --git a/ui/src/components/layout/TopNav.module.css b/ui/src/components/layout/TopNav.module.css new file mode 100644 index 00000000..cac7dd1f --- /dev/null +++ b/ui/src/components/layout/TopNav.module.css @@ -0,0 +1,114 @@ +.topnav { + position: sticky; + top: 0; + z-index: 100; + background: var(--topnav-bg); + backdrop-filter: blur(20px) saturate(1.2); + border-bottom: 1px solid var(--border-subtle); + padding: 0 24px; + display: flex; + align-items: center; + height: 56px; + gap: 32px; +} + +.logo { + font-family: var(--font-mono); + font-weight: 600; + font-size: 16px; + color: var(--amber); + letter-spacing: -0.5px; + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + text-decoration: none; +} + +.logo:hover { color: var(--amber); } + +.navLinks { + display: flex; + gap: 4px; + list-style: none; +} + +.navLink { + padding: 8px 16px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + transition: all 0.15s; + text-decoration: none; +} + +.navLink:hover { + color: var(--text-primary); + background: var(--bg-raised); +} + +.navLinkActive { + composes: navLink; + color: var(--amber); + background: var(--amber-glow); +} + +.navRight { + margin-left: auto; + display: flex; + align-items: center; + gap: 16px; +} + +.envBadge { + font-family: var(--font-mono); + font-size: 11px; + padding: 4px 10px; + border-radius: 99px; + background: var(--green-glow); + color: var(--green); + border: 1px solid rgba(16, 185, 129, 0.2); + font-weight: 500; +} + +.themeToggle { + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 8px; + cursor: pointer; + color: var(--text-muted); + font-size: 16px; + display: flex; + align-items: center; + transition: all 0.15s; +} + +.themeToggle:hover { + border-color: var(--text-muted); + color: var(--text-primary); +} + +.userInfo { + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); + display: flex; + align-items: center; + gap: 8px; +} + +.logoutBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 12px; + padding: 4px; + transition: color 0.15s; +} + +.logoutBtn:hover { + color: var(--rose); +} diff --git a/ui/src/components/layout/TopNav.tsx b/ui/src/components/layout/TopNav.tsx new file mode 100644 index 00000000..89e9c84b --- /dev/null +++ b/ui/src/components/layout/TopNav.tsx @@ -0,0 +1,44 @@ +import { NavLink } from 'react-router'; +import { useThemeStore } from '../../theme/theme-store'; +import { useAuthStore } from '../../auth/auth-store'; +import styles from './TopNav.module.css'; + +export function TopNav() { + const { theme, toggle } = useThemeStore(); + const { username, logout } = useAuthStore(); + + return ( + + ); +} diff --git a/ui/src/components/shared/AppBadge.tsx b/ui/src/components/shared/AppBadge.tsx new file mode 100644 index 00000000..e7184a09 --- /dev/null +++ b/ui/src/components/shared/AppBadge.tsx @@ -0,0 +1,20 @@ +import styles from './shared.module.css'; + +const COLORS = ['#3b82f6', '#f0b429', '#10b981', '#a855f7', '#f43f5e', '#22d3ee', '#ec4899']; + +function hashColor(name: string) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + return COLORS[Math.abs(hash) % COLORS.length]; +} + +export function AppBadge({ name }: { name: string }) { + return ( + + + {name} + + ); +} diff --git a/ui/src/components/shared/DurationBar.tsx b/ui/src/components/shared/DurationBar.tsx new file mode 100644 index 00000000..faf50904 --- /dev/null +++ b/ui/src/components/shared/DurationBar.tsx @@ -0,0 +1,30 @@ +import styles from './shared.module.css'; + +function durationClass(ms: number) { + if (ms < 100) return styles.barFast; + if (ms < 1000) return styles.barMedium; + return styles.barSlow; +} + +function durationColor(ms: number) { + if (ms < 100) return 'var(--green)'; + if (ms < 1000) return 'var(--amber)'; + return 'var(--rose)'; +} + +export function DurationBar({ duration }: { duration: number }) { + const widthPct = Math.min(100, (duration / 5000) * 100); + return ( +
+ + {duration.toLocaleString()}ms + +
+
+
+
+ ); +} diff --git a/ui/src/components/shared/FilterChip.tsx b/ui/src/components/shared/FilterChip.tsx new file mode 100644 index 00000000..57556081 --- /dev/null +++ b/ui/src/components/shared/FilterChip.tsx @@ -0,0 +1,23 @@ +import styles from './shared.module.css'; + +interface FilterChipProps { + label: string; + active: boolean; + accent?: 'green' | 'rose' | 'blue'; + count?: number; + onClick: () => void; +} + +export function FilterChip({ label, active, accent, count, onClick }: FilterChipProps) { + const accentClass = accent ? styles[`chip${accent.charAt(0).toUpperCase()}${accent.slice(1)}`] : ''; + return ( + + {accent && } + {label} + {count !== undefined && {count.toLocaleString()}} + + ); +} diff --git a/ui/src/components/shared/Pagination.tsx b/ui/src/components/shared/Pagination.tsx new file mode 100644 index 00000000..f92a797d --- /dev/null +++ b/ui/src/components/shared/Pagination.tsx @@ -0,0 +1,60 @@ +import styles from './shared.module.css'; + +interface PaginationProps { + total: number; + offset: number; + limit: number; + onChange: (offset: number) => void; +} + +export function Pagination({ total, offset, limit, onChange }: PaginationProps) { + const currentPage = Math.floor(offset / limit) + 1; + const totalPages = Math.max(1, Math.ceil(total / limit)); + + if (totalPages <= 1) return null; + + const pages: (number | '...')[] = []; + if (totalPages <= 7) { + for (let i = 1; i <= totalPages; i++) pages.push(i); + } else { + pages.push(1); + if (currentPage > 3) pages.push('...'); + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + pages.push(i); + } + if (currentPage < totalPages - 2) pages.push('...'); + pages.push(totalPages); + } + + return ( +
+ + {pages.map((p, i) => + p === '...' ? ( + + ) : ( + + ), + )} + +
+ ); +} diff --git a/ui/src/components/shared/StatCard.tsx b/ui/src/components/shared/StatCard.tsx new file mode 100644 index 00000000..da6c6c0a --- /dev/null +++ b/ui/src/components/shared/StatCard.tsx @@ -0,0 +1,21 @@ +import styles from './shared.module.css'; + +interface StatCardProps { + label: string; + value: string; + accent: 'amber' | 'cyan' | 'rose' | 'green' | 'blue'; + change?: string; + changeDirection?: 'up' | 'down' | 'neutral'; +} + +export function StatCard({ label, value, accent, change, changeDirection = 'neutral' }: StatCardProps) { + return ( +
+
{label}
+
{value}
+ {change && ( +
{change}
+ )} +
+ ); +} diff --git a/ui/src/components/shared/StatusPill.tsx b/ui/src/components/shared/StatusPill.tsx new file mode 100644 index 00000000..d029437e --- /dev/null +++ b/ui/src/components/shared/StatusPill.tsx @@ -0,0 +1,17 @@ +import styles from './shared.module.css'; + +const STATUS_MAP = { + COMPLETED: { className: styles.pillCompleted, label: 'Completed' }, + FAILED: { className: styles.pillFailed, label: 'Failed' }, + RUNNING: { className: styles.pillRunning, label: 'Running' }, +} as const; + +export function StatusPill({ status }: { status: string }) { + const info = STATUS_MAP[status as keyof typeof STATUS_MAP] ?? STATUS_MAP.COMPLETED; + return ( + + + {info.label} + + ); +} diff --git a/ui/src/components/shared/shared.module.css b/ui/src/components/shared/shared.module.css new file mode 100644 index 00000000..797e52c5 --- /dev/null +++ b/ui/src/components/shared/shared.module.css @@ -0,0 +1,201 @@ +/* ─── Status Pill ─── */ +.statusPill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border-radius: 99px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.3px; +} + +.statusDot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.pillCompleted { background: var(--green-glow); color: var(--green); } +.pillFailed { background: var(--rose-glow); color: var(--rose); } +.pillRunning { background: rgba(59, 130, 246, 0.12); color: var(--blue); } +.pillRunning .statusDot { animation: livePulse 1.5s ease-in-out infinite; } + +/* ─── Duration Bar ─── */ +.durationBar { + display: flex; + align-items: center; + gap: 8px; +} + +.bar { + width: 60px; + height: 4px; + background: var(--bg-base); + border-radius: 2px; + overflow: hidden; +} + +.barFill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; +} + +.barFast { background: var(--green); } +.barMedium { background: var(--amber); } +.barSlow { background: var(--rose); } + +/* ─── Stat Card ─── */ +.statCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + padding: 16px 20px; + position: relative; + overflow: hidden; + transition: border-color 0.2s; +} + +.statCard:hover { border-color: var(--border); } + +.statCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; +} + +.amber::before { background: linear-gradient(90deg, var(--amber), transparent); } +.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); } +.rose::before { background: linear-gradient(90deg, var(--rose), transparent); } +.green::before { background: linear-gradient(90deg, var(--green), transparent); } +.blue::before { background: linear-gradient(90deg, var(--blue), transparent); } + +.statLabel { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + margin-bottom: 8px; +} + +.statValue { + font-family: var(--font-mono); + font-size: 26px; + font-weight: 600; + letter-spacing: -1px; +} + +.amber .statValue { color: var(--amber); } +.cyan .statValue { color: var(--cyan); } +.rose .statValue { color: var(--rose); } +.green .statValue { color: var(--green); } +.blue .statValue { color: var(--blue); } + +.statChange { + font-size: 11px; + font-family: var(--font-mono); + margin-top: 4px; +} + +.up { color: var(--rose); } +.down { color: var(--green); } +.neutral { color: var(--text-muted); } + +/* ─── App Badge ─── */ +.appBadge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 4px; + font-size: 11px; + font-family: var(--font-mono); + color: var(--text-secondary); +} + +.appDot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +/* ─── Filter Chip ─── */ +.chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 5px 12px; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 99px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; + user-select: none; +} + +.chip:hover { border-color: var(--text-muted); color: var(--text-primary); } + +.chipActive { background: var(--amber-glow); border-color: var(--amber-dim); color: var(--amber); } +.chipActive.chipGreen { background: var(--green-glow); border-color: rgba(16, 185, 129, 0.3); color: var(--green); } +.chipActive.chipRose { background: var(--rose-glow); border-color: rgba(244, 63, 94, 0.3); color: var(--rose); } +.chipActive.chipBlue { background: rgba(59, 130, 246, 0.12); border-color: rgba(59, 130, 246, 0.3); color: var(--blue); } + +.chipDot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + display: inline-block; +} + +.chipCount { + font-family: var(--font-mono); + font-size: 10px; + opacity: 0.7; +} + +/* ─── Pagination ─── */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin-top: 20px; +} + +.pageBtn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.pageBtn:hover:not(:disabled) { border-color: var(--border); background: var(--bg-raised); } +.pageBtnActive { background: var(--amber-glow); border-color: var(--amber-dim); color: var(--amber); } +.pageBtnDisabled { opacity: 0.3; cursor: default; } + +.pageEllipsis { + color: var(--text-muted); + padding: 0 4px; + font-family: var(--font-mono); +} diff --git a/ui/src/config.ts b/ui/src/config.ts new file mode 100644 index 00000000..41d6ee1c --- /dev/null +++ b/ui/src/config.ts @@ -0,0 +1,13 @@ +declare global { + interface Window { + __CAMELEER_CONFIG__?: { + apiBaseUrl?: string; + }; + } +} + +export const config = { + get apiBaseUrl(): string { + return window.__CAMELEER_CONFIG__?.apiBaseUrl ?? '/api/v1'; + }, +}; diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 00000000..845c7dc3 --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,27 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from 'react-router'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from './theme/ThemeProvider'; +import { router } from './router'; +import './theme/fonts.css'; +import './theme/tokens.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + + + , +); diff --git a/ui/src/pages/executions/ExchangeDetail.module.css b/ui/src/pages/executions/ExchangeDetail.module.css new file mode 100644 index 00000000..1fcb3453 --- /dev/null +++ b/ui/src/pages/executions/ExchangeDetail.module.css @@ -0,0 +1,75 @@ +.sidebar { + width: 280px; + flex-shrink: 0; +} + +.title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + margin-bottom: 12px; +} + +.kv { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 12px; + font-size: 12px; +} + +.kvKey { + color: var(--text-muted); + font-weight: 500; +} + +.kvValue { + font-family: var(--font-mono); + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; +} + +.bodyPreview { + margin-top: 16px; + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 12px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + max-height: 120px; + overflow: auto; + white-space: pre-wrap; + word-break: break-all; +} + +.bodyLabel { + font-family: var(--font-body); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + display: block; + margin-bottom: 6px; +} + +.errorPreview { + margin-top: 12px; + background: var(--rose-glow); + border: 1px solid rgba(244, 63, 94, 0.2); + border-radius: var(--radius-sm); + padding: 10px 12px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--rose); + max-height: 80px; + overflow: auto; +} + +@media (max-width: 1200px) { + .sidebar { width: 100%; } +} diff --git a/ui/src/pages/executions/ExchangeDetail.tsx b/ui/src/pages/executions/ExchangeDetail.tsx new file mode 100644 index 00000000..33f3c6e1 --- /dev/null +++ b/ui/src/pages/executions/ExchangeDetail.tsx @@ -0,0 +1,45 @@ +import { useProcessorSnapshot } from '../../api/queries/executions'; +import type { ExecutionSummary } from '../../api/schema'; +import styles from './ExchangeDetail.module.css'; + +interface ExchangeDetailProps { + execution: ExecutionSummary; +} + +export function ExchangeDetail({ execution }: ExchangeDetailProps) { + // Fetch the first processor's snapshot (index 0) for body preview + const { data: snapshot } = useProcessorSnapshot(execution.executionId, 0); + + return ( +
+

Exchange Details

+
+
Exchange ID
+
{execution.executionId}
+
Correlation
+
{execution.correlationId ?? '-'}
+
Application
+
{execution.agentId}
+
Route
+
{execution.routeId}
+
Timestamp
+
{new Date(execution.startTime).toISOString()}
+
Duration
+
{execution.duration}ms
+
Processors
+
{execution.processorCount}
+
+ + {snapshot?.body && ( +
+ Input Body + {snapshot.body} +
+ )} + + {execution.errorMessage && ( +
{execution.errorMessage}
+ )} +
+ ); +} diff --git a/ui/src/pages/executions/ExecutionExplorer.module.css b/ui/src/pages/executions/ExecutionExplorer.module.css new file mode 100644 index 00000000..1496ca2b --- /dev/null +++ b/ui/src/pages/executions/ExecutionExplorer.module.css @@ -0,0 +1,72 @@ +.pageHeader { + margin-bottom: 24px; + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; +} + +.pageHeader h1 { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text-primary); +} + +.subtitle { + font-size: 13px; + color: var(--text-muted); + margin-top: 2px; +} + +.liveIndicator { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--green); + font-family: var(--font-mono); + font-weight: 500; +} + +.liveDot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); + animation: livePulse 2s ease-in-out infinite; +} + +.statsBar { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +.resultsHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + padding: 0 4px; +} + +.resultsCount { + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.resultsCount strong { + color: var(--text-secondary); +} + +/* ─── Responsive ─── */ +@media (max-width: 1200px) { + .statsBar { grid-template-columns: repeat(3, 1fr); } +} + +@media (max-width: 768px) { + .statsBar { grid-template-columns: 1fr 1fr; } +} diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx new file mode 100644 index 00000000..c0f0c784 --- /dev/null +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -0,0 +1,67 @@ +import { useSearchExecutions } from '../../api/queries/executions'; +import { useExecutionSearch } from './use-execution-search'; +import { StatCard } from '../../components/shared/StatCard'; +import { Pagination } from '../../components/shared/Pagination'; +import { SearchFilters } from './SearchFilters'; +import { ResultsTable } from './ResultsTable'; +import styles from './ExecutionExplorer.module.css'; + +export function ExecutionExplorer() { + const { toSearchRequest, offset, limit, setOffset } = useExecutionSearch(); + const searchRequest = toSearchRequest(); + const { data, isLoading, isFetching } = useSearchExecutions(searchRequest); + + const total = data?.total ?? 0; + const results = data?.results ?? []; + + // Derive stats from current search results + const failedCount = results.filter((r) => r.status === 'FAILED').length; + const avgDuration = results.length > 0 + ? Math.round(results.reduce((sum, r) => sum + r.duration, 0) / results.length) + : 0; + + const showFrom = total > 0 ? offset + 1 : 0; + const showTo = Math.min(offset + limit, total); + + return ( + <> + {/* Page Header */} +
+
+

Transaction Explorer

+
Search and analyze route executions
+
+
+ + LIVE +
+
+ + {/* Stats Bar */} +
+ + + + + +
+ + {/* Filters */} + + + {/* Results Header */} +
+ + Showing {showFrom}–{showTo} of {total.toLocaleString()} results + {isFetching && !isLoading && ' · updating...'} + +
+ + {/* Results Table */} + + + {/* Pagination */} + + + ); +} diff --git a/ui/src/pages/executions/ProcessorTree.module.css b/ui/src/pages/executions/ProcessorTree.module.css new file mode 100644 index 00000000..d2c5f85e --- /dev/null +++ b/ui/src/pages/executions/ProcessorTree.module.css @@ -0,0 +1,97 @@ +.tree { + flex: 1; + min-width: 0; +} + +.title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + margin-bottom: 12px; +} + +.procNode { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 12px; + border-radius: var(--radius-sm); + margin-bottom: 2px; + transition: background 0.1s; + position: relative; +} + +.procNode:hover { background: var(--bg-surface); } + +.procConnector { + position: absolute; + left: 22px; + top: 28px; + bottom: -4px; + width: 1px; + background: var(--border); +} + +.procNode:last-child .procConnector { display: none; } + +.procIcon { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; + z-index: 1; + font-family: var(--font-mono); +} + +.iconEndpoint { background: rgba(59, 130, 246, 0.15); color: var(--blue); border: 1px solid rgba(59, 130, 246, 0.3); } +.iconProcessor { background: var(--green-glow); color: var(--green); border: 1px solid rgba(16, 185, 129, 0.3); } +.iconEip { background: rgba(168, 85, 247, 0.12); color: #a855f7; border: 1px solid rgba(168, 85, 247, 0.3); } +.iconError { background: var(--rose-glow); color: var(--rose); border: 1px solid rgba(244, 63, 94, 0.3); } + +.procInfo { flex: 1; min-width: 0; } + +.procType { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} + +.procUri { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.procTiming { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + flex-shrink: 0; + text-align: right; +} + +.procDuration { + font-weight: 600; + color: var(--text-secondary); +} + +.nested { + margin-left: 24px; +} + +.loading { + color: var(--text-muted); + font-size: 12px; + font-family: var(--font-mono); + padding: 12px; +} diff --git a/ui/src/pages/executions/ProcessorTree.tsx b/ui/src/pages/executions/ProcessorTree.tsx new file mode 100644 index 00000000..aaea78c9 --- /dev/null +++ b/ui/src/pages/executions/ProcessorTree.tsx @@ -0,0 +1,70 @@ +import { useExecutionDetail } from '../../api/queries/executions'; +import type { ProcessorNode as ProcessorNodeType } from '../../api/schema'; +import styles from './ProcessorTree.module.css'; + +const ICON_MAP: Record = { + from: { label: 'EP', className: styles.iconEndpoint }, + to: { label: 'EP', className: styles.iconEndpoint }, + toD: { label: 'EP', className: styles.iconEndpoint }, + choice: { label: 'CB', className: styles.iconEip }, + when: { label: 'CB', className: styles.iconEip }, + otherwise: { label: 'CB', className: styles.iconEip }, + split: { label: 'CB', className: styles.iconEip }, + aggregate: { label: 'CB', className: styles.iconEip }, + filter: { label: 'CB', className: styles.iconEip }, + multicast: { label: 'CB', className: styles.iconEip }, + recipientList: { label: 'CB', className: styles.iconEip }, + routingSlip: { label: 'CB', className: styles.iconEip }, + dynamicRouter: { label: 'CB', className: styles.iconEip }, + exception: { label: '!!', className: styles.iconError }, + onException: { label: '!!', className: styles.iconError }, +}; + +function getIcon(type: string, status: string) { + if (status === 'FAILED') return { label: '!!', className: styles.iconError }; + const key = type.toLowerCase(); + return ICON_MAP[key] ?? { label: 'PR', className: styles.iconProcessor }; +} + +export function ProcessorTree({ executionId }: { executionId: string }) { + const { data, isLoading } = useExecutionDetail(executionId); + + if (isLoading) return
Loading processor tree...
; + if (!data) return null; + + return ( +
+

Processor Execution Tree

+ {data.processors.map((proc) => ( + + ))} +
+ ); +} + +function ProcessorNodeView({ node }: { node: ProcessorNodeType }) { + const icon = getIcon(node.processorType, node.status); + + return ( +
+
+
+
{icon.label}
+
+
{node.processorType}
+ {node.uri &&
{node.uri}
} +
+
+ {node.duration}ms +
+
+ {node.children.length > 0 && ( +
+ {node.children.map((child) => ( + + ))} +
+ )} +
+ ); +} diff --git a/ui/src/pages/executions/ResultsTable.module.css b/ui/src/pages/executions/ResultsTable.module.css new file mode 100644 index 00000000..d15bdede --- /dev/null +++ b/ui/src/pages/executions/ResultsTable.module.css @@ -0,0 +1,104 @@ +.tableWrap { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.thead { + background: var(--bg-raised); + border-bottom: 1px solid var(--border); +} + +.th { + padding: 12px 16px; + text-align: left; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + user-select: none; + white-space: nowrap; +} + +.row { + border-bottom: 1px solid var(--border-subtle); + transition: background 0.1s; + cursor: pointer; +} + +.row:last-child { border-bottom: none; } +.row:hover { background: var(--bg-raised); } + +.td { + padding: 12px 16px; + vertical-align: middle; + white-space: nowrap; +} + +.tdExpand { + width: 32px; + text-align: center; + color: var(--text-muted); + font-size: 16px; + transition: transform 0.2s; +} + +.expanded .tdExpand { + transform: rotate(90deg); + color: var(--amber); +} + +.correlationId { + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ─── Detail Row ─── */ +.detailRow { + display: none; +} + +.detailRowVisible { + display: table-row; +} + +.detailCell { + padding: 0 !important; + background: var(--bg-base); + border-bottom: 1px solid var(--border); +} + +.detailContent { + padding: 20px 24px; + display: flex; + gap: 24px; +} + +/* ─── Loading / Empty ─── */ +.emptyState { + text-align: center; + padding: 48px 24px; + color: var(--text-muted); + font-size: 14px; +} + +.loadingOverlay { + text-align: center; + padding: 48px 24px; + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 13px; +} + +@media (max-width: 1200px) { + .detailContent { flex-direction: column; } +} diff --git a/ui/src/pages/executions/ResultsTable.tsx b/ui/src/pages/executions/ResultsTable.tsx new file mode 100644 index 00000000..3423dc5b --- /dev/null +++ b/ui/src/pages/executions/ResultsTable.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react'; +import type { ExecutionSummary } from '../../api/schema'; +import { StatusPill } from '../../components/shared/StatusPill'; +import { DurationBar } from '../../components/shared/DurationBar'; +import { AppBadge } from '../../components/shared/AppBadge'; +import { ProcessorTree } from './ProcessorTree'; +import { ExchangeDetail } from './ExchangeDetail'; +import styles from './ResultsTable.module.css'; + +interface ResultsTableProps { + results: ExecutionSummary[]; + loading: boolean; +} + +function formatTime(iso: string) { + return new Date(iso).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + }); +} + +export function ResultsTable({ results, loading }: ResultsTableProps) { + const [expandedId, setExpandedId] = useState(null); + + if (loading && results.length === 0) { + return ( +
+
Loading executions...
+
+ ); + } + + if (results.length === 0) { + return ( +
+
No executions found matching your filters.
+
+ ); + } + + return ( +
+ + + + + + + + + + + + + + {results.map((exec) => { + const isExpanded = expandedId === exec.executionId; + return ( + setExpandedId(isExpanded ? null : exec.executionId)} + /> + ); + })} + +
+ TimestampStatusApplicationRouteCorrelation IDDurationProcessors
+
+ ); +} + +function ResultRow({ + exec, + isExpanded, + onToggle, +}: { + exec: ExecutionSummary; + isExpanded: boolean; + onToggle: () => void; +}) { + return ( + <> + + › + {formatTime(exec.startTime)} + + + + + + + {exec.routeId} + + {exec.correlationId ?? '-'} + + + + + {exec.processorCount} + + {isExpanded && ( + + +
+ + +
+ + + )} + + ); +} diff --git a/ui/src/pages/executions/SearchFilters.module.css b/ui/src/pages/executions/SearchFilters.module.css new file mode 100644 index 00000000..85da73ff --- /dev/null +++ b/ui/src/pages/executions/SearchFilters.module.css @@ -0,0 +1,214 @@ +.filterBar { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + padding: 16px 20px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.filterRow { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.searchInputWrap { + flex: 1; + min-width: 300px; + position: relative; +} + +.searchIcon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + color: var(--text-muted); +} + +.searchInput { + width: 100%; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 14px 10px 40px; + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 13px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.searchInput::placeholder { + color: var(--text-muted); + font-family: var(--font-body); +} + +.searchInput:focus { + border-color: var(--amber-dim); + box-shadow: 0 0 0 3px var(--amber-glow); +} + +.searchHint { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + font-family: var(--font-mono); + font-size: 10px; + padding: 3px 8px; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-muted); +} + +.filterGroup { + display: flex; + align-items: center; + gap: 6px; +} + +.filterLabel { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + white-space: nowrap; +} + +.filterChips { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.separator { + width: 1px; + height: 24px; + background: var(--border); + margin: 0 4px; +} + +.dateInput { + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 8px 12px; + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 12px; + outline: none; + width: 180px; + transition: border-color 0.2s; +} + +.dateInput:focus { border-color: var(--amber-dim); } + +.dateArrow { + color: var(--text-muted); + font-size: 12px; +} + +.durationRange { + display: flex; + align-items: center; + gap: 8px; +} + +.rangeInput { + width: 100px; + accent-color: var(--amber); + cursor: pointer; +} + +.rangeLabel { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + min-width: 50px; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-raised); + color: var(--text-secondary); + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btn:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--text-muted); } + +.btnPrimary { + composes: btn; + background: var(--amber); + color: #0a0e17; + border-color: var(--amber); + font-weight: 600; +} + +.btnPrimary:hover { background: var(--amber-hover); border-color: var(--amber-hover); color: #0a0e17; } + +.filterTags { + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} + +.filterTag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: var(--amber-glow); + border: 1px solid rgba(240, 180, 41, 0.2); + border-radius: 99px; + font-size: 12px; + color: var(--amber); + font-family: var(--font-mono); +} + +.filterTagRemove { + cursor: pointer; + opacity: 0.5; + font-size: 14px; + line-height: 1; + background: none; + border: none; + color: inherit; +} + +.filterTagRemove:hover { opacity: 1; } + +.clearAll { + font-size: 11px; + color: var(--text-muted); + cursor: pointer; + padding: 4px 8px; + background: none; + border: none; +} + +.clearAll:hover { color: var(--rose); } + +@media (max-width: 768px) { + .filterRow { flex-direction: column; align-items: stretch; } + .searchInputWrap { min-width: unset; } +} diff --git a/ui/src/pages/executions/SearchFilters.tsx b/ui/src/pages/executions/SearchFilters.tsx new file mode 100644 index 00000000..76787321 --- /dev/null +++ b/ui/src/pages/executions/SearchFilters.tsx @@ -0,0 +1,124 @@ +import { useRef, useCallback } from 'react'; +import { useExecutionSearch } from './use-execution-search'; +import { FilterChip } from '../../components/shared/FilterChip'; +import styles from './SearchFilters.module.css'; + +export function SearchFilters() { + const { + status, toggleStatus, + timeFrom, setTimeFrom, + timeTo, setTimeTo, + durationMax, setDurationMax, + text, setText, + clearAll, + } = useExecutionSearch(); + + const debounceRef = useRef>(undefined); + + const handleTextChange = useCallback( + (value: string) => { + clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => setText(value), 300); + }, + [setText], + ); + + const activeTags: { label: string; onRemove: () => void }[] = []; + if (text) activeTags.push({ label: `text:"${text}"`, onRemove: () => setText('') }); + if (timeFrom) activeTags.push({ label: `from:${timeFrom}`, onRemove: () => setTimeFrom('') }); + if (timeTo) activeTags.push({ label: `to:${timeTo}`, onRemove: () => setTimeTo('') }); + if (durationMax && durationMax < 5000) { + activeTags.push({ label: `duration:≤${durationMax}ms`, onRemove: () => setDurationMax(null) }); + } + + return ( +
+ {/* Row 1: Search */} +
+
+ + + + + handleTextChange(e.target.value)} + /> + ⌘K +
+ +
+ + {/* Row 2: Status chips + date + duration */} +
+
+ +
+ toggleStatus('COMPLETED')} /> + toggleStatus('FAILED')} /> + toggleStatus('RUNNING')} /> +
+
+ +
+ +
+ + setTimeFrom(e.target.value)} + /> + + setTimeTo(e.target.value)} + /> +
+ +
+ +
+ +
+ 0ms + { + const v = Number(e.target.value); + setDurationMax(v >= 5000 ? null : v); + }} + /> + ≤ {durationMax ?? 5000}ms +
+
+
+ + {/* Row 3: Active filter tags */} + {activeTags.length > 0 && ( +
+
+ {activeTags.map((tag) => ( + + {tag.label} + + + ))} + +
+
+ )} +
+ ); +} diff --git a/ui/src/pages/executions/use-execution-search.ts b/ui/src/pages/executions/use-execution-search.ts new file mode 100644 index 00000000..3fd3b95d --- /dev/null +++ b/ui/src/pages/executions/use-execution-search.ts @@ -0,0 +1,77 @@ +import { create } from 'zustand'; +import type { SearchRequest } from '../../api/schema'; + +interface ExecutionSearchState { + status: string[]; + timeFrom: string; + timeTo: string; + durationMin: number | null; + durationMax: number | null; + text: string; + offset: number; + limit: number; + + setStatus: (statuses: string[]) => void; + toggleStatus: (s: string) => void; + setTimeFrom: (v: string) => void; + setTimeTo: (v: string) => void; + setDurationMin: (v: number | null) => void; + setDurationMax: (v: number | null) => void; + setText: (v: string) => void; + setOffset: (v: number) => void; + clearAll: () => void; + toSearchRequest: () => SearchRequest; +} + +export const useExecutionSearch = create((set, get) => ({ + status: ['COMPLETED', 'FAILED'], + timeFrom: '', + timeTo: '', + durationMin: null, + durationMax: null, + text: '', + offset: 0, + limit: 25, + + setStatus: (statuses) => set({ status: statuses, offset: 0 }), + toggleStatus: (s) => + set((state) => ({ + status: state.status.includes(s) + ? state.status.filter((x) => x !== s) + : [...state.status, s], + offset: 0, + })), + setTimeFrom: (v) => set({ timeFrom: v, offset: 0 }), + setTimeTo: (v) => set({ timeTo: v, offset: 0 }), + setDurationMin: (v) => set({ durationMin: v, offset: 0 }), + setDurationMax: (v) => set({ durationMax: v, offset: 0 }), + setText: (v) => set({ text: v, offset: 0 }), + setOffset: (v) => set({ offset: v }), + clearAll: () => + set({ + status: ['COMPLETED', 'FAILED', 'RUNNING'], + timeFrom: '', + timeTo: '', + durationMin: null, + durationMax: null, + text: '', + offset: 0, + }), + + toSearchRequest: (): SearchRequest => { + const s = get(); + const statusStr = s.status.length > 0 && s.status.length < 3 + ? s.status.join(',') + : undefined; + return { + status: statusStr ?? undefined, + timeFrom: s.timeFrom ? new Date(s.timeFrom).toISOString() : undefined, + timeTo: s.timeTo ? new Date(s.timeTo).toISOString() : undefined, + durationMin: s.durationMin, + durationMax: s.durationMax, + text: s.text || undefined, + offset: s.offset, + limit: s.limit, + }; + }, +})); diff --git a/ui/src/router.tsx b/ui/src/router.tsx new file mode 100644 index 00000000..8e289213 --- /dev/null +++ b/ui/src/router.tsx @@ -0,0 +1,24 @@ +import { createBrowserRouter, Navigate } from 'react-router'; +import { AppShell } from './components/layout/AppShell'; +import { ProtectedRoute } from './auth/ProtectedRoute'; +import { LoginPage } from './auth/LoginPage'; +import { ExecutionExplorer } from './pages/executions/ExecutionExplorer'; + +export const router = createBrowserRouter([ + { + path: '/login', + element: , + }, + { + element: , + children: [ + { + element: , + children: [ + { index: true, element: }, + { path: 'executions', element: }, + ], + }, + ], + }, +]); diff --git a/ui/src/theme/ThemeProvider.tsx b/ui/src/theme/ThemeProvider.tsx new file mode 100644 index 00000000..977a6ab9 --- /dev/null +++ b/ui/src/theme/ThemeProvider.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useThemeStore } from './theme-store'; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const theme = useThemeStore((s) => s.theme); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); + + return <>{children}; +} diff --git a/ui/src/theme/fonts.css b/ui/src/theme/fonts.css new file mode 100644 index 00000000..0c5c6772 --- /dev/null +++ b/ui/src/theme/fonts.css @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@300;400;500;600&display=swap'); diff --git a/ui/src/theme/theme-store.ts b/ui/src/theme/theme-store.ts new file mode 100644 index 00000000..49b3b083 --- /dev/null +++ b/ui/src/theme/theme-store.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +type Theme = 'dark' | 'light'; + +interface ThemeState { + theme: Theme; + toggle: () => void; +} + +const stored = localStorage.getItem('cameleer-theme') as Theme | null; +const initial: Theme = stored === 'light' ? 'light' : 'dark'; + +export const useThemeStore = create((set) => ({ + theme: initial, + toggle: () => + set((state) => { + const next = state.theme === 'dark' ? 'light' : 'dark'; + localStorage.setItem('cameleer-theme', next); + return { theme: next }; + }), +})); diff --git a/ui/src/theme/tokens.css b/ui/src/theme/tokens.css new file mode 100644 index 00000000..673e3e6c --- /dev/null +++ b/ui/src/theme/tokens.css @@ -0,0 +1,143 @@ +/* ─── Dark Theme (default) ─── */ +:root { + --bg-deep: #060a13; + --bg-base: #0a0e17; + --bg-surface: #111827; + --bg-raised: #1a2332; + --bg-hover: #1e2d3d; + --border: #1e2d3d; + --border-subtle: #152030; + --text-primary: #e2e8f0; + --text-secondary: #8b9cb6; + --text-muted: #4a5e7a; + --amber: #f0b429; + --amber-dim: #b8860b; + --amber-glow: rgba(240, 180, 41, 0.15); + --cyan: #22d3ee; + --cyan-dim: #0e7490; + --cyan-glow: rgba(34, 211, 238, 0.12); + --rose: #f43f5e; + --rose-dim: #9f1239; + --rose-glow: rgba(244, 63, 94, 0.12); + --green: #10b981; + --green-glow: rgba(16, 185, 129, 0.12); + --blue: #3b82f6; + --purple: #a855f7; + --font-body: 'DM Sans', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + + /* TopNav glass */ + --topnav-bg: rgba(6, 10, 19, 0.85); + /* Button primary hover */ + --amber-hover: #d4a017; +} + +/* ─── Light Theme ─── */ +[data-theme="light"] { + --bg-deep: #f7f5f2; + --bg-base: #efecea; + --bg-surface: #ffffff; + --bg-raised: #f3f1ee; + --bg-hover: #eae7e3; + --border: #d4cfc8; + --border-subtle: #e4e0db; + --text-primary: #1c1917; + --text-secondary: #57534e; + --text-muted: #a8a29e; + --amber: #b45309; + --amber-dim: #92400e; + --amber-glow: rgba(180, 83, 9, 0.07); + --cyan: #0e7490; + --cyan-dim: #155e75; + --cyan-glow: rgba(14, 116, 144, 0.06); + --rose: #be123c; + --rose-dim: #9f1239; + --rose-glow: rgba(190, 18, 60, 0.05); + --green: #047857; + --green-glow: rgba(4, 120, 87, 0.06); + --blue: #1d4ed8; + --purple: #7c3aed; + + --topnav-bg: rgba(247, 245, 242, 0.85); + --amber-hover: #92400e; +} + +/* ─── Global Reset & Body ─── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg-deep); + color: var(--text-primary); + font-family: var(--font-body); + font-size: 14px; + line-height: 1.5; + min-height: 100vh; + overflow-x: hidden; +} + +a { color: var(--amber); text-decoration: none; } +a:hover { color: var(--text-primary); } + +/* ─── Background Treatment ─── */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 800px 400px at 20% 20%, rgba(240, 180, 41, 0.03), transparent), + radial-gradient(ellipse 600px 600px at 80% 80%, rgba(34, 211, 238, 0.02), transparent); + pointer-events: none; + z-index: 0; +} + +body::after { + content: ''; + position: fixed; + inset: 0; + opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg width='400' height='400' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 200 Q100 150 200 200 T400 200' fill='none' stroke='%23f0b429' stroke-width='1'/%3E%3Cpath d='M0 220 Q120 170 200 220 T400 220' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 180 Q80 130 200 180 T400 180' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 100 Q150 60 200 100 T400 100' fill='none' stroke='%2322d3ee' stroke-width='0.5'/%3E%3Cpath d='M0 300 Q100 260 200 300 T400 300' fill='none' stroke='%2322d3ee' stroke-width='0.5'/%3E%3C/svg%3E"); + background-size: 400px 400px; + pointer-events: none; + z-index: 0; +} + +[data-theme="light"] body::before, +[data-theme="light"] body::after { + opacity: 0.015; +} + +/* ─── Animations ─── */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes livePulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); } + 50% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); } +} + +.animate-in { animation: fadeIn 0.3s ease-out both; } +.delay-1 { animation-delay: 0.05s; } +.delay-2 { animation-delay: 0.1s; } +.delay-3 { animation-delay: 0.15s; } +.delay-4 { animation-delay: 0.2s; } +.delay-5 { animation-delay: 0.25s; } + +/* ─── Scrollbar ─── */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* ─── Utility ─── */ +.mono { font-family: var(--font-mono); font-size: 12px; } +.text-muted { color: var(--text-muted); } +.text-secondary { color: var(--text-secondary); } diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json new file mode 100644 index 00000000..af516fcc --- /dev/null +++ b/ui/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json new file mode 100644 index 00000000..8a67f62f --- /dev/null +++ b/ui/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 00000000..12c4b2c5 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8081', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + }, +});