diff --git a/frontend/goquery-ui/.dockerignore b/frontend/goquery-ui/.dockerignore new file mode 100644 index 00000000..cc0931f2 --- /dev/null +++ b/frontend/goquery-ui/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +dist +.git +.gitignore +.DS_Store diff --git a/frontend/goquery-ui/.env.development b/frontend/goquery-ui/.env.development new file mode 100644 index 00000000..ab8e2e9e --- /dev/null +++ b/frontend/goquery-ui/.env.development @@ -0,0 +1,9 @@ +# Global Query API base URL for development +GQ_API_BASE_URL=http://localhost:8145 + +# Comma-separated list of host resolver options shown in Settings +# Read by webpack dev server to generate /env.js as window.__ENV__.HOST_RESOLVER_TYPES +HOST_RESOLVER_TYPES=string,gethosts + +# Enable Server-Sent Events by default on load +SSE_ON_LOAD=true diff --git a/frontend/goquery-ui/.gitignore b/frontend/goquery-ui/.gitignore new file mode 100644 index 00000000..4b906a03 --- /dev/null +++ b/frontend/goquery-ui/.gitignore @@ -0,0 +1,5 @@ +global-query_*_openapi.yaml + +# deps and distribution +node_modules/ +dist/ diff --git a/frontend/goquery-ui/.prettierrc b/frontend/goquery-ui/.prettierrc new file mode 100644 index 00000000..238d4d93 --- /dev/null +++ b/frontend/goquery-ui/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/frontend/goquery-ui/Dockerfile b/frontend/goquery-ui/Dockerfile new file mode 100644 index 00000000..6fcb90f9 --- /dev/null +++ b/frontend/goquery-ui/Dockerfile @@ -0,0 +1,43 @@ +# ---- build ---- + +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ +RUN --mount=type=cache,target=/home/nonroot/.npm npm ci --no-audit --no-fund +COPY . . +RUN npm run build + +# ---- runtime (Caddy) ---- + +FROM caddy:2-alpine AS caddy-builder + +# Use Alpine as the base for better security and fewer vulnerabilities +FROM alpine:3.19 + +# Install ca-certificates for HTTPS support +RUN apk add --no-cache ca-certificates + +# Copy Caddy binary from the builder image +COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy + +# Create a non-root user for running Caddy +RUN addgroup -g 1001 caddy && \ + adduser -D -u 1001 -G caddy -h /var/lib/caddy -s /sbin/nologin caddy +RUN mkdir -p /var/run/env /opt/app/www /etc/caddy /data /config && \ + chown -R caddy:caddy /var/run/env /opt/app/www /etc/caddy /data /config + +# static files go to a read-only dir +COPY --from=build --chown=caddy:caddy /app/dist /opt/app/www +## ensure protocol map is present as a static asset as well +COPY --from=build --chown=caddy:caddy /app/src/api/proto-map.json /opt/app/www/proto-map.json + +# caddy config +COPY --chown=caddy:caddy deploy/Caddyfile /etc/caddy/Caddyfile + +# Switch to non-root user +USER caddy + +EXPOSE 5137 + +# default command +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] diff --git a/frontend/goquery-ui/Makefile b/frontend/goquery-ui/Makefile new file mode 100644 index 00000000..257649cf --- /dev/null +++ b/frontend/goquery-ui/Makefile @@ -0,0 +1,119 @@ +# Simple Makefile for goquery-ui local development +# Focus: install deps, build bundle, watch, serve, docker setup + +# configurable vars +NPM ?= npm +PORT ?= 8000 +DIST_DIR := dist +WEBPACK := node_modules/.bin/webpack +# optional lock file (any one). If none exists we still allow install. +LOCKFILE := $(firstword $(wildcard package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock)) +# OpenAPI spec version (can be overridden: `make VERSION=4.1.19 types`) +VERSION ?= 4.1.18 +OPENAPI_SPEC := global-query_$(VERSION)_openapi.yaml +GENERATED_TYPES := src/api/generated.ts + +# default target +.DEFAULT_GOAL := help + +.PHONY: help +help: + @echo "Available targets:" + @echo " make install - install node dependencies" + @echo " make build - build bundle -> $(DIST_DIR)/bundle.js" + @echo " make watch - incremental rebuild (webpack --watch)" + @echo " make serve - serve static files on http://localhost:$(PORT) (python simple server)" + @echo " make dev - build then instructions to run watch+serve in 2 shells" + @echo " make clean - remove dist directory" + @echo " make format - format TypeScript/JavaScript files with prettier" + @echo " make generate-index - create a minimal index.html if missing" + @echo " make types - fetch OpenAPI $(VERSION) and generate TS types ($(GENERATED_TYPES))" + @echo " make regen - install + types + build" + @echo " make docker-dev - docker compose dev profile (hot reload)" + @echo " make docker-stop - stop docker compose dev profile" + @echo " make docker-logs - view docker compose dev logs" + @echo " make docker-prod - docker compose prod-like profile (Caddy)" + @echo " make docker-build - build Caddy image locally" + +# install: re-run if package.json or chosen lockfile changes +node_modules: package.json $(LOCKFILE) + $(NPM) install + @touch node_modules + +.PHONY: install +install: node_modules + +# fetch OpenAPI spec from GitHub releases if missing or version changed +$(OPENAPI_SPEC): + @echo "Fetching OpenAPI spec version $(VERSION) ..." + curl -fsSL -o "$@" "https://github.com/els0r/goProbe/releases/download/v$(VERSION)/global-query_$(VERSION)_openapi.yaml" + @echo "Saved $@" + +# openapi type generation (uses local dev dependency openapi-typescript) +$(GENERATED_TYPES): $(OPENAPI_SPEC) | install + $(NPM) exec -- openapi-typescript "$(OPENAPI_SPEC)" --output "$(GENERATED_TYPES)" + +.PHONY: types +types: $(GENERATED_TYPES) + +.PHONY: regen +regen: install types build + +# generate a minimal index.html (idempotent) +index.html: + @[ -f index.html ] || echo '\n\nGoquery UI\n\n
\n\n\n' > index.html + +.PHONY: generate-index +generate-index: index.html + @echo "index.html present" + +.PHONY: build +build: install index.html + $(NPM) run build + +.PHONY: watch +watch: install + $(WEBPACK) --watch + +# simple static file server (CTRL+C to stop) +.PHONY: serve +serve: build + python3 -m http.server $(PORT) + +.PHONY: dev +dev: build + @echo "DEPRECATED: preferably use \"make docker-dev\"" + @echo "Run these in two terminals for live development:" \ + "\n Terminal 1: make watch" \ + "\n Terminal 2: make serve" \ + "\nThen open http://localhost:$(PORT)" + +.PHONY: clean +clean: + rm -rf $(DIST_DIR) $(GENERATED_TYPES) + +.PHONY: format +format: install + $(NPM) exec -- prettier --write "src/**/*.{js,jsx,ts,tsx}" + +.PHONY: docker-dev +docker-dev: + @echo "Starting Docker development server with hot reload..." + @echo "Access the application at http://localhost:5173" + docker compose --profile dev up --build + +.PHONY: docker-stop +docker-stop: + docker compose --profile dev down + +.PHONY: docker-logs +docker-logs: + docker compose --profile dev logs -f + +.PHONY: docker-prod +docker-prod: types + docker compose --profile prod up --build + +.PHONY: docker-build +docker-build: types + docker build -t goquery-ui:caddy-local . diff --git a/frontend/goquery-ui/README.md b/frontend/goquery-ui/README.md new file mode 100644 index 00000000..3a435b9f --- /dev/null +++ b/frontend/goquery-ui/README.md @@ -0,0 +1,122 @@ +# Goquery UI — Network Usage Explorer + +Lightweight table + graph UI for exploring network flow data via the Global Query API. + +## Prerequisites + +- Node.js 18+ +- npm 9+ + +## Install + +```bash +npm ci +``` + +## Develop + +Run with Docker Compose from this folder. + +### Dev (hot reload via webpack-dev-server) + +```bash +docker compose --profile dev up +``` + +Open + +### Without Docker + +Start webpack in watch mode and open `index.html` in a local static server (or your browser directly): + +```bash +npm run dev +``` + +The bundle is emitted to `dist/` and `index.html` loads it. + +## Build + +```bash +npm run build +``` + +## Deployment + +### Prod-like (Caddy serves built SPA with runtime env.js) + +```bash +docker compose --profile prod up --build +``` + +Open + +Optional .env (next to docker-compose.yml) to override runtime values: + +```bash +GQ_API_BASE_URL=http://localhost:8081 +HOST_RESOLVER_TYPES=dns,cache,local +SSE_ON_LOAD=true +``` + +Makefile shortcuts: + +```bash +make docker-dev # same as compose dev profile +make docker-prod # same as compose prod profile +make docker-build # build Caddy image locally +``` + +## Using the UI + +- Time range: quick presets (5m…30d) or set exact From/To. +- Hosts Query: free text filter (sets `query_hosts`). +- Interfaces: comma-separated list (sets `ifaces`). +- Attributes: choose which columns to group by (leave blank for all). +- Condition: free text filter (ANDs expressions like `proto=TCP and (dport=80 or dport=443)`). +- Sorting / Limit: choose metric and direction; limit applies to non-time queries. +- Run: executes the query and renders results. + +### Interactions + +- Graph tab + - Click an IP: opens service breakdown (proto/dport) with in/out and totals. + - Click an interface: opens services for that host/interface (scoped by host_id + iface). + - Click a host: shows per-interface totals for the host. +- Table tab + - Click a row: opens a temporal drilldown (attributes = time) scoped by row’s host_id + iface and filtered by sip/dip/dport/proto. + - Press Enter: opens temporal drilldown for the first row when no panel is open. + +### Panels + +- Escape closes any open panel; clicking outside also closes it. +- Unidirectional slots/cards are highlighted in red. +- Zero values are suppressed (no `0 B` / `0 pkts`). +- Temporal drilldown: + - Header shows `HOST — IFACE` with total Bytes/Packets. + - Only attributes visible in the table are shown in the header row. + - Consecutive timestamps on the same day show only HH:MM:SS (timezone suffix hidden). + +## Saved Views & Export + +- Save view: stores current params in localStorage. Load from the dropdown. +- Export CSV: exports the current table with selected columns and totals. + +## Troubleshooting + +- If the editor reports a spurious import error for a newly added view file, run a fresh typecheck: + +```bash +npm run typecheck +``` + +- If the Global Query API changes, regenerate client types: + +```bash +npm run generate:types +``` + +## Notes + +- For development without a server, open `index.html` directly; API calls must resolve against your environment-provided endpoint if required by `client.ts`. +- This UI is a standalone package under `applications/network-observability/src/goquery-ui`. diff --git a/frontend/goquery-ui/deploy/Caddyfile b/frontend/goquery-ui/deploy/Caddyfile new file mode 100644 index 00000000..f5b3e393 --- /dev/null +++ b/frontend/goquery-ui/deploy/Caddyfile @@ -0,0 +1,53 @@ +{ + # We're behind an Ingress; don't manage TLS here + auto_https off + admin off +} + +:5137 { + root * /opt/app/www + encode zstd gzip + + # Security headers (tune CSP to your domains!) + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy no-referrer-when-downgrade + Cross-Origin-Opener-Policy same-origin + Cross-Origin-Resource-Policy same-origin + Cross-Origin-Embedder-Policy require-corp + Permissions-Policy "geolocation=(), microphone=(), camera=()" + Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; connect-src 'self' http: https:; frame-ancestors 'none';" + } + + # Cache policies + @assets { + path *.js *.css *.woff *.woff2 *.ttf *.otf *.eot + } + header @assets Cache-Control "public, max-age=31536000, immutable" + + @images { + path *.png *.jpg *.jpeg *.gif *.webp *.svg + } + header @images Cache-Control "public, max-age=604800" + + # runtime env.js (no-store) served from a writable volume + @envjs path /env.js + header @envjs Cache-Control "no-store" + header @envjs Content-Type "application/javascript" + route @envjs { + file_server { + root /var/run/env + } + } + + # Serve static files first + file_server + + # SPA fallback: if request isn't an existing file AND not env.js, serve index.html + @notFile { + not file + not path /env.js + } + rewrite @notFile /index.html +} diff --git a/frontend/goquery-ui/docker-compose.yml b/frontend/goquery-ui/docker-compose.yml new file mode 100644 index 00000000..0879730d --- /dev/null +++ b/frontend/goquery-ui/docker-compose.yml @@ -0,0 +1,53 @@ +volumes: + env_run: {} + caddy_data: {} + caddy_config: {} + +services: + dev: + profiles: ["dev"] + image: node:20-bookworm-slim + working_dir: /app + command: sh -c "npm ci && npm run dev" + ports: + - "5173:5173" + env_file: + - .env.development + environment: + NODE_ENV: development + volumes: + - ./:/app:delegated + - /app/node_modules + + env-writer: + profiles: ['prod'] + image: debian:bookworm-slim + command: bash /scripts/write-env.sh + environment: + GQ_API_BASE_URL: ${GQ_API_BASE_URL:-http://localhost:8081} + HOST_RESOLVER_TYPES: ${HOST_RESOLVER_TYPES:-string} + SSE_ON_LOAD: ${SSE_ON_LOAD:-true} + volumes: + - env_run:/var/run/env + - ./scripts:/scripts:ro + + web: + profiles: ['prod'] + build: + context: . + dockerfile: Dockerfile + image: goquery-ui:caddy-local + depends_on: + env-writer: + condition: service_completed_successfully + ports: + - '8080:8080' + read_only: true + security_opt: + - no-new-privileges:true + volumes: + - env_run:/var/run/env + - caddy_data:/data + - caddy_config:/config + environment: + NODE_ENV: production diff --git a/frontend/goquery-ui/img/goquery-graph-wireframe.png b/frontend/goquery-ui/img/goquery-graph-wireframe.png new file mode 100644 index 00000000..51c37e68 Binary files /dev/null and b/frontend/goquery-ui/img/goquery-graph-wireframe.png differ diff --git a/frontend/goquery-ui/img/ip-detail-wireframe.png b/frontend/goquery-ui/img/ip-detail-wireframe.png new file mode 100644 index 00000000..028aa6ae Binary files /dev/null and b/frontend/goquery-ui/img/ip-detail-wireframe.png differ diff --git a/frontend/goquery-ui/img/occurrence-wireframe.png b/frontend/goquery-ui/img/occurrence-wireframe.png new file mode 100644 index 00000000..3b58c9cf Binary files /dev/null and b/frontend/goquery-ui/img/occurrence-wireframe.png differ diff --git a/frontend/goquery-ui/img/query-input-panel-wireframe.png b/frontend/goquery-ui/img/query-input-panel-wireframe.png new file mode 100644 index 00000000..078fd30c Binary files /dev/null and b/frontend/goquery-ui/img/query-input-panel-wireframe.png differ diff --git a/frontend/goquery-ui/img/temporal-view-wireframe.png b/frontend/goquery-ui/img/temporal-view-wireframe.png new file mode 100644 index 00000000..01625c08 Binary files /dev/null and b/frontend/goquery-ui/img/temporal-view-wireframe.png differ diff --git a/frontend/goquery-ui/index.html b/frontend/goquery-ui/index.html new file mode 100644 index 00000000..904b4905 --- /dev/null +++ b/frontend/goquery-ui/index.html @@ -0,0 +1,15 @@ + + + + + + Goquery UI + + + + + + +
+ + diff --git a/frontend/goquery-ui/package-lock.json b/frontend/goquery-ui/package-lock.json new file mode 100644 index 00000000..c19da220 --- /dev/null +++ b/frontend/goquery-ui/package-lock.json @@ -0,0 +1,5195 @@ +{ + "name": "goquery-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "goquery-ui", + "version": "0.1.0", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", + "autoprefixer": "10.4.16", + "css-loader": "6.7.2", + "dotenv": "16.4.5", + "html-webpack-plugin": "5.6.0", + "openapi-typescript": "6.7.0", + "postcss": "8.4.31", + "postcss-loader": "7.3.3", + "style-loader": "3.3.3", + "tailwindcss": "3.4.7", + "ts-loader": "9.5.0", + "typescript": "5.3.3", + "webpack": "5.94.0", + "webpack-cli": "5.1.4", + "webpack-dev-server": "4.15.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.23", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.2.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/scheduler": { + "version": "0.26.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/batch": { + "version": "0.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001733", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.18", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.199", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/express": { + "version": "4.21.2", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript": { + "version": "6.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "fast-glob": "^3.3.1", + "js-yaml": "^4.1.0", + "supports-color": "^9.4.0", + "undici": "^5.23.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.2.0", + "jiti": "^1.18.2", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.2.0", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-loader": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.94.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/frontend/goquery-ui/package.json b/frontend/goquery-ui/package.json new file mode 100644 index 00000000..3d86fe4b --- /dev/null +++ b/frontend/goquery-ui/package.json @@ -0,0 +1,34 @@ +{ + "name": "goquery-ui", + "private": true, + "version": "0.1.0", + "description": "Network observability flow exploration UI (table + graph)", + "scripts": { + "build": "webpack --config webpack.config.js", + "dev": "webpack serve --config webpack.config.js", + "typecheck": "tsc --noEmit", + "generate:types": "openapi-typescript global-query_4.1.17_openapi.yaml --output src/api/generated.ts" + }, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "typescript": "5.3.3", + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", + "webpack": "5.94.0", + "webpack-cli": "5.1.4", + "ts-loader": "9.5.0", + "openapi-typescript": "6.7.0", + "tailwindcss": "3.4.7", + "postcss": "8.4.31", + "autoprefixer": "10.4.16", + "postcss-loader": "7.3.3", + "css-loader": "6.7.2", + "style-loader": "3.3.3", + "dotenv": "16.4.5", + "webpack-dev-server": "4.15.1", + "html-webpack-plugin": "5.6.0" + } +} diff --git a/frontend/goquery-ui/postcss.config.cjs b/frontend/goquery-ui/postcss.config.cjs new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/frontend/goquery-ui/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/goquery-ui/scripts/write-env.sh b/frontend/goquery-ui/scripts/write-env.sh new file mode 100755 index 00000000..75bb1d59 --- /dev/null +++ b/frontend/goquery-ui/scripts/write-env.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -eu + +# Create the directory +mkdir -p /var/run/env + +# Set umask for proper permissions +umask 022 + +# Write the environment JavaScript file +cat > /var/run/env/env.js << 'EOF' +// Generated by docker-compose env-writer +window.__ENV__ = { + GQ_API_BASE_URL: "${GQ_API_BASE_URL}", + HOST_RESOLVER_TYPES: "${HOST_RESOLVER_TYPES}", + SSE_ON_LOAD: "${SSE_ON_LOAD}" +}; +EOF + +# Substitute environment variables +sed -i "s|\${GQ_API_BASE_URL}|${GQ_API_BASE_URL:-/api}|g" /var/run/env/env.js +sed -i "s|\${HOST_RESOLVER_TYPES}|${HOST_RESOLVER_TYPES:-string}|g" /var/run/env/env.js +sed -i "s|\${SSE_ON_LOAD}|${SSE_ON_LOAD:-true}|g" /var/run/env/env.js + +# Set proper file permissions for caddy user (uid:gid 1001:1001) +chown 1001:1001 /var/run/env/env.js +chmod 644 /var/run/env/env.js + +echo "Environment file created successfully at /var/run/env/env.js" diff --git a/frontend/goquery-ui/src/api/client.ts b/frontend/goquery-ui/src/api/client.ts new file mode 100644 index 00000000..96e883f4 --- /dev/null +++ b/frontend/goquery-ui/src/api/client.ts @@ -0,0 +1,587 @@ +import { + ApiError, + FlowRecord, + extractFlows, + QueryParamsUI, + ErrorModelSchema, + SummarySchema, +} from './domain' +import { getApiBaseUrl } from '../env' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore generated after `make types` +import type { components } from './generated' + +// default timeout increased to support global-query across many hosts +const DEFAULT_TIMEOUT_MS = 300_000 + +type Args = components['schemas']['Args'] +type ResultSchema = components['schemas']['Result'] +export interface ClientConfig { + baseUrl: string + timeoutMs?: number +} + +export class GlobalQueryClient { + private baseUrl: string + private timeout: number + + constructor(cfg: ClientConfig) { + this.baseUrl = cfg.baseUrl.replace(/\/$/, '') + this.timeout = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS + } + + private bustUrl(path: string): string { + try { + const url = new URL(path, this.baseUrl) + url.searchParams.set('_ts', String(Date.now())) + return url.toString() + } catch { + // Fallback: naive concat + const sep = path.includes('?') ? '&' : '?' + return `${this.baseUrl.replace(/\/$/, '')}${path}${sep}_ts=${Date.now()}` + } + } + + private async delay(ms: number) { + return new Promise((res) => setTimeout(res, ms)) + } + + private isLikelyConnReset(err: any): boolean { + try { + const name = String(err?.name || '') + const msg = String(err?.message || '') + const bodyMsg = String((err?.body as any)?.message || '') + const s = (msg + ' ' + bodyMsg).toLowerCase() + // heuristics for Chromium/WebKit/Gecko + return ( + name === 'TypeError' || + s.includes('err_connection_reset') || + s.includes('networkerror') || + s.includes('network error') + ) + } catch { + return false + } + } + + // validate a query without running it. Returns void on success (HTTP 204), throws ApiError on failure + async validateQueryUI(params: QueryParamsUI, signal?: AbortSignal): Promise { + const args = buildArgs(params) + const url = this.bustUrl('/_query/validate') + for (let attempt = 0; attempt < 2; attempt++) { + const controller = !signal ? new AbortController() : undefined + const timeout = setTimeout(() => controller?.abort(), this.timeout) + try { + const res = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(args), + signal: signal ?? controller?.signal, + cache: 'no-store', + } as RequestInit) + if (res.status === 204) return + if (!res.ok) { + const body = await safeJson(res) + let problem: ErrorModelSchema | undefined + if ( + body && + typeof body === 'object' && + ('detail' in (body as any) || 'errors' in (body as any)) + ) { + problem = body as ErrorModelSchema + } + throw apiError(res.status, body, problem) + } + // treat as success + return + } catch (e: any) { + if (e?.name === 'AbortError') throw abortError() + if (attempt === 0 && this.isLikelyConnReset(e)) { + await this.delay(150) + continue + } + if (isApiError(e)) throw e + throw unknownError(e) + } finally { + clearTimeout(timeout) + } + } + // should not reach here; both attempts failed with thrown errors above + throw unknownError(new Error('validate failed')) + } + + // run a query from UI params and return flattened flows + async runQueryUI( + params: QueryParamsUI, + signal?: AbortSignal + ): Promise<{ + flows: FlowRecord[] + summary?: SummarySchema + hostsStatuses?: Record + }> { + const args = buildArgs(params) + const url = this.bustUrl('/_query') + for (let attempt = 0; attempt < 2; attempt++) { + const controller = !signal ? new AbortController() : undefined + const timeout = setTimeout(() => controller?.abort(), this.timeout) + try { + const res = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(args), + signal: signal ?? controller?.signal, + cache: 'no-store', + } as RequestInit) + if (!res.ok) { + const body = await safeJson(res) + let problem: ErrorModelSchema | undefined + if ( + body && + typeof body === 'object' && + ('detail' in (body as any) || 'errors' in (body as any)) + ) { + problem = body as ErrorModelSchema + } + throw apiError(res.status, body, problem) + } + const json = (await res.json()) as ResultSchema + const hostsStatuses = (json as any)?.hosts_statuses as + | Record + | undefined + return { + flows: extractFlows(json), + summary: json?.summary as any, + hostsStatuses, + } + } catch (e: any) { + if (e?.name === 'AbortError') throw abortError() + if (attempt === 0 && this.isLikelyConnReset(e)) { + await this.delay(150) + continue + } + if (isApiError(e)) throw e + throw unknownError(e) + } finally { + clearTimeout(timeout) + } + } + // should not reach here; both attempts failed with thrown errors above + throw unknownError(new Error('request failed')) + } + + // stream query results via Server-Sent Events (POST /_query/sse). The server emits named events: + // - "partialResult": data is a Result JSON chunk; call onPartial with extracted flows + // - "finalResult": data is the final Result JSON; call onFinal then close + // - "error": data may include a message/host; forward to onError but do not abort + // Optionally, a "progress" event with {done,total} may be sent. + // Returns a disposer to close the stream. + streamQueryUI( + params: QueryParamsUI, + handlers: { + onPartial?: (flows: FlowRecord[], summary?: SummarySchema) => void + onFinal?: (flows: FlowRecord[], summary?: SummarySchema) => void + onError?: (err: ApiError | { message?: string; [k: string]: unknown }) => void + onProgress?: (p: { done?: number; total?: number }) => void + onMeta?: (meta: { + hostsStatuses?: Record + hostErrorCount?: number + hostOkCount?: number + }) => void + } + ): { close: () => void } { + const args = buildArgs(params) + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), this.timeout) + let closed = false + let readerRef: ReadableStreamDefaultReader | undefined + + const unwrapPayload = (data: any): any => { + if (!data) return data + if (Array.isArray(data)) return { rows: data } + if (typeof data !== 'object') return data + // recursively unwrap common wrapper fields until stable + let cur: any = data + const unwrapOnce = (x: any): any => { + if (!x || typeof x !== 'object') return x + if (Array.isArray(x)) return { rows: x } + if (x.result) return x.result + if (x.partialResult) return x.partialResult + if (x.finalResult) return x.finalResult + return x + } + // limit iterations to avoid infinite loops + for (let i = 0; i < 5; i++) { + const next = unwrapOnce(cur) + if (next === cur) break + cur = next + } + // normalize common shapes to { rows } + if (!cur.rows) { + if (Array.isArray(cur.data)) cur = { ...cur, rows: cur.data } + else if (Array.isArray(cur.flows)) cur = { ...cur, rows: cur.flows } + else if (cur.rows && Array.isArray(cur.rows.data)) cur = { ...cur, rows: cur.rows.data } + } + return cur + } + + const processEvent = (evt: { event?: string; data?: string }) => { + const name = (evt.event || 'message').trim() + const lname = name.toLowerCase() + const raw = evt.data || '' + const data = (() => { + try { + return raw ? JSON.parse(raw) : undefined + } catch { + return undefined + } + })() + if (lname === 'partialresult' || lname === 'partial' || lname === 'message') { + if (!data) return + try { + const payload: any = unwrapPayload(data) + const flows = extractFlows(payload as ResultSchema) + const summary = (data as any)?.summary ?? (payload as any)?.summary + const statuses = (data as any)?.hosts_statuses ?? (payload as any)?.hosts_statuses + if (statuses && typeof statuses === 'object') { + let err = 0, + ok = 0 + for (const k of Object.keys(statuses)) { + const c = String((statuses as any)[k]?.code || '').toLowerCase() + if (c === 'ok') ok++ + else err++ + } + handlers.onMeta?.({ + hostsStatuses: statuses as any, + hostErrorCount: err, + hostOkCount: ok, + }) + } + handlers.onPartial?.(flows, summary as any) + } catch (e) { + handlers.onError?.(unknownError(e)) + } + return + } + if (lname === 'finalresult' || lname === 'final') { + try { + const payload: any = unwrapPayload(data) + const flows = extractFlows(payload as ResultSchema) + const summary = (data as any)?.summary ?? (payload as any)?.summary + const statuses = (data as any)?.hosts_statuses ?? (payload as any)?.hosts_statuses + if (statuses && typeof statuses === 'object') { + let err = 0, + ok = 0 + for (const k of Object.keys(statuses)) { + const c = String((statuses as any)[k]?.code || '').toLowerCase() + if (c === 'ok') ok++ + else err++ + } + handlers.onMeta?.({ + hostsStatuses: statuses as any, + hostErrorCount: err, + hostOkCount: ok, + }) + } + handlers.onFinal?.(flows, summary as any) + } finally { + // caller's close will abort; we also clear timeout here + clearTimeout(timeout) + closed = true + controller.abort() + } + return + } + if (lname === 'progress') { + if (data && typeof data === 'object') handlers.onProgress?.(data as any) + return + } + if (lname === 'error') { + if (data && typeof data === 'object') handlers.onError?.(data as any) + else handlers.onError?.(unknownError(new Error('sse error event'))) + return + } + // heuristic fallback: if payload includes rows, treat as partial; if it signals completion, treat as final + if (data && typeof data === 'object') { + try { + const payload: any = unwrapPayload(data) + const rows = (payload as any)?.rows + const isRowsArray = Array.isArray(rows) + const isFinal = !!((payload as any)?.final || (data as any)?.final) + if (isRowsArray) { + const flows = extractFlows(payload as ResultSchema) + if (isFinal) handlers.onFinal?.(flows, (payload as any)?.summary as any) + else handlers.onPartial?.(flows, (payload as any)?.summary as any) + if (isFinal) { + clearTimeout(timeout) + closed = true + controller.abort() + } + return + } + } catch (e) { + // fall through to ignore + } + } + // ignore other events + } + + // kick off POST fetch that returns text/event-stream + ;(async () => { + let triedReconnect = false + try { + const connect = async () => + await fetch(this.bustUrl('/_query/sse'), { + method: 'POST', + headers: { + accept: 'text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify(args), + signal: controller.signal, + cache: 'no-store', + } as RequestInit) + let res = await connect() + // if server closed connection aggressively, attempt one quick reconnect + if (!res.ok) { + const body = await safeJson(res) + let problem: ErrorModelSchema | undefined + if ( + body && + typeof body === 'object' && + ('detail' in (body as any) || 'errors' in (body as any)) + ) { + problem = body as ErrorModelSchema + } + throw apiError(res.status, body, problem) + } + if (!res.ok) { + const body = await safeJson(res) + let problem: ErrorModelSchema | undefined + if ( + body && + typeof body === 'object' && + ('detail' in (body as any) || 'errors' in (body as any)) + ) { + problem = body as ErrorModelSchema + } + throw apiError(res.status, body, problem) + } + const reader = res.body?.getReader() + if (!reader) throw unknownError(new Error('no response body')) + readerRef = reader + const decoder = new TextDecoder('utf-8') + let buf = '' + while (true) { + const { value, done } = await reader.read() + if (done) break + buf += decoder.decode(value, { stream: true }) + // normalize newlines and parse complete events (\n\n delimiter) + buf = buf.replace(/\r\n/g, '\n') + let idx + while ((idx = buf.indexOf('\n\n')) >= 0) { + const rawEvt = buf.slice(0, idx) + buf = buf.slice(idx + 2) + // parse one event block + const evt: { event?: string; data?: string } = {} + const lines = rawEvt.split('\n') + for (const ln of lines) { + if (!ln) continue + if (ln.startsWith(':')) continue // comment + const m = ln.match(/^(\w+):\s?(.*)$/) + if (!m) continue + const k = m[1] + const v = m[2] + if (k === 'event') evt.event = v + else if (k === 'data') evt.data = (evt.data ? evt.data + '\n' : '') + v + } + processEvent(evt) + if (closed) return + } + } + // stream ended: process any trailing, unterminated event block + if (buf && buf.trim().length > 0) { + const rawEvt = buf.replace(/\r\n/g, '\n') + const evt: { event?: string; data?: string } = {} + const lines = rawEvt.split('\n') + for (const ln of lines) { + if (!ln) continue + if (ln.startsWith(':')) continue + const m = ln.match(/^(\w+):\s?(.*)$/) + if (!m) continue + const k = m[1] + const v = m[2] + if (k === 'event') evt.event = v + else if (k === 'data') evt.data = (evt.data ? evt.data + '\n' : '') + v + } + if (evt.event || evt.data) { + processEvent(evt) + if (closed) return + } + } + } catch (e: any) { + if (!closed && !triedReconnect && this.isLikelyConnReset(e)) { + triedReconnect = true + try { + await this.delay(150) + const res = await fetch(this.bustUrl('/_query/sse'), { + method: 'POST', + headers: { + accept: 'text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify(args), + signal: controller.signal, + cache: 'no-store', + } as RequestInit) + if (!res.ok) { + const body = await safeJson(res) + let problem: ErrorModelSchema | undefined + if ( + body && + typeof body === 'object' && + ('detail' in (body as any) || 'errors' in (body as any)) + ) { + problem = body as ErrorModelSchema + } + throw apiError(res.status, body, problem) + } + const reader = res.body?.getReader() + if (!reader) throw unknownError(new Error('no response body')) + readerRef = reader + // fall-through to normal loop by rethrowing a sentinel is complex; instead, we simply return and let caller re-run stream + // However, to keep current API, if reconnect succeeds we proceed with the same reading loop + const decoder = new TextDecoder('utf-8') + let buf = '' + while (true) { + const { value, done } = await reader.read() + if (done) break + buf += decoder.decode(value, { stream: true }) + buf = buf.replace(/\r\n/g, '\n') + let idx + while ((idx = buf.indexOf('\n\n')) >= 0) { + const rawEvt = buf.slice(0, idx) + buf = buf.slice(idx + 2) + const evt: { event?: string; data?: string } = {} + const lines = rawEvt.split('\n') + for (const ln of lines) { + if (!ln) continue + if (ln.startsWith(':')) continue + const m = ln.match(/^(\w+):\s?(.*)$/) + if (!m) continue + const k = m[1] + const v = m[2] + if (k === 'event') evt.event = v + else if (k === 'data') evt.data = (evt.data ? evt.data + '\n' : '') + v + } + processEvent(evt) + if (closed) return + } + } + return + } catch (re) { + if (!closed) handlers.onError?.(isApiError(re) ? (re as any) : unknownError(re)) + } + } else if (e?.name === 'AbortError') { + if (!closed) handlers.onError?.(abortError()) + } else if (!closed) { + handlers.onError?.(isApiError(e) ? e : unknownError(e)) + } + } finally { + clearTimeout(timeout) + } + })() + + return { + close: () => { + try { + clearTimeout(timeout) + // mark closed first to prevent catch handler from reporting AbortError + closed = true + // proactively cancel the reader to close the stream + try { + readerRef?.cancel() + } catch {} + controller.abort() + } catch {} + }, + } + } +} + +let _defaultClient: GlobalQueryClient | undefined +export function getGlobalQueryClient(): GlobalQueryClient { + if (_defaultClient) return _defaultClient + const base = getApiBaseUrl() + _defaultClient = new GlobalQueryClient({ baseUrl: base }) + return _defaultClient +} + +// allow UI to change backend dynamically +export function setGlobalQueryBaseUrl(baseUrl: string, timeoutMs?: number): GlobalQueryClient { + const base = (baseUrl || '').replace(/\/$/, '') + _defaultClient = new GlobalQueryClient({ + baseUrl: base || getApiBaseUrl(), + timeoutMs, + }) + return _defaultClient +} + +function buildArgs(p: QueryParamsUI): Args { + return { + query: p.query, + query_hosts: p.query_hosts, + ifaces: p.ifaces, + first: p.first, + last: p.last, + condition: p.condition, + in: p.in_only || undefined, + out: p.out_only || undefined, + sum: p.sum || undefined, + num_results: p.limit, + sort_by: p.sort_by, + sort_ascending: p.sort_ascending, + // forward UI-selected hosts resolver to backend schema field + query_hosts_resolver_type: p.hosts_resolver || undefined, + format: 'json', + } +} + +function apiError(status: number, body: unknown, problem?: ErrorModelSchema): ApiError { + return { + name: 'ApiError', + message: `api request failed: status=${status}`, + status, + category: status >= 500 ? 'network' : 'client', + body, + problem, + } +} + +function abortError(): ApiError { + return { + name: 'AbortError', + message: 'request aborted', + category: 'network', + } +} + +function unknownError(err: unknown): ApiError { + return { + name: 'UnknownError', + message: 'unknown error', + category: 'unknown', + body: err, + } +} + +function isApiError(e: any): e is ApiError { + return e && typeof e === 'object' && 'category' in e +} + +async function safeJson(res: Response): Promise { + try { + return await res.json() + } catch { + return undefined + } +} diff --git a/frontend/goquery-ui/src/api/domain.ts b/frontend/goquery-ui/src/api/domain.ts new file mode 100644 index 00000000..7eaefbb6 --- /dev/null +++ b/frontend/goquery-ui/src/api/domain.ts @@ -0,0 +1,99 @@ +// domain.ts - curated UI/domain layer built on top of generated OpenAPI types +// GENERATED SCHEMA TYPES: import from './generated' +// This file contains only stable abstractions used by UI components. + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: generated file may not exist until `make types` runs +import type { components } from './generated' + +// type aliases for clarity +export type ResultSchema = components['schemas']['Result'] +export type RowSchema = components['schemas']['Row'] +export type CountersSchema = components['schemas']['Counters'] +export type AttributesSchema = components['schemas']['Attributes'] +export type LabelsSchema = components['schemas']['Labels'] +export type ErrorModelSchema = components['schemas']['ErrorModel'] +export type SummarySchema = components['schemas']['Summary'] + +// Flattened flow record for table/graph usage. +export interface FlowRecord { + sip: string + dip: string + dport: number | null + proto: number | null + bytes_in: number + bytes_out: number + packets_in: number + packets_out: number + host?: string + host_id?: string + iface?: string + interval_end?: string + bidirectional: boolean + _raw: RowSchema +} + +export function flattenRow(row: RowSchema): FlowRecord { + const a = row.attributes || ({} as AttributesSchema) + const c = row.counters || ({} as CountersSchema) + const l = row.labels || ({} as LabelsSchema) + const bytes_in = (c as any).br ?? 0 + const bytes_out = (c as any).bs ?? 0 + const packets_in = (c as any).pr ?? 0 + const packets_out = (c as any).ps ?? 0 + return { + sip: (a as any).sip || '', + dip: (a as any).dip || '', + dport: (a as any).dport ?? null, + proto: (a as any).proto ?? null, + bytes_in, + bytes_out, + packets_in, + packets_out, + host: (l as any).host, + host_id: (l as any).host_id, + iface: (l as any).iface, + interval_end: (l as any).timestamp, + bidirectional: bytes_in > 0 && bytes_out > 0 && packets_in > 0 && packets_out > 0, + _raw: row, + } +} + +export function extractFlows(result: ResultSchema | undefined | null): FlowRecord[] { + if (!result?.rows) return [] + const rows: RowSchema[] = (result.rows ?? []) as unknown as RowSchema[] + return rows.map((r: RowSchema) => flattenRow(r)) +} + +export interface QueryParamsUI { + first: string + last: string + ifaces: string + query: string + // free text hosts query (separate from generic attribute list in `query`) + query_hosts?: string + // selected hosts resolver type from Settings; forwarded to backend as query_hosts_resolver_type + hosts_resolver?: string + condition?: string + limit: number + sort_by: 'bytes' | 'packets' + sort_ascending: boolean + in_only?: boolean + out_only?: boolean + sum?: boolean +} + +export interface PagedLike { + data: T[] + total?: number + displayed?: number +} + +export type ApiErrorCategory = 'network' | 'client' | 'parse' | 'unknown' + +export interface ApiError extends Error { + status?: number + category: ApiErrorCategory + body?: unknown + problem?: ErrorModelSchema +} diff --git a/frontend/goquery-ui/src/api/generated.ts b/frontend/goquery-ui/src/api/generated.ts new file mode 100644 index 00000000..bc4722a2 --- /dev/null +++ b/frontend/goquery-ui/src/api/generated.ts @@ -0,0 +1,956 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +/** OneOf type helpers */ +type Without = { [P in Exclude]?: never } +type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U +type OneOf = T extends [infer Only] + ? Only + : T extends [infer A, infer B, ...infer Rest] + ? OneOf<[XOR, ...Rest]> + : never + +export interface paths { + '/-/health': { + /** + * Get application health + * @description Get info whether the application is running. + */ + get: operations['get-health'] + } + '/-/info': { + /** + * Get application info + * @description Get runtime information about the application. + */ + get: operations['get-info'] + } + '/-/ready': { + /** + * Get application readiness + * @description Get info whether the application is ready. + */ + get: operations['get-ready'] + } + '/_query': { + /** + * Run query + * @description Runs a query based on the parameters provided in the body + */ + post: operations['query-post-run'] + } + '/_query/sse': { + /** + * Run query with server sent events (SSE) + * @description Runs a query based on the parameters provided in the body. Pushes back partial results via SSE + */ + post: operations['query-post-run-sse'] + } + '/_query/validate': { + /** + * Validate query parameters + * @description Validates query parameters (1) for integrity (2) attempting to prepare a query statement from them + */ + get: operations['query-get-validate'] + /** + * Validate query parameters + * @description Validates query parameters (1) for integrity (2) attempting to prepare a query statement from them + */ + post: operations['query-post-validate'] + } +} + +export type webhooks = Record + +export interface components { + schemas: { + Args: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/Args.json + */ + $schema?: string + /** + * @description Caller stores who produced the arguments + * @example goQuery + */ + caller?: string + /** + * @description Condition to filter data by + * @example port=80 & proto=TCP + */ + condition?: string + /** @description Configures DNS resolution of sip,dip results */ + dns_resolution?: components['schemas']['DNSResolution'] + /** + * @description The first timestamp to query + * @example 2020-08-12T09:47:00+02:00 + */ + first?: string + /** + * @description Output format + * @example json + * @enum {string} + */ + format?: 'json' | 'txt' | 'csv' + /** + * Format: int64 + * @description Host ID from which data is queried + * @example 123456 + */ + host_id?: number + /** + * @description Hostname from which data is queried + * @example hostA + */ + hostname?: string + /** + * @description Interfaces to query, can also be a regexp if wrapped into forward slashes '/eth[0-3]/' + * @example eth0,eth1 + */ + ifaces: string + /** + * @description Only show incoming packets/bytes + * @example false + */ + in?: boolean + /** + * Format: int64 + * @description Keepalive message interval (duration) for query + * @example 2000000000 + */ + keepalive?: number + /** + * @description The last timestamp to query + * @example -24h + */ + last?: string + /** + * @description Live can be used to request live flow data (in addition to DB results) + * @example false + */ + live?: boolean + /** + * @description Use less memory for query processing + * @example false + */ + low_mem?: boolean + /** + * Format: int64 + * @description Maximum percentage of available host memory to use for query processing + * @default 60 + * @example 80 + */ + max_mem_pct?: number + /** + * Format: int64 + * @description Number of results to return/print + * @default 1000 + * @example 25 + */ + num_results?: number + /** + * @description Only show outgoing packets/bytes + * @example false + */ + out?: boolean + /** + * @description Query type / Attributes to aggregate by + * @example sip,dip,dport,proto + */ + query: string + /** + * @description Hosts for which data is queried + * @example hostA,hostB,hostC + */ + query_hosts?: string + /** + * @description Resolver type for hosts queries + * @example string + */ + query_hosts_resolver_type?: string + /** + * @description Sort ascending instead of descending + * @example false + */ + sort_ascending?: boolean + /** + * @description Colum to sort by + * @default bytes + * @example packets + * @enum {string} + */ + sort_by?: 'bytes' | 'packets' + /** + * @description Show sum of incoming/outgoing packets/bytes + * @example false + */ + sum?: boolean + } + Attributes: { + /** + * Format: ipv4 + * @description Destination IP + * @example 8.8.8.8 + */ + dip?: string + /** + * Format: int32 + * @description Destination port + * @example 80 + */ + dport?: number + /** + * Format: int32 + * @description IP protocol number + * @example 6 + */ + proto?: number + /** + * Format: ipv4 + * @description Source IP + * @example 10.81.45.1 + */ + sip?: string + } + Counters: { + /** + * Format: int64 + * @description Bytes received + * @example 1024 + */ + br?: number + /** + * Format: int64 + * @description Bytes sent + * @example 512 + */ + bs?: number + /** + * Format: int64 + * @description Packets received + * @example 2 + */ + pr?: number + /** + * Format: int64 + * @description Packets sent + * @example 1 + */ + ps?: number + } + DNSResolution: { + /** + * @description Enable reverse DNS lookups + * @example false + */ + enabled: boolean + /** + * Format: int64 + * @description Maximum number of rows to resolve + * @example 25 + */ + max_rows?: number + /** + * Format: int64 + * @description Timeout for reverse DNS lookups + * @default 1000000000 + * @example 2000000000 + */ + timeout?: number + } | null + DetailError: { + /** + * @description A human-readable explanation specific to this occurrence of the problem. + * @example Property foo is required but is missing. + */ + detail?: string + /** @description Optional list of individual error details */ + errors?: components['schemas']['ErrorDetail'][] | null + /** + * Format: uri + * @description A URI reference that identifies the specific occurrence of the problem. + * @example https://example.com/error-log/abc123 + */ + instance?: string + /** + * Format: int64 + * @description HTTP status code + * @example 400 + */ + status?: number + /** + * @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + * @example Bad Request + */ + title?: string + /** + * Format: uri + * @description A URI reference to human-readable documentation for the error. + * @default about:blank + * @example https://example.com/errors/example + */ + type?: string + } + ErrorDetail: { + /** @description Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' */ + location?: string + /** @description Error message text */ + message?: string + /** @description The value at the given location */ + value?: unknown + } + ErrorModel: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/ErrorModel.json + */ + $schema?: string + /** + * @description A human-readable explanation specific to this occurrence of the problem. + * @example Property foo is required but is missing. + */ + detail?: string + /** @description Optional list of individual error details */ + errors?: components['schemas']['ErrorDetail'][] | null + /** + * Format: uri + * @description A URI reference that identifies the specific occurrence of the problem. + * @example https://example.com/error-log/abc123 + */ + instance?: string + /** + * Format: int64 + * @description HTTP status code + * @example 400 + */ + status?: number + /** + * @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + * @example Bad Request + */ + title?: string + /** + * Format: uri + * @description A URI reference to human-readable documentation for the error. + * @default about:blank + * @example https://example.com/errors/example + */ + type?: string + } + FinalResult: { + /** + * @description Hostname from which the result originated + * @example hostA + */ + hostname?: string + /** @description Statuses of all hosts queried */ + hosts_statuses: { + [key: string]: components['schemas']['Status'] + } + /** @description Query which was run */ + query: components['schemas']['Query'] + /** @description Data rows returned */ + rows: components['schemas']['Row'][] | null + /** @description Status of the result */ + status: components['schemas']['Status'] + /** @description Traffic totals and query statistics */ + summary: components['schemas']['Summary'] + } + GetHealthOutputBody: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/GetHealthOutputBody.json + */ + $schema?: string + /** + * @description Health status of application + * @example healthy + */ + status: string + } + GetInfoOutputBody: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/GetInfoOutputBody.json + */ + $schema?: string + /** + * @description Full git commit SHA + * @example 824f58479a8f326cb350085b3a0e287645e11bc1 + */ + commit?: string + /** + * @description Service name + * @example global-query + */ + name: string + /** @description Name of kubernetes pod (if available) */ + pod?: string + /** + * @description Service (semantic) version + * @example 4.0.0-824f5847 + */ + version: string + } + GetReadyOutputBody: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/GetReadyOutputBody.json + */ + $schema?: string + /** + * @description Ready status of application + * @example ready + */ + status: string + } + Hits: { + /** + * Format: int64 + * @description Number of flow records in Rows displayed/returned + * @example 25 + */ + displayed: number + /** + * Format: int64 + * @description Total number of flow records matching the condition + * @example 1034 + */ + total: number + } + Keepalive: Record + Labels: { + /** + * @description Hostname of the host on which the flow was observed + * @example hostA + */ + host?: string + /** + * @description ID of the host on which the flow was observed + * @example 123456 + */ + host_id?: string + /** + * @description Interface on which the flow was observed + * @example eth0 + */ + iface?: string + /** + * Format: date-time + * @description Timestamp (end) of the 5-minute interval storing the flow record + * @example 2024-04-12T03:20:00+02:00 + */ + timestamp?: string + } + PartialResult: { + /** + * @description Hostname from which the result originated + * @example hostA + */ + hostname?: string + /** @description Statuses of all hosts queried */ + hosts_statuses: { + [key: string]: components['schemas']['Status'] + } + /** @description Query which was run */ + query: components['schemas']['Query'] + /** @description Data rows returned */ + rows: components['schemas']['Row'][] | null + /** @description Status of the result */ + status: components['schemas']['Status'] + /** @description Traffic totals and query statistics */ + summary: components['schemas']['Summary'] + } + Query: { + /** + * @description Attributes which were queried + * @example [ + * "sip", + * "dip", + * "dport" + * ] + */ + attributes: string[] | null + /** + * @description Condition which was provided + * @example port=80 && proto=TCP + */ + condition?: string + } + Result: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + * @example https://example.com/schemas/Result.json + */ + $schema?: string + /** + * @description Hostname from which the result originated + * @example hostA + */ + hostname?: string + /** @description Statuses of all hosts queried */ + hosts_statuses: { + [key: string]: components['schemas']['Status'] + } + /** @description Query which was run */ + query: components['schemas']['Query'] + /** @description Data rows returned */ + rows: components['schemas']['Row'][] | null + /** @description Status of the result */ + status: components['schemas']['Status'] + /** @description Traffic totals and query statistics */ + summary: components['schemas']['Summary'] + } + Row: { + /** @description Query attributes by which flows are grouped */ + attributes: components['schemas']['Attributes'] + /** @description Flow counters */ + counters: components['schemas']['Counters'] + /** @description Labels / partitions the row belongs to */ + labels?: components['schemas']['Labels'] + } + Stats: { + /** + * Format: int64 + * @description Blocks which could not be loaded or processed + */ + blocks_corrupted: number + /** + * Format: int64 + * @description Number of blocks loaded from disk + */ + blocks_processed: number + /** + * Format: int64 + * @description Effective block size after decompression + */ + bytes_decompressed: number + /** + * Format: int64 + * @description Bytes loaded from disk + */ + bytes_loaded: number + /** + * Format: int64 + * @description Number of directories processed + */ + directories_processed: number + /** + * Format: int64 + * @description Total number of workloads to be processed + */ + workloads: number + } + Status: { + /** + * @description Status code + * @example empty + * @enum {string} + */ + code: 'empty' | 'error' | 'missing_data' | 'ok' + /** + * @description Optional status description + * @example no results returned + */ + message?: string + } + Summary: { + /** @description Was there any data available to query at all */ + data_available: boolean + /** @description Flow records returned in total and records present in rows */ + hits: components['schemas']['Hits'] + /** + * @description Interfaces which were queried + * @example [ + * "eth0", + * "eth1" + * ] + */ + interfaces: string[] | null + /** @description Stats tracks interactions with the underlying DB data */ + stats?: components['schemas']['Stats'] + /** + * Format: date-time + * @description Start of the queried interval + * @example 2020-08-12T09:47:00+02:00 + */ + time_first: string + /** + * Format: date-time + * @description End of the queried interval + * @example 2024-04-12T09:47:00+02:00 + */ + time_last: string + /** @description Query runtime fields */ + timings: components['schemas']['Timings'] + /** @description Total traffic volume and packets observed over the queried time range */ + totals: components['schemas']['Counters'] + } + Timings: { + /** + * Format: int64 + * @description Query runtime in nanoseconds + * @example 235000000 + */ + query_duration_ns: number + /** + * Format: date-time + * @description Query start time + */ + query_start: string + /** + * Format: int64 + * @description DNS resolution time for all IPs in nanoseconds + * @example 515000000 + */ + resolution?: number + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} + +export type $defs = Record + +export type external = Record + +export interface operations { + /** + * Get application health + * @description Get info whether the application is running. + */ + 'get-health': { + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': components['schemas']['GetHealthOutputBody'] + } + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Get application info + * @description Get runtime information about the application. + */ + 'get-info': { + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': components['schemas']['GetInfoOutputBody'] + } + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Get application readiness + * @description Get info whether the application is ready. + */ + 'get-ready': { + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': components['schemas']['GetReadyOutputBody'] + } + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Run query + * @description Runs a query based on the parameters provided in the body + */ + 'query-post-run': { + requestBody?: { + content: { + 'application/json': components['schemas']['Args'] + } + } + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': components['schemas']['Result'] + } + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Run query with server sent events (SSE) + * @description Runs a query based on the parameters provided in the body. Pushes back partial results via SSE + */ + 'query-post-run-sse': { + requestBody?: { + content: { + 'application/json': components['schemas']['Args'] + } + } + responses: { + /** @description OK */ + 200: { + content: { + 'text/event-stream': OneOf< + [ + { + data: components['schemas']['FinalResult'] + /** + * @description The event name. + * @constant + */ + event: 'finalResult' + /** @description The event ID. */ + id?: number + /** @description The retry time in milliseconds. */ + retry?: number + }, + { + data: components['schemas']['Keepalive'] + /** + * @description The event name. + * @constant + */ + event: 'keepalive' + /** @description The event ID. */ + id?: number + /** @description The retry time in milliseconds. */ + retry?: number + }, + { + data: components['schemas']['PartialResult'] + /** + * @description The event name. + * @constant + */ + event: 'partialResult' + /** @description The event ID. */ + id?: number + /** @description The retry time in milliseconds. */ + retry?: number + }, + { + data: components['schemas']['DetailError'] + /** + * @description The event name. + * @constant + */ + event: 'queryError' + /** @description The event ID. */ + id?: number + /** @description The retry time in milliseconds. */ + retry?: number + }, + ] + >[] + } + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Validate query parameters + * @description Validates query parameters (1) for integrity (2) attempting to prepare a query statement from them + */ + 'query-get-validate': { + parameters: { + query?: { + /** + * @description Query type / Attributes to aggregate by + * @example sip,dip,dport,proto + */ + query?: string + /** + * @description Interfaces to query, can also be a regexp if wrapped into forward slashes '/eth[0-3]/' + * @example eth0,eth1 + */ + ifaces?: string + /** + * @description Hosts for which data is queried + * @example hostA,hostB,hostC + */ + query_hosts?: string + /** + * @description Resolver type for hosts queries + * @example string + */ + hosts_resolver?: string + /** + * @description Hostname from which data is queried + * @example hostA + */ + hostname?: string + /** + * @description Host ID from which data is queried + * @example 123456 + */ + host_id?: number + /** + * @description Condition to filter data by + * @example port=80 & proto=TCP + */ + condition?: string + /** + * @description Only show incoming packets/bytes + * @example false + */ + in?: boolean + /** + * @description Only show outgoing packets/bytes + * @example false + */ + out?: boolean + /** + * @description Show sum of incoming/outgoing packets/bytes + * @example false + */ + sum?: boolean + /** + * @description The first timestamp to query + * @example 2020-08-12T09:47:00+02:00 + */ + first?: string + /** + * @description The last timestamp to query + * @example -24h + */ + last?: string + /** + * @description Output format + * @example json + */ + format?: 'json' | 'txt' | 'csv' + /** + * @description Colum to sort by + * @example packets + */ + sort_by?: 'bytes' | 'packets' + /** + * @description Number of results to return/print + * @example 25 + */ + num_results?: number + /** + * @description Sort ascending instead of descending + * @example false + */ + sort_ascending?: boolean + /** + * @description Maximum percentage of available host memory to use for query processing + * @example 80 + */ + max_mem_pct?: number + /** + * @description Use less memory for query processing + * @example false + */ + low_mem?: boolean + /** + * @description Keepalive message interval (duration) for query + * @example 2000000000 + */ + keepalive?: number + /** + * @description Caller stores who produced the arguments + * @example goQuery + */ + caller?: string + /** + * @description Live can be used to request live flow data (in addition to DB results) + * @example false + */ + live?: boolean + /** + * @description Enable reverse DNS lookups + * @example false + */ + dns_enabled?: boolean + /** + * @description Timeout for reverse DNS lookups + * @example 2000000000 + */ + dns_timeout?: number + /** + * @description Maximum number of rows to resolve + * @example 25 + */ + dns_max_rows?: number + } + } + responses: { + /** @description No Content */ + 204: { + content: never + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } + /** + * Validate query parameters + * @description Validates query parameters (1) for integrity (2) attempting to prepare a query statement from them + */ + 'query-post-validate': { + requestBody?: { + content: { + 'application/json': components['schemas']['Args'] + } + } + responses: { + /** @description No Content */ + 204: { + content: never + } + /** @description Error */ + default: { + content: { + 'application/problem+json': components['schemas']['ErrorModel'] + } + } + } + } +} diff --git a/frontend/goquery-ui/src/api/proto-map.json b/frontend/goquery-ui/src/api/proto-map.json new file mode 100644 index 00000000..3e6e05d7 --- /dev/null +++ b/frontend/goquery-ui/src/api/proto-map.json @@ -0,0 +1,431 @@ +{ + "0": { + "text": "HOPOPT" + }, + "1": { + "text": "ICMP" + }, + "2": { + "text": "IGMP" + }, + "3": { + "text": "GGP" + }, + "4": { + "text": "IP" + }, + "5": { + "text": "Stream" + }, + "6": { + "text": "TCP" + }, + "7": { + "text": "CBT" + }, + "8": { + "text": "EGP " + }, + "9": { + "text": "IGP" + }, + "10": { + "text": "BBN-RCC-MON" + }, + "11": { + "text": "NVP-II" + }, + "12": { + "text": "PUP" + }, + "13": { + "text": "ARGUS" + }, + "14": { + "text": "EMCON" + }, + "15": { + "text": "XNET" + }, + "16": { + "text": "CHAOS" + }, + "17": { + "text": "UDP" + }, + "18": { + "text": "Multiplexing" + }, + "19": { + "text": "DCN-MEAS (DCN Measurement Subsystems)" + }, + "20": { + "text": "HMP" + }, + "21": { + "text": "PRM" + }, + "22": { + "text": "XNS-IDP" + }, + "23": { + "text": "TRUNK-1" + }, + "24": { + "text": "TRUNK-2" + }, + "25": { + "text": "LEAF-1" + }, + "26": { + "text": "LEAF-2" + }, + "27": { + "text": "RDP" + }, + "28": { + "text": "IRTP" + }, + "29": { + "text": "ISO-TP4" + }, + "30": { + "text": "NETBLT" + }, + "31": { + "text": "MFE-NSP" + }, + "32": { + "text": "MERIT-INP" + }, + "33": { + "text": "DCCP" + }, + "34": { + "text": "3PC" + }, + "35": { + "text": "IDPR" + }, + "36": { + "text": "XTP" + }, + "37": { + "text": "DDP" + }, + "38": { + "text": "IDPR-CMTP" + }, + "39": { + "text": "TP++" + }, + "40": { + "text": "IL" + }, + "41": { + "text": "6in4 Verkapselung von IPv6- in IPv4-Pakete" + }, + "42": { + "text": "SDRP" + }, + "43": { + "text": "IPv6-Route" + }, + "44": { + "text": "IPv6-Frag" + }, + "45": { + "text": "IDRP" + }, + "46": { + "text": "RSVP" + }, + "47": { + "text": "GRE" + }, + "48": { + "text": "MHRP" + }, + "49": { + "text": "BNA" + }, + "50": { + "text": "ESP" + }, + "51": { + "text": "AH" + }, + "52": { + "text": "I-NLSP" + }, + "53": { + "text": "SWIPE" + }, + "54": { + "text": "NARP" + }, + "55": { + "text": "MOBILE" + }, + "56": { + "text": "TLSP" + }, + "57": { + "text": "SKIP" + }, + "58": { + "text": "IPv6-ICMP" + }, + "59": { + "text": "IPv6-NoNxt" + }, + "60": { + "text": "IPv6-Opts" + }, + "61": { + "text": "Jedes Host-interne Protokoll" + }, + "62": { + "text": "CFTP" + }, + "63": { + "text": "Jedes lokale Netz" + }, + "64": { + "text": "SAT-EXPAK" + }, + "65": { + "text": "KRYPTOLAN" + }, + "66": { + "text": "RVD" + }, + "67": { + "text": "IPPC" + }, + "68": { + "text": "Jedes verteilte Dateisystem" + }, + "69": { + "text": "SAT-MON" + }, + "70": { + "text": "VISA" + }, + "71": { + "text": "IPCV" + }, + "72": { + "text": "CPNX" + }, + "73": { + "text": "CPHB" + }, + "74": { + "text": "WSN" + }, + "75": { + "text": "PVP" + }, + "76": { + "text": "BR-SAT-MON" + }, + "77": { + "text": "SUN-ND" + }, + "78": { + "text": "WB-MON" + }, + "79": { + "text": "WB-EXPAK" + }, + "80": { + "text": "ISO-IP" + }, + "81": { + "text": "VMTP" + }, + "82": { + "text": "SECURE-VMTP" + }, + "83": { + "text": "VINES" + }, + "84": { + "text": "TTP" + }, + "85": { + "text": "NSFNET-IGP" + }, + "86": { + "text": "DGP" + }, + "87": { + "text": "TCF" + }, + "88": { + "text": "EIGRP" + }, + "89": { + "text": "OSPF" + }, + "90": { + "text": "Sprite-RPC" + }, + "91": { + "text": "LARP" + }, + "92": { + "text": "MTP" + }, + "93": { + "text": "AX.25" + }, + "94": { + "text": "IPIP" + }, + "95": { + "text": "MICP" + }, + "96": { + "text": "SCC-SP" + }, + "97": { + "text": "ETHERIP" + }, + "98": { + "text": "ENCAP" + }, + "99": { + "text": "Jeder private Verschlüsselungsentwurf" + }, + "100": { + "text": "GMTP" + }, + "101": { + "text": "IFMP" + }, + "102": { + "text": "PNNI" + }, + "103": { + "text": "PIM" + }, + "104": { + "text": "ARIS" + }, + "105": { + "text": "SCPS" + }, + "106": { + "text": "QNX" + }, + "107": { + "text": "A/N" + }, + "108": { + "text": "IPComp" + }, + "109": { + "text": "SNP" + }, + "110": { + "text": "Compaq-Peer" + }, + "111": { + "text": "IPX-in-IP" + }, + "112": { + "text": "VRRP" + }, + "113": { + "text": "PGM" + }, + "114": { + "text": "any 0-hop protocol" + }, + "115": { + "text": "L2TP" + }, + "116": { + "text": "DDX" + }, + "117": { + "text": "IATP" + }, + "118": { + "text": "STP" + }, + "119": { + "text": "SRP" + }, + "120": { + "text": "UTI" + }, + "121": { + "text": "SMP" + }, + "122": { + "text": "SM" + }, + "123": { + "text": "PTP" + }, + "124": { + "text": "ISIS over IPv4" + }, + "125": { + "text": "FIRE" + }, + "126": { + "text": "CRTP" + }, + "127": { + "text": "CRUDP" + }, + "128": { + "text": "SSCOPMCE" + }, + "129": { + "text": "IPLT" + }, + "130": { + "text": "SPS" + }, + "131": { + "text": "PIPE" + }, + "132": { + "text": "SCTP" + }, + "133": { + "text": "FC" + }, + "134": { + "text": "RSVP-E2E-IGNORE" + }, + "135": { + "text": "Mobility Header" + }, + "136": { + "text": "UDPLite" + }, + "137": { + "text": "MPLS-in-IP" + }, + "138": { + "text": "manet" + }, + "139": { + "text": "HIP" + }, + "140": { + "text": "Shim6" + }, + "141": { + "text": "WESP" + }, + "142": { + "text": "ROHC" + } +} diff --git a/frontend/goquery-ui/src/components/App.tsx b/frontend/goquery-ui/src/components/App.tsx new file mode 100644 index 00000000..b5f4ee2c --- /dev/null +++ b/frontend/goquery-ui/src/components/App.tsx @@ -0,0 +1,2060 @@ +import React, { useEffect, useState, useCallback, useRef } from 'react' +import { getGlobalQueryClient, setGlobalQueryBaseUrl } from '../api/client' +import { FlowRecord, QueryParamsUI, SummarySchema } from '../api/domain' +import { parseParams, serializeParams } from '../state/queryState' +import { TableView } from '../views/TableView' +import { GraphView } from '../views/GraphView' +import { IpDetailsPanel } from '../views/IpDetailsPanel' +import { IfaceDetailsPanel } from '../views/IfaceDetailsPanel' +import { HostDetailsPanel } from '../views/HostDetailsPanel' +import { TemporalDetailsPanel } from '../views/TemporalDetailsPanel' +import { AttributesSelect, parseAttributeQuery, buildAttributeQuery } from './AttributesSelect' +import { buildTextTable } from '../views/exportText' +import { env } from '../env' +import { formatDurationNs, humanBytes, humanPackets } from '../utils/format' +import { DisplaySummary } from './DisplaySummary' + +interface ErrorBannerProps { + error: unknown +} +function ErrorBanner({ error }: ErrorBannerProps) { + if (!error) return null + const [open, setOpen] = React.useState(false) + let simple = '' + let problem: any | undefined + if (typeof error === 'string') simple = error + else if (error && typeof error === 'object') { + const e: any = error + simple = e.message || 'error' + if (e.problem) problem = e.problem + } + return ( +
+
+
{simple}
+ {problem && ( + + )} +
+ {problem && open && ( +
+ {problem.detail &&
{problem.detail}
} + {Array.isArray(problem.errors) && problem.errors.length > 0 && ( +
    + {problem.errors.map((er: any, i: number) => ( +
  • +
    + {er.location || '(unknown)'}:{' '} + {er.message || 'validation error'} +
    + {er.value !== undefined && + (typeof er.value === 'object' && er.value !== null ? ( +
    +                        {JSON.stringify(er.value, null, 2)}
    +                      
    + ) : ( +
    value: {formatValue(er.value)}
    + ))} +
  • + ))} +
+ )} +
+ )} +
+ ) +} + +function formatValue(v: unknown): string { + if (v === null) return 'null' + if (v === undefined) return 'undefined' + if (typeof v === 'string') return JSON.stringify(v) + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + try { + return JSON.stringify(v) + } catch { + return '[unserializable]' + } +} + +// normalize strings for error matching: lower-case and unify curly apostrophes +function normalizeText(s: string | undefined | null): string { + if (!s) return '' + return String(s) + .toLowerCase() + .replace(/\u2019/g, "'") + .trim() +} + +function formatTimestamp(ts: string | undefined): string { + if (!ts) return '—' + try { + const d = new Date(ts) + if (isNaN(d.getTime())) return ts + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + } catch { + return ts + } +} + +function humanRangeDuration(startIso?: string | null, endIso?: string | null): string { + if (!startIso || !endIso) return '' + const start = new Date(startIso).getTime() + const end = new Date(endIso).getTime() + if (!isFinite(start) || !isFinite(end)) return '' + let ms = Math.max(0, end - start) + const dayMs = 24 * 60 * 60 * 1000 + const hourMs = 60 * 60 * 1000 + const minMs = 60 * 1000 + const secMs = 1000 + const d = Math.floor(ms / dayMs) + ms -= d * dayMs + const h = Math.floor(ms / hourMs) + ms -= h * hourMs + const m = Math.floor(ms / minMs) + ms -= m * minMs + const s = Math.floor(ms / secMs) + const parts: string[] = [] + if (d > 0) parts.push(d + 'd') + if (h > 0 || d > 0) parts.push(h + 'h') + if (m > 0 || (d === 0 && h === 0)) parts.push(m + 'm') + // Only show seconds if duration < 1m + if (d === 0 && h === 0 && m === 0) parts.push(s + 's') + return parts.join('') +} + +function isoToLocalInput(iso?: string | null): string { + if (!iso) return '' + try { + const d = new Date(iso) + if (isNaN(d.getTime())) return '' + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` + } catch { + return '' + } +} + +function localInputToIso(val: string): string | undefined { + if (!val) return undefined + const d = new Date(val) + if (isNaN(d.getTime())) return undefined + return d.toISOString() +} + +// sanitize comma-separated host list: trim items and drop empties +function sanitizeHostList(raw?: string | null): string | undefined { + if (!raw) return undefined + const items = String(raw) + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + return items.length ? items.join(',') : undefined +} + +interface SummaryStatProps { + label: string + value: React.ReactNode + multiline?: boolean +} +function SummaryStat({ label, value, multiline }: SummaryStatProps) { + const isSimple = typeof value === 'string' || typeof value === 'number' + const valueClass = multiline + ? 'text-[13px] font-medium text-gray-100 leading-tight break-words' + : 'truncate text-[13px] font-medium text-gray-100' + return ( +
+
{label}
+
+ {value} +
+
+ ) +} + +const DEFAULT_FIRST_MINUTES = 10 +const DEFAULTS: QueryParamsUI = { + first: '', + last: '', + ifaces: '', + query: '', + condition: undefined as any, + limit: 200, + sort_by: 'bytes', + sort_ascending: false, +} + +// Ensure any externally loaded params (e.g., from localStorage) are valid +function sanitizeUIParams(p: any): QueryParamsUI { + const merged: QueryParamsUI = { + ...DEFAULTS, + ...(p || {}), + } + merged.sort_by = merged.sort_by === 'packets' ? 'packets' : 'bytes' + merged.sort_ascending = !!merged.sort_ascending + merged.limit = Math.max(1, Number(merged.limit) || DEFAULTS.limit) + merged.first = typeof merged.first === 'string' ? merged.first : DEFAULTS.first + merged.last = typeof merged.last === 'string' ? merged.last : DEFAULTS.last + merged.ifaces = typeof merged.ifaces === 'string' ? merged.ifaces : '' + merged.query = typeof merged.query === 'string' ? merged.query : '' + return merged +} + +function buildInitialParams(): QueryParamsUI { + const parsed = parseParams(window.location.search) + const lastDate = new Date() + const firstDate = new Date(lastDate.getTime() - DEFAULT_FIRST_MINUTES * 60 * 1000) + // normalize attributes selection: if empty/All, store explicit list to satisfy backend min-length + const attr = parseAttributeQuery(parsed.query) + const normalizedQuery = buildAttributeQuery(attr.values, attr.all) + return { + ...DEFAULTS, + ...parsed, + query_hosts: sanitizeHostList(parsed.query_hosts), + query: normalizedQuery, + first: parsed.first || firstDate.toISOString(), + last: parsed.last || lastDate.toISOString(), + } +} + +const tabs = [ + { id: 'table', label: 'Table' }, + { id: 'graph', label: 'Graph' }, +] as const + +type TabId = (typeof tabs)[number]['id'] + +export default function App() { + const [params, setParams] = useState(() => buildInitialParams()) + const [activeTab, setActiveTab] = useState('table') + const [rows, setRows] = useState([]) + const [summary, setSummary] = useState(undefined) + const [ipDetail, setIpDetail] = useState<{ + ip: string + rows: FlowRecord[] + loading: boolean + error?: unknown + summary?: SummarySchema + } | null>(null) + const [ifaceDetail, setIfaceDetail] = useState<{ + host: string + iface: string + rows: FlowRecord[] + loading: boolean + error?: unknown + summary?: SummarySchema + } | null>(null) + const [hostDetail, setHostDetail] = useState<{ + hostId: string + hostName: string + rows: FlowRecord[] + loading: boolean + error?: unknown + summary?: SummarySchema + } | null>(null) + const [temporalDetail, setTemporalDetail] = useState<{ + meta: { + host: string + iface: string + sip: string + dip: string + dport?: number | null + proto?: number | null + } + attrsShown: string[] + rows: FlowRecord[] + loading: boolean + error?: unknown + summary?: SummarySchema + } | null>(null) + const [loading, setLoading] = useState(false) + const [streaming, setStreaming] = useState(false) + const [error, setError] = useState('') + const [fieldErrors, setFieldErrors] = useState>({}) + const [streamErrors, setStreamErrors] = useState>([]) + const [progress, setProgress] = useState<{ done?: number; total?: number }>({}) + const [hostsStatuses, setHostsStatuses] = useState< + Record + >({}) + const [hostErrorCount, setHostErrorCount] = useState(0) + const [hostOkCount, setHostOkCount] = useState(0) + const [ifaceDetailOpen, setIfaceDetailOpen] = useState(false) + const [copiedToast, setCopiedToast] = useState(false) + const streamCloserRef = useRef<{ close: () => void } | null>(null) + // settings state + const defaultBackend = env.GQ_API_BASE_URL || 'http://localhost:8145' + const LS_BACKEND_KEY = 'goquery_ui_backend_url' + const LS_STREAMING_KEY = 'goquery_ui_use_streaming' + const LS_HOSTS_RESOLVER_KEY = 'goquery_ui_hosts_resolver' + const [backendUrl, setBackendUrl] = useState(() => { + try { + const saved = localStorage.getItem(LS_BACKEND_KEY) + return saved || defaultBackend + } catch { + return defaultBackend + } + }) + const [useStreaming, setUseStreaming] = useState(() => { + try { + const saved = localStorage.getItem(LS_STREAMING_KEY) + if (saved === '1' || saved === 'true') return true + if (saved === '0' || saved === 'false') return false + } catch {} + // fallback to runtime env default + return !!env.SSE_ON_LOAD + }) + // Hosts Resolver selection; prefer a saved value if valid, else first available option + const [hostsResolver, setHostsResolver] = useState(() => { + const opts = Array.isArray(env.HOST_RESOLVER_TYPES) ? env.HOST_RESOLVER_TYPES : [] + try { + const saved = localStorage.getItem(LS_HOSTS_RESOLVER_KEY) + if (typeof saved === 'string' && saved.length > 0 && opts.includes(saved)) return saved + } catch {} + return opts[0] || '' + }) + const [settingsOpen, setSettingsOpen] = useState(false) + + // persist backend selection and apply to client on change + useEffect(() => { + try { + localStorage.setItem(LS_BACKEND_KEY, backendUrl) + } catch {} + setGlobalQueryBaseUrl(backendUrl) + }, [backendUrl]) + + // persist streaming preference + useEffect(() => { + try { + localStorage.setItem(LS_STREAMING_KEY, useStreaming ? '1' : '0') + } catch {} + }, [useStreaming]) + + // persist hosts resolver selection + useEffect(() => { + try { + localStorage.setItem(LS_HOSTS_RESOLVER_KEY, hostsResolver) + } catch {} + }, [hostsResolver]) + + // guard against stale/invalid saved value not present in env options + useEffect(() => { + const opts = Array.isArray(env.HOST_RESOLVER_TYPES) ? env.HOST_RESOLVER_TYPES : [] + if (hostsResolver && !opts.includes(hostsResolver)) { + const fallback = opts[0] || '' + if (fallback !== hostsResolver) setHostsResolver(fallback) + } + }, [hostsResolver]) + + // global Escape closes any details, settings, or interfaces modal + useEffect(() => { + const onEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + if (ifaceDetailOpen) { + setIfaceDetailOpen(false) + e.preventDefault() + return + } + if (ipDetail || ifaceDetail || hostDetail || temporalDetail) { + closeAllDetails() + e.preventDefault() + return + } + if (settingsOpen) { + setSettingsOpen(false) + e.preventDefault() + } + } + } + window.addEventListener('keydown', onEsc) + return () => window.removeEventListener('keydown', onEsc) + }, [ipDetail, ifaceDetail, hostDetail, temporalDetail, settingsOpen, ifaceDetailOpen]) + // interfaces free-text (comma separated). only commit on blur / enter to reduce churn + const [ifacesInput, setIfacesInput] = useState(params.ifaces) + // hosts free text field stored in query_hosts + const [hostsInput, setHostsInput] = useState(params.query_hosts || '') + // condition buffered input (commit only on space or blur) + const [conditionInput, setConditionInput] = useState(params.condition || '') + // attributes multi-select encoded into params.query (empty string => All) + const attrState = parseAttributeQuery(params.query) + function onAttributesChange(next: { values: string[]; all: boolean }) { + const q = buildAttributeQuery(next.values, next.all) + setParams((p) => ({ ...p, query: q })) + } + function closeAllDetails() { + setIpDetail(null) + setIfaceDetail(null) + setHostDetail(null) + setTemporalDetail(null) + } + function commitInterfaces() { + setParams((p) => ({ ...p, ifaces: ifacesInput.trim() })) + } + function commitHosts() { + setParams((p) => ({ ...p, query_hosts: sanitizeHostList(hostsInput) })) + } + function commitCondition(next?: string) { + const raw = next ?? conditionInput + setParams((p) => ({ + ...p, + condition: raw.trim() ? raw : undefined, + })) + // Trigger validation immediately when committing from SPACE/blur + void validateCurrent({ conditionOverride: raw }) + } + + // --- Validation helpers --- + const validateAbortRef = useRef(null) + // Abort controller for non-streaming runs + const runAbortRef = useRef(null) + // Small cooldown to avoid racing a new request onto a just-aborted connection + const lastCancelAtRef = useRef(0) + + // Map API error to field errors and a banner error, mirroring run() behavior + const mapValidationError = useCallback( + (e: any): { fields: Record; banner: any } => { + // Ignore user-initiated aborts: don't surface as banner errors + try { + const name = String((e as any)?.name || '') + const msg = String((e as any)?.message || '') + if (name === 'AbortError' || normalizeText(msg) === 'request aborted') { + return { fields: {}, banner: '' } + } + } catch {} + const fields: Record = {} + let banner: any = e + if ( + e && + typeof e === 'object' && + (e as any).problem && + Array.isArray((e as any).problem.errors) + ) { + const isLoc = (loc: string, key: string) => + loc === `body.${key}` || + loc.startsWith(`body.${key}.`) || + loc.startsWith(`body.${key}[`) || + loc === key + for (const er of (e as any).problem.errors as any[]) { + const locRaw = String(er.location || '') + const loc = locRaw.toLowerCase() + const rawMsg = String(er.message || 'validation error').trim() + const isCondition = isLoc(loc, 'condition') + let msg = isCondition + ? rawMsg.replace( + /^(\s*)([a-z])/, + (_m: string, ws: string, ch: string) => ws + ch.toUpperCase() + ) + : rawMsg.charAt(0).toUpperCase() + rawMsg.slice(1) + if (!isCondition && er.value !== undefined) msg += ` -- value: ${formatValue(er.value)}` + const normRaw = normalizeText(rawMsg) + if ( + normRaw === 'list of target hosts is empty' || + normRaw === "couldn't prepare query: list of target hosts is empty" || + normRaw.includes('list of target hosts is empty') + ) { + fields.hosts = msg + continue + } + const isResolverField = + isLoc(loc, 'query_hosts_resolver_type') || isLoc(loc, 'hosts_resolver') + const isHostsField = + isLoc(loc, 'query_hosts') || isLoc(loc, 'hostname') || isLoc(loc, 'host_id') + if (isLoc(loc, 'ifaces')) fields.ifaces = msg + else if (!isResolverField && isHostsField) fields.hosts = msg + else if (isLoc(loc, 'query') || isLoc(loc, 'attributes')) fields.attributes = msg + else if (isCondition) fields.condition = msg + else if (isLoc(loc, 'first')) fields.first = msg + else if (isLoc(loc, 'last')) fields.last = msg + else if (isLoc(loc, 'num_results')) fields.limit = msg + else if (isLoc(loc, 'sort_by')) fields.sort_by = msg + } + const errs: any[] = (e as any).problem.errors as any[] + const first = errs[0] || {} + const msgText = + String(first?.message || '') + .toLowerCase() + .includes('unexpected property') && first?.location + ? `Unexpected property: ${first.location}` + : 'API request failed: validation failed' + banner = { message: msgText, problem: (e as any).problem, status: (e as any).status } + } else { + // Special-case mapping for non-problem errors + const status = (e as any)?.status + let combined = '' + const prob: any = (e as any)?.problem + if (prob) { + if (typeof prob.detail === 'string') combined += ' ' + prob.detail + if (Array.isArray(prob.errors)) { + combined += ' ' + prob.errors.map((er: any) => String(er?.message || '')).join(' ') + } + } + const body: any = (e as any)?.body + if (typeof body === 'string') combined += ' ' + body + else if (body && typeof body === 'object' && typeof body.message === 'string') + combined += ' ' + body.message + const lc = normalizeText(combined) + if ( + lc.includes("couldn't prepare query: list of target hosts is empty") || + (status === 500 && lc.includes('list of target hosts is empty')) + ) { + fields.hosts = 'List of target hosts is empty' + banner = { message: 'API request failed: validation failed', problem: prob, status } + } + } + return { fields, banner } + }, + [] + ) + + // Build effective params merging uncommitted inputs and normalizing attribute query + const computeFinalParams = useCallback( + (over?: { conditionOverride?: string; hostsOverride?: string }): QueryParamsUI => { + // include any uncommitted condition input + const effectiveParamsBase = + (over?.conditionOverride ?? conditionInput) !== (params.condition || '') + ? { ...params, condition: (over?.conditionOverride ?? conditionInput) || undefined } + : params + // include uncommitted Hosts input (normalized) + const mergedHosts = + (over?.hostsOverride ?? hostsInput) !== (effectiveParamsBase.query_hosts || '') + ? sanitizeHostList(over?.hostsOverride ?? hostsInput) + : effectiveParamsBase.query_hosts + const effectiveParams = + mergedHosts !== effectiveParamsBase.query_hosts + ? { ...effectiveParamsBase, query_hosts: mergedHosts } + : effectiveParamsBase + // normalize attributes query: when 'All', send explicit full list (backend requires min length) + const normalizedQuery = buildAttributeQuery( + parseAttributeQuery(effectiveParams.query).values, + parseAttributeQuery(effectiveParams.query).all + ) + const finalParams = + normalizedQuery === effectiveParams.query + ? effectiveParams + : { ...effectiveParams, query: normalizedQuery } + return finalParams + }, + [params, conditionInput, hostsInput] + ) + + async function validateCurrent(over?: { conditionOverride?: string; hostsOverride?: string }) { + try { + const finalParams = computeFinalParams(over) + // set backend dynamically for the validator as well + setGlobalQueryBaseUrl(backendUrl) + // abort any in-flight validation + try { + validateAbortRef.current?.abort() + } catch {} + const ctrl = new AbortController() + validateAbortRef.current = ctrl + await getGlobalQueryClient().validateQueryUI( + { ...finalParams, hosts_resolver: hostsResolver || undefined }, + ctrl.signal + ) + // success: clear field and banner errors + setFieldErrors({}) + setError('') + return true + } catch (e: any) { + const mapped = mapValidationError(e) + if (Object.keys(mapped.fields).length > 0) setFieldErrors(mapped.fields) + setError(mapped.banner) + return false + } + } + + useEffect(() => { + const search = serializeParams(params) + const url = new URL(window.location.href) + url.search = search + window.history.replaceState({}, '', url.toString()) + }, [params]) + + const run = useCallback(async () => { + // if we just canceled, wait a brief moment to let the transport settle + const sinceCancel = Date.now() - (lastCancelAtRef.current || 0) + if (sinceCancel >= 0 && sinceCancel < 120) { + await new Promise((r) => setTimeout(r, 120 - sinceCancel)) + } + // cancel any previous stream + if (streamCloserRef.current) { + try { + streamCloserRef.current.close() + } catch {} + streamCloserRef.current = null + } + // cancel any previous non-streaming request + if (runAbortRef.current) { + try { + runAbortRef.current.abort() + } catch {} + runAbortRef.current = null + } + setLoading(true) + setStreaming(!!useStreaming) + // don't clear errors yet; wait for validation OK + setStreamErrors([]) + setProgress({}) + setHostsStatuses({}) + setHostErrorCount(0) + setHostOkCount(0) + try { + const finalParams = computeFinalParams() + if (finalParams !== params) { + // sync params state (will also update URL) + setParams(finalParams) + } + // set backend dynamically + setGlobalQueryBaseUrl(backendUrl) + setRows([]) + setSummary(undefined) + // Preflight validate; only proceed when valid + const valid = await validateCurrent() + if (!valid) { + setLoading(false) + setStreaming(false) + return + } + // clear any leftover errors now that validation passed + setError('') + setFieldErrors({}) + if (useStreaming) { + // start SSE stream; server will send partialResult events until finalResult + const closer = getGlobalQueryClient().streamQueryUI( + { ...finalParams, hosts_resolver: hostsResolver || undefined }, + { + onPartial: (flows, sum) => { + // server may emit partial updates with rows=null (no row data yet); only replace when we actually have rows + if (Array.isArray(flows) && flows.length > 0) { + setRows(flows) + } + if (sum) setSummary(sum) + }, + onFinal: (flows, sum) => { + // Always replace with the final, server-sorted result so sort_ascending takes effect + setRows(flows) + if (sum) setSummary(sum) + setStreaming(false) + setLoading(false) + streamCloserRef.current = null + }, + onError: (er: any) => { + const msg = typeof er?.message === 'string' ? er.message : 'stream error' + const host = typeof er?.host === 'string' ? er.host : undefined + setStreamErrors((prev) => [...prev, { message: msg, host }]) + }, + onProgress: (p) => setProgress(p || {}), + onMeta: (meta) => { + if (meta?.hostsStatuses) setHostsStatuses(meta.hostsStatuses) + if (typeof meta?.hostErrorCount === 'number') setHostErrorCount(meta.hostErrorCount) + if (typeof meta?.hostOkCount === 'number') setHostOkCount(meta.hostOkCount) + }, + } + ) + streamCloserRef.current = closer + } else { + // normal non-streaming request to /_query + try { + const ctrl = new AbortController() + runAbortRef.current = ctrl + const data = await getGlobalQueryClient().runQueryUI( + { + ...finalParams, + hosts_resolver: hostsResolver || undefined, + }, + ctrl.signal + ) + setRows(data.flows) + setSummary(data.summary) + if (data.hostsStatuses) { + setHostsStatuses(data.hostsStatuses) + let err = 0, + ok = 0 + for (const k of Object.keys(data.hostsStatuses)) { + const c = String((data.hostsStatuses as any)[k]?.code || '').toLowerCase() + if (c === 'ok') ok++ + else err++ + } + setHostErrorCount(err) + setHostOkCount(ok) + } + } finally { + runAbortRef.current = null + setLoading(false) + setStreaming(false) + } + } + } catch (e: any) { + // extract problem+json field errors to map inputs + if ( + e && + typeof e === 'object' && + (e as any).problem && + Array.isArray((e as any).problem.errors) + ) { + const fe: Record = {} + const isLoc = (loc: string, key: string) => + loc === `body.${key}` || + loc.startsWith(`body.${key}.`) || + loc.startsWith(`body.${key}[`) || + loc === key + for (const er of (e as any).problem.errors as any[]) { + const locRaw = String(er.location || '') + const loc = locRaw.toLowerCase() + const rawMsg = String(er.message || 'validation error').trim() + // keep condition messages formatting (multi-line caret pointers), but capitalize first letter + const isCondition = isLoc(loc, 'condition') + let msg = isCondition + ? rawMsg.replace( + /^(\s*)([a-z])/, + (_m: string, ws: string, ch: string) => ws + ch.toUpperCase() + ) + : rawMsg.charAt(0).toUpperCase() + rawMsg.slice(1) + // append value context only for non-condition fields to avoid duplicating formatted output + if (!isCondition && er.value !== undefined) msg += ` -- value: ${formatValue(er.value)}` + // HACK: if backend returns this specific text, attribute to Hosts Query + const normRaw = normalizeText(rawMsg) + if ( + normRaw === 'list of target hosts is empty' || + normRaw === "couldn't prepare query: list of target hosts is empty" || + normRaw.includes('list of target hosts is empty') + ) { + fe.hosts = msg + continue + } + const isResolverField = + isLoc(loc, 'query_hosts_resolver_type') || isLoc(loc, 'hosts_resolver') + const isHostsField = + isLoc(loc, 'query_hosts') || isLoc(loc, 'hostname') || isLoc(loc, 'host_id') + if (isLoc(loc, 'ifaces')) fe.ifaces = msg + // do not attach resolver field errors to any single input; keep in banner details only + else if (!isResolverField && isHostsField) fe.hosts = msg + else if (isLoc(loc, 'query') || isLoc(loc, 'attributes')) fe.attributes = msg + else if (isCondition) fe.condition = msg + else if (isLoc(loc, 'first')) fe.first = msg + else if (isLoc(loc, 'last')) fe.last = msg + else if (isLoc(loc, 'num_results')) fe.limit = msg + else if (isLoc(loc, 'sort_by')) fe.sort_by = msg + } + setFieldErrors(fe) + // Special-case: unexpected property => friendly message with location + const errs: any[] = (e as any).problem.errors as any[] + const first = errs[0] || {} + const msgText = + String(first?.message || '') + .toLowerCase() + .includes('unexpected property') && first?.location + ? `Unexpected property: ${first.location}` + : 'API request failed: validation failed' + setError({ + message: msgText, + problem: (e as any).problem, + status: (e as any).status, + }) + } + } finally { + // if in streaming mode, final handler turns off; otherwise already cleared + } + }, [params, conditionInput, hostsInput, backendUrl, useStreaming, hostsResolver]) + + // Allow user to cancel an in-flight query (both streaming and non-streaming) + const cancelRun = useCallback(() => { + lastCancelAtRef.current = Date.now() + try { + // stop SSE stream if active + if (streamCloserRef.current) { + streamCloserRef.current.close() + streamCloserRef.current = null + } + } catch {} + try { + // abort fetch for non-streaming + if (runAbortRef.current) { + runAbortRef.current.abort() + runAbortRef.current = null + } + } catch {} + try { + // abort any in-flight validation to avoid surfacing an AbortError banner + if (validateAbortRef.current) { + validateAbortRef.current.abort() + validateAbortRef.current = null + } + } catch {} + // clear transient UI state + setError('') + setFieldErrors({}) + setStreamErrors([]) + setProgress({}) + setHostsStatuses({}) + setHostErrorCount(0) + setHostOkCount(0) + setLoading(false) + setStreaming(false) + }, []) + + // Auto-run only for non-streaming; for SSE, run only when the user presses the Run button + useEffect(() => { + if (!useStreaming) { + void run() + } + // We intentionally exclude `run` and `conditionInput` to avoid validating on every keystroke. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params, backendUrl, hostsResolver, useStreaming]) + + // open temporal details for a specific row (shared by click and keyboard shortcut) + const openTemporalForRow = useCallback( + async (r: FlowRecord) => { + const condParts: string[] = [] + if (r.sip) condParts.push(`sip=${r.sip}`) + if (r.dip) condParts.push(`dip=${r.dip}`) + if (r.dport !== null && r.dport !== undefined) condParts.push(`dport=${r.dport}`) + if (r.proto !== null && r.proto !== undefined) condParts.push(`proto=${r.proto}`) + const condition = condParts.join(' and ') + const hostId = r.host_id || '' + const iface = r.iface || '' + const meta = { + host: r.host || hostId, + iface, + sip: r.sip || '', + dip: r.dip || '', + dport: r.dport, + proto: r.proto, + } + const attrsShown = attrState.all + ? ['sip', 'dip', 'dport', 'proto'] + : attrState.values.map((v) => (v === 'protocol' ? 'proto' : v === 'port' ? 'dport' : v)) + closeAllDetails() + setTemporalDetail({ meta, attrsShown, rows: [], loading: true }) + try { + const detailParams: QueryParamsUI = { + ...params, + query: 'time', + condition: condition || undefined, + query_hosts: hostId || undefined, + ifaces: iface || '', + limit: 100000, + sort_by: 'bytes', + sort_ascending: false, + } + const data = await getGlobalQueryClient().runQueryUI({ + ...detailParams, + hosts_resolver: 'string', + }) + setTemporalDetail({ + meta, + attrsShown, + rows: data.flows, + summary: data.summary, + loading: false, + }) + } catch (e: any) { + setTemporalDetail({ + meta, + attrsShown, + rows: [], + loading: false, + error: e, + }) + } + }, + [params, attrState] + ) + + // Enter opens temporal details for the first row if in table view and no panel is open + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + const anyDetail = ipDetail || ifaceDetail || hostDetail || temporalDetail + if (!anyDetail && activeTab === 'table' && rows.length > 0) { + e.preventDefault() + void openTemporalForRow(rows[0]) + } + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [activeTab, rows, ipDetail, ifaceDetail, hostDetail, temporalDetail, openTemporalForRow]) + + // simple saved views (localStorage) + const [savedViews, setSavedViews] = useState>( + () => { + try { + const raw = JSON.parse(localStorage.getItem('goquery_ui_views') || '[]') + if (!Array.isArray(raw)) return [] + return raw.map((v: any) => ({ + name: String(v?.name ?? ''), + params: sanitizeUIParams(v?.params), + })) + } catch { + return [] + } + } + ) + function persistViews(next: Array<{ name: string; params: QueryParamsUI }>) { + setSavedViews(next) + try { + localStorage.setItem('goquery_ui_views', JSON.stringify(next)) + } catch {} + } + const [saveViewName, setSaveViewName] = useState('') + function onSaveView() { + const name = (saveViewName || '').trim() + if (!name) return + const next = [...savedViews.filter((v) => v.name !== name), { name, params }] + persistViews(next) + setSaveViewName('') + } + function onLoadView(name: string) { + const found = savedViews.find((v) => v.name === name) + if (!found) return + setParams(sanitizeUIParams(found.params)) + } + function exportCSV() { + if (!rows.length) return + const anyHost = rows.some((r) => !!r.host) + const anyIface = rows.some((r) => !!r.iface) + const shown = attrState.all + ? ['sip', 'dip', 'dport', 'proto'] + : attrState.values.map((v) => (v === 'protocol' ? 'proto' : v === 'port' ? 'dport' : v)) + const headers = [ + ...(anyHost ? ['host'] : []), + ...(anyIface ? ['iface'] : []), + ...shown, + 'bytes_in', + 'bytes_out', + 'bytes_total', + 'packets_in', + 'packets_out', + 'packets_total', + ] + const escape = (v: any) => { + if (v === null || v === undefined) return '' + const s = String(v) + if (s.includes(',') || s.includes('"') || s.includes('\n')) + return '"' + s.replace(/"/g, '""') + '"' + return s + } + const lines = [headers.join(',')] + for (const r of rows) { + const values: any[] = [] + if (anyHost) values.push(r.host || '') + if (anyIface) values.push(r.iface || '') + for (const a of shown) values.push((r as any)[a] ?? '') + const bt = (r.bytes_in || 0) + (r.bytes_out || 0) + const pt = (r.packets_in || 0) + (r.packets_out || 0) + values.push(r.bytes_in || 0, r.bytes_out || 0, bt, r.packets_in || 0, r.packets_out || 0, pt) + lines.push(values.map(escape).join(',')) + } + const blob = new Blob([lines.join('\n')], { + type: 'text/csv;charset=utf-8;', + }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'goquery-export.csv' + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + } + + // query for IP details (proto,dport) when opened + const openIpDetails = useCallback( + async (ip: string) => { + // close other panels + setIfaceDetail(null) + setHostDetail(null) + setTemporalDetail(null) + setIpDetail({ ip, rows: [], loading: true }) + try { + // honor existing condition: concatenate with AND + const baseCondRaw = (conditionInput || params.condition || '').trim() + const ipCond = `host=${ip}` + const combinedCond = baseCondRaw ? `(${baseCondRaw}) and (${ipCond})` : ipCond + // Determine all host IDs that have a vertex connected to this IP (as sip or dip) + // We use the current result rows to infer which hosts are relevant for this IP. + const hostIdSet = new Set() + for (const r of rows) { + if ((r.sip === ip || r.dip === ip) && r.host_id) hostIdSet.add(r.host_id) + } + const hostIds = Array.from(hostIdSet) + const detailParams: QueryParamsUI = { + ...params, + query: 'proto,dport', + condition: combinedCond, + // Override hosts scoping for IP details: restrict to connected hosts only + query_hosts: hostIds.length ? hostIds.join(',') : undefined, + limit: Math.max(1, params.limit || 1), + sort_by: 'bytes', + sort_ascending: false, + } + const data = await getGlobalQueryClient().runQueryUI({ + ...detailParams, + hosts_resolver: 'string', + }) + setIpDetail({ + ip, + rows: data.flows, + summary: data.summary, + loading: false, + }) + } catch (e: any) { + setIpDetail({ ip, rows: [], loading: false, error: e }) + } + }, + [params, conditionInput, rows] + ) + + // query for Interface details: attributes iface,port,protocol; scope by host_id and selected iface + const openIfaceDetails = useCallback( + async (hostId: string, iface: string) => { + // close other panels + setIpDetail(null) + setHostDetail(null) + setTemporalDetail(null) + if (!hostId || !iface) { + setIfaceDetail({ + host: hostId || '(unknown)', + iface: iface || '(iface)', + rows: [], + loading: false, + error: 'Missing host or interface for details', + }) + return + } + // resolve human-readable host name from current rows for display + const displayHost = rows.find((r) => r.host_id === hostId)?.host || hostId + setIfaceDetail({ host: displayHost, iface, rows: [], loading: true }) + try { + const detailParams: QueryParamsUI = { + ...params, + // per requirements: attributes = iface,port,protocol; limit scope via host_id and selected interface inputs + query: 'iface,port,protocol', + query_hosts: hostId, + ifaces: iface, + condition: undefined, + limit: Math.max(1, params.limit || 1), + sort_by: 'bytes', + sort_ascending: false, + } + const data = await getGlobalQueryClient().runQueryUI({ + ...detailParams, + hosts_resolver: 'string', + }) + setIfaceDetail({ + host: displayHost, + iface, + rows: data.flows, + summary: data.summary, + loading: false, + }) + } catch (e: any) { + setIfaceDetail({ + host: displayHost, + iface, + rows: [], + loading: false, + error: e, + }) + } + }, + [params, conditionInput, rows] + ) + + // host details: show interfaces grouped, query attributes: ifaces, scoped by host_id + const openHostDetails = useCallback( + async (hostId: string) => { + // close other panels + setIpDetail(null) + setIfaceDetail(null) + setTemporalDetail(null) + if (!hostId) return + const hostName = rows.find((r) => r.host_id === hostId)?.host || hostId + setHostDetail({ hostId, hostName, rows: [], loading: true }) + try { + const detailParams: QueryParamsUI = { + ...params, + query: 'iface', + query_hosts: hostId, + condition: undefined, + limit: Math.max(1, params.limit || 1), + sort_by: 'bytes', + sort_ascending: false, + } + const data = await getGlobalQueryClient().runQueryUI({ + ...detailParams, + hosts_resolver: 'string', + }) + setHostDetail({ + hostId, + hostName, + rows: data.flows, + summary: data.summary, + loading: false, + }) + } catch (e: any) { + setHostDetail({ hostId, hostName, rows: [], loading: false, error: e }) + } + }, + [params, rows] + ) + + function onTimePreset(minutes: number) { + const lastDate = new Date() + const firstDate = new Date(lastDate.getTime() - minutes * 60 * 1000) + setParams((p) => ({ + ...p, + first: firstDate.toISOString(), + last: lastDate.toISOString(), + })) + } + + return ( +
+
+
+
+ Goquery / Network Usage +
+
+
+
+ {/* Query Input Panel */} +
+ +
+ {/* Row 1 */} +
+ {/* Hosts Query */} +
+ + setHostsInput(e.target.value)} + onBlur={commitHosts} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + commitHosts() + } + }} + /> + {fieldErrors.hosts && ( +
{fieldErrors.hosts}
+ )} +
+ {/* Interfaces */} +
+ + setIfacesInput(e.target.value)} + onBlur={commitInterfaces} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + commitInterfaces() + } + }} + /> + {fieldErrors.ifaces && ( +
{fieldErrors.ifaces}
+ )} +
+ {/* Attributes */} +
+ + + {fieldErrors.attributes && ( +
{fieldErrors.attributes}
+ )} +
+ {/* spacer to keep grid alignment */} +
+ {/* (Run button moved below panel) */} +
+ {/* Row 2 - aligned under each primary field */} +
+ {/* Time Range under Hosts */} +
+ +
+ {[5, 10, 30, 60, 360, 720, 1440, 2880, 10080, 43200, 129600, 259200].map((m) => { + let label: string + if (m < 60) label = `${m}m` + else if (m === 60) label = '1h' + else if (m < 1440) label = `${m / 60}h` + else if (m % 1440 === 0 && m / 1440 >= 2) label = `${m / 1440}d` + else label = `${m / 60}h` + return ( + + ) + })} +
+ {/* Manual From/To override presets */} +
+
+ + { + const iso = localInputToIso(e.target.value) + if (iso) setParams((p) => ({ ...p, first: iso })) + }} + /> + {fieldErrors.first && ( +
{fieldErrors.first}
+ )} +
+
+ + { + const iso = localInputToIso(e.target.value) + if (iso) setParams((p) => ({ ...p, last: iso })) + }} + /> + {fieldErrors.last && ( +
{fieldErrors.last}
+ )} +
+
+
+ {/* Sort By under Interfaces */} +
+ + + {fieldErrors.sort_by && ( +
{fieldErrors.sort_by}
+ )} + +
+ {/* Limit under Attributes */} +
+ + + setParams((p) => ({ + ...p, + limit: Math.max(1, Number(e.target.value) || 1), + })) + } + /> + {fieldErrors.limit && ( +
{fieldErrors.limit}
+ )} + + {[10, 25, 50, 100, 250, 500, 1000].map((n) => ( + +
+ {/* Spacer to maintain grid alignment for Run column */} +
+
+ {/* Row 3 - Condition full width */} +
+
+ +