From 32694c6737dc738cdbc65b1fd8e2de78099bb12b Mon Sep 17 00:00:00 2001 From: SuhaanCoding Date: Fri, 23 Jan 2026 19:24:25 -0800 Subject: [PATCH 1/2] added datatypes --- auv_cpp/src/core/CMakeLists.txt | 9 ++ auv_cpp/src/core/types.hpp | 186 ++++++++++++++++++++++++++++++ auv_cpp/tests/core/CMakeLists.txt | 15 +++ auv_cpp/tests/core/test_types.cpp | 132 +++++++++++++++++++++ 4 files changed, 342 insertions(+) create mode 100644 auv_cpp/src/core/CMakeLists.txt create mode 100644 auv_cpp/src/core/types.hpp create mode 100644 auv_cpp/tests/core/CMakeLists.txt create mode 100644 auv_cpp/tests/core/test_types.cpp diff --git a/auv_cpp/src/core/CMakeLists.txt b/auv_cpp/src/core/CMakeLists.txt new file mode 100644 index 0000000..6435294 --- /dev/null +++ b/auv_cpp/src/core/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(auv_core INTERFACE) + +target_include_directories(auv_core INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +target_link_libraries(auv_core INTERFACE + Eigen3::Eigen +) diff --git a/auv_cpp/src/core/types.hpp b/auv_cpp/src/core/types.hpp new file mode 100644 index 0000000..2b97986 --- /dev/null +++ b/auv_cpp/src/core/types.hpp @@ -0,0 +1,186 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +struct State; +struct SerialState; + +/// Log message sources +enum class LogSource { + MAIN, // Main system loop + CTRL, // Control system + NAV, // Navigation + WSKT, // WebSocket handler + LCAL, // Localization + PRCP // Perception +}; + +/// Log message destinations +enum class LogDest { + BASE, // Send to base station + LOG // Local logging only +}; + +/// 3D vector (NED frame) +using Vec3 = std::array; + +/// 3x3 matrix +using Mat3x3 = std::array, 3>; + +/** + * @brief AUV state in NED (North-East-Down) frame + * + * Units: meters, m/s, degrees, rad/s + */ +struct State { + Vec3 position = {0.0, 0.0, 0.0}; ///< [N, E, D] meters + Vec3 velocity = {0.0, 0.0, 0.0}; ///< [N, E, D] m/s + Vec3 attitude = {0.0, 0.0, 0.0}; ///< [Roll, Pitch, Yaw] degrees + Vec3 angular_velocity = {0.0, 0.0, 0.0}; ///< [P, Q, R] rad/s + + State() = default; + State(const Vec3& pos, const Vec3& vel, const Vec3& att, const Vec3& ang_vel) + : position(pos), velocity(vel), attitude(att), angular_velocity(ang_vel) {} +}; + +using SerialState = State; + +/// State with body-frame forces and torques +struct ExpandedState : State { + Vec3 local_force = {0.0, 0.0, 0.0}; ///< [X, Y, Z] Newtons + Vec3 local_torque = {0.0, 0.0, 0.0}; ///< [X, Y, Z] N·m + + ExpandedState() = default; + ExpandedState(const State& base, const Vec3& force, const Vec3& torque) + : State(base), local_force(force), local_torque(torque) {} +}; + +/// State with mass properties for simulation +struct InitialState : ExpandedState { + double mass = 0.0; + Mat3x3 inertia = {{{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}}; + + InitialState() = default; +}; + +/** + * @brief Motor speed commands for 4 thrusters + * + * All values in range [-1.0, 1.0] + */ +struct MotorSpeeds { + double forward = 0.0; + double turn = 0.0; + double front = 0.0; + double back = 0.0; + + MotorSpeeds() = default; + + MotorSpeeds(double fwd, double trn, double fnt, double bck) + : forward(fwd), turn(trn), front(fnt), back(bck) { + validate(); + } + + void validate() const { + for (double speed : {forward, turn, front, back}) { + if (speed < -1.0 || speed > 1.0) { + throw std::invalid_argument("speeds must be from -1.0 to 1.0"); + } + } + } + + /// Create with values clamped to [-1.0, 1.0] + static MotorSpeeds clamped(double fwd, double trn, double fnt, double bck) { + auto clamp = [](double v) { return std::max(-1.0, std::min(1.0, v)); }; + MotorSpeeds result; + result.forward = clamp(fwd); + result.turn = clamp(trn); + result.front = clamp(fnt); + result.back = clamp(bck); + return result; + } +}; + +using LogContent = std::variant; + +/// Log message for system communication +struct Log { + LogSource source; + std::string type; + LogContent content; + LogDest dest = LogDest::LOG; + + Log(LogSource src, std::string msg_type, LogContent msg_content, + LogDest destination = LogDest::LOG) + : source(src) + , type(std::move(msg_type)) + , content(std::move(msg_content)) + , dest(destination) {} +}; + +using Callback = std::function; + +/// Scheduled callback that executes after a duration +struct Promise { + std::string name; + double duration; + Callback callback; + std::chrono::steady_clock::time_point init_time; + + Promise(std::string promise_name, double duration_seconds, Callback cb) + : name(std::move(promise_name)) + , duration(duration_seconds) + , callback(std::move(cb)) + , init_time(std::chrono::steady_clock::now()) {} + + bool is_ready() const { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration(now - init_time).count(); + return elapsed >= duration; + } +}; + +/// Command dispatch entry +struct Dispatch { + std::string log; + Callback func; + + Dispatch(std::string log_msg, Callback handler) + : log(std::move(log_msg)), func(std::move(handler)) {} +}; + +/// Command from base station +struct Cmd { + std::string command; + std::string content; // JSON string, parsed elsewhere + + Cmd() = default; + Cmd(std::string cmd, std::string cont) + : command(std::move(cmd)), content(std::move(cont)) {} +}; + +constexpr const char* to_string(LogSource source) { + switch (source) { + case LogSource::MAIN: return "MAIN"; + case LogSource::CTRL: return "CTRL"; + case LogSource::NAV: return "NAV"; + case LogSource::WSKT: return "WSKT"; + case LogSource::LCAL: return "LCAL"; + case LogSource::PRCP: return "PRCP"; + } + return "UNKNOWN"; +} + +constexpr const char* to_string(LogDest dest) { + switch (dest) { + case LogDest::BASE: return "BASE"; + case LogDest::LOG: return "LOG"; + } + return "UNKNOWN"; +} diff --git a/auv_cpp/tests/core/CMakeLists.txt b/auv_cpp/tests/core/CMakeLists.txt new file mode 100644 index 0000000..30e3506 --- /dev/null +++ b/auv_cpp/tests/core/CMakeLists.txt @@ -0,0 +1,15 @@ +# ============================================================================= +# tests/core/CMakeLists.txt - Core types tests +# ============================================================================= + +# Create test executable +add_executable(test_types test_types.cpp) + +# Link against our core library +target_link_libraries(test_types PRIVATE + auv_core + Threads::Threads +) + +# Register with CTest +add_test(NAME test_types COMMAND test_types) diff --git a/auv_cpp/tests/core/test_types.cpp b/auv_cpp/tests/core/test_types.cpp new file mode 100644 index 0000000..0652bc2 --- /dev/null +++ b/auv_cpp/tests/core/test_types.cpp @@ -0,0 +1,132 @@ +#include +#include "core/types.hpp" +#include + +template +void print_array(const std::array& arr, const char* name) { + std::cout << name << ": ["; + for (size_t i = 0; i < N; ++i) { + std::cout << arr[i]; + if (i < N - 1) std::cout << ", "; + } + std::cout << "]\n"; +} + +void test_state() { + std::cout << "\n=== Testing State ===\n"; + + // Default construction + State s1; + assert(s1.position[0] == 0.0); + assert(s1.velocity[2] == 0.0); + + // Parameterized construction + State s2({10.0, 20.0, -5.0}, {1.0, 0.5, 0.0}, {0.0, 5.0, 90.0}, {0.0, 0.0, 0.1}); + assert(s2.position[0] == 10.0); + assert(s2.attitude[2] == 90.0); + + // Modification + s1.position[2] = -3.0; + assert(s1.position[2] == -3.0); + + std::cout << "State tests passed!\n"; +} + +void test_motor_speeds() { + std::cout << "\n=== Testing MotorSpeeds ===\n"; + + // Default + MotorSpeeds m1; + assert(m1.forward == 0.0); + + // Valid + MotorSpeeds m2(0.5, -0.3, 0.0, 0.8); + assert(m2.forward == 0.5); + + // Invalid should throw + bool caught = false; + try { + MotorSpeeds m3(1.5, 0.0, 0.0, 0.0); + } catch (const std::invalid_argument&) { + caught = true; + } + assert(caught); + + // Clamped + MotorSpeeds m4 = MotorSpeeds::clamped(1.5, -2.0, 0.5, 0.0); + assert(m4.forward == 1.0); + assert(m4.turn == -1.0); + + std::cout << "MotorSpeeds tests passed!\n"; +} + +void test_log() { + std::cout << "\n=== Testing Log ===\n"; + + Log log1(LogSource::CTRL, "info", "Control loop started"); + assert(log1.source == LogSource::CTRL); + assert(log1.dest == LogDest::LOG); + + State s; + s.position = {1.0, 2.0, 3.0}; + Log log2(LogSource::LCAL, "state", s, LogDest::BASE); + assert(log2.dest == LogDest::BASE); + + assert(std::holds_alternative(log1.content)); + assert(std::holds_alternative(log2.content)); + + std::cout << "Log tests passed!\n"; +} + +void test_promise() { + std::cout << "\n=== Testing Promise ===\n"; + + bool executed = false; + Promise p("test", 0.0, [&executed]() { executed = true; }); + + assert(p.name == "test"); + if (p.is_ready()) { + p.callback(); + } + assert(executed); + + Promise p2("delayed", 1.0, []() {}); + assert(!p2.is_ready()); + + std::cout << "Promise tests passed!\n"; +} + +void test_inheritance() { + std::cout << "\n=== Testing Inheritance ===\n"; + + ExpandedState es; + es.position = {1.0, 2.0, 3.0}; + es.local_force = {10.0, 0.0, 5.0}; + assert(es.position[0] == 1.0); + assert(es.local_force[0] == 10.0); + + InitialState is; + is.mass = 10.0; + is.inertia[0][0] = 1.0; + assert(is.mass == 10.0); + + std::cout << "Inheritance tests passed!\n"; +} + +int main() { + std::cout << "========================================\n"; + std::cout << "Nautilus AUV - Core Types Tests\n"; + std::cout << "========================================\n"; + + test_state(); + test_motor_speeds(); + test_log(); + test_promise(); + test_inheritance(); + + std::cout << "\n========================================\n"; + std::cout << "All tests passed!\n"; + std::cout << "========================================\n"; + + return 0; +} From 85881afb1bd3f51aaa303d0a69ce42fc8a4ef652 Mon Sep 17 00:00:00 2001 From: SuhaanCoding Date: Sun, 25 Jan 2026 03:46:23 -0800 Subject: [PATCH 2/2] Add CI/CD workflows for C++ and Frontend --- .github/workflows/cpp.yml | 119 +++++++++++++++++++++++++ .github/workflows/frontend.yml | 103 +++++++++++++++++++++ .pre-commit-config.yaml | 42 +++++++++ .yamllint.yml | 15 ++++ auv_cpp/.clang-tidy | 30 +++++++ auv_cpp/.dockerignore | 13 +++ auv_cpp/Dockerfile | 25 ++++++ docker-compose.yml | 68 ++++++++++++++ web_app/frontend_gui/.dockerignore | 18 ++++ web_app/frontend_gui/.prettierrc | 11 +++ web_app/frontend_gui/Dockerfile | 13 +++ web_app/frontend_gui/eslint.config.js | 72 +++++++++++---- web_app/frontend_gui/package.json | 24 ++++- web_app/frontend_gui/src/test/setup.ts | 1 + web_app/frontend_gui/vitest.config.ts | 23 +++++ 15 files changed, 556 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/cpp.yml create mode 100644 .github/workflows/frontend.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .yamllint.yml create mode 100644 auv_cpp/.clang-tidy create mode 100644 auv_cpp/.dockerignore create mode 100644 auv_cpp/Dockerfile create mode 100644 docker-compose.yml create mode 100644 web_app/frontend_gui/.dockerignore create mode 100644 web_app/frontend_gui/.prettierrc create mode 100644 web_app/frontend_gui/Dockerfile create mode 100644 web_app/frontend_gui/src/test/setup.ts create mode 100644 web_app/frontend_gui/vitest.config.ts diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml new file mode 100644 index 0000000..b353195 --- /dev/null +++ b/.github/workflows/cpp.yml @@ -0,0 +1,119 @@ +name: C++ CI + +on: + push: + branches: [main, develop, cpp-migration] + paths: + - "auv_cpp/**" + - ".github/workflows/cpp.yml" + pull_request: + branches: [main, develop, cpp-migration] + paths: + - "auv_cpp/**" + +jobs: + format-check: + name: Format Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t auv-cpp auv_cpp/ + + - name: Check clang-format + run: | + docker run --rm auv-cpp bash -c " + find src include tests -name '*.cpp' -o -name '*.hpp' -o -name '*.h' 2>/dev/null | \ + xargs -r clang-format --dry-run --Werror + " + + lint: + name: Static Analysis + runs-on: ubuntu-latest + needs: format-check + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t auv-cpp auv_cpp/ + + - name: Run clang-tidy + run: | + docker run --rm auv-cpp bash -c " + cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DBUILD_TESTS=ON && \ + find src -name '*.cpp' 2>/dev/null | xargs -r clang-tidy -p build --quiet + " + continue-on-error: true + + build-test: + name: Build & Test (${{ matrix.build_type }}) + runs-on: ubuntu-latest + needs: format-check + + strategy: + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: auv_cpp + load: true + tags: auv-cpp:${{ matrix.build_type }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and Test + run: | + docker run --rm auv-cpp:${{ matrix.build_type }} bash -c " + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DBUILD_TESTS=ON \ + ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS=--coverage' || '' }} && \ + cmake --build build && \ + ctest --test-dir build --output-on-failure + " + + coverage: + name: Code Coverage + runs-on: ubuntu-latest + needs: build-test + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t auv-cpp auv_cpp/ + + - name: Build with coverage and run tests + run: | + docker run --rm -v ${{ github.workspace }}/coverage:/coverage auv-cpp bash -c " + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=ON \ + -DCMAKE_CXX_FLAGS='--coverage' && \ + cmake --build build && \ + ctest --test-dir build --output-on-failure && \ + lcov --capture --directory build \ + --output-file /coverage/coverage.info --ignore-errors mismatch && \ + lcov --remove /coverage/coverage.info '/usr/*' '*/tests/*' \ + --output-file /coverage/coverage.info --ignore-errors unused + " + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage/coverage.info + flags: cpp + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 0000000..56c4f3f --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,103 @@ +name: Frontend CI + +on: + push: + branches: [main, develop] + paths: + - "web_app/frontend_gui/**" + - ".github/workflows/frontend.yml" + pull_request: + branches: [main, develop] + paths: + - "web_app/frontend_gui/**" + +jobs: + lint: + name: Lint & Format Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: web_app/frontend_gui + load: true + tags: frontend:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: ESLint check + run: docker run --rm frontend npm run lint + continue-on-error: true # Don't block until existing issues are fixed + + - name: Prettier check + run: docker run --rm frontend npm run format:check + continue-on-error: true # Don't block until existing issues are fixed + + typecheck: + name: Type Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t frontend web_app/frontend_gui/ + + - name: TypeScript type check + run: docker run --rm frontend npm run typecheck + continue-on-error: true # Don't block until existing issues are fixed + + build: + name: Build + runs-on: ubuntu-latest + # Removed dependency on lint/typecheck so build runs even if they fail + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: web_app/frontend_gui + load: true + tags: frontend:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build production bundle + run: docker run --rm frontend npm run build + + test: + name: Tests + runs-on: ubuntu-latest + needs: lint + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t frontend web_app/frontend_gui/ + + - name: Run Vitest with coverage + run: | + docker run --rm -v ${{ github.workspace }}/coverage:/app/coverage frontend \ + npm run test:coverage + continue-on-error: true + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: coverage/coverage-final.json + flags: frontend + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d9032b7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +# Pre-commit hooks for Neptune repository (C++ and Frontend only) +# Install: pip install pre-commit && pre-commit install +# Run manually: pre-commit run --all-files + +repos: + # General file checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: [--unsafe] + - id: check-json + - id: check-added-large-files + args: ["--maxkb=1000"] + - id: check-merge-conflict + + # C++ - clang-format + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.8 + hooks: + - id: clang-format + files: ^auv_cpp/.*\.(cpp|hpp|h)$ + args: [--style=file] + + # TypeScript/JavaScript - ESLint + Prettier + # NOTE: Disabled locally - CI will run these checks + # To lint manually: cd web_app/frontend_gui && npm run lint + # To format: cd web_app/frontend_gui && npm run format + + # YAML validation + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + args: [-c, .yamllint.yml] + files: \.(yaml|yml)$ + +ci: + autofix_commit_msg: "style: auto-fix by pre-commit hooks" + autoupdate_commit_msg: "chore: update pre-commit hooks" diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..9962218 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,15 @@ +extends: default + +rules: + line-length: + max: 120 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + truthy: + allowed-values: ["true", "false", "on", "off", "yes", "no"] + comments: + min-spaces-from-content: 1 + document-start: disable + indentation: + spaces: 2 + indent-sequences: true diff --git a/auv_cpp/.clang-tidy b/auv_cpp/.clang-tidy new file mode 100644 index 0000000..b1ae60b --- /dev/null +++ b/auv_cpp/.clang-tidy @@ -0,0 +1,30 @@ +--- +Checks: > + -*, + bugprone-*, + clang-analyzer-*, + cppcoreguidelines-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-magic-numbers, + -cppcoreguidelines-avoid-magic-numbers, + -readability-identifier-length, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay + +WarningsAsErrors: '' + +HeaderFilterRegex: '.*' + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.NamespaceCase + value: lower_case diff --git a/auv_cpp/.dockerignore b/auv_cpp/.dockerignore new file mode 100644 index 0000000..2c5c4ce --- /dev/null +++ b/auv_cpp/.dockerignore @@ -0,0 +1,13 @@ +# Ignore build artifacts +build/ +cmake-build-*/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Git +.git/ +.gitignore diff --git a/auv_cpp/Dockerfile b/auv_cpp/Dockerfile new file mode 100644 index 0000000..2ac5961 --- /dev/null +++ b/auv_cpp/Dockerfile @@ -0,0 +1,25 @@ +FROM debian:bookworm-slim + +# Install build tools and C++ dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + git \ + clang-format \ + clang-tidy \ + # C++ dependencies (from CLAUDE.md section 5.5) + libeigen3-dev \ + libboost-all-dev \ + libyaml-cpp-dev \ + # Testing & coverage + lcov \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy source code +COPY . . + +# Default command: build and test +CMD ["bash", "-c", "cmake -B build -G Ninja -DBUILD_TESTS=ON && cmake --build build && ctest --test-dir build --output-on-failure"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e1e0194 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,68 @@ +services: + # C++ build environment + cpp: + build: + context: ./auv_cpp + dockerfile: Dockerfile + volumes: + - ./auv_cpp:/app + working_dir: /app + command: bash + stdin_open: true + tty: true + + # C++ build (one-shot) - cleans build dir to avoid Dev Container conflicts + cpp-build: + build: + context: ./auv_cpp + dockerfile: Dockerfile + volumes: + - ./auv_cpp:/app + working_dir: /app + command: bash -c "rm -rf build && cmake -B build -G Ninja -DBUILD_TESTS=ON && cmake --build build" + + # C++ test (one-shot) - cleans build dir to avoid Dev Container conflicts + cpp-test: + build: + context: ./auv_cpp + dockerfile: Dockerfile + volumes: + - ./auv_cpp:/app + working_dir: /app + command: > + bash -c "rm -rf build && + cmake -B build -G Ninja -DBUILD_TESTS=ON && + cmake --build build && + ctest --test-dir build --output-on-failure" + + # Frontend dev server + frontend: + build: + context: ./web_app/frontend_gui + dockerfile: Dockerfile + volumes: + - ./web_app/frontend_gui:/app + - /app/node_modules # Use container's node_modules + ports: + - "5173:5173" + command: npm run dev -- --host + + # Frontend build (one-shot) + frontend-build: + build: + context: ./web_app/frontend_gui + dockerfile: Dockerfile + volumes: + - ./web_app/frontend_gui:/app + - /app/node_modules + command: npm run build + + # Frontend lint (one-shot) + frontend-lint: + build: + context: ./web_app/frontend_gui + dockerfile: Dockerfile + volumes: + - ./web_app/frontend_gui:/app + - /app/node_modules + command: bash -c "npm run lint && npm run format:check" diff --git a/web_app/frontend_gui/.dockerignore b/web_app/frontend_gui/.dockerignore new file mode 100644 index 0000000..4c03d27 --- /dev/null +++ b/web_app/frontend_gui/.dockerignore @@ -0,0 +1,18 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Git +.git/ +.gitignore + +# Coverage +coverage/ diff --git a/web_app/frontend_gui/.prettierrc b/web_app/frontend_gui/.prettierrc new file mode 100644 index 0000000..9fd45fe --- /dev/null +++ b/web_app/frontend_gui/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "jsxSingleQuote": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/web_app/frontend_gui/Dockerfile b/web_app/frontend_gui/Dockerfile new file mode 100644 index 0000000..27e47a8 --- /dev/null +++ b/web_app/frontend_gui/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-slim + +WORKDIR /app + +# Install dependencies first (for better caching) +COPY package*.json ./ +RUN npm ci + +# Copy source code +COPY . . + +# Default command: run dev server +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/web_app/frontend_gui/eslint.config.js b/web_app/frontend_gui/eslint.config.js index 238d2e4..c58bd71 100644 --- a/web_app/frontend_gui/eslint.config.js +++ b/web_app/frontend_gui/eslint.config.js @@ -1,38 +1,72 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' +import js from "@eslint/js"; +import globals from "globals"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; -export default [ - { ignores: ['dist'] }, +export default tseslint.config( + { ignores: ["dist", "node_modules", "coverage"] }, { - files: ['**/*.{js,jsx}'], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, parserOptions: { - ecmaVersion: 'latest', ecmaFeatures: { jsx: true }, - sourceType: 'module', + sourceType: "module", }, }, - settings: { react: { version: '18.3' } }, + settings: { react: { version: "19.0" } }, plugins: { react, - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { - ...js.configs.recommended.rules, ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, ...reactHooks.configs.recommended.rules, - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "warn", }, }, -] + { + files: ["**/*.{js,jsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaFeatures: { jsx: true }, + sourceType: "module", + }, + }, + settings: { react: { version: "19.0" } }, + plugins: { + react, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + ...reactHooks.configs.recommended.rules, + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + } +); diff --git a/web_app/frontend_gui/package.json b/web_app/frontend_gui/package.json index 2928cdb..9b46c14 100644 --- a/web_app/frontend_gui/package.json +++ b/web_app/frontend_gui/package.json @@ -7,7 +7,14 @@ "start": "vite", "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint src/ --ext .ts,.tsx", + "lint:fix": "eslint src/ --ext .ts,.tsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@react-three/fiber": "^9.1.2", @@ -20,13 +27,26 @@ "three": "^0.174.0" }, "devDependencies": { + "@eslint/js": "^9.0.0", + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^16.0.0", "@types/leaflet": "^1.9.17", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.2", "@types/react-grid-layout": "^1.3.5", "@types/three": "^0.175.0", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^2.0.0", + "eslint": "^9.0.0", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.5", + "globals": "^15.0.0", + "jsdom": "^25.0.0", + "prettier": "^3.2.0", "typescript": "^5.8.3", - "vite": "^6.2.6" + "typescript-eslint": "^8.0.0", + "vite": "^6.2.6", + "vitest": "^2.0.0" } } diff --git a/web_app/frontend_gui/src/test/setup.ts b/web_app/frontend_gui/src/test/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/web_app/frontend_gui/src/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/web_app/frontend_gui/vitest.config.ts b/web_app/frontend_gui/vitest.config.ts new file mode 100644 index 0000000..304f4bb --- /dev/null +++ b/web_app/frontend_gui/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/test/setup.ts", + include: ["src/**/*.{test,spec}.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "src/**/*.d.ts", + "src/test/**", + "src/main.tsx", + "src/vite-env.d.ts", + ], + }, + }, +});