commit 725a72a7734cde1f291a7ee33907c216a3934cca Author: Conner Majic Date: Thu Apr 16 21:59:42 2026 -0600 Initial commit: establish deterministic rod-string solver stack. Set up the C solver core, Node API orchestration, TS GUI workflow, and engineering documentation with cleaned repo hygiene for private Git hosting. Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d32b0f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +**/node_modules/ + +dist/ +**/dist/ +build/ +coverage/ +.vite/ + +solver-c/solver_main +solver-c/solver_fea_main +solver-c/test_solver +*.o +*.out + +.DS_Store +*.log + +.env +.env.* + +.vscode/ +.idea/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6ae4dbb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,69 @@ +# Agent rulebook — Rods Cursor solver + +This file is the **canonical** instruction set for AI agents working in this repository. Read it before changing code or docs. + +## Authoritative docs (read order) + +1. **[Agents/MATH_SPEC.md](Agents/MATH_SPEC.md)** — Equations, discretization, BCs, paper citations (`references/papers/README.md`). **Do not invent math** not grounded there or in a cited calibration test. +2. **[Agents/COMPUTE_PLAN.md](Agents/COMPUTE_PLAN.md)** — Living handoff: repo map, current gaps vs `MATH_SPEC.md`, deferred work, jump table. +3. **[README.md](README.md)** — Human/agent quickstart, API examples, validation commands. + +## Product intent + +Build a **transparent, deterministic** rod-string / dynamometer solver stack: **C** is the authoritative numerics engine (predictive + diagnostic, FDM + FEA); **Node (`solver-api`)** is XML/IO, orchestration, card QA, and API versioning; **TypeScript (`gui-ts`)** is workflow and visualization (not the primary solver). + +Outcomes: + +- Physically explainable outputs, not black-box curves. +- **Dynamometer cards** (polished + downhole) as the central engineering surface. +- Preserve **inspectability** of imported XML (`rawFields`, `unsupportedFields`, `verbose`). +- Multi-model workflows: run **FDM** and **FEA**, compare, surface discrepancies and assumptions. + +## Architecture (non-negotiable) + +| Layer | Responsibility | +|-------------|----------------| +| `solver-c/` | All heavy numerics: damped wave FDM, bar FEM, trajectory preprocess, diagnostic transfer, gravity/buoyancy, deviated-well friction proxies, iterative diagnostic FEA when enabled. | +| `solver-api/` | HTTP, `parseCaseXml`, SI normalization, JSON → C stdin, surface-card QA, response shaping (`schemaVersion`, `comparison`, `verbose`, `pumpMovement`). | +| `gui-ts/` | UI: import, plots, 3D survey when implemented, warnings. **No** primary PDE solve in TS except temporary shims during migration (avoid new TS numerics). | + +Same C binaries power Docker, local `make test`, and CI. + +## Guardrails + +- **Determinism:** identical inputs → identical outputs (regression tests and golden hashes depend on this). +- **API fields:** do not remove `verbose`, `pumpMovement`, or `comparison` without bumping `schemaVersion` and documenting migration. +- **No undocumented “magic” multipliers.** If a simplification is not in `MATH_SPEC.md`, tag it `heuristic` in code comments and add a ticket in `COMPUTE_PLAN.md` or a calibration test in `docs/engineering/validation.md`. +- **XML:** never fabricate field names or values not read from a real case file. Unknown fields stay in `rawFields` / `unsupportedFields`. +- **Units:** internal solve uses **SI** (`docs/engineering/units.md`). Convert at the API/parser boundary. +- **Papers:** grounding lives in `references/papers/README.md` (Romero & Almeida; Everitt & Jennings; SPE-173970 Araujo et al.; Eisner et al.). + +## GUI (product) + +The GUI is first-class for end users: continuous visibility of survey and cards, 3D trajectory when implemented, side-by-side solver comparison. Agent work may still touch **compute-only** paths without GUI in a given PR; do not add new numerics in the GUI. + +## When you change the solver + +1. Update `MATH_SPEC.md` if equations or BCs change. +2. Update `COMPUTE_PLAN.md` gap table / status. +3. Run `make test` and golden regression (`solver-api` tests include golden hash where configured). +4. If C CLI or JSON contract changes, update `README.md`, `docker-compose.yml` gcc lines, and root `Dockerfile` compile commands. + +## Repository map (compute) + +| Path | Role | +|------|------| +| `solver-c/src/solver.c` | FDM predictive + shared numerics | +| `solver-c/src/solver_fea.c` | FEA predictive + diagnostic iteration (when applicable) | +| `solver-c/src/solver_diagnostic.c` | Diagnostic FDM (surface card BC) | +| `solver-c/src/trajectory.c` | Survey → curvature / inclination on rod grid (SPE-173970 style) | +| `solver-c/src/json_stdin.c` | Minimal JSON stdin parser for C drivers | +| `solver-api/src/solverClient.js` | Build SI JSON, `execFile` C with stdin | +| `solver-api/src/xmlParser.js` | Parse case + SI conversion | +| `solver-api/src/app.js` | Routes, comparison payload, `schemaVersion` | +| `data/cases/base-case.xml` | Canonical regression case | +| `data/golden/` | Golden response fingerprints | + +--- + +*Merged from legacy `Agents/Instructions.md`. Product vision details are maintained in `README.md`.* diff --git a/Agents/COMPUTE_PLAN.md b/Agents/COMPUTE_PLAN.md new file mode 100644 index 0000000..c33d6e0 --- /dev/null +++ b/Agents/COMPUTE_PLAN.md @@ -0,0 +1,213 @@ +# Compute handoff — Rods Cursor + +**Owner:** Agent team +**Status:** Active +**Last updated:** 2026-04-16 + +**Canonical math:** [MATH_SPEC.md](MATH_SPEC.md) +**Agent rules:** [AGENTS.md](../AGENTS.md) at repo root +**Reference bibliography:** [references/papers/README.md](../references/papers/README.md) + +--- + +## 1. Session context + +Math-first rod-string / dynamometer stack: + +- **C (`solver-c`)** — Authoritative FDM + FEA + diagnostic transfer + trajectory preprocess. **JSON on stdin** to `solver_main` / `solver_fea_main` (`schemaVersion: 2`). +- **Node (`solver-api`)** — XML → SI model, surface-card QA, orchestration, `comparison` / `verbose` / `pumpMovement`, `schemaVersion` in responses. +- **GUI (`gui-ts`)** — Workflow / plots, XML round-trip editor, engineering checks, and 3D wellbore visualization. + +User goals: SROD-like transparency, measured card → downhole + pump movement, FDM vs FEA comparison, no invented physics. + +--- + +## 2. Repository map + +| Path | Role | +|------|------| +| `solver-c/src/solver.c` | FDM predictive + gravity/buoyancy + variable \(EA\rho\) + side-load Coulomb | +| `solver-c/src/solver_fea.c` | FEA predictive + Rayleigh damping + diagnostic bisection (Eisner-style) | +| `solver-c/src/solver_diagnostic.c` | Diagnostic FDM from surface card | +| `solver-c/src/trajectory.c` | Survey → curvature / inclination on rod grid | +| `solver-c/src/json_stdin.c` | stdin JSON → `SolverInputs` | +| `solver-api/src/xmlParser.js` | Parse + **SI** normalization | +| `solver-api/src/solverClient.js` | Build JSON, run C | +| `solver-api/src/cardQa.js` | Surface card validation | +| `solver-api/src/app.js` | HTTP, extended `comparison`, `validate-card` | +| `data/cases/base-case.xml` | Canonical case | +| `data/golden/default.solve.sha256` | Golden fingerprint for `/solve/default` | + +--- + +## 3. Status vs MATH_SPEC (gap table) + +| Item | MATH_SPEC § | Code status | +|------|-------------|-------------| +| Variable \(EA\) FDM | §1–3 | **Done** — per-node `area_m2`, `modulus_pa`, `density_kg_m3` | +| Gravity + buoyancy | §1 | **Done** — distributed \(f = \rho g A - \rho_f g A\) (simplified annulus = rod area) | +| Explicit FD + CFL | §3 | **Done** — `verbose.numerics` | +| 3D survey / curvature | §5 | **Partial** — `trajectory.c` resamples MD/inc/azi; full Araujo Eqs. 14–18 system = future refinement | +| Lukasiewicz lateral PDE | §4 | **Partial** — side load from \(T,\kappa,\phi\) proxy + Coulomb (`heuristic` tag in verbose) | +| Valve state machine | §7 | **Partial** — simplified phase + `gasInterference` flag from load plateau heuristic | +| Diagnostic FDM in C | §7 | **Done** — `solver_diagnostic.c` | +| Diagnostic FEA + iteration | §6–7 | **Done** — bisection on bottom load in `solver_fea.c` when `workflow=diagnostic` | +| Rayleigh \(\alpha,\beta\) from XML damping | §2 | **Done** — tied to rod length + damping factors | +| Multi-material taper | §1 | **Done** — `RodTypeArray` / modulus arrays in JSON | +| Fourier analytical baseline | §3 | **Deferred** | +| Full tube–tube contact (Eisner) | §6 | **Deferred** | + +--- + +## 4. API (quick reference) + +```http +POST /solve +Content-Type: application/json + +{ + "xml": "...", + "solverModel": "fdm|fea|both", + "workflow": "predictive|diagnostic", + "surfaceCard": { "position": [], "load": [], "time": [] }, + "options": { "schemaVersion": 2 } +} +``` + +- `GET /solve/default` — predictive only; supports `?solverModel=`. +- `POST /solve/validate-card` — QA only (no solve). +- Response: `schemaVersion`, `units: "SI"`, `solver`, `solvers?`, `comparison` (extended), `pumpMovement?`, `verbose`, `runMetadata`. + +--- + +## 5. Runbook + +```bash +make test +./solver-c/test_solver +cd solver-api && npm test + +# Docker +docker compose up -d --build +make smoke +``` + +`docker-compose.yml` gcc lines must list all `solver-c/src/*.c` objects used by `main.c` / `main_fea.c`. + +--- + +## 6. Deferred (explicit) + +| Topic | Notes | +|-------|--------| +| Full **Costa / SPE-173970** 3D wave with \(K_1,K_2\) lateral load PDE in C | Currently curvature + side-load **proxy**; implement from paper, not ad hoc | +| **Torsion** | Not in current four-paper backbone | +| **Pumping-unit kinematics** (Svinos tables, crank motion from `PumpingUnitID`) | Harmonic default; unit geometry unused | +| **Inverse calibration** | Fit damping / friction to measured downhole card | +| **Fourier** analytical diagnostic | Optional `comparison.fourier` | +| **GUI** 3D survey + layout | **Partial done** — Results tab includes 3D projected wellbore + rod/pump overlay + DLS contour; not a full 3D engine (no camera controls / mesh terrain yet) | +| **GUI diagnostic workflow** | Tabbed UI ships predictive solve end-to-end; diagnostic requires surface-card upload path in Kinematics (calls `POST /solve/validate-card` + `POST /solve` with `workflow=diagnostic`) — not wired in this pass | +| **GUI Pump / Fluid / Kinematics first-class mapping** | Tabs render editable fields but rely on `rawFields` round-trip rather than dedicated serializer logic; audit once solver-api adds explicit fields for `PumpFillageOption`, pumping-unit kinematics, etc. | +| **GUI fatigue / API RP 11BR table** | Backend does not emit a fatigue payload yet; surface in Results tab when `solver.fatigue` exists | + +--- + +## 6.1 GUI checks/visualization shipped + +- Fixed engineering gate in Solver tab: + - run blocked when `|PumpDepth - sum(TaperLengthArray)| > 15 m`. + - survey MD monotonicity and minimum station-count checks. +- Fixed DLS bad-section threshold: + - warnings + 3D contour use `15 deg/100` as the "bad section" limit. +- Results tab now shows: + - uPlot dynacard overlays, + - 3D projected wellbore with rod gradient and pump marker, + - interactive 3D view controls (rotate, pan, zoom, perspective/orthographic toggle, reset), + - highlighted bad-DLS segments, + - trajectory analytics table with row↔3D segment cross-highlight, + - side-load overlay mode (when `solver.profiles.sideLoadProfile` is available), + - pump-placement diagnostics panel + navigation actions, + - export actions (3D SVG, 3D PNG, summary JSON). + - keyboard-accessible trajectory segment selection (`Enter`/`Space`) and clear-highlight control. +- Kinematics/Solver workflow now supports diagnostic execution end-to-end in GUI: + - measured card paste input (`position,load` rows), + - `POST /solve/validate-card` QA call from Kinematics tab, + - diagnostic solve payload wiring (`workflow=diagnostic`, `surfaceCard`), + - solve options include profile generation for visualization overlays. + +--- + +## 7. Jump table + +| Task | Start | +|------|--------| +| FDM numerics | `solver-c/src/solver.c` | +| FEA + diagnostic bisection | `solver-c/src/solver_fea.c` | +| Diagnostic FDM | `solver-c/src/solver_diagnostic.c` | +| Trajectory | `solver-c/src/trajectory.c` | +| JSON CLI | `solver-c/src/json_stdin.c`, `main.c`, `main_fea.c` | +| SI + XML | `solver-api/src/xmlParser.js` | +| Run C | `solver-api/src/solverClient.js` | +| Card QA | `solver-api/src/cardQa.js` | +| HTTP | `solver-api/src/app.js` | +| Tests | `solver-api/tests/api.test.js`, `solver-c/tests/test_solver.c` | + +--- + +## 8. Quality Program (next execution block) + +This section is the execution plan for the next pass, optimized for "feature-rich but solid". + +### 8.1 Priority 1 — Correctness gates first + +- Build a field-sensitivity harness (per mapped input field, ±1% perturbation). +- Enforce invariants in solver outputs: + - finite values everywhere, + - gas fraction bounds, + - physically valid valve-state transitions, + - profile array length consistency. +- Expand deterministic goldens beyond base case. + +**Acceptance gate:** no merge if sensitivity/invariant tests fail. + +### 8.2 Priority 2 — Solver fidelity + +- Reduce remaining heuristic terms toward equation-backed Costa/Araujo + Lukasiewicz formulations. +- Harden valve/gas state model transitions with explicit edge-case fixtures. +- Add stability fallback behavior and numerical-health reporting. + +**Acceptance gate:** documented equation coverage increases; no stability regressions. + +### 8.3 Priority 3 — Validation depth + +- Add cross-model agreement matrix (FDM vs FEA) across canonical cases. +- Add synthetic/analytical sanity cases where expected trends are known. +- Track residual drift trends over commits. + +**Acceptance gate:** tolerance matrix passes for all canonical cases. + +### 8.4 Priority 4 — Contract hardening + +- Keep `schemaVersion: 2` additive contract stable by default. +- Enforce option-gated heavy payloads (`profiles`, `diagnostics`, `fourier`). +- Add traceability metadata endpoint/payload support for GUI and audits. + +**Acceptance gate:** backward-compat tests pass on default endpoints. + +### 8.5 Priority 5 — CI/release readiness + +- Add sanitizer runs (ASan/UBSan) for C paths. +- Add runtime/performance budgets on representative cases. +- Enforce quality artifact generation in CI (comparison summaries + drift reports). + +**Acceptance gate:** CI quality lane green. + +### 8.6 Multi-agent execution split + +- Agent A: solver numerics/fidelity. +- Agent B: GUI integration + import/export + rendering. +- Agent C: verification harness + CI guardrails + independent audit. + +--- + +*Extend §3 and §8 together whenever milestones ship.* diff --git a/Agents/MATH_SPEC.md b/Agents/MATH_SPEC.md new file mode 100644 index 0000000..dee44bb --- /dev/null +++ b/Agents/MATH_SPEC.md @@ -0,0 +1,133 @@ +# Math specification — literature backbone + +**Status:** Living document. Every implemented term should trace to a citation below or to an explicit `heuristic` calibration note in code + `docs/engineering/validation.md`. + +**Primary references:** see `references/papers/README.md` for citation details and source links. + +- Romero, A., and Almeida, A. R. (2014). *A Numerical Sucker-Rod Pumping Analysis Tool*. SPE Artificial Lift Conference - Latin America and the Caribbean. [SPE-169395-MS](https://doi.org/10.2118/169395-MS) +- Everitt, T. A., and Jennings, J. W. (1992). *An Improved Finite-Difference Calculation of Downhole Dynamometer Cards for Sucker-Rod Pumps*. SPE Production Engineering. [SPE-18189-PA](https://doi.org/10.2118/18189-PA) +- Araujo, O., et al. (SPE-173970). *3D Rod String Dynamics in Deviated Wells* (minimum-curvature and coupled dynamics reference used for trajectory coupling). +- Eisner, B., Langbauer, C., and Fruhwirth, R. (2022). *A finite element approach for dynamic sucker-rod diagnostics* (Newmark + Rayleigh + diagnostic iteration basis). +- Lukasiewicz, H. (as summarized in Eisner et al.) coupled axial/lateral force balance for deviated rod strings. + +--- + +## 1. Gibbs one-dimensional damped wave (vertical / uniform rod) + +**Romero & Almeida — Eq. (2)** (viscous damped wave, constant \(A,E,\rho\)): + +\[ +\frac{\partial^2 u}{\partial t^2} = a^2 \frac{\partial^2 u}{\partial x^2} - \varsigma \frac{\partial u}{\partial t} +\] + +with \(a^2 = E/\rho\) (wave speed) and \(\varsigma\) viscous damping coefficient per unit mass (paper uses lumped fluid damping narrative). + +**Variable cross-section (Everitt & Jennings — Eq. 2 form)** after multiplying through by \(\rho A\); axial stiffness gradient: + +\[ +\frac{\partial}{\partial x}\left( EA \frac{\partial u}{\partial x} \right) = \rho A \frac{\partial^2 u}{\partial t^2} + c \rho A \frac{\partial u}{\partial t} - f_{\text{body}} +\] + +where \(f_{\text{body}}\) collects distributed **weight and buoyancy** along the rod (gravity along tangent; buoyancy from fluid — **Lukasiewicz** axial force balance in Eisner Eq. (2) discussion / Lukasiewicz coupled model referenced in Eisner). + +**Implementation note:** Code uses discrete \(E_i, A_i, \rho_i\) per node or segment, harmonic or measured surface \(u(0,t)\), and bottom boundary coupling (pump / valve / spring-damper per roadmap). + +--- + +## 2. Damping conventions + +- **Gibbs dimensionless damping** \(\nu\): related to decay rate; Romero cites \(\varsigma\) proportional to velocity; Eisner Eq. (1) uses \(\frac{\pi a \nu}{2L}\frac{\partial u}{\partial t}\) form for vertical damped wave. +- **Rayleigh damping (FEA, Eisner):** \(\mathbf{C} = \alpha \mathbf{M} + \beta \mathbf{K}\) for bar elements. +- **XML factors:** `UpStrokeDampingFactor`, `DownStrokeDampingFactor`, `NonDimensionalFluidDamping` modulate effective \(\gamma\) or \(\alpha,\beta\) per phase (mapped in solver; see code comments). + +--- + +## 3. Explicit finite-difference stencil (diagnostic / deviated extension) + +**Everitt & Jennings — Eq. (3)** (conceptual explicit FD recursion transferring displacements downhole from surface card). The paper gives a five-point relation in \((i,j)\) space (space index \(i\), time \(j\)) with coefficients involving \(a\), \(c\), \(\Delta t\), \(\Delta x\), and \(EA\). + +**Variable \(EA\):** harmonic mean or segment fluxes: + +\[ +\text{flux}_{i+\frac12} = E_{i+\frac12} A_{i+\frac12} \left( u_{i+1} - u_i \right) +\] + +Laplacian-like term formed from \(\partial/\partial x (EA \partial u/\partial x)\) discretization (matches current diagnostic JS prior to C port). + +**CFL:** explicit wave requires \(\Delta t \le \text{CFL} \cdot \Delta x / a_{\max}\) with \(a_{\max} = \max \sqrt{E/\rho}\). Code clamps effective CFL (see `verbose.numerics.cflEffective`). + +--- + +## 4. Deviated wells — coupled axial / lateral (reference) + +**Lukasiewicz (via Eisner Eqs. (2)–(3))** — force balance along rod tangent and normal; includes \(\rho g A \cos\phi\), viscosity term \(\nu\), Coulomb \(\mu N\), curvature \(R\), and lateral equilibrium. + +**Current implementation:** distributed **side load** \(N_i\) and inclination-aware **Coulomb friction** \(F_{f,i} = \mu_{\text{eff}} N_i \operatorname{sgn}(v_i)\) are computed per node and injected into FDM/diagnostic/FEA updates. API exposes `profiles.sideLoadProfile` and `profiles.frictionProfile` when `options.enableProfiles=true`. + +--- + +## 5. Three-dimensional trajectory (SPE-173970) + +**Araujo et al. — Eqs. (5)–(24):** minimum-curvature method between survey stations: unit tangent, curvature angle \(\gamma_i\), radius \(r_{c,i}\), binormal, center of curvature, position propagation \(R(s_{i+1})\), and interpolation of any MD \(s\) within a segment. + +**Implementation:** `solver-c/src/trajectory.c` uses minimum-curvature style tangent interpolation and exports node-wise \(\kappa(s)\), inclination, and azimuth on the rod grid for side-load/friction coupling and `trajectory3D` output. + +--- + +## 6. Dynamic 1D bar FEM (Eisner et al.) + +- **Bar stiffness / mass:** consistent element \(K_e\), \(M_e\) for linear shape functions. +- **Newmark-β** (\(\beta=\frac14\), \(\gamma=\frac12\)) for transient axial dynamics. +- **Bottom BC:** spring-damper + friction; diagnostic mode adds **iterative plunger load** adjustment so that computed top reaction matches measured polished load (Eisner Fig. 4 principle — restart / bisection per time step). + +--- + +## 7. Boundary conditions + +| Mode | Surface | Bottom | +|------|---------|--------| +| **Predictive** | Harmonic \(u(0,t)\) (Romero **Eq. (4)** style crank motion) or measured surrogate | Pump: spring-damper + friction + valve-state pressure balance; gas fraction inferred from chamber state | +| **Diagnostic** | Measured \(u(0,t)\) and \(F(0,t)\) from dynamometer card (Everitt) | Same pump BC; FEA adjusts unknown plunger load and reports valve/gas timeline | + +**Surface card QA (Eisner narrative):** ≥ 75 samples recommended for Fourier-style tools; we enforce minimum samples and cycle checks in API (`POST /solve/validate-card`). + +--- + +## 8. Outputs (API / solver) + +- Polished and downhole **cards** \((x, F)\). +- **Pump movement:** plunger position series, velocity, stroke, period. +- **Optional profiles:** `stressProfile`, `sideLoadProfile`, `trajectory3D` (when `schemaVersion >= 2`). +- **Comparison:** FDM vs FEA peak deltas + point-wise residuals + optional Fourier analytical baseline (`comparison.fourier` when enabled). + +--- + +## 9. Symbols (SI internal) + +| Symbol | Unit | Meaning | +|--------|------|---------| +| \(u\) | m | Axial displacement | +| \(F\) | N | Axial force (tension positive) | +| \(E\) | Pa | Young’s modulus | +| \(A\) | m² | Cross-sectional area | +| \(\rho\) | kg/m³ | Rod density | +| \(\phi\) | rad | Inclination from vertical | +| \(\kappa\) | 1/m | Path curvature | + +--- + +## 10. Implementation quality rules (for next execution) + +- Every newly introduced solver term must be tagged as one of: + - `equation-backed` (with paper/equation reference), + - `heuristic` (with rationale + planned replacement). +- No silent heuristic drift: if a coefficient changes, update validation fixtures and notes. +- Any new field wired into equations must be reflected in `docs/engineering/field-traceability.md`. +- Any new coupled term must include at least: + - one unit-scale numerical test, + - one integration case assertion, + - one regression guard. + +--- + +*End of math spec backbone. Extend with equation numbers from PDFs as features land.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95b3386 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-alpine AS api-deps +WORKDIR /app/solver-api +COPY solver-api/package.json solver-api/package-lock.json* ./ +RUN npm install + +FROM node:20-alpine AS gui-deps +WORKDIR /app/gui-ts +COPY gui-ts/package.json gui-ts/package-lock.json* ./ +RUN npm install + +FROM alpine:3.20 AS solver-build +RUN apk add --no-cache build-base +WORKDIR /app +COPY solver-c ./solver-c +RUN SRCS="./solver-c/src/solver_common.c ./solver-c/src/json_stdin.c ./solver-c/src/trajectory.c ./solver-c/src/solver_diagnostic.c ./solver-c/src/solver.c ./solver-c/src/solver_fea.c ./solver-c/src/solver_fourier.c" \ + && gcc -std=c99 -I./solver-c/include $$SRCS ./solver-c/src/main.c -lm -o ./solver-c/solver_main \ + && gcc -std=c99 -I./solver-c/include $$SRCS ./solver-c/src/main_fea.c -lm -o ./solver-c/solver_fea_main \ + && gcc -std=c99 -I./solver-c/include $$SRCS ./solver-c/tests/test_solver.c -lm -o ./solver-c/test_solver \ + && ./solver-c/test_solver + +FROM node:20-alpine AS ci +WORKDIR /app +COPY --from=solver-build /app/solver-c ./solver-c +COPY --from=api-deps /app/solver-api/node_modules ./solver-api/node_modules +COPY --from=gui-deps /app/gui-ts/node_modules ./gui-ts/node_modules +COPY solver-api ./solver-api +COPY gui-ts ./gui-ts +COPY data ./data +CMD ["sh", "-c", "cd /app/solver-api && npm test && cd /app/gui-ts && npm test"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..12ab2cb --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +SHELL := /bin/bash + +.PHONY: run down logs test smoke + +run: + docker compose up --build + +down: + docker compose down + +logs: + docker compose logs -f + +test: + cd solver-api && npm test + cd gui-ts && npm test + ./solver-c/test_solver + +smoke: + @echo "Checking API health..." + @for i in {1..30}; do \ + if curl --fail --silent http://localhost:4400/health > /dev/null; then \ + echo "API healthy"; \ + break; \ + fi; \ + sleep 1; \ + if [ $$i -eq 30 ]; then \ + echo "API did not become healthy in time"; \ + exit 1; \ + fi; \ + done + @echo "Checking solve endpoint with base case..." + @curl --fail --silent http://localhost:4400/solve/default > /dev/null && echo "Solve endpoint healthy" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d6081f --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# Rods Cursor — Rod-string solver (math-first) + +Deterministic **C** numerical core (FDM + FEA), **Node** API for XML and orchestration, **TypeScript** GUI for workflow. See **[AGENTS.md](AGENTS.md)** for agent rules and **[Agents/MATH_SPEC.md](Agents/MATH_SPEC.md)** for equations and paper citations. + +## Repository layout + +| Directory | Purpose | +|-----------|---------| +| `solver-c/` | C: damped-wave FDM, dynamic bar FEM, diagnostic transfer, trajectory preprocess, **JSON stdin** drivers | +| `solver-api/` | Express: `POST /solve`, `GET /solve/default`, `POST /case/parse`, `POST /solve/validate-card`, XML → SI | +| `gui-ts/` | Vite + React tabbed case editor + uPlot dynacards | +| `data/cases/` | `base-case.xml` and regression inputs | +| `data/golden/` | Golden SHA-256 fingerprint for default solve regression | +| `Agents/` | `MATH_SPEC.md`, `COMPUTE_PLAN.md` (handoff) | +| `docs/engineering/` | Architecture, schema, units, validation | +| `references/papers/` | Literature citations and access notes for the solver math backbone | + +## Vision + +Build a transparent, deterministic rod-string and wellbore mechanics platform that is field-usable and research-grade. + +- Physically explainable pumping-system simulations, not black-box outputs. +- Dynamometer cards as the primary interpretation surface. +- Inspectability of imported case data and solver assumptions. +- Multi-model validation (FDM vs FEA) with clear comparison metadata. + +## Prerequisites + +- **Local:** `gcc`, `make`, Node 20+, `npm` +- **Docker:** Docker Engine + Compose plugin + +## Run locally + +### Build and run C solver (stdin JSON) + +The API spawns `solver-c/solver_main` and pipes **one JSON object** on stdin (`schemaVersion: 2`). Legacy 9-argument CLI is **removed**. + +```bash +gcc -std=c99 -I./solver-c/include \ + ./solver-c/src/solver_common.c \ + ./solver-c/src/json_stdin.c \ + ./solver-c/src/trajectory.c \ + ./solver-c/src/solver_diagnostic.c \ + ./solver-c/src/solver.c \ + ./solver-c/src/solver_fea.c \ + ./solver-c/src/solver_fourier.c \ + ./solver-c/src/main.c -lm -o ./solver-c/solver_main + +gcc -std=c99 -I./solver-c/include \ + ./solver-c/src/solver_common.c \ + ./solver-c/src/json_stdin.c \ + ./solver-c/src/trajectory.c \ + ./solver-c/src/solver_diagnostic.c \ + ./solver-c/src/solver.c \ + ./solver-c/src/solver_fea.c \ + ./solver-c/src/solver_fourier.c \ + ./solver-c/src/main_fea.c -lm -o ./solver-c/solver_fea_main + +./solver-c/test_solver +``` + +### API + GUI + +```bash +cd solver-api && npm install && npm run dev +cd gui-ts && npm install && npm run dev +``` + +- API: `http://localhost:4400/health` +- GUI: `http://localhost:5173` + +## Run with Docker + +```bash +make run # or: docker compose up --build +make smoke # requires API on 4400 +make down +``` + +## Validation + +```bash +make test # solver-api vitest + gui-ts tests + solver-c test_solver +./solver-c/test_solver +``` + +Golden hash: `solver-api` tests assert `/solve/default` body matches `data/golden/default.solve.sha256` (after normalizing `generatedAt`). + +## API examples + +### Predictive (default base case) + +```bash +curl -sS "http://localhost:4400/solve/default?solverModel=both" | jq '.schemaVersion, .runMetadata, .comparison | keys' +``` + +### Predictive (POST XML) + +```bash +curl -sS -X POST http://localhost:4400/solve \ + -H "Content-Type: application/json" \ + -d "{\"xml\": $(jq -Rs . < data/cases/base-case.xml), \"solverModel\": \"fdm\"}" | jq '.solver.pointCount, .schemaVersion' +``` + +### Extended physics outputs (profiles/diagnostics/fourier) + +```bash +curl -sS -X POST http://localhost:4400/solve \ + -H "Content-Type: application/json" \ + -d "{\"xml\": $(jq -Rs . < data/cases/base-case.xml), \"solverModel\": \"both\", \"options\": {\"enableProfiles\": true, \"enableDiagnosticsDetail\": true, \"enableFourierBaseline\": true, \"fourierHarmonics\": 10}}" \ + | jq '.solver.profiles.nodeCount, .solver.diagnostics.valveStates[0], .comparison.fourier.harmonics' +``` + +### Diagnostic (measured surface card) + +Build `surfaceCard` from a predictive run or field data: + +```bash +CARD=$(curl -sS "http://localhost:4400/solve/default?solverModel=fdm") +# Then POST xml + workflow + surfaceCard (see solver-api/tests/api.test.js) +``` + +### Surface card QA only + +```bash +curl -sS -X POST http://localhost:4400/solve/validate-card \ + -H "Content-Type: application/json" \ + -d '{"surfaceCard":{"position":[0,1,2],"load":[10,11,12]}}' | jq . +``` + +### Parse XML only (no solve) + +```bash +curl -sS -X POST http://localhost:4400/case/parse \ + -H "Content-Type: application/json" \ + -d "{\"xml\": $(jq -Rs . < data/cases/base-case.xml)}" | jq '.schemaVersion, .model.wellName, (.unsupportedFields | length)' +``` + +Returns `{ model, rawFields, unsupportedFields, warnings, schemaVersion: 2 }` — same shape as `GET /case/default`, but for an uploaded XML string. Used by the GUI to hydrate its case editor from an import. + +## GUI tabbed case editor + +`gui-ts` renders a tabbed UI backed by a single `CaseState` that round-trips to/from the XML document. On first load it pulls `GET /case/default` and populates every tab; editing any field mutates `CaseState`, which is serialized back into `` on solve or export. Untouched XML fields (fatigue, IPR blocks, pumping-unit catalog keys, etc.) are preserved verbatim in the output. + +| Tab | Contents | +|-----|----------| +| Well | Well name, company, units selection, pump depth, tubing anchor / size | +| Trajectory | Editable MD / Inc / Az survey table with vertical + deviated presets | +| Kinematics | Pumping speed (SPM), speed option, unit ID, upstroke/downstroke percentages | +| Rod String | Taper table (diameter, length, modulus, rod type) with base-case preset | +| Pump | Plunger diameter, friction, intake pressure, fillage option + percent | +| Fluid | Water cut, water SG, oil API, tubing gradient | +| Solver | `solverModel` (fdm/fea/both), workflow selector, damping + friction knobs, engineering checks gate, **Run Solver** | +| Results | KPI banner, uPlot dynacard (polished + downhole, with FEA overlay when applicable), FDM↔FEA comparison, warnings, unsupported-field list, 3D wellbore/rod/pump view with DLS or side-load contour, trajectory analytics table, pump diagnostics, export tools | +| Advanced / XML | File upload + paste box (POST `/case/parse`), export current state as XML, raw-field inspector | + +### Built-in engineering checks + +- **Pump depth vs total rod length:** solver run is blocked if the absolute mismatch exceeds **15 m**. +- **Trajectory integrity:** requires at least 2 stations and strictly increasing measured depth. +- **DLS warning threshold:** if max dogleg severity exceeds **15 deg/100**, the UI surfaces a warning. + +These are fixed guardrails (not user configurable) to keep behavior deterministic and consistent across sessions. + +### 3D wellbore visualization + +The Results tab includes a 3D projected wellbore panel: + +- Tubing trajectory polyline colored by DLS contour (green/yellow/red). +- **Bad sections** highlighted in red for DLS >= **15 deg/100**. +- Rod string overlay drawn from surface to rod total length with depth color gradient. +- Pump marker placed along trajectory at `PumpDepth`. +- Interactive controls: drag to rotate, `Shift+drag` to pan, mouse wheel / buttons to zoom, projection toggle (perspective/orthographic), and reset view. +- Overlay modes: + - `DLS`: uses fixed bad-section threshold `15 deg/100` + - `Side-load risk`: colors by normalized side-load profile returned from solver outputs (`options.enableProfiles=true`) + +### Trajectory analytics + cross-highlight + +- Results includes a per-segment trajectory table (`MD start/end`, `ΔMD`, `DLS`, severity). +- Clicking a segment row highlights the corresponding 3D trajectory segment. +- Clicking a segment in 3D highlights the corresponding table row. +- Filter toggle supports "bad sections only". +- Keyboard accessibility: segment rows are focusable and selectable with `Enter` / `Space`. + +### Pump placement diagnostics + +Results tab now reports: +- nearest survey station to pump depth, +- pump-to-station `ΔMD`, +- survey-end to pump `ΔMD`, +- rod-total to pump `Δ`, +- tubing-anchor to pump `Δ`, +with quick navigation buttons back to Well / Trajectory / Rod tabs for correction. + +### Visualization artifact export + +Results tab export buttons: +- 3D wellbore **SVG** +- 3D wellbore **PNG** +- run/check summary **JSON** + +These are generated client-side from the rendered SVG and current run/check state. + +### Diagnostic workflow (GUI wired) + +- Kinematics tab accepts measured surface card points as `position,load` rows. +- `Validate Surface Card` calls `POST /solve/validate-card`. +- Solver tab `workflow=diagnostic` now sends `surfaceCard` to `POST /solve`. +- Solve calls include `options.enableProfiles=true` so side-load overlays can be rendered. + +## Solver modes + +| `solverModel` | Behavior | +|----------------|----------| +| `fdm` | Finite-difference damped wave + variable rod + trajectory friction | +| `fea` | 1D bar FEM + Newmark + Rayleigh damping | +| `both` | Runs FDM + FEA; returns `solvers` and extended `comparison` | + +| `workflow` | Behavior | +|-------------|----------| +| `predictive` | Harmonic surface motion (unless overridden later) | +| `diagnostic` | Surface card BC; FDM in C; FEA uses bisection on pump load to match measured top load | + +## Optional CI image + +```bash +docker build -t rods-ci . +docker run --rm rods-ci +``` + +## Where to read next + +1. [AGENTS.md](AGENTS.md) +2. [Agents/MATH_SPEC.md](Agents/MATH_SPEC.md) +3. [Agents/COMPUTE_PLAN.md](Agents/COMPUTE_PLAN.md) +4. [docs/engineering/units.md](docs/engineering/units.md) +5. [docs/engineering/validation.md](docs/engineering/validation.md) diff --git a/data/cases/base-case.xml b/data/cases/base-case.xml new file mode 100644 index 0000000..6c4f13c --- /dev/null +++ b/data/cases/base-case.xml @@ -0,0 +1,170 @@ + + + + 0 + 1 + 0 + Conner + 0:14.4:108.5:124.4:115.2:96.4:80.6:184.5:83.7:102.8:316:146.7:159.3:275.7:266.1:170.1:289.5:293.3:297.2:288.8:303.5:32.7:70.6:75.6:76.5:80.2:81.6:80.2:79.8:80.7:80.7:80.4:79.7:78.9:79.2:79.2:78.6:77.4:76.5:76.4:77.3:77.4:77.8:78.2:78:78.3:78.2:78.2:78.9:79.8:80.2:80.2:80.3:80.3:80.6:79.8:79.1:78.2:78.2:79:79.3:79.3:79.3:79.3:79.3:79.7:78.8:79.5:79.2:79:77.9:76.7:76.4:76.9:76:76.7:77.3:76.9:76.7:77.4:76.9:77.1:76.7:77.1:76.7:76.4:76:76.4:76.4:76.2:76:75.7:75:75.5:76.4:76.4:75.5:76.4:75.3:76:74.8:75.3:75.1:76.4:76.7:77.8:77.3:77.3:77.6:77.4:77.3:78.1:78.7:77.9:77.3:77.3:77.3:76.7:76:77.3:77.6:78.3:79.2:79.2:78.8:78.8:78.7:77.8:79:78.5:78.5:78.3:77.9:77.8:77.8:77.3:77.8:77.9:76.5:76.4:76.4:76.7:76.5:76.9:76.4:76:76.9:76.9:78.5:78.5:77.3:79:79.5:78.8:80.2:80.1:81.5:80.1:79.7:80.1:79.5:80.6:80.8:80.6:80.8:81.5:81.6:81.6:80.4:80.6:79.7:79.5:78.8:78.1:76.7:75:76.2:75.3:76.2:76.2:75.1:76.2:74.8:75.3:75.5:75.3:75.5:74.6:73.9:72.3 + 0 + 0 + 0 + 2 + 0 + Base Case + 0 + 0 + + Veren + 0 + 0 + 1 - 367.3 (cm) + + 0 + 4/28/2025 + . + 0 + 190 + 4/28/2025 + 0.15 + 0 + 10 + 0 + 8580 + 0 + 0 + 0 + 2275 + 43 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0:0.3:0.7:0.7:0.7:0.8:1:0:0.1:0.8:0.4:1.4:0.5:1.1:0.8:0.7:0.2:0.5:0.8:0.8:0.8:1.1:3.3:5.8:8.1:10.5:12.8:15.6:17.9:21.5:25.2:29:31.9:33.7:35.1:36.4:37.8:40.7:43.6:46.8:48.9:50.4:51.7:53:56.1:60.2:62.6:62.8:64.8:68.1:71.6:73.1:74.7:75.4:77.4:80:82.5:84.5:85.2:87:87.9:88.6:89.52:89.8:89.8:90:90.3:90:90:89.5:89.9:90:89.7:89.4:89.9:90:90.1:90.6:90.6:90.4:90.5:90.4:90.4:90.2:90.2:90.5:90.6:90.1:89.8:89.7:90.3:90.3:90.2:90.7:91.6:91.9:91.6:90.4:90.4:89.6:89.9:89.4:89.3:89.5:89.4:89.9:90.1:90:90.6:90.3:90.1:90.9:91.5:91.6:91.4:90.8:90.7:90.1:90.1:90.1:90.2:90.8:91.1:91.3:91.2:91.1:91:90.8:90:89.8:89.4:89.3:88.9:89.2:89.4:90.4:90.2:90:90.8:91:90:90.1:91.2:90.8:90.5:90.7:90.2:90.2:90.4:90.6:90.1:89.8:90.4:90.5:90.3:90.5:91.4:91:90.9:90.6:90.9:91.1:91.6:91.9:90.7:89.8:89.5:89.6:89.2:88.3:88.8:88.8:89.6:91.1:91.8:92:91.5:91.1:90.6:90.6:90.8:90.1:89.9:89.2:88.9:89.9:89.7:90.7:90.8:90.8 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 10 + 50 + 40 + 200 + 0:211.25:302.71:347.65:439.95:533.59:629.78:658.7:754.99:851.37:880.27:976.66:1005.51:1034.41:1082.54:1178.69:1207.53:1303.89:1361.65:1390.31:1399.97:1409.6:1419.27:1428.93:1438.55:1448.17:1457.82:1467.46:1477.11:1486.74:1496.39:1506.08:1515.65:1525.27:1534.9:1544.5:1554.13:1563.77:1573.36:1582.99:1592.55:1602.16:1611.79:1621.4:1631.01:1640.63:1650.24:1659.87:1669.5:1679.14:1688.78:1698.45:1708.04:1717.66:1727.29:1736.93:1746.55:1756.16:1765.78:1775.44:1782:1789.92:1797:1799.16:1808.29:1817.68:1826.74:1835.79:1845.06:1854.32:1863.35:1872.6:1881.88:1890.82:1900.16:1909.46:1919.16:1928.51:1937.17:1946.58:1955.93:1965.54:1974.49:1983.44:1992.94:2002.25:2011.28:2020.31:2029.42:2038.8:2047.95:2057.18:2066.19:2075.62:2084.57:2093.47:2102.88:2112.2:2121.16:2130.42:2139.38:2148.68:2158.13:2167.47:2177.11:2186.72:2196.38:2206:2215.63:2225.21:2234.82:2244.44:2254.09:2263.69:2273.32:2282.94:2292.57:2302.14:2311.78:2321.39:2331.02:2340.63:2350.31:2359.99:2369.61:2379.29:2388.88:2398.49:2408.07:2417.68:2427.33:2437:2446.63:2456.33:2465.9:2475.6:2485.23:2494.81:2504.44:2514.07:2523.67:2533.29:2542.92:2552.6:2562.27:2571.92:2581.55:2591.16:2600.81:2610.45:2620.06:2629.69:2639.29:2648.89:2658.49:2668.16:2677.78:2687.37:2697.01:2706.66:2716.28:2725.91:2735.53:2745.15:2754.77:2764.46:2774.08:2783.64:2793.26:2802.88:2812.51:2822.01:2831.66:2841.39:2851.03:2860.66:2870.25:2879.84:2889.5:2899.12:2908.71:2918.34:2928:2937.63:2947.25:2956.81:2966.43:2976.06:2985.67:2995 + 0 + 0 + 0 + 3 + 1.5 + Norris PPS-Standard + + 0 + 0 + 0 + 1 + 1.5 + 0 + 0 + 2 + 0 + 50 + 50 + 0 + 0 + 0 + 60 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1727 + 31.75 + 80 + 1 + 200 + 5 + 1 + HG320-256-144 + 1802 + 100 + 0 + 5 + 0 + 0 + 0 + 0.2 + M:M:M:N:M::::: + 0:0:0:0:0:0:0:0:0:0 + 11.43 + 7.62 + 0 + 3:3:2:3:3:0:0:0:0:0 + -1 + 24 + 0 + 275.79 + 0.8 + 0 + 0 + 0 + 0 + + 1 + 1 + 100 + + 5 + 1 + 22.225:19.05:38.1:19.05:19.05:0:0:0:0:0 + -1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1:-1 + 86:86:10:36:9:0:0:0:0:0 + 0:0:0:0:0:0:0:0:0:0 + 30.5:30.5:30.5:30.5:30.5:0:0:0:0:0 + 792897.055:792897.055:620528.13:792897.055:792897.055:0:0:0:0:0 + 2.224:1.634:6:1.634:1.634:0:0:0:0:0:0:0:0:0:0:0 + 0 + 0 + 1361.3 + 9.989 + 3 + 0 + 2 + 0.05 + 9.0.0 + 0 + 3 + 73 + 1.096 + 1 + 191/01-27-007-09W2/00 + 0.1 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + Vogel + 0:0:0 + + diff --git a/data/golden/default.solve.sha256 b/data/golden/default.solve.sha256 new file mode 100644 index 0000000..00d50ae --- /dev/null +++ b/data/golden/default.solve.sha256 @@ -0,0 +1 @@ +d433dd1061c9f26679507fac42299d97d6d9c0b446651eeaa6ac03529e424fa0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bb35375 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +services: + solver-api: + build: + context: ./solver-api + dockerfile: Dockerfile + container_name: rods-solver-api + working_dir: /app + ports: + - "4400:4400" + volumes: + - ./solver-api/src:/app/src + - ./data:/data + - ./solver-c:/solver-c + command: sh -c "SRCS='/solver-c/src/solver_common.c /solver-c/src/json_stdin.c /solver-c/src/trajectory.c /solver-c/src/solver_diagnostic.c /solver-c/src/solver.c /solver-c/src/solver_fea.c /solver-c/src/solver_fourier.c' && gcc -std=c99 -I/solver-c/include $$SRCS /solver-c/src/main.c -lm -o /solver-c/solver_main && gcc -std=c99 -I/solver-c/include $$SRCS /solver-c/src/main_fea.c -lm -o /solver-c/solver_fea_main && npm run dev" + + gui-ts: + build: + context: ./gui-ts + dockerfile: Dockerfile + container_name: rods-gui-ts + working_dir: /app + ports: + - "5173:5173" + environment: + - VITE_API_BASE=http://localhost:4400 + volumes: + - ./gui-ts:/app + - /app/node_modules + command: npm run dev -- --host 0.0.0.0 --port 5173 + depends_on: + - solver-api diff --git a/docs/engineering/architecture.md b/docs/engineering/architecture.md new file mode 100644 index 0000000..80a08fa --- /dev/null +++ b/docs/engineering/architecture.md @@ -0,0 +1,70 @@ +# Architecture and requirement traceability + +Owner: Rods Project Team +Status: Active +Last Updated: 2026-04-16 +Version: 0.2.0 + +## Module boundaries + +- **`solver-c`:** Authoritative numerics — FDM, FEA, diagnostic FDM, trajectory preprocess, distributed side-load/friction coupling, valve/gas dynamic diagnostics, optional Fourier analytical baseline, gravity/buoyancy, variable \(E(x), A(x), \rho(x)\), JSON **stdin** contract (`schemaVersion: 2`). No HTTP/XML. +- **`solver-api`:** Local HTTP service — parse XML, convert to **SI**, surface-card QA, spawn C binaries with JSON pipe, attach `verbose`, `comparison`, `pumpMovement`, `schemaVersion`. +- **`gui-ts`:** Workflow and visualization (tabbed case editor, XML round-trip, dynacards, engineering checks, 3D wellbore preview); not the primary PDE solver. +- **`data/cases`:** Canonical inputs and regression cases. +- **`data/golden`:** Fingerprints for deterministic API regression. +- **`references/papers/README.md`:** Citation index for external literature used by `Agents/MATH_SPEC.md`. + +## C driver contract + +Executables read **one JSON object from stdin** (UTF-8). Required top-level keys are produced by `solver-api/src/solverClient.js` — see that file for the exact shape (`workflow`, `model`, optional `surfaceCard`, optional `options`). + +`options` supports: +- `enableProfiles` — includes trajectory + side-load + friction profile payloads. +- `enableDiagnosticsDetail` — includes valve-state and chamber/gas time-series. +- `enableFourierBaseline` — computes analytical Fourier baseline in C. +- `fourierHarmonics` — harmonic count for baseline reconstruction. + +Legacy argv-based invocation has been **removed** to avoid drift between CLI and API. + +## MVP requirement traceability + +| Requirement | Module | Notes | +|-------------|--------|--------| +| Real C solver path | `solver-c`, `solver-api` | JSON pipe; no mock-only solve | +| XML inspectability | `solver-api`, `gui-ts` | `rawFields`, `unsupportedFields` | +| Output cards primary | `solver-c`, `gui-ts` | Polished + downhole series | +| Determinism | `solver-c`, `solver-api` | Golden hash; no randomness | +| Warnings / numerics | `solver-c`, `solver-api` | `warnings`, `verbose.numerics`, CFL | +| 3D survey visibility | `gui-ts` | Results tab includes projected 3D wellbore with DLS/side-load overlay modes + rod/pump overlays + interactive camera controls | + +## System flow + +1. GUI or client sends XML (+ options) to API. +2. API parses to canonical **SI** model + preserves unknown fields. +3. Optional: `POST /solve/validate-card` runs QA only. +4. API builds JSON and runs `solver_main` / `solver_fea_main` with stdin. +5. C returns JSON stdout; API merges `parsed`, `comparison`, `runMetadata`. +6. GUI renders cards, survey, inspection panels. + +## Execution guidelines for upcoming hardening + +- Keep defaults stable; grow contract additively behind options. +- Treat C as numerical source-of-truth; API orchestrates and validates. +- Require test evidence for every new equation term: + - unit-level numeric sanity, + - integration-level behavior, + - deterministic regression impact. +- Pair every new feature with: + - one "happy path" case, + - one edge/stress case, + - one regression gate entry. + +## GUI guardrails and thresholds + +- Pump-depth consistency gate: solver run is blocked when `abs(PumpDepth - totalRodLength) > 15 m`. +- DLS bad-section threshold: `15 deg/100` used both for warning logic and 3D contour highlighting. +- Thresholds are intentionally fixed (not user-configurable) to keep deterministic behavior. +- Diagnostic GUI path is now wired: + - measured card input + QA call in Kinematics tab, + - `workflow=diagnostic` run from Solver tab with `surfaceCard` payload. +- Results now include trajectory segment analytics and artifact export actions (3D SVG/PNG, summary JSON). diff --git a/docs/engineering/case-schema.md b/docs/engineering/case-schema.md new file mode 100644 index 0000000..f1a6c72 --- /dev/null +++ b/docs/engineering/case-schema.md @@ -0,0 +1,62 @@ +# Case schema and import contract + +Owner: Rods Project Team +Status: Active +Last Updated: 2026-04-16 +Version: 0.2.0 + +## Canonical input + +- Root: `INPRoot/Case` +- Example: `data/cases/base-case.xml` + +## Parsed fields used by solver (SI after `xmlParser`) + +### Core (required) + +- `PumpingSpeed` — SPM +- `PumpDepth` — MD to pump (case units → m) +- `MeasuredDepthArray`, `InclinationFromVerticalArray` — trajectory (`:` separated) +- `AzimuthFromNorthArray` — optional for vertical; required for full 3D curvature in API validation + +### Friction / contact + +- `RodFrictionCoefficient` — Coulomb \(\mu\) baseline +- `StuffingBoxFriction`, `PumpFriction` — case force units → N +- `MoldedGuideFrictionRatio`, `WheeledGuideFrictionRatio`, `OtherGuideFrictionRatio` — scale effective \(\mu\) along string (`heuristic` blend) + +### Rod string / materials + +- `TaperDiameterArray`, `TaperLengthArray`, `TaperModulusArray`, `TaperWeightArray`, `TaperMTSArray` +- `RodTypeArray` — maps to steel vs fiberglass density/modulus defaults when modulus not set +- `TubingAnchorLocation` +- `SinkerBarDiameter`, `SinkerBarLength` +- `RodGuideTypeArray`, `RodGuideWeightArray` — metadata / future contact + +### Damping + +- `UpStrokeDampingFactor`, `DownStrokeDampingFactor` +- `NonDimensionalFluidDamping` — Gibbs-style dimensionless damping input + +### Pump / fluid (valve + buoyancy helpers) + +- `PumpDiameter`, `PumpIntakePressure`, `PumpFillageOption`, `PercentPumpFillage` +- `WaterCut`, `WaterSpecGravity`, `FluidLevelOilGravity`, `TubingGradient`, `TubingSize` +- `PercentageUpstrokeTime`, `PercentageDownstrokeTime` +- `PumpingUnitID`, `PumpingSpeedOption` — metadata for future kinematics + +### Units + +- `UnitsSelection` — drives internal conversion (see `units.md`) + +## Unknown / unsupported field policy + +- Parse immediate `Case` children into `rawFields`. +- Fields outside the MVP subset used for **solver** remain in `unsupportedFields` but are **preserved** in API responses. +- Do not silently drop imported tags. + +## Validation rules + +- Required for parse: `PumpingSpeed`, `PumpDepth`, `MeasuredDepthArray`, `InclinationFromVerticalArray`. +- Trajectory arrays must have equal lengths after parse. +- Numeric parse failures throw explicit errors. diff --git a/docs/engineering/field-traceability.md b/docs/engineering/field-traceability.md new file mode 100644 index 0000000..a1eca36 --- /dev/null +++ b/docs/engineering/field-traceability.md @@ -0,0 +1,84 @@ +# Field Traceability Matrix + +Last Updated: 2026-04-17 + +## Scope + +This matrix tracks how `data/cases/base-case.xml` fields flow through the solver: + +1. XML parse (`solver-api/src/xmlParser.js`) +2. SI conversion (`normalizeToSi`) +3. C payload (`solver-api/src/solverClient.js`) +4. Equation/physics use (`solver-c/src/*.c`) + +## Coverage summary (base-case) + +- Total XML fields: **165** +- Fields present in MVP schema and present in base-case: **41** +- Unsupported/preserved fields: **124** + +## A) Fully used in solver equations + +| XML field | Parsed + SI | Payload key(s) | Equation use | +|---|---|---|---| +| `PumpingSpeed` | yes | `pumping_speed` | time step / period in FDM/FEA | +| `PumpDepth` | yes | `pump_depth` | rod length, hydrostatic terms | +| `TubingAnchorLocation` | yes | `tubing_anchor_location` | rod length | +| `RodFrictionCoefficient` | yes | `rod_friction_coefficient` | Coulomb friction scaling | +| `StuffingBoxFriction` | yes | `stuffing_box_friction` | polished-load boundary friction | +| `PumpFriction` | yes | `pump_friction` | downhole boundary friction | +| `MeasuredDepthArray` | yes | `survey_md_m` | trajectory/node mapping | +| `InclinationFromVerticalArray` | yes | `survey_inc_rad` | trajectory + gravity projection | +| `AzimuthFromNorthArray` | yes | `survey_azi_rad` | trajectory curvature | +| `TaperDiameterArray` | yes | `area_m2` | axial stiffness/mass terms | +| `TaperLengthArray` | yes | rod-node assembly | rod section distribution | +| `TaperModulusArray` | yes | `modulus_pa` | stiffness terms | +| `RodTypeArray` | yes | density/modulus defaults | mass/stiffness terms | +| `UpStrokeDampingFactor` | yes | `upstroke_damping` | FEA damping calibration | +| `DownStrokeDampingFactor` | yes | `downstroke_damping` | damping (path-level) | +| `NonDimensionalFluidDamping` | yes | `non_dim_damping` | FEA damping factor | +| `MoldedGuideFrictionRatio` | yes | `molded_guide_mu_scale` | friction scaling | +| `WheeledGuideFrictionRatio` | yes | `wheeled_guide_mu_scale` | friction scaling | +| `OtherGuideFrictionRatio` | yes | `other_guide_mu_scale` | friction scaling | +| `PumpDiameter` | yes | `pump_diameter_m` | chamber pressure / valve logic | +| `PumpIntakePressure` | yes | `pump_intake_pressure_pa` | hydro/chamber pressure | +| `PumpFillageOption` | yes | `pump_fillage_option` | gas-interference logic | +| `PercentPumpFillage` | yes | `percent_pump_fillage` | gas-fraction logic | +| `WaterCut` + `WaterSpecGravity` + `FluidLevelOilGravity` | yes | `fluid_density_kg_m3` | buoyancy/hydrostatic terms | +| `TaperWeightArray` | yes | `weight_n_per_m` | node buoyed-weight in side-load | +| `TaperMTSArray` | yes | `mts_n` | friction scaling term | +| `RodGuideWeightArray` | yes | `guide_weight_n_per_m` | side-load normal-force term | +| `SinkerBarDiameter` + `SinkerBarLength` | yes | `sinker_bar_*` | added sinker side-load contribution | + +## B) Parsed/payloaded but not yet active in governing equations + +| XML field | Current state | Planned use | +|---|---|---| +| `TubingSize` | parsed + converted to `tubing_id_m` + payloaded | annular buoyancy/contact refinements | +| `TubingGradient` | parsed + converted to `tubingGradientPaM`; not payloaded to C math | hydraulic pressure model | +| `PercentageUpstrokeTime` | payloaded (`percent_upstroke_time`) | non-harmonic kinematics timing | +| `PercentageDownstrokeTime` | payloaded (`percent_downstroke_time`) | non-harmonic kinematics timing | +| `PumpingUnitID` | parsed only | pumping-unit geometry/kinematics tables | +| `PumpingSpeedOption` | parsed only | drive/kinematics modes | +| `RodGuideTypeArray` | parsed only | type-specific contact/friction law | + +## C) Unsupported but preserved in API + +All non-MVP fields from base-case remain in `rawFields` and are listed in `unsupportedFields`; they are not currently used in C equations. + +Representative examples: + +- `ActualCounterbalance` +- `BulkModulus` +- `CasingHeadPressure` +- `PumpEfficiency` +- `IPRInputMode` +- `VogalPointList` +- `SeparatorPressure` +- `PowerLineFrequency` + +## D) Contract notes + +- Heavy output blocks (`profiles`, `diagnostics`, `fourierBaseline`) are emitted by C. +- Fourier baseline is computed when `options.enableFourierBaseline=true`. +- Default deterministic golden hash updated after physics/field-wiring changes. diff --git a/docs/engineering/units.md b/docs/engineering/units.md new file mode 100644 index 0000000..4ac409b --- /dev/null +++ b/docs/engineering/units.md @@ -0,0 +1,46 @@ +# Units and internal conventions + +Owner: Rods Project Team +Status: Active +Last Updated: 2026-04-16 + +## Principle + +All values passed from `solver-api` to `solver-c` **JSON** are **SI**: + +| Quantity | SI unit | +|----------|---------| +| Length | m | +| Force | N | +| Pressure | Pa | +| Mass density | kg/m³ | +| Time | s | +| Angle | rad (stored in JSON as rad; XML may be degrees) | +| SPM | 1/min (dimensionally s⁻¹ scale; kept as `pumping_speed` scalar per stroke period \(T = 60/\text{SPM}\) s) | + +## `UnitsSelection` (XML) + +`base-case.xml` uses `2` (example). Parser maps: + +| Code | Assumption in parser | +|------|----------------------| +| `0` / missing | Field units match legacy **oilfield mixed** inch–ft–lbf where applicable (see below) | +| `2` | **Imperial oilfield** — lengths in **ft**, diameters in **in**, moduli in **Mpsi** (×10⁶ psi), forces in **lbf**, pressures in **psi** | +| Other | Treated like `2` with warning in `parsed.warnings` (`heuristic`) | + +### Conversion factors (exact) + +- `1 in = 0.0254 m` +- `1 ft = 0.3048 m` +- `1 lbf = 4.4482216152605 N` +- `1 psi = 6894.757293168 Pa` +- `1 Mpsi = 6.894757293168e9 Pa` +- `deg → rad`: multiply by \(\pi/180\) + +### Fluid density helper + +Mixture density from `WaterCut`, `WaterSpecGravity`, `FluidLevelOilGravity` uses simplified API formula for buoyancy; tagged `heuristic` in `model.fluidDensityHeuristic`. + +## JSON to C + +`SolverInputs` receives only SI. GUI may show field units; API documentation states SI in solve payload. diff --git a/docs/engineering/validation.md b/docs/engineering/validation.md new file mode 100644 index 0000000..5c76f20 --- /dev/null +++ b/docs/engineering/validation.md @@ -0,0 +1,83 @@ +# Validation plan + +Last Updated: 2026-04-16 + +## Determinism + +- Same XML + same `solverModel` + `workflow` → identical `solver.card` arrays (bitwise for floats at printed precision in C stdout path). +- API responses include `runMetadata.generatedAt` — **stripped** before golden hash in tests. + +## Golden file + +- File: `data/golden/default.solve.sha256` +- Content: SHA-256 of **canonical JSON** string from `GET /solve/default?solverModel=fdm` with `generatedAt` removed and stable key ordering (`stableStringify` in tests). + +## Unit coverage + +- XML parser: numeric + array + unit conversion. +- `cardQa.js`: min samples, cycle closure, spike rejection. +- C: `test_solver` — determinism, bounds, static equilibrium helper, undamped wave CFL identity, FDM vs FEA peak tolerance. + +## Cross-model (FDM vs FEA) + +- C unit gate (`solver-c/tests/test_solver.c`): `|maxPolishedLoad_FDM - maxPolishedLoad_FEA| < 7e5 N` on an SI-normalized fixture (interim; tighten when BC parity improves). + +## Integration + +- `solver-api/tests/api.test.js` — solve routes, diagnostic path, golden hash, `validate-card`, `both` comparison keys. +- `comparison` schema v2 includes: + - `peakLoadDeltas` for Pmax/Pmin and Dmax/Dmin + - `pointwiseResiduals.series[]` (position + polished/downhole residuals) + - `residualSummary` RMS + - `fourier` optional baseline block (`null` unless `options.enableFourierBaseline=true`) + +## Extended physics validation + +- `options.enableProfiles=true` must return `solver.profiles` with `nodeCount > 0` and finite trajectory/side-load/friction arrays. +- `options.enableDiagnosticsDetail=true` must return `solver.diagnostics` with valve-state booleans and finite chamber/gas series. +- `options.enableFourierBaseline=true` must return non-null `comparison.fourier` and finite residual RMS metrics. + +## Priority validation roadmap (1–5) + +### 1) Correctness gates (mandatory) + +- Field sensitivity checks: each mapped physics input gets ±1% perturbation and expected directional assertions. +- Invariant checks: + - finite loads/stresses/profiles, + - gas fraction within bounds, + - consistent valve-state transitions, + - no malformed profile/diagnostic arrays. +- Deterministic checks on multi-case golden set. + +### 2) Fidelity checks + +- Equation-backed vs heuristic term audit must be explicit in docs. +- Stability checks under high-deviation/high-friction cases. + +### 3) Cross-model checks + +- Case matrix compares FDM vs FEA on: + - peak polished load, + - peak downhole load, + - net stroke, + - residual RMS. + +### 4) Contract checks + +- Default `/solve` and `/solve/default` responses remain backward compatible. +- Option-gated payload behavior is tested for on/off combinations. + +### 5) CI checks + +- C sanitizers (ASan/UBSan) lane. +- Performance budget lane (fixed representative cases). +- Artifact lane emits comparison/drift summaries. + +## Analytical targets (C tests) + +- **Static rod:** uniform bar vertical hang — numerical mean tension trend vs \(\sum \rho g A \Delta x\) (approximate check). +- **Wave CFL:** \(a \Delta t / \Delta x \le 1\) for explicit scheme after clamp. + +## GUI + +- Smoke: import `base-case.xml`, run solver — manual / Playwright future. diff --git a/gui-ts/Dockerfile b/gui-ts/Dockerfile new file mode 100644 index 0000000..f1b6951 --- /dev/null +++ b/gui-ts/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] diff --git a/gui-ts/index.html b/gui-ts/index.html new file mode 100644 index 0000000..272f248 --- /dev/null +++ b/gui-ts/index.html @@ -0,0 +1,12 @@ + + + + + + Rod Solver GUI + + +
+ + + diff --git a/gui-ts/package-lock.json b/gui-ts/package-lock.json new file mode 100644 index 0000000..45b4194 --- /dev/null +++ b/gui-ts/package-lock.json @@ -0,0 +1,3213 @@ +{ + "name": "gui-ts", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gui-ts", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "uplot": "^1.6.32" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.6.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "jsdom": "^24.1.1", + "typescript": "^5.5.4", + "vite": "^5.3.5", + "vitest": "^2.0.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/electron-to-chromium": { + "version": "1.5.338", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz", + "integrity": "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", + "license": "MIT" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/gui-ts/package.json b/gui-ts/package.json new file mode 100644 index 0000000..5137bfc --- /dev/null +++ b/gui-ts/package.json @@ -0,0 +1,28 @@ +{ + "name": "gui-ts", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "vitest run" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "uplot": "^1.6.32" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.6.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "jsdom": "^24.1.1", + "typescript": "^5.5.4", + "vite": "^5.3.5", + "vitest": "^2.0.4" + } +} diff --git a/gui-ts/src/App.test.tsx b/gui-ts/src/App.test.tsx new file mode 100644 index 0000000..7a29390 --- /dev/null +++ b/gui-ts/src/App.test.tsx @@ -0,0 +1,240 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { App } from "./App"; + +const DEFAULT_CASE = { + model: { + wellName: "191/01-27-007-09W2/00", + company: "Veren", + measuredDepth: [0, 100, 200], + inclination: [0, 10, 20], + azimuth: [0, 90, 180], + pumpingSpeed: 5, + pumpDepth: 1727 + }, + rawFields: { + WellName: "191/01-27-007-09W2/00", + Company: "Veren", + PumpDepth: "1727", + PumpingSpeed: "5", + UnitsSelection: "2", + MeasuredDepthArray: "0:100:200", + InclinationFromVerticalArray: "0:10:20", + AzimuthFromNorthArray: "0:90:180", + TaperDiameterArray: "22.225:19.05", + TaperLengthArray: "800:927", + TaperModulusArray: "30.5:30.5", + RodTypeArray: "3:3", + DesignModeIndex: "0" + }, + unsupportedFields: ["DesignModeIndex"], + warnings: [] +}; + +function mockFetchOk(body: T, init: { ok?: boolean } = {}): Response { + return { + ok: init.ok ?? true, + json: async () => body + } as unknown as Response; +} + +beforeEach(() => { + vi.restoreAllMocks(); + if (typeof window !== "undefined") { + window.history.replaceState(null, "", "/"); + } +}); + +describe("App tabbed shell", () => { + it("hydrates the form from GET /case/default and renders the Well tab by default", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async (url: RequestInfo) => { + if (String(url).endsWith("/case/default")) { + return mockFetchOk(DEFAULT_CASE); + } + throw new Error(`Unexpected fetch: ${String(url)}`); + }) + ); + + render(); + + await waitFor(() => { + const wellInput = screen.getByLabelText(/Well Name/i) as HTMLInputElement; + expect(wellInput.value).toBe("191/01-27-007-09W2/00"); + }); + }); + + it("switches between tabs", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => mockFetchOk(DEFAULT_CASE)) + ); + + render(); + await waitFor(() => screen.getByLabelText(/Well Name/i)); + + fireEvent.click(screen.getByRole("tab", { name: /Trajectory/i })); + expect( + screen.getByText(/Well Trajectory — Survey Table/i) + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: /Rod String/i })); + expect(screen.getByText(/Rod String Taper Sections/i)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: /Advanced/i })); + expect(screen.getByText(/Raw XML fields/i)).toBeInTheDocument(); + }); + + it("submits synthesized XML to POST /solve when the user runs the solver", async () => { + const solveBody = { + schemaVersion: 2, + units: "SI", + parsed: DEFAULT_CASE, + parseWarnings: [], + solver: { + pointCount: 2, + maxPolishedLoad: 10, + minPolishedLoad: 1, + maxDownholeLoad: 9, + minDownholeLoad: 2, + warnings: [], + card: [ + { position: -1, polishedLoad: 1, downholeLoad: 2 }, + { position: 1, polishedLoad: 10, downholeLoad: 9 } + ] + }, + runMetadata: { + deterministic: true, + pointCount: 2, + generatedAt: "2026-04-15T00:00:00.000Z", + solverModel: "fdm", + workflow: "predictive" + } + }; + const calls: Array<{ url: string; init: RequestInit | undefined }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: RequestInfo, init?: RequestInit) => { + calls.push({ url: String(url), init }); + if (String(url).endsWith("/case/default")) return mockFetchOk(DEFAULT_CASE); + if (String(url).endsWith("/solve")) return mockFetchOk(solveBody); + throw new Error(`Unexpected fetch: ${String(url)}`); + }) + ); + + render(); + await waitFor(() => screen.getByLabelText(/Well Name/i)); + + fireEvent.click(screen.getByRole("tab", { name: /^Solver$/i })); + fireEvent.click(screen.getByRole("button", { name: /Run Solver/i })); + + await waitFor(() => + expect(calls.some((c) => c.url.endsWith("/solve") && c.init?.method === "POST")).toBe( + true + ) + ); + + const solveCall = calls.find((c) => c.url.endsWith("/solve")); + const body = JSON.parse(String(solveCall?.init?.body ?? "{}")); + expect(body.solverModel).toBeDefined(); + expect(typeof body.xml).toBe("string"); + expect(body.xml).toContain("191/01-27-007-09W2/00"); + }); + + it("blocks solver run when engineering checks report blocking errors", async () => { + const badCase = { + ...DEFAULT_CASE, + rawFields: { + ...DEFAULT_CASE.rawFields, + PumpDepth: "3000", + TaperLengthArray: "100:100" + } + }; + const calls: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: RequestInfo) => { + calls.push(String(url)); + if (String(url).endsWith("/case/default")) return mockFetchOk(badCase); + return mockFetchOk({}); + }) + ); + + render(); + await waitFor(() => screen.getByLabelText(/Well Name/i)); + fireEvent.click(screen.getByRole("tab", { name: /^Solver$/i })); + const runButton = screen.getByRole("button", { name: /Fix checks to run/i }); + expect(runButton).toBeDisabled(); + expect(calls.some((url) => url.endsWith("/solve"))).toBe(false); + }); + + it("runs diagnostic workflow with validated surface card payload", async () => { + const solveBody = { + schemaVersion: 2, + units: "SI", + parsed: DEFAULT_CASE, + parseWarnings: [], + solver: { + pointCount: 2, + maxPolishedLoad: 10, + minPolishedLoad: 1, + maxDownholeLoad: 9, + minDownholeLoad: 2, + warnings: [], + card: [ + { position: -1, polishedLoad: 1, downholeLoad: 2 }, + { position: 1, polishedLoad: 10, downholeLoad: 9 } + ] + }, + runMetadata: { + deterministic: true, + pointCount: 2, + generatedAt: "2026-04-15T00:00:00.000Z", + solverModel: "fdm", + workflow: "diagnostic" + } + }; + const calls: Array<{ url: string; init: RequestInit | undefined }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: RequestInfo, init?: RequestInit) => { + calls.push({ url: String(url), init }); + if (String(url).endsWith("/case/default")) return mockFetchOk(DEFAULT_CASE); + if (String(url).endsWith("/solve/validate-card")) { + return mockFetchOk({ ok: true, qa: { ok: true }, schemaVersion: 2 }); + } + if (String(url).endsWith("/solve")) return mockFetchOk(solveBody); + throw new Error(`Unexpected fetch: ${String(url)}`); + }) + ); + + render(); + await waitFor(() => screen.getByLabelText(/Well Name/i)); + + fireEvent.click(screen.getByRole("tab", { name: /Kinematics/i })); + fireEvent.change(screen.getByPlaceholderText(/-1.2,12000/i), { + target: { value: "-1,10000\n0,12000\n1,11000\n2,10500" } + }); + fireEvent.click(screen.getByRole("button", { name: /Validate Surface Card/i })); + await waitFor(() => + expect(calls.some((c) => c.url.endsWith("/solve/validate-card"))).toBe(true) + ); + + fireEvent.click(screen.getByRole("tab", { name: /^Solver$/i })); + fireEvent.click(screen.getByRole("radio", { name: /Diagnostic/i })); + fireEvent.click(screen.getByRole("button", { name: /Run Solver/i })); + + await waitFor(() => + expect(calls.some((c) => c.url.endsWith("/solve") && c.init?.method === "POST")).toBe( + true + ) + ); + + const solveCall = calls.find((c) => c.url.endsWith("/solve")); + const body = JSON.parse(String(solveCall?.init?.body ?? "{}")); + expect(body.workflow).toBe("diagnostic"); + expect(Array.isArray(body.surfaceCard.position)).toBe(true); + expect(Array.isArray(body.surfaceCard.load)).toBe(true); + }); +}); diff --git a/gui-ts/src/App.tsx b/gui-ts/src/App.tsx new file mode 100644 index 0000000..0cc0776 --- /dev/null +++ b/gui-ts/src/App.tsx @@ -0,0 +1 @@ +export { App } from "./ui/App"; diff --git a/gui-ts/src/api/client.ts b/gui-ts/src/api/client.ts new file mode 100644 index 0000000..5c26cde --- /dev/null +++ b/gui-ts/src/api/client.ts @@ -0,0 +1,86 @@ +import type { ParsedCase, SolveResponse } from "../types"; + +const API_BASE = + (import.meta as unknown as { env?: { VITE_API_BASE?: string } }).env?.VITE_API_BASE || + "http://localhost:4400"; + +export type SolverModel = "fdm" | "fea" | "both"; +export type Workflow = "predictive" | "diagnostic"; + +export type SurfaceCard = { + position: number[]; + load: number[]; + time?: number[]; +}; + +async function handleJson(resp: Response): Promise { + const body = await resp.json().catch(() => ({})); + if (!resp.ok) { + const message = + (body && typeof body === "object" && "error" in body && typeof body.error === "string" + ? body.error + : null) || `Request failed: HTTP ${resp.status}`; + throw new Error(message); + } + return body as T; +} + +export async function fetchDefaultCase(signal?: AbortSignal): Promise { + const resp = await fetch(`${API_BASE}/case/default`, { signal }); + return handleJson(resp); +} + +export async function parseCaseXmlApi(xml: string, signal?: AbortSignal): Promise { + const resp = await fetch(`${API_BASE}/case/parse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ xml }), + signal + }); + return handleJson(resp); +} + +export type SolveArgs = { + xml: string; + solverModel: SolverModel; + workflow?: Workflow; + surfaceCard?: SurfaceCard; + options?: { + enableProfiles?: boolean; + enableDiagnosticsDetail?: boolean; + enableFourierBaseline?: boolean; + fourierHarmonics?: number; + }; +}; + +export async function solveCase(args: SolveArgs, signal?: AbortSignal): Promise { + const body: Record = { + xml: args.xml, + solverModel: args.solverModel + }; + if (args.workflow) body.workflow = args.workflow; + if (args.surfaceCard) body.surfaceCard = args.surfaceCard; + if (args.options) body.options = args.options; + const resp = await fetch(`${API_BASE}/solve`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal + }); + return handleJson(resp); +} + +export async function validateSurfaceCard( + surfaceCard: SurfaceCard, + signal?: AbortSignal +): Promise<{ ok: boolean; qa: Record; schemaVersion: number }> { + const resp = await fetch(`${API_BASE}/solve/validate-card`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ surfaceCard }), + signal + }); + return handleJson(resp); +} + +export { API_BASE }; diff --git a/gui-ts/src/main.tsx b/gui-ts/src/main.tsx new file mode 100644 index 0000000..bd34b2a --- /dev/null +++ b/gui-ts/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/gui-ts/src/state/__tests__/engineeringChecks.test.ts b/gui-ts/src/state/__tests__/engineeringChecks.test.ts new file mode 100644 index 0000000..f851d35 --- /dev/null +++ b/gui-ts/src/state/__tests__/engineeringChecks.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { EMPTY_CASE_STATE } from "../caseModel"; +import { + DLS_BAD_SECTION_THRESHOLD, + PUMP_ROD_MISMATCH_M, + runEngineeringChecks +} from "../engineeringChecks"; + +describe("engineering checks fixed thresholds", () => { + it("blocks run when pump depth and rod length mismatch exceeds 15 m", () => { + const state = { + ...EMPTY_CASE_STATE, + pumpDepth: 1000, + taper: [ + { diameter: 19.05, length: 980, modulus: 30.5, rodType: 3 } + ], + survey: [ + { md: 0, inc: 0, azi: 0 }, + { md: 1000, inc: 0, azi: 0 } + ] + }; + + const checks = runEngineeringChecks(state); + expect(PUMP_ROD_MISMATCH_M).toBe(15); + expect(checks.hasBlockingError).toBe(true); + expect(checks.issues.some((i) => i.code === "PUMP_ROD_MISMATCH_15M")).toBe(true); + }); + + it("flags DLS warning above 15 deg/100 threshold", () => { + const state = { + ...EMPTY_CASE_STATE, + pumpDepth: 1000, + taper: [ + { diameter: 19.05, length: 1000, modulus: 30.5, rodType: 3 } + ], + survey: [ + { md: 0, inc: 0, azi: 0 }, + { md: 100, inc: 20, azi: 0 }, + { md: 200, inc: 45, azi: 180 } + ] + }; + + const checks = runEngineeringChecks(state); + expect(DLS_BAD_SECTION_THRESHOLD).toBe(15); + expect(checks.issues.some((i) => i.code === "DLS_HIGH")).toBe(true); + }); +}); + diff --git a/gui-ts/src/state/__tests__/xmlExport.test.ts b/gui-ts/src/state/__tests__/xmlExport.test.ts new file mode 100644 index 0000000..160f76d --- /dev/null +++ b/gui-ts/src/state/__tests__/xmlExport.test.ts @@ -0,0 +1,111 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import type { ParsedCase } from "../../types"; +import { hydrateFromParsed } from "../xmlImport"; +import { serializeCaseXml } from "../xmlExport"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const BASE_CASE_XML = path.resolve(HERE, "../../../../data/cases/base-case.xml"); + +/** + * Minimal ParsedCase builder that extracts text content of every direct child + * of the element. Mirrors the subset of `xml2js` behaviour that the + * GUI import path depends on (strings only, no xml2js attr bag here since + * round-trip on first-class fields does not require attribute preservation). + */ +function parseWithDom(xmlText: string): ParsedCase { + const doc = new DOMParser().parseFromString(xmlText, "application/xml"); + const parseErr = doc.getElementsByTagName("parsererror"); + if (parseErr.length) throw new Error(parseErr[0].textContent ?? "XML parse error"); + + const caseNode = doc.getElementsByTagName("Case")[0]; + if (!caseNode) throw new Error("missing Case element"); + + const rawFields: Record = {}; + for (let i = 0; i < caseNode.children.length; i += 1) { + const child = caseNode.children[i]; + rawFields[child.tagName] = (child.textContent ?? "").trim(); + } + + return { + model: { + wellName: rawFields.WellName ?? "", + company: rawFields.Company ?? "", + pumpingSpeed: Number(rawFields.PumpingSpeed ?? 0), + pumpDepth: Number(rawFields.PumpDepth ?? 0), + measuredDepth: [], + inclination: [], + azimuth: [] + }, + rawFields, + unsupportedFields: [] + }; +} + +describe("xmlExport round-trip", () => { + const xmlText = fs.readFileSync(BASE_CASE_XML, "utf-8"); + + function raw(parsed: ParsedCase, key: string): string { + const value = parsed.rawFields[key]; + return typeof value === "string" ? value : ""; + } + + it("preserves every first-class field through hydrate -> serialize", () => { + const parsed = parseWithDom(xmlText); + const state = hydrateFromParsed(parsed); + const exportedXml = serializeCaseXml(state); + const reparsed = parseWithDom(exportedXml); + + // Well metadata + expect(raw(reparsed, "WellName")).toBe(raw(parsed, "WellName")); + expect(raw(reparsed, "Company")).toBe(raw(parsed, "Company")); + + // Numeric first-class fields + expect(Number(raw(reparsed, "PumpDepth"))).toBe(Number(raw(parsed, "PumpDepth"))); + expect(Number(raw(reparsed, "PumpingSpeed"))).toBe(Number(raw(parsed, "PumpingSpeed"))); + expect(Number(raw(reparsed, "WaterCut"))).toBe(Number(raw(parsed, "WaterCut"))); + expect(Number(raw(reparsed, "UnitsSelection"))).toBe( + Number(raw(parsed, "UnitsSelection")) + ); + + // Arrays preserved (value-wise — original has trailing zeros we may have stripped) + const origMd = raw(parsed, "MeasuredDepthArray").split(":").map(Number); + const reMd = raw(reparsed, "MeasuredDepthArray").split(":").map(Number); + expect(reMd).toEqual(origMd); + + const origInc = raw(parsed, "InclinationFromVerticalArray").split(":").map(Number); + const reInc = raw(reparsed, "InclinationFromVerticalArray").split(":").map(Number); + expect(reInc).toEqual(origInc); + + const origTaperD = raw(parsed, "TaperDiameterArray").split(":").map(Number); + const reTaperD = raw(reparsed, "TaperDiameterArray").split(":").map(Number); + expect(reTaperD).toEqual(origTaperD); + }); + + it("preserves unsupported / untouched fields verbatim", () => { + const parsed = parseWithDom(xmlText); + const state = hydrateFromParsed(parsed); + const exportedXml = serializeCaseXml(state); + const reparsed = parseWithDom(exportedXml); + + // Representative fields not first-class in the GUI. + expect(raw(reparsed, "Analyst")).toBe(raw(parsed, "Analyst")); + expect(raw(reparsed, "CrankHole")).toBe(raw(parsed, "CrankHole")); + expect(raw(reparsed, "MoldedGuideType")).toBe(raw(parsed, "MoldedGuideType")); + expect(raw(reparsed, "Version")).toBe(raw(parsed, "Version")); + expect(raw(reparsed, "IPRInputMode")).toBe(raw(parsed, "IPRInputMode")); + }); + + it("reflects user edits in the serialized XML", () => { + const parsed = parseWithDom(xmlText); + const state = hydrateFromParsed(parsed); + const edited = { ...state, wellName: "CHANGED-WELL", pumpingSpeed: 7.5 }; + const exportedXml = serializeCaseXml(edited); + const reparsed = parseWithDom(exportedXml); + + expect(raw(reparsed, "WellName")).toBe("CHANGED-WELL"); + expect(Number(raw(reparsed, "PumpingSpeed"))).toBe(7.5); + }); +}); diff --git a/gui-ts/src/state/caseModel.ts b/gui-ts/src/state/caseModel.ts new file mode 100644 index 0000000..c83096f --- /dev/null +++ b/gui-ts/src/state/caseModel.ts @@ -0,0 +1,179 @@ +/** + * Canonical GUI-side case model. + * + * `CaseState` mirrors the first-class fields produced by + * `solver-api/src/xmlParser.js#parseCaseXml` (SI), plus an escape-hatch + * `rawFields` map holding every original XML element keyed by its tag name. + * + * The GUI is the source of truth while editing; `serializeCaseXml` in + * `./xmlExport.ts` converts it back into the XML document shape that + * `POST /solve` expects. + */ + +export type RawFieldValue = string | Record | undefined; + +export type TaperRow = { + /** Taper diameter in the XML's native unit (mm in base case). */ + diameter: number; + /** Taper length in the XML's native unit (typically metres / feet mix). */ + length: number; + /** Young's modulus in Mpsi (base case stores 30.5). */ + modulus: number; + /** Rod type code (0=steel, 3=fiberglass, 2=sinker, etc.). */ + rodType: number; +}; + +export type SurveyRow = { + md: number; + inc: number; + azi: number; +}; + +export type CaseState = { + // --- Well / metadata --- + wellName: string; + company: string; + + // --- Depths / tubing --- + pumpDepth: number; + tubingAnchorLocation: number; + tubingSize: number; + + // --- Kinematics / surface BC --- + pumpingSpeed: number; + pumpingSpeedOption: number; + pumpingUnitId: string; + + // --- Trajectory --- + survey: SurveyRow[]; + + // --- Rod string --- + taper: TaperRow[]; + + // --- Pump --- + pumpDiameter: number; + pumpFriction: number; + pumpIntakePressure: number; + pumpFillageOption: number; + percentPumpFillage: number; + percentUpstrokeTime: number; + percentDownstrokeTime: number; + + // --- Fluid --- + waterCut: number; + waterSpecGravity: number; + fluidLevelOilGravity: number; + tubingGradient: number; + + // --- Friction / damping --- + rodFrictionCoefficient: number; + stuffingBoxFriction: number; + moldedGuideFrictionRatio: number; + wheeledGuideFrictionRatio: number; + otherGuideFrictionRatio: number; + upStrokeDamping: number; + downStrokeDamping: number; + nonDimensionalFluidDamping: number; + + // --- Units --- + unitsSelection: number; + + // --- Escape hatch: every raw child element, preserved verbatim. --- + rawFields: Record; + + /** + * Insertion order of fields in the originally loaded XML. Preserved so + * exports produce readable diffs against the input file. + */ + rawFieldOrder: string[]; +}; + +/** + * Runtime settings that are not part of the XML case file but that the GUI + * needs to send to the solver API. + */ +export type RunSettings = { + solverModel: "fdm" | "fea" | "both"; + workflow: "predictive" | "diagnostic"; +}; + +export const INITIAL_RUN_SETTINGS: RunSettings = { + solverModel: "both", + workflow: "predictive" +}; + +export const EMPTY_CASE_STATE: CaseState = { + wellName: "", + company: "", + pumpDepth: 0, + tubingAnchorLocation: 0, + tubingSize: 0, + pumpingSpeed: 0, + pumpingSpeedOption: 0, + pumpingUnitId: "", + survey: [], + taper: [], + pumpDiameter: 0, + pumpFriction: 0, + pumpIntakePressure: 0, + pumpFillageOption: 0, + percentPumpFillage: 0, + percentUpstrokeTime: 50, + percentDownstrokeTime: 50, + waterCut: 0, + waterSpecGravity: 1, + fluidLevelOilGravity: 35, + tubingGradient: 0, + rodFrictionCoefficient: 0, + stuffingBoxFriction: 0, + moldedGuideFrictionRatio: 1, + wheeledGuideFrictionRatio: 1, + otherGuideFrictionRatio: 1, + upStrokeDamping: 0, + downStrokeDamping: 0, + nonDimensionalFluidDamping: 0, + unitsSelection: 0, + rawFields: {}, + rawFieldOrder: [] +}; + +/** Fields that `serializeCaseXml` writes explicitly (and should therefore not be duplicated from rawFields). */ +export const FIRST_CLASS_XML_KEYS = [ + "WellName", + "Company", + "PumpDepth", + "TubingAnchorLocation", + "TubingSize", + "PumpingSpeed", + "PumpingSpeedOption", + "PumpingUnitID", + "MeasuredDepthArray", + "InclinationFromVerticalArray", + "AzimuthFromNorthArray", + "TaperDiameterArray", + "TaperLengthArray", + "TaperModulusArray", + "RodTypeArray", + "PumpDiameter", + "PumpFriction", + "PumpIntakePressure", + "PumpFillageOption", + "PercentPumpFillage", + "PercentageUpstrokeTime", + "PercentageDownstrokeTime", + "WaterCut", + "WaterSpecGravity", + "FluidLevelOilGravity", + "TubingGradient", + "RodFrictionCoefficient", + "StuffingBoxFriction", + "MoldedGuideFrictionRatio", + "WheeledGuideFrictionRatio", + "OtherGuideFrictionRatio", + "UpStrokeDampingFactor", + "DownStrokeDampingFactor", + "NonDimensionalFluidDamping", + "UnitsSelection" +] as const; + +export type FirstClassXmlKey = (typeof FIRST_CLASS_XML_KEYS)[number]; diff --git a/gui-ts/src/state/engineeringChecks.ts b/gui-ts/src/state/engineeringChecks.ts new file mode 100644 index 0000000..bfa7d65 --- /dev/null +++ b/gui-ts/src/state/engineeringChecks.ts @@ -0,0 +1,83 @@ +import type { CaseState } from "./caseModel"; +import { computeDoglegSeverityDegPer100 } from "./trajectoryMetrics"; + +export const PUMP_ROD_MISMATCH_M = 15; +export const DLS_BAD_SECTION_THRESHOLD = 15; + +export type EngineeringIssue = { + severity: "warning" | "error"; + code: string; + message: string; +}; + +export type EngineeringChecks = { + issues: EngineeringIssue[]; + hasBlockingError: boolean; +}; + +export function runEngineeringChecks(state: CaseState): EngineeringChecks { + const issues: EngineeringIssue[] = []; + + const activeTaper = state.taper.filter((t) => Number.isFinite(t.length) && t.length > 0); + const rodTotal = activeTaper.reduce((acc, t) => acc + t.length, 0); + const pumpDepth = state.pumpDepth; + if (rodTotal > 0 && pumpDepth > 0) { + const diff = Math.abs(pumpDepth - rodTotal); + if (diff > PUMP_ROD_MISMATCH_M) { + issues.push({ + severity: "error", + code: "PUMP_ROD_MISMATCH_15M", + message: `Pump depth (${pumpDepth.toFixed(1)}) and total rod length (${rodTotal.toFixed( + 1 + )}) differ by ${diff.toFixed(1)} m (> ${PUMP_ROD_MISMATCH_M} m limit).` + }); + } + } + + if (state.survey.length < 2) { + issues.push({ + severity: "error", + code: "SURVEY_TOO_SHORT", + message: "Trajectory needs at least 2 survey stations." + }); + } else { + let nonMonotonic = false; + let maxDls = 0; + for (let i = 1; i < state.survey.length; i += 1) { + if (state.survey[i].md <= state.survey[i - 1].md) nonMonotonic = true; + maxDls = Math.max(maxDls, computeDoglegSeverityDegPer100(state.survey[i - 1], state.survey[i])); + } + if (nonMonotonic) { + issues.push({ + severity: "error", + code: "SURVEY_MD_NON_MONOTONIC", + message: "Measured depth must strictly increase between survey stations." + }); + } + if (maxDls > DLS_BAD_SECTION_THRESHOLD) { + issues.push({ + severity: "warning", + code: "DLS_HIGH", + message: `High dogleg severity detected (max ${maxDls.toFixed( + 2 + )} deg/100 > ${DLS_BAD_SECTION_THRESHOLD} deg/100 bad-section threshold).` + }); + } + const maxMd = state.survey[state.survey.length - 1].md; + if (pumpDepth > 0 && maxMd > 0 && maxMd < pumpDepth - 10) { + issues.push({ + severity: "warning", + code: "SURVEY_BELOW_PUMP_MISSING", + message: `Trajectory ends at MD ${maxMd.toFixed( + 1 + )}, shallower than pump depth ${pumpDepth.toFixed(1)}.` + }); + } + } + + return { + issues, + hasBlockingError: issues.some((issue) => issue.severity === "error") + }; +} + diff --git a/gui-ts/src/state/trajectoryMetrics.ts b/gui-ts/src/state/trajectoryMetrics.ts new file mode 100644 index 0000000..41d7e25 --- /dev/null +++ b/gui-ts/src/state/trajectoryMetrics.ts @@ -0,0 +1,73 @@ +import type { SurveyRow } from "./caseModel"; + +export type TrajectoryPoint3D = { x: number; y: number; z: number; md: number }; +export type TrajectorySegment = { + index: number; + a: TrajectoryPoint3D; + b: TrajectoryPoint3D; + dMd: number; + dls: number; +}; + +export function computeDoglegSeverityDegPer100(rowA: SurveyRow, rowB: SurveyRow): number { + const dMd = rowB.md - rowA.md; + if (!Number.isFinite(dMd) || dMd <= 1e-6) return 0; + const inc1 = (rowA.inc * Math.PI) / 180; + const inc2 = (rowB.inc * Math.PI) / 180; + const azi1 = (rowA.azi * Math.PI) / 180; + const azi2 = (rowB.azi * Math.PI) / 180; + const cosDogleg = + Math.cos(inc1) * Math.cos(inc2) + Math.sin(inc1) * Math.sin(inc2) * Math.cos(azi2 - azi1); + const clamped = Math.min(1, Math.max(-1, cosDogleg)); + const doglegDeg = (Math.acos(clamped) * 180) / Math.PI; + return (doglegDeg / dMd) * 100; +} + +export function buildTrajectorySegments(survey: SurveyRow[]): TrajectorySegment[] { + if (survey.length < 2) return []; + const points: TrajectoryPoint3D[] = [{ x: 0, y: 0, z: 0, md: survey[0].md }]; + for (let i = 1; i < survey.length; i += 1) { + const prev = survey[i - 1]; + const curr = survey[i]; + const dMd = Math.max(curr.md - prev.md, 0); + const incRad = (curr.inc * Math.PI) / 180; + const azRad = (curr.azi * Math.PI) / 180; + const dx = dMd * Math.sin(incRad) * Math.sin(azRad); + const dy = dMd * Math.sin(incRad) * Math.cos(azRad); + const dz = dMd * Math.cos(incRad); + const last = points[points.length - 1]; + points.push({ x: last.x + dx, y: last.y + dy, z: last.z + dz, md: curr.md }); + } + const segments: TrajectorySegment[] = []; + for (let i = 1; i < points.length; i += 1) { + segments.push({ + index: i - 1, + a: points[i - 1], + b: points[i], + dMd: Math.max(points[i].md - points[i - 1].md, 0), + dls: computeDoglegSeverityDegPer100(survey[i - 1], survey[i]) + }); + } + return segments; +} + +export function interpolateAlongMd( + segments: TrajectorySegment[], + mdTarget: number +): TrajectoryPoint3D | null { + if (!segments.length) return null; + for (const segment of segments) { + if (mdTarget >= segment.a.md && mdTarget <= segment.b.md) { + const span = Math.max(segment.b.md - segment.a.md, 1e-9); + const t = (mdTarget - segment.a.md) / span; + return { + x: segment.a.x + (segment.b.x - segment.a.x) * t, + y: segment.a.y + (segment.b.y - segment.a.y) * t, + z: segment.a.z + (segment.b.z - segment.a.z) * t, + md: mdTarget + }; + } + } + return { ...segments[segments.length - 1].b }; +} + diff --git a/gui-ts/src/state/useCaseStore.ts b/gui-ts/src/state/useCaseStore.ts new file mode 100644 index 0000000..1df65f1 --- /dev/null +++ b/gui-ts/src/state/useCaseStore.ts @@ -0,0 +1,130 @@ +import { useCallback, useMemo, useState } from "react"; +import type { CaseState, SurveyRow, TaperRow } from "./caseModel"; +import { EMPTY_CASE_STATE } from "./caseModel"; + +export type CaseStore = { + state: CaseState; + setState: (next: CaseState) => void; + update: (key: K, value: CaseState[K]) => void; + setSurvey: (rows: SurveyRow[]) => void; + addSurveyRow: (row?: Partial) => void; + removeSurveyRow: (index: number) => void; + updateSurveyRow: (index: number, patch: Partial) => void; + setTaper: (rows: TaperRow[]) => void; + addTaperRow: (row?: Partial) => void; + removeTaperRow: (index: number) => void; + updateTaperRow: (index: number, patch: Partial) => void; + setRawField: (key: string, value: string) => void; +}; + +export function useCaseStore(initial: CaseState = EMPTY_CASE_STATE): CaseStore { + const [state, setStateInternal] = useState(initial); + + const setState = useCallback((next: CaseState) => setStateInternal(next), []); + + const update = useCallback( + (key: K, value: CaseState[K]) => { + setStateInternal((prev) => ({ ...prev, [key]: value })); + }, + [] + ); + + const setSurvey = useCallback((rows: SurveyRow[]) => { + setStateInternal((prev) => ({ ...prev, survey: rows })); + }, []); + + const addSurveyRow = useCallback((row: Partial = {}) => { + setStateInternal((prev) => ({ + ...prev, + survey: [...prev.survey, { md: row.md ?? 0, inc: row.inc ?? 0, azi: row.azi ?? 0 }] + })); + }, []); + + const removeSurveyRow = useCallback((index: number) => { + setStateInternal((prev) => ({ + ...prev, + survey: prev.survey.filter((_, i) => i !== index) + })); + }, []); + + const updateSurveyRow = useCallback((index: number, patch: Partial) => { + setStateInternal((prev) => ({ + ...prev, + survey: prev.survey.map((row, i) => (i === index ? { ...row, ...patch } : row)) + })); + }, []); + + const setTaper = useCallback((rows: TaperRow[]) => { + setStateInternal((prev) => ({ ...prev, taper: rows })); + }, []); + + const addTaperRow = useCallback((row: Partial = {}) => { + setStateInternal((prev) => ({ + ...prev, + taper: [ + ...prev.taper, + { + diameter: row.diameter ?? 0, + length: row.length ?? 0, + modulus: row.modulus ?? 30.5, + rodType: row.rodType ?? 0 + } + ] + })); + }, []); + + const removeTaperRow = useCallback((index: number) => { + setStateInternal((prev) => ({ + ...prev, + taper: prev.taper.filter((_, i) => i !== index) + })); + }, []); + + const updateTaperRow = useCallback((index: number, patch: Partial) => { + setStateInternal((prev) => ({ + ...prev, + taper: prev.taper.map((row, i) => (i === index ? { ...row, ...patch } : row)) + })); + }, []); + + const setRawField = useCallback((key: string, value: string) => { + setStateInternal((prev) => { + const nextRaw = { ...prev.rawFields, [key]: value }; + const order = prev.rawFieldOrder.includes(key) + ? prev.rawFieldOrder + : [...prev.rawFieldOrder, key]; + return { ...prev, rawFields: nextRaw, rawFieldOrder: order }; + }); + }, []); + + return useMemo( + () => ({ + state, + setState, + update, + setSurvey, + addSurveyRow, + removeSurveyRow, + updateSurveyRow, + setTaper, + addTaperRow, + removeTaperRow, + updateTaperRow, + setRawField + }), + [ + state, + setState, + update, + setSurvey, + addSurveyRow, + removeSurveyRow, + updateSurveyRow, + setTaper, + addTaperRow, + removeTaperRow, + updateTaperRow, + setRawField + ] + ); +} diff --git a/gui-ts/src/state/xmlExport.ts b/gui-ts/src/state/xmlExport.ts new file mode 100644 index 0000000..e37847d --- /dev/null +++ b/gui-ts/src/state/xmlExport.ts @@ -0,0 +1,160 @@ +import type { CaseState, RawFieldValue } from "./caseModel"; +import { FIRST_CLASS_XML_KEYS } from "./caseModel"; + +/** + * Serialize a CaseState back into the XML document shape expected by + * `POST /solve`. Preserves original field ordering when available and keeps + * untouched `rawFields` verbatim. + */ +export function serializeCaseXml(state: CaseState): string { + const firstClassSet = new Set(FIRST_CLASS_XML_KEYS); + const firstClassValues = buildFirstClassMap(state); + + // Preserve original order, then append any newly-added fields. + const order: string[] = []; + const seen = new Set(); + for (const key of state.rawFieldOrder) { + if (!seen.has(key)) { + order.push(key); + seen.add(key); + } + } + for (const key of FIRST_CLASS_XML_KEYS) { + if (!seen.has(key)) { + order.push(key); + seen.add(key); + } + } + for (const key of Object.keys(state.rawFields)) { + if (!seen.has(key)) { + order.push(key); + seen.add(key); + } + } + + const lines: string[] = []; + lines.push(''); + lines.push(""); + lines.push(" "); + for (const key of order) { + const firstClass = firstClassValues.get(key); + if (firstClass !== undefined) { + lines.push(` ${renderElement(key, firstClass, null)}`); + } else if (firstClassSet.has(key)) { + // First-class key but no explicit value mapped — skip. + continue; + } else { + const raw = state.rawFields[key]; + lines.push(` ${renderElement(key, textOf(raw), attrsOf(raw))}`); + } + } + lines.push(" "); + lines.push(""); + return lines.join("\n") + "\n"; +} + +function buildFirstClassMap(state: CaseState): Map { + const m = new Map(); + m.set("WellName", state.wellName); + m.set("Company", state.company); + m.set("PumpDepth", formatNumber(state.pumpDepth)); + m.set("TubingAnchorLocation", formatNumber(state.tubingAnchorLocation)); + m.set("TubingSize", formatNumber(state.tubingSize)); + m.set("PumpingSpeed", formatNumber(state.pumpingSpeed)); + m.set("PumpingSpeedOption", formatNumber(state.pumpingSpeedOption)); + m.set("PumpingUnitID", state.pumpingUnitId); + + m.set("MeasuredDepthArray", serializeColonArray(state.survey.map((r) => r.md))); + m.set("InclinationFromVerticalArray", serializeColonArray(state.survey.map((r) => r.inc))); + m.set("AzimuthFromNorthArray", serializeColonArray(state.survey.map((r) => r.azi))); + + m.set("TaperDiameterArray", serializeColonArray(state.taper.map((r) => r.diameter))); + m.set("TaperLengthArray", serializeColonArray(state.taper.map((r) => r.length))); + m.set("TaperModulusArray", serializeColonArray(state.taper.map((r) => r.modulus))); + m.set("RodTypeArray", serializeColonArray(state.taper.map((r) => r.rodType))); + + m.set("PumpDiameter", formatNumber(state.pumpDiameter)); + m.set("PumpFriction", formatNumber(state.pumpFriction)); + m.set("PumpIntakePressure", formatNumber(state.pumpIntakePressure)); + m.set("PumpFillageOption", formatNumber(state.pumpFillageOption)); + m.set("PercentPumpFillage", formatNumber(state.percentPumpFillage)); + m.set("PercentageUpstrokeTime", formatNumber(state.percentUpstrokeTime)); + m.set("PercentageDownstrokeTime", formatNumber(state.percentDownstrokeTime)); + + m.set("WaterCut", formatNumber(state.waterCut)); + m.set("WaterSpecGravity", formatNumber(state.waterSpecGravity)); + m.set("FluidLevelOilGravity", formatNumber(state.fluidLevelOilGravity)); + m.set("TubingGradient", formatNumber(state.tubingGradient)); + + m.set("RodFrictionCoefficient", formatNumber(state.rodFrictionCoefficient)); + m.set("StuffingBoxFriction", formatNumber(state.stuffingBoxFriction)); + m.set("MoldedGuideFrictionRatio", formatNumber(state.moldedGuideFrictionRatio)); + m.set("WheeledGuideFrictionRatio", formatNumber(state.wheeledGuideFrictionRatio)); + m.set("OtherGuideFrictionRatio", formatNumber(state.otherGuideFrictionRatio)); + m.set("UpStrokeDampingFactor", formatNumber(state.upStrokeDamping)); + m.set("DownStrokeDampingFactor", formatNumber(state.downStrokeDamping)); + m.set("NonDimensionalFluidDamping", formatNumber(state.nonDimensionalFluidDamping)); + + m.set("UnitsSelection", formatNumber(state.unitsSelection)); + return m; +} + +function textOf(value: RawFieldValue): string { + if (value === undefined || value === null) return ""; + if (typeof value === "string") return value; + if (typeof value === "object") { + const obj = value as Record; + if (typeof obj._ === "string") return obj._; + } + return ""; +} + +function attrsOf(value: RawFieldValue): Record | null { + if (value && typeof value === "object") { + const attrs = (value as Record).$; + if (attrs && typeof attrs === "object") { + const out: Record = {}; + for (const [k, v] of Object.entries(attrs as Record)) { + out[k] = String(v); + } + return out; + } + } + return null; +} + +function renderElement( + tag: string, + text: string, + attrs: Record | null +): string { + const attrStr = attrs + ? Object.entries(attrs) + .map(([k, v]) => ` ${k}="${escapeXml(v)}"`) + .join("") + : ""; + if (!text) { + return `<${tag}${attrStr} />`; + } + return `<${tag}${attrStr}>${escapeXml(text)}`; +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function formatNumber(value: number): string { + if (!Number.isFinite(value)) return "0"; + if (Number.isInteger(value)) return String(value); + // Trim trailing zeros while keeping up to 6 decimals to match base-case style. + const fixed = value.toFixed(6); + return fixed.replace(/\.?0+$/, ""); +} + +function serializeColonArray(values: number[]): string { + if (values.length === 0) return "0"; + return values.map((v) => formatNumber(v)).join(":"); +} diff --git a/gui-ts/src/state/xmlImport.ts b/gui-ts/src/state/xmlImport.ts new file mode 100644 index 0000000..f1fa7ac --- /dev/null +++ b/gui-ts/src/state/xmlImport.ts @@ -0,0 +1,145 @@ +import type { ParsedCase } from "../types"; +import { + EMPTY_CASE_STATE, + type CaseState, + type RawFieldValue, + type SurveyRow, + type TaperRow +} from "./caseModel"; + +/** Flatten xml2js node to its text content (preserves attr bag in '$' if present). */ +function textOf(value: RawFieldValue): string { + if (value === undefined || value === null) return ""; + if (typeof value === "string") return value; + if (typeof value === "object") { + const obj = value as Record; + if (typeof obj._ === "string") return obj._; + if (Object.keys(obj).length === 1 && "$" in obj) return ""; + } + return ""; +} + +function numberOf(value: RawFieldValue, fallback = 0): number { + const text = textOf(value).trim(); + if (!text) return fallback; + const n = Number(text); + return Number.isFinite(n) ? n : fallback; +} + +function stringOf(value: RawFieldValue, fallback = ""): string { + const text = textOf(value).trim(); + return text || fallback; +} + +function parseColonArray(value: RawFieldValue): number[] { + const text = textOf(value); + if (!text) return []; + return text + .split(":") + .map((piece) => piece.trim()) + .filter((piece) => piece.length > 0) + .map((piece) => Number(piece)) + .filter((n) => Number.isFinite(n)); +} + +/** + * Hydrate a CaseState from the `parsed` block returned by the solver-api. + * Values come primarily from `rawFields` (pre-normalization) so the GUI + * edits the XML-native units directly. `parsed.model` is used only as a + * fallback when `rawFields` lacks an entry. + */ +export function hydrateFromParsed(parsed: ParsedCase): CaseState { + const raw = (parsed.rawFields ?? {}) as Record; + const model = parsed.model ?? ({} as ParsedCase["model"]); + const rawFieldOrder = Object.keys(raw); + + const md = parseColonArray(raw.MeasuredDepthArray); + const inc = parseColonArray(raw.InclinationFromVerticalArray); + const azi = parseColonArray(raw.AzimuthFromNorthArray); + const surveyLen = Math.max(md.length, inc.length, azi.length); + const survey: SurveyRow[] = []; + for (let i = 0; i < surveyLen; i += 1) { + survey.push({ + md: md[i] ?? 0, + inc: inc[i] ?? 0, + azi: azi[i] ?? 0 + }); + } + + const diam = parseColonArray(raw.TaperDiameterArray); + const length = parseColonArray(raw.TaperLengthArray); + const modulus = parseColonArray(raw.TaperModulusArray); + const rodType = parseColonArray(raw.RodTypeArray); + const taperLen = Math.max(diam.length, length.length, modulus.length, rodType.length); + const taper: TaperRow[] = []; + for (let i = 0; i < taperLen; i += 1) { + // Stop appending "zero" rows once we've passed the meaningful entries; + // TaperCount is the authoritative limit but we keep all rows to preserve + // round-trip exactly. + taper.push({ + diameter: diam[i] ?? 0, + length: length[i] ?? 0, + modulus: modulus[i] ?? 0, + rodType: rodType[i] ?? 0 + }); + } + + return { + ...EMPTY_CASE_STATE, + wellName: stringOf(raw.WellName, model.wellName ?? ""), + company: stringOf(raw.Company, model.company ?? ""), + + pumpDepth: numberOf(raw.PumpDepth, model.pumpDepth ?? 0), + tubingAnchorLocation: numberOf(raw.TubingAnchorLocation, model.tubingAnchorLocation ?? 0), + tubingSize: numberOf(raw.TubingSize, model.tubingSize ?? 0), + + pumpingSpeed: numberOf(raw.PumpingSpeed, model.pumpingSpeed ?? 0), + pumpingSpeedOption: numberOf(raw.PumpingSpeedOption, model.pumpingSpeedOption ?? 0), + pumpingUnitId: stringOf(raw.PumpingUnitID, model.pumpingUnitId ?? ""), + + survey, + taper, + + pumpDiameter: numberOf(raw.PumpDiameter, model.pumpDiameter ?? 0), + pumpFriction: numberOf(raw.PumpFriction, model.pumpFriction ?? 0), + pumpIntakePressure: numberOf(raw.PumpIntakePressure, model.pumpIntakePressure ?? 0), + pumpFillageOption: numberOf(raw.PumpFillageOption, model.pumpFillageOption ?? 0), + percentPumpFillage: numberOf(raw.PercentPumpFillage, model.percentPumpFillage ?? 0), + percentUpstrokeTime: numberOf(raw.PercentageUpstrokeTime, model.percentUpstrokeTime ?? 50), + percentDownstrokeTime: numberOf(raw.PercentageDownstrokeTime, model.percentDownstrokeTime ?? 50), + + waterCut: numberOf(raw.WaterCut, model.waterCut ?? 0), + waterSpecGravity: numberOf(raw.WaterSpecGravity, model.waterSpecGravity ?? 1), + fluidLevelOilGravity: numberOf(raw.FluidLevelOilGravity, model.fluidLevelOilGravity ?? 35), + tubingGradient: numberOf(raw.TubingGradient, model.tubingGradient ?? 0), + + rodFrictionCoefficient: numberOf( + raw.RodFrictionCoefficient, + model.rodFrictionCoefficient ?? 0 + ), + stuffingBoxFriction: numberOf(raw.StuffingBoxFriction, model.stuffingBoxFriction ?? 0), + moldedGuideFrictionRatio: numberOf( + raw.MoldedGuideFrictionRatio, + model.moldedGuideFrictionRatio ?? 1 + ), + wheeledGuideFrictionRatio: numberOf( + raw.WheeledGuideFrictionRatio, + model.wheeledGuideFrictionRatio ?? 1 + ), + otherGuideFrictionRatio: numberOf( + raw.OtherGuideFrictionRatio, + model.otherGuideFrictionRatio ?? 1 + ), + upStrokeDamping: numberOf(raw.UpStrokeDampingFactor, model.upStrokeDamping ?? 0), + downStrokeDamping: numberOf(raw.DownStrokeDampingFactor, model.downStrokeDamping ?? 0), + nonDimensionalFluidDamping: numberOf( + raw.NonDimensionalFluidDamping, + model.nonDimensionalFluidDamping ?? 0 + ), + + unitsSelection: numberOf(raw.UnitsSelection, model.unitsSelection ?? 0), + + rawFields: { ...raw }, + rawFieldOrder + }; +} diff --git a/gui-ts/src/styles.css b/gui-ts/src/styles.css new file mode 100644 index 0000000..84690ea --- /dev/null +++ b/gui-ts/src/styles.css @@ -0,0 +1,383 @@ +:root { + --bg: #0b1220; + --panel: #111827; + --panel-2: #0f172a; + --panel-3: #1e293b; + --border: #334155; + --border-strong: #475569; + --text: #e5e7eb; + --text-dim: #94a3b8; + --text-muted: #64748b; + --accent: #38bdf8; + --accent-2: #f59e0b; + --danger: #ef4444; + --ok: #4ade80; +} + +* { box-sizing: border-box; } + +html, body, #root { + margin: 0; + padding: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: Inter, "Segoe UI", system-ui, Arial, sans-serif; + font-size: 13px; + line-height: 1.4; +} + +.app-shell { + max-width: 1180px; + margin: 0 auto; + padding: 12px; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: linear-gradient(90deg, #0f172a, #111827); + border: 1px solid var(--border); + border-radius: 6px 6px 0 0; + font-size: 14px; +} +.app-title { font-weight: 600; letter-spacing: 0.02em; } +.app-logo { margin-right: 8px; color: var(--accent); } + +.app-header-meta { display: flex; gap: 6px; } + +.pill { + display: inline-block; + padding: 2px 8px; + font-size: 11px; + letter-spacing: 0.04em; + text-transform: uppercase; + border: 1px solid var(--border-strong); + border-radius: 999px; + color: var(--text-dim); + background: var(--panel-2); +} + +/* Tab strip */ +.tab-strip { + display: flex; + flex-wrap: wrap; + gap: 2px; + padding: 0; + margin: 0; + background: var(--panel-2); + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); +} +.tab { + padding: 8px 14px; + background: transparent; + color: var(--text-dim); + border: none; + border-right: 1px solid var(--border); + cursor: pointer; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.01em; +} +.tab:hover { color: var(--text); background: var(--panel-3); } +.tab-active { + color: var(--text); + background: var(--panel); + border-bottom: 2px solid var(--accent); +} + +/* Tab body */ +.tab-body { + flex: 1; + background: var(--panel); + border: 1px solid var(--border); + border-top: none; + border-radius: 0 0 6px 6px; + padding: 14px; + min-height: 520px; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Fieldset */ +.panel-fieldset { + border: 1px solid var(--border); + border-radius: 6px; + padding: 12px 14px; + background: var(--panel-2); + margin: 0; +} +.panel-fieldset legend { + padding: 0 6px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-dim); +} + +.panel-note { + font-size: 12px; + color: var(--text-dim); + margin: 4px 0 8px 0; +} +.panel-note code { background: var(--panel-3); padding: 1px 4px; border-radius: 3px; } + +/* Label+input row */ +.panel-row { + display: grid; + grid-template-columns: 200px 1fr; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} +.panel-row label { font-size: 12px; color: var(--text-dim); } +.panel-row-input { display: flex; flex-direction: column; gap: 3px; } +.panel-row-hint { font-size: 11px; color: var(--text-muted); } + +.tab-grid { display: grid; gap: 12px; } +.tab-grid.two { grid-template-columns: 1fr 1fr; } +.tab-grid.three { grid-template-columns: 1fr 1fr 1fr; } + +@media (max-width: 880px) { + .tab-grid.two, .tab-grid.three { grid-template-columns: 1fr; } + .panel-row { grid-template-columns: 140px 1fr; } +} + +/* Inputs */ +.panel-input { + width: 100%; + padding: 6px 8px; + background: var(--panel-3); + color: var(--text); + border: 1px solid var(--border); + border-radius: 4px; + font: inherit; + font-size: 12px; +} +.panel-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(56,189,248,0.15); +} +.panel-input:disabled { opacity: 0.5; cursor: not-allowed; } + +.panel-checkbox { display: inline-flex; align-items: center; gap: 6px; } + +.panel-radio-group { display: flex; flex-wrap: wrap; gap: 12px; } +.panel-radio { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; } + +/* Buttons */ +.btn { + padding: 6px 12px; + background: var(--panel-3); + color: var(--text); + border: 1px solid var(--border-strong); + border-radius: 4px; + font: inherit; + font-size: 12px; + cursor: pointer; + white-space: nowrap; + transition: background 0.12s ease, border-color 0.12s ease; +} +.btn:hover { background: #243448; border-color: var(--accent); } +.btn:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-primary { background: #0ea5e9; border-color: #0284c7; color: #001018; font-weight: 600; } +.btn-primary:hover { background: #38bdf8; border-color: #0ea5e9; } +.btn-danger { color: #fecaca; border-color: #7f1d1d; background: transparent; padding: 2px 8px; font-size: 11px; } +.btn-danger:hover { background: #7f1d1d; color: #fff; border-color: #991b1b; } +.btn-secondary { background: var(--panel-3); } + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin: 4px 0 8px 0; +} +.action-row { display: flex; justify-content: flex-end; gap: 8px; padding-top: 10px; } + +/* Tables */ +.table-scroll { + max-height: 380px; + overflow: auto; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--panel-3); +} +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} +.data-table thead th { + position: sticky; + top: 0; + background: var(--panel); + text-align: left; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + font-weight: 600; + color: var(--text-dim); + z-index: 1; +} +.data-table tbody td { + padding: 3px 6px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} +.data-table tbody tr.row-selected td { + background: rgba(56, 189, 248, 0.16); +} +.data-table tbody tr[role="button"]:focus-visible td { + outline: 1px solid #38bdf8; + outline-offset: -1px; + background: rgba(56, 189, 248, 0.1); +} +.data-table tbody td .panel-input { padding: 3px 6px; font-size: 11px; } +.data-table .empty-row { text-align: center; color: var(--text-muted); padding: 18px; } + +/* KPI grid */ +.kpi-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} +.kpi-cell { + background: var(--panel-3); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px 10px; +} +.kpi-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin-bottom: 3px; +} +.kpi-val { font-size: 14px; font-weight: 600; color: var(--text); } + +@media (max-width: 900px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } } + +/* Callouts */ +.callout { + padding: 10px 12px; + border-radius: 4px; + background: var(--panel-3); + border-left: 3px solid var(--border-strong); + font-size: 12px; +} +.callout-info { border-left-color: var(--accent); } +.callout-error { border-left-color: var(--danger); color: #fca5a5; } +.callout-warning { border-left-color: var(--accent-2); color: #fcd34d; } + +.warning-list { margin: 0; padding-left: 16px; color: #fcd34d; font-size: 12px; } +.warning-list li { margin-bottom: 3px; } + +.mono-block { + font-family: "JetBrains Mono", "Fira Code", "Courier New", monospace; + font-size: 11px; + background: var(--panel-3); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px; + max-height: 280px; + overflow: auto; + white-space: pre-wrap; +} + +.advanced-textarea { + width: 100%; + margin-top: 8px; + padding: 8px; + font-family: "JetBrains Mono", "Fira Code", "Courier New", monospace; + font-size: 11px; + background: var(--panel-3); + color: var(--text); + border: 1px solid var(--border); + border-radius: 4px; + resize: vertical; +} + +/* Status bar */ +.app-statusbar { + display: flex; + gap: 16px; + padding: 6px 12px; + margin-top: 8px; + font-size: 11px; + color: var(--text-dim); + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 4px; +} + +/* Spinner */ +.spinner { + display: inline-block; + width: 10px; + height: 10px; + border: 2px solid var(--border-strong); + border-top-color: var(--accent); + border-radius: 50%; + margin-right: 6px; + vertical-align: -2px; + animation: spin 0.75s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* uPlot palette overrides for dark theme */ +.uplot-host .u-legend { color: var(--text-dim); font-size: 11px; } +.uplot-host .u-wrap { background: var(--panel-3); } + +/* 3D wellbore */ +.wellbore-3d-wrap { + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel-3); + padding: 8px; +} +.wellbore-3d { + width: 100%; + height: auto; + display: block; + border: 1px solid var(--border); + border-radius: 4px; + background: #020617; +} +.wellbore-legend { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 8px; + font-size: 11px; + color: var(--text-dim); +} +.wellbore-legend span { + display: inline-flex; + align-items: center; + gap: 6px; +} +.wellbore-legend i { + width: 12px; + height: 12px; + border-radius: 2px; + display: inline-block; + border: 1px solid rgba(255, 255, 255, 0.2); +} +.wellbore-kpis { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 6px; + font-size: 11px; + color: var(--text-dim); +} diff --git a/gui-ts/src/testSetup.ts b/gui-ts/src/testSetup.ts new file mode 100644 index 0000000..d861a4c --- /dev/null +++ b/gui-ts/src/testSetup.ts @@ -0,0 +1,48 @@ +import "@testing-library/jest-dom"; + +/** + * uPlot touches `window.matchMedia` at module-load time for HiDPI handling; + * jsdom doesn't provide it, so stub a minimal matcher before the import + * graph resolves. + */ +if (typeof window !== "undefined" && typeof window.matchMedia !== "function") { + window.matchMedia = (query: string) => + ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false + } as unknown as MediaQueryList); +} + +if (typeof window !== "undefined" && typeof window.ResizeObserver === "undefined") { + class ResizeObserverStub { + observe() {} + unobserve() {} + disconnect() {} + } + (window as unknown as { ResizeObserver: typeof ResizeObserverStub }).ResizeObserver = + ResizeObserverStub; +} + +if (typeof window !== "undefined" && typeof HTMLCanvasElement !== "undefined") { + const proto = HTMLCanvasElement.prototype as HTMLCanvasElement["prototype"] & { + getContext?: (contextId: string, options?: unknown) => unknown; + }; + if (typeof proto.getContext !== "function") { + proto.getContext = () => null; + } else { + const original = proto.getContext; + proto.getContext = function (contextId: string, options?: unknown) { + try { + return original.call(this, contextId, options); + } catch { + return null; + } + }; + } +} diff --git a/gui-ts/src/types.ts b/gui-ts/src/types.ts new file mode 100644 index 0000000..b1e203b --- /dev/null +++ b/gui-ts/src/types.ts @@ -0,0 +1,145 @@ +/** + * Types mirroring the solver-api JSON responses. These are advisory — the + * server remains the source of truth (`schemaVersion: 2`). + */ + +export type ParsedModel = { + wellName: string; + company: string; + measuredDepth: number[]; + inclination: number[]; + azimuth: number[]; + pumpingSpeed: number; + pumpDepth: number; + tubingAnchorLocation?: number; + rodFrictionCoefficient?: number; + stuffingBoxFriction?: number; + pumpFriction?: number; + waterCut?: number; + waterSpecGravity?: number; + fluidLevelOilGravity?: number; + taperDiameter?: number[]; + taperLength?: number[]; + taperModulus?: number[]; + rodType?: number[]; + tubingSize?: number; + unitsSelection?: number; + upStrokeDamping?: number; + downStrokeDamping?: number; + nonDimensionalFluidDamping?: number; + moldedGuideFrictionRatio?: number; + wheeledGuideFrictionRatio?: number; + otherGuideFrictionRatio?: number; + pumpDiameter?: number; + pumpIntakePressure?: number; + tubingGradient?: number; + pumpFillageOption?: number; + percentPumpFillage?: number; + percentUpstrokeTime?: number; + percentDownstrokeTime?: number; + pumpingUnitId?: string; + pumpingSpeedOption?: number; +}; + +export type ParsedCase = { + model: ParsedModel; + unsupportedFields: string[]; + rawFields: Record | undefined>; + warnings?: string[]; +}; + +export type CardPoint = { + position: number; + polishedLoad: number; + downholeLoad: number; + polishedStressPa?: number; + sideLoadN?: number; +}; + +export type SolverOutput = { + pointCount: number; + maxPolishedLoad: number; + minPolishedLoad: number; + maxDownholeLoad: number; + minDownholeLoad: number; + gasInterference?: boolean; + maxCfl?: number; + waveSpeedRefMPerS?: number; + warnings: string[]; + card: CardPoint[]; + pumpMovement?: { + stroke: number; + position: number[]; + velocity: number[]; + }; + profiles?: { + nodeCount: number; + trajectory3D: Array<{ md: number; curvature: number; inclination: number; azimuth: number }>; + sideLoadProfile: number[]; + frictionProfile: number[]; + }; + diagnostics?: { + valveStates: Array<{ travelingOpen: boolean; standingOpen: boolean }>; + chamberPressurePa: number[]; + gasFraction: number[]; + }; + fourierBaseline?: null | { + harmonics: number; + residualRmsPolished: number; + residualRmsDownhole: number; + card: Array<{ position: number; polishedLoad: number; downholeLoad: number }>; + }; +}; + +export type SolveResponse = { + schemaVersion?: number; + units?: string; + parseWarnings?: string[]; + surfaceCardQa?: Record | null; + fingerprint?: string; + parsed: ParsedCase; + solver: SolverOutput; + pumpMovement?: SolverOutput["pumpMovement"] | null; + solvers?: { + fdm: SolverOutput; + fea: SolverOutput; + }; + comparison?: { + schemaVersion?: number; + peakLoadDeltas?: { + polishedMaxDelta: number; + polishedMinDelta: number; + downholeMaxDelta: number; + downholeMinDelta: number; + }; + polishedMaxDelta: number; + polishedMinDelta: number; + downholeMaxDelta: number; + downholeMinDelta: number; + residualSummary?: { points: number; rms: number }; + pointwiseResiduals?: { + points: number; + series: Array<{ + position: number; + polishedLoadResidual: number; + downholeLoadResidual: number; + }>; + }; + fourier?: null | { + baselineName?: string; + points?: number; + residualRms?: number; + }; + }; + verbose?: Record; + runMetadata: { + deterministic: boolean; + pointCount: number; + generatedAt: string; + source?: string; + solverModel?: "fdm" | "fea" | "both"; + workflow?: string; + schemaVersion?: number; + units?: string; + }; +}; diff --git a/gui-ts/src/ui/App.tsx b/gui-ts/src/ui/App.tsx new file mode 100644 index 0000000..60211ec --- /dev/null +++ b/gui-ts/src/ui/App.tsx @@ -0,0 +1,262 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Tabs, type TabDef } from "./Tabs"; +import { useCaseStore } from "../state/useCaseStore"; +import { + EMPTY_CASE_STATE, + type RunSettings, + INITIAL_RUN_SETTINGS +} from "../state/caseModel"; +import { hydrateFromParsed } from "../state/xmlImport"; +import { serializeCaseXml } from "../state/xmlExport"; +import { fetchDefaultCase, solveCase, validateSurfaceCard, type SurfaceCard } from "../api/client"; +import type { SolveResponse } from "../types"; +import { WellTab } from "./tabs/WellTab"; +import { TrajectoryTab } from "./tabs/TrajectoryTab"; +import { KinematicsTab } from "./tabs/KinematicsTab"; +import { RodStringTab } from "./tabs/RodStringTab"; +import { PumpTab } from "./tabs/PumpTab"; +import { FluidTab } from "./tabs/FluidTab"; +import { SolverTab } from "./tabs/SolverTab"; +import { ResultsTab } from "./tabs/ResultsTab"; +import { AdvancedTab } from "./tabs/AdvancedTab"; +import { runEngineeringChecks } from "../state/engineeringChecks"; + +const TABS: TabDef[] = [ + { id: "tab-well", label: "Well" }, + { id: "tab-trajectory", label: "Trajectory" }, + { id: "tab-kinematics", label: "Kinematics" }, + { id: "tab-rod", label: "Rod String" }, + { id: "tab-pump", label: "Pump" }, + { id: "tab-fluid", label: "Fluid" }, + { id: "tab-solver", label: "Solver" }, + { id: "tab-results", label: "Results" }, + { id: "tab-advanced", label: "Advanced / XML" } +]; + +export function App() { + const store = useCaseStore(EMPTY_CASE_STATE); + const [runSettings, setRunSettings] = useState(INITIAL_RUN_SETTINGS); + const [activeTab, setActiveTab] = useState(TABS[0].id); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastRunAt, setLastRunAt] = useState(null); + const [elapsed, setElapsed] = useState(null); + const [statusMessage, setStatusMessage] = useState("Loading base case…"); + const [surfaceCardText, setSurfaceCardText] = useState(""); + const [surfaceCardQaMessage, setSurfaceCardQaMessage] = useState(null); + const [surfaceCardQaError, setSurfaceCardQaError] = useState(null); + const [validatingSurfaceCard, setValidatingSurfaceCard] = useState(false); + const hydrated = useRef(false); + const engineeringChecks = useMemo(() => runEngineeringChecks(store.state), [store.state]); + + useEffect(() => { + if (hydrated.current) return; + hydrated.current = true; + (async () => { + try { + const parsed = await fetchDefaultCase(); + store.setState(hydrateFromParsed(parsed)); + setStatusMessage("Base case loaded — ready to edit / solve"); + } catch (e) { + setStatusMessage("Failed to load base case"); + setError(e instanceof Error ? e.message : String(e)); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const parsedSurfaceCard = useMemo(() => parseSurfaceCard(surfaceCardText), [surfaceCardText]); + + const handleValidateSurfaceCard = useCallback(async () => { + setValidatingSurfaceCard(true); + setSurfaceCardQaMessage(null); + setSurfaceCardQaError(null); + try { + if (parsedSurfaceCard.errors.length) { + throw new Error(parsedSurfaceCard.errors.join(" ")); + } + if (parsedSurfaceCard.position.length < 4) { + throw new Error("Need at least 4 points for a useful diagnostic surface card."); + } + const qa = await validateSurfaceCard({ + position: parsedSurfaceCard.position, + load: parsedSurfaceCard.load + }); + if (qa.ok) { + setSurfaceCardQaMessage(`QA OK (schema v${qa.schemaVersion}).`); + } else { + setSurfaceCardQaMessage("QA returned warnings. You can still run diagnostic solve."); + } + } catch (error) { + setSurfaceCardQaError(error instanceof Error ? error.message : String(error)); + } finally { + setValidatingSurfaceCard(false); + } + }, [parsedSurfaceCard]); + + const handleRun = useCallback(async () => { + if (engineeringChecks.hasBlockingError) { + setError("Please fix blocking engineering checks before running the solver."); + setStatusMessage("Blocked by engineering checks"); + setActiveTab("tab-solver"); + return; + } + setLoading(true); + setError(null); + setStatusMessage("Running solver…"); + const t0 = performance.now(); + try { + const xml = serializeCaseXml(store.state); + let surfaceCard: SurfaceCard | undefined; + if (runSettings.workflow === "diagnostic") { + if (parsedSurfaceCard.errors.length) { + throw new Error(`Diagnostic card input invalid: ${parsedSurfaceCard.errors.join(" ")}`); + } + if (parsedSurfaceCard.position.length < 4) { + throw new Error("Diagnostic workflow requires a surface card with at least 4 points."); + } + surfaceCard = { + position: parsedSurfaceCard.position, + load: parsedSurfaceCard.load + }; + } + const resp = await solveCase({ + xml, + solverModel: runSettings.solverModel, + workflow: runSettings.workflow, + surfaceCard, + options: { + enableProfiles: true, + enableDiagnosticsDetail: runSettings.workflow === "diagnostic" + } + }); + setResult(resp); + const dt = (performance.now() - t0) / 1000; + setElapsed(dt); + setLastRunAt(new Date().toLocaleTimeString()); + setStatusMessage(`Done in ${dt.toFixed(1)}s`); + setActiveTab("tab-results"); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setStatusMessage("Error"); + } finally { + setLoading(false); + } + }, [engineeringChecks.hasBlockingError, parsedSurfaceCard, runSettings, store.state]); + + const handleExportXml = useCallback(() => { + const xml = serializeCaseXml(store.state); + const blob = new Blob([xml], { type: "application/xml" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + const safeName = (store.state.wellName || "case").replace(/[^a-z0-9_.-]+/gi, "_"); + anchor.download = `${safeName || "case"}.xml`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + setStatusMessage("XML exported"); + }, [store.state]); + + return ( +
+
+
+ + Rods-Cursor — Case Editor & Solver +
+
+ {runSettings.solverModel.toUpperCase()} + {runSettings.workflow} + {lastRunAt && Last: {lastRunAt}} +
+
+ + + +
+ {activeTab === "tab-well" && } + {activeTab === "tab-trajectory" && } + {activeTab === "tab-kinematics" && ( + + )} + {activeTab === "tab-rod" && } + {activeTab === "tab-pump" && } + {activeTab === "tab-fluid" && } + {activeTab === "tab-solver" && ( + + )} + {activeTab === "tab-results" && ( + + )} + {activeTab === "tab-advanced" && } +
+ +
+ {statusMessage} + Well: {store.state.wellName || "—"} + Taper sections: {store.state.taper.filter((t) => t.length > 0).length} + Survey stations: {store.state.survey.length} +
+
+ ); +} + +function parseSurfaceCard(text: string): { + position: number[]; + load: number[]; + errors: string[]; +} { + const lines = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + const position: number[] = []; + const load: number[] = []; + const errors: string[] = []; + for (let i = 0; i < lines.length; i += 1) { + const parts = lines[i].split(/[,\s;]+/).filter(Boolean); + if (parts.length < 2) { + errors.push(`Line ${i + 1} must contain position and load values.`); + continue; + } + const p = Number(parts[0]); + const l = Number(parts[1]); + if (!Number.isFinite(p) || !Number.isFinite(l)) { + errors.push(`Line ${i + 1} has non-numeric values.`); + continue; + } + position.push(p); + load.push(l); + } + return { position, load, errors }; +} diff --git a/gui-ts/src/ui/Tabs.tsx b/gui-ts/src/ui/Tabs.tsx new file mode 100644 index 0000000..3127f67 --- /dev/null +++ b/gui-ts/src/ui/Tabs.tsx @@ -0,0 +1,50 @@ +import { useEffect } from "react"; + +export type TabDef = { id: string; label: string }; + +export type TabsProps = { + tabs: TabDef[]; + active: string; + onChange: (id: string) => void; + ariaLabel?: string; +}; + +export function Tabs(props: TabsProps) { + useEffect(() => { + if (typeof window === "undefined") return; + const syncHash = () => { + const hash = window.location.hash.replace(/^#/, ""); + if (hash && props.tabs.some((t) => t.id === hash) && hash !== props.active) { + props.onChange(hash); + } + }; + window.addEventListener("hashchange", syncHash); + syncHash(); + return () => window.removeEventListener("hashchange", syncHash); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.active]); + + return ( +
+ {props.tabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/gui-ts/src/ui/common/CheckboxField.tsx b/gui-ts/src/ui/common/CheckboxField.tsx new file mode 100644 index 0000000..fd82aef --- /dev/null +++ b/gui-ts/src/ui/common/CheckboxField.tsx @@ -0,0 +1,22 @@ +export type CheckboxFieldProps = { + id?: string; + checked: boolean; + onChange: (checked: boolean) => void; + label?: string; + disabled?: boolean; +}; + +export function CheckboxField(props: CheckboxFieldProps) { + return ( + + ); +} diff --git a/gui-ts/src/ui/common/Fieldset.tsx b/gui-ts/src/ui/common/Fieldset.tsx new file mode 100644 index 0000000..0afa30b --- /dev/null +++ b/gui-ts/src/ui/common/Fieldset.tsx @@ -0,0 +1,13 @@ +import type { PropsWithChildren } from "react"; + +export function Fieldset({ + legend, + children +}: PropsWithChildren<{ legend: string }>) { + return ( +
+ {legend} + {children} +
+ ); +} diff --git a/gui-ts/src/ui/common/NumberField.tsx b/gui-ts/src/ui/common/NumberField.tsx new file mode 100644 index 0000000..f0edf7a --- /dev/null +++ b/gui-ts/src/ui/common/NumberField.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; + +export type NumberFieldProps = { + id?: string; + value: number; + onChange: (value: number) => void; + step?: number; + min?: number; + max?: number; + placeholder?: string; + disabled?: boolean; + ariaLabel?: string; +}; + +/** + * Controlled numeric input that keeps a local string buffer so users can + * type partial values (e.g. "-", "0.", "1e") without being clobbered. + */ +export function NumberField(props: NumberFieldProps) { + const { value, onChange, ...rest } = props; + const [buffer, setBuffer] = useState(Number.isFinite(value) ? String(value) : ""); + + useEffect(() => { + if (!Number.isFinite(value)) return; + setBuffer((prev) => { + const parsed = Number(prev); + if (Number.isFinite(parsed) && parsed === value) return prev; + return String(value); + }); + }, [value]); + + return ( + { + const next = e.target.value; + setBuffer(next); + const parsed = Number(next); + if (next === "") { + onChange(0); + } else if (Number.isFinite(parsed)) { + onChange(parsed); + } + }} + /> + ); +} diff --git a/gui-ts/src/ui/common/RadioGroup.tsx b/gui-ts/src/ui/common/RadioGroup.tsx new file mode 100644 index 0000000..80bb168 --- /dev/null +++ b/gui-ts/src/ui/common/RadioGroup.tsx @@ -0,0 +1,29 @@ +export type RadioOption = { value: V; label: string }; + +export type RadioGroupProps = { + name: string; + value: V; + onChange: (value: V) => void; + options: Array>; + disabled?: boolean; +}; + +export function RadioGroup(props: RadioGroupProps) { + return ( +
+ {props.options.map((opt) => ( + + ))} +
+ ); +} diff --git a/gui-ts/src/ui/common/Row.tsx b/gui-ts/src/ui/common/Row.tsx new file mode 100644 index 0000000..c0da76b --- /dev/null +++ b/gui-ts/src/ui/common/Row.tsx @@ -0,0 +1,18 @@ +import type { PropsWithChildren, ReactNode } from "react"; + +export function Row({ + label, + htmlFor, + hint, + children +}: PropsWithChildren<{ label: ReactNode; htmlFor?: string; hint?: ReactNode }>) { + return ( +
+ +
+ {children} + {hint ?
{hint}
: null} +
+
+ ); +} diff --git a/gui-ts/src/ui/common/SelectField.tsx b/gui-ts/src/ui/common/SelectField.tsx new file mode 100644 index 0000000..416a1fb --- /dev/null +++ b/gui-ts/src/ui/common/SelectField.tsx @@ -0,0 +1,40 @@ +export type SelectOption = { + value: V; + label: string; +}; + +export type SelectFieldProps = { + id?: string; + value: V; + onChange: (value: V) => void; + options: Array>; + disabled?: boolean; + ariaLabel?: string; +}; + +export function SelectField(props: SelectFieldProps) { + return ( + + ); +} diff --git a/gui-ts/src/ui/common/TextField.tsx b/gui-ts/src/ui/common/TextField.tsx new file mode 100644 index 0000000..50dc870 --- /dev/null +++ b/gui-ts/src/ui/common/TextField.tsx @@ -0,0 +1,23 @@ +export type TextFieldProps = { + id?: string; + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; + ariaLabel?: string; +}; + +export function TextField(props: TextFieldProps) { + return ( + props.onChange(e.target.value)} + /> + ); +} diff --git a/gui-ts/src/ui/common/UPlotChart.tsx b/gui-ts/src/ui/common/UPlotChart.tsx new file mode 100644 index 0000000..b18e761 --- /dev/null +++ b/gui-ts/src/ui/common/UPlotChart.tsx @@ -0,0 +1,63 @@ +import { useEffect, useRef } from "react"; +import uPlot from "uplot"; +import type { Options, AlignedData } from "uplot"; +import "uplot/dist/uPlot.min.css"; + +export type UPlotChartProps = { + data: AlignedData; + options: Options; + height?: number; +}; + +/** + * Thin React wrapper around uPlot. Re-creates the chart whenever options + * identity changes and calls `setData` when data changes in place. + */ +export function UPlotChart(props: UPlotChartProps) { + const hostRef = useRef(null); + const instanceRef = useRef(null); + + useEffect(() => { + if (!hostRef.current) return undefined; + if (typeof navigator !== "undefined" && /jsdom/i.test(navigator.userAgent)) { + return undefined; + } + // Skip under jsdom-style environments without a real 2D canvas context. + try { + const probe = document.createElement("canvas").getContext("2d"); + if (!probe) return undefined; + } catch { + return undefined; + } + const opts: Options = { + ...props.options, + width: hostRef.current.clientWidth || props.options.width || 600, + height: props.height ?? props.options.height ?? 260 + }; + const chart = new uPlot(opts, props.data, hostRef.current); + instanceRef.current = chart; + + const resize = () => { + if (!hostRef.current || !instanceRef.current) return; + instanceRef.current.setSize({ + width: hostRef.current.clientWidth || 600, + height: props.height ?? 260 + }); + }; + window.addEventListener("resize", resize); + + return () => { + window.removeEventListener("resize", resize); + chart.destroy(); + instanceRef.current = null; + }; + // Intentionally only re-create on options identity change; data updates handled below. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.options, props.height]); + + useEffect(() => { + instanceRef.current?.setData(props.data); + }, [props.data]); + + return
; +} diff --git a/gui-ts/src/ui/common/Wellbore3DView.tsx b/gui-ts/src/ui/common/Wellbore3DView.tsx new file mode 100644 index 0000000..0803a9d --- /dev/null +++ b/gui-ts/src/ui/common/Wellbore3DView.tsx @@ -0,0 +1,342 @@ +import { useMemo, useRef, useState } from "react"; +import type { CaseState } from "../../state/caseModel"; +import { DLS_BAD_SECTION_THRESHOLD } from "../../state/engineeringChecks"; +import { + buildTrajectorySegments, + interpolateAlongMd, + type TrajectoryPoint3D, + type TrajectorySegment +} from "../../state/trajectoryMetrics"; + +type ProjectionMode = "perspective" | "orthographic"; +export type OverlayMode = "dls" | "sideLoad"; + +export type Wellbore3DViewProps = { + caseState: CaseState; + overlayMode?: OverlayMode; + sideLoadProfile?: number[] | null; + highlightedSegmentIndex?: number | null; + onSegmentSelect?: (segmentIndex: number | null) => void; + svgId?: string; + width?: number; + height?: number; +}; + +function colorForDls(dls: number): string { + if (dls >= DLS_BAD_SECTION_THRESHOLD) return "#ef4444"; + if (dls >= DLS_BAD_SECTION_THRESHOLD * 0.5) return "#f59e0b"; + return "#22c55e"; +} + +function colorForSideLoad(value: number, max: number): string { + if (!Number.isFinite(value) || !Number.isFinite(max) || max <= 1e-6) return "#22c55e"; + const ratio = Math.max(0, Math.min(1, value / max)); + if (ratio >= 0.85) return "#ef4444"; + if (ratio >= 0.45) return "#f59e0b"; + return "#22c55e"; +} + +function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t; +} + +function colorForRod(t: number): string { + const r = Math.round(lerp(56, 217, t)); + const g = Math.round(lerp(189, 70, t)); + const b = Math.round(lerp(248, 239, t)); + return `rgb(${r}, ${g}, ${b})`; +} + +function project( + p: TrajectoryPoint3D, + bounds: { minX: number; maxX: number; minY: number; maxY: number; minZ: number; maxZ: number }, + width: number, + height: number, + view: { + yaw: number; + pitch: number; + zoom: number; + panX: number; + panY: number; + projection: ProjectionMode; + } +): { x: number; y: number } { + const cx = (bounds.minX + bounds.maxX) * 0.5; + const cy = (bounds.minY + bounds.maxY) * 0.5; + const cz = (bounds.minZ + bounds.maxZ) * 0.5; + let x = p.x - cx; + let y = p.y - cy; + let z = p.z - cz; + const yaw = view.yaw; + const pitch = view.pitch; + const x1 = x * Math.cos(yaw) - y * Math.sin(yaw); + const y1 = x * Math.sin(yaw) + y * Math.cos(yaw); + const z1 = z; + const x2 = x1; + const y2 = y1 * Math.cos(pitch) - z1 * Math.sin(pitch); + const z2 = y1 * Math.sin(pitch) + z1 * Math.cos(pitch); + x = x2; + y = y2; + z = z2; + const extent = Math.max(bounds.maxX - bounds.minX, bounds.maxY - bounds.minY, bounds.maxZ - bounds.minZ, 1); + const scale = ((Math.min(width, height) * 0.72) / extent) * view.zoom; + const depth = view.projection === "perspective" ? 1 / (1 + z * 0.001) : 1; + return { + x: width * 0.5 + view.panX + x * scale * depth, + y: height * 0.5 + view.panY - y * scale * depth + }; +} + +export function Wellbore3DView({ + caseState, + overlayMode = "dls", + sideLoadProfile = null, + highlightedSegmentIndex = null, + onSegmentSelect, + svgId = "wellbore-3d-svg", + width = 840, + height = 420 +}: Wellbore3DViewProps) { + const [view, setView] = useState({ + yaw: 0.8, + pitch: 0.55, + zoom: 1, + panX: 0, + panY: 0, + projection: "perspective" as ProjectionMode + }); + const dragState = useRef<{ + active: boolean; + mode: "rotate" | "pan"; + x: number; + y: number; + }>({ active: false, mode: "rotate", x: 0, y: 0 }); + + const geom = useMemo(() => { + const segments = buildTrajectorySegments(caseState.survey); + if (!segments.length) return null; + const points = [segments[0].a, ...segments.map((s) => s.b)]; + const xs = points.map((p) => p.x); + const ys = points.map((p) => p.y); + const zs = points.map((p) => p.z); + const bounds = { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys), + minZ: Math.min(...zs), + maxZ: Math.max(...zs) + }; + const rodLength = caseState.taper.reduce((sum, row) => sum + Math.max(0, row.length), 0); + const pumpPoint = interpolateAlongMd(segments, caseState.pumpDepth); + const sideLoadMax = sideLoadProfile?.length + ? Math.max(...sideLoadProfile.filter((v) => Number.isFinite(v)), 0) + : 0; + return { segments, bounds, rodLength, pumpPoint, sideLoadMax }; + }, [caseState, sideLoadProfile]); + + if (!geom) { + return

Need at least 2 survey stations to render 3D wellbore.

; + } + + const maxDls = Math.max(...geom.segments.map((segment) => segment.dls), 0); + const highDlsCount = geom.segments.filter( + (segment) => segment.dls >= DLS_BAD_SECTION_THRESHOLD + ).length; + const totalLen = geom.segments[geom.segments.length - 1].b.md; + + return ( +
+
+ + + + + + Drag: rotate | Shift+drag: pan | Mouse wheel: zoom + +
+ + 3D Wellbore Viewer + + onSegmentSelect?.(null)} + onPointerDown={(event) => { + dragState.current = { + active: true, + mode: event.shiftKey ? "pan" : "rotate", + x: event.clientX, + y: event.clientY + }; + (event.currentTarget as SVGRectElement).setPointerCapture(event.pointerId); + }} + onPointerMove={(event) => { + if (!dragState.current.active) return; + const dx = event.clientX - dragState.current.x; + const dy = event.clientY - dragState.current.y; + dragState.current.x = event.clientX; + dragState.current.y = event.clientY; + if (dragState.current.mode === "pan") { + setView((prev) => ({ ...prev, panX: prev.panX + dx, panY: prev.panY + dy })); + } else { + setView((prev) => ({ + ...prev, + yaw: prev.yaw + dx * 0.006, + pitch: Math.max(-1.25, Math.min(1.25, prev.pitch + dy * 0.004)) + })); + } + }} + onPointerUp={(event) => { + dragState.current.active = false; + (event.currentTarget as SVGRectElement).releasePointerCapture(event.pointerId); + }} + onPointerLeave={() => { + dragState.current.active = false; + }} + onWheel={(event) => { + event.preventDefault(); + const factor = event.deltaY < 0 ? 1.06 : 1 / 1.06; + setView((prev) => ({ + ...prev, + zoom: Math.max(0.35, Math.min(4, prev.zoom * factor)) + })); + }} + /> + {geom.segments.map((segment, idx) => { + const a = project(segment.a, geom.bounds, width, height, view); + const b = project(segment.b, geom.bounds, width, height, view); + const sideLoad = + sideLoadProfile && sideLoadProfile.length + ? sideLoadProfile[Math.min(idx, sideLoadProfile.length - 1)] ?? 0 + : 0; + const stroke = + overlayMode === "sideLoad" + ? colorForSideLoad(sideLoad, geom.sideLoadMax) + : colorForDls(segment.dls); + const active = highlightedSegmentIndex === idx; + return ( + onSegmentSelect?.(idx)} + style={{ cursor: "pointer" }} + /> + ); + })} + {geom.segments.map((segment, idx) => { + const rodEndMd = geom.rodLength; + if (segment.a.md > rodEndMd) return null; + const clippedEnd = segment.b.md > rodEndMd + ? interpolateAlongMd([segment], rodEndMd) + : segment.b; + if (!clippedEnd) return null; + const a = project(segment.a, geom.bounds, width, height, view); + const b = project(clippedEnd, geom.bounds, width, height, view); + const t = Math.min(1, Math.max(0, clippedEnd.md / Math.max(rodEndMd, 1))); + return ( + + ); + })} + {geom.pumpPoint && ( + (() => { + const p = project(geom.pumpPoint as TrajectoryPoint3D, geom.bounds, width, height, view); + return ( + + + + + ); + })() + )} + +
+ {overlayMode === "dls" ? ( + <> + Low DLS (< {(DLS_BAD_SECTION_THRESHOLD * 0.5).toFixed(1)}) + Moderate DLS + Bad section DLS (≥ {DLS_BAD_SECTION_THRESHOLD}) + + ) : ( + <> + Low side-load risk + Moderate side-load risk + High side-load risk + + )} + Rod string gradient +
+
+ Max DLS: {maxDls.toFixed(2)} deg/100 + Bad-DLS segments: {highDlsCount} + Total MD: {totalLen.toFixed(1)} + Pump MD: {caseState.pumpDepth.toFixed(1)} +
+
+ ); +} + diff --git a/gui-ts/src/ui/common/__tests__/Wellbore3DView.test.tsx b/gui-ts/src/ui/common/__tests__/Wellbore3DView.test.tsx new file mode 100644 index 0000000..a780b3a --- /dev/null +++ b/gui-ts/src/ui/common/__tests__/Wellbore3DView.test.tsx @@ -0,0 +1,33 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Wellbore3DView } from "../Wellbore3DView"; +import { EMPTY_CASE_STATE } from "../../../state/caseModel"; + +describe("Wellbore3DView controls", () => { + it("renders projection/zoom/reset controls and toggles projection", () => { + const state = { + ...EMPTY_CASE_STATE, + pumpDepth: 1000, + survey: [ + { md: 0, inc: 0, azi: 0 }, + { md: 500, inc: 15, azi: 35 }, + { md: 1000, inc: 30, azi: 65 } + ], + taper: [{ diameter: 19.05, length: 1000, modulus: 30.5, rodType: 3 }] + }; + + render(); + const projectionBtn = screen.getByRole("button", { name: /Projection:/i }); + expect(projectionBtn).toHaveTextContent("Projection: perspective"); + + fireEvent.click(projectionBtn); + expect(screen.getByRole("button", { name: /Projection:/i })).toHaveTextContent( + "Projection: orthographic" + ); + + expect(screen.getByRole("button", { name: "Zoom +" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Zoom -" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Reset View" })).toBeInTheDocument(); + }); +}); + diff --git a/gui-ts/src/ui/tabs/AdvancedTab.tsx b/gui-ts/src/ui/tabs/AdvancedTab.tsx new file mode 100644 index 0000000..7dfc111 --- /dev/null +++ b/gui-ts/src/ui/tabs/AdvancedTab.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import type { CaseStore } from "../../state/useCaseStore"; +import { Fieldset } from "../common/Fieldset"; +import { serializeCaseXml } from "../../state/xmlExport"; +import { hydrateFromParsed } from "../../state/xmlImport"; +import { parseCaseXmlApi } from "../../api/client"; +import { textOf, describeRawField } from "./rawFieldHelpers"; + +type Props = { + store: CaseStore; +}; + +export function AdvancedTab({ store }: Props) { + const { state, setState } = store; + const [pasted, setPasted] = useState(""); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + async function importXml(xml: string) { + setBusy(true); + setError(null); + setMessage(null); + try { + const parsed = await parseCaseXmlApi(xml); + const next = hydrateFromParsed(parsed); + setState(next); + setMessage( + `Imported case with ${Object.keys(parsed.rawFields).length} XML fields (${parsed.unsupportedFields.length} unsupported).` + ); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + } + + async function onFile(file: File) { + const text = await file.text(); + setPasted(text); + void importXml(text); + } + + function exportXml() { + const xml = serializeCaseXml(state); + const blob = new Blob([xml], { type: "application/xml" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + const safeName = (state.wellName || "case").replace(/[^a-z0-9_.-]+/gi, "_"); + anchor.download = `${safeName || "case"}.xml`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + setMessage("Exported current state as XML."); + setError(null); + } + + return ( + <> +
+

+ Upload a case XML file or paste its contents. Parsing is performed by + POST /case/parse in the solver-api so the result + matches the canonical parser exactly. +

+
+ + + +
+