jetzt mit Header, 3 Tabellen

This commit is contained in:
Eric Neuber 2025-11-20 20:12:25 +01:00
parent f2417ec65d
commit 220161a70a
10 changed files with 636 additions and 102 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
table_config.json
Cargo.lock
.DS_Store

52
Cargo.lock generated
View File

@ -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"

View File

@ -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
View File

@ -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": ""
}
} }
``` ```

View File

@ -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()

View File

@ -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
View 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!';
}
}

View File

@ -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%;
}
}

View File

@ -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
View 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>