377 lines
14 KiB
Markdown
377 lines
14 KiB
Markdown
<h1 align="center">STA</h1>
|
||
<div align="center">
|
||
<strong>
|
||
Smart Temperature & Appliance Control
|
||
</strong>
|
||
</div>
|
||
<br/>
|
||
|
||
<div align="center">
|
||
<!-- Wakapi -->
|
||
<img alt="Coding Time Badge" src="https://clock.phundrak.com/api/badge/phundrak/interval:any/project:sta">
|
||
<!-- Emacs -->
|
||
<a href="https://www.gnu.org/software/emacs/"><img src="https://img.shields.io/badge/Emacs-30.2-blueviolet.svg?style=flat-square&logo=GNU%20Emacs&logoColor=white" /></a>
|
||
</div>
|
||
<br/>
|
||
|
||
> **🤖 AI-Assisted Development Notice**: This project uses Claude Code as a development assistant for task planning, code organization, and workflow management. However, all code is human-written, reviewed, and validated by the project maintainer. AI is used as a productivity tool, not as the author of the implementation.
|
||
|
||
Web-based Modbus relay control system for managing 8-channel relay modules over TCP.
|
||
|
||
## 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
|
||
|
||
**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.
|
||
|
||
|
||
## Architecture
|
||
|
||
**Current:**
|
||
- **Backend**: Rust 2024 with Poem web framework (hexagonal architecture)
|
||
- **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
|
||
- **Modbus Integration**: tokio-modbus for Modbus TCP communication
|
||
- **Persistence**: SQLite for relay labels with compile-time SQL verification
|
||
|
||
**Planned:**
|
||
- **Deployment**: Backend on Raspberry Pi, frontend on Cloudflare Pages
|
||
- **Access**: Traefik reverse proxy with Authelia authentication
|
||
|
||
## Quick Start
|
||
|
||
### Prerequisites
|
||
|
||
- Rust 1.83+ (edition 2024)
|
||
- Just command runner
|
||
|
||
### Development
|
||
|
||
```bash
|
||
# Run backend development server
|
||
just backend run
|
||
|
||
# Run frontend
|
||
just frontend run
|
||
|
||
# Run backend tests
|
||
just backend test
|
||
|
||
# Run backend linter
|
||
just backend lint
|
||
|
||
# Format backend code
|
||
just backend format
|
||
|
||
# Watch mode with bacon
|
||
bacon # clippy-all (default)
|
||
bacon test # test watcher
|
||
```
|
||
|
||
### Configuration
|
||
|
||
Edit `backend/settings/base.yaml` for Modbus device settings:
|
||
|
||
```yaml
|
||
modbus:
|
||
host: "192.168.1.200"
|
||
port: 502
|
||
slave_id: 1
|
||
timeout_secs: 5
|
||
|
||
relay:
|
||
label_max_length: 50
|
||
```
|
||
|
||
Override with environment variables:
|
||
```bash
|
||
APP__MODBUS__HOST=192.168.1.100 cargo run
|
||
```
|
||
|
||
#### CORS Configuration
|
||
|
||
**Development Mode** (frontend on `localhost:5173`):
|
||
|
||
```yaml
|
||
# backend/settings/development.yaml
|
||
cors:
|
||
allowed_origins: ["*"] # Permissive for local development
|
||
allow_credentials: false # MUST be false with wildcard
|
||
max_age_secs: 3600
|
||
```
|
||
|
||
**Production Mode** (frontend on Cloudflare Pages):
|
||
|
||
```yaml
|
||
# backend/settings/production.yaml
|
||
cors:
|
||
allowed_origins:
|
||
- "https://sta.yourdomain.com" # Specific origin only
|
||
allow_credentials: true # Required for Authelia authentication
|
||
max_age_secs: 3600
|
||
```
|
||
|
||
**Security Notes:**
|
||
- Wildcard `"*"` origin is **only allowed with `allow_credentials: false`**
|
||
- Production **must** use specific origins (e.g., `https://sta.example.com`)
|
||
- Multiple origins are supported as a list
|
||
- Credentials must be enabled for Authelia authentication to work
|
||
|
||
**Hardcoded Security Defaults:**
|
||
- **Methods**: GET, POST, PUT, PATCH, DELETE, OPTIONS (all required methods)
|
||
- **Headers**: content-type, authorization (minimum for API + auth)
|
||
|
||
**Fail-Safe Defaults:**
|
||
- `allowed_origins: []` (restrictive - no origins allowed)
|
||
- `allow_credentials: false`
|
||
- `max_age_secs: 3600` (1 hour)
|
||
|
||
See [CORS Configuration Guide](docs/cors-configuration.md) for complete documentation.
|
||
|
||
## API Documentation
|
||
|
||
The server provides OpenAPI documentation via Swagger UI:
|
||
- Swagger UI: `http://localhost:3100/`
|
||
- 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):**
|
||
- `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
|
||
|
||
## Project Structure
|
||
|
||
**Monorepo Layout:**
|
||
```
|
||
sta/
|
||
├── backend/ # Rust backend
|
||
│ ├── src/
|
||
│ │ ├── 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
|
||
│ │ │ ├── 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
|
||
│ │ │ ├── health/
|
||
│ │ │ │ └── health_monitor.rs - Health monitoring
|
||
│ │ │ └── use_cases/
|
||
│ │ │ ├── get_all_relays.rs - List all relays
|
||
│ │ │ └── toggle_relay.rs - Toggle single relay
|
||
│ │ │
|
||
│ │ ├── 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 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 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
|
||
│
|
||
├── 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, 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
|
||
|
||
**Currently Used:**
|
||
- Rust 2024 edition
|
||
- Poem 3.1 (web framework with OpenAPI support)
|
||
- Tokio 1.48 (async runtime)
|
||
- tokio-modbus (Modbus TCP client for relay hardware)
|
||
- SQLx 0.8 (async SQLite with compile-time SQL verification)
|
||
- async-trait (async methods in traits)
|
||
- config (YAML configuration)
|
||
- tracing + tracing-subscriber (structured logging)
|
||
- governor (rate limiting)
|
||
- thiserror (error handling)
|
||
- serde + serde_yaml (configuration deserialization)
|
||
|
||
**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
|
||
|
||
**Phase 0.5 CORS Testing:**
|
||
- **Unit Tests**: 11 tests in `backend/src/settings/cors.rs`
|
||
- CorsSettings deserialization (5 tests)
|
||
- From<CorsSettings> for Cors trait (6 tests)
|
||
- Security validation (wildcard + credentials check)
|
||
- **Integration Tests**: 9 tests in `backend/tests/cors_test.rs`
|
||
- Preflight OPTIONS requests
|
||
- Actual request CORS headers
|
||
- Max-age configuration
|
||
- Credentials handling
|
||
- Allowed methods verification
|
||
- Wildcard origin behavior
|
||
- Multiple origins support
|
||
- Unauthorized origin rejection
|
||
|
||
**Test Coverage Achieved**: 15 comprehensive tests covering all CORS scenarios
|
||
|
||
**Phase 2 Domain Layer Testing:**
|
||
- **Unit Tests**: 50+ tests embedded in domain modules
|
||
- RelayId validation (5 tests)
|
||
- RelayState serialization (3 tests)
|
||
- RelayLabel validation (5 tests)
|
||
- Relay aggregate behavior (8 tests)
|
||
- ModbusAddress conversion (3 tests)
|
||
- HealthStatus state transitions (15 tests)
|
||
- **TDD Approach**: Red-Green-Refactor for all implementations
|
||
- **Coverage**: 100% for domain layer (zero external dependencies)
|
||
|
||
**Test Coverage Achieved**: 100% domain layer coverage with comprehensive test suites
|
||
|
||
**Phase 3 Infrastructure Testing:**
|
||
- **MockRelayController Tests**: 6 tests in `mock_controller.rs`
|
||
- Read/write state operations
|
||
- Read/write all relay states
|
||
- Invalid relay ID handling
|
||
- Thread-safe concurrent access
|
||
- **ModbusRelayController Tests**: Hardware integration tests (#[ignore])
|
||
- Real hardware communication tests
|
||
- Connection timeout handling
|
||
- **SqliteRelayLabelRepository Tests**: Database layer tests
|
||
- CRUD operations on relay labels
|
||
- In-memory database for fast tests
|
||
- Compile-time SQL verification
|
||
- **HealthMonitor Tests**: 15+ tests in `health_monitor.rs`
|
||
- State transitions (Healthy -> Degraded -> Unhealthy)
|
||
- Recovery from failure states
|
||
- Concurrent access safety
|
||
|
||
**Test Coverage Achieved**: Comprehensive coverage across all layers with TDD approach
|
||
|
||
## Documentation
|
||
|
||
### Configuration Guides
|
||
- [CORS Configuration](docs/cors-configuration.md) - Cross-origin setup for frontend-backend communication
|
||
- [Modbus Hardware Documentation](docs/Modbus_POE_ETH_Relay.md) - 8-channel relay device documentation
|
||
|
||
### Architecture Documentation
|
||
- [Domain Layer Architecture](docs/domain-layer.md) - Type-driven domain design and implementation
|
||
- [Domain Layer Details](specs/001-modbus-relay-control/domain-layer-architecture.md) - Comprehensive domain layer documentation
|
||
- [Phase 2 Lessons Learned](specs/001-modbus-relay-control/lessons-learned.md) - Implementation insights and best practices
|
||
|
||
### Development Guides
|
||
- [Project Constitution](specs/constitution.md) - Architectural principles and development guidelines
|
||
- [Modbus Relay Control Spec](specs/001-modbus-relay-control/spec.md) - Feature specification
|
||
- [CLAUDE.md](CLAUDE.md) - Developer guide and code style rules
|
||
|
||
## License
|
||
|
||
This project is under the AGPL-3.0 license. You can find it in the [LICENSE.md](LICENSE.md) file.
|