docs(cors): add CORS configuration planning and tasks

Add comprehensive CORS planning documentation and task breakdown for
Phase 0.5 (8 tasks: T009-T016).

- Create research-cors.md with security analysis and decisions
- Add FR-022a to spec.md for production CORS requirements
- Update tasks.md: 94 → 102 tasks across 9 phases
- Document CORS in README and plan.md

Configuration approach: hybrid (configurable origins/credentials,
hardcoded methods/headers) with restrictive fail-safe defaults.
This commit is contained in:
2026-01-01 23:29:31 +01:00
parent 8e4433ceaa
commit 2365bbc9b3
5 changed files with 678 additions and 78 deletions

View File

@@ -1,14 +1,14 @@
# Implementation Tasks: Modbus Relay Control System
**Feature**: 001-modbus-relay-control
**Total Tasks**: 94 tasks across 8 phases
**MVP Delivery**: Phase 4 complete (Task 49)
**Parallelizable Tasks**: 35 tasks marked with `[P]`
**Total Tasks**: 102 tasks across 9 phases
**MVP Delivery**: Phase 4 complete (Task 57)
**Parallelizable Tasks**: 39 tasks marked with `[P]`
**Approach**: Type-Driven Development (TyDD) + Test-Driven Development (TDD), Backend API first
---
## Phase 1: Setup & Foundation (0.5 days)
## Phase 1: Setup & Foundation (0.5 days) DONE
**Purpose**: Initialize project dependencies and directory structure
@@ -55,7 +55,7 @@
- **Test**: `npm run dev` starts frontend dev server
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T008** [P] [Setup] [TDD] Generate TypeScript API client from OpenAPI
- [x] **T008** [P] [Setup] [TDD] Generate TypeScript API client from OpenAPI
- Add poem-openapi spec generation in startup.rs
- Generate frontend/src/api/client.ts from OpenAPI spec
- **Test**: TypeScript client compiles without errors
@@ -64,13 +64,140 @@
---
## Phase 0.5: CORS Configuration & Production Security (0.5 days)
**Purpose**: Replace permissive `Cors::new()` with configurable production-ready CORS
**⚠️ TDD CRITICAL**: Write failing tests FIRST for every configuration and function
- [x] **T009** [P] [Setup] [TDD] Write tests for CorsSettings struct
- Test: CorsSettings deserializes from YAML correctly ✓
- Test: Default CorsSettings has empty allowed_origins (restrictive fail-safe) ✓
- Test: CorsSettings with wildcard origin deserializes correctly ✓
- Test: Settings::new() loads cors section from development.yaml ✓
- Test: CorsSettings with partial fields uses defaults ✓
- **File**: backend/src/settings.rs (in tests module)
- **Complexity**: Low | **Uncertainty**: Low
- **Tests Written**: 5 tests (cors_settings_deserialize_from_yaml, cors_settings_default_has_empty_origins, cors_settings_with_wildcard_deserializes, settings_loads_cors_section_from_yaml, cors_settings_deserialize_with_defaults)
- [ ] **T010** [P] [Setup] [TDD] Add CorsSettings struct to settings.rs
- Struct fields: `allowed_origins: Vec<String>`, `allow_credentials: bool`, `max_age_secs: i32`
- Implement Default with restrictive settings: `allowed_origins: vec![]`, `allow_credentials: false`, `max_age_secs: 3600`
- Add `#[derive(Debug, serde::Deserialize, Clone)]` to struct
- Add `#[serde(default)]` attribute to Settings.cors field
- Update Settings struct to include `pub cors: CorsSettings`
- **File**: backend/src/settings.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T011** [Setup] [TDD] Update development.yaml with permissive CORS settings
- Add cors section with `allowed_origins: ["*"]`, `allow_credentials: false`, `max_age_secs: 3600`
- Update `frontend_url` from `http://localhost:3000` to `http://localhost:5173` (Vite default port)
- **Test**: cargo run loads development config without errors
- **File**: backend/settings/development.yaml
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T012** [P] [Setup] [TDD] Create production.yaml with restrictive CORS settings
- Add cors section with `allowed_origins: ["REACTED"]`, `allow_credentials: true`, `max_age_secs: 3600`
- Add `frontend_url: "https://REDACTED"`
- Add production-specific application settings (protocol: https, host: 0.0.0.0)
- **Test**: Settings::new() with APP_ENVIRONMENT=production loads config
- **File**: backend/settings/production.yaml
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T013** [Setup] [TDD] Write tests for build_cors() function
- Test: build_cors() with wildcard origin creates permissive Cors (allows any origin)
- Test: build_cors() with specific origin creates restrictive Cors
- Test: build_cors() with `credentials=true` and wildcard origin returns error (browser constraint violation)
- Test: build_cors() sets correct methods (GET, POST, PUT, PATCH, DELETE, OPTIONS)
- Test: build_cors() sets correct headers (content-type, authorization)
- Test: build_cors() sets max_age from settings
- **File**: backend/src/startup.rs (in tests module)
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T014** [Setup] [TDD] Implement build_cors() free function in startup.rs
- Function signature: `fn build_cors(settings: &CorsSettings) -> Cors`
- Validate: if `allow_credentials=true` AND `allowed_origins` contains "*", panic with clear error message
- Iterate over `allowed_origins` and call `cors.allow_origin()` for each
- Hardcode methods: `vec![Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE, Method::OPTIONS]`
- Hardcode headers: `vec![header::CONTENT_TYPE, header::AUTHORIZATION]`
- Set `allow_credentials` from settings
- Set `max_age` from settings.max_age_secs
- Add structured logging: `tracing::info!(allowed_origins = ?settings.allowed_origins, allow_credentials = settings.allow_credentials, "CORS middleware configured")`
- **File**: backend/src/startup.rs
- **Complexity**: Medium | **Uncertainty**: Low
**Pseudocode**:
```rust
fn build_cors(settings: &CorsSettings) -> Cors {
// Validation
if settings.allow_credentials && settings.allowed_origins.contains(&"*".to_string()) {
panic!("CORS misconfiguration: wildcard origin not allowed with credentials=true");
}
let mut cors = Cors::new();
// Configure origins
for origin in &settings.allowed_origins {
cors = cors.allow_origin(origin.as_str());
}
// Hardcoded methods (API-specific)
cors = cors.allow_methods(vec![
Method::GET, Method::POST, Method::PUT,
Method::PATCH, Method::DELETE, Method::OPTIONS
]);
// Hardcoded headers (minimum for API)
cors = cors.allow_headers(vec![
header::CONTENT_TYPE,
header::AUTHORIZATION,
]);
// Configure from settings
cors = cors
.allow_credentials(settings.allow_credentials)
.max_age(settings.max_age_secs);
tracing::info!(
target: "backend::startup",
allowed_origins = ?settings.allowed_origins,
allow_credentials = settings.allow_credentials,
max_age_secs = settings.max_age_secs,
"CORS middleware configured"
);
cors
}
```
- [ ] **T015** [Setup] [TDD] Replace Cors::new() with build_cors() in middleware chain
- In `From<Application> for RunnableApplication`, replace `.with(Cors::new())` with `.with(build_cors(&value.settings.cors))`
- Add necessary imports: `poem::http::{Method, header}`
- Ensure CORS is applied after rate limiting (order: RateLimit → CORS → Data)
- **Test**: Integration test verifies CORS headers are present
- **File**: backend/src/startup.rs (line ~86)
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T016** [P] [Setup] [TDD] Write integration tests for CORS headers
- Test: OPTIONS preflight request to `/api/health` returns correct CORS headers
- Test: GET `/api/health` with Origin header returns `Access-Control-Allow-Origin` header
- Test: Preflight response includes `Access-Control-Max-Age` matching configuration
- Test: Response includes `Access-Control-Allow-Credentials` when configured
- Test: Response includes correct `Access-Control-Allow-Methods` (GET, POST, PUT, PATCH, DELETE, OPTIONS)
- **File**: backend/tests/integration/cors_test.rs (new file)
- **Complexity**: Medium | **Uncertainty**: Low
**Checkpoint**: CORS configuration complete, production-ready security with environment-specific settings
---
## Phase 2: Domain Layer - Type-Driven Development (1 day)
**Purpose**: Build domain types with 100% test coverage, bottom-to-top
**⚠️ TDD CRITICAL**: Write failing tests FIRST for every type, then implement
- [ ] **T009** [US1] [TDD] Write tests for RelayId newtype
- [ ] **T017** [US1] [TDD] Write tests for RelayId newtype
- Test: RelayId::new(1) → Ok(RelayId(1))
- Test: RelayId::new(8) → Ok(RelayId(8))
- Test: RelayId::new(0) → Err(InvalidRelayId)
@@ -79,27 +206,27 @@
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T010** [US1] [TDD] Implement RelayId newtype with validation
- [ ] **T018** [US1] [TDD] Implement RelayId newtype with validation
- #[repr(transparent)] newtype wrapping u8
- Constructor validates 1..=8 range
- Implement Display, Debug, Clone, Copy, PartialEq, Eq
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T011** [P] [US1] [TDD] Write tests for RelayState enum
- [ ] **T019** [P] [US1] [TDD] Write tests for RelayState enum
- Test: RelayState::On → serializes to "on"
- Test: RelayState::Off → serializes to "off"
- Test: Parse "on"/"off" from strings
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T012** [P] [US1] [TDD] Implement RelayState enum
- [ ] **T020** [P] [US1] [TDD] Implement RelayState enum
- Enum: On, Off
- Implement Display, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize/Deserialize
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T013** [US1] [TDD] Write tests for Relay aggregate
- [ ] **T021** [US1] [TDD] Write tests for Relay aggregate
- Test: Relay::new(RelayId(1), RelayState::Off, None) creates relay
- Test: relay.toggle() flips state
- Test: relay.turn_on() sets state to On
@@ -107,13 +234,13 @@
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T014** [US1] [TDD] Implement Relay aggregate
- [ ] **T022** [US1] [TDD] Implement Relay aggregate
- Struct: Relay { id: RelayId, state: RelayState, label: Option<RelayLabel> }
- Methods: new(), toggle(), turn_on(), turn_off(), state(), label()
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T015** [P] [US4] [TDD] Write tests for RelayLabel newtype
- [ ] **T023** [P] [US4] [TDD] Write tests for RelayLabel newtype
- Test: RelayLabel::new("Pump") → Ok
- Test: RelayLabel::new("A".repeat(50)) → Ok
- Test: RelayLabel::new("") → Err(EmptyLabel)
@@ -121,26 +248,26 @@
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T016** [P] [US4] [TDD] Implement RelayLabel newtype
- [ ] **T024** [P] [US4] [TDD] Implement RelayLabel newtype
- #[repr(transparent)] newtype wrapping String
- Constructor validates 1..=50 length
- Implement Display, Debug, Clone, PartialEq, Eq
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T017** [US1] [TDD] Write tests for ModbusAddress type
- [ ] **T025** [US1] [TDD] Write tests for ModbusAddress type
- Test: ModbusAddress::from(RelayId(1)) → ModbusAddress(0)
- Test: ModbusAddress::from(RelayId(8)) → ModbusAddress(7)
- **File**: src/domain/modbus.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T018** [US1] [TDD] Implement ModbusAddress type with From<RelayId>
- [ ] **T026** [US1] [TDD] Implement ModbusAddress type with From<RelayId>
- #[repr(transparent)] newtype wrapping u16
- Implement From<RelayId> with offset: user 1-8 → Modbus 0-7
- **File**: src/domain/modbus.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T019** [US3] [TDD] Write tests and implement HealthStatus enum
- [ ] **T027** [US3] [TDD] Write tests and implement HealthStatus enum
- Enum: Healthy, Degraded { consecutive_errors: u32 }, Unhealthy { reason: String }
- Test transitions between states
- **File**: src/domain/health.rs
@@ -154,20 +281,20 @@
**Purpose**: Implement Modbus client, mocks, and persistence
- [ ] **T020** [P] [US1] [TDD] Write tests for MockRelayController
- [ ] **T028** [P] [US1] [TDD] Write tests for MockRelayController
- Test: read_state() returns mocked state
- Test: write_state() updates mocked state
- Test: read_all() returns 8 relays in known state
- **File**: src/infrastructure/modbus/mock_controller.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T021** [P] [US1] [TDD] Implement MockRelayController
- [ ] **T029** [P] [US1] [TDD] Implement MockRelayController
- Struct with Arc<Mutex<HashMap<RelayId, RelayState>>>
- Implement RelayController trait with in-memory state
- **File**: src/infrastructure/modbus/mock_controller.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T022** [US1] [TDD] Define RelayController trait
- [ ] **T030** [US1] [TDD] Define RelayController trait
- async fn read_state(&self, id: RelayId) → Result<RelayState, ControllerError>
- async fn write_state(&self, id: RelayId, state: RelayState) → Result<(), ControllerError>
- async fn read_all(&self) → Result<Vec<(RelayId, RelayState)>, ControllerError>
@@ -175,14 +302,14 @@
- **File**: src/infrastructure/modbus/controller.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T023** [P] [US1] [TDD] Define ControllerError enum
- [ ] **T031** [P] [US1] [TDD] Define ControllerError enum
- Variants: ConnectionError(String), Timeout(u64), ModbusException(String), InvalidRelayId(u8)
- Implement std::error::Error, Display, Debug
- Use thiserror derive macros
- **File**: src/infrastructure/modbus/error.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T024** [US1] [TDD] Write tests for ModbusRelayController
- [ ] **T032** [US1] [TDD] Write tests for ModbusRelayController
- **REQUIRES HARDWARE/MOCK**: Integration test with tokio_modbus::test utilities
- Test: Connection succeeds with valid config (Modbus TCP on port 502)
- Test: read_state() returns correct coil value
@@ -390,43 +517,43 @@
---
- [ ] **T026** [US1] [TDD] Integration test with real hardware (optional)
- [ ] **T034** [US1] [TDD] Integration test with real hardware (optional)
- **REQUIRES PHYSICAL DEVICE**: Test against actual Modbus relay at configured IP
- Skip if device unavailable, rely on MockRelayController for CI
- **File**: tests/integration/modbus_hardware_test.rs
- **Complexity**: Medium | **Uncertainty**: High
- **Note**: Use #[ignore] attribute, run with cargo test -- --ignored
- [ ] **T027** [P] [US4] [TDD] Write tests for RelayLabelRepository trait
- [ ] **T035** [P] [US4] [TDD] Write tests for RelayLabelRepository trait
- Test: get_label(RelayId(1)) → Option<RelayLabel>
- Test: set_label(RelayId(1), label) → Result<(), RepositoryError>
- Test: delete_label(RelayId(1)) → Result<(), RepositoryError>
- **File**: src/infrastructure/persistence/label_repository.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T028** [P] [US4] [TDD] Implement SQLite RelayLabelRepository
- [ ] **T036** [P] [US4] [TDD] Implement SQLite RelayLabelRepository
- Implement get_label(), set_label(), delete_label() using SQLx
- Use sqlx::query! macros for compile-time SQL verification
- **File**: src/infrastructure/persistence/sqlite_label_repository.rs
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T029** [US4] [TDD] Write tests for in-memory mock LabelRepository
- [ ] **T037** [US4] [TDD] Write tests for in-memory mock LabelRepository
- For testing without SQLite dependency
- **File**: src/infrastructure/persistence/mock_label_repository.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T030** [US4] [TDD] Implement in-memory mock LabelRepository
- [ ] **T038** [US4] [TDD] Implement in-memory mock LabelRepository
- HashMap-based implementation
- **File**: src/infrastructure/persistence/mock_label_repository.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T031** [US3] [TDD] Write tests for HealthMonitor service
- [ ] **T039** [US3] [TDD] Write tests for HealthMonitor service
- Test: track_success() transitions Degraded → Healthy
- Test: track_failure() transitions Healthy → Degraded → Unhealthy
- **File**: src/application/health_monitor.rs
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T032** [US3] [TDD] Implement HealthMonitor service
- [ ] **T040** [US3] [TDD] Implement HealthMonitor service
- Track consecutive errors, transition states per FR-020, FR-021
- **File**: src/application/health_monitor.rs
- **Complexity**: Medium | **Uncertainty**: Low
@@ -443,36 +570,36 @@
### Application Layer
- [ ] **T033** [US1] [TDD] Write tests for ToggleRelayUseCase
- [ ] **T041** [US1] [TDD] Write tests for ToggleRelayUseCase
- Test: execute(RelayId(1)) toggles relay state via controller
- Test: execute() returns error if controller fails
- **File**: src/application/use_cases/toggle_relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T034** [US1] [TDD] Implement ToggleRelayUseCase
- [ ] **T042** [US1] [TDD] Implement ToggleRelayUseCase
- Orchestrate: read current state → toggle → write new state
- **File**: src/application/use_cases/toggle_relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T035** [P] [US1] [TDD] Write tests for GetAllRelaysUseCase
- [ ] **T043** [P] [US1] [TDD] Write tests for GetAllRelaysUseCase
- Test: execute() returns all 8 relays with states
- **File**: src/application/use_cases/get_all_relays.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T036** [P] [US1] [TDD] Implement GetAllRelaysUseCase
- [ ] **T044** [P] [US1] [TDD] Implement GetAllRelaysUseCase
- Call controller.read_all(), map to domain Relay objects
- **File**: src/application/use_cases/get_all_relays.rs
- **Complexity**: Low | **Uncertainty**: Low
### Presentation Layer (Backend API)
- [ ] **T037** [US1] [TDD] Define RelayDto in presentation layer
- [ ] **T045** [US1] [TDD] Define RelayDto in presentation layer
- Fields: id (u8), state ("on"/"off"), label (Option<String>)
- Implement From<Relay> for RelayDto
- **File**: src/presentation/dto/relay_dto.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T038** [US1] [TDD] Define API error responses
- [ ] **T046** [US1] [TDD] Define API error responses
- ApiError enum with status codes and messages
- Implement poem::error::ResponseError
- **File**: src/presentation/error.rs
@@ -627,26 +754,26 @@
---
- [ ] **T040** [US1] [TDD] Write contract tests for GET /api/relays
- [ ] **T048** [US1] [TDD] Write contract tests for GET /api/relays
- Test: Returns 200 with array of 8 RelayDto
- Test: Each relay has id 1-8, state, and optional label
- **File**: tests/contract/test_relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T041** [US1] [TDD] Implement GET /api/relays endpoint
- [ ] **T049** [US1] [TDD] Implement GET /api/relays endpoint
- #[oai(path = "/relays", method = "get")]
- Call GetAllRelaysUseCase, map to RelayDto
- **File**: src/presentation/api/relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T042** [US1] [TDD] Write contract tests for POST /api/relays/{id}/toggle
- [ ] **T050** [US1] [TDD] Write contract tests for POST /api/relays/{id}/toggle
- Test: Returns 200 with updated RelayDto
- Test: Returns 404 for id < 1 or id > 8
- Test: State actually changes in controller
- **File**: tests/contract/test_relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T043** [US1] [TDD] Implement POST /api/relays/{id}/toggle endpoint
- [ ] **T051** [US1] [TDD] Implement POST /api/relays/{id}/toggle endpoint
- #[oai(path = "/relays/:id/toggle", method = "post")]
- Parse id, call ToggleRelayUseCase, return updated state
- **File**: src/presentation/api/relay_api.rs
@@ -654,12 +781,12 @@
### Frontend Implementation
- [ ] **T044** [P] [US1] [TDD] Create RelayDto TypeScript interface
- [ ] **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
- [ ] **T045** [P] [US1] [TDD] Create API client service
- [ ] **T053** [P] [US1] [TDD] Create API client service
- getAllRelays(): Promise<RelayDto[]>
- toggleRelay(id: number): Promise<RelayDto>
- **File**: frontend/src/api/relayApi.ts
@@ -816,14 +943,14 @@
---
- [ ] **T047** [US1] [TDD] Create RelayCard component
- [ ] **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
- [ ] **T048** [US1] [TDD] Create RelayGrid component
- [ ] **T056** [US1] [TDD] Create RelayGrid component
- Use useRelayPolling composable
- Render 8 RelayCard components
- Handle toggle events by calling API
@@ -831,7 +958,7 @@
- **File**: frontend/src/components/RelayGrid.vue
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T049** [US1] [TDD] Integration test for US1
- [ ] **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
@@ -847,49 +974,49 @@
**Independent Test**: POST /api/relays/all/on turns all 8 relays on
- [ ] **T050** [US2] [TDD] Write tests for BulkControlUseCase
- [ ] **T058** [US2] [TDD] Write tests for BulkControlUseCase
- Test: execute(BulkOperation::AllOn) turns all relays on
- Test: execute(BulkOperation::AllOff) turns all relays off
- **File**: src/application/use_cases/bulk_control.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T051** [US2] [TDD] Implement BulkControlUseCase
- [ ] **T059** [US2] [TDD] Implement BulkControlUseCase
- Call controller.write_all(state)
- **File**: src/application/use_cases/bulk_control.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T052** [US2] [TDD] Define BulkOperation enum
- [ ] **T060** [US2] [TDD] Define BulkOperation enum
- Variants: AllOn, AllOff
- **File**: src/domain/relay.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T053** [US2] [TDD] Write contract tests for POST /api/relays/all/on
- [ ] **T061** [US2] [TDD] Write contract tests for POST /api/relays/all/on
- Test: Returns 200, all relays turn on
- **File**: tests/contract/test_relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T054** [US2] [TDD] Implement POST /api/relays/all/on endpoint
- [ ] **T062** [US2] [TDD] Implement POST /api/relays/all/on endpoint
- Call BulkControlUseCase with AllOn
- **File**: src/presentation/api/relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T055** [P] [US2] [TDD] Write contract tests for POST /api/relays/all/off
- [ ] **T063** [P] [US2] [TDD] Write contract tests for POST /api/relays/all/off
- Test: Returns 200, all relays turn off
- **File**: tests/contract/test_relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T056** [P] [US2] [TDD] Implement POST /api/relays/all/off endpoint
- [ ] **T064** [P] [US2] [TDD] Implement POST /api/relays/all/off endpoint
- Call BulkControlUseCase with AllOff
- **File**: src/presentation/api/relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T057** [US2] [TDD] Add bulk control buttons to frontend
- [ ] **T065** [US2] [TDD] Add bulk control buttons to frontend
- Add "All On" and "All Off" buttons to RelayGrid component
- Call API endpoints and refresh relay states
- **File**: frontend/src/components/RelayGrid.vue
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T058** [US2] [TDD] Integration test for US2
- [ ] **T066** [US2] [TDD] Integration test for US2
- Click "All On" → verify all 8 relays turn on
- Click "All Off" → verify all 8 relays turn off
- **File**: frontend/tests/e2e/bulk-control.spec.ts
@@ -905,47 +1032,47 @@
**Independent Test**: GET /api/health returns health status
- [ ] **T059** [US3] [TDD] Write tests for GetHealthUseCase
- [ ] **T067** [US3] [TDD] Write tests for GetHealthUseCase
- Test: Returns Healthy when controller is responsive
- Test: Returns Degraded after 3 consecutive errors
- Test: Returns Unhealthy after 10 consecutive errors
- **File**: src/application/use_cases/get_health.rs
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T060** [US3] [TDD] Implement GetHealthUseCase
- [ ] **T068** [US3] [TDD] Implement GetHealthUseCase
- Use HealthMonitor to track controller status
- Return current HealthStatus
- **File**: src/application/use_cases/get_health.rs
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T061** [US3] [TDD] Define HealthDto
- [ ] **T069** [US3] [TDD] Define HealthDto
- Fields: status ("healthy"/"degraded"/"unhealthy"), consecutive_errors (optional), reason (optional)
- **File**: src/presentation/dto/health_dto.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T062** [US3] [TDD] Write contract tests for GET /api/health
- [ ] **T070** [US3] [TDD] Write contract tests for GET /api/health
- Test: Returns 200 with HealthDto
- **File**: tests/contract/test_health_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T063** [US3] [TDD] Implement GET /api/health endpoint
- [ ] **T071** [US3] [TDD] Implement GET /api/health endpoint
- Call GetHealthUseCase, map to HealthDto
- **File**: src/presentation/api/health_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T064** [P] [US3] [TDD] Add firmware version display (optional)
- [ ] **T072** [P] [US3] [TDD] Add firmware version display (optional)
- If controller supports firmware_version(), display in UI
- **File**: frontend/src/components/DeviceInfo.vue
- **Complexity**: Low | **Uncertainty**: Medium
- **Note**: Device may not support this feature
- [ ] **T065** [US3] [TDD] Create HealthIndicator component
- [ ] **T073** [US3] [TDD] Create HealthIndicator component
- Display connection status with color-coded indicator
- Show firmware version if available
- **File**: frontend/src/components/HealthIndicator.vue
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T066** [US3] [TDD] Integrate HealthIndicator in RelayGrid
- [ ] **T074** [US3] [TDD] Integrate HealthIndicator in RelayGrid
- Fetch health status in useRelayPolling composable
- Pass to HealthIndicator component
- **File**: frontend/src/components/RelayGrid.vue
@@ -961,37 +1088,37 @@
**Independent Test**: PUT /api/relays/{id}/label sets label, GET /api/relays returns label
- [ ] **T067** [US4] [TDD] Write tests for SetLabelUseCase
- [ ] **T075** [US4] [TDD] Write tests for SetLabelUseCase
- Test: execute(RelayId(1), "Pump") sets label
- Test: execute with empty label returns error
- Test: execute with 51-char label returns error
- **File**: src/application/use_cases/set_label.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T068** [US4] [TDD] Implement SetLabelUseCase
- [ ] **T076** [US4] [TDD] Implement SetLabelUseCase
- Validate label with RelayLabel::new()
- Call label_repository.set_label()
- **File**: src/application/use_cases/set_label.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T069** [US4] [TDD] Write contract tests for PUT /api/relays/{id}/label
- [ ] **T077** [US4] [TDD] Write contract tests for PUT /api/relays/{id}/label
- Test: Returns 200, label is persisted
- Test: Returns 400 for invalid label
- **File**: tests/contract/test_relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T070** [US4] [TDD] Implement PUT /api/relays/{id}/label endpoint
- [ ] **T078** [US4] [TDD] Implement PUT /api/relays/{id}/label endpoint
- Parse id and label, call SetLabelUseCase
- **File**: src/presentation/api/relay_api.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T071** [US4] [TDD] Add label editing to RelayCard component
- [ ] **T079** [US4] [TDD] Add label editing to RelayCard component
- Click label → show input field
- Submit → call PUT /api/relays/{id}/label
- **File**: frontend/src/components/RelayCard.vue
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T072** [US4] [TDD] Integration test for US4
- [ ] **T080** [US4] [TDD] Integration test for US4
- Set label for relay 1 → refresh → verify label persists
- **File**: frontend/tests/e2e/relay-labeling.spec.ts
- **Complexity**: Low | **Uncertainty**: Low
@@ -1004,56 +1131,56 @@
**Purpose**: Testing, documentation, and production readiness
- [ ] **T073** [P] Add comprehensive logging at all architectural boundaries
- [ ] **T081** [P] Add comprehensive logging at all architectural boundaries
- Log all API requests/responses
- Log all Modbus operations
- Log health status transitions
- **Files**: All API and infrastructure modules
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T074** [P] Add OpenAPI documentation for all endpoints
- [ ] **T082** [P] Add OpenAPI documentation for all endpoints
- Document request/response schemas
- Add example values
- Tag endpoints appropriately
- **File**: src/presentation/api/*.rs
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T075** [P] Run cargo clippy and fix all warnings
- [ ] **T083** [P] Run cargo clippy and fix all warnings
- Ensure compliance with strict linting
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T076** [P] Run cargo fmt and format all code
- [ ] **T084** [P] Run cargo fmt and format all code
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T077** Generate test coverage report
- [ ] **T085** Generate test coverage report
- Run: just coverage
- Ensure > 80% coverage for domain and application layers
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T078** [P] Run cargo audit for dependency vulnerabilities
- [ ] **T086** [P] Run cargo audit for dependency vulnerabilities
- Fix any high/critical vulnerabilities
- **Complexity**: Low | **Uncertainty**: Medium
- [ ] **T079** [P] Update README.md with deployment instructions
- [ ] **T087** [P] Update README.md with deployment instructions
- Document environment variables
- Document Modbus device configuration
- Add quickstart guide
- **File**: README.md
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T080** [P] Create Docker image for backend
- [ ] **T088** [P] Create Docker image for backend
- Multi-stage build with Rust
- Include SQLite database setup
- **File**: Dockerfile
- **Complexity**: Medium | **Uncertainty**: Low
- [ ] **T081** [P] Create production settings/production.yaml
- [ ] **T089** [P] Create production settings/production.yaml
- Configure for actual device IP
- Set appropriate timeouts and retry settings
- **File**: settings/production.yaml
- **Complexity**: Low | **Uncertainty**: Low
- [ ] **T082** Deploy to production environment
- [ ] **T090** Deploy to production environment
- Test with actual Modbus relay device
- Verify all user stories work end-to-end
- **Complexity**: Medium | **Uncertainty**: High