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>
12 KiB
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 intopics(approved=0) → write allobjectfields + RF metadata (rssi, snr, dr, f_cnt, f_port) to InfluxDB measurementsensor_data- Tags:
dev_eui,device_name,application_name - Timestamp from ChirpStack
timefield - 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_hoursif > 0, otherwise global threshold - Status logic:
last_seenis 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_statusin 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
- If TOTP enabled: return
- 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
- LoginPage: Username/password form, conditional TOTP input step
- DashboardPage: 4 status tiles (active/warning/alarm/unknown counts) + active alarm list
- 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)
- UnknownTopicsPage: List of unapproved topics, dropdown to assign to free sensors
- AlarmsPage: Active alarms and warnings list
- UsersPage (admin only): User CRUD, 2FA enable/disable
6.2 Charts (Recharts)
- Line chart: Time series,
connectNullsenabled - 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/lorawebExecStart=/usr/bin/docker compose upExecStop=/usr/bin/docker compose downRestart=always
7.5 Secrets
All via .env file (not in git). .env.example as template with:
JWT_SECRET,ADMIN_PASSWORDINFLUX_TOKEN,INFLUX_ORG,INFLUX_BUCKETDOCKER_INFLUXDB_INIT_*for InfluxDB bootstrap
8. Implementation Order (Bottom-Up)
- Database migrations (SQL file + programmatic init)
- Daemon (HTTP ingest + alarm monitor)
- REST API (auth, CRUD, InfluxDB proxy)
- Frontend (pages, charts, responsive layout)
- Docker & infrastructure (Dockerfiles, Compose, nginx, systemd)
Each layer is testable independently before the next is built.