This commit is contained in:
Daniel Nagel 2026-09-08 19:40:48 +02:00
parent dcb1307bb5
commit 1cdcd0581c
4 changed files with 121 additions and 27 deletions

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# Homelab Watchdog
Überwacht Server, Container und Netzwerkgeräte im Homelab.
Eine Liste von Hosts wird zyklisch geprüft, nicht erreichbare
dienste werden rot markiert und in MariaDB geloggt.
## Architektur: Hybrid Python + Rust
Nach Evaluation wurde eine hybride Architektur gewählt, um
die steile Lernkurve von Rust (async, TUI, DB-Handling) zu
vermeiden und trotzdem Rust zu lernen.
| Layer | Sprache | Aufgabe |
|-------|---------|---------|
| **Manager** | Python | TUI (`textual`/`rich`), Timer, Config, MariaDB, ruft Worker auf |
| **Worker** | Rust | Kleine, synchron CLI-Binaries (z.B. `ping_worker`). IP als Arg, JSON auf stdout. |
| **Datenbank** | MariaDB | Zentrale Wahrheit für Status, Fehler, Zeitstempel |
### Datenfluss
1. Python Manager liest Host-Liste (JSON/DB).
2. Alle X Minuten: `subprocess.run(["./ping_worker", "<ip>"])` pro Host.
3. Rust Worker pingt synchron, gibt `{"address":"...","reachable":true,"rtt_ms":12}` aus.
4. Python parst JSON, schreibt in MariaDB, aktualisiert TUI.
## Projektstruktur (geplant)

View File

@ -11,4 +11,20 @@ pub(crate) struct Hosts {
pub(crate) address: String,
pub(crate) last_checked: Option<tokio::time::Instant>,
pub(crate) state_since: tokio::time::Instant,
}
pub(crate) fn generate_gen_hosts() {
let google = Hosts {
name:String::from("Google"),
state: HostState::Offline,
address:String :: from("8.8.8.8"),
last_checked: None,
state_since: tokio::time::Instant::now(),
};
let router = Hosts {
name:String::from("Unify Router"),
state: HostState::Offline,
address:String :: from("192.168.0.1"),
last_checked: None,
state_since: tokio::time::Instant::now(),};
}

View File

@ -1,38 +1,25 @@
mod host;
use tokio::time::{sleep, Duration};
use host::{Hosts, HostState};
use host::{Hosts, HostState, generate_gen_hosts};
use tokio::process::Command;
async fn example() {
let time =5;
println!("Time .. On");
sleep(Duration::from_secs(time)).await;
async fn ping_once(host: &Hosts) -> String {
let output = Command::new("ping")
.args(["-c", "1", &host.address])
.output()
.await;
// Transform bytes from output to string
let text_output = String::from_utf8_lossy(&output.output()).to_string();
return text_output
}
#[tokio::main]
async fn main() {
let google = Hosts {
name:String::from("Google"),
state: HostState::Offline,
address:String :: from("8.8.8.8"),
last_checked: None,
state_since: tokio::time::Instant::now(),
};
let router = Hosts {
name:String::from("Unify Router"),
state: HostState::Offline,
address:String :: from("192.168.0.1"),
last_checked: None,
state_since: tokio::time::Instant::now(),
};
let pve1 = Hosts {
name:String::from("pve Server 1"),
state: HostState::Offline,
address:String :: from("192.168.0.231"),
last_checked: None,
state_since: tokio::time::Instant::now(),
};
let list = vec![google, router, pve1];
generate_gen_hosts();
let list = vec![host.google, host.router];
println!("{:?}", list);
example().await;

65
system_compress.txt Normal file
View File

@ -0,0 +1,65 @@
=== KONTEXT KOMPENDIUM ===
Nutzer: Daniel, SPS/Industrie-Hintergrund, KOP/SCL, Python, C#, JS, Rust-Anfänger.
Stil: Step-by-Step, max 1-2 Schritte pro Antwort. Erklärungen mit SPS-Analogien.
Kein fertiger Code als Lösung, nur Mini-Hinweise/Puzzleteile.
Nutzer darf selbst puzzeln. Bei Frage "Lösung bitte" -> Lösung geben.
=== ARCHITEKTUR (Hybrid) ===
Manager: Python (TUI, Timer, MariaDB, Config).
Worker: Rust (sync CLI, single-purpose, JSON stdout).
Kommunikation: subprocess.run(["./binary", "<ip>"], capture_output=True).
KEIN async/TUI in Rust. KEIN PyO3/maturin. KEINE Rust-DLLs für Python.
=== AKTUELLES PROJEKT: ping_worker ===
Separates Cargo-Projekt.
Aufgabe: IP aus args -> ping -> JSON stdout.
Status: Projekt neu, Cargo.toml steht, src/main.rs leer/Grundgerüst.
Nächster Schritt: Station 1: env::args() parsen, IP ausgeben, Usage bei fehlendem Arg.
=== TECH STACK (ping_worker) ===
[package] edition="2021" oder "2024" (Nutzer hat 2024, stable seit 1.85)
[dependencies]
serde = { version="1", features=["derive"] }
serde_json = "1"
KEIN tokio. KEIN anyhow. KEIN ratatui. KEIN crossterm.
=== WISSENSSTAND NUTZER (Rust) ===
- Cargo.toml: dependencies, features (hat tokio full gemeistert).
- struct, enum, Vec, #[derive(Debug)], Option<T>.
- std::process::Command (Output, .status.success(), .stdout als Vec<u8>).
- String::from_utf8_lossy -> Cow -> .to_string().
- match / if let / ? Operator (Theorie, teilweise Praxis).
- tokio::time::Instant (gelöscht aus aktuellem Scope).
- env::args() (neu, aktuelle Station).
- sync vs async Unterschied bekannt (blockierend vs Scheduler).
=== OFFENE PUNKTE / LEHRPLAN ===
1. ping_worker: args parsen.
2. ping_worker: Command::new("ping").args([...]).output() (sync!).
3. ping_worker: Struct mit Serialize, JSON stdout.
4. Python Manager: pymysql, textual/rich, Timer-Loop.
5. MariaDB Schema: host_status (id, name, address, reachable, checked_at, error_msg).
6. Integration Python <-> Rust.
=== DOs & DONTs ===
DO: Kleine Schritte. DO: SPS-Analogien (DB=Vec/Struct, OB1=main loop, Baugruppe=Crate).
DO: Einfache Vokabeln erklären (Ownership, Borrow, Lifetime, Result).
DO: Bei Fehlermeldungen: Fehler 1,2,3 klar trennen, nicht alles auf einmal.
DONT: Keine 5 Schritte auf einmal generieren.
DONT: Kein komplexes async/await/TUI für diesen Worker.
DONT: Kein "das ist einfach" sagen. Ehrlich über Rust-Steilheit.
DONT: Kein globaler State in Rust (static/Mutex) für diesen Worker.
=== DATEISTRUKTUR (aktuell) ===
homelab-watchdog/
├── HomelabWatchdog/ (altes Projekt, mit tokio/ratatui, ruht)
└── ping_worker/ (aktiv)
├── Cargo.toml
├── src/main.rs
├── ai_chat_help.txt
└── README.md
=== VERSIONS-HINWEISSE ===
Rust Edition 2024 ist stable (Rust >= 1.85).
tokio features = ["full"] nur wenn tokio verwendet wird (nicht hier).
serde features = ["derive"] zwingend für #[derive(Serialize)].