Erste Version
This commit is contained in:
parent
7e133d55de
commit
f2417ec65d
2100
Cargo.lock
generated
Normal file
2100
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@ -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
Dockerfile
Normal file
44
Dockerfile
Normal file
@ -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"]
|
||||
171
README.md
171
README.md
@ -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
src/main.rs
Normal file
139
src/main.rs
Normal file
@ -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
|
||||
}
|
||||
49
static/script.js
Normal file
49
static/script.js
Normal file
@ -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!';
|
||||
}
|
||||
}
|
||||
162
static/style.css
Normal file
162
static/style.css
Normal file
@ -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;
|
||||
}
|
||||
54
templates/index.html
Normal file
54
templates/index.html
Normal file
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user