# 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.