diff --git a/CLAUDE.md b/CLAUDE.md index 0a5d7f2..3f065bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,17 +103,115 @@ make coverage-html make clean-coverage ``` +### Web Control Panel Development + +```bash +# Build and run axon_panel web interface +cd apps/axon_panel +npm install # Install dependencies (first time only) +npm run dev # Start development server (http://localhost:5173) +npm run build # Build for production (outputs to dist/) +npm run preview # Preview production build +``` + +## Application Architecture + +Axon consists of four main components that work together: + +| Component | Purpose | Technology | Location | +|-----------|---------|------------|----------| +| **axon_recorder** | Core recording engine with HTTP RPC API | C++17 | [apps/axon_recorder/](apps/axon_recorder/) | +| **axon_transfer** | S3 transfer daemon (standalone) | C++17 | [apps/axon_transfer/](apps/axon_transfer/) | +| **axon_panel** | Web-based control UI (frontend only) | Vue 3 + Vite | [apps/axon_panel/](apps/axon_panel/) | +| **axon_config** | Robot initialization and config collection | C++17 | [apps/axon_config/](apps/axon_config/) | + +### Application Interaction + +``` +┌─────────────────────┐ +│ axon_config │ Robot initialization (one-time setup) +│ (CLI Tool) │ → Collects robot type, SN, sensor config, URDF +└─────────────────────┘ + │ + │ Config files + ▼ +┌─────────────────────┐ HTTP RPC ┌─────────────────────┐ +│ axon_panel │◄──────────────────►│ axon_recorder │ +│ (Vue 3 Web UI) │ State Control │ (C++ Backend) │ +│ - Monitor state │ - config │ - HTTP RPC Server │ +│ - Control buttons │ - begin │ - Plugin Loader │ +│ - View stats │ - pause/resume │ - MCAP Writer │ +│ - Activity log │ - finish/cancel │ - Worker Threads │ +└─────────────────────┘ └──────────┬──────────┘ + │ + │ Upload requests + ▼ + ┌─────────────────────┐ + │ axon_transfer │ + │ (Transfer Daemon) │ + │ - S3 multipart │ + │ - Retry logic │ + │ - State recovery │ + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ S3 Storage │ + └─────────────────────┘ +``` + +**Panel Control Flow:** `axon_panel` (Vue 3 frontend) sends HTTP RPC commands to `axon_recorder` to control state transitions: +- `POST /rpc/config` → IDLE → READY +- `POST /rpc/begin` → READY → RECORDING +- `POST /rpc/pause` → RECORDING → PAUSED +- `POST /rpc/resume` → PAUSED → RECORDING +- `POST /rpc/finish` → RECORDING/PAUSED → IDLE + +**Note on Current Status:** +- `axon_recorder`: Fully implemented C++ application +- `axon_transfer`: Standalone daemon pending design; currently uses [core/axon_uploader/](core/axon_uploader/) library integrated into recorder +- `axon_panel`: Fully implemented Vue 3 SPA at [apps/axon_panel/](apps/axon_panel/) +- `axon_config`: Placeholder only; CLI interface and functionality pending design + +### axon_panel - Web Control Panel + +**Location:** [apps/axon_panel/](apps/axon_panel/) + +**Purpose:** Browser-based interface for monitoring and controlling the recorder + +**Features:** +- Real-time state monitoring and statistics +- Visual state machine diagram with Vue Flow +- Recording control (config/begin/pause/resume/finish/cancel) +- Activity logging with color-coded messages +- Responsive design (desktop + mobile) + +**Development:** +```bash +cd apps/axon_panel +npm install +npm run dev # Development server (http://localhost:5173) +npm run build # Production build to dist/ +``` + +**Key Files:** +- `apps/axon_panel/src/App.vue` - Root component with state management +- `apps/axon_panel/src/api/rpc.js` - RPC API client +- `apps/axon_panel/src/components/` - Vue components (StatePanel, ControlPanel, etc.) + +**See:** [docs/designs/frontend-design.md](docs/designs/frontend-design.md) for complete architecture + ## High-Level Architecture The system follows a layered architecture with a 4-state task-centric FSM and a plugin-based middleware integration layer: ``` -Server/Fleet Manager (ros-bridge) → Recording Services → State Machine → MCAP Writer - ↓ ↓ ↓ - HTTP Callbacks Worker Threads SPSC Queues - ↓ ↓ ↓ - CachedRecordingConfig Start/Finish Lock-free - (YAML Configuration) Notify Message Transfer +Server/Fleet Manager → Recording Services → State Machine → MCAP Writer + ↓ ↓ ↓ ↓ + HTTP RPC API HTTP Callbacks Worker Threads SPSC Queues + ↓ ↓ ↓ ↓ + Task Config Start/Finish Lock-free Message Transfer + (YAML) Notify Transfer ``` ### Plugin-Based Middleware Architecture @@ -132,23 +230,34 @@ Axon/ ├── core/ # Middleware-agnostic core libraries │ ├── axon_mcap/ # MCAP writer (no ROS dependencies) │ ├── axon_logging/ # Logging infrastructure (no ROS dependencies) -│ └── axon_uploader/ # S3 uploader (no ROS dependencies) +│ └── axon_uploader/ # S3 uploader library (no ROS dependencies) │ ├── middlewares/ # Middleware-specific plugins and filters │ ├── ros1/ # ROS1 (Noetic) plugin → libaxon_ros1.so │ ├── ros2/ # ROS2 (Humble/Jazzy/Rolling) plugin → libaxon_ros2.so -│ │ └── src/ros2_plugin/ # ROS2 plugin implementation │ ├── zenoh/ # Zenoh plugin → libaxon_zenoh.so +│ ├── mock/ # Mock plugin for testing (no ROS required) +│ │ └── src/mock_plugin/ # Mock plugin implementation │ └── filters/ # Data processing filters (shared across plugins) -│ └── depthlitez/ # Depth image compression library +│ ├── include/ # Depth compressor header +│ ├── src/ # Depth compressor implementation +│ └── depthlitez/ # DepthLiteZ library (private submodule) │ ├── apps/ # Main applications │ ├── axon_recorder/ # Plugin loader and HTTP RPC server -│ └── plugin_example/ # Example plugin implementation +│ ├── axon_panel/ # Vue 3 web control panel +│ ├── axon_config/ # Robot configuration CLI tool (placeholder) +│ └── axon_transfer/ # S3 transfer daemon (placeholder) +│ +├── python/ # Python client library +│ └── axon_client/ # Async/sync HTTP client │ └── docs/designs/ # Design documents ├── rpc-api-design.md # HTTP RPC API specification - └── depth-compression-filter.md # Depth compression design + ├── frontend-design.md # AxonPanel web UI architecture + ├── middleware-plugin-architecture-design.md # Plugin architecture + ├── license-management-design.md # REUSE licensing + └── depth-compression-filter.md # Depth compression design ``` **Plugin ABI Interface:** @@ -165,6 +274,13 @@ The plugin interface is defined in [apps/axon_recorder/plugin_loader.hpp](apps/a 5. Middleware-specific bugs are isolated to plugin code 6. Filters in `middlewares/filters/` can be shared across plugins +**Mock Plugin for Testing:** +The mock plugin ([middlewares/mock/src/mock_plugin/](middlewares/mock/src/mock_plugin/)) provides a reference implementation for E2E testing without ROS dependencies: +- Simulates message publishing and subscription +- Implements the full plugin C ABI interface +- Enables CI testing without requiring ROS installation +- Test scripts: [test_e2e_with_mock.sh](middlewares/mock/test_e2e_with_mock.sh), [test_full_workflow.sh](middlewares/mock/test_full_workflow.sh) + ### State Machine ``` @@ -448,6 +564,34 @@ Keep descriptions under 72 characters. Use imperative mood ("add" not "added"). **Note**: With the new plugin architecture, most ROS-specific code is isolated within `middlewares/ros1/` and `middlewares/ros2/` plugins. Core code in `core/` and `apps/` should remain middleware-agnostic. +### License Management (REUSE) + +This project uses [REUSE](https://reuse.software/) for license compliance. All source files must include SPDX headers: + +```c +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ +``` + +**Adding licenses to new files:** +```bash +# Auto-add headers to C/C++ files +reuse annotate --year 2026 --copyright "ArcheBase" --license "MulanPSL-2.0" --style c + +# Check compliance +reuse lint +``` + +**Project-wide rules in [REUSE.toml](REUSE.toml):** +- Frontend assets (`apps/axon_panel/**`) are covered by a single annotation +- Mock files follow the pattern `**/*_mock.*` +- Dependencies and build artifacts are excluded + +**Note**: With the new plugin architecture, most ROS-specific code is isolated within `middlewares/ros1/` and `middlewares/ros2/` plugins. Core code in `core/` and `apps/` should remain middleware-agnostic. + ## Refactoring Guidelines **When refactoring code in this codebase, follow these principles:** @@ -604,17 +748,21 @@ grep -r "AXON_ROS1\|AXON_ROS2" build/ | Purpose | Location | |---------|----------| | Core libraries | `core/axon_*/` | -| ROS1 plugin | `middlewares/ros1/src/ros1_plugin/` (CMake) | -| ROS2 plugin | `middlewares/ros2/src/ros2_plugin/` (CMake) | -| Zenoh plugin | `middlewares/zenoh/` (CMake) | +| ROS1 plugin | `middlewares/ros1/` | +| ROS2 plugin | `middlewares/ros2/` | +| Zenoh plugin | `middlewares/zenoh/` | +| Mock plugin (testing) | `middlewares/mock/src/mock_plugin/` | | Filters | `middlewares/filters/` (shared data processing) | -| Depth compression | `middlewares/filters/depthlitez/` | -| Main app (HTTP RPC) | `apps/axon_recorder/` | -| Plugin example | `apps/plugin_example/` | +| Depth compression | `middlewares/filters/depthlitez/` (private) | +| Recorder app (HTTP RPC) | `apps/axon_recorder/` | +| Transfer daemon | `apps/axon_transfer/` | +| Web control panel | `apps/axon_panel/` (Vue 3) | +| Config tool CLI | `apps/axon_config/` | | Plugin ABI interface | `apps/axon_recorder/plugin_loader.hpp` | | Tests | `*/test/` or `*/test_*.cpp` | | CMake modules | `cmake/` | | Design docs | `docs/designs/` | +| Python client | `python/axon_client/` | ### Plugin Development @@ -623,9 +771,28 @@ To create a new middleware plugin: 1. **Define the plugin ABI** - Implement the C interface in [apps/axon_recorder/plugin_loader.hpp](apps/axon_recorder/plugin_loader.hpp) 2. **Export descriptor function** - Each plugin must export `axon_get_plugin_descriptor()` 3. **Compile as shared library** - Build as `.so` with C linkage for ABI functions -4. **Example reference** - See [apps/plugin_example/](apps/plugin_example/) and [middlewares/ros2/src/ros2_plugin/](middlewares/ros2/src/ros2_plugin/) +4. **Example reference** - See [middlewares/mock/src/mock_plugin/](middlewares/mock/src/mock_plugin/) for a minimal plugin, or [middlewares/ros2/](middlewares/ros2/) for a full ROS2 implementation **ABI Versioning:** - `AxonPluginDescriptor` contains `abi_version_major` and `abi_version_minor` - Always verify compatibility before loading plugins - Reserve space in vtable for future extensions + +### Application Development Workflows + +**Current Status:** +- `axon_panel`: Fully implemented Vue 3 SPA at [apps/axon_panel/](apps/axon_panel/) +- `axon_transfer`: Standalone daemon pending design; currently uses [core/axon_uploader/](core/axon_uploader/) library integrated into recorder +- `axon_config`: Placeholder only; CLI interface and functionality pending design + +**axon_panel Development:** +```bash +# Terminal 1: Start recorder +./build/axon_recorder/axon_recorder --plugin ./build/middlewares/libaxon_ros2.so + +# Terminal 2: Start panel dev server +cd apps/axon_panel +npm run dev # Starts at http://localhost:5173 +``` + +**See:** [docs/designs/frontend-design.md](docs/designs/frontend-design.md) for complete panel architecture diff --git a/Makefile b/Makefile index a08a767..c5c00d5 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,13 @@ help: @printf "%s\n" " $(BLUE)make app-axon-recorder$(NC) - Build axon_recorder plugin loader" @printf "%s\n" " $(BLUE)make app-plugin-example$(NC) - Build plugin example" @echo "" + @printf "%s\n" "$(YELLOW)Mock Middleware (Testing):$(NC)" + @printf "%s\n" " $(BLUE)make build-mock$(NC) - Build mock middleware plugin" + @printf "%s\n" " $(BLUE)make test-mock-e2e$(NC) - Run mock plugin E2E test (standalone)" + @printf "%s\n" " $(BLUE)make test-mock-load$(NC) - Run mock plugin load test" + @printf "%s\n" " $(BLUE)make test-mock-integration$(NC) - Run mock middleware integration E2E test" + @printf "%s\n" " $(BLUE)make test-mock-all$(NC) - Run all mock middleware tests" + @echo "" @printf "%s\n" "$(YELLOW)ROS Middlewares:$(NC)" @printf "%s\n" " $(BLUE)make build$(NC) - Build (auto-detects ROS1/ROS2)" @printf "%s\n" " $(BLUE)make build-ros1$(NC) - Build ROS1 (Noetic)" @@ -416,6 +423,46 @@ app-axon-recorder: build-core app-plugin-example: build-core @printf "%s\n" "$(GREEN)✓ plugin example built$(NC)" +# ============================================================================= +# Mock Middleware Targets +# ============================================================================= + +# Build mock middleware +build-mock: + @printf "%s\n" "$(YELLOW)Building mock middleware...$(NC)" + @mkdir -p middlewares/mock/src/mock_plugin/build + @cd middlewares/mock/src/mock_plugin/build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ + -DCMAKE_INSTALL_PREFIX=$(PROJECT_ROOT)/middlewares/mock/install && \ + cmake --build . -j$(NPROC) + @printf "%s\n" "$(GREEN)✓ Mock middleware built$(NC)" + +# Test mock plugin E2E +test-mock-e2e: build-mock + @printf "%s\n" "$(YELLOW)Running mock plugin E2E test...$(NC)" + @cd middlewares/mock/src/mock_plugin/build && ./test_mock_plugin_e2e + @printf "%s\n" "$(GREEN)✓ Mock plugin E2E test passed$(NC)" + +# Test mock plugin load +test-mock-load: build-mock + @printf "%s\n" "$(YELLOW)Running mock plugin load test...$(NC)" + @cd middlewares/mock/src/mock_plugin/build && \ + ./test_mock_plugin_load ./libmock_plugin.so + @printf "%s\n" "$(GREEN)✓ Mock plugin load test passed$(NC)" + +# Test mock middleware integration with axon_recorder +test-mock-integration: build-mock build-core + @printf "%s\n" "$(YELLOW)Running mock middleware integration E2E test...$(NC)" + @cd middlewares/mock && ./test_e2e_with_mock.sh + @printf "%s\n" "$(GREEN)✓ Mock middleware integration test passed$(NC)" + +# Run all mock middleware tests +test-mock-all: build-mock + @printf "%s\n" "$(YELLOW)Running all mock middleware tests...$(NC)" + @cd middlewares/mock && ./test_full_workflow.sh + @printf "%s\n" "$(GREEN)✓ All mock middleware tests passed$(NC)" + # ============================================================================= # ROS Middleware Targets # ============================================================================= @@ -483,6 +530,9 @@ test: test-core clean: @printf "%s\n" "$(YELLOW)Cleaning build artifacts...$(NC)" @rm -rf $(BUILD_DIR) $(COVERAGE_DIR) + @cd middlewares/ros2 && rm -rf build install log 2>/dev/null || true + @cd middlewares/ros1 && catkin clean --yes 2>/dev/null || true + @rm -rf middlewares/mock/src/mock_plugin/build middlewares/mock/install 2>/dev/null || true @printf "%s\n" "$(GREEN)✓ All build artifacts cleaned$(NC)" # Install target @@ -1174,4 +1224,3 @@ format-ci: printf "%s\n" "$(YELLOW)Or format files individually: $(CLANG_FORMAT) -i $(NC)" && \ exit 1) @printf "%s\n" "$(GREEN)✓ Code formatting check passed$(NC)" - diff --git a/REUSE.toml b/REUSE.toml index 2b732ba..d915ad3 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -161,8 +161,8 @@ SPDX-License-Identifier = "NO-COPYRIGHT" [[annotations]] path = "**/*_mock.*" -SPDX-FileCopyrightText = "NO-COPYRIGHT" -SPDX-License-Identifier = "NO-COPYRIGHT" +SPDX-FileCopyrightText = "Copyright (c) 2026 ArcheBase" +SPDX-License-Identifier = "MulanPSL-2.0" # Ignore dependency directories [[annotations]] @@ -174,3 +174,10 @@ SPDX-License-Identifier = "NO-COPYRIGHT" path = "install/**" SPDX-FileCopyrightText = "NO-COPYRIGHT" SPDX-License-Identifier = "NO-COPYRIGHT" + +# Frontend assets (JavaScript, CSS, HTML, Vue components) +[[annotations]] +path = "apps/axon_panel/**" +precedence = "override" +SPDX-FileCopyrightText = "Copyright (c) 2026 ArcheBase" +SPDX-License-Identifier = "MulanPSL-2.0" diff --git a/apps/axon_config/CMakeLists.txt b/apps/axon_config/CMakeLists.txt new file mode 100644 index 0000000..607f45b --- /dev/null +++ b/apps/axon_config/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(axon_config LANGUAGES CXX) + +# C++17 standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# TODO: Add executable, CLI argument parsing, and configuration modules diff --git a/apps/axon_config/README.md b/apps/axon_config/README.md new file mode 100644 index 0000000..239c4ce --- /dev/null +++ b/apps/axon_config/README.md @@ -0,0 +1,17 @@ +# Axon Config Application + +**Status:** Placeholder - Design pending + +## Purpose + +Robot initialization and configuration collection tool. + +## Design Notes + +TODO: Define CLI interface for: +- Robot type discovery +- Serial number collection +- Sensor configuration +- URDF file collection + +This tool will implement configuration management functionality directly (no separate library). diff --git a/apps/axon_config/src/main.cpp b/apps/axon_config/src/main.cpp new file mode 100644 index 0000000..bb59ddc --- /dev/null +++ b/apps/axon_config/src/main.cpp @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + +// Axon Config - Main Entry Point +// Placeholder implementation - Design pending + +#include + +int main(int argc, char** argv) { + std::cout << "Axon Config - Robot initialization tool - Design pending\n"; + return 0; +} diff --git a/apps/axon_panel/.gitignore b/apps/axon_panel/.gitignore new file mode 100644 index 0000000..54117ee --- /dev/null +++ b/apps/axon_panel/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.DS_Store +*.log +.env +.env.local +.env.production diff --git a/apps/axon_panel/README.md b/apps/axon_panel/README.md new file mode 100644 index 0000000..61468e0 --- /dev/null +++ b/apps/axon_panel/README.md @@ -0,0 +1,105 @@ +# Axon Webtool - RPC Debugger + +A Vue 3-based web interface for debugging and controlling the Axon Recorder HTTP RPC API. + +## Features + +- **Real-time State Monitoring**: View current recorder state and task configuration +- **Recording Statistics**: Monitor messages received, written, dropped, and file size +- **Control Panel**: Execute RPC commands (config, begin, pause, resume, finish, cancel) +- **Activity Log**: Track all RPC interactions with timestamps +- **Visual State Transitions**: See available state transitions + +## Prerequisites + +- Node.js 18+ and npm/yarn +- Axon Recorder running with HTTP RPC server on port 8080 + +## Installation + +```bash +cd tools/webtool +npm install +``` + +## Development + +```bash +# Start development server (runs on port 3000) +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## Usage + +1. Start the Axon Recorder with HTTP RPC enabled: + ```bash + axon_recorder --config config.yaml + ``` + +2. In another terminal, start the webtool: + ```bash + cd tools/webtool + npm run dev + ``` + +3. Open your browser to `http://localhost:3000` + +## RPC Endpoints + +The webtool interacts with the following RPC endpoints: + +- `GET /health` - Health check +- `GET /rpc/state` - Get current state +- `GET /rpc/stats` - Get recording statistics +- `POST /rpc/config` - Set task configuration +- `POST /rpc/begin` - Start recording +- `POST /rpc/finish` - Finish recording +- `POST /rpc/pause` - Pause recording +- `POST /rpc/resume` - Resume recording +- `POST /rpc/cancel` - Cancel recording +- `POST /rpc/clear` - Clear configuration +- `POST /rpc/quit` - Quit program + +See [docs/designs/rpc-api-design.md](../../docs/designs/rpc-api-design.md) for detailed API documentation. + +## Configuration + +The webtool proxies requests to the Axon Recorder at `http://localhost:8080`. To change the backend URL: + +```bash +# Set environment variable +export VITE_API_BASE_URL=http://your-server:port +npm run dev +``` + +## Project Structure + +``` +tools/webtool/ +├── src/ +│ ├── api/ # API client +│ │ └── rpc.js +│ ├── components/ # Vue components +│ │ ├── ConnectionStatus.vue +│ │ ├── StatePanel.vue +│ │ ├── ControlPanel.vue +│ │ ├── ConfigPanel.vue +│ │ └── LogPanel.vue +│ ├── App.vue # Root component +│ └── main.js # Entry point +├── index.html +├── package.json +└── vite.config.js +``` + +## Technology Stack + +- Vue 3 - Progressive JavaScript framework +- Vite - Build tool and dev server +- Axios - HTTP client diff --git a/apps/axon_panel/index.html b/apps/axon_panel/index.html new file mode 100644 index 0000000..56bd161 --- /dev/null +++ b/apps/axon_panel/index.html @@ -0,0 +1,35 @@ + + + + + + + + AxonPanel + + + +
+ + + diff --git a/apps/axon_panel/package-lock.json b/apps/axon_panel/package-lock.json new file mode 100644 index 0000000..c515e54 --- /dev/null +++ b/apps/axon_panel/package-lock.json @@ -0,0 +1,1729 @@ +{ + "name": "axon-panel", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "axon-panel", + "version": "0.1.0", + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue-flow/background": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz", + "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/controls": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz", + "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.1.tgz", + "integrity": "sha512-3IxaMBLvWRbznZ4CuK0kVhp4Y4lCDQx9nhi48Swp6PwPw29KNhmiKd2kaBogYeWjGLb/tLjlE9V0s3jEmKCYWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/apps/axon_panel/package.json b/apps/axon_panel/package.json new file mode 100644 index 0000000..73b5c8a --- /dev/null +++ b/apps/axon_panel/package.json @@ -0,0 +1,21 @@ +{ + "name": "axon-panel", + "version": "0.1.0", + "description": "Axon Recorder Web Control Panel", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/apps/axon_panel/run.sh b/apps/axon_panel/run.sh new file mode 100644 index 0000000..dce8897 --- /dev/null +++ b/apps/axon_panel/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Quick start script for Axon Webtool + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Check if node_modules exists +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + npm install +fi + +echo "Starting Axon Webtool development server..." +echo "Open http://localhost:3000 in your browser" +echo "" +echo "Make sure Axon Recorder is running on port 8080" +echo "" + +npm run dev diff --git a/apps/axon_panel/src/App.vue b/apps/axon_panel/src/App.vue new file mode 100644 index 0000000..dd6af6c --- /dev/null +++ b/apps/axon_panel/src/App.vue @@ -0,0 +1,502 @@ + + + + + + + diff --git a/apps/axon_panel/src/api/rpc.js b/apps/axon_panel/src/api/rpc.js new file mode 100644 index 0000000..264a8bd --- /dev/null +++ b/apps/axon_panel/src/api/rpc.js @@ -0,0 +1,122 @@ +import axios from 'axios' + +const BASE_URL = import.meta.env.VITE_API_BASE_URL || '' + +// Create axios instance with interceptors for debugging +const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json' + } +}) + +// Request interceptor +apiClient.interceptors.request.use( + (config) => { + if (import.meta.env.DEV) { + console.log('[API Request]', config.method.toUpperCase(), config.url, config.data) + } + return config + }, + (error) => { + if (import.meta.env.DEV) { + console.error('[API Request Error]', error) + } + return Promise.reject(error) + } +) + +// Response interceptor +apiClient.interceptors.response.use( + (response) => { + if (import.meta.env.DEV) { + console.log('[API Response]', response.config.url, response.data) + } + return response + }, + (error) => { + if (import.meta.env.DEV) { + console.error('[API Response Error]', error.config?.url, error.message) + if (error.response) { + console.error('[API Error Response]', error.response.data) + } + } + return Promise.reject(error) + } +) + +export const rpcApi = { + // Health check + async health() { + const response = await apiClient.get('/health') + return response.data + }, + + // Get current state + async getState() { + const response = await apiClient.get('/rpc/state') + return response.data + }, + + // Get statistics + async getStats() { + const response = await apiClient.get('/rpc/stats') + return response.data + }, + + // Set task configuration + async setConfig(taskConfig) { + const response = await apiClient.post('/rpc/config', { + task_config: taskConfig + }) + return response.data + }, + + // Begin recording + async begin(taskId) { + const response = await apiClient.post('/rpc/begin', { + task_id: taskId + }) + return response.data + }, + + // Finish recording + async finish(taskId) { + const response = await apiClient.post('/rpc/finish', { + task_id: taskId + }) + return response.data + }, + + // Pause recording + async pause() { + const response = await apiClient.post('/rpc/pause') + return response.data + }, + + // Resume recording + async resume() { + const response = await apiClient.post('/rpc/resume') + return response.data + }, + + // Cancel recording + async cancel(taskId) { + const response = await apiClient.post('/rpc/cancel', { + task_id: taskId + }) + return response.data + }, + + // Clear configuration + async clear() { + const response = await apiClient.post('/rpc/clear') + return response.data + }, + + // Quit program + async quit() { + const response = await apiClient.post('/rpc/quit') + return response.data + } +} diff --git a/apps/axon_panel/src/components/ConfigPanel.vue b/apps/axon_panel/src/components/ConfigPanel.vue new file mode 100644 index 0000000..876f0d7 --- /dev/null +++ b/apps/axon_panel/src/components/ConfigPanel.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/apps/axon_panel/src/components/ConnectionStatus.vue b/apps/axon_panel/src/components/ConnectionStatus.vue new file mode 100644 index 0000000..30b4af1 --- /dev/null +++ b/apps/axon_panel/src/components/ConnectionStatus.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/apps/axon_panel/src/components/ControlPanel.vue b/apps/axon_panel/src/components/ControlPanel.vue new file mode 100644 index 0000000..72654f6 --- /dev/null +++ b/apps/axon_panel/src/components/ControlPanel.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/apps/axon_panel/src/components/LogPanel.vue b/apps/axon_panel/src/components/LogPanel.vue new file mode 100644 index 0000000..bd2cac8 --- /dev/null +++ b/apps/axon_panel/src/components/LogPanel.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/apps/axon_panel/src/components/StateMachineDiagram.vue b/apps/axon_panel/src/components/StateMachineDiagram.vue new file mode 100644 index 0000000..d2600c7 --- /dev/null +++ b/apps/axon_panel/src/components/StateMachineDiagram.vue @@ -0,0 +1,485 @@ + + + + + diff --git a/apps/axon_panel/src/components/StatePanel.vue b/apps/axon_panel/src/components/StatePanel.vue new file mode 100644 index 0000000..4e76299 --- /dev/null +++ b/apps/axon_panel/src/components/StatePanel.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/apps/axon_panel/src/edge-label-fix.css b/apps/axon_panel/src/edge-label-fix.css new file mode 100644 index 0000000..1b17e25 --- /dev/null +++ b/apps/axon_panel/src/edge-label-fix.css @@ -0,0 +1,103 @@ +/* Global CSS for Vue Flow edge labels - force black color */ +/* This file is NOT scoped to ensure it applies to Vue Flow's dynamically generated elements */ + +/* Force all edge label text to be black */ +.vue-flow__edgeLabel text, +.vue-flow__edgeLabel tspan, +.vue-flow__edge-textwrapper text, +.vue-flow__edge-textwrapper tspan, +svg .vue-flow__edgeLabel text, +svg .vue-flow__edgeLabel tspan, +svg .vue-flow__edge-textwrapper text, +svg .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Additional selectors for edge types */ +.vue-flow__edge.forward-edge .vue-flow__edgeLabel text, +.vue-flow__edge.forward-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.forward-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.forward-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.forward-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.forward-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.forward-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.forward-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel text, +.vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.return-edge .vue-flow__edgeLabel text, +.vue-flow__edge.return-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.return-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.return-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.return-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.return-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.return-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.return-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.cancel-edge .vue-flow__edgeLabel text, +.vue-flow__edge.cancel-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.cancel-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.cancel-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Hide label backgrounds */ +.vue-flow__edgeLabelbg, +.vue-flow__edge-textbg, +svg .vue-flow__edgeLabelbg, +svg .vue-flow__edge-textbg { + display: none !important; + opacity: 0 !important; +} + +/* Additional high-specificity selectors */ +html body .vue-flow__edgeLabel text, +html body .vue-flow__edgeLabel tspan, +html body .vue-flow__edge-textwrapper text, +html body .vue-flow__edge-textwrapper tspan, +html body svg .vue-flow__edgeLabel text, +html body svg .vue-flow__edgeLabel tspan, +html body svg .vue-flow__edge-textwrapper text, +html body svg .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Target all text elements in Vue Flow */ +div[id*="vue-flow"] text, +div[id*="vue-flow"] tspan, +svg[id*="vue-flow"] text, +svg[id*="vue-flow"] tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + diff --git a/apps/axon_panel/src/main.js b/apps/axon_panel/src/main.js new file mode 100644 index 0000000..1bce120 --- /dev/null +++ b/apps/axon_panel/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './edge-label-fix.css' + +createApp(App).mount('#app') diff --git a/apps/axon_panel/vite.config.js b/apps/axon_panel/vite.config.js new file mode 100644 index 0000000..562c11e --- /dev/null +++ b/apps/axon_panel/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + proxy: { + '/rpc': { + target: 'http://localhost:8080', + changeOrigin: true + }, + '/health': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + } +}) diff --git a/apps/axon_recorder/http_server.cpp b/apps/axon_recorder/http_server.cpp index fa2832d..0e9d0fa 100644 --- a/apps/axon_recorder/http_server.cpp +++ b/apps/axon_recorder/http_server.cpp @@ -15,6 +15,7 @@ #include #include +#include "task_config.hpp" #include "version.hpp" // Logging infrastructure @@ -545,6 +546,25 @@ HttpServer::RpcResponse HttpServer::handle_rpc_get_state(const nlohmann::json& p response.data["state"] = "unknown"; } + // Get task config if available (READY, RECORDING, PAUSED states) + if (callbacks_.get_task_config) { + const TaskConfig* task_config = callbacks_.get_task_config(); + if (task_config) { + nlohmann::json config_json; + config_json["task_id"] = task_config->task_id; + config_json["device_id"] = task_config->device_id; + config_json["data_collector_id"] = task_config->data_collector_id; + config_json["scene"] = task_config->scene; + config_json["subscene"] = task_config->subscene; + config_json["skills"] = task_config->skills; + config_json["factory"] = task_config->factory; + config_json["operator_name"] = task_config->operator_name; + config_json["topics"] = task_config->topics; + // Note: Don't include sensitive fields like callback URLs and tokens + response.data["task_config"] = config_json; + } + } + // Get stats from callback to check if running if (callbacks_.get_stats) { try { diff --git a/apps/axon_recorder/http_server.hpp b/apps/axon_recorder/http_server.hpp index 88cf048..1479687 100644 --- a/apps/axon_recorder/http_server.hpp +++ b/apps/axon_recorder/http_server.hpp @@ -20,6 +20,11 @@ namespace axon { namespace recorder { +/** + * Forward declaration + */ +struct TaskConfig; + /** * HTTP RPC Server for AxonRecorder * @@ -40,6 +45,7 @@ class HttpServer { using PauseRecordingCallback = std::function; using ResumeRecordingCallback = std::function; using ClearConfigCallback = std::function; + using GetTaskConfigCallback = std::function; using QuitCallback = std::function; /** @@ -74,6 +80,7 @@ class HttpServer { PauseRecordingCallback pause_recording; ResumeRecordingCallback resume_recording; ClearConfigCallback clear_config; + GetTaskConfigCallback get_task_config; QuitCallback quit; }; diff --git a/apps/axon_recorder/recorder.cpp b/apps/axon_recorder/recorder.cpp index 2265b5b..bc5c89a 100644 --- a/apps/axon_recorder/recorder.cpp +++ b/apps/axon_recorder/recorder.cpp @@ -440,6 +440,10 @@ bool AxonRecorder::start_http_server(const std::string& host, uint16_t port) { return this->transition_to(RecorderState::IDLE, error_msg); }; + callbacks.get_task_config = [this]() -> const TaskConfig* { + return this->get_task_config(); + }; + callbacks.quit = [this]() -> void { this->request_shutdown(); }; diff --git a/apps/axon_recorder/test/CMakeLists.txt b/apps/axon_recorder/test/CMakeLists.txt index 461125d..62bb617 100644 --- a/apps/axon_recorder/test/CMakeLists.txt +++ b/apps/axon_recorder/test/CMakeLists.txt @@ -328,39 +328,14 @@ endif() # ============================================================================= # E2E Tests # ============================================================================= -# Build mock middleware plugin for E2E testing -option(AXON_BUILD_E2E_TESTS "Build E2E test support (mock plugin)" ON) +# E2E tests now use the unified mock middleware from middlewares/mock/ +# Build with: make build-mock from project root +option(AXON_BUILD_E2E_TESTS "Build E2E test support" ON) if(AXON_BUILD_E2E_TESTS) - message(STATUS "Configuring E2E test support...") - - set(E2E_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/e2e) - - # Build mock middleware plugin - add_library(axon_mock MODULE - ${E2E_TEST_DIR}/mock_middleware.cpp - ) - - target_include_directories(axon_mock PRIVATE - ${CMAKE_SOURCE_DIR}/include - ) - - target_compile_features(axon_mock PRIVATE cxx_std_17) - - # Set output properties - set_target_properties(axon_mock PROPERTIES - PREFIX "" - OUTPUT_NAME "axon_mock" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/middlewares - ) - - # Installation - install(TARGETS axon_mock - LIBRARY DESTINATION lib/middlewares - ) - - message(STATUS "E2E test support configured successfully") - message(STATUS " Mock plugin: ${CMAKE_BINARY_DIR}/middlewares/libaxon_mock.so") + message(STATUS "E2E tests configured to use mock middleware from middlewares/mock/") + message(STATUS " Build mock plugin with: make build-mock") + message(STATUS " Mock plugin location: middlewares/mock/src/mock_plugin/build/libmock_plugin.so") endif() # ============================================================================= diff --git a/apps/axon_recorder/test/README.md b/apps/axon_recorder/test/README.md index 36c5581..53fdac6 100644 --- a/apps/axon_recorder/test/README.md +++ b/apps/axon_recorder/test/README.md @@ -24,8 +24,7 @@ test/ └── e2e/ # End-to-end tests (full workflow) ├── run_e2e_tests.sh # Main E2E test script ├── run_docker_e2e.sh # Docker-based E2E tests - ├── mock_middleware.cpp # Mock middleware plugin - ├── CMakeLists.txt # Mock plugin build config + ├── CMakeLists.txt # E2E test config └── README.md # E2E test documentation ``` diff --git a/apps/axon_recorder/test/e2e/CMakeLists.txt b/apps/axon_recorder/test/e2e/CMakeLists.txt deleted file mode 100644 index c7f6287..0000000 --- a/apps/axon_recorder/test/e2e/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.12) - -project(axon_mock CXX) - -# Build mock middleware plugin for E2E testing -# Note: No external ABI header dependency - plugin defines its own ABI structures -add_library(axon_mock MODULE - mock_middleware.cpp -) - -target_compile_features(axon_mock PRIVATE cxx_std_17) - -# Set output properties -set_target_properties(axon_mock PROPERTIES - PREFIX "" - OUTPUT_NAME "axon_mock" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} -) - -# Installation (optional) -install(TARGETS axon_mock - LIBRARY DESTINATION lib/middlewares -) - diff --git a/apps/axon_recorder/test/e2e/README.md b/apps/axon_recorder/test/e2e/README.md deleted file mode 100644 index 35612b1..0000000 --- a/apps/axon_recorder/test/e2e/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# E2E Tests for Axon Recorder - -This directory contains end-to-end tests for the axon_recorder application. - -## Overview - -The E2E tests verify the complete recording workflow from start to finish: - -1. **Process Management**: Starting and stopping the recorder -2. **HTTP API**: Testing all REST endpoints -3. **Recording Workflow**: Cache config → Start → Pause → Resume → Stop -4. **File Generation**: MCAP file and sidecar JSON verification -5. **Metadata Validation**: Checking task metadata in output files - -## Test Structure - -``` -e2e/ -├── run_e2e_tests.sh # Main E2E test script -├── run_docker_e2e.sh # Docker-based E2E test runner -├── README.md # This file -└── test_data/ # Generated during test execution - ├── recordings/ # MCAP files - ├── recorder.log # Recorder output - └── stats.json # Statistics -``` - -## Prerequisites - -### Local Testing - -```bash -# Build the project -cd /path/to/Axon -make build - -# Install dependencies (if not already installed) -sudo apt-get install curl python3 -``` - -### Docker Testing - -```bash -# Install Docker -sudo apt-get install docker.io -``` - -## Running Tests - -### Local Environment - -```bash -# Run all E2E tests -cd apps/axon_recorder/test/e2e -./run_e2e_tests.sh -``` - -### Docker Environment - -```bash -# Run in ROS2 Humble environment -cd apps/axon_recorder/test/e2e -./run_docker_e2e.sh - -# Specify ROS version -ROS_VERSION=rolling ./run_docker_e2e.sh -``` - -## Test Coverage - -### HTTP API Tests - -| Test | Endpoint | Description | -|------|----------|-------------| -| `test_health_check` | `GET /health` | Verify recorder is running | -| `test_cache_config` | `POST /api/v1/config/cache` | Cache task configuration | -| `test_start_recording` | `POST /api/v1/recording/start` | Start recording | -| `test_recording_status` | `GET /api/v1/recording/status` | Check recording state | -| `test_pause_recording` | `POST /api/v1/recording/pause` | Pause recording | -| `test_resume_recording` | `POST /api/v1/recording/resume` | Resume recording | -| `test_stop_recording` | `POST /api/v1/recording/stop` | Stop recording | - -### File Verification Tests - -| Test | Description | -|------|-------------| -| `verify_mcap_file` | Check MCAP file exists and is not empty | -| `verify_sidecar_file` | Validate sidecar JSON and required fields | - -### Sidecar Validation - -The sidecar JSON file is validated for: - -- **Valid JSON**: Parses correctly -- **Required fields**: `version`, `task_id`, `device_id`, `scene`, `recording_start_time`, `recording_end_time`, `checksum` -- **Task metadata**: Matches the cached configuration - -## Test Flow - -``` -1. Setup - ├── Create test data directory - ├── Check recorder binary exists - └── Generate test configuration - -2. Start Recorder - ├── Launch axon_recorder process - ├── Wait for startup - └── Verify process is running - -3. HTTP API Tests - ├── Health check - ├── Cache configuration - ├── Start recording - ├── Check status (RECORDING) - ├── Pause recording - ├── Check status (PAUSED) - ├── Resume recording - └── Stop recording - -4. File Verification - ├── Find MCAP file - ├── Check file size > 0 - ├── Find sidecar JSON - ├── Validate JSON syntax - └── Check required fields - -5. Cleanup - ├── Stop recorder process - └── Remove test data -``` - -## Configuration - -### Test Configuration - -Tests use the following defaults: - -| Variable | Default | Description | -|----------|---------|-------------| -| `HTTP_PORT` | 8080 | HTTP server port | -| `TEST_TASK_ID` | `e2e_test_` | Unique task ID | -| `TEST_DEVICE_ID` | `test_robot_01` | Device identifier | -| `TEST_SCENE` | `e2e_test_scene` | Scene name | - -### Custom Configuration - -You can modify test parameters by editing the configuration section in `run_e2e_tests.sh`: - -```bash -# Test configuration -HTTP_PORT=8080 -TEST_TASK_ID="e2e_test_$(date +%s)" -TEST_DEVICE_ID="test_robot_01" -TEST_SCENE="e2e_test_scene" -``` - -## Troubleshooting - -### Recorder Fails to Start - -Check the recorder log: -```bash -cat test_data/recorder.log -``` - -### HTTP Requests Fail - -Verify the recorder is running: -```bash -curl http://localhost:8080/health -``` - -### MCAP File Not Created - -1. Check the dataset path in test config -2. Verify disk space is available -3. Review recorder logs for errors - -### Sidecar File Missing - -1. Verify task config was cached -2. Check that recording was stopped properly -3. Look for errors in metadata injection - -## Adding New Tests - -To add a new E2E test: - -1. Create a test function in `run_e2e_tests.sh`: - ```bash - test_my_new_feature() { - log_info "Testing my new feature..." - # Your test code here - if [[ condition ]]; then - log_info "Test passed" - return 0 - else - log_error "Test failed" - return 1 - fi - } - ``` - -2. Add the test to the `tests` array: - ```bash - local tests=( - "test_health_check" - "test_my_new_feature" # Add here - # ... other tests - ) - ``` - -3. Run the tests and verify: - ```bash - ./run_e2e_tests.sh - ``` - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: E2E Tests - -on: [push, pull_request] - -jobs: - e2e: - runs-on: ubuntu-latest - strategy: - matrix: - ros_version: [humble, jazzy, rolling] - steps: - - uses: actions/checkout@v3 - - name: Run E2E tests - run: | - cd apps/axon_recorder/test/e2e - ROS_VERSION=${{ matrix.ros_version }} ./run_docker_e2e.sh -``` - -## Cleanup - -To manually clean up test artifacts: - -```bash -# Stop any running recorder -pkill -f axon_recorder - -# Remove test data -rm -rf apps/axon_recorder/test/e2e/test_data - -# Remove Docker artifacts -docker system prune -f -``` diff --git a/apps/axon_recorder/test/e2e/run_e2e_tests.sh b/apps/axon_recorder/test/e2e/run_e2e_tests.sh index 271ef8d..167f70f 100755 --- a/apps/axon_recorder/test/e2e/run_e2e_tests.sh +++ b/apps/axon_recorder/test/e2e/run_e2e_tests.sh @@ -32,11 +32,8 @@ else RECORDER_BIN="${BUILD_DIR}/axon_recorder/axon_recorder" fi -# Mock plugin path -MOCK_PLUGIN="${BUILD_DIR}/middlewares/axon_mock.so" -if [[ ! -f "${MOCK_PLUGIN}" ]]; then - MOCK_PLUGIN="${PROJECT_ROOT}/apps/axon_recorder/build/axon_mock.so" -fi +# Mock plugin path - built in-place by mock middleware's own build system +MOCK_PLUGIN="${PROJECT_ROOT}/middlewares/mock/src/mock_plugin/build/libmock_plugin.so" # Colors for output RED='\033[0;31m' @@ -113,7 +110,8 @@ setup() { if [[ ! -f "${MOCK_PLUGIN}" ]]; then log_warn "Mock plugin not found at ${MOCK_PLUGIN}" log_info "E2E tests will attempt to run without mock middleware" - log_info "Build with: cd apps/axon_recorder/test/e2e && cmake . && make" + log_info "Build mock middleware with:" + log_info " cd middlewares/mock/src/mock_plugin/build && cmake .. && make" MOCK_PLUGIN="" fi diff --git a/apps/axon_transfer/CMakeLists.txt b/apps/axon_transfer/CMakeLists.txt new file mode 100644 index 0000000..f9b4d63 --- /dev/null +++ b/apps/axon_transfer/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(axon_transfer LANGUAGES CXX) + +# C++17 standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# TODO: Add executable, dependencies, and installation rules diff --git a/apps/axon_transfer/README.md b/apps/axon_transfer/README.md new file mode 100644 index 0000000..ff95499 --- /dev/null +++ b/apps/axon_transfer/README.md @@ -0,0 +1,16 @@ +# Axon Transfer Application + +**Status:** Placeholder - Design pending + +## Purpose + +Standalone S3 transfer daemon application for uploading MCAP files to cloud storage. + +## Design Notes + +TODO: Define application architecture, CLI interface, and configuration format. + +## Related Components + +- Core library: [core/axon_uploader/](../../core/axon_uploader/) +- Design doc: [docs/designs/edge-uploader-design.md](../../docs/designs/edge-uploader-design.md) diff --git a/apps/axon_transfer/src/main.cpp b/apps/axon_transfer/src/main.cpp new file mode 100644 index 0000000..da4cca4 --- /dev/null +++ b/apps/axon_transfer/src/main.cpp @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + +// Axon Transfer - Main Entry Point +// Placeholder implementation - Design pending + +#include + +int main(int argc, char** argv) { + std::cout << "Axon Transfer - S3 transfer daemon - Design pending\n"; + return 0; +} diff --git a/apps/plugin_example/CMakeLists.txt b/apps/plugin_example/CMakeLists.txt deleted file mode 100644 index a74c601..0000000 --- a/apps/plugin_example/CMakeLists.txt +++ /dev/null @@ -1,54 +0,0 @@ -cmake_minimum_required(VERSION 3.12) -project(axon_plugin_example) - -# Default to C++17 -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -if(NOT CMAKE_CXX_STANDARD_REQUIRED) - set(CMAKE_CXX_STANDARD_REQUIRED ON) -endif() - -# Compiler warnings -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# Build the simple load test executable -add_executable(simple_load_test - simple_load_test.cpp - ../axon_recorder/plugin_loader.cpp -) - -target_include_directories(simple_load_test PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../axon_recorder -) - -target_link_libraries(simple_load_test - ${CMAKE_DL_LIBS} -) - -# Build the full plugin loader test executable -add_executable(plugin_loader_test - plugin_loader_test.cpp - ../axon_recorder/plugin_loader.cpp -) - -# Include directories -target_include_directories(plugin_loader_test PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../axon_recorder -) - -# Link libraries -target_link_libraries(plugin_loader_test - ${CMAKE_DL_LIBS} - pthread -) - -# Install -install(TARGETS simple_load_test plugin_loader_test - RUNTIME DESTINATION bin -) diff --git a/apps/plugin_example/plugin_loader_test.cpp b/apps/plugin_example/plugin_loader_test.cpp deleted file mode 100644 index 8430be5..0000000 --- a/apps/plugin_example/plugin_loader_test.cpp +++ /dev/null @@ -1,305 +0,0 @@ -// SPDX-FileCopyrightText: 2026 ArcheBase -// -// SPDX-License-Identifier: MulanPSL-2.0 - -/** - * @file plugin_loader_test.cpp - * @brief Plugin loader test example for Axon middleware plugins - * - * This example demonstrates how to: - * 1. Load a ROS2 plugin dynamically - * 2. Initialize the plugin with configuration - * 3. Set message callback - * 4. Subscribe to topics - * 5. Spin the executor - * 6. Cleanup and unload - */ - -#include "../axon_recorder/plugin_loader.hpp" - -#include -#include -#include -#include - -using namespace axon; - -// Global flag for graceful shutdown -static std::sig_atomic_t g_running = 1; - -// Signal handler for Ctrl+C -void signal_handler(int signal) { - (void)signal; - std::cout << "\n[INFO] Shutdown signal received..." << std::endl; - g_running = 0; -} - -// ============================================================================= -// Message callback implementation -// ============================================================================= -void message_callback( - const char* topic_name, const uint8_t* message_data, size_t message_size, - const char* message_type, uint64_t timestamp, void* user_data -) { - (void)user_data; - - // Print message received info - std::cout << "[MSG] Topic: " << topic_name << " | Type: " << message_type - << " | Size: " << message_size << " bytes" - << " | Timestamp: " << timestamp << std::endl; - - // Print first 32 bytes of message data (hexdump style) - constexpr size_t MAX_DUMP = 32; - size_t dump_size = std::min(message_size, MAX_DUMP); - - std::cout << " Data: "; - for (size_t i = 0; i < dump_size; ++i) { - printf("%02x ", message_data[i]); - } - if (message_size > MAX_DUMP) { - std::cout << "..."; - } - std::cout << std::endl; -} - -// ============================================================================= -// Test scenarios -// ============================================================================= - -/** - * Test 1: Basic plugin loading and initialization - */ -bool test_load_and_init(PluginLoader& loader, const std::string& plugin_path) { - std::cout << "\n=== Test 1: Load and Initialize Plugin ===" << std::endl; - - // Load the plugin - auto plugin_name_opt = loader.load(plugin_path); - if (!plugin_name_opt) { - std::cerr << "[ERROR] Failed to load plugin: " << loader.get_last_error() << std::endl; - return false; - } - - std::string plugin_name = *plugin_name_opt; - std::cout << "[OK] Loaded plugin: " << plugin_name << std::endl; - - // Get descriptor - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor) { - std::cerr << "[ERROR] Failed to get plugin descriptor" << std::endl; - return false; - } - - // Print plugin info - std::cout << "[INFO] Plugin Details:" << std::endl; - std::cout << " - ABI Version: " << descriptor->abi_version_major << "." - << descriptor->abi_version_minor << std::endl; - std::cout << " - Middleware: " << descriptor->middleware_name << std::endl; - std::cout << " - Version: " << descriptor->middleware_version << std::endl; - std::cout << " - Plugin: " << descriptor->plugin_version << std::endl; - - // Initialize plugin with JSON config - const char* config_json = R"({ - "node_name": "axon_plugin_test", - "use_sim_time": false - })"; - - auto* plugin = loader.get_plugin(plugin_name); - AxonStatus status = descriptor->vtable->init(config_json); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to initialize plugin, status: " << status << std::endl; - return false; - } - - plugin->initialized = true; - std::cout << "[OK] Plugin initialized" << std::endl; - - return true; -} - -/** - * Test 2: Subscribe to topics - */ -bool test_subscribe(PluginLoader& loader, const std::string& plugin_name) { - std::cout << "\n=== Test 2: Subscribe to Topics ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor || !descriptor->vtable->subscribe) { - std::cerr << "[ERROR] Plugin does not support subscribe" << std::endl; - return false; - } - - // List of topics to subscribe - std::vector> topics = { - {"/imu/data", "sensor_msgs/msg/Imu"}, - {"/camera0/rgb", "sensor_msgs/msg/Image"}, - }; - - for (const auto& [topic, type] : topics) { - std::cout << "[INFO] Subscribing to: " << topic << " (" << type << ")" << std::endl; - - AxonStatus status = descriptor->vtable->subscribe( - topic.c_str(), - type.c_str(), - nullptr, // options_json (none for this test) - message_callback, - nullptr // user_data - ); - - if (status != AXON_SUCCESS) { - std::cout << "[WARN] Failed to subscribe to " << topic << ", status: " << status << std::endl; - // Continue with other topics - } else { - std::cout << "[OK] Subscribed to: " << topic << std::endl; - } - } - - return true; -} - -/** - * Test 3: Spin the plugin - */ -bool test_spin(PluginLoader& loader, const std::string& plugin_name, int seconds) { - std::cout << "\n=== Test 3: Spin Plugin (" << seconds << " seconds) ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor || !descriptor->vtable->start) { - std::cerr << "[ERROR] Plugin does not support start" << std::endl; - return false; - } - - auto* plugin = loader.get_plugin(plugin_name); - - // Start spinning (this creates the executor thread) - std::cout << "[INFO] Starting plugin executor... (Press Ctrl+C to stop early)" << std::endl; - AxonStatus status = descriptor->vtable->start(); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to start spinning, status: " << status << std::endl; - return false; - } - - plugin->running = true; - - // Wait for the specified duration while the executor runs in background - auto start_time = std::chrono::steady_clock::now(); - - while (g_running) { - // Check timeout - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ) - .count(); - - if (elapsed >= seconds) { - break; - } - - // Sleep for a bit - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - std::cout << "[INFO] Spin test completed" << std::endl; - - return true; -} - -/** - * Test 4: Cleanup and unload - */ -bool test_cleanup(PluginLoader& loader, const std::string& plugin_name) { - std::cout << "\n=== Test 4: Cleanup and Unload ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - auto* plugin = loader.get_plugin(plugin_name); - - // Shutdown plugin - if (plugin && plugin->running && descriptor->vtable->stop) { - std::cout << "[INFO] Shutting down plugin..." << std::endl; - AxonStatus status = descriptor->vtable->stop(); - - if (status != AXON_SUCCESS) { - std::cerr << "[WARN] Shutdown returned error: " << status << std::endl; - } - plugin->running = false; - } - - // Unload plugin - if (loader.unload(plugin_name)) { - std::cout << "[OK] Plugin unloaded" << std::endl; - return true; - } else { - std::cerr << "[ERROR] Failed to unload: " << loader.get_last_error() << std::endl; - return false; - } -} - -// ============================================================================= -// Main entry point -// ============================================================================= -int main(int argc, char* argv[]) { - std::cout << "========================================" << std::endl; - std::cout << " Axon Plugin Loader Test" << std::endl; - std::cout << "========================================" << std::endl; - - // Setup signal handler - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Parse command line arguments - std::string plugin_path = - "/home/xlw/src/Axon/middlewares/ros2/install/axon_ros2_plugin/lib/axon/plugins/" - "libaxon_ros2_plugin.so"; - int test_duration_seconds = 10; - - if (argc > 1) { - plugin_path = argv[1]; - } - if (argc > 2) { - test_duration_seconds = std::stoi(argv[2]); - } - - std::cout << "[INFO] Plugin path: " << plugin_path << std::endl; - std::cout << "[INFO] Test duration: " << test_duration_seconds << " seconds" << std::endl; - - // Create plugin loader - PluginLoader loader; - - // Run tests - stop on first failure - // Test 1: Load and initialize (critical - must pass to continue) - if (!test_load_and_init(loader, plugin_path)) { - std::cerr << "\n[FAIL] Test 1 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 2: Subscribe - if (!test_subscribe(loader, "ROS2")) { - std::cerr << "\n[FAIL] Test 2 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 3: Spin - if (!test_spin(loader, "ROS2", test_duration_seconds)) { - std::cerr << "\n[FAIL] Test 3 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 4: Cleanup - if (!test_cleanup(loader, "ROS2")) { - std::cerr << "\n[FAIL] Test 4 failed" << std::endl; - loader.unload_all(); - return 1; - } - // Final cleanup - loader.unload_all(); - - std::cout << "\n========================================" << std::endl; - std::cout << " All tests PASSED" << std::endl; - std::cout << "========================================" << std::endl; - - return 0; -} diff --git a/apps/plugin_example/simple_load_test.cpp b/apps/plugin_example/simple_load_test.cpp deleted file mode 100644 index 1483c57..0000000 --- a/apps/plugin_example/simple_load_test.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2026 ArcheBase -// -// SPDX-License-Identifier: MulanPSL-2.0 - -/** - * @file simple_load_test.cpp - * @brief Minimal test to verify plugin can be loaded and descriptor accessed - * - * This is a simplified test that only checks: - * 1. The plugin library can be opened with dlopen - * 2. The axon_get_plugin_descriptor symbol exists - * 3. The descriptor is valid - * - * This does NOT require ROS2 to be running. - */ - -#include - -#include "../axon_recorder/plugin_loader.hpp" - -using namespace axon; - -int main(int argc, char* argv[]) { - std::cout << "=== Simple Plugin Load Test ===" << std::endl; - - // Parse arguments - if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - std::cerr << "Example: " << argv[0] << " /path/to/libaxon_ros2_plugin.so" << std::endl; - return 1; - } - - std::string plugin_path = argv[1]; - std::cout << "Plugin path: " << plugin_path << std::endl << std::endl; - - // Create loader - PluginLoader loader; - - // Test 1: Load plugin - std::cout << "[TEST 1] Loading plugin..." << std::endl; - auto plugin_name_opt = loader.load(plugin_path); - if (!plugin_name_opt) { - std::cerr << "[FAIL] Could not load plugin" << std::endl; - std::cerr << "Error: " << loader.get_last_error() << std::endl; - return 1; - } - std::cout << "[PASS] Plugin loaded: " << *plugin_name_opt << std::endl << std::endl; - - // Test 2: Get descriptor - std::cout << "[TEST 2] Getting descriptor..." << std::endl; - const auto* descriptor = loader.get_descriptor(*plugin_name_opt); - if (!descriptor) { - std::cerr << "[FAIL] Could not get descriptor" << std::endl; - return 1; - } - std::cout << "[PASS] Descriptor found" << std::endl; - - // Print plugin info - std::cout << "\nPlugin Information:" << std::endl; - std::cout << " ABI Version: " << descriptor->abi_version_major << "." - << descriptor->abi_version_minor << std::endl; - std::cout << " Middleware: " - << (descriptor->middleware_name ? descriptor->middleware_name : "N/A") << std::endl; - std::cout << " Middleware Version: " - << (descriptor->middleware_version ? descriptor->middleware_version : "N/A") - << std::endl; - std::cout << " Plugin Version: " - << (descriptor->plugin_version ? descriptor->plugin_version : "N/A") << std::endl; - - // Test 3: Check vtable - std::cout << "\n[TEST 3] Checking vtable..." << std::endl; - if (!descriptor->vtable) { - std::cerr << "[FAIL] Vtable is null" << std::endl; - return 1; - } - std::cout << "[PASS] Vtable exists" << std::endl; - - std::cout << "\nVtable Functions:" << std::endl; - std::cout << " init: " << (descriptor->vtable->init ? "✓" : "✗") << std::endl; - std::cout << " start: " << (descriptor->vtable->start ? "✓" : "✗") << std::endl; - std::cout << " stop: " << (descriptor->vtable->stop ? "✓" : "✗") << std::endl; - std::cout << " subscribe: " << (descriptor->vtable->subscribe ? "✓" : "✗") << std::endl; - std::cout << " publish: " << (descriptor->vtable->publish ? "✓" : "✗") << std::endl; - - // Test 4: Unload - std::cout << "\n[TEST 4] Unloading plugin..." << std::endl; - if (!loader.unload(*plugin_name_opt)) { - std::cerr << "[FAIL] Could not unload plugin" << std::endl; - return 1; - } - std::cout << "[PASS] Plugin unloaded" << std::endl; - - std::cout << "\n=== All Tests PASSED ===" << std::endl; - return 0; -} diff --git a/docker/scripts/run_e2e_tests.sh b/docker/scripts/run_e2e_tests.sh index e04122e..f552b8b 100755 --- a/docker/scripts/run_e2e_tests.sh +++ b/docker/scripts/run_e2e_tests.sh @@ -201,6 +201,23 @@ else } fi +# Build the mock plugin separately (it has its own CMakeLists.txt) +echo "" +echo "Building mock plugin..." +MOCK_PLUGIN_BUILD_DIR="${WORKSPACE_ROOT}/middlewares/mock/src/mock_plugin/build" +mkdir -p "${MOCK_PLUGIN_BUILD_DIR}" +cd "${MOCK_PLUGIN_BUILD_DIR}" + +cmake -DCMAKE_BUILD_TYPE=Release .. || { + echo "ERROR: Failed to configure mock plugin" + exit 1 +} + +cmake --build . -j$(nproc) || { + echo "ERROR: Failed to build mock plugin" + exit 1 +} + # Re-source workspace after build ros_workspace_source_workspace "${WORKSPACE_ROOT}" || { ros_workspace_error "Failed to source workspace after build" @@ -218,11 +235,10 @@ if [ ! -f "$RECORDER_BIN" ]; then fi echo "✓ axon_recorder binary found at ${RECORDER_BIN}" -# Check mock plugin -MOCK_PLUGIN="${WORKSPACE_ROOT}/build/middlewares/axon_mock.so" +# Check mock plugin - it's built in its own build directory +MOCK_PLUGIN="${MOCK_PLUGIN_BUILD_DIR}/libmock_plugin.so" if [ ! -f "$MOCK_PLUGIN" ]; then echo "ERROR: Mock plugin not found at ${MOCK_PLUGIN}" - echo "This should have been built with AXON_BUILD_MOCK_PLUGIN=ON" exit 1 fi echo "✓ Mock plugin found at ${MOCK_PLUGIN}" diff --git a/docs/designs/frontend-design.md b/docs/designs/frontend-design.md new file mode 100644 index 0000000..44760e5 --- /dev/null +++ b/docs/designs/frontend-design.md @@ -0,0 +1,574 @@ +# AxonPanel - Frontend Design Document + +**Date:** 2025-01-21 +**Status:** Implemented + +## Overview + +AxonPanel is a modern Vue 3-based web interface for debugging and controlling the Axon Recorder HTTP RPC API. It provides real-time monitoring, recording control, and visual state machine representation. + + +## Features + +- **Real-time State Monitoring**: View current recorder state and task configuration +- **Recording Statistics**: Monitor messages received, written, dropped, and file size +- **Control Panel**: Execute RPC commands (config, begin, pause, resume, finish, cancel) +- **Activity Log**: Track all RPC interactions with timestamps and color-coded messages +- **Visual State Machine**: Interactive diagram showing state transitions +- **Responsive Design**: Mobile-friendly interface with touch-optimized controls +- **Auto-refresh**: Polls state and statistics every second + +## Architecture + +### Component Structure + +``` +App.vue (Root Component) +├── ConnectionStatus.vue - Connection status and health check +├── StatePanel.vue - Statistics and task configuration display +├── ControlPanel.vue - Command buttons and state machine diagram +│ └── StateMachineDiagram.vue - Visual state transition diagram +├── ConfigPanel.vue - Modal form for task configuration +└── LogPanel.vue - Activity log display +``` + +### Data Flow + +``` +User Action → ControlPanel + ↓ +App.vue (Command Handler) + ↓ +RPC API Client (api/rpc.js) + ↓ +Axon Recorder (HTTP Server) + ↓ +Response → App.vue (State Update) + ↓ +Component Re-render (Vue Reactivity) +``` + +### Directory Structure + +``` +tools/axon_panel/ +├── src/ +│ ├── api/ +│ │ └── rpc.js # Centralized API client +│ ├── components/ +│ │ ├── ConnectionStatus.vue +│ │ ├── StatePanel.vue +│ │ ├── ControlPanel.vue +│ │ ├── StateMachineDiagram.vue +│ │ ├── ConfigPanel.vue +│ │ └── LogPanel.vue +│ ├── App.vue # Root component +│ ├── main.js # Entry point +│ └── edge-label-fix.css # Vue Flow edge label fix +├── index.html # HTML template +├── package.json # Dependencies +├── vite.config.js # Vite configuration +└── README.md # User documentation +``` + +## Key Components + +### 1. App.vue - Main Application Container + +**File:** [tools/axon_panel/src/App.vue](../../tools/axon_panel/src/App.vue) + +**Responsibilities:** +- Manages global state (current state, task config, statistics, logs) +- Handles RPC command execution and error handling +- Polls server for state updates (1-second interval) +- Manages activity log with FIFO (100 entries max) + +**State Management:** +```javascript +const connected = ref(false) // Server connection status +const health = ref(null) // Health check data +const currentState = ref('idle') // Current recorder state +const taskConfig = ref(null) // Cached task configuration +const currentTaskId = ref('') // Active task ID +const stats = ref(null) // Recording statistics +const showConfigPanel = ref(false) // Config modal visibility +const showLogs = ref(true) // Log panel visibility +const logs = ref([]) // Activity log entries +``` + +**Polling Strategy:** +- Automatic state refresh every 1000ms +- Starts on component mount +- Stops on component unmount +- Fetches both state and stats + +**Command Flow:** +1. User clicks button → Emit command event +2. `handleCommand()` executes appropriate RPC call +3. Success → Log message + refresh state +4. Error → Log error message + +### 2. ConnectionStatus.vue - Connection Indicator + +**File:** [tools/axon_panel/src/components/ConnectionStatus.vue](../../tools/axon_panel/src/components/ConnectionStatus.vue) + +**Features:** +- Visual connection status badge (Connected/Disconnected) +- Display recorder version +- Manual refresh button + +**Health Check:** +- Endpoint: `GET /health` +- Updates: On mount + manual refresh +- Error handling: Graceful degradation + +**Props:** +```javascript +{ + connected: Boolean, // Connection status + health: Object // Health data { version, running, state } +} +``` + +**Emits:** +- `refresh` - Manual health check request + +### 3. StatePanel.vue - Statistics Display + +**File:** [tools/axon_panel/src/components/StatePanel.vue](../../tools/axon_panel/src/components/StatePanel.vue) + +**Displays:** +- Task configuration: + - Task ID, Device ID, Scene, Factory + - Topics (as tags) +- Recording statistics: + - Messages received (formatted number) + - Messages written (formatted number) + - Messages dropped (formatted number) + - File size (formatted bytes) + +**Formatting Functions:** +```javascript +formatNumber(1523456) // "1,523,456" +formatBytes(2147483648) // "2.00 GB" +``` + +**Update Frequency:** +- Syncs with parent's 1-second polling +- Manual refresh button + +### 4. ControlPanel.vue - Command Center + +**File:** [tools/axon_panel/src/components/ControlPanel.vue](../../tools/axon_panel/src/components/ControlPanel.vue) + +**Features:** +- Command buttons with state-based enable/disable +- Current task ID display +- Embedded state machine diagram + +**Button Logic:** +```javascript +config: enabled when state !== 'recording' && state !== 'paused' +begin: enabled when state === 'ready' +pause: enabled when state === 'recording' +resume: enabled when state === 'paused' +finish: enabled when state === 'recording' || state === 'paused' +cancel: enabled when state === 'recording' || state === 'paused' +clear: enabled when state === 'ready' +quit: always enabled +``` + +**Emits:** +- `command` - With command name as payload + +### 5. StateMachineDiagram.vue - Visual State Machine + +**File:** [tools/axon_panel/src/components/StateMachineDiagram.vue](../../tools/axon_panel/src/components/StateMachineDiagram.vue) + +**Technology:** Vue Flow (flow chart library) + +**Features:** +- Four-state visualization (IDLE, READY, RECORDING, PAUSED) +- Dynamic edge visibility based on current state +- Active state highlighting with color coding +- Smooth transitions between states + +**State Colors:** +``` +IDLE: Gray (#9ca3af) +READY: Green (#10b981) +RECORDING: Red (#ef4444) +PAUSED: Yellow (#f59e0b) +``` + +**Edge Visibility Logic:** +```javascript +idle: [idle→ready] +ready: [ready→recording, ready→idle] +recording: [recording→paused, recording→idle] +paused: [paused→recording, paused→idle] +``` + +**Special Implementation:** +- Forces edge labels to black (CSS workaround for Vue Flow) +- Disables panning, zooming, and node dragging +- Responsive sizing (350px height) + +### 6. ConfigPanel.vue - Task Configuration Modal + +**File:** [tools/axon_panel/src/components/ConfigPanel.vue](../../tools/axon_panel/src/components/ConfigPanel.vue) + +**Features:** +- Modal dialog with Teleport to body +- Form validation (HTML5 required attributes) +- Pre-filled default values for quick testing +- Comma-separated array inputs (skills, topics) +- Only shown when state !== 'recording' && state !== 'paused' + +**Form Fields:** +```javascript +{ + task_id: string, // Required + device_id: string, // Required + data_collector_id: string, // Optional + scene: string, // Optional + subscene: string, // Optional + factory: string, // Optional + operator_name: string, // Optional + skills: string[], // Comma-separated input + topics: string[], // Comma-separated input + start_callback_url: string, // Optional (URL validation) + finish_callback_url: string, // Optional (URL validation) + user_token: string // Optional (textarea) +} +``` + +**Default Values:** +```javascript +task_id: `task_${Date.now()}` +device_id: 'robot_01' +data_collector_id: 'collector_01' +scene: 'warehouse_navigation' +subscene: 'aisle_traversal' +factory: 'factory_shenzhen' +operator_name: 'operator_001' +skills: ['navigation', 'obstacle_avoidance'] +topics: ['/camera/image', '/lidar/scan', '/odom'] +``` + +**Emits:** +- `submit` - With form data object +- `cancel` - Modal closed without submission + +### 7. LogPanel.vue - Activity Logger + +**File:** [tools/axon_panel/src/components/LogPanel.vue](../../tools/axon_panel/src/components/LogPanel.vue) + +**Features:** +- Chronological log with timestamps +- Color-coded message types +- Terminal-like appearance (dark background) +- Empty state message + +**Message Types:** +```javascript +{ + timestamp: string, // HH:MM:SS format + message: string, // Log message + type: string // 'info' | 'success' | 'warning' | 'error' +} +``` + +**Color Scheme:** +```javascript +info: Blue (#60a5fa) +success: Green (#34d399) +warning: Yellow (#fbbf24) +error: Red (#f87171) +``` + +**Props:** +```javascript +{ + logs: Array // Array of log entry objects +} +``` + +## RPC API Integration + +**File:** [tools/axon_panel/src/api/rpc.js](../../tools/axon_panel/src/api/rpc.js) + +The centralized API client provides methods for all RPC endpoints: + +```javascript +// Query endpoints +rpcApi.health() // GET /health +rpcApi.getState() // GET /rpc/state +rpcApi.getStats() // GET /rpc/stats + +// Command endpoints +rpcApi.setConfig(config) // POST /rpc/config +rpcApi.begin(taskId) // POST /rpc/begin +rpcApi.finish(taskId) // POST /rpc/finish +rpcApi.pause() // POST /rpc/pause +rpcApi.resume() // POST /rpc/resume +rpcApi.cancel(taskId) // POST /rpc/cancel +rpcApi.clear() // POST /rpc/clear +rpcApi.quit() // POST /rpc/quit +``` + +**Base URL Configuration:** +```javascript +const API_BASE_URL = 'http://localhost:8080' +``` + +**Response Format:** +All methods return promises that resolve to the response JSON: +```javascript +{ + success: boolean, + message: string, + data: { + state: string, + task_id: string, + // ... other fields + } +} +``` + +See [RPC API Design](rpc-api-design.md) for detailed API documentation. + +## State Machine + +The application follows the Axon Recorder state machine: + +``` +IDLE ──(POST /rpc/config)──► READY ──(POST /rpc/begin)──► RECORDING ↔ PAUSED + ▲ │ │ + │ │ │ + └────────(POST /rpc/finish)───┴──────────────(POST /rpc/finish)──────┘ + │ + ▼ + (POST /rpc/quit) + │ + ▼ + Program Exit +``` + +### State Transitions + +| Current State | Valid Commands | Result State | +|---------------|----------------|--------------| +| `IDLE` | config | `READY` | +| `READY` | begin | `RECORDING` | +| `READY` | clear | `IDLE` | +| `RECORDING` | pause | `PAUSED` | +| `RECORDING` | finish | `IDLE` | +| `RECORDING` | cancel | `IDLE` | +| `PAUSED` | resume | `RECORDING` | +| `PAUSED` | finish | `IDLE` | +| `PAUSED` | cancel | `IDLE` | +| Any State | quit | Program Exit | + +## UI/UX Design + +### Design Principles + +1. **Mobile-First**: Responsive layout optimized for tablets and phones +2. **Touch-Friendly**: Minimum 44px tap targets for buttons +3. **Visual Feedback**: Color-coded states and animated transitions +4. **Real-Time Updates**: Auto-refreshing data without manual reload +5. **Error Handling**: User-friendly error messages in activity log + +### Color Scheme + +``` +Primary: Indigo (#4f46e5) - Header, active elements +Success: Green (#10b981) - Ready state, success messages +Danger: Red (#ef4444) - Recording state, errors, cancel action +Warning: Yellow (#f59e0b) - Paused state, warnings +Neutral: Gray (#6b7280) - Idle state, labels +``` + +### Responsive Breakpoints + +**Desktop (> 768px):** +- Multi-column grid layout (auto-fit, minmax(350px, 1fr)) +- Floating log panel (600px × 500px, fixed position) +- Full-size buttons and text +- Hover states for buttons + +**Mobile (≤ 768px):** +- Single-column layout +- Full-width log panel (60vh height, fixed to bottom) +- Touch-optimized buttons (min 44px × 44px) +- Prevented pull-to-refresh: `overscroll-behavior-y: contain` +- Prevented zoom on input focus: `font-size: 16px` + +### Animations + +**Modal Transitions:** +```css +.modal-enter-active, .modal-leave-active { + transition: all 0.3s ease; +} +.modal-enter-from, .modal-leave-to { + opacity: 0; +} +.modal-enter-from .modal-content, .modal-leave-to .modal-content { + transform: scale(0.95); +} +``` + +**Slide Transitions (Log Panel):** +```css +.slide-enter-active, .slide-leave-active { + transition: all 0.3s ease; +} +.slide-enter-from, .slide-leave-to { + opacity: 0; + transform: translateY(20px) scale(0.95); +} +``` + +## Technology Stack + +- **Vue 3** (v3.4+): Progressive JavaScript framework with Composition API +- **Vite** (v5.0+): Build tool and dev server with HMR +- **Vue Flow** (v1.48+): Interactive flow chart library for state machine visualization +- **Axios** (v1.6+): HTTP client for API requests + +### Dependencies + +```json +{ + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } +} +``` + +## Development + +### File Naming Convention +- Components: PascalCase (e.g., `ConnectionStatus.vue`) +- Utilities: camelCase (e.g., `rpc.js`) + +### Code Style +- **Vue 3 Composition API**: Uses `