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

12 KiB
Raw Blame History

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-slimcargo fetchcargo 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-alpinenpm install --legacy-peer-depsnpm 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.