jetzt mit Header, 3 Tabellen
This commit is contained in:
parent
f2417ec65d
commit
220161a70a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
table_config.json
|
||||||
|
Cargo.lock
|
||||||
|
.DS_Store
|
||||||
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -19,6 +19,29 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-files"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576"
|
||||||
|
dependencies = [
|
||||||
|
"actix-http",
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"derive_more",
|
||||||
|
"futures-core",
|
||||||
|
"http-range",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"v_htmlescape",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.11.2"
|
version = "3.11.2"
|
||||||
@ -656,6 +679,12 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@ -935,6 +964,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@ -1471,6 +1510,7 @@ dependencies = [
|
|||||||
name = "table-server"
|
name = "table-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"actix-files",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1626,6 +1666,12 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@ -1662,6 +1708,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "v_htmlescape"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.4.0"
|
actix-web = "4.4.0"
|
||||||
|
actix-files = "0.6.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
116
README.md
116
README.md
@ -9,10 +9,12 @@ table-server/
|
|||||||
├── src/
|
├── src/
|
||||||
│ └── main.rs
|
│ └── main.rs
|
||||||
├── templates/
|
├── templates/
|
||||||
│ └── index.html
|
│ ├── index.html
|
||||||
|
│ └── settings.html
|
||||||
├── static/
|
├── static/
|
||||||
│ ├── style.css
|
│ ├── style.css
|
||||||
│ └── script.js
|
│ ├── script.js
|
||||||
|
│ └── settings.js
|
||||||
├── Cargo.toml
|
├── Cargo.toml
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
@ -21,12 +23,17 @@ table-server/
|
|||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
- Webbasierte Sensor-Konfigurationstabelle mit 6 Spalten
|
- **3 separate Tabellen** für verschiedene Sensor-Gruppen
|
||||||
|
- **Navigation** mit aktivem Status-Indikator
|
||||||
|
- **Header mit Logo** für professionelles Erscheinungsbild
|
||||||
|
- **Zeilen hinzufügen/löschen** dynamisch zur Laufzeit
|
||||||
|
- **Einstellungsseite** für MQTT und InfluxDB Konfiguration
|
||||||
- Editierbare Textfelder (Bezeichnung, Adresse, Type, Faktor)
|
- Editierbare Textfelder (Bezeichnung, Adresse, Type, Faktor)
|
||||||
- Toggle-Schalter für Boolean-Werte (MQTT, InfluxDB)
|
- Toggle-Schalter für Boolean-Werte (MQTT, InfluxDB)
|
||||||
- Persistierung in JSON-Datei
|
- **Zentrale JSON-Persistierung** für alle Tabellen und Einstellungen
|
||||||
- Einfache REST-API
|
- REST-API für Daten-Management
|
||||||
- Docker-Unterstützung
|
- Docker-Unterstützung
|
||||||
|
- Responsive Design
|
||||||
|
|
||||||
## Lokale Entwicklung
|
## Lokale Entwicklung
|
||||||
|
|
||||||
@ -89,27 +96,39 @@ docker-compose up -d
|
|||||||
## Verwendung
|
## Verwendung
|
||||||
|
|
||||||
1. Öffne `http://localhost:8080` im Browser
|
1. Öffne `http://localhost:8080` im Browser
|
||||||
2. Bearbeite die Sensor-Konfigurationen:
|
2. Navigiere zwischen den Tabellen über das Menü:
|
||||||
- **Bezeichnung**: Name des Sensors
|
- **Tabelle 1, 2, 3**: Verschiedene Sensor-Gruppen
|
||||||
- **Adresse**: IP-Adresse oder Identifier
|
- **⚙️ Einstellungen**: MQTT und InfluxDB Konfiguration
|
||||||
- **Type**: Sensor-Typ (z.B. Temperatur, Luftfeuchtigkeit)
|
3. In den Tabellen:
|
||||||
- **Faktor**: Numerischer Korrekturfaktor
|
- **➕ Zeile hinzufügen**: Neue Sensor-Einträge erstellen
|
||||||
- **MQTT**: Toggle-Schalter für MQTT-Aktivierung
|
- **🗑️ Löschen**: Einzelne Zeilen entfernen
|
||||||
- **InfluxDB**: Toggle-Schalter für InfluxDB-Aktivierung
|
- **Felder bearbeiten**:
|
||||||
3. Klicke auf "Speichern" um die Änderungen zu persistieren
|
- Bezeichnung: Name des Sensors
|
||||||
4. Die Daten werden in `table_config.json` gespeichert
|
- Adresse: IP-Adresse oder Identifier
|
||||||
|
- Type: Sensor-Typ (z.B. Temperatur, Luftfeuchtigkeit)
|
||||||
|
- Faktor: Numerischer Korrekturfaktor
|
||||||
|
- MQTT: Toggle-Schalter für MQTT-Aktivierung
|
||||||
|
- InfluxDB: Toggle-Schalter für InfluxDB-Aktivierung
|
||||||
|
4. **💾 Speichern**: Änderungen persistieren
|
||||||
|
5. Alle Daten werden zentral in `table_config.json` gespeichert
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
- `GET /` - Zeigt die HTML-Seite mit der Tabelle
|
- `GET /` - Zeigt Tabelle 1
|
||||||
- `POST /api/save` - Speichert die Tabellendaten
|
- `GET /table/table2` - Zeigt Tabelle 2
|
||||||
|
- `GET /table/table3` - Zeigt Tabelle 3
|
||||||
|
- `GET /settings` - Zeigt Einstellungsseite
|
||||||
|
- `POST /api/save` - Speichert eine Tabelle
|
||||||
|
- `POST /api/save-settings` - Speichert die Einstellungen
|
||||||
|
- `GET /static/*` - Statische Dateien (CSS, JS)
|
||||||
|
|
||||||
### Beispiel API-Request
|
### Beispiel API-Request (Tabelle speichern)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8080/api/save \
|
curl -X POST http://localhost:8080/api/save \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
|
"table_id": "table1",
|
||||||
"rows": [
|
"rows": [
|
||||||
{
|
{
|
||||||
"bezeichnung": "Sensor 1",
|
"bezeichnung": "Sensor 1",
|
||||||
@ -118,28 +137,33 @@ curl -X POST http://localhost:8080/api/save \
|
|||||||
"faktor": "1.0",
|
"faktor": "1.0",
|
||||||
"mqtt": true,
|
"mqtt": true,
|
||||||
"influxdb": false
|
"influxdb": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"bezeichnung": "Sensor 2",
|
|
||||||
"adresse": "192.168.1.101",
|
|
||||||
"type": "Luftfeuchtigkeit",
|
|
||||||
"faktor": "0.5",
|
|
||||||
"mqtt": false,
|
|
||||||
"influxdb": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Beispiel API-Request (Einstellungen speichern)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/save-settings \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"mqtt_broker": "localhost",
|
||||||
|
"mqtt_port": "1883",
|
||||||
|
"influxdb_url": "http://localhost:8086",
|
||||||
|
"influxdb_token": "your-token-here"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
## Konfigurationsdatei
|
## Konfigurationsdatei
|
||||||
|
|
||||||
Die Sensor-Daten werden in `table_config.json` gespeichert:
|
Die komplette Anwendungskonfiguration wird in `table_config.json` gespeichert:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rows": [
|
"table1": [
|
||||||
{
|
{
|
||||||
"bezeichnung": "Sensor 1",
|
"bezeichnung": "Temp Sensor 1",
|
||||||
"adresse": "192.168.1.100",
|
"adresse": "192.168.1.100",
|
||||||
"type": "Temperatur",
|
"type": "Temperatur",
|
||||||
"faktor": "1.0",
|
"faktor": "1.0",
|
||||||
@ -147,22 +171,40 @@ Die Sensor-Daten werden in `table_config.json` gespeichert:
|
|||||||
"influxdb": false
|
"influxdb": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bezeichnung": "Sensor 2",
|
"bezeichnung": "Temp Sensor 2",
|
||||||
"adresse": "192.168.1.101",
|
"adresse": "192.168.1.101",
|
||||||
"type": "Luftfeuchtigkeit",
|
"type": "Temperatur",
|
||||||
"faktor": "0.5",
|
"faktor": "1.0",
|
||||||
"mqtt": false,
|
"mqtt": false,
|
||||||
"influxdb": true
|
"influxdb": true
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
"table2": [
|
||||||
{
|
{
|
||||||
"bezeichnung": "Sensor 3",
|
"bezeichnung": "Humidity Sensor 1",
|
||||||
"adresse": "192.168.1.102",
|
"adresse": "192.168.1.200",
|
||||||
"type": "Druck",
|
"type": "Luftfeuchtigkeit",
|
||||||
"faktor": "2.0",
|
"faktor": "0.5",
|
||||||
"mqtt": true,
|
"mqtt": true,
|
||||||
"influxdb": true
|
"influxdb": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"table3": [
|
||||||
|
{
|
||||||
|
"bezeichnung": "Pressure Sensor 1",
|
||||||
|
"adresse": "192.168.1.300",
|
||||||
|
"type": "Druck",
|
||||||
|
"faktor": "2.0",
|
||||||
|
"mqtt": true,
|
||||||
|
"influxdb": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"mqtt_broker": "localhost",
|
||||||
|
"mqtt_port": "1883",
|
||||||
|
"influxdb_url": "http://localhost:8086",
|
||||||
|
"influxdb_token": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
166
src/main.rs
166
src/main.rs
@ -1,6 +1,7 @@
|
|||||||
use actix_web::{web, App, HttpResponse, HttpServer, Result};
|
use actix_web::{web, App, HttpResponse, HttpServer, Result};
|
||||||
|
use actix_files as fs;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs as std_fs;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
@ -15,16 +16,38 @@ struct TableRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
struct TableData {
|
struct Settings {
|
||||||
rows: Vec<TableRow>,
|
mqtt_broker: String,
|
||||||
|
mqtt_port: String,
|
||||||
|
influxdb_url: String,
|
||||||
|
influxdb_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TableData {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
TableData {
|
Settings {
|
||||||
rows: vec![
|
mqtt_broker: "localhost".to_string(),
|
||||||
|
mqtt_port: "1883".to_string(),
|
||||||
|
influxdb_url: "http://localhost:8086".to_string(),
|
||||||
|
influxdb_token: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct AppConfig {
|
||||||
|
table1: Vec<TableRow>,
|
||||||
|
table2: Vec<TableRow>,
|
||||||
|
table3: Vec<TableRow>,
|
||||||
|
settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
AppConfig {
|
||||||
|
table1: vec![
|
||||||
TableRow {
|
TableRow {
|
||||||
bezeichnung: "Sensor 1".to_string(),
|
bezeichnung: "Temp Sensor 1".to_string(),
|
||||||
adresse: "192.168.1.100".to_string(),
|
adresse: "192.168.1.100".to_string(),
|
||||||
r#type: "Temperatur".to_string(),
|
r#type: "Temperatur".to_string(),
|
||||||
faktor: "1.0".to_string(),
|
faktor: "1.0".to_string(),
|
||||||
@ -32,39 +55,52 @@ impl Default for TableData {
|
|||||||
influxdb: false,
|
influxdb: false,
|
||||||
},
|
},
|
||||||
TableRow {
|
TableRow {
|
||||||
bezeichnung: "Sensor 2".to_string(),
|
bezeichnung: "Temp Sensor 2".to_string(),
|
||||||
adresse: "192.168.1.101".to_string(),
|
adresse: "192.168.1.101".to_string(),
|
||||||
r#type: "Luftfeuchtigkeit".to_string(),
|
r#type: "Temperatur".to_string(),
|
||||||
faktor: "0.5".to_string(),
|
faktor: "1.0".to_string(),
|
||||||
mqtt: false,
|
mqtt: false,
|
||||||
influxdb: true,
|
influxdb: true,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
table2: vec![
|
||||||
TableRow {
|
TableRow {
|
||||||
bezeichnung: "Sensor 3".to_string(),
|
bezeichnung: "Humidity Sensor 1".to_string(),
|
||||||
adresse: "192.168.1.102".to_string(),
|
adresse: "192.168.1.200".to_string(),
|
||||||
r#type: "Druck".to_string(),
|
r#type: "Luftfeuchtigkeit".to_string(),
|
||||||
faktor: "2.0".to_string(),
|
faktor: "0.5".to_string(),
|
||||||
mqtt: true,
|
mqtt: true,
|
||||||
influxdb: true,
|
influxdb: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
table3: vec![
|
||||||
|
TableRow {
|
||||||
|
bezeichnung: "Pressure Sensor 1".to_string(),
|
||||||
|
adresse: "192.168.1.300".to_string(),
|
||||||
|
r#type: "Druck".to_string(),
|
||||||
|
faktor: "2.0".to_string(),
|
||||||
|
mqtt: true,
|
||||||
|
influxdb: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: Settings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
table: Mutex<TableData>,
|
config: Mutex<AppConfig>,
|
||||||
config_path: String,
|
config_path: String,
|
||||||
templates: Tera,
|
templates: Tera,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn load_or_create(config_path: &str) -> Self {
|
fn load_or_create(config_path: &str) -> Self {
|
||||||
let table = match fs::read_to_string(config_path) {
|
let config = match std_fs::read_to_string(config_path) {
|
||||||
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let default = TableData::default();
|
let default = AppConfig::default();
|
||||||
let _ = fs::write(config_path, serde_json::to_string_pretty(&default).unwrap());
|
let _ = std_fs::write(config_path, serde_json::to_string_pretty(&default).unwrap());
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -78,24 +114,26 @@ impl AppState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AppState {
|
AppState {
|
||||||
table: Mutex::new(table),
|
config: Mutex::new(config),
|
||||||
config_path: config_path.to_string(),
|
config_path: config_path.to_string(),
|
||||||
templates: tera,
|
templates: tera,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self) -> Result<(), std::io::Error> {
|
fn save(&self) -> Result<(), std::io::Error> {
|
||||||
let table = self.table.lock().unwrap();
|
let config = self.config.lock().unwrap();
|
||||||
let json = serde_json::to_string_pretty(&*table)?;
|
let json = serde_json::to_string_pretty(&*config)?;
|
||||||
fs::write(&self.config_path, json)
|
std_fs::write(&self.config_path, json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||||
let table = data.table.lock().unwrap();
|
let config = data.config.lock().unwrap();
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("rows", &table.rows);
|
context.insert("rows", &config.table1);
|
||||||
|
context.insert("table_id", "table1");
|
||||||
|
context.insert("active_page", "table1");
|
||||||
|
|
||||||
let html = data.templates.render("index.html", &context)
|
let html = data.templates.render("index.html", &context)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@ -106,13 +144,81 @@ async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
|||||||
Ok(HttpResponse::Ok().content_type("text/html").body(html))
|
Ok(HttpResponse::Ok().content_type("text/html").body(html))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn table_page(data: web::Data<AppState>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||||
|
let table_id = path.into_inner();
|
||||||
|
let config = data.config.lock().unwrap();
|
||||||
|
|
||||||
|
let rows = match table_id.as_str() {
|
||||||
|
"table1" => &config.table1,
|
||||||
|
"table2" => &config.table2,
|
||||||
|
"table3" => &config.table3,
|
||||||
|
_ => return Ok(HttpResponse::NotFound().body("Table not found")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("rows", rows);
|
||||||
|
context.insert("table_id", &table_id);
|
||||||
|
context.insert("active_page", &table_id);
|
||||||
|
|
||||||
|
let html = data.templates.render("index.html", &context)
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Template error: {}", e);
|
||||||
|
actix_web::error::ErrorInternalServerError("Template error")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||||
|
let config = data.config.lock().unwrap();
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("settings", &config.settings);
|
||||||
|
context.insert("active_page", "settings");
|
||||||
|
|
||||||
|
let html = data.templates.render("settings.html", &context)
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Template error: {}", e);
|
||||||
|
actix_web::error::ErrorInternalServerError("Template error")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SaveTableRequest {
|
||||||
|
table_id: String,
|
||||||
|
rows: Vec<TableRow>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn save_table(
|
async fn save_table(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
table_data: web::Json<TableData>,
|
req: web::Json<SaveTableRequest>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let mut table = data.table.lock().unwrap();
|
let mut config = data.config.lock().unwrap();
|
||||||
*table = table_data.into_inner();
|
|
||||||
drop(table);
|
match req.table_id.as_str() {
|
||||||
|
"table1" => config.table1 = req.rows.clone(),
|
||||||
|
"table2" => config.table2 = req.rows.clone(),
|
||||||
|
"table3" => config.table3 = req.rows.clone(),
|
||||||
|
_ => return Ok(HttpResponse::BadRequest().json(serde_json::json!({"status": "error", "message": "Invalid table_id"}))),
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
match data.save() {
|
||||||
|
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
|
||||||
|
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_settings(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
settings: web::Json<Settings>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let mut config = data.config.lock().unwrap();
|
||||||
|
config.settings = settings.into_inner();
|
||||||
|
drop(config);
|
||||||
|
|
||||||
match data.save() {
|
match data.save() {
|
||||||
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
|
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
|
||||||
@ -131,7 +237,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.app_data(app_state.clone())
|
.app_data(app_state.clone())
|
||||||
.route("/", web::get().to(index))
|
.route("/", web::get().to(index))
|
||||||
|
.route("/table/{id}", web::get().to(table_page))
|
||||||
|
.route("/settings", web::get().to(settings_page))
|
||||||
.route("/api/save", web::post().to(save_table))
|
.route("/api/save", web::post().to(save_table))
|
||||||
|
.route("/api/save-settings", web::post().to(save_settings))
|
||||||
|
.service(fs::Files::new("/static", "./static"))
|
||||||
})
|
})
|
||||||
.bind("0.0.0.0:8080")?
|
.bind("0.0.0.0:8080")?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@ -1,14 +1,60 @@
|
|||||||
|
function addRow() {
|
||||||
|
const tableBody = document.getElementById('tableBody');
|
||||||
|
const rowCount = tableBody.querySelectorAll('tr').length;
|
||||||
|
|
||||||
|
const newRow = document.createElement('tr');
|
||||||
|
newRow.setAttribute('data-row', rowCount);
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<td><input type='text' class='text-input' data-field='bezeichnung' value='' /></td>
|
||||||
|
<td><input type='text' class='text-input' data-field='adresse' value='' /></td>
|
||||||
|
<td><input type='text' class='text-input' data-field='type' value='' /></td>
|
||||||
|
<td><input type='text' class='text-input' data-field='faktor' value='1.0' /></td>
|
||||||
|
<td>
|
||||||
|
<label class='switch'>
|
||||||
|
<input type='checkbox' class='bool-input' data-field='mqtt' />
|
||||||
|
<span class='slider'></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class='switch'>
|
||||||
|
<input type='checkbox' class='bool-input' data-field='influxdb' />
|
||||||
|
<span class='slider'></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="delete-btn" onclick="deleteRow(this)">🗑️</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRow(button) {
|
||||||
|
const row = button.closest('tr');
|
||||||
|
if (confirm('Möchten Sie diese Zeile wirklich löschen?')) {
|
||||||
|
row.remove();
|
||||||
|
updateRowIndices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowIndices() {
|
||||||
|
const rows = document.querySelectorAll('#tableBody tr');
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
row.setAttribute('data-row', index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const rowCount = document.querySelectorAll('tbody tr').length;
|
const tableRows = document.querySelectorAll('#tableBody tr');
|
||||||
|
|
||||||
for (let i = 0; i < rowCount; i++) {
|
tableRows.forEach((row) => {
|
||||||
const bezeichnung = document.querySelector(`input[data-row='${i}'][data-field='bezeichnung']`).value;
|
const bezeichnung = row.querySelector("input[data-field='bezeichnung']").value;
|
||||||
const adresse = document.querySelector(`input[data-row='${i}'][data-field='adresse']`).value;
|
const adresse = row.querySelector("input[data-field='adresse']").value;
|
||||||
const type = document.querySelector(`input[data-row='${i}'][data-field='type']`).value;
|
const type = row.querySelector("input[data-field='type']").value;
|
||||||
const faktor = document.querySelector(`input[data-row='${i}'][data-field='faktor']`).value;
|
const faktor = row.querySelector("input[data-field='faktor']").value;
|
||||||
const mqtt = document.querySelector(`input[data-row='${i}'][data-field='mqtt']`).checked;
|
const mqtt = row.querySelector("input[data-field='mqtt']").checked;
|
||||||
const influxdb = document.querySelector(`input[data-row='${i}'][data-field='influxdb']`).checked;
|
const influxdb = row.querySelector("input[data-field='influxdb']").checked;
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
bezeichnung,
|
bezeichnung,
|
||||||
@ -18,7 +64,7 @@ async function saveTable() {
|
|||||||
mqtt,
|
mqtt,
|
||||||
influxdb
|
influxdb
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/save', {
|
const response = await fetch('/api/save', {
|
||||||
@ -26,7 +72,10 @@ async function saveTable() {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ rows: rows })
|
body: JSON.stringify({
|
||||||
|
table_id: tableId,
|
||||||
|
rows: rows
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageDiv = document.getElementById('message');
|
const messageDiv = document.getElementById('message');
|
||||||
|
|||||||
40
static/settings.js
Normal file
40
static/settings.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
async function saveSettings() {
|
||||||
|
const mqtt_broker = document.getElementById('mqtt_broker').value;
|
||||||
|
const mqtt_port = document.getElementById('mqtt_port').value;
|
||||||
|
const influxdb_url = document.getElementById('influxdb_url').value;
|
||||||
|
const influxdb_token = document.getElementById('influxdb_token').value;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
mqtt_broker,
|
||||||
|
mqtt_port,
|
||||||
|
influxdb_url,
|
||||||
|
influxdb_token
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/save-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(settings)
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
if (response.ok) {
|
||||||
|
messageDiv.className = 'message success';
|
||||||
|
messageDiv.textContent = '✓ Einstellungen erfolgreich gespeichert!';
|
||||||
|
} else {
|
||||||
|
messageDiv.className = 'message error';
|
||||||
|
messageDiv.textContent = '✗ Fehler beim Speichern der Einstellungen!';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
messageDiv.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
messageDiv.className = 'message error';
|
||||||
|
messageDiv.textContent = '✗ Verbindungsfehler!';
|
||||||
|
}
|
||||||
|
}
|
||||||
179
static/style.css
179
static/style.css
@ -1,16 +1,79 @@
|
|||||||
body {
|
* {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
margin: 0;
|
||||||
max-width: 1200px;
|
padding: 0;
|
||||||
margin: 30px auto;
|
box-sizing: border-box;
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 15px 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
.container {
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 30px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 30px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,8 +84,18 @@ h1 {
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styles */
|
||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@ -64,7 +137,7 @@ tr:hover {
|
|||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle Switch Styles */
|
/* Toggle Switch */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -114,11 +187,16 @@ input:checked + .slider:before {
|
|||||||
transform: translateX(26px);
|
transform: translateX(26px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
/* Buttons */
|
||||||
display: block;
|
.button-group {
|
||||||
width: 200px;
|
display: flex;
|
||||||
margin: 30px auto 0;
|
gap: 15px;
|
||||||
padding: 14px;
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn, .add-btn {
|
||||||
|
padding: 14px 30px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@ -129,15 +207,35 @@ input:checked + .slider:before {
|
|||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn:hover {
|
.save-btn:hover, .add-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn:active {
|
.save-btn:active, .add-btn:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: #da190b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
.message {
|
.message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@ -160,3 +258,52 @@ input:checked + .slider:before {
|
|||||||
border: 1px solid #f5c6cb;
|
border: 1px solid #f5c6cb;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Page */
|
||||||
|
.settings-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .text-input {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn, .add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,11 +7,29 @@
|
|||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="40" rx="8" fill="#667eea"/>
|
||||||
|
<path d="M12 20L18 26L28 14" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="logo-text">Sensor Manager</span>
|
||||||
|
</div>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-link {% if active_page == 'table1' %}active{% endif %}">Tabelle 1</a>
|
||||||
|
<a href="/table/table2" class="nav-link {% if active_page == 'table2' %}active{% endif %}">Tabelle 2</a>
|
||||||
|
<a href="/table/table3" class="nav-link {% if active_page == 'table3' %}active{% endif %}">Tabelle 3</a>
|
||||||
|
<a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🔧 Sensor Konfiguration</h1>
|
<h1>🔧 Sensor Konfiguration - {{ table_id | upper }}</h1>
|
||||||
<div id="message" class="message"></div>
|
<div id="message" class="message"></div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table>
|
<table id="sensorTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Bezeichnung</th>
|
<th>Bezeichnung</th>
|
||||||
@ -20,35 +38,45 @@
|
|||||||
<th>Faktor</th>
|
<th>Faktor</th>
|
||||||
<th>MQTT</th>
|
<th>MQTT</th>
|
||||||
<th>InfluxDB</th>
|
<th>InfluxDB</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="tableBody">
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr>
|
<tr data-row="{{ loop.index0 }}">
|
||||||
<td><input type='text' class='text-input' data-row='{{ loop.index0 }}' data-field='bezeichnung' value='{{ row.bezeichnung }}' /></td>
|
<td><input type='text' class='text-input' data-field='bezeichnung' value='{{ row.bezeichnung }}' /></td>
|
||||||
<td><input type='text' class='text-input' data-row='{{ loop.index0 }}' data-field='adresse' value='{{ row.adresse }}' /></td>
|
<td><input type='text' class='text-input' data-field='adresse' value='{{ row.adresse }}' /></td>
|
||||||
<td><input type='text' class='text-input' data-row='{{ loop.index0 }}' data-field='type' value='{{ row.type }}' /></td>
|
<td><input type='text' class='text-input' data-field='type' value='{{ row.type }}' /></td>
|
||||||
<td><input type='text' class='text-input' data-row='{{ loop.index0 }}' data-field='faktor' value='{{ row.faktor }}' /></td>
|
<td><input type='text' class='text-input' data-field='faktor' value='{{ row.faktor }}' /></td>
|
||||||
<td>
|
<td>
|
||||||
<label class='switch'>
|
<label class='switch'>
|
||||||
<input type='checkbox' class='bool-input' data-row='{{ loop.index0 }}' data-field='mqtt' {% if row.mqtt %}checked{% endif %} />
|
<input type='checkbox' class='bool-input' data-field='mqtt' {% if row.mqtt %}checked{% endif %} />
|
||||||
<span class='slider'></span>
|
<span class='slider'></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<label class='switch'>
|
<label class='switch'>
|
||||||
<input type='checkbox' class='bool-input' data-row='{{ loop.index0 }}' data-field='influxdb' {% if row.influxdb %}checked{% endif %} />
|
<input type='checkbox' class='bool-input' data-field='influxdb' {% if row.influxdb %}checked{% endif %} />
|
||||||
<span class='slider'></span>
|
<span class='slider'></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="delete-btn" onclick="deleteRow(this)">🗑️</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<button class="save-btn" onclick="saveTable()">💾 Speichern</button>
|
<div class="button-group">
|
||||||
|
<button class="add-btn" onclick="addRow()">➕ Zeile hinzufügen</button>
|
||||||
|
<button class="save-btn" onclick="saveTable()">💾 Speichern</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tableId = "{{ table_id }}";
|
||||||
|
</script>
|
||||||
<script src="/static/script.js"></script>
|
<script src="/static/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
61
templates/settings.html
Normal file
61
templates/settings.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Einstellungen</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="40" rx="8" fill="#667eea"/>
|
||||||
|
<path d="M12 20L18 26L28 14" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="logo-text">Sensor Manager</span>
|
||||||
|
</div>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-link {% if active_page == 'table1' %}active{% endif %}">Tabelle 1</a>
|
||||||
|
<a href="/table/table2" class="nav-link {% if active_page == 'table2' %}active{% endif %}">Tabelle 2</a>
|
||||||
|
<a href="/table/table3" class="nav-link {% if active_page == 'table3' %}active{% endif %}">Tabelle 3</a>
|
||||||
|
<a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>⚙️ Einstellungen</h1>
|
||||||
|
<div id="message" class="message"></div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>MQTT Konfiguration</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mqtt_broker">MQTT Broker:</label>
|
||||||
|
<input type="text" id="mqtt_broker" class="text-input" value="{{ settings.mqtt_broker }}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mqtt_port">MQTT Port:</label>
|
||||||
|
<input type="text" id="mqtt_port" class="text-input" value="{{ settings.mqtt_port }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>InfluxDB Konfiguration</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="influxdb_url">InfluxDB URL:</label>
|
||||||
|
<input type="text" id="influxdb_url" class="text-input" value="{{ settings.influxdb_url }}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="influxdb_token">InfluxDB Token:</label>
|
||||||
|
<input type="password" id="influxdb_token" class="text-input" value="{{ settings.influxdb_token }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" onclick="saveSettings()">💾 Einstellungen speichern</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/settings.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user