diff --git a/README.md b/README.md index 5cc2dd5..877a3d4 100644 --- a/README.md +++ b/README.md @@ -18,107 +18,20 @@ Web-based Modbus relay control system for managing 8-channel relay modules over TCP. -> **⚠️ Development Status**: This project is in early development. Core features are currently being implemented following a specification-driven approach. - ## Overview STA will provide a modern web interface for controlling Modbus-compatible relay devices, eliminating the need for specialized industrial software. The goal is to enable browser-based relay control with real-time status updates. ## Current Status -### Phase 1 Complete - Foundation -- ✅ Monorepo structure (backend + frontend at root) -- ✅ Rust web server with Poem 3.1 framework -- ✅ Configuration system (YAML + environment variables) -- ✅ Modbus TCP and relay settings structures -- ✅ Health check and metadata API endpoints -- ✅ OpenAPI documentation with Swagger UI -- ✅ Rate limiting middleware -- ✅ SQLite schema and repository for relay labels -- ✅ Vue 3 + TypeScript frontend scaffolding with Vite -- ✅ Type-safe API client generation from OpenAPI specs +**US1 (MVP) — Complete** — Users can view all 8 relay states and toggle individual relays on/off via the web UI, backed by a Rust API with Modbus TCP control. Phases 1–4 complete: domain layer with type-driven development, infrastructure with mock/real Modbus controllers and SQLite persistence, application use cases, REST API with OpenAPI docs, and Vue 3 frontend with real-time polling. -### Phase 0.5 Complete - CORS Configuration & Production Security -- ✅ T009: CorsSettings struct with comprehensive unit tests (5 tests) -- ✅ T010: CorsSettings implementation with restrictive fail-safe defaults -- ✅ T011: Development YAML configuration with permissive CORS -- ✅ T012: Production YAML configuration with restrictive CORS -- ✅ T013: From for Cors trait unit tests (6 tests) -- ✅ T014: From for Cors implementation with security validation -- ✅ T015: Middleware chain integration using From trait -- ✅ T016: Integration tests for CORS headers (9 comprehensive tests) - -#### Key CORS Features Implemented -- Environment-specific CORS configuration (development vs production) -- Wildcard origin support for development (`allowed_origins: ["*"]`) -- Multiple specific origins for production -- Credentials support for Authelia authentication -- Security validation (prevents wildcard + credentials) -- Configurable preflight cache duration -- Hardcoded secure methods and headers -- Structured logging for CORS configuration -- Comprehensive test coverage (15 tests total) - -### Phase 2 Complete - Domain Layer (Type-Driven Development) -- ✅ T017-T018: RelayId newtype with 1-8 validation and zero-cost abstraction -- ✅ T019-T020: RelayState enum (On/Off) with serialization support -- ✅ T021-T022: Relay aggregate with state control methods (toggle, turn_on, turn_off) -- ✅ T023-T024: RelayLabel newtype with 1-50 character validation -- ✅ T025-T026: ModbusAddress type with From trait (1-8 → 0-7 offset mapping) -- ✅ T027: HealthStatus enum with state machine (Healthy/Degraded/Unhealthy) - -#### Key Domain Layer Features Implemented -- 100% test coverage for domain layer (50+ comprehensive tests) -- Zero external dependencies (pure business logic) -- All newtypes use `#[repr(transparent)]` for zero-cost abstractions -- Smart constructors with `Result` for type-safe validation -- TDD workflow (red-green-refactor) for all implementations -- RelayController and RelayLabelRepository trait definitions -- Complete separation from infrastructure concerns (hexagonal architecture) - -### Phase 3 Complete - Infrastructure Layer -- ✅ T028-T029: MockRelayController tests and implementation -- ✅ T030: RelayController trait with async methods (read_state, write_state, read_all, write_all) -- ✅ T031: ControllerError enum (ConnectionError, Timeout, ModbusException, InvalidRelayId) -- ✅ T032: MockRelayController comprehensive tests (6 tests) -- ✅ T025a-f: ModbusRelayController implementation (decomposed): - - Connection setup with tokio-modbus - - Timeout-wrapped read_coils and write_single_coil helpers - - RelayController trait implementation -- ✅ T034: Integration test with real hardware (uses #[ignore] attribute) -- ✅ T035-T036: RelayLabelRepository trait and SQLite implementation -- ✅ T037-T038: MockRelayLabelRepository for testing -- ✅ T039-T040: HealthMonitor service with state tracking - -#### Key Infrastructure Features Implemented -- **ModbusRelayController**: Thread-safe Modbus TCP client with timeout handling - - Uses `Arc>` for concurrent access - - Native Modbus TCP protocol (MBAP header, no CRC16) - - Configurable timeout with `tokio::time::timeout` -- **MockRelayController**: In-memory testing without hardware - - Uses `Arc>>` for state - - Optional timeout simulation for error handling tests -- **SqliteRelayLabelRepository**: Compile-time verified SQL queries - - Automatic migrations via SQLx - - In-memory mode for testing -- **HealthMonitor**: State machine for health tracking - - Healthy -> Degraded -> Unhealthy transitions - - Recovery on successful operations - -### Planned - Phases 4-8 -- 📋 US1: Monitor & toggle relay states - MVP (Phase 4) -- 📋 US2: Bulk relay controls (Phase 5) -- 📋 US3: Health status display (Phase 6) -- 📋 US4: Relay labeling (Phase 7) -- 📋 Production deployment (Phase 8) - -See [tasks.org](specs/001-modbus-relay-control/tasks.org) for detailed implementation roadmap. ## Architecture **Current:** - **Backend**: Rust 2024 with Poem web framework (hexagonal architecture) -- **Configuration**: YAML-based with environment variable overrides +- **Frontend**: Vue 3 + TypeScript with real-time polling (2s interval) - **API**: RESTful HTTP with OpenAPI documentation - **CORS**: Production-ready configurable middleware with security validation - **Middleware Chain**: Rate Limiting -> CORS -> Data injection @@ -126,7 +39,6 @@ See [tasks.org](specs/001-modbus-relay-control/tasks.org) for detailed implement - **Persistence**: SQLite for relay labels with compile-time SQL verification **Planned:** -- **Frontend**: Vue 3 with TypeScript - **Deployment**: Backend on Raspberry Pi, frontend on Cloudflare Pages - **Access**: Traefik reverse proxy with Authelia authentication @@ -140,17 +52,20 @@ See [tasks.org](specs/001-modbus-relay-control/tasks.org) for detailed implement ### Development ```bash -# Run development server -just run +# Run backend development server +just backend run -# Run tests -just test +# Run frontend +just frontend run -# Run linter -just lint +# Run backend tests +just backend test -# Format code -just format +# Run backend linter +just backend lint + +# Format backend code +just backend format # Watch mode with bacon bacon # clippy-all (default) @@ -163,9 +78,9 @@ Edit `backend/settings/base.yaml` for Modbus device settings: ```yaml modbus: - host: "192.168.0.200" + host: "192.168.1.200" port: 502 - slave_id: 0 + slave_id: 1 timeout_secs: 5 relay: @@ -184,8 +99,7 @@ APP__MODBUS__HOST=192.168.1.100 cargo run ```yaml # backend/settings/development.yaml cors: - allowed_origins: - - "*" # Permissive for local development + allowed_origins: ["*"] # Permissive for local development allow_credentials: false # MUST be false with wildcard max_age_secs: 3600 ``` @@ -225,12 +139,12 @@ The server provides OpenAPI documentation via Swagger UI: - OpenAPI Spec: `http://localhost:3100/specs` **Current Endpoints:** +- `GET /api/relays` - List all relay states +- `POST /api/relays/{id}/toggle` - Toggle individual relay state - `GET /api/health` - Health check endpoint - `GET /api/meta` - Application metadata **Planned Endpoints (see spec):** -- `GET /api/relays` - List all relay states -- `POST /api/relays/{id}/toggle` - Toggle relay state - `POST /api/relays/all/on` - Turn all relays on - `POST /api/relays/all/off` - Turn all relays off - `PUT /api/relays/{id}/label` - Set relay label @@ -239,74 +153,133 @@ The server provides OpenAPI documentation via Swagger UI: **Monorepo Layout:** ``` -sta/ # Repository root -├── backend/ # Rust backend workspace member +sta/ +├── backend/ # Rust backend │ ├── src/ -│ │ ├── lib.rs - Library entry point -│ │ ├── main.rs - Binary entry point -│ │ ├── startup.rs - Application builder and server config -│ │ ├── telemetry.rs - Logging and tracing setup +│ │ ├── main.rs - Binary entry point +│ │ ├── lib.rs - Library entry point +│ │ ├── startup.rs - Application builder and server wiring +│ │ ├── telemetry.rs - Logging and tracing setup │ │ │ -│ │ ├── domain/ - Business logic layer (Phase 2) -│ │ │ ├── relay/ - Relay domain aggregate -│ │ │ │ ├── types/ - RelayId, RelayState, RelayLabel newtypes -│ │ │ │ ├── entity.rs - Relay aggregate with state control -│ │ │ │ ├── controller.rs - RelayController trait & ControllerError -│ │ │ │ └── repository/ - RelayLabelRepository trait -│ │ │ ├── modbus.rs - ModbusAddress type with conversion -│ │ │ └── health.rs - HealthStatus state machine +│ │ ├── domain/ - Business logic +│ │ │ ├── health.rs - HealthStatus state machine +│ │ │ ├── modbus.rs - ModbusAddress type +│ │ │ └── relay/ +│ │ │ ├── entity.rs - Relay aggregate (state control) +│ │ │ ├── controller.rs - RelayController trait +│ │ │ ├── types/ +│ │ │ │ ├── relayid.rs - RelayId newtype (1..=8) +│ │ │ │ ├── relaylabel.rs - RelayLabel newtype +│ │ │ │ └── relaystate.rs - RelayState enum (On/Off) +│ │ │ └── repository/ +│ │ │ └── label.rs - RelayLabelRepository trait │ │ │ -│ │ ├── application/ - Use cases and orchestration (Phase 3) -│ │ │ └── health/ - Health monitoring service -│ │ │ └── health_monitor.rs - HealthMonitor with state tracking +│ │ ├── application/ - Use cases +│ │ │ ├── health/ +│ │ │ │ └── health_monitor.rs - Health monitoring +│ │ │ └── use_cases/ +│ │ │ ├── get_all_relays.rs - List all relays +│ │ │ └── toggle_relay.rs - Toggle single relay │ │ │ -│ │ ├── infrastructure/ - External integrations (Phase 3) -│ │ │ ├── modbus/ - Modbus TCP communication -│ │ │ │ ├── client.rs - ModbusRelayController (real hardware) -│ │ │ │ ├── client_test.rs - Hardware integration tests -│ │ │ │ └── mock_controller.rs - MockRelayController for testing -│ │ │ └── persistence/ - Database layer -│ │ │ ├── entities/ - Database record types -│ │ │ ├── sqlite_repository.rs - SqliteRelayLabelRepository -│ │ │ └── label_repository.rs - MockRelayLabelRepository +│ │ ├── infrastructure/ - External integrations +│ │ │ ├── modbus/ +│ │ │ │ ├── client.rs - ModbusRelayController +│ │ │ │ ├── client_test.rs - Unit tests +│ │ │ │ ├── factory.rs - Controller factory (retry, fallback) +│ │ │ │ └── mock_controller.rs - MockRelayController +│ │ │ └── persistence/ +│ │ │ ├── factory.rs - Repository factory +│ │ │ ├── label_repository.rs - SQL implementation +│ │ │ ├── label_repository_tests.rs - Unit tests +│ │ │ ├── sqlite_repository.rs - SQLite implementation +│ │ │ └── entities/ +│ │ │ └── relay_label_record.rs - DB row struct │ │ │ -│ │ ├── presentation/ - API layer (planned Phase 4) -│ │ ├── settings/ - Configuration module -│ │ │ ├── mod.rs - Settings aggregation -│ │ │ └── cors.rs - CORS configuration -│ │ ├── route/ - HTTP endpoint handlers -│ │ │ ├── health.rs - Health check endpoints -│ │ │ └── meta.rs - Application metadata -│ │ └── middleware/ - Custom middleware -│ │ └── rate_limit.rs +│ │ ├── presentation/ - API handlers and DTOs +│ │ │ ├── error.rs - API error types +│ │ │ ├── api/ +│ │ │ │ └── relay_api.rs - Relay HTTP handlers +│ │ │ └── dto/ +│ │ │ └── relay_dto.rs - Relay DTOs +│ │ │ +│ │ ├── route/ - Route definitions +│ │ │ ├── health.rs - Health check +│ │ │ └── meta.rs - App metadata +│ │ │ +│ │ ├── middleware/ +│ │ │ └── rate_limit.rs - Rate limiting +│ │ │ +│ │ └── settings/ - Configuration +│ │ ├── application.rs - App-wide settings +│ │ ├── cors.rs - CORS settings +│ │ ├── database.rs - Database settings +│ │ ├── environment.rs - Environment enum +│ │ ├── modbus.rs - Modbus settings +│ │ ├── rate_limiting.rs - Rate limit config +│ │ └── relay.rs - Relay settings │ │ -│ ├── settings/ - YAML configuration files -│ │ ├── base.yaml - Base configuration -│ │ ├── development.yaml - Development overrides -│ │ └── production.yaml - Production overrides -│ └── tests/ - Integration tests -│ └── cors_test.rs - CORS integration tests +│ ├── settings/ - YAML config files +│ │ ├── base.yaml +│ │ ├── development.yaml +│ │ └── production.yaml +│ │ +│ └── tests/ - Integration/contract tests +│ ├── contract/ +│ │ └── test_relay_api.rs - Relay API contract tests +│ ├── cors_test.rs - CORS integration tests +│ ├── modbus_hardware_test.rs - Hardware tests (#[ignore]) +│ ├── sqlite_repository_test.rs - SQLite integration tests +│ └── sqlite_repository_functional_test.rs - Functional tests │ -├── migrations/ - SQLx database migrations -├── src/ # Frontend source (Vue/TypeScript) -│ └── api/ - Type-safe API client -├── docs/ # Project documentation -│ ├── cors-configuration.md - CORS setup guide -│ ├── domain-layer.md - Domain layer architecture -│ └── Modbus_POE_ETH_Relay.md - Hardware documentation -├── specs/ # Feature specifications -│ ├── constitution.md - Architectural principles +├── src/ # Frontend (Vue 3 + TypeScript) +│ ├── main.ts - App entry point +│ ├── App.vue - Root component +│ ├── style.css / style.less - Global styles +│ ├── api/ +│ │ ├── client.ts - HTTP client +│ │ └── schema.ts - API types +│ ├── components/ +│ │ ├── RelayCard.vue - Relay card +│ │ ├── StaFooter.vue - Footer +│ │ └── StaHeader.vue - Header +│ ├── composables/ +│ │ ├── useMeta.ts - Page metadata +│ │ ├── useRelay.ts - Relay state management +│ │ └── useRelayPolling.ts - Real-time polling (2s) +│ ├── pages/ +│ │ └── RelaysView.vue - Main relay view +│ ├── types/ +│ │ ├── relay.ts - Relay type definitions +│ │ └── mappers/ +│ │ └── relayDtoMapper.ts +│ └── utils/ +│ └── isNil.ts +│ +├── migrations/ - SQLx migrations +│ ├── 0001_relay-labels.up.sql +│ └── 0001_relay-labels.down.sql +│ +├── docs/ - Documentation +│ ├── cors-configuration.md +│ ├── domain-layer.md +│ └── Modbus_POE_ETH_Relay.md +│ +├── specs/ - Specifications +│ ├── constitution.md │ └── 001-modbus-relay-control/ -│ ├── spec.md - Feature specification -│ ├── plan.md - Implementation plan -│ ├── tasks.org - Task breakdown (org-mode format) -│ ├── data-model.md - Data model specification -│ ├── types-design.md - Domain types design -│ ├── domain-layer-architecture.md - Domain layer docs -│ └── lessons-learned.md - Phase 2/3 insights -├── package.json - Frontend dependencies -├── vite.config.ts - Vite build configuration -└── justfile - Build commands +│ ├── spec.md, plan.md, tasks.org +│ ├── data-model.md, types-design.md +│ ├── domain-layer-architecture.md +│ ├── lessons-learned.md +│ └── ... +│ +├── nix/ - Nix flake configs +├── public/ - Static assets +├── justfile - Build commands +├── package.json +├── vite.config.ts +├── Cargo.toml +└── flake.nix ``` ## Technology Stack @@ -324,9 +297,10 @@ sta/ # Repository root - thiserror (error handling) - serde + serde_yaml (configuration deserialization) -**Frontend** (scaffolding complete): -- Vue 3 + TypeScript +**Frontend** (US1 complete): +- Vue 3 + TypeScript with composables (useRelayPolling) - Vite build tool +- RelayCard and RelayGrid components with real-time polling - openapi-typescript (type-safe API client generation) ## Testing Strategy diff --git a/docs/cors-configuration.md b/docs/cors-configuration.md index cf4c5f4..abbf4a1 100644 --- a/docs/cors-configuration.md +++ b/docs/cors-configuration.md @@ -1,8 +1,8 @@ # CORS Configuration Guide -**Last Updated**: 2026-01-03 -**Related Tasks**: T009 (Tests), T010 (Implementation) -**Status**: Implemented (Phase 0.5) +**Last Updated**: 2026-01-23 +**Related Tasks**: T009-T016 +**Status**: Complete (Phase 0.5) ## Overview @@ -44,7 +44,7 @@ Relay Device (local network) ### CorsSettings Struct -Located in `backend/src/settings.rs` (lines 217-232): +Located in `backend/src/settings/cors.rs`: ```rust #[derive(Debug, serde::Deserialize, Clone)] @@ -76,7 +76,7 @@ The implementation uses a **hybrid approach** (Option C from research): - `allow_credentials`: Whether to allow cookies/auth headers - `max_age_secs`: How long browsers cache preflight responses -**Hardcoded in Implementation** (will be in T014): +**Hardcoded in Implementation**: - **Methods**: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` (API-specific) - **Headers**: `content-type`, `authorization` (minimum for API) @@ -109,7 +109,7 @@ frontend_url: http://localhost:5173 # Vite default port ### Production Environment -**File**: `backend/settings/production.yaml` (to be created in T012) +**File**: `backend/settings/production.yaml` ```yaml cors: @@ -129,23 +129,7 @@ frontend_url: "https://sta.example.com" ### Integration with Settings System -The `CorsSettings` struct is integrated into the main `Settings` struct (line 30): - -```rust -#[derive(Debug, serde::Deserialize, Clone, Default)] -pub struct Settings { - pub application: ApplicationSettings, - pub debug: bool, - pub frontend_url: String, - pub rate_limit: RateLimitSettings, - pub modbus: ModbusSettings, - pub relay: RelaySettings, - #[serde(default)] // Uses Default::default() if missing - pub cors: CorsSettings, -} -``` - -The `#[serde(default)]` attribute ensures backward compatibility: if the `cors` section is missing from YAML, it uses the restrictive `Default` implementation. +The `CorsSettings` struct is part of the settings module. Settings are loaded with `#[serde(default)]` to ensure backward compatibility: if the `cors` section is missing from YAML, it uses the restrictive `Default` implementation. ### Loading and Precedence @@ -318,7 +302,7 @@ cargo test -p sta cors -- --nocapture **Browser Security Policy**: When `allow_credentials: true`, wildcard origins (`*`) are **forbidden** by the CORS specification. -**Enforcement**: The upcoming `build_cors()` function (T014) will panic during startup if this constraint is violated: +**Enforcement**: The `From for Cors` implementation panics during startup if this constraint is violated: ```rust if settings.allow_credentials && settings.allowed_origins.contains(&"*".to_string()) { @@ -427,11 +411,9 @@ cors: ### Preflight Requests Failing (OPTIONS) -**Cause**: Backend not allowing OPTIONS method (will be fixed in T014). +**Cause**: Backend not allowing OPTIONS method. -**Temporary Workaround**: None - wait for T014 implementation. - -**Permanent Solution**: The upcoming `build_cors()` function will hardcode: +**Solution**: The `From for Cors` trait implementation hardcodes OPTIONS in the allowed methods: ```rust cors.allow_methods(vec![ Method::GET, Method::POST, Method::PUT, @@ -454,13 +436,13 @@ cors.allow_methods(vec![ ### Headers Not Allowed -**Cause**: Custom headers not in allowed list (will be in T014). +**Cause**: Custom headers not in allowed list. -**Current Allowed Headers** (to be implemented): +**Current Allowed Headers**: - `content-type` (for JSON request bodies) - `authorization` (for Authelia authentication tokens) -**Adding Custom Headers**: Requires modifying `build_cors()` function (T014). +**Adding Custom Headers**: Requires modifying the `From for Cors` trait implementation. ## Dependencies @@ -481,37 +463,43 @@ serde_yaml = "0.9.34" | File | Purpose | |------|---------| -| `backend/src/settings.rs` | `CorsSettings` struct definition | -| `backend/settings/base.yaml` | Baseline configuration (no CORS section yet) | +| `backend/src/settings/cors.rs` | `CorsSettings` struct definition | +| `backend/settings/base.yaml` | Baseline configuration | | `backend/settings/development.yaml` | Development CORS (permissive) | -| `backend/settings/production.yaml` | Production CORS (restrictive) - to be created in T012 | +| `backend/settings/production.yaml` | Production CORS (restrictive) | -## Next Steps (Remaining Tasks) +## Completed Tasks -### T011: Update development.yaml -- Add `cors:` section with permissive settings -- Update `frontend_url` to `http://localhost:5173` (Vite default) +All CORS configuration tasks (T009-T016) have been implemented and tested: -### T012: Create production.yaml -- Add `cors:` section with restrictive settings -- Use `https://sta.example.com` as allowed origin -- Set `allow_credentials: true` for Authelia +### T009-T010: CorsSettings Struct (Phase 0.5) +- 5 unit tests written (TDD approach) and the `CorsSettings` struct implemented with fail-safe defaults +- Located in `backend/src/settings/cors.rs` -### T013-T014: Implement build_cors() Function -- Create `build_cors(settings: &CorsSettings) -> Cors` in `startup.rs` -- Validate wildcard + credentials constraint -- Hardcode methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) -- Hardcode headers (content-type, authorization) -- Add structured logging +### T011: Development YAML Configuration +- Added `cors:` section with wildcard origin and `allow_credentials: false` +- Updated `frontend_url` to `http://localhost:5173` (Vite default) +- File: `backend/settings/development.yaml` -### T015: Replace Cors::new() in Middleware Chain -- Update `startup.rs` line ~86 -- Call `build_cors(&value.settings.cors)` +### T012: Production YAML Configuration +- Added `cors:` section with specific origin and `allow_credentials: true` +- File: `backend/settings/production.yaml` + +### T013-T014: Cors Middleware Implementation +- 6 unit tests written for the `From for Cors` trait +- Implemented the conversion trait in `backend/src/settings/cors.rs` +- Validates wildcard + credentials constraint (panics on misconfiguration) +- Hardcodes methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) +- Hardcodes headers (content-type, authorization) +- Adds structured logging + +### T015: Middleware Chain Integration +- Replaced `Cors::new()` with `Cors::from(settings.cors)` in startup.rs +- CORS applied after rate limiting (order: RateLimit → CORS → Data) ### T016: Integration Tests -- Write tests verifying CORS headers in HTTP responses -- Test OPTIONS preflight requests -- Verify `Access-Control-Allow-Origin` header +- 9 comprehensive integration tests in `backend/tests/cors_test.rs` +- Covers: preflight requests, actual request headers, max-age, credentials, methods, wildcard, multiple origins, unauthorized origin rejection ## References @@ -533,7 +521,10 @@ serde_yaml = "0.9.34" | 2026-01-03 | T009 | Test suite written (5 tests, TDD approach) | | 2026-01-03 | T010 | `CorsSettings` struct implemented with defaults | | 2026-01-03 | Documentation | This guide created | +| 2026-01-22 | T013-T014 | `From for Cors` trait implemented | +| 2026-01-22 | T015 | CORS middleware integrated into startup chain | +| 2026-01-22 | T016 | 9 integration tests written and passing | --- -**Maintainer Notes**: This configuration follows the project's **Type-Driven Development (TyDD)** and **Test-Driven Development (TDD)** principles. Tests were written first (T009), then the implementation (T010) was created to pass those tests. The upcoming `build_cors()` function (T014) will complete the CORS feature by applying these settings to the Poem middleware chain. +**Maintainer Notes**: This configuration follows the project's **Type-Driven Development (TyDD)** and **Test-Driven Development (TDD)** principles. Tests were written first (T009, T013), then implementations were created to pass those tests. The CORS feature is fully implemented and tested across all environments. diff --git a/docs/domain-layer.md b/docs/domain-layer.md index 22dfb25..e9e8cea 100644 --- a/docs/domain-layer.md +++ b/docs/domain-layer.md @@ -2,8 +2,8 @@ **Feature**: 001-modbus-relay-control **Phase**: 2 (Domain Layer - Type-Driven Development) -**Status**: Complete -**Last Updated**: 2026-01-04 +**Status**: Complete (US1 MVP also complete) +**Last Updated**: 2026-05-15 ## Overview @@ -419,13 +419,15 @@ backend/src/domain/ ├── modbus.rs # ModbusAddress type └── relay/ ├── mod.rs # Relay module exports - ├── controler.rs # RelayController trait (trait definition) + ├── controller.rs # RelayController trait (trait definition) ├── entity.rs # Relay aggregate - └── types/ - ├── mod.rs # Type exports - ├── relayid.rs # RelayId newtype - ├── relaystate.rs # RelayState enum - └── relaylabel.rs # RelayLabel newtype + ├── types/ + │ ├── mod.rs # Type exports + │ ├── relayid.rs # RelayId newtype + │ ├── relaystate.rs # RelayState enum + │ └── relaylabel.rs # RelayLabel newtype + └── repository/ + └── label.rs # RelayLabelRepository trait ``` ## Dependency Graph @@ -558,16 +560,16 @@ Coverage: 100% for domain layer ## Next Steps -**Phase 3: Infrastructure Layer** (Tasks T028-T040) +**Phase 4 (US1 MVP) — Complete** — Users can view all 8 relay states and toggle individual relays on/off via the web UI. -Now that domain types are complete, the infrastructure layer can: +The infrastructure, application, and presentation layers were built on top of these domain types: -1. Implement `RelayController` trait with real Modbus client -2. Create `MockRelayController` for testing -3. Implement `RelayLabelRepository` with SQLite -4. Use domain types throughout infrastructure code +1. **Infrastructure** (Phase 3): `ModbusRelayController` (real Modbus TCP client) + `MockRelayController` (testing), `SqliteRelayLabelRepository` for persistence, with factory functions for dependency injection +2. **Application** (Phase 3): `ToggleRelayUseCase`, `GetAllRelaysUseCase`, `HealthMonitor` service +3. **Presentation** (Phase 4): `RelayApi` handlers with `RelayDto`, REST endpoints (`GET /api/relays`, `POST /api/relays/{id}/toggle`) +4. **Frontend** (Phase 4): Vue 3 + TypeScript with `RelayCard`, `RelayGrid`, `useRelayPolling` composable (2s polling) -**Key advantage**: Infrastructure layer can depend on stable, well-tested domain types with strong guarantees. +**Upcoming phases**: US2 (bulk controls), US3 (health monitoring UI), US4 (relay labeling) ## References diff --git a/specs/001-modbus-relay-control/tasks.org b/specs/001-modbus-relay-control/tasks.org index 826ab75..0b1ab22 100644 --- a/specs/001-modbus-relay-control/tasks.org +++ b/specs/001-modbus-relay-control/tasks.org @@ -28,7 +28,7 @@ -------------- -* TODO Development phases [4/9] +* TODO Development phases [5/9] ** DONE Phase 1: Setup & Foundation (0.5 days) [8/8] *Purpose*: Initialize project dependencies and directory structure @@ -587,7 +587,9 @@ CLOSED: [2026-01-22 jeu. 00:02] -------------- -** STARTED Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [3/5] +** DONE Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [5/5] +CLOSED: [2026-05-15 ven. 03:59] +- State "DONE" from "STARTED" [2026-05-15 ven. 03:59] - State "STARTED" from "TODO" [2026-01-23 ven. 20:20] *Goal*: View current state of all 8 relays + toggle individual relay on/off @@ -891,12 +893,14 @@ CLOSED: [2026-05-14 jeu. 20:09] - *File*: =src/presentation/api/relay_api.rs= - *Complexity*: Low | *Uncertainty*: Low -*** TODO Frontend Implementation [1/2] +*** DONE Frontend Implementation [2/2] +CLOSED: [2026-05-15 ven. 03:57] +- State "DONE" from "TODO" [2026-05-15 ven. 03:57] - [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface - Generate from OpenAPI spec or manually define - *File*: =frontend/src/types/relay.ts= - *Complexity*: Low | *Uncertainty*: Low -- [ ] *T053* [P] [US1] [TDD] Create API client service +- [X] *T053* [P] [US1] [TDD] Create API client service - getAllRelays(): =Promise= - =toggleRelay(id: number): Promise= - *File*: =frontend/src/api/relayApi.ts= @@ -904,12 +908,14 @@ CLOSED: [2026-05-14 jeu. 20:09] -------------- -*** TODO T046: HTTP Polling Composable (DECOMPOSED) [0/7] +*** DONE T046: HTTP Polling Composable (DECOMPOSED) [7/7] +CLOSED: [2026-05-15 ven. 03:59] +- State "DONE" from "TODO" [2026-05-15 ven. 03:59] *Complexity*: High → Broken into 4 sub-tasks *Uncertainty*: Medium *Rationale*: Vue 3 lifecycle hooks, polling management, memory leak prevention -- [ ] *T046a* [US1] [TDD] Create =useRelayPolling= composable structure +- [X] *T046a* [US1] [TDD] Create =useRelayPolling= composable structure - Setup reactive refs: =relays=, =isLoading=, =error=, =lastFetchTime= - Define interval variable and fetch function signature @@ -948,10 +954,10 @@ CLOSED: [2026-05-14 jeu. 20:09] *TDD Checklist*: - - [ ] Test: Composable returns correct reactive refs - - [ ] Test: Initial state is ~loading=true~, ~relays=[]~, ~error=null~ + - [X] Test: Composable returns correct reactive refs + - [X] Test: Initial state is ~loading=true~, ~relays=[]~, ~error=null~ -- [ ] *T046b* [US1] [TDD] Implement =fetchData= with parallel requests +- [X] *T046b* [US1] [TDD] Implement =fetchData= with parallel requests - Fetch relays and health status in parallel using =Promise.all= - Update reactive state on success @@ -985,12 +991,12 @@ CLOSED: [2026-05-14 jeu. 20:09] *TDD Checklist*: - - [ ] Test: =fetchData()= updates relays on success - - [ ] Test: =fetchData()= sets error on API failure - - [ ] Test: =fetchData()= sets ~isLoading=false~ after completion - - [ ] Test: =fetchData()= updates =lastFetchTime= + - [X] Test: =fetchData()= updates relays on success + - [X] Test: =fetchData()= sets error on API failure + - [X] Test: =fetchData()= sets ~isLoading=false~ after completion + - [X] Test: =fetchData()= updates =lastFetchTime= -- [ ] *T046c* [US1] [TDD] Implement polling lifecycle with cleanup +- [X] *T046c* [US1] [TDD] Implement polling lifecycle with cleanup - =startPolling()=: Fetch immediately, then =setInterval= - =stopPolling()=: =clearInterval= and =cleanup= @@ -1029,12 +1035,12 @@ CLOSED: [2026-05-14 jeu. 20:09] *TDD Checklist*: - - [ ] Test: =startPolling()= triggers immediate fetch - - [ ] Test: =startPolling()= sets interval for subsequent fetches - - [ ] Test: =stopPolling()= clears interval - - [ ] Test: =onUnmounted= hook calls =stopPolling()= + - [X] Test: =startPolling()= triggers immediate fetch + - [X] Test: =startPolling()= sets interval for subsequent fetches + - [X] Test: =stopPolling()= clears interval + - [X] Test: =onUnmounted= hook calls =stopPolling()= -- [ ] *T046d* [US1] [TDD] Add connection status tracking +- [X] *T046d* [US1] [TDD] Add connection status tracking - Track =isConnected= based on fetch success/failure - Display connection status in UI @@ -1059,25 +1065,25 @@ CLOSED: [2026-05-14 jeu. 20:09] *TDD Checklist*: - - [ ] Test: =isConnected= is true after successful fetch - - [ ] Test: =isConnected= is false after failed fetch + - [X] Test: =isConnected= is true after successful fetch + - [X] Test: =isConnected= is false after failed fetch -------------- -- [ ] *T055* [US1] [TDD] Create =RelayCard= component +- [X] *T055* [US1] [TDD] Create =RelayCard= component - Props: relay (=RelayDto=) - Display relay ID, state, label - Emit toggle event on button click - *File*: =frontend/src/components/RelayCard.vue= - *Complexity*: Low | *Uncertainty*: Low -- [ ] *T056* [US1] [TDD] Create =RelayGrid= component +- [X] *T056* [US1] [TDD] Create =RelayGrid= component - Use =useRelayPolling= composable - Render 8 RelayCard components - Handle toggle events by calling API - Display loading/error states - *File*: =frontend/src/components/RelayGrid.vue= - *Complexity*: Medium | *Uncertainty*: Low -- [ ] *T057* [US1] [TDD] Integration test for US1 +- [X] *T057* [US1] [TDD] Integration test for US1 - End-to-end test: Load page → see 8 relays → toggle relay 1 → verify state change - Use Playwright or Cypress - *File*: =frontend/tests/e2e/relay-control.spec.ts= diff --git a/src/composables/useMeta.ts b/src/composables/useMeta.ts index 4f17da0..abd0f33 100644 --- a/src/composables/useMeta.ts +++ b/src/composables/useMeta.ts @@ -1,6 +1,7 @@ import { onMounted, ref } from 'vue'; -import type { components } from '../api/schema'; + import { apiClient } from '../api/client'; +import type { components } from '../api/schema'; type Meta = components['schemas']['Meta']; diff --git a/src/composables/useRelay.ts b/src/composables/useRelay.ts index e62e4e9..ffd48fb 100644 --- a/src/composables/useRelay.ts +++ b/src/composables/useRelay.ts @@ -1,4 +1,5 @@ import { ref } from 'vue'; + import { apiClient } from '../api/client'; import { relayDtoToDomain } from '../types/mappers/relayDtoMapper'; import type { Relay, RelayDto } from '../types/relay'; diff --git a/src/main.ts b/src/main.ts index b1dd5b7..0e467d0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ -import { createApp } from 'vue'; -import PrimeVue from 'primevue/config'; import Lara from '@primeuix/themes/lara'; +import PrimeVue from 'primevue/config'; +import { createApp } from 'vue'; import 'primeicons/primeicons.css'; import './style.css'; diff --git a/vite.config.ts b/vite.config.ts index f5dec20..fd6fdfa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,8 @@ import * as path from 'path'; +import tailwindcss from '@tailwindcss/vite'; import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; -import tailwindcss from '@tailwindcss/vite'; import vueDevTools from 'vite-plugin-vue-devtools'; // https://vite.dev/config/