epaper-display/LORAWEB_NEU/docs/superpowers/specs/2026-03-19-loraweb-design.md
Christian Mueller e706b22f31 Add design specification for LoRaWAN Web Portal
Based on Pflichtenheft v2.2, documents the full system design
including daemon, API, frontend, and infrastructure with agreed
deviations (Axum 0.8, SQLx 0.8, Vite 6, Tailwind 4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:27:49 +01:00

321 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LoRaWAN Web Portal Design Specification
**Date:** 2026-03-19
**Based on:** Pflichtenheft v2.2 (März 2026)
**Approach:** Bottom-up implementation, pflichtenheft as goal with flexibility in implementation details
---
## 1. Overview
Web-based monitoring portal for LoRaWAN sensor infrastructure. Receives uplink data from IoT devices via ChirpStack HTTP Integration, stores measurements in InfluxDB, manages device/sensor configuration in SQLite, and presents everything through a secured React frontend.
### Key Decisions (Deviations from Pflichtenheft)
| Area | Pflichtenheft | Implementation | Reason |
|------|--------------|----------------|--------|
| SQLx | 0.7 | 0.8 | Current version, better SQLite support |
| Axum | 0.7 | 0.8 | Current version, improved API |
| Vite | 5 | 6 | Faster builds, no breaking changes for us |
| Tailwind | 3.4 | 4 | CSS-based engine, better performance |
| Rust Edition | 2021 | 2024 | Current standard |
| Deploy path | — | /opt/loraweb | As specified in pflichtenheft |
React 18, `config` crate, and core architecture remain unchanged.
---
## 2. Project Structure
```
LORAWEB_NEU/
├── Cargo.toml # Workspace root (members: backend/daemon, backend/api)
├── backend/
│ ├── daemon/
│ │ ├── Cargo.toml
│ │ ├── Dockerfile
│ │ └── src/
│ │ ├── main.rs # Tokio runtime, task spawning
│ │ ├── config.rs # AppConfig via config crate
│ │ ├── http.rs # Axum HTTP ingest listener
│ │ ├── influx.rs # InfluxDB writer
│ │ ├── db.rs # Topic registration (SQLite)
│ │ └── alarm.rs # Alarm monitor loop
│ └── api/
│ ├── Cargo.toml
│ ├── Dockerfile
│ └── src/
│ ├── main.rs # Router, DB init, AppState
│ ├── auth.rs # JWT, Argon2, TOTP
│ ├── sensors.rs # CRUD + data + display-config
│ ├── topics.rs # Topic management
│ ├── alarms.rs # Alarm endpoints
│ └── users.rs # User CRUD + 2FA
├── frontend/
│ ├── src/
│ │ ├── App.tsx # Router, protected routes
│ │ ├── api/client.ts # Axios instance + API functions
│ │ ├── store/authStore.ts # React Context auth state
│ │ ├── pages/
│ │ │ ├── LoginPage.tsx
│ │ │ ├── DashboardPage.tsx
│ │ │ ├── SensorsPage.tsx # DataModal, multi-chart, mobile cards
│ │ │ ├── UnknownTopicsPage.tsx
│ │ │ ├── AlarmsPage.tsx
│ │ │ └── UsersPage.tsx
│ │ └── components/
│ │ ├── Layout.tsx # Responsive sidebar + hamburger drawer
│ │ └── StatusBadge.tsx
│ ├── Dockerfile
│ └── nginx.conf
├── database/
│ └── migrations/001_initial.sql
├── deploy/
│ ├── docker-compose.yml # Project name: loraweb
│ ├── .env.example
│ └── loraweb.service # systemd unit
└── docs/
```
---
## 3. Database Schema (SQLite)
Three tables, created idempotently (`CREATE TABLE IF NOT EXISTS`) at startup by both daemon and API.
### topics
Represents a physical LoRaWAN device identified by DevEUI.
| Column | Type | Description |
|--------|------|-------------|
| id | TEXT (UUID v4) | Primary key |
| topic | TEXT UNIQUE | DevEUI |
| approved | INTEGER (0/1) | 0 = unknown, 1 = approved |
| last_seen | TEXT (RFC3339) | Last uplink timestamp |
| alarm_status | TEXT | active / warning / alarm / unknown |
| alarm_threshold_hours | REAL | Per-device threshold (0 = use global) |
| created_at | TEXT (RFC3339) | First registration |
### sensors
Logical sensor entity assigned to a topic.
| Column | Type | Description |
|--------|------|-------------|
| id | TEXT (UUID v4) | Primary key |
| name | TEXT | Display name |
| description | TEXT | Optional description |
| location | TEXT | Location string |
| sensor_type | TEXT | Type (e.g. "Temperatur") |
| active | INTEGER (0/1) | Active flag |
| topic_id | TEXT FK → topics.id | Assigned topic (nullable) |
| display_config | TEXT (JSON) | Per-sensor field config |
| created_at | TEXT (RFC3339) | Creation date |
### users
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK AUTOINCREMENT | Primary key |
| username | TEXT UNIQUE | Username |
| password_hash | TEXT | Argon2id hash |
| role | TEXT | "user" or "admin" |
| totp_secret | TEXT | Base32 TOTP secret (nullable) |
| totp_enabled | INTEGER (0/1) | 2FA active |
| created_at | TEXT (RFC3339) | Creation date |
---
## 4. Daemon Design
Single Rust binary running two async tasks via Tokio:
### 4.1 HTTP Ingest (Axum, Port 8080)
- `POST /?event=up`: Parse ChirpStack JSON → register unknown DevEUI in `topics` (approved=0) → write all `object` fields + RF metadata (rssi, snr, dr, f_cnt, f_port) to InfluxDB measurement `sensor_data`
- Tags: `dev_eui`, `device_name`, `application_name`
- Timestamp from ChirpStack `time` field
- Other events (join, ack, txack, log, status): return 200 OK, no processing
- InfluxDB errors: log and continue, no crash
### 4.2 Alarm Monitor (Tokio interval loop)
- Runs every `check_interval_secs` (default 60)
- Queries all topics with `approved=1`
- Per topic: use `alarm_threshold_hours` if > 0, otherwise global threshold
- Status logic:
- `last_seen` is NULL → `unknown`
- hours since last seen < warning threshold `active`
- hours since last seen < alarm threshold `warning`
- hours since last seen >= alarm threshold → `alarm`
- Updates `alarm_status` in SQLite
### 4.3 Configuration
Via `config` crate: loads `daemon-config.toml` + environment variables (prefix `LORAWEB__`).
| Key | Default | Description |
|-----|---------|-------------|
| http.bind | 0.0.0.0 | Bind address |
| http.port | 8080 | HTTP port |
| influx.url | http://localhost:8086 | InfluxDB URL |
| influx.token | — | API token |
| influx.org | loraweb | Organization |
| influx.bucket | sensors | Bucket |
| database.path | loraweb.db | SQLite path |
| alarm.warning_threshold_hours | 2.0 | Warning threshold |
| alarm.alarm_threshold_hours | 6.0 | Alarm threshold |
| alarm.check_interval_secs | 60 | Check interval |
### 4.4 Startup
Log banner showing ingest URL and ChirpStack configuration hint.
---
## 5. REST API Design
Axum on port 3001, shared SQLite DB with daemon.
### 5.1 Authentication
- `POST /api/auth/login`: Verify username/password (Argon2id)
- If TOTP enabled: return `{ totp_required: true }`
- Second request with TOTP code → issue JWT
- If no TOTP: issue JWT directly
- JWT: HS256, 24h validity, contains user id + role
- Auth middleware extracts Bearer token, injects user info
- Default admin created at first startup (password from env var)
### 5.2 Endpoints
| Route | Method | Auth | Description |
|-------|--------|------|-------------|
| /api/auth/login | POST | No | Login (+ optional TOTP) |
| /api/sensors | GET | Yes | List all sensors |
| /api/sensors | POST | Yes | Create sensor |
| /api/sensors/:id | GET | Yes | Sensor detail |
| /api/sensors/:id | PUT | Yes | Update sensor |
| /api/sensors/:id | DELETE | Yes | Delete sensor (resets topic to unapproved) |
| /api/sensors/:id/data | GET | Yes | InfluxDB proxy, ?hours=N |
| /api/sensors/:id/display-config | GET | Yes | Get field config |
| /api/sensors/:id/display-config | PUT | Yes | Save field config |
| /api/topics | GET | Yes | List all topics |
| /api/topics/:id | PUT | Yes | Approve, assign, update |
| /api/topics/:id/threshold | PUT | Yes | Set alarm threshold |
| /api/alarms | GET | Yes | Alarm summary |
| /api/users | GET | Admin | List users |
| /api/users | POST | Admin | Create user |
| /api/users/:id | PUT | Admin | Update user |
| /api/users/:id | DELETE | Admin | Delete user |
| /api/users/:id/totp | POST | Admin | Enable 2FA (returns secret + QR URI) |
| /api/users/:id/totp | DELETE | Admin | Disable 2FA |
### 5.3 InfluxDB Proxy
- Sends Flux query to InfluxDB via reqwest
- Receives annotated CSV (after pivot)
- Parses CSV to JSON array server-side
- Returns JSON to frontend — no InfluxDB credentials exposed
---
## 6. Frontend Design
React 18 + TypeScript, Vite 6, Tailwind 4, Recharts.
### 6.1 Pages
1. **LoginPage**: Username/password form, conditional TOTP input step
2. **DashboardPage**: 4 status tiles (active/warning/alarm/unknown counts) + active alarm list
3. **SensorsPage**: Table (desktop) / cards (mobile), CRUD modals, DataModal with two tabs:
- "Diagramme": Charts per configured panels (D1-D8), time selector, RF metadata toggle
- "Einrichten": Per-field config (label, unit, chart type, panel assignment)
4. **UnknownTopicsPage**: List of unapproved topics, dropdown to assign to free sensors
5. **AlarmsPage**: Active alarms and warnings list
6. **UsersPage** (admin only): User CRUD, 2FA enable/disable
### 6.2 Charts (Recharts)
- **Line chart**: Time series, `connectNulls` enabled
- **Bar chart (daily)**: Frontend aggregates raw data by date
- **Bar chart (weekly)**: Frontend aggregates by calendar week
- **Pie chart**: Percentage distribution
- Fields assigned to panels D1-D8; same panel number = same chart
- RF metadata (RSSI, SNR, DR) toggleable in line charts
- Time ranges: 1h, 6h, 24h, 7d, 30d, 90d
- Data table below charts: last 20 values with configured labels/units
### 6.3 Responsive Layout
- Desktop (`lg:` breakpoint, 1024px+): Static sidebar navigation
- Mobile: Top bar with hamburger icon → animated slide-in drawer
- Tables switch to card lists on mobile (`hidden sm:block` / `sm:hidden`)
- DataModal: Centered overlay on desktop, bottom-sheet on mobile
### 6.4 Auth & State
- React Context API for auth state
- localStorage: `loraweb_token`, `loraweb_user`
- Axios interceptor: 401 response → automatic logout + redirect to login
- All pages auto-refresh every 15 seconds
---
## 7. Infrastructure
### 7.1 Docker Compose
Project name: `loraweb`. Four services:
| Service | Image | Ports (host) | Volumes |
|---------|-------|-------------|---------|
| daemon | Custom (Rust) | 8080 | loraweb-data:/data |
| api | Custom (Rust) | — (internal) | loraweb-data:/data |
| frontend | Custom (nginx) | 80 | — |
| influxdb | influxdb:2.7 | — (internal) | loraweb-influx:/var/lib/influxdb2 |
All containers run as user `loraweb` (UID 1001).
### 7.2 Dockerfiles
**Rust (daemon + api)**: Multi-stage build
- Stage 1: `rust:1.88-slim``cargo fetch``cargo build --release`
- Stage 2: `debian:bookworm-slim` → copy binary, create non-root user
- Build context: project root (Cargo workspace)
**Frontend**: Multi-stage build
- Stage 1: `node:20-alpine``npm install --legacy-peer-deps``npm run build`
- Stage 2: `nginx:alpine` → copy dist + nginx.conf
### 7.3 nginx.conf
- `location /api/``proxy_pass http://api:3001/api/`
- `location /` → serve static files, `try_files $uri /index.html` (SPA fallback)
### 7.4 systemd
`deploy/loraweb.service`:
- `WorkingDirectory=/opt/loraweb`
- `ExecStart=/usr/bin/docker compose up`
- `ExecStop=/usr/bin/docker compose down`
- `Restart=always`
### 7.5 Secrets
All via `.env` file (not in git). `.env.example` as template with:
- `JWT_SECRET`, `ADMIN_PASSWORD`
- `INFLUX_TOKEN`, `INFLUX_ORG`, `INFLUX_BUCKET`
- `DOCKER_INFLUXDB_INIT_*` for InfluxDB bootstrap
---
## 8. Implementation Order (Bottom-Up)
1. Database migrations (SQL file + programmatic init)
2. Daemon (HTTP ingest + alarm monitor)
3. REST API (auth, CRUD, InfluxDB proxy)
4. Frontend (pages, charts, responsive layout)
5. Docker & infrastructure (Dockerfiles, Compose, nginx, systemd)
Each layer is testable independently before the next is built.