Erste Version
This commit is contained in:
1 parent
7e133d55de
commit
f2417ec65d
8 files changed
+2729
-1
No files matched your search
Generated
+2100
File diff suppressed because it is too large.
Load diff
+11
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "table-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "4.4.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tera = "1.19"
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Build Stage
|
||||||
|
FROM rust:1.75 as builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy manifest files
|
||||||
|
COPY Cargo.toml ./
|
||||||
|
|
||||||
|
# Copy source code and templates
|
||||||
|
COPY src ./src
|
||||||
|
COPY templates ./templates
|
||||||
|
COPY static ./static
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# Runtime Stage
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
# Install required dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the binary from builder
|
||||||
|
COPY --from=builder /usr/src/app/target/release/table-server /app/table-server
|
||||||
|
|
||||||
|
# Copy templates and static files
|
||||||
|
COPY --from=builder /usr/src/app/templates /app/templates
|
||||||
|
COPY --from=builder /usr/src/app/static /app/static
|
||||||
|
|
||||||
|
# Create directory for config file
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set environment to use the data directory
|
||||||
|
ENV CONFIG_PATH=/app/data/table_config.json
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["/app/table-server"]
|
||||||
@@ -1,2 +1,171 @@
|
|||||||
# paramod-rust
|
# Tabellen Webserver
|
||||||
|
|
||||||
|
Ein einfacher Rust-Webserver, der eine editierbare 3x3-Tabelle bereitstellt und in einer JSON-Konfigurationsdatei persistiert.
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
table-server/
|
||||||
|
├── src/
|
||||||
|
│ └── main.rs
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html
|
||||||
|
├── static/
|
||||||
|
│ ├── style.css
|
||||||
|
│ └── script.js
|
||||||
|
├── Cargo.toml
|
||||||
|
├── Dockerfile
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
- Webbasierte Sensor-Konfigurationstabelle mit 6 Spalten
|
||||||
|
- Editierbare Textfelder (Bezeichnung, Adresse, Type, Faktor)
|
||||||
|
- Toggle-Schalter für Boolean-Werte (MQTT, InfluxDB)
|
||||||
|
- Persistierung in JSON-Datei
|
||||||
|
- Einfache REST-API
|
||||||
|
- Docker-Unterstützung
|
||||||
|
|
||||||
|
## Lokale Entwicklung
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- Rust (Version 1.75 oder höher)
|
||||||
|
- Cargo
|
||||||
|
|
||||||
|
### Installation und Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Projekt erstellen
|
||||||
|
cargo new table-server
|
||||||
|
cd table-server
|
||||||
|
|
||||||
|
# Dependencies installieren und starten
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Server läuft dann auf `http://localhost:8080`
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
### Container bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t table-server .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 -v $(pwd)/data:/app/data table-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Mit Volume-Mount bleibt die Konfigurationsdatei auch nach Container-Neustarts erhalten.
|
||||||
|
|
||||||
|
### Docker Compose (optional)
|
||||||
|
|
||||||
|
Erstelle eine `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
table-server:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Starten mit:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
1. Öffne `http://localhost:8080` im Browser
|
||||||
|
2. Bearbeite die Sensor-Konfigurationen:
|
||||||
|
- **Bezeichnung**: Name des Sensors
|
||||||
|
- **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
|
||||||
|
3. Klicke auf "Speichern" um die Änderungen zu persistieren
|
||||||
|
4. Die Daten werden in `table_config.json` gespeichert
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /` - Zeigt die HTML-Seite mit der Tabelle
|
||||||
|
- `POST /api/save` - Speichert die Tabellendaten
|
||||||
|
|
||||||
|
### Beispiel API-Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/save \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"bezeichnung": "Sensor 1",
|
||||||
|
"adresse": "192.168.1.100",
|
||||||
|
"type": "Temperatur",
|
||||||
|
"faktor": "1.0",
|
||||||
|
"mqtt": true,
|
||||||
|
"influxdb": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bezeichnung": "Sensor 2",
|
||||||
|
"adresse": "192.168.1.101",
|
||||||
|
"type": "Luftfeuchtigkeit",
|
||||||
|
"faktor": "0.5",
|
||||||
|
"mqtt": false,
|
||||||
|
"influxdb": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurationsdatei
|
||||||
|
|
||||||
|
Die Sensor-Daten werden in `table_config.json` gespeichert:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"bezeichnung": "Sensor 1",
|
||||||
|
"adresse": "192.168.1.100",
|
||||||
|
"type": "Temperatur",
|
||||||
|
"faktor": "1.0",
|
||||||
|
"mqtt": true,
|
||||||
|
"influxdb": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bezeichnung": "Sensor 2",
|
||||||
|
"adresse": "192.168.1.101",
|
||||||
|
"type": "Luftfeuchtigkeit",
|
||||||
|
"faktor": "0.5",
|
||||||
|
"mqtt": false,
|
||||||
|
"influxdb": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bezeichnung": "Sensor 3",
|
||||||
|
"adresse": "192.168.1.102",
|
||||||
|
"type": "Druck",
|
||||||
|
"faktor": "2.0",
|
||||||
|
"mqtt": true,
|
||||||
|
"influxdb": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
use actix_web::{web, App, HttpResponse, HttpServer, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct TableRow {
|
||||||
|
bezeichnung: String,
|
||||||
|
adresse: String,
|
||||||
|
r#type: String,
|
||||||
|
faktor: String,
|
||||||
|
mqtt: bool,
|
||||||
|
influxdb: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct TableData {
|
||||||
|
rows: Vec<TableRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TableData {
|
||||||
|
fn default() -> Self {
|
||||||
|
TableData {
|
||||||
|
rows: vec![
|
||||||
|
TableRow {
|
||||||
|
bezeichnung: "Sensor 1".to_string(),
|
||||||
|
adresse: "192.168.1.100".to_string(),
|
||||||
|
r#type: "Temperatur".to_string(),
|
||||||
|
faktor: "1.0".to_string(),
|
||||||
|
mqtt: true,
|
||||||
|
influxdb: false,
|
||||||
|
},
|
||||||
|
TableRow {
|
||||||
|
bezeichnung: "Sensor 2".to_string(),
|
||||||
|
adresse: "192.168.1.101".to_string(),
|
||||||
|
r#type: "Luftfeuchtigkeit".to_string(),
|
||||||
|
faktor: "0.5".to_string(),
|
||||||
|
mqtt: false,
|
||||||
|
influxdb: true,
|
||||||
|
},
|
||||||
|
TableRow {
|
||||||
|
bezeichnung: "Sensor 3".to_string(),
|
||||||
|
adresse: "192.168.1.102".to_string(),
|
||||||
|
r#type: "Druck".to_string(),
|
||||||
|
faktor: "2.0".to_string(),
|
||||||
|
mqtt: true,
|
||||||
|
influxdb: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
table: Mutex<TableData>,
|
||||||
|
config_path: String,
|
||||||
|
templates: Tera,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
fn load_or_create(config_path: &str) -> Self {
|
||||||
|
let table = match fs::read_to_string(config_path) {
|
||||||
|
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||||
|
Err(_) => {
|
||||||
|
let default = TableData::default();
|
||||||
|
let _ = fs::write(config_path, serde_json::to_string_pretty(&default).unwrap());
|
||||||
|
default
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tera = match Tera::new("templates/**/*") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Template parsing error: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AppState {
|
||||||
|
table: Mutex::new(table),
|
||||||
|
config_path: config_path.to_string(),
|
||||||
|
templates: tera,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> Result<(), std::io::Error> {
|
||||||
|
let table = self.table.lock().unwrap();
|
||||||
|
let json = serde_json::to_string_pretty(&*table)?;
|
||||||
|
fs::write(&self.config_path, json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||||
|
let table = data.table.lock().unwrap();
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("rows", &table.rows);
|
||||||
|
|
||||||
|
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 save_table(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
table_data: web::Json<TableData>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let mut table = data.table.lock().unwrap();
|
||||||
|
*table = table_data.into_inner();
|
||||||
|
drop(table);
|
||||||
|
|
||||||
|
match data.save() {
|
||||||
|
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
|
||||||
|
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let config_path = "table_config.json";
|
||||||
|
let app_state = web::Data::new(AppState::load_or_create(config_path));
|
||||||
|
|
||||||
|
println!("Server läuft auf http://0.0.0.0:8080");
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(app_state.clone())
|
||||||
|
.route("/", web::get().to(index))
|
||||||
|
.route("/api/save", web::post().to(save_table))
|
||||||
|
})
|
||||||
|
.bind("0.0.0.0:8080")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
async function saveTable() {
|
||||||
|
const rows = [];
|
||||||
|
const rowCount = document.querySelectorAll('tbody tr').length;
|
||||||
|
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const bezeichnung = document.querySelector(`input[data-row='${i}'][data-field='bezeichnung']`).value;
|
||||||
|
const adresse = document.querySelector(`input[data-row='${i}'][data-field='adresse']`).value;
|
||||||
|
const type = document.querySelector(`input[data-row='${i}'][data-field='type']`).value;
|
||||||
|
const faktor = document.querySelector(`input[data-row='${i}'][data-field='faktor']`).value;
|
||||||
|
const mqtt = document.querySelector(`input[data-row='${i}'][data-field='mqtt']`).checked;
|
||||||
|
const influxdb = document.querySelector(`input[data-row='${i}'][data-field='influxdb']`).checked;
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
bezeichnung,
|
||||||
|
adresse,
|
||||||
|
type,
|
||||||
|
faktor,
|
||||||
|
mqtt,
|
||||||
|
influxdb
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ rows: rows })
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
if (response.ok) {
|
||||||
|
messageDiv.className = 'message success';
|
||||||
|
messageDiv.textContent = '✓ Erfolgreich gespeichert!';
|
||||||
|
} else {
|
||||||
|
messageDiv.className = 'message error';
|
||||||
|
messageDiv.textContent = '✗ Fehler beim Speichern!';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
messageDiv.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
messageDiv.className = 'message error';
|
||||||
|
messageDiv.textContent = '✗ Verbindungsfehler!';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch Styles */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
margin: 30px auto 0;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sensor Konfiguration</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔧 Sensor Konfiguration</h1>
|
||||||
|
<div id="message" class="message"></div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bezeichnung</th>
|
||||||
|
<th>Adresse</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Faktor</th>
|
||||||
|
<th>MQTT</th>
|
||||||
|
<th>InfluxDB</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<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-row='{{ loop.index0 }}' 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-row='{{ loop.index0 }}' data-field='faktor' value='{{ row.faktor }}' /></td>
|
||||||
|
<td>
|
||||||
|
<label class='switch'>
|
||||||
|
<input type='checkbox' class='bool-input' data-row='{{ loop.index0 }}' data-field='mqtt' {% if row.mqtt %}checked{% endif %} />
|
||||||
|
<span class='slider'></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class='switch'>
|
||||||
|
<input type='checkbox' class='bool-input' data-row='{{ loop.index0 }}' data-field='influxdb' {% if row.influxdb %}checked{% endif %} />
|
||||||
|
<span class='slider'></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button class="save-btn" onclick="saveTable()">💾 Speichern</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user