From 3c8d1ea39d4e75ab07ff4df509e85a6244ef38d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= <94198850+q-soriarty@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:52:47 +0100 Subject: [PATCH 01/18] fix(ci): pin cla action version and remove npm cache from commitlint (#106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ci): pin cla action version and remove npm cache from commitlint Pin contributor-assistant/github-action to v2.6.1 — the bare v2 major version tag does not exist. Remove cache: 'npm' from commitlint workflow since package-lock.json is gitignored and commitlint is installed globally. Add 'ci' to allowed scope-enum in commitlintrc. Co-Authored-By: Claude Opus 4.6 * chore(ci): trigger ci rerun with updated pr title Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .commitlintrc.json | 1 + .github/workflows/cla.yml | 2 +- .github/workflows/commitlint.yml | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.commitlintrc.json b/.commitlintrc.json index 152feb5..9acdcfc 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -28,6 +28,7 @@ "monitor-server", "docs", "config", + "ci", "deps" ] ], diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 4df238e..d550800 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: CLA Assistant - uses: contributor-assistant/github-action@v2 + uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index a37d9b0..3f4e103 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -19,7 +19,6 @@ jobs: uses: actions/setup-node@v6 with: node-version: '20' - cache: 'npm' - name: Install commitlint run: | From 34f02ad2f99d482fe0572f56157828d829794a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= <94198850+q-soriarty@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:04:06 +0100 Subject: [PATCH 02/18] fix(ci): disable subject-case rule for acronym-heavy project (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable subject-case: lower-case rule — acronyms like DTO, MQTT, TwinCAT, CLAUDE.md are unavoidable in commit subjects and cause false positives. Add ci scope to CLAUDE.md. Co-authored-by: Claude Opus 4.6 --- .commitlintrc.json | 6 +----- CLAUDE.md | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.commitlintrc.json b/.commitlintrc.json index 9acdcfc..96b0617 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -33,11 +33,7 @@ ] ], "scope-empty": [0], - "subject-case": [ - 2, - "always", - "lower-case" - ], + "subject-case": [0], "subject-empty": [2, "never"], "subject-full-stop": [2, "never", "."], "header-max-length": [2, "always", 72], diff --git a/CLAUDE.md b/CLAUDE.md index 7c05bcb..75ff540 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ Commits are validated by commitlint via husky git hook. **All commits must follo Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` -Scopes: `frontend`, `backend`, `build-server`, `monitor-server`, `docs`, `config`, `deps` +Scopes: `frontend`, `backend`, `build-server`, `monitor-server`, `docs`, `config`, `ci`, `deps` Rules: imperative mood, lowercase subject, no trailing period, max 72 chars. Use `!` after type/scope for breaking changes. From 7386b6b8d896a8ed6313ab1edbc596f0c7390a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= Date: Sat, 14 Feb 2026 15:27:06 +0100 Subject: [PATCH 03/18] fix(ci): store CLA signatures on develop branch Cherry-pick from hotfix/fix-cla-signatures-branch. The main branch is protected, so CLA action cannot commit signatures/cla.json there. Use develop instead. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index d550800..32ffb8b 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -26,7 +26,7 @@ jobs: with: path-to-signatures: "signatures/cla.json" path-to-document: "https://gist.github.com/q-soriarty/615e916cd19a34f5f1efa1110592b32a" - branch: "main" + branch: "develop" allowlist: "dependabot[bot],github-actions[bot]" custom-notsigned-prcomment: | Thank you for your contribution! Before we can merge this PR, you need to sign our [Contributor License Agreement](https://gist.github.com/q-soriarty/615e916cd19a34f5f1efa1110592b32a). From 60d8363c98c1e14cbf5c112e84240f17fa03980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= Date: Sat, 14 Feb 2026 15:30:05 +0100 Subject: [PATCH 04/18] fix(ci): use dedicated cla-signatures branch Cherry-pick from hotfix/cla-signatures-branch. Both main and develop are protected, so CLA action needs a dedicated unprotected branch for signature storage. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 32ffb8b..7e2a8d9 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -26,7 +26,7 @@ jobs: with: path-to-signatures: "signatures/cla.json" path-to-document: "https://gist.github.com/q-soriarty/615e916cd19a34f5f1efa1110592b32a" - branch: "develop" + branch: "cla-signatures" allowlist: "dependabot[bot],github-actions[bot]" custom-notsigned-prcomment: | Thank you for your contribution! Before we can merge this PR, you need to sign our [Contributor License Agreement](https://gist.github.com/q-soriarty/615e916cd19a34f5f1efa1110592b32a). From e2864369d18ce3850e0b488338722478d58f5f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= <94198850+q-soriarty@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:39:16 +0100 Subject: [PATCH 05/18] docs: add architecture design documents for all modules (#98) * docs: add architecture design documents for all modules Add BUILD_SERVER_DESIGN.md covering Beckhoff Automation Interface patterns, pipeline architecture, COM interop constraints, and code generation strategy. Add MODULE_ARCHITECTURE.md covering project structure, dependency graphs, and technology choices per module. Update ARCHITECTURE.md with cross-references to new design documents. Co-Authored-By: Claude Opus 4.6 * ci: trigger CLA re-check * ci: trigger CLA re-check --------- Co-authored-by: Claude Opus 4.6 --- doc/ARCHITECTURE.md | 10 ++ doc/BUILD_SERVER_DESIGN.md | 232 +++++++++++++++++++++++++++++++++++++ doc/MODULE_ARCHITECTURE.md | 180 ++++++++++++++++++++++++++++ 3 files changed, 422 insertions(+) create mode 100644 doc/BUILD_SERVER_DESIGN.md create mode 100644 doc/MODULE_ARCHITECTURE.md diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index bdbf2ef..9b8acdf 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -64,10 +64,14 @@ FlowForge is a visual PLC programming platform: ## Components +> **Detailed design documents**: See [MODULE_ARCHITECTURE.md](MODULE_ARCHITECTURE.md) for project structure, dependency graphs, and technology choices per module. See [BUILD_SERVER_DESIGN.md](BUILD_SERVER_DESIGN.md) for Beckhoff Automation Interface patterns, pipeline architecture, and COM interop details. + ### 1. Visual Editor (Frontend) **Purpose**: Provide intuitive node-based visual programming interface +**Architecture**: Feature-based folder organization with shared utilities. See [MODULE_ARCHITECTURE.md](MODULE_ARCHITECTURE.md). + **Key Responsibilities**: - Render visual programming canvas - Handle user interactions (drag-drop, connections) @@ -84,6 +88,8 @@ FlowForge is a visual PLC programming platform: **Purpose**: Manage projects, orchestrate builds, and handle authentication +**Architecture**: Clean Architecture Lite (3 projects: Application, Infrastructure, Api). See [MODULE_ARCHITECTURE.md](MODULE_ARCHITECTURE.md). + **Key Responsibilities**: - User authentication via Keycloak (OIDC JWT validation) with role-based access control - User management admin API (facade over Keycloak Admin REST API) @@ -105,6 +111,8 @@ FlowForge is a visual PLC programming platform: **Purpose**: Convert visual programs to TwinCAT PLC projects +**Architecture**: Single project with pipeline pattern (Pipeline, CodeGen, TwinCat, Services). See [BUILD_SERVER_DESIGN.md](BUILD_SERVER_DESIGN.md). + **Key Responsibilities**: - Clone/fetch project repo from GitHub - Parse JSON intermediate format (flow JSON) @@ -124,6 +132,8 @@ FlowForge is a visual PLC programming platform: **Purpose**: Provide real-time PLC data streaming to the frontend without loading the backend API +**Architecture**: Single project with organized folders (Hubs, Auth, Services). See [MODULE_ARCHITECTURE.md](MODULE_ARCHITECTURE.md). + **Key Responsibilities**: - Read PLC variables via ADS over MQTT (cyclic reads or event-based subscriptions) - Convert ADS data to frontend-consumable format diff --git a/doc/BUILD_SERVER_DESIGN.md b/doc/BUILD_SERVER_DESIGN.md new file mode 100644 index 0000000..252ca7a --- /dev/null +++ b/doc/BUILD_SERVER_DESIGN.md @@ -0,0 +1,232 @@ +# Build Server Design + +## Overview + +The FlowForge Build Server converts visual flow programs into TwinCAT PLC projects using the Beckhoff Automation Interface. It runs on dedicated Windows Server instances (one per TwinCAT version) and connects to the main stack via REST + MQTT. + +This document captures the architectural decisions, constraints, and patterns derived from Beckhoff documentation, community projects, and production experience. + +## Critical Constraints + +| Constraint | Reason | Impact | +|-----------|--------|--------| +| **32-bit (x86) execution** | Automation Interface COM is 32-bit only | `x86` in .csproj | +| **STA threading** | COM interop requires Single-Threaded Apartment | `[STAThread]` on entry + dedicated STA thread for pipeline | +| **IOleMessageFilter** | VS DTE rejects COM calls while busy — crashes without it | Mandatory MessageFilter wrapper | +| **One VS instance per build** | No concurrent DTE usage per process | Queue-based sequential processing | +| **TwinCAT Engineering license** | Required for project creation, compilation, activation | Build server = dedicated Windows Server | +| **Visual Studio DTE dependency** | Automation Interface accessed via `TcXaeShell.DTE.15.0` ProgID | VS or TcXaeShell must be installed | + +## Beckhoff Automation Interface + +The Automation Interface is a COM-based API provided by Beckhoff for programmatic control of TwinCAT Engineering (XAE). Key interfaces: + +### Core COM Interfaces + +- **`EnvDTE.DTE`** — Visual Studio Development Tools Environment. Entry point for solution management (open, build, close). +- **`ITcSysManager`** — TwinCAT System Manager. Root interface for navigating and manipulating the TwinCAT project tree. +- **`ITcSmTreeItem`** — Tree item in the TwinCAT project hierarchy. Provides `CreateChild()`, `LookupChild()`, property access. +- **`ITcPlcIECProject`** — PLC IEC project interface. Provides `GenerateBootProject()`, `CheckAllObjects()`, `PlcLogin()`, `PlcStart()`. + +### Key Methods + +| Interface | Method | Purpose | +|-----------|--------|---------| +| `DTE` | `Solution.Open(path)` | Open TwinCAT solution | +| `DTE` | `Solution.SolutionBuild.Build(true)` | Compile the solution | +| `ITcSysManager` | `LookupTreeItem(path)` | Navigate project tree by path | +| `ITcSmTreeItem` | `CreateChild(name, subType, vInfo, templatePath)` | Add POU, GVL, DUT, task | +| `ITcSmTreeItem` | `ImportChild(path, ...)` | Import from PLCopen XML | +| `ITcSysManager` | `ActivateConfiguration()` | Download config to PLC runtime | +| `ITcSysManager` | `StartRestartTwinCAT()` | Start/restart TwinCAT runtime | +| `ITcPlcIECProject` | `GenerateBootProject(true)` | Generate boot project for autostart | + +### TwinCAT Project Tree Structure + +The TwinCAT system manager organizes projects as a tree: + +``` +TIRC (Root) +├── TIPC (PLC) +│ └── PLC Project +│ ├── POUs/ +│ │ ├── MAIN (PRG) +│ │ ├── FB_Timer (FB) +│ │ └── FC_Calculate (FUN) +│ ├── DUTs/ +│ │ └── ST_MachineState (STRUCT) +│ ├── GVLs/ +│ │ └── GVL_Main +│ └── Libraries/ +├── TINC (NC) +├── TIID (I/O) +│ └── Device 1 (EtherCAT Master) +└── TIRT (Realtime) + └── Task 1 + └── Linked PLC Program +``` + +### Tree Item SubTypes + +| Type | SubType Value | Description | +|------|--------------|-------------| +| POU (Program) | 604 | PROGRAM POU | +| POU (Function Block) | 604 | FUNCTION_BLOCK POU | +| POU (Function) | 604 | FUNCTION POU | +| GVL | 615 | Global Variable List | +| DUT (Struct) | 606 | Data Unit Type (Structure) | +| DUT (Enum) | 606 | Data Unit Type (Enumeration) | +| Task | — | Realtime task | + +## Pipeline Architecture + +The build server processes jobs through a sequential pipeline of discrete steps. Each step has a single responsibility and operates on a shared `BuildContext`. + +### Pipeline Flow + +``` +CloneRepo → ParseFlow → ValidateFlow → CreateProject → GenerateCode + → ConfigureTasks → Compile → GenerateBootProject → CommitResult + → [DeployStep] (conditional, only for Deploy jobs) +``` + +### BuildContext + +Mutable context object passed through all pipeline steps: + +``` +BuildContext +├── Job — BuildJobDto (from backend) +├── WorkspacePath — temp directory for this build +├── FlowDocument — parsed flow JSON +├── Solution — DTE Solution reference +├── SysManager — ITcSysManager reference +├── PlcProject — ITcSmTreeItem for PLC project node +├── GeneratedFiles — list of generated file paths +├── Errors — accumulated error messages +├── Timings — per-step execution times +└── CancellationToken +``` + +### Step Descriptions + +1. **CloneRepoStep** — Git clone or fetch the project repository using LibGit2Sharp with user credentials from the job. +2. **ParseFlowStep** — Deserialize `flow/program.json` from the cloned repo into a `FlowDocument` object. +3. **ValidateFlowStep** — Structural validation: check all connections reference valid ports, detect cycles, verify required parameters. +4. **CreateProjectStep** — Create a new TwinCAT solution using `DTE`, add PLC project from machine type template via `ITcSmTreeItem.CreateChild()`. +5. **GenerateCodeStep** — Translate flow nodes to IEC 61131-3 Structured Text using node translators. Populate POUs, GVLs, DUTs via `ITcSmTreeItem.CreateChild()`. +6. **ConfigureTasksStep** — Create realtime tasks under TIRT, link PLC programs to tasks. +7. **CompileStep** — Call `DTE.Solution.SolutionBuild.Build(true)`, collect errors from ErrorList. +8. **GenerateBootProjectStep** — Call `ITcPlcIECProject.GenerateBootProject(true)` + `CheckAllObjects()` for autostart capability. +9. **CommitResultStep** — Git commit the generated `.tsproj` and ST files, push to the project repo. +10. **DeployStep** (conditional) — Only for Deploy jobs. Call `ActivateConfiguration()` + `StartRestartTwinCAT()` via `ITcSysManager`. Deploy lock and 4-eyes validation happen in the backend before the job reaches the build server. + +## Code Generation Strategy + +### FlowDocument JSON to IEC 61131-3 ST + +The code generator translates the visual flow (nodes + connections) into Structured Text: + +1. **Topological sort** — Order nodes by data dependencies (connections define edges). +2. **Node translation** — Each node type has a dedicated `INodeTranslator` that emits ST code. +3. **Variable declarations** — Collect all node inputs/outputs, generate `VAR`/`VAR_INPUT`/`VAR_OUTPUT` blocks. +4. **Connection wiring** — Generate assignment statements for connections between nodes. +5. **POU generation** — Wrap translated code in `PROGRAM`/`FUNCTION_BLOCK` declarations. + +### Node Translator Pattern + +Each visual node type maps to an `INodeTranslator` implementation: + +``` +INodeTranslator +├── TimerTranslator — TON/TOF/TP timer blocks +├── CounterTranslator — CTU/CTD/CTUD counter blocks +├── ComparisonTranslator — GT/LT/EQ/GE/LE comparisons +├── MathTranslator — ADD/SUB/MUL/DIV operations +├── LogicTranslator — AND/OR/NOT/XOR gates +├── MoveTranslator — MOVE/assignment operations +└── (extensible per node type) +``` + +Each translator receives the node definition and its resolved input connections, and returns: +- Variable declarations (type, initial value) +- Body statements (ST code) +- Any required library references + +## COM Interop Patterns + +### MessageFilter (Mandatory) + +The `IOleMessageFilter` implementation handles COM call rejection by the DTE when it's busy processing. Without this, random `RPC_E_CALL_REJECTED` exceptions crash the build process. + +Key behavior: +- `HandleInComingCall` — Always returns `SERVERCALL_ISHANDLED` (accept all incoming calls). +- `RetryRejectedCall` — Returns 99ms retry delay when the DTE is busy (`SERVERCALL_RETRYLATER`). This causes the COM runtime to automatically retry the call after a short delay. +- `MessagePending` — Returns `PENDINGMSG_WAITDEFPROCESS` (default processing). + +This pattern is proven in both TcUnit-Verifier and the Beckhoff CodeGenerationDemo samples. + +### VisualStudioInstance Facade + +Wraps `EnvDTE.DTE` lifecycle management behind `IVisualStudioInstance`: + +- **Creation** — `Activator.CreateInstance(Type.GetTypeFromProgID("TcXaeShell.DTE.15.0"))` or version-specific ProgID. +- **Version detection** — Parse .sln file to determine required VS/XAE version, select matching ProgID. +- **UI suppression** — `DTE.SuppressUI = true`, `DTE.MainWindow.Visible = false` for headless operation. +- **Build** — `DTE.Solution.SolutionBuild.Build(true)` with error collection from `DTE.ToolWindows.ErrorList`. +- **Cleanup** — `DTE.Quit()` in `finally` block to prevent orphaned devenv.exe processes. + +### AutomationInterface Facade + +Wraps `ITcSysManager` operations behind `IAutomationInterface`: + +- **Tree navigation** — `LookupTreeItem("TIPC^PLC Project^POUs")` with typed path helpers. +- **Child creation** — `CreateChild(name, subType, vInfo, templatePath)` with subtype constants. +- **Configuration activation** — `ActivateConfiguration()` → downloads to target PLC. +- **Runtime control** — `StartRestartTwinCAT()` → starts/restarts the TwinCAT runtime. + +## Template Management + +Machine type templates accelerate project creation by providing pre-configured PLC project structures: + +- **Template format** — `.tpzip` files (TwinCAT Project ZIP archives). +- **Template types** — 3-axis standalone, 3+1 axis standalone, x-division rotary table, etc. +- **Usage** — `ITcSmTreeItem.CreateChild(name, 0, null, templatePath)` to create PLC project from template. +- **Storage** — Templates stored on the build server file system, path resolved by `TemplateManager` from machine type enum. + +## Build vs Deploy Workflow + +### Build (Build-only) + +1. Backend inserts build job into PostgreSQL queue. +2. Build server claims job via REST (`FOR UPDATE SKIP LOCKED`). +3. Pipeline executes steps 1-9 (CloneRepo through CommitResult). +4. Generated PLC solution is committed and pushed to the project repo. +5. Build result reported to backend via REST. + +### Deploy (Build + Deploy) + +1. Backend validates deploy permissions (4-eyes for production, deploy lock check). +2. Backend inserts deploy job into PostgreSQL queue. +3. Build server claims job, executes steps 1-9 (same as build). +4. **DeployStep** (step 10) activates configuration on target PLC: + - `ITcSysManager.ActivateConfiguration()` — downloads project to PLC. + - `ITcSysManager.StartRestartTwinCAT()` — starts/restarts the runtime. + - Both operations go through ADS over MQTT to the target PLC. +5. Deploy result reported to backend via REST. + +## ADS over MQTT Deployment + +PLC activation uses ADS (Automation Device Specification) protocol tunneled over MQTT: + +- **Central MQTT broker** — All PLCs and build servers connect to a single broker. +- **No per-server ADS routes** — Eliminates manual route configuration on each PLC. +- **Topic structure** — `flowforge/deploy/request/{target-id}` for deploy commands. +- **Build server as gateway** — The build server has TwinCAT Engineering, which provides the ADS client. It publishes ADS commands to the MQTT broker, which routes them to the target PLC. + +## References + +- [Beckhoff InfoSys — Automation Interface](https://infosys.beckhoff.com/content/1033/tc3_automationinterface/index.html) +- [TcUnit — TwinCAT Unit Testing Framework](https://github.com/tcunit/TcUnit) (VisualStudioInstance pattern, MessageFilter implementation) +- [CodeGenerationDemo](src/build-server/samples/) — Beckhoff sample for Automation Interface scripting +- [AllTwinCAT — CI/CD for TwinCAT](https://alltwincat.com/) — Blog series on Jenkins integration and automated builds diff --git a/doc/MODULE_ARCHITECTURE.md b/doc/MODULE_ARCHITECTURE.md new file mode 100644 index 0000000..684bf49 --- /dev/null +++ b/doc/MODULE_ARCHITECTURE.md @@ -0,0 +1,180 @@ +# Module Architecture + +## Overview + +FlowForge is composed of four main modules plus a shared library, each with an architecture pattern suited to its complexity and deployment model. + +``` +┌─────────────────────────────────────────────────────────┐ +│ FlowForge.Shared │ +│ (DTOs, Enums, Constants) │ +└────────┬──────────────┬──────────────┬──────────────┬────┘ + │ │ │ │ + ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐ + │ Backend │ │Build Server│ │ Monitor │ │Frontend │ + │ (3 proj)│ │ (1 proj) │ │ (1 proj)│ │ (React) │ + └─────────┘ └───────────┘ └─────────┘ └─────────┘ +``` + +## Shared DTO Library (`FlowForge.Shared`) + +**Pattern**: Standalone class library with no external dependencies. + +**Rationale**: Multiple .NET modules (Backend, Build Server, Monitor Server) exchange data via REST and MQTT. A shared library ensures type consistency across module boundaries without coupling modules to each other. + +**Contents**: +- **Models/** — DTOs for Flow, Build, Deploy, Project, Target, Auth, Monitor domains. +- **Enums/** — Permission, ProjectRole, BuildStatus, DeployStatus. +- **Mqtt/** — Static topic string builders (type-safe topic construction). + +**Design rules**: +- No logic — only data structures and constants. +- No external NuGet dependencies — keeps the library lightweight and universally referenceable. +- Immutable where possible — DTOs use `init` properties. + +## Backend — Clean Architecture Lite (3 Projects) + +**Pattern**: Simplified Clean Architecture with three projects: Application (core), Infrastructure (externals), Api (HTTP surface). + +**Rationale**: The backend has the highest complexity — it integrates with PostgreSQL, Keycloak, GitHub, MQTT, Docker, and SignalR. Separating concerns into three projects enables: +- Independent testing of business logic (Application) without database or HTTP. +- Swappable infrastructure (e.g., different git providers, different databases). +- Clear dependency direction: Api → Application ← Infrastructure. + +### Project: `FlowForge.Backend.Application` + +The core business logic layer. References only `FlowForge.Shared`. + +- **Entities/** — Domain entities mapped to PostgreSQL tables (Project, BuildJob, PlcTarget, etc.). +- **Interfaces/** — Repository and service abstractions (IProjectRepository, IGitService, etc.). +- **Services/** — Business logic implementations (ProjectService, BuildService, DeployService, etc.). +- **Validation/** — Domain validation rules (FlowDocumentValidator). + +### Project: `FlowForge.Backend.Infrastructure` + +External integrations. References `Application` + `Shared`. + +- **Persistence/** — EF Core DbContext, entity configurations, migrations. +- **Repositories/** — `IRepository` implementations using EF Core. +- **Git/** — LibGit2Sharp implementation of `IGitService`. +- **Mqtt/** — MQTTnet implementation of `IMqttService`. +- **Docker/** — HttpClient-based `IDockerService` (talks to docker-socket-proxy). +- **Keycloak/** — HttpClient-based `IKeycloakAdminService` (Keycloak Admin REST API). +- **Security/** — AES encryption for per-user token storage. +- **DependencyInjection.cs** — `AddInfrastructure()` extension method for clean DI registration. + +### Project: `FlowForge.Backend.Api` + +HTTP surface layer. References `Application` + `Infrastructure` + `Shared`. + +- **Controllers/** — REST API endpoints (Projects, Build, Deploy, Targets, Monitor, Admin). +- **Hubs/** — SignalR hubs (BuildHub with typed client interface). +- **Middleware/** — Error handling (ProblemDetails), request logging. +- **Auth/** — Keycloak JWT setup, claims extensions. +- **Configuration/** — Options classes (FlowForgeOptions, KeycloakOptions, MqttOptions). + +### Dependency Graph + +``` +Api ──────→ Application ←────── Infrastructure + │ │ │ + └────────────────┼────────────────────┘ + ▼ + Shared +``` + +**Key rule**: Application has NO reference to Infrastructure. Service interfaces are defined in Application, implemented in Infrastructure, and wired via DI in Api. + +## Build Server — Single Project, Organized Folders + +**Pattern**: Single project with folder-based organization (Pipeline, CodeGen, TwinCat, Services). + +**Rationale**: The build server has a focused responsibility (convert flow → TwinCAT project) and runs as an isolated process on Windows. Multi-project separation would add complexity without proportional benefit. The pipeline pattern provides internal structure. + +See [BUILD_SERVER_DESIGN.md](BUILD_SERVER_DESIGN.md) for detailed design. + +**Folder structure**: +- **Pipeline/** — `IBuildStep` interface, `BuildContext`, `BuildPipeline` orchestrator, concrete steps. +- **CodeGen/** — `ICodeGenerator`, `StructuredTextGenerator`, `PlcProjectBuilder`, node translators. +- **TwinCat/** — COM facades (`IVisualStudioInstance`, `IAutomationInterface`, `MessageFilter`, tree item constants, template manager). +- **Services/** — `BuildJobClient` (existing), `MqttHandler`, `GitWorkspace`, `WorkspaceManager`. + +**Key architectural decisions**: +1. **Pipeline pattern** — Each build step is an `IBuildStep` with `ExecuteAsync(BuildContext, CancellationToken)`. Steps are executed sequentially by `BuildPipeline`. Enables logging, timing, and error handling per step. +2. **Facade pattern for COM** — `IVisualStudioInstance` and `IAutomationInterface` wrap COM objects behind testable interfaces. +3. **Strategy pattern for code generation** — `INodeTranslator` per visual node type. New node types = new translator class. +4. **Template-based project creation** — Machine type templates stored as `.tpzip` files, resolved by `TemplateManager`. + +## Monitor Server — Single Project, Organized Folders + +**Pattern**: Single project with folder-based organization (Hubs, Auth, Services). + +**Rationale**: The monitor server is lightweight — it streams PLC data from MQTT to SignalR. Minimal complexity doesn't warrant multi-project separation. + +**Folder structure**: +- **Hubs/** — `PlcDataHub` (existing) + `IPlcDataHubClient` typed interface. +- **Auth/** — `TokenValidator` for short-lived HMAC tokens. +- **Services/** — `IMqttAdsClient` / `MqttAdsClient` for ADS over MQTT, `SubscriptionManager` for per-connection tracking. + +## Frontend — Feature-Based Folders + +**Pattern**: Feature-based folder organization with shared utilities. + +**Rationale**: Feature-based structure scales better than layer-based (e.g., all components in one folder). Each feature is self-contained with its own components, hooks, and types. Shared code lives in `shared/`. + +**Folder structure**: +``` +src/ +├── api/ — HTTP client, endpoint constants, TypeScript types +├── auth/ — Keycloak OIDC provider, auth hooks, route guards +├── layout/ — App shell (sidebar, header, content area) +├── features/ +│ ├── editor/ — Flow canvas, node palette, inspector, custom nodes +│ ├── projects/ — Project list, create, templates +│ ├── build/ — Build panel, history, logs +│ ├── deploy/ — Deploy panel, lock indicator, approval dialog +│ ├── targets/ — PLC target management +│ ├── monitoring/ — Live PLC data monitoring +│ └── admin/ — User management (Keycloak facade) +└── shared/ — Reusable components, hooks, utilities +``` + +**Key dependencies**: +- `zustand` — Lightweight state management (simpler than Redux for this scale). +- `@tanstack/react-query` — Server state management (caching, refetching, optimistic updates). +- `keycloak-js` — Keycloak OIDC client adapter. +- `@microsoft/signalr` — SignalR client for real-time updates. +- `react-router-dom` — Client-side routing. + +## Test Project Organization + +**Pattern**: One test project per source project, using xUnit + NSubstitute + FluentAssertions. + +``` +test/ +├── FlowForge.Shared.Tests/ +├── FlowForge.Backend.Api.Tests/ +├── FlowForge.Backend.Application.Tests/ +├── FlowForge.Backend.Infrastructure.Tests/ (+Testcontainers.PostgreSql) +├── FlowForge.BuildServer.Tests/ +└── FlowForge.MonitorServer.Tests/ +``` + +**Rationale**: +- **xUnit** — Modern, extensible, widely used in .NET ecosystem. +- **NSubstitute** — Clean mocking syntax, ideal for interface-heavy architecture. +- **FluentAssertions** — Readable assertion syntax, better error messages. +- **Testcontainers** — Real PostgreSQL for Infrastructure tests (no in-memory fakes). + +## Technology Choices Summary + +| Module | Runtime | Key Libraries | +|--------|---------|---------------| +| Shared | .NET 9.0 | (none) | +| Backend.Application | .NET 9.0 | (none — interfaces only) | +| Backend.Infrastructure | .NET 9.0 | EF Core, Npgsql, MQTTnet, LibGit2Sharp | +| Backend.Api | .NET 9.0 (ASP.NET Core) | SignalR, JWT Bearer | +| Build Server | .NET 9.0 (x86) | MQTTnet, LibGit2Sharp, COM Interop | +| Monitor Server | .NET 9.0 (ASP.NET Core) | SignalR, MQTTnet | +| Frontend | TypeScript/React 19 | React Flow, zustand, react-query, keycloak-js, signalr | +| Tests | .NET 9.0 | xUnit, NSubstitute, FluentAssertions, Testcontainers | From 98739f84657f4eef98cf6d0a09ea7e2ea8e5115f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= <94198850+q-soriarty@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:39:21 +0100 Subject: [PATCH 06/18] feat(config): add shared DTO library (#99) * feat(config): add shared DTO library with models, enums, and MQTT topics Create FlowForge.Shared class library containing DTOs for Flow, Build, Deploy, Project, Target, Auth, and Monitor domains. Add enums for Permission and ProjectRole. Add type-safe MQTT topic builders. Co-Authored-By: Claude Opus 4.6 * ci: trigger CLA re-check --------- Co-authored-by: Claude Opus 4.6 --- .../FlowForge.Shared/Enums/Permission.cs | 18 +++++++++++++ .../FlowForge.Shared/Enums/ProjectRole.cs | 13 +++++++++ .../FlowForge.Shared/FlowForge.Shared.csproj | 10 +++++++ .../Models/Auth/UserInfoDto.cs | 13 +++++++++ .../Models/Build/BuildJobDto.cs | 19 +++++++++++++ .../Models/Build/BuildProgressDto.cs | 12 +++++++++ .../Models/Build/BuildResultDto.cs | 13 +++++++++ .../Models/Build/BuildStatus.cs | 13 +++++++++ .../Models/Deploy/DeployRequestDto.cs | 11 ++++++++ .../Models/Deploy/DeployResultDto.cs | 12 +++++++++ .../Models/Deploy/DeployStatus.cs | 15 +++++++++++ .../Models/Flow/FlowConnection.cs | 10 +++++++ .../Models/Flow/FlowDocument.cs | 13 +++++++++ .../FlowForge.Shared/Models/Flow/FlowNode.cs | 12 +++++++++ .../FlowForge.Shared/Models/Flow/FlowPort.cs | 10 +++++++ .../Models/Flow/NodePosition.cs | 10 +++++++ .../Models/Monitor/MonitorSessionDto.cs | 12 +++++++++ .../Models/Monitor/PlcVariableValueDto.cs | 12 +++++++++ .../Models/Project/ProjectDetailDto.cs | 19 +++++++++++++ .../Models/Project/ProjectSummaryDto.cs | 15 +++++++++++ .../Models/Target/PlcTargetDto.cs | 16 +++++++++++ .../Models/Target/TargetGroupDto.cs | 12 +++++++++ .../FlowForge.Shared/Mqtt/MqttTopics.cs | 27 +++++++++++++++++++ 23 files changed, 317 insertions(+) create mode 100644 src/shared/FlowForge.Shared/Enums/Permission.cs create mode 100644 src/shared/FlowForge.Shared/Enums/ProjectRole.cs create mode 100644 src/shared/FlowForge.Shared/FlowForge.Shared.csproj create mode 100644 src/shared/FlowForge.Shared/Models/Auth/UserInfoDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Build/BuildJobDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Build/BuildProgressDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Build/BuildResultDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Build/BuildStatus.cs create mode 100644 src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Deploy/DeployResultDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Deploy/DeployStatus.cs create mode 100644 src/shared/FlowForge.Shared/Models/Flow/FlowConnection.cs create mode 100644 src/shared/FlowForge.Shared/Models/Flow/FlowDocument.cs create mode 100644 src/shared/FlowForge.Shared/Models/Flow/FlowNode.cs create mode 100644 src/shared/FlowForge.Shared/Models/Flow/FlowPort.cs create mode 100644 src/shared/FlowForge.Shared/Models/Flow/NodePosition.cs create mode 100644 src/shared/FlowForge.Shared/Models/Monitor/MonitorSessionDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Monitor/PlcVariableValueDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Project/ProjectDetailDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Project/ProjectSummaryDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs create mode 100644 src/shared/FlowForge.Shared/Models/Target/TargetGroupDto.cs create mode 100644 src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs diff --git a/src/shared/FlowForge.Shared/Enums/Permission.cs b/src/shared/FlowForge.Shared/Enums/Permission.cs new file mode 100644 index 0000000..97b8519 --- /dev/null +++ b/src/shared/FlowForge.Shared/Enums/Permission.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Enums; + +public enum Permission +{ + ProjectView, + ProjectCreate, + ProjectEdit, + ProjectDelete, + Build, + Deploy, + TargetManage, + Monitor, + AdminUsers, + AdminSystem +} diff --git a/src/shared/FlowForge.Shared/Enums/ProjectRole.cs b/src/shared/FlowForge.Shared/Enums/ProjectRole.cs new file mode 100644 index 0000000..d22c91e --- /dev/null +++ b/src/shared/FlowForge.Shared/Enums/ProjectRole.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Enums; + +public enum ProjectRole +{ + Viewer, + Editor, + Builder, + Deployer, + Owner +} diff --git a/src/shared/FlowForge.Shared/FlowForge.Shared.csproj b/src/shared/FlowForge.Shared/FlowForge.Shared.csproj new file mode 100644 index 0000000..4900ca5 --- /dev/null +++ b/src/shared/FlowForge.Shared/FlowForge.Shared.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + enable + enable + FlowForge.Shared + + + diff --git a/src/shared/FlowForge.Shared/Models/Auth/UserInfoDto.cs b/src/shared/FlowForge.Shared/Models/Auth/UserInfoDto.cs new file mode 100644 index 0000000..436ad4a --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Auth/UserInfoDto.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Auth; + +public record UserInfoDto +{ + public string Id { get; init; } = string.Empty; + public string UserName { get; init; } = string.Empty; + public string Email { get; init; } = string.Empty; + public string DisplayName { get; init; } = string.Empty; + public IReadOnlyList Roles { get; init; } = []; +} diff --git a/src/shared/FlowForge.Shared/Models/Build/BuildJobDto.cs b/src/shared/FlowForge.Shared/Models/Build/BuildJobDto.cs new file mode 100644 index 0000000..18ad350 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Build/BuildJobDto.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Build; + +public record BuildJobDto +{ + public Guid Id { get; init; } + public Guid ProjectId { get; init; } + public string ProjectName { get; init; } = string.Empty; + public string RepoUrl { get; init; } = string.Empty; + public string Branch { get; init; } = "main"; + public string TwinCatVersion { get; init; } = string.Empty; + public string RequestedBy { get; init; } = string.Empty; + public BuildStatus Status { get; init; } + public bool IncludeDeploy { get; init; } + public string? TargetAmsNetId { get; init; } + public DateTimeOffset CreatedAt { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Build/BuildProgressDto.cs b/src/shared/FlowForge.Shared/Models/Build/BuildProgressDto.cs new file mode 100644 index 0000000..dd1c237 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Build/BuildProgressDto.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Build; + +public record BuildProgressDto +{ + public Guid BuildId { get; init; } + public string Stage { get; init; } = string.Empty; + public int Percentage { get; init; } + public string Message { get; init; } = string.Empty; +} diff --git a/src/shared/FlowForge.Shared/Models/Build/BuildResultDto.cs b/src/shared/FlowForge.Shared/Models/Build/BuildResultDto.cs new file mode 100644 index 0000000..ba6d3d5 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Build/BuildResultDto.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Build; + +public record BuildResultDto +{ + public Guid BuildId { get; init; } + public bool Success { get; init; } + public IReadOnlyList Errors { get; init; } = []; + public string? CommitSha { get; init; } + public DateTimeOffset CompletedAt { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Build/BuildStatus.cs b/src/shared/FlowForge.Shared/Models/Build/BuildStatus.cs new file mode 100644 index 0000000..1e6b0d5 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Build/BuildStatus.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Build; + +public enum BuildStatus +{ + Pending, + Claimed, + InProgress, + Completed, + Failed +} diff --git a/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs b/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs new file mode 100644 index 0000000..af1ca3d --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Deploy; + +public record DeployRequestDto +{ + public Guid ProjectId { get; init; } + public string TargetAmsNetId { get; init; } = string.Empty; + public string? ApproverId { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Deploy/DeployResultDto.cs b/src/shared/FlowForge.Shared/Models/Deploy/DeployResultDto.cs new file mode 100644 index 0000000..ca0ba20 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Deploy/DeployResultDto.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Deploy; + +public record DeployResultDto +{ + public Guid DeployId { get; init; } + public bool Success { get; init; } + public string? Error { get; init; } + public DateTimeOffset CompletedAt { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Deploy/DeployStatus.cs b/src/shared/FlowForge.Shared/Models/Deploy/DeployStatus.cs new file mode 100644 index 0000000..8a7fc26 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Deploy/DeployStatus.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Deploy; + +public enum DeployStatus +{ + Pending, + AwaitingApproval, + Approved, + InProgress, + Completed, + Failed, + Rejected +} diff --git a/src/shared/FlowForge.Shared/Models/Flow/FlowConnection.cs b/src/shared/FlowForge.Shared/Models/Flow/FlowConnection.cs new file mode 100644 index 0000000..7ae7094 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Flow/FlowConnection.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Flow; + +public record FlowConnection +{ + public FlowPort From { get; init; } = new(); + public FlowPort To { get; init; } = new(); +} diff --git a/src/shared/FlowForge.Shared/Models/Flow/FlowDocument.cs b/src/shared/FlowForge.Shared/Models/Flow/FlowDocument.cs new file mode 100644 index 0000000..264b30c --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Flow/FlowDocument.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Flow; + +public record FlowDocument +{ + public string Name { get; init; } = string.Empty; + public string Version { get; init; } = string.Empty; + public IReadOnlyList Nodes { get; init; } = []; + public IReadOnlyList Connections { get; init; } = []; + public Dictionary Metadata { get; init; } = []; +} diff --git a/src/shared/FlowForge.Shared/Models/Flow/FlowNode.cs b/src/shared/FlowForge.Shared/Models/Flow/FlowNode.cs new file mode 100644 index 0000000..fb98898 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Flow/FlowNode.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Flow; + +public record FlowNode +{ + public string Id { get; init; } = string.Empty; + public string Type { get; init; } = string.Empty; + public NodePosition Position { get; init; } = new(); + public Dictionary Parameters { get; init; } = []; +} diff --git a/src/shared/FlowForge.Shared/Models/Flow/FlowPort.cs b/src/shared/FlowForge.Shared/Models/Flow/FlowPort.cs new file mode 100644 index 0000000..1a62aec --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Flow/FlowPort.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Flow; + +public record FlowPort +{ + public string NodeId { get; init; } = string.Empty; + public string PortName { get; init; } = string.Empty; +} diff --git a/src/shared/FlowForge.Shared/Models/Flow/NodePosition.cs b/src/shared/FlowForge.Shared/Models/Flow/NodePosition.cs new file mode 100644 index 0000000..cd867cd --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Flow/NodePosition.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Flow; + +public record NodePosition +{ + public double X { get; init; } + public double Y { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Monitor/MonitorSessionDto.cs b/src/shared/FlowForge.Shared/Models/Monitor/MonitorSessionDto.cs new file mode 100644 index 0000000..e0e48d5 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Monitor/MonitorSessionDto.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Monitor; + +public record MonitorSessionDto +{ + public string SessionId { get; init; } = string.Empty; + public string SignalREndpoint { get; init; } = string.Empty; + public string AuthToken { get; init; } = string.Empty; + public string TargetAmsNetId { get; init; } = string.Empty; +} diff --git a/src/shared/FlowForge.Shared/Models/Monitor/PlcVariableValueDto.cs b/src/shared/FlowForge.Shared/Models/Monitor/PlcVariableValueDto.cs new file mode 100644 index 0000000..0bafc5e --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Monitor/PlcVariableValueDto.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Monitor; + +public record PlcVariableValueDto +{ + public string Path { get; init; } = string.Empty; + public object? Value { get; init; } + public string DataType { get; init; } = string.Empty; + public DateTimeOffset Timestamp { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Project/ProjectDetailDto.cs b/src/shared/FlowForge.Shared/Models/Project/ProjectDetailDto.cs new file mode 100644 index 0000000..fdd5a0e --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Project/ProjectDetailDto.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.Shared.Models.Flow; + +namespace FlowForge.Shared.Models.Project; + +public record ProjectDetailDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string RepoUrl { get; init; } = string.Empty; + public string Branch { get; init; } = "main"; + public string? LastCommitSha { get; init; } + public FlowDocument? Flow { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Project/ProjectSummaryDto.cs b/src/shared/FlowForge.Shared/Models/Project/ProjectSummaryDto.cs new file mode 100644 index 0000000..0f8e399 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Project/ProjectSummaryDto.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Project; + +public record ProjectSummaryDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string RepoUrl { get; init; } = string.Empty; + public string? LastCommitSha { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs b/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs new file mode 100644 index 0000000..8bb3d03 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Target; + +public record PlcTargetDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public string AmsNetId { get; init; } = string.Empty; + public string TwinCatVersion { get; init; } = string.Empty; + public IReadOnlyList Labels { get; init; } = []; + public Guid? GroupId { get; init; } + public bool IsProductionTarget { get; init; } + public bool DeployLocked { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Target/TargetGroupDto.cs b/src/shared/FlowForge.Shared/Models/Target/TargetGroupDto.cs new file mode 100644 index 0000000..7850dfc --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Target/TargetGroupDto.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Target; + +public record TargetGroupDto +{ + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public IReadOnlyList Targets { get; init; } = []; +} diff --git a/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs b/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs new file mode 100644 index 0000000..a89f4bc --- /dev/null +++ b/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Mqtt; + +public static class MqttTopics +{ + private const string Prefix = "flowforge"; + + public static string BuildNotify(string twinCatVersion) => + $"{Prefix}/build/notify/{twinCatVersion}"; + + public static string BuildProgress(Guid buildId) => + $"{Prefix}/build/progress/{buildId}"; + + public static string DeployRequest(string targetId) => + $"{Prefix}/deploy/request/{targetId}"; + + public static string AdsRead(string amsNetId) => + $"{Prefix}/ads/read/{amsNetId}"; + + public static string AdsWrite(string amsNetId) => + $"{Prefix}/ads/write/{amsNetId}"; + + public static string AdsNotification(string amsNetId) => + $"{Prefix}/ads/notification/{amsNetId}"; +} From 0ed96c59f6ff174c861df35d25a0707de79f5ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bir=C3=B3=2C=20Csaba=20Attila?= <94198850+q-soriarty@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:39:27 +0100 Subject: [PATCH 07/18] feat(frontend): add feature-based arch with auth and layout (#103) * feat(frontend): add feature-based arch with auth, layout, modules Add feature-based folder structure with auth (Keycloak), layout, and feature modules (editor, projects, build, deploy, targets, monitoring, admin). Configure React Flow, Zustand, React Query, and SignalR hooks. Co-Authored-By: Claude Opus 4.6 * ci: trigger commitlint re-check * ci: trigger CLA re-check --------- Co-authored-by: Claude Opus 4.6 --- src/frontend/package-lock.json | 2332 ++++++++++++++++- src/frontend/package.json | 14 +- src/frontend/src/api/client.ts | 25 + src/frontend/src/api/endpoints.ts | 43 + src/frontend/src/api/types.ts | 119 + src/frontend/src/auth/AuthProvider.tsx | 32 + src/frontend/src/auth/ProtectedRoute.tsx | 16 + src/frontend/src/auth/permissions.ts | 49 + src/frontend/src/auth/useAuth.ts | 13 + src/frontend/src/features/admin/AdminPage.tsx | 6 + src/frontend/src/features/admin/UsersPage.tsx | 6 + .../admin/components/RoleAssignment.tsx | 6 + .../admin/components/UserEditDialog.tsx | 6 + .../features/admin/components/UserTable.tsx | 6 + .../src/features/admin/hooks/useRoles.ts | 6 + .../src/features/admin/hooks/useUsers.ts | 6 + .../deploy/components/ApprovalDialog.tsx | 6 + .../deploy/components/DeployLockIndicator.tsx | 6 + .../deploy/components/DeployPanel.tsx | 6 + .../src/features/deploy/hooks/useDeploy.ts | 6 + .../features/deploy/hooks/useDeployLock.ts | 6 + .../src/features/editor/EditorPage.tsx | 6 + .../features/editor/components/FlowCanvas.tsx | 6 + .../editor/components/NodeInspector.tsx | 6 + .../editor/components/NodePalette.tsx | 6 + .../src/features/editor/hooks/useAutoSave.ts | 6 + .../features/editor/hooks/useFlowDocument.ts | 6 + .../features/editor/hooks/useNodeDragDrop.ts | 6 + .../features/editor/nodes/ComparisonNode.tsx | 6 + .../src/features/editor/nodes/CounterNode.tsx | 6 + .../src/features/editor/nodes/InputNode.tsx | 6 + .../src/features/editor/nodes/OutputNode.tsx | 6 + .../src/features/editor/nodes/TimerNode.tsx | 6 + .../src/features/editor/nodes/nodeRegistry.ts | 16 + .../src/features/editor/types/flow.types.ts | 4 + .../src/features/monitoring/MonitorPage.tsx | 6 + .../monitoring/components/ValueDisplay.tsx | 6 + .../monitoring/components/VariableList.tsx | 6 + .../components/VariableSubscriber.tsx | 6 + .../monitoring/hooks/useMonitorSession.ts | 6 + .../features/monitoring/hooks/usePlcData.ts | 6 + .../features/projects/ProjectCreatePage.tsx | 6 + .../src/features/projects/ProjectListPage.tsx | 6 + .../projects/components/ProjectCard.tsx | 6 + .../projects/components/TemplateSelector.tsx | 6 + .../features/projects/hooks/useProjects.ts | 6 + .../features/projects/hooks/useTemplates.ts | 6 + .../src/features/targets/TargetListPage.tsx | 6 + .../targets/components/TargetCard.tsx | 6 + .../targets/components/TargetGroupPanel.tsx | 6 + .../targets/components/TargetLabelEditor.tsx | 6 + .../features/targets/hooks/useTargetGroups.ts | 6 + .../src/features/targets/hooks/useTargets.ts | 6 + src/frontend/src/layout/AppLayout.tsx | 20 + src/frontend/src/layout/Header.tsx | 7 + src/frontend/src/layout/Sidebar.tsx | 7 + .../src/shared/components/ConfirmDialog.tsx | 6 + .../src/shared/components/ErrorBoundary.tsx | 26 + .../src/shared/components/LoadingSpinner.tsx | 6 + .../src/shared/components/StatusBadge.tsx | 6 + src/frontend/src/shared/hooks/useApi.ts | 6 + src/frontend/src/shared/hooks/useSignalR.ts | 6 + src/frontend/src/shared/utils/formatters.ts | 10 + 63 files changed, 2877 insertions(+), 138 deletions(-) create mode 100644 src/frontend/src/api/client.ts create mode 100644 src/frontend/src/api/endpoints.ts create mode 100644 src/frontend/src/api/types.ts create mode 100644 src/frontend/src/auth/AuthProvider.tsx create mode 100644 src/frontend/src/auth/ProtectedRoute.tsx create mode 100644 src/frontend/src/auth/permissions.ts create mode 100644 src/frontend/src/auth/useAuth.ts create mode 100644 src/frontend/src/features/admin/AdminPage.tsx create mode 100644 src/frontend/src/features/admin/UsersPage.tsx create mode 100644 src/frontend/src/features/admin/components/RoleAssignment.tsx create mode 100644 src/frontend/src/features/admin/components/UserEditDialog.tsx create mode 100644 src/frontend/src/features/admin/components/UserTable.tsx create mode 100644 src/frontend/src/features/admin/hooks/useRoles.ts create mode 100644 src/frontend/src/features/admin/hooks/useUsers.ts create mode 100644 src/frontend/src/features/deploy/components/ApprovalDialog.tsx create mode 100644 src/frontend/src/features/deploy/components/DeployLockIndicator.tsx create mode 100644 src/frontend/src/features/deploy/components/DeployPanel.tsx create mode 100644 src/frontend/src/features/deploy/hooks/useDeploy.ts create mode 100644 src/frontend/src/features/deploy/hooks/useDeployLock.ts create mode 100644 src/frontend/src/features/editor/EditorPage.tsx create mode 100644 src/frontend/src/features/editor/components/FlowCanvas.tsx create mode 100644 src/frontend/src/features/editor/components/NodeInspector.tsx create mode 100644 src/frontend/src/features/editor/components/NodePalette.tsx create mode 100644 src/frontend/src/features/editor/hooks/useAutoSave.ts create mode 100644 src/frontend/src/features/editor/hooks/useFlowDocument.ts create mode 100644 src/frontend/src/features/editor/hooks/useNodeDragDrop.ts create mode 100644 src/frontend/src/features/editor/nodes/ComparisonNode.tsx create mode 100644 src/frontend/src/features/editor/nodes/CounterNode.tsx create mode 100644 src/frontend/src/features/editor/nodes/InputNode.tsx create mode 100644 src/frontend/src/features/editor/nodes/OutputNode.tsx create mode 100644 src/frontend/src/features/editor/nodes/TimerNode.tsx create mode 100644 src/frontend/src/features/editor/nodes/nodeRegistry.ts create mode 100644 src/frontend/src/features/editor/types/flow.types.ts create mode 100644 src/frontend/src/features/monitoring/MonitorPage.tsx create mode 100644 src/frontend/src/features/monitoring/components/ValueDisplay.tsx create mode 100644 src/frontend/src/features/monitoring/components/VariableList.tsx create mode 100644 src/frontend/src/features/monitoring/components/VariableSubscriber.tsx create mode 100644 src/frontend/src/features/monitoring/hooks/useMonitorSession.ts create mode 100644 src/frontend/src/features/monitoring/hooks/usePlcData.ts create mode 100644 src/frontend/src/features/projects/ProjectCreatePage.tsx create mode 100644 src/frontend/src/features/projects/ProjectListPage.tsx create mode 100644 src/frontend/src/features/projects/components/ProjectCard.tsx create mode 100644 src/frontend/src/features/projects/components/TemplateSelector.tsx create mode 100644 src/frontend/src/features/projects/hooks/useProjects.ts create mode 100644 src/frontend/src/features/projects/hooks/useTemplates.ts create mode 100644 src/frontend/src/features/targets/TargetListPage.tsx create mode 100644 src/frontend/src/features/targets/components/TargetCard.tsx create mode 100644 src/frontend/src/features/targets/components/TargetGroupPanel.tsx create mode 100644 src/frontend/src/features/targets/components/TargetLabelEditor.tsx create mode 100644 src/frontend/src/features/targets/hooks/useTargetGroups.ts create mode 100644 src/frontend/src/features/targets/hooks/useTargets.ts create mode 100644 src/frontend/src/layout/AppLayout.tsx create mode 100644 src/frontend/src/layout/Header.tsx create mode 100644 src/frontend/src/layout/Sidebar.tsx create mode 100644 src/frontend/src/shared/components/ConfirmDialog.tsx create mode 100644 src/frontend/src/shared/components/ErrorBoundary.tsx create mode 100644 src/frontend/src/shared/components/LoadingSpinner.tsx create mode 100644 src/frontend/src/shared/components/StatusBadge.tsx create mode 100644 src/frontend/src/shared/hooks/useApi.ts create mode 100644 src/frontend/src/shared/hooks/useSignalR.ts create mode 100644 src/frontend/src/shared/utils/formatters.ts diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 276ea6c..53b201a 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -8,18 +8,97 @@ "name": "flowforge-frontend", "version": "0.1.0", "dependencies": { + "@microsoft/signalr": "^10.0.0", + "@tanstack/react-query": "^5.90.21", "@xyflow/react": "^12.0.0", + "keycloak-js": "^26.2.3", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.13.0", + "zustand": "^5.0.11" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "jsdom": "^28.0.0", + "msw": "^2.12.10", "typescript": "~5.7.0", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^4.0.18" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "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": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.8.tgz", + "integrity": "sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -254,6 +333,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "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", @@ -302,6 +391,138 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", + "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", + "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.1", + "@csstools/css-calc": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -744,6 +965,112 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -794,6 +1121,62 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/signalr": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-10.0.0.tgz", + "integrity": "sha512-0BRqz/uCx3JdrOqiqgFhih/+hfTERaUfCZXFB52uMaZJrKaPRzHzMuqVsJC/V3pt7NozcNXGspjKiQEK+X7P2w==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1151,60 +1534,188 @@ "win32" ] }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, - "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, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" } }, - "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==", + "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/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@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/@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==", + "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": { - "@babel/types": "^7.28.2" + "@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/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "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/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "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", + "peer": true + }, + "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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { @@ -1245,6 +1756,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1272,6 +1790,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1293,6 +1818,117 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xyflow/react": { "version": "12.10.0", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz", @@ -1308,6 +1944,34 @@ "react-dom": ">=17" } }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@xyflow/system": { "version": "0.0.74", "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz", @@ -1325,6 +1989,72 @@ "d3-zoom": "^3.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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", + "peer": true, + "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/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -1335,6 +2065,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -1390,63 +2130,212 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { - "node": ">=12" + "node": ">= 12" } }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { "node": ">=12" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=12" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { "node": ">=12" } }, @@ -1515,6 +2404,58 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", + "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1533,6 +2474,31 @@ } } }, + "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/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", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -1540,6 +2506,33 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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-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/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1592,6 +2585,44 @@ "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/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.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/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1610,6 +2641,16 @@ } } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1635,93 +2676,466 @@ "node": ">=6.9.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true, "license": "MIT" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "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", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=6" + "node": ">= 14" } }, - "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==", + "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": "ISC", + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, - "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==", + "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" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "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": ">=8" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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": "ISC" + "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz", + "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^5.3.7", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.20.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", + "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/keycloak-js": { + "version": "26.2.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz", + "integrity": "sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, + "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", + "peer": true, + "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/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "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/msw": { + "version": "2.12.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", + "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.41.2", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -1760,6 +3174,49 @@ "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", + "peer": true, + "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==", + "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==", + "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==", + "license": "MIT" + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -1781,6 +3238,14 @@ "react": "^19.2.4" } }, + "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", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1791,6 +3256,91 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -1836,6 +3386,19 @@ "fsevents": "~2.3.2" } }, + "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.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -1852,6 +3415,32 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1862,6 +3451,115 @@ "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1879,6 +3577,73 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "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": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -1893,6 +3658,35 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -1924,6 +3718,16 @@ "browserslist": ">= 4.21.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -2008,6 +3812,219 @@ } } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -2015,21 +4032,61 @@ "dev": true, "license": "ISC" }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { - "use-sync-external-store": "^1.2.2" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=12.7.0" + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2040,6 +4097,9 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/src/frontend/package.json b/src/frontend/package.json index 8f443ab..0d6bf66 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -9,15 +9,25 @@ "preview": "vite preview" }, "dependencies": { + "@microsoft/signalr": "^10.0.0", + "@tanstack/react-query": "^5.90.21", "@xyflow/react": "^12.0.0", + "keycloak-js": "^26.2.3", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.13.0", + "zustand": "^5.0.11" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "jsdom": "^28.0.0", + "msw": "^2.12.10", "typescript": "~5.7.0", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^4.0.18" } } diff --git a/src/frontend/src/api/client.ts b/src/frontend/src/api/client.ts new file mode 100644 index 0000000..e5e95bf --- /dev/null +++ b/src/frontend/src/api/client.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +const BASE_URL = "/api"; + +export async function apiFetch( + path: string, + options?: RequestInit, +): Promise { + const token = sessionStorage.getItem("auth_token"); + const response = await fetch(`${BASE_URL}${path}`, { + ...options, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options?.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`); + } + + return response.json() as Promise; +} diff --git a/src/frontend/src/api/endpoints.ts b/src/frontend/src/api/endpoints.ts new file mode 100644 index 0000000..822128a --- /dev/null +++ b/src/frontend/src/api/endpoints.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export const endpoints = { + health: "/health", + + projects: { + list: "/projects", + detail: (id: string) => `/projects/${id}`, + create: "/projects", + }, + + build: { + enqueue: "/build", + claim: "/build/claim", + result: (buildId: string) => `/build/${buildId}/result`, + history: (projectId: string) => `/build/history/${projectId}`, + }, + + deploy: { + request: "/deploy", + approve: (deployId: string) => `/deploy/${deployId}/approve`, + lock: (targetId: string) => `/deploy/lock/${targetId}`, + unlock: (targetId: string) => `/deploy/unlock/${targetId}`, + }, + + targets: { + list: "/targets", + detail: (id: string) => `/targets/${id}`, + groups: "/targets/groups", + }, + + monitor: { + start: "/monitor/start", + stop: (sessionId: string) => `/monitor/${sessionId}/stop`, + }, + + admin: { + users: "/admin/users", + user: (id: string) => `/admin/users/${id}`, + roles: "/admin/roles", + }, +} as const; diff --git a/src/frontend/src/api/types.ts b/src/frontend/src/api/types.ts new file mode 100644 index 0000000..4a73666 --- /dev/null +++ b/src/frontend/src/api/types.ts @@ -0,0 +1,119 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Mirrors FlowForge.Shared DTOs + +export interface FlowDocument { + name: string; + version: string; + nodes: FlowNode[]; + connections: FlowConnection[]; + metadata: Record; +} + +export interface FlowNode { + id: string; + type: string; + position: { x: number; y: number }; + parameters: Record; +} + +export interface FlowConnection { + from: { nodeId: string; portName: string }; + to: { nodeId: string; portName: string }; +} + +export type BuildStatus = + | "Pending" + | "Claimed" + | "InProgress" + | "Completed" + | "Failed"; + +export interface BuildJob { + id: string; + projectId: string; + projectName: string; + status: BuildStatus; + includeDeploy: boolean; + createdAt: string; +} + +export interface BuildResult { + buildId: string; + success: boolean; + errors: string[]; + commitSha: string | null; + completedAt: string; +} + +export interface BuildProgress { + buildId: string; + stage: string; + percentage: number; + message: string; +} + +export type DeployStatus = + | "Pending" + | "AwaitingApproval" + | "Approved" + | "InProgress" + | "Completed" + | "Failed" + | "Rejected"; + +export interface ProjectSummary { + id: string; + name: string; + description: string; + repoUrl: string; + lastCommitSha: string | null; + createdAt: string; + updatedAt: string; +} + +export interface ProjectDetail extends ProjectSummary { + branch: string; + flow: FlowDocument | null; +} + +export interface PlcTarget { + id: string; + name: string; + amsNetId: string; + twinCatVersion: string; + labels: string[]; + groupId: string | null; + isProductionTarget: boolean; + deployLocked: boolean; +} + +export interface TargetGroup { + id: string; + name: string; + description: string; + targets: PlcTarget[]; +} + +export interface UserInfo { + id: string; + userName: string; + email: string; + displayName: string; + roles: string[]; +} + +export interface MonitorSession { + sessionId: string; + signalREndpoint: string; + authToken: string; + targetAmsNetId: string; +} + +export interface PlcVariableValue { + path: string; + value: unknown; + dataType: string; + timestamp: string; +} diff --git a/src/frontend/src/auth/AuthProvider.tsx b/src/frontend/src/auth/AuthProvider.tsx new file mode 100644 index 0000000..d0283fc --- /dev/null +++ b/src/frontend/src/auth/AuthProvider.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { createContext, type ReactNode } from "react"; +import type { UserInfo } from "../api/types"; + +export interface AuthContextType { + user: UserInfo | null; + isAuthenticated: boolean; + login: () => void; + logout: () => void; + token: string | null; +} + +export const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + // TODO: Initialize Keycloak, handle OIDC flow, manage token lifecycle + return ( + {}, + logout: () => {}, + token: null, + }} + > + {children} + + ); +} diff --git a/src/frontend/src/auth/ProtectedRoute.tsx b/src/frontend/src/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..8084ce8 --- /dev/null +++ b/src/frontend/src/auth/ProtectedRoute.tsx @@ -0,0 +1,16 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { Navigate } from "react-router-dom"; +import { useAuth } from "./useAuth"; +import type { ReactNode } from "react"; + +export function ProtectedRoute({ children }: { children: ReactNode }) { + const { isAuthenticated } = useAuth(); + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} diff --git a/src/frontend/src/auth/permissions.ts b/src/frontend/src/auth/permissions.ts new file mode 100644 index 0000000..666ec0b --- /dev/null +++ b/src/frontend/src/auth/permissions.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export type Permission = + | "project:view" + | "project:create" + | "project:edit" + | "project:delete" + | "build" + | "deploy" + | "target:manage" + | "monitor" + | "admin:users" + | "admin:system"; + +const rolePermissions: Record = { + viewer: ["project:view", "monitor"], + editor: ["project:view", "project:edit", "monitor"], + builder: ["project:view", "project:edit", "build", "monitor"], + deployer: [ + "project:view", + "project:edit", + "build", + "deploy", + "monitor", + "target:manage", + ], + admin: [ + "project:view", + "project:create", + "project:edit", + "project:delete", + "build", + "deploy", + "target:manage", + "monitor", + "admin:users", + "admin:system", + ], +}; + +export function hasPermission( + roles: string[], + permission: Permission, +): boolean { + return roles.some((role) => + rolePermissions[role]?.includes(permission), + ); +} diff --git a/src/frontend/src/auth/useAuth.ts b/src/frontend/src/auth/useAuth.ts new file mode 100644 index 0000000..24df505 --- /dev/null +++ b/src/frontend/src/auth/useAuth.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { useContext } from "react"; +import { AuthContext, type AuthContextType } from "./AuthProvider"; + +export function useAuth(): AuthContextType { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/src/frontend/src/features/admin/AdminPage.tsx b/src/frontend/src/features/admin/AdminPage.tsx new file mode 100644 index 0000000..82d18fb --- /dev/null +++ b/src/frontend/src/features/admin/AdminPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function AdminPage() { + return
AdminPage
; +} diff --git a/src/frontend/src/features/admin/UsersPage.tsx b/src/frontend/src/features/admin/UsersPage.tsx new file mode 100644 index 0000000..4b9425e --- /dev/null +++ b/src/frontend/src/features/admin/UsersPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function UsersPage() { + return
UsersPage
; +} diff --git a/src/frontend/src/features/admin/components/RoleAssignment.tsx b/src/frontend/src/features/admin/components/RoleAssignment.tsx new file mode 100644 index 0000000..06c6bb9 --- /dev/null +++ b/src/frontend/src/features/admin/components/RoleAssignment.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function RoleAssignment() { + return
RoleAssignment
; +} diff --git a/src/frontend/src/features/admin/components/UserEditDialog.tsx b/src/frontend/src/features/admin/components/UserEditDialog.tsx new file mode 100644 index 0000000..ca2aa57 --- /dev/null +++ b/src/frontend/src/features/admin/components/UserEditDialog.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function UserEditDialog() { + return
UserEditDialog
; +} diff --git a/src/frontend/src/features/admin/components/UserTable.tsx b/src/frontend/src/features/admin/components/UserTable.tsx new file mode 100644 index 0000000..97c6d94 --- /dev/null +++ b/src/frontend/src/features/admin/components/UserTable.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function UserTable() { + return
UserTable
; +} diff --git a/src/frontend/src/features/admin/hooks/useRoles.ts b/src/frontend/src/features/admin/hooks/useRoles.ts new file mode 100644 index 0000000..a0827ee --- /dev/null +++ b/src/frontend/src/features/admin/hooks/useRoles.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useRoles() { + return []; +} diff --git a/src/frontend/src/features/admin/hooks/useUsers.ts b/src/frontend/src/features/admin/hooks/useUsers.ts new file mode 100644 index 0000000..26b0f42 --- /dev/null +++ b/src/frontend/src/features/admin/hooks/useUsers.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useUsers() { + return []; +} diff --git a/src/frontend/src/features/deploy/components/ApprovalDialog.tsx b/src/frontend/src/features/deploy/components/ApprovalDialog.tsx new file mode 100644 index 0000000..fe0ef6e --- /dev/null +++ b/src/frontend/src/features/deploy/components/ApprovalDialog.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ApprovalDialog() { + return
ApprovalDialog
; +} diff --git a/src/frontend/src/features/deploy/components/DeployLockIndicator.tsx b/src/frontend/src/features/deploy/components/DeployLockIndicator.tsx new file mode 100644 index 0000000..3bd4d66 --- /dev/null +++ b/src/frontend/src/features/deploy/components/DeployLockIndicator.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function DeployLockIndicator() { + return
DeployLockIndicator
; +} diff --git a/src/frontend/src/features/deploy/components/DeployPanel.tsx b/src/frontend/src/features/deploy/components/DeployPanel.tsx new file mode 100644 index 0000000..ed40915 --- /dev/null +++ b/src/frontend/src/features/deploy/components/DeployPanel.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function DeployPanel() { + return
DeployPanel
; +} diff --git a/src/frontend/src/features/deploy/hooks/useDeploy.ts b/src/frontend/src/features/deploy/hooks/useDeploy.ts new file mode 100644 index 0000000..a163a94 --- /dev/null +++ b/src/frontend/src/features/deploy/hooks/useDeploy.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useDeploy() { + return {}; +} diff --git a/src/frontend/src/features/deploy/hooks/useDeployLock.ts b/src/frontend/src/features/deploy/hooks/useDeployLock.ts new file mode 100644 index 0000000..3a8ec21 --- /dev/null +++ b/src/frontend/src/features/deploy/hooks/useDeployLock.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useDeployLock() { + return { isLocked: false }; +} diff --git a/src/frontend/src/features/editor/EditorPage.tsx b/src/frontend/src/features/editor/EditorPage.tsx new file mode 100644 index 0000000..d65c678 --- /dev/null +++ b/src/frontend/src/features/editor/EditorPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function EditorPage() { + return
EditorPage
; +} diff --git a/src/frontend/src/features/editor/components/FlowCanvas.tsx b/src/frontend/src/features/editor/components/FlowCanvas.tsx new file mode 100644 index 0000000..9ce8b7f --- /dev/null +++ b/src/frontend/src/features/editor/components/FlowCanvas.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function FlowCanvas() { + return
FlowCanvas
; +} diff --git a/src/frontend/src/features/editor/components/NodeInspector.tsx b/src/frontend/src/features/editor/components/NodeInspector.tsx new file mode 100644 index 0000000..784ffd5 --- /dev/null +++ b/src/frontend/src/features/editor/components/NodeInspector.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function NodeInspector() { + return
NodeInspector
; +} diff --git a/src/frontend/src/features/editor/components/NodePalette.tsx b/src/frontend/src/features/editor/components/NodePalette.tsx new file mode 100644 index 0000000..581f339 --- /dev/null +++ b/src/frontend/src/features/editor/components/NodePalette.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function NodePalette() { + return
NodePalette
; +} diff --git a/src/frontend/src/features/editor/hooks/useAutoSave.ts b/src/frontend/src/features/editor/hooks/useAutoSave.ts new file mode 100644 index 0000000..202d989 --- /dev/null +++ b/src/frontend/src/features/editor/hooks/useAutoSave.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useAutoSave() { + // no-op +} diff --git a/src/frontend/src/features/editor/hooks/useFlowDocument.ts b/src/frontend/src/features/editor/hooks/useFlowDocument.ts new file mode 100644 index 0000000..e2e715b --- /dev/null +++ b/src/frontend/src/features/editor/hooks/useFlowDocument.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useFlowDocument() { + return null; +} diff --git a/src/frontend/src/features/editor/hooks/useNodeDragDrop.ts b/src/frontend/src/features/editor/hooks/useNodeDragDrop.ts new file mode 100644 index 0000000..38b2d08 --- /dev/null +++ b/src/frontend/src/features/editor/hooks/useNodeDragDrop.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useNodeDragDrop() { + return {}; +} diff --git a/src/frontend/src/features/editor/nodes/ComparisonNode.tsx b/src/frontend/src/features/editor/nodes/ComparisonNode.tsx new file mode 100644 index 0000000..d5bba16 --- /dev/null +++ b/src/frontend/src/features/editor/nodes/ComparisonNode.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ComparisonNode() { + return
ComparisonNode
; +} diff --git a/src/frontend/src/features/editor/nodes/CounterNode.tsx b/src/frontend/src/features/editor/nodes/CounterNode.tsx new file mode 100644 index 0000000..98f13d3 --- /dev/null +++ b/src/frontend/src/features/editor/nodes/CounterNode.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function CounterNode() { + return
CounterNode
; +} diff --git a/src/frontend/src/features/editor/nodes/InputNode.tsx b/src/frontend/src/features/editor/nodes/InputNode.tsx new file mode 100644 index 0000000..416b28e --- /dev/null +++ b/src/frontend/src/features/editor/nodes/InputNode.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function InputNode() { + return
InputNode
; +} diff --git a/src/frontend/src/features/editor/nodes/OutputNode.tsx b/src/frontend/src/features/editor/nodes/OutputNode.tsx new file mode 100644 index 0000000..412b27f --- /dev/null +++ b/src/frontend/src/features/editor/nodes/OutputNode.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function OutputNode() { + return
OutputNode
; +} diff --git a/src/frontend/src/features/editor/nodes/TimerNode.tsx b/src/frontend/src/features/editor/nodes/TimerNode.tsx new file mode 100644 index 0000000..5fe51b7 --- /dev/null +++ b/src/frontend/src/features/editor/nodes/TimerNode.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TimerNode() { + return
TimerNode
; +} diff --git a/src/frontend/src/features/editor/nodes/nodeRegistry.ts b/src/frontend/src/features/editor/nodes/nodeRegistry.ts new file mode 100644 index 0000000..531b37b --- /dev/null +++ b/src/frontend/src/features/editor/nodes/nodeRegistry.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { InputNode } from './InputNode'; +import { OutputNode } from './OutputNode'; +import { TimerNode } from './TimerNode'; +import { CounterNode } from './CounterNode'; +import { ComparisonNode } from './ComparisonNode'; + +export const nodeTypes: Record = { + input: InputNode, + output: OutputNode, + timer: TimerNode, + counter: CounterNode, + comparison: ComparisonNode, +}; diff --git a/src/frontend/src/features/editor/types/flow.types.ts b/src/frontend/src/features/editor/types/flow.types.ts new file mode 100644 index 0000000..2909f3f --- /dev/null +++ b/src/frontend/src/features/editor/types/flow.types.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export type { FlowDocument, FlowNode, FlowConnection } from '../../../api/types'; diff --git a/src/frontend/src/features/monitoring/MonitorPage.tsx b/src/frontend/src/features/monitoring/MonitorPage.tsx new file mode 100644 index 0000000..86cc217 --- /dev/null +++ b/src/frontend/src/features/monitoring/MonitorPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function MonitorPage() { + return
MonitorPage
; +} diff --git a/src/frontend/src/features/monitoring/components/ValueDisplay.tsx b/src/frontend/src/features/monitoring/components/ValueDisplay.tsx new file mode 100644 index 0000000..11ea7e4 --- /dev/null +++ b/src/frontend/src/features/monitoring/components/ValueDisplay.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ValueDisplay() { + return
ValueDisplay
; +} diff --git a/src/frontend/src/features/monitoring/components/VariableList.tsx b/src/frontend/src/features/monitoring/components/VariableList.tsx new file mode 100644 index 0000000..e8d62a6 --- /dev/null +++ b/src/frontend/src/features/monitoring/components/VariableList.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function VariableList() { + return
VariableList
; +} diff --git a/src/frontend/src/features/monitoring/components/VariableSubscriber.tsx b/src/frontend/src/features/monitoring/components/VariableSubscriber.tsx new file mode 100644 index 0000000..aab81ed --- /dev/null +++ b/src/frontend/src/features/monitoring/components/VariableSubscriber.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function VariableSubscriber() { + return
VariableSubscriber
; +} diff --git a/src/frontend/src/features/monitoring/hooks/useMonitorSession.ts b/src/frontend/src/features/monitoring/hooks/useMonitorSession.ts new file mode 100644 index 0000000..538c392 --- /dev/null +++ b/src/frontend/src/features/monitoring/hooks/useMonitorSession.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useMonitorSession() { + return null; +} diff --git a/src/frontend/src/features/monitoring/hooks/usePlcData.ts b/src/frontend/src/features/monitoring/hooks/usePlcData.ts new file mode 100644 index 0000000..b8ed0d2 --- /dev/null +++ b/src/frontend/src/features/monitoring/hooks/usePlcData.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function usePlcData() { + return {}; +} diff --git a/src/frontend/src/features/projects/ProjectCreatePage.tsx b/src/frontend/src/features/projects/ProjectCreatePage.tsx new file mode 100644 index 0000000..488d415 --- /dev/null +++ b/src/frontend/src/features/projects/ProjectCreatePage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ProjectCreatePage() { + return
ProjectCreatePage
; +} diff --git a/src/frontend/src/features/projects/ProjectListPage.tsx b/src/frontend/src/features/projects/ProjectListPage.tsx new file mode 100644 index 0000000..bdcb9b6 --- /dev/null +++ b/src/frontend/src/features/projects/ProjectListPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ProjectListPage() { + return
ProjectListPage
; +} diff --git a/src/frontend/src/features/projects/components/ProjectCard.tsx b/src/frontend/src/features/projects/components/ProjectCard.tsx new file mode 100644 index 0000000..3c2d09a --- /dev/null +++ b/src/frontend/src/features/projects/components/ProjectCard.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function ProjectCard() { + return
ProjectCard
; +} diff --git a/src/frontend/src/features/projects/components/TemplateSelector.tsx b/src/frontend/src/features/projects/components/TemplateSelector.tsx new file mode 100644 index 0000000..46fbf6e --- /dev/null +++ b/src/frontend/src/features/projects/components/TemplateSelector.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TemplateSelector() { + return
TemplateSelector
; +} diff --git a/src/frontend/src/features/projects/hooks/useProjects.ts b/src/frontend/src/features/projects/hooks/useProjects.ts new file mode 100644 index 0000000..b11f6f4 --- /dev/null +++ b/src/frontend/src/features/projects/hooks/useProjects.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useProjects() { + return []; +} diff --git a/src/frontend/src/features/projects/hooks/useTemplates.ts b/src/frontend/src/features/projects/hooks/useTemplates.ts new file mode 100644 index 0000000..66064a7 --- /dev/null +++ b/src/frontend/src/features/projects/hooks/useTemplates.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useTemplates() { + return []; +} diff --git a/src/frontend/src/features/targets/TargetListPage.tsx b/src/frontend/src/features/targets/TargetListPage.tsx new file mode 100644 index 0000000..66305de --- /dev/null +++ b/src/frontend/src/features/targets/TargetListPage.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TargetListPage() { + return
TargetListPage
; +} diff --git a/src/frontend/src/features/targets/components/TargetCard.tsx b/src/frontend/src/features/targets/components/TargetCard.tsx new file mode 100644 index 0000000..cc2dfca --- /dev/null +++ b/src/frontend/src/features/targets/components/TargetCard.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TargetCard() { + return
TargetCard
; +} diff --git a/src/frontend/src/features/targets/components/TargetGroupPanel.tsx b/src/frontend/src/features/targets/components/TargetGroupPanel.tsx new file mode 100644 index 0000000..ec152e9 --- /dev/null +++ b/src/frontend/src/features/targets/components/TargetGroupPanel.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TargetGroupPanel() { + return
TargetGroupPanel
; +} diff --git a/src/frontend/src/features/targets/components/TargetLabelEditor.tsx b/src/frontend/src/features/targets/components/TargetLabelEditor.tsx new file mode 100644 index 0000000..49422d2 --- /dev/null +++ b/src/frontend/src/features/targets/components/TargetLabelEditor.tsx @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function TargetLabelEditor() { + return
TargetLabelEditor
; +} diff --git a/src/frontend/src/features/targets/hooks/useTargetGroups.ts b/src/frontend/src/features/targets/hooks/useTargetGroups.ts new file mode 100644 index 0000000..79ec50d --- /dev/null +++ b/src/frontend/src/features/targets/hooks/useTargetGroups.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useTargetGroups() { + return []; +} diff --git a/src/frontend/src/features/targets/hooks/useTargets.ts b/src/frontend/src/features/targets/hooks/useTargets.ts new file mode 100644 index 0000000..64a869a --- /dev/null +++ b/src/frontend/src/features/targets/hooks/useTargets.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function useTargets() { + return []; +} diff --git a/src/frontend/src/layout/AppLayout.tsx b/src/frontend/src/layout/AppLayout.tsx new file mode 100644 index 0000000..a0cea36 --- /dev/null +++ b/src/frontend/src/layout/AppLayout.tsx @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { Outlet } from "react-router-dom"; +import { Sidebar } from "./Sidebar"; +import { Header } from "./Header"; + +export function AppLayout() { + return ( +
+ +
+
+
+ +
+
+
+ ); +} diff --git a/src/frontend/src/layout/Header.tsx b/src/frontend/src/layout/Header.tsx new file mode 100644 index 0000000..eb25b93 --- /dev/null +++ b/src/frontend/src/layout/Header.tsx @@ -0,0 +1,7 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function Header() { + // TODO: User info, logout button + return
; +} diff --git a/src/frontend/src/layout/Sidebar.tsx b/src/frontend/src/layout/Sidebar.tsx new file mode 100644 index 0000000..3dc2510 --- /dev/null +++ b/src/frontend/src/layout/Sidebar.tsx @@ -0,0 +1,7 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +export function Sidebar() { + // TODO: Navigation links (Projects, Targets, Admin) + return