docs: update documentation of STA
This commit is contained in:
@@ -18,107 +18,20 @@
|
|||||||
|
|
||||||
Web-based Modbus relay control system for managing 8-channel relay modules over TCP.
|
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
|
## 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.
|
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
|
## Current Status
|
||||||
|
|
||||||
### Phase 1 Complete - Foundation
|
**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.
|
||||||
- ✅ 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
|
|
||||||
|
|
||||||
### 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<CorsSettings> for Cors trait unit tests (6 tests)
|
|
||||||
- ✅ T014: From<CorsSettings> 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<RelayId> 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<T, E>` 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<Mutex<Context>>` 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<Mutex<HashMap<RelayId, RelayState>>>` 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
|
## Architecture
|
||||||
|
|
||||||
**Current:**
|
**Current:**
|
||||||
- **Backend**: Rust 2024 with Poem web framework (hexagonal architecture)
|
- **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
|
- **API**: RESTful HTTP with OpenAPI documentation
|
||||||
- **CORS**: Production-ready configurable middleware with security validation
|
- **CORS**: Production-ready configurable middleware with security validation
|
||||||
- **Middleware Chain**: Rate Limiting -> CORS -> Data injection
|
- **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
|
- **Persistence**: SQLite for relay labels with compile-time SQL verification
|
||||||
|
|
||||||
**Planned:**
|
**Planned:**
|
||||||
- **Frontend**: Vue 3 with TypeScript
|
|
||||||
- **Deployment**: Backend on Raspberry Pi, frontend on Cloudflare Pages
|
- **Deployment**: Backend on Raspberry Pi, frontend on Cloudflare Pages
|
||||||
- **Access**: Traefik reverse proxy with Authelia authentication
|
- **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
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run development server
|
# Run backend development server
|
||||||
just run
|
just backend run
|
||||||
|
|
||||||
# Run tests
|
# Run frontend
|
||||||
just test
|
just frontend run
|
||||||
|
|
||||||
# Run linter
|
# Run backend tests
|
||||||
just lint
|
just backend test
|
||||||
|
|
||||||
# Format code
|
# Run backend linter
|
||||||
just format
|
just backend lint
|
||||||
|
|
||||||
|
# Format backend code
|
||||||
|
just backend format
|
||||||
|
|
||||||
# Watch mode with bacon
|
# Watch mode with bacon
|
||||||
bacon # clippy-all (default)
|
bacon # clippy-all (default)
|
||||||
@@ -163,9 +78,9 @@ Edit `backend/settings/base.yaml` for Modbus device settings:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
modbus:
|
modbus:
|
||||||
host: "192.168.0.200"
|
host: "192.168.1.200"
|
||||||
port: 502
|
port: 502
|
||||||
slave_id: 0
|
slave_id: 1
|
||||||
timeout_secs: 5
|
timeout_secs: 5
|
||||||
|
|
||||||
relay:
|
relay:
|
||||||
@@ -184,8 +99,7 @@ APP__MODBUS__HOST=192.168.1.100 cargo run
|
|||||||
```yaml
|
```yaml
|
||||||
# backend/settings/development.yaml
|
# backend/settings/development.yaml
|
||||||
cors:
|
cors:
|
||||||
allowed_origins:
|
allowed_origins: ["*"] # Permissive for local development
|
||||||
- "*" # Permissive for local development
|
|
||||||
allow_credentials: false # MUST be false with wildcard
|
allow_credentials: false # MUST be false with wildcard
|
||||||
max_age_secs: 3600
|
max_age_secs: 3600
|
||||||
```
|
```
|
||||||
@@ -225,12 +139,12 @@ The server provides OpenAPI documentation via Swagger UI:
|
|||||||
- OpenAPI Spec: `http://localhost:3100/specs`
|
- OpenAPI Spec: `http://localhost:3100/specs`
|
||||||
|
|
||||||
**Current Endpoints:**
|
**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/health` - Health check endpoint
|
||||||
- `GET /api/meta` - Application metadata
|
- `GET /api/meta` - Application metadata
|
||||||
|
|
||||||
**Planned Endpoints (see spec):**
|
**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/on` - Turn all relays on
|
||||||
- `POST /api/relays/all/off` - Turn all relays off
|
- `POST /api/relays/all/off` - Turn all relays off
|
||||||
- `PUT /api/relays/{id}/label` - Set relay label
|
- `PUT /api/relays/{id}/label` - Set relay label
|
||||||
@@ -239,74 +153,133 @@ The server provides OpenAPI documentation via Swagger UI:
|
|||||||
|
|
||||||
**Monorepo Layout:**
|
**Monorepo Layout:**
|
||||||
```
|
```
|
||||||
sta/ # Repository root
|
sta/
|
||||||
├── backend/ # Rust backend workspace member
|
├── backend/ # Rust backend
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── lib.rs - Library entry point
|
│ │ ├── main.rs - Binary entry point
|
||||||
│ │ ├── main.rs - Binary entry point
|
│ │ ├── lib.rs - Library entry point
|
||||||
│ │ ├── startup.rs - Application builder and server config
|
│ │ ├── startup.rs - Application builder and server wiring
|
||||||
│ │ ├── telemetry.rs - Logging and tracing setup
|
│ │ ├── telemetry.rs - Logging and tracing setup
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── domain/ - Business logic layer (Phase 2)
|
│ │ ├── domain/ - Business logic
|
||||||
│ │ │ ├── relay/ - Relay domain aggregate
|
│ │ │ ├── health.rs - HealthStatus state machine
|
||||||
│ │ │ │ ├── types/ - RelayId, RelayState, RelayLabel newtypes
|
│ │ │ ├── modbus.rs - ModbusAddress type
|
||||||
│ │ │ │ ├── entity.rs - Relay aggregate with state control
|
│ │ │ └── relay/
|
||||||
│ │ │ │ ├── controller.rs - RelayController trait & ControllerError
|
│ │ │ ├── entity.rs - Relay aggregate (state control)
|
||||||
│ │ │ │ └── repository/ - RelayLabelRepository trait
|
│ │ │ ├── controller.rs - RelayController trait
|
||||||
│ │ │ ├── modbus.rs - ModbusAddress type with conversion
|
│ │ │ ├── types/
|
||||||
│ │ │ └── health.rs - HealthStatus state machine
|
│ │ │ │ ├── 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)
|
│ │ ├── application/ - Use cases
|
||||||
│ │ │ └── health/ - Health monitoring service
|
│ │ │ ├── health/
|
||||||
│ │ │ └── health_monitor.rs - HealthMonitor with state tracking
|
│ │ │ │ └── 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)
|
│ │ ├── infrastructure/ - External integrations
|
||||||
│ │ │ ├── modbus/ - Modbus TCP communication
|
│ │ │ ├── modbus/
|
||||||
│ │ │ │ ├── client.rs - ModbusRelayController (real hardware)
|
│ │ │ │ ├── client.rs - ModbusRelayController
|
||||||
│ │ │ │ ├── client_test.rs - Hardware integration tests
|
│ │ │ │ ├── client_test.rs - Unit tests
|
||||||
│ │ │ │ └── mock_controller.rs - MockRelayController for testing
|
│ │ │ │ ├── factory.rs - Controller factory (retry, fallback)
|
||||||
│ │ │ └── persistence/ - Database layer
|
│ │ │ │ └── mock_controller.rs - MockRelayController
|
||||||
│ │ │ ├── entities/ - Database record types
|
│ │ │ └── persistence/
|
||||||
│ │ │ ├── sqlite_repository.rs - SqliteRelayLabelRepository
|
│ │ │ ├── factory.rs - Repository factory
|
||||||
│ │ │ └── label_repository.rs - MockRelayLabelRepository
|
│ │ │ ├── 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)
|
│ │ ├── presentation/ - API handlers and DTOs
|
||||||
│ │ ├── settings/ - Configuration module
|
│ │ │ ├── error.rs - API error types
|
||||||
│ │ │ ├── mod.rs - Settings aggregation
|
│ │ │ ├── api/
|
||||||
│ │ │ └── cors.rs - CORS configuration
|
│ │ │ │ └── relay_api.rs - Relay HTTP handlers
|
||||||
│ │ ├── route/ - HTTP endpoint handlers
|
│ │ │ └── dto/
|
||||||
│ │ │ ├── health.rs - Health check endpoints
|
│ │ │ └── relay_dto.rs - Relay DTOs
|
||||||
│ │ │ └── meta.rs - Application metadata
|
│ │ │
|
||||||
│ │ └── middleware/ - Custom middleware
|
│ │ ├── route/ - Route definitions
|
||||||
│ │ └── rate_limit.rs
|
│ │ │ ├── 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
|
│ ├── settings/ - YAML config files
|
||||||
│ │ ├── base.yaml - Base configuration
|
│ │ ├── base.yaml
|
||||||
│ │ ├── development.yaml - Development overrides
|
│ │ ├── development.yaml
|
||||||
│ │ └── production.yaml - Production overrides
|
│ │ └── production.yaml
|
||||||
│ └── tests/ - Integration tests
|
│ │
|
||||||
│ └── cors_test.rs - CORS integration tests
|
│ └── 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 (Vue 3 + TypeScript)
|
||||||
├── src/ # Frontend source (Vue/TypeScript)
|
│ ├── main.ts - App entry point
|
||||||
│ └── api/ - Type-safe API client
|
│ ├── App.vue - Root component
|
||||||
├── docs/ # Project documentation
|
│ ├── style.css / style.less - Global styles
|
||||||
│ ├── cors-configuration.md - CORS setup guide
|
│ ├── api/
|
||||||
│ ├── domain-layer.md - Domain layer architecture
|
│ │ ├── client.ts - HTTP client
|
||||||
│ └── Modbus_POE_ETH_Relay.md - Hardware documentation
|
│ │ └── schema.ts - API types
|
||||||
├── specs/ # Feature specifications
|
│ ├── components/
|
||||||
│ ├── constitution.md - Architectural principles
|
│ │ ├── 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/
|
│ └── 001-modbus-relay-control/
|
||||||
│ ├── spec.md - Feature specification
|
│ ├── spec.md, plan.md, tasks.org
|
||||||
│ ├── plan.md - Implementation plan
|
│ ├── data-model.md, types-design.md
|
||||||
│ ├── tasks.org - Task breakdown (org-mode format)
|
│ ├── domain-layer-architecture.md
|
||||||
│ ├── data-model.md - Data model specification
|
│ ├── lessons-learned.md
|
||||||
│ ├── types-design.md - Domain types design
|
│ └── ...
|
||||||
│ ├── domain-layer-architecture.md - Domain layer docs
|
│
|
||||||
│ └── lessons-learned.md - Phase 2/3 insights
|
├── nix/ - Nix flake configs
|
||||||
├── package.json - Frontend dependencies
|
├── public/ - Static assets
|
||||||
├── vite.config.ts - Vite build configuration
|
├── justfile - Build commands
|
||||||
└── justfile - Build commands
|
├── package.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── Cargo.toml
|
||||||
|
└── flake.nix
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
@@ -324,9 +297,10 @@ sta/ # Repository root
|
|||||||
- thiserror (error handling)
|
- thiserror (error handling)
|
||||||
- serde + serde_yaml (configuration deserialization)
|
- serde + serde_yaml (configuration deserialization)
|
||||||
|
|
||||||
**Frontend** (scaffolding complete):
|
**Frontend** (US1 complete):
|
||||||
- Vue 3 + TypeScript
|
- Vue 3 + TypeScript with composables (useRelayPolling)
|
||||||
- Vite build tool
|
- Vite build tool
|
||||||
|
- RelayCard and RelayGrid components with real-time polling
|
||||||
- openapi-typescript (type-safe API client generation)
|
- openapi-typescript (type-safe API client generation)
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|||||||
+46
-55
@@ -1,8 +1,8 @@
|
|||||||
# CORS Configuration Guide
|
# CORS Configuration Guide
|
||||||
|
|
||||||
**Last Updated**: 2026-01-03
|
**Last Updated**: 2026-01-23
|
||||||
**Related Tasks**: T009 (Tests), T010 (Implementation)
|
**Related Tasks**: T009-T016
|
||||||
**Status**: Implemented (Phase 0.5)
|
**Status**: Complete (Phase 0.5)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Relay Device (local network)
|
|||||||
|
|
||||||
### CorsSettings Struct
|
### CorsSettings Struct
|
||||||
|
|
||||||
Located in `backend/src/settings.rs` (lines 217-232):
|
Located in `backend/src/settings/cors.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Debug, serde::Deserialize, Clone)]
|
#[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
|
- `allow_credentials`: Whether to allow cookies/auth headers
|
||||||
- `max_age_secs`: How long browsers cache preflight responses
|
- `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)
|
- **Methods**: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` (API-specific)
|
||||||
- **Headers**: `content-type`, `authorization` (minimum for API)
|
- **Headers**: `content-type`, `authorization` (minimum for API)
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ frontend_url: http://localhost:5173 # Vite default port
|
|||||||
|
|
||||||
### Production Environment
|
### Production Environment
|
||||||
|
|
||||||
**File**: `backend/settings/production.yaml` (to be created in T012)
|
**File**: `backend/settings/production.yaml`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cors:
|
cors:
|
||||||
@@ -129,23 +129,7 @@ frontend_url: "https://sta.example.com"
|
|||||||
|
|
||||||
### Integration with Settings System
|
### Integration with Settings System
|
||||||
|
|
||||||
The `CorsSettings` struct is integrated into the main `Settings` struct (line 30):
|
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.
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
### Loading and Precedence
|
### 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.
|
**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<CorsSettings> for Cors` implementation panics during startup if this constraint is violated:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
if settings.allow_credentials && settings.allowed_origins.contains(&"*".to_string()) {
|
if settings.allow_credentials && settings.allowed_origins.contains(&"*".to_string()) {
|
||||||
@@ -427,11 +411,9 @@ cors:
|
|||||||
|
|
||||||
### Preflight Requests Failing (OPTIONS)
|
### 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.
|
**Solution**: The `From<CorsSettings> for Cors` trait implementation hardcodes OPTIONS in the allowed methods:
|
||||||
|
|
||||||
**Permanent Solution**: The upcoming `build_cors()` function will hardcode:
|
|
||||||
```rust
|
```rust
|
||||||
cors.allow_methods(vec![
|
cors.allow_methods(vec![
|
||||||
Method::GET, Method::POST, Method::PUT,
|
Method::GET, Method::POST, Method::PUT,
|
||||||
@@ -454,13 +436,13 @@ cors.allow_methods(vec![
|
|||||||
|
|
||||||
### Headers Not Allowed
|
### 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)
|
- `content-type` (for JSON request bodies)
|
||||||
- `authorization` (for Authelia authentication tokens)
|
- `authorization` (for Authelia authentication tokens)
|
||||||
|
|
||||||
**Adding Custom Headers**: Requires modifying `build_cors()` function (T014).
|
**Adding Custom Headers**: Requires modifying the `From<CorsSettings> for Cors` trait implementation.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -481,37 +463,43 @@ serde_yaml = "0.9.34"
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `backend/src/settings.rs` | `CorsSettings` struct definition |
|
| `backend/src/settings/cors.rs` | `CorsSettings` struct definition |
|
||||||
| `backend/settings/base.yaml` | Baseline configuration (no CORS section yet) |
|
| `backend/settings/base.yaml` | Baseline configuration |
|
||||||
| `backend/settings/development.yaml` | Development CORS (permissive) |
|
| `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
|
All CORS configuration tasks (T009-T016) have been implemented and tested:
|
||||||
- Add `cors:` section with permissive settings
|
|
||||||
- Update `frontend_url` to `http://localhost:5173` (Vite default)
|
|
||||||
|
|
||||||
### T012: Create production.yaml
|
### T009-T010: CorsSettings Struct (Phase 0.5)
|
||||||
- Add `cors:` section with restrictive settings
|
- 5 unit tests written (TDD approach) and the `CorsSettings` struct implemented with fail-safe defaults
|
||||||
- Use `https://sta.example.com` as allowed origin
|
- Located in `backend/src/settings/cors.rs`
|
||||||
- Set `allow_credentials: true` for Authelia
|
|
||||||
|
|
||||||
### T013-T014: Implement build_cors() Function
|
### T011: Development YAML Configuration
|
||||||
- Create `build_cors(settings: &CorsSettings) -> Cors` in `startup.rs`
|
- Added `cors:` section with wildcard origin and `allow_credentials: false`
|
||||||
- Validate wildcard + credentials constraint
|
- Updated `frontend_url` to `http://localhost:5173` (Vite default)
|
||||||
- Hardcode methods (GET, POST, PUT, PATCH, DELETE, OPTIONS)
|
- File: `backend/settings/development.yaml`
|
||||||
- Hardcode headers (content-type, authorization)
|
|
||||||
- Add structured logging
|
|
||||||
|
|
||||||
### T015: Replace Cors::new() in Middleware Chain
|
### T012: Production YAML Configuration
|
||||||
- Update `startup.rs` line ~86
|
- Added `cors:` section with specific origin and `allow_credentials: true`
|
||||||
- Call `build_cors(&value.settings.cors)`
|
- File: `backend/settings/production.yaml`
|
||||||
|
|
||||||
|
### T013-T014: Cors Middleware Implementation
|
||||||
|
- 6 unit tests written for the `From<CorsSettings> 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
|
### T016: Integration Tests
|
||||||
- Write tests verifying CORS headers in HTTP responses
|
- 9 comprehensive integration tests in `backend/tests/cors_test.rs`
|
||||||
- Test OPTIONS preflight requests
|
- Covers: preflight requests, actual request headers, max-age, credentials, methods, wildcard, multiple origins, unauthorized origin rejection
|
||||||
- Verify `Access-Control-Allow-Origin` header
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
@@ -533,7 +521,10 @@ serde_yaml = "0.9.34"
|
|||||||
| 2026-01-03 | T009 | Test suite written (5 tests, TDD approach) |
|
| 2026-01-03 | T009 | Test suite written (5 tests, TDD approach) |
|
||||||
| 2026-01-03 | T010 | `CorsSettings` struct implemented with defaults |
|
| 2026-01-03 | T010 | `CorsSettings` struct implemented with defaults |
|
||||||
| 2026-01-03 | Documentation | This guide created |
|
| 2026-01-03 | Documentation | This guide created |
|
||||||
|
| 2026-01-22 | T013-T014 | `From<CorsSettings> 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.
|
||||||
|
|||||||
+17
-15
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
**Feature**: 001-modbus-relay-control
|
**Feature**: 001-modbus-relay-control
|
||||||
**Phase**: 2 (Domain Layer - Type-Driven Development)
|
**Phase**: 2 (Domain Layer - Type-Driven Development)
|
||||||
**Status**: Complete
|
**Status**: Complete (US1 MVP also complete)
|
||||||
**Last Updated**: 2026-01-04
|
**Last Updated**: 2026-05-15
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -419,13 +419,15 @@ backend/src/domain/
|
|||||||
├── modbus.rs # ModbusAddress type
|
├── modbus.rs # ModbusAddress type
|
||||||
└── relay/
|
└── relay/
|
||||||
├── mod.rs # Relay module exports
|
├── mod.rs # Relay module exports
|
||||||
├── controler.rs # RelayController trait (trait definition)
|
├── controller.rs # RelayController trait (trait definition)
|
||||||
├── entity.rs # Relay aggregate
|
├── entity.rs # Relay aggregate
|
||||||
└── types/
|
├── types/
|
||||||
├── mod.rs # Type exports
|
│ ├── mod.rs # Type exports
|
||||||
├── relayid.rs # RelayId newtype
|
│ ├── relayid.rs # RelayId newtype
|
||||||
├── relaystate.rs # RelayState enum
|
│ ├── relaystate.rs # RelayState enum
|
||||||
└── relaylabel.rs # RelayLabel newtype
|
│ └── relaylabel.rs # RelayLabel newtype
|
||||||
|
└── repository/
|
||||||
|
└── label.rs # RelayLabelRepository trait
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependency Graph
|
## Dependency Graph
|
||||||
@@ -558,16 +560,16 @@ Coverage: 100% for domain layer
|
|||||||
|
|
||||||
## Next Steps
|
## 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
|
1. **Infrastructure** (Phase 3): `ModbusRelayController` (real Modbus TCP client) + `MockRelayController` (testing), `SqliteRelayLabelRepository` for persistence, with factory functions for dependency injection
|
||||||
2. Create `MockRelayController` for testing
|
2. **Application** (Phase 3): `ToggleRelayUseCase`, `GetAllRelaysUseCase`, `HealthMonitor` service
|
||||||
3. Implement `RelayLabelRepository` with SQLite
|
3. **Presentation** (Phase 4): `RelayApi` handlers with `RelayDto`, REST endpoints (`GET /api/relays`, `POST /api/relays/{id}/toggle`)
|
||||||
4. Use domain types throughout infrastructure code
|
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
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
* TODO Development phases [4/9]
|
* TODO Development phases [5/9]
|
||||||
** DONE Phase 1: Setup & Foundation (0.5 days) [8/8]
|
** DONE Phase 1: Setup & Foundation (0.5 days) [8/8]
|
||||||
*Purpose*: Initialize project dependencies and directory structure
|
*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]
|
- State "STARTED" from "TODO" [2026-01-23 ven. 20:20]
|
||||||
*Goal*: View current state of all 8 relays + toggle individual relay on/off
|
*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=
|
- *File*: =src/presentation/api/relay_api.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *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
|
- [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
||||||
- Generate from OpenAPI spec or manually define
|
- Generate from OpenAPI spec or manually define
|
||||||
- *File*: =frontend/src/types/relay.ts=
|
- *File*: =frontend/src/types/relay.ts=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
- [ ] *T053* [P] [US1] [TDD] Create API client service
|
- [X] *T053* [P] [US1] [TDD] Create API client service
|
||||||
- getAllRelays(): =Promise<RelayDto[]>=
|
- getAllRelays(): =Promise<RelayDto[]>=
|
||||||
- =toggleRelay(id: number): Promise=
|
- =toggleRelay(id: number): Promise=
|
||||||
- *File*: =frontend/src/api/relayApi.ts=
|
- *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
|
*Complexity*: High → Broken into 4 sub-tasks
|
||||||
*Uncertainty*: Medium
|
*Uncertainty*: Medium
|
||||||
*Rationale*: Vue 3 lifecycle hooks, polling management, memory leak prevention
|
*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=
|
- Setup reactive refs: =relays=, =isLoading=, =error=, =lastFetchTime=
|
||||||
- Define interval variable and fetch function signature
|
- Define interval variable and fetch function signature
|
||||||
@@ -948,10 +954,10 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
|||||||
|
|
||||||
*TDD Checklist*:
|
*TDD Checklist*:
|
||||||
|
|
||||||
- [ ] Test: Composable returns correct reactive refs
|
- [X] Test: Composable returns correct reactive refs
|
||||||
- [ ] Test: Initial state is ~loading=true~, ~relays=[]~, ~error=null~
|
- [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=
|
- Fetch relays and health status in parallel using =Promise.all=
|
||||||
- Update reactive state on success
|
- Update reactive state on success
|
||||||
@@ -985,12 +991,12 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
|||||||
|
|
||||||
*TDD Checklist*:
|
*TDD Checklist*:
|
||||||
|
|
||||||
- [ ] Test: =fetchData()= updates relays on success
|
- [X] Test: =fetchData()= updates relays on success
|
||||||
- [ ] Test: =fetchData()= sets error on API failure
|
- [X] Test: =fetchData()= sets error on API failure
|
||||||
- [ ] Test: =fetchData()= sets ~isLoading=false~ after completion
|
- [X] Test: =fetchData()= sets ~isLoading=false~ after completion
|
||||||
- [ ] Test: =fetchData()= updates =lastFetchTime=
|
- [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=
|
- =startPolling()=: Fetch immediately, then =setInterval=
|
||||||
- =stopPolling()=: =clearInterval= and =cleanup=
|
- =stopPolling()=: =clearInterval= and =cleanup=
|
||||||
@@ -1029,12 +1035,12 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
|||||||
|
|
||||||
*TDD Checklist*:
|
*TDD Checklist*:
|
||||||
|
|
||||||
- [ ] Test: =startPolling()= triggers immediate fetch
|
- [X] Test: =startPolling()= triggers immediate fetch
|
||||||
- [ ] Test: =startPolling()= sets interval for subsequent fetches
|
- [X] Test: =startPolling()= sets interval for subsequent fetches
|
||||||
- [ ] Test: =stopPolling()= clears interval
|
- [X] Test: =stopPolling()= clears interval
|
||||||
- [ ] Test: =onUnmounted= hook calls =stopPolling()=
|
- [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
|
- Track =isConnected= based on fetch success/failure
|
||||||
- Display connection status in UI
|
- Display connection status in UI
|
||||||
@@ -1059,25 +1065,25 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
|||||||
|
|
||||||
*TDD Checklist*:
|
*TDD Checklist*:
|
||||||
|
|
||||||
- [ ] Test: =isConnected= is true after successful fetch
|
- [X] Test: =isConnected= is true after successful fetch
|
||||||
- [ ] Test: =isConnected= is false after failed 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=)
|
- Props: relay (=RelayDto=)
|
||||||
- Display relay ID, state, label
|
- Display relay ID, state, label
|
||||||
- Emit toggle event on button click
|
- Emit toggle event on button click
|
||||||
- *File*: =frontend/src/components/RelayCard.vue=
|
- *File*: =frontend/src/components/RelayCard.vue=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
- [ ] *T056* [US1] [TDD] Create =RelayGrid= component
|
- [X] *T056* [US1] [TDD] Create =RelayGrid= component
|
||||||
- Use =useRelayPolling= composable
|
- Use =useRelayPolling= composable
|
||||||
- Render 8 RelayCard components
|
- Render 8 RelayCard components
|
||||||
- Handle toggle events by calling API
|
- Handle toggle events by calling API
|
||||||
- Display loading/error states
|
- Display loading/error states
|
||||||
- *File*: =frontend/src/components/RelayGrid.vue=
|
- *File*: =frontend/src/components/RelayGrid.vue=
|
||||||
- *Complexity*: Medium | *Uncertainty*: Low
|
- *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
|
- End-to-end test: Load page → see 8 relays → toggle relay 1 → verify state change
|
||||||
- Use Playwright or Cypress
|
- Use Playwright or Cypress
|
||||||
- *File*: =frontend/tests/e2e/relay-control.spec.ts=
|
- *File*: =frontend/tests/e2e/relay-control.spec.ts=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import type { components } from '../api/schema';
|
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
|
import type { components } from '../api/schema';
|
||||||
|
|
||||||
type Meta = components['schemas']['Meta'];
|
type Meta = components['schemas']['Meta'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
||||||
import type { Relay, RelayDto } from '../types/relay';
|
import type { Relay, RelayDto } from '../types/relay';
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { createApp } from 'vue';
|
|
||||||
import PrimeVue from 'primevue/config';
|
|
||||||
import Lara from '@primeuix/themes/lara';
|
import Lara from '@primeuix/themes/lara';
|
||||||
|
import PrimeVue from 'primevue/config';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools';
|
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
|||||||
Reference in New Issue
Block a user