Files
RAPPORT-SERVER-APP/src-tauri/src/firstaid.rs
T
karim e2d2fd9fa2 Initial source: RAPPORT Server-App v0.1.0
- Tauri-2-Admin-UI fuer den Rapport-Compose-Stack
- React-Frontend (JSX, kein TS) mit Material-Symbols-Icons
- Service-Cards mit Live-Stats (CPU/RAM), Logs, Restart/Stop
- Backup-/Restore-System mit pg_dumpall + Retention
- Container-Auto-Updates mit Pre-Backup
- App-Auto-Updater (Tauri signiert) gegen latest.json im Repo-Root
- HTTPS-WebUI (axum/rustls) mit Basic-Auth, CSRF, Rate-Limit, Security-Headers
- Setup-Wizard: lädt Docker+Colima+Lima direct von GitHub/docker.com nach ~/.rapport/bin/
- Tray-Modus + macOS-Notifications + Auto-Recovery
- Login-Item via tauri-plugin-autostart
2026-05-24 17:03:50 +02:00

178 lines
5.7 KiB
Rust

//! Erste-Hilfe-Aktionen fuer den Notfall.
//!
//! Drei destruktive Aktionen mit unterschiedlichem Risiko:
//! - `recreate_containers` (mild): compose down + up --force-recreate.
//! Container neu, Volumes bleiben → Daten safe.
//! - `reset_pgdata` (NUKE): compose down -v + up. Volumes WEG → Postgres
//! wird von Grund auf neu initialisiert. Erfordert Pre-Backup.
//! - `diagnose_bundle`: kein Schaden — sammelt Logs, ps, version, config
//! (redacted) in eine Text-Datei unter backups/.
use crate::{backup::BackupInfo, paths, services};
use chrono::Local;
use serde::Serialize;
use tokio::process::Command;
#[derive(Debug, Clone, Serialize)]
pub struct RecreateResult {
pub log: String,
pub finished_at_iso: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ResetResult {
pub safety_backup: BackupInfo,
pub log: String,
pub finished_at_iso: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnoseResult {
pub filename: String,
pub bytes: u64,
}
async fn compose(args: &[&str]) -> Result<String, String> {
let out = Command::new("docker")
.current_dir(services::compose_dir())
.arg("compose")
.args(args)
.output()
.await
.map_err(|e| format!("spawn: {e}"))?;
let combined = format!(
"{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
if !out.status.success() {
return Err(format!("compose {args:?} failed: {combined}"));
}
Ok(combined)
}
/// Container neu erstellen — Volumes bleiben.
pub async fn recreate_containers() -> Result<RecreateResult, String> {
crate::events::warn("Erste Hilfe: Container neu erstellen ...").await;
let _ = compose(&["down"]).await;
let log = compose(&["up", "-d", "--force-recreate"]).await?;
crate::events::info("Erste Hilfe: Container neu erstellt").await;
Ok(RecreateResult {
log,
finished_at_iso: Local::now().to_rfc3339(),
})
}
/// VOLL DESTRUKTIV: Volumes platt + frisches Init.
pub async fn reset_pgdata() -> Result<ResetResult, String> {
crate::events::warn("Erste Hilfe: PGDATA-RESET — erst Safety-Backup").await;
let safety = crate::backup::create()
.await
.map_err(|e| format!("Safety-Backup vor PGDATA-Reset fehlgeschlagen — Reset abgebrochen: {e}"))?;
crate::events::warn(format!(
"Erste Hilfe: stoppe Stack inkl. Volumes (Safety: {})",
safety.filename
))
.await;
let _ = compose(&["down", "-v"]).await?;
let log = compose(&["up", "-d"]).await?;
crate::events::error(format!(
"Erste Hilfe: PGDATA platt gemacht und neu initialisiert (Safety: {})",
safety.filename
))
.await;
Ok(ResetResult {
safety_backup: safety,
log,
finished_at_iso: Local::now().to_rfc3339(),
})
}
/// Sammelt Diagnose-Informationen in eine Text-Datei unter backups/.
pub async fn diagnose_bundle() -> Result<DiagnoseResult, String> {
paths::ensure_dirs().map_err(|e| format!("ensure_dirs: {e}"))?;
let stamp = Local::now().format("%Y%m%d-%H%M%S").to_string();
let filename = format!("diagnose-{stamp}.txt");
let path = paths::backups_dir().join(&filename);
let mut out = String::new();
out.push_str(&format!("# RAPPORT Server-App Diagnose ({})\n\n", Local::now().to_rfc3339()));
out.push_str("## docker version\n```\n");
out.push_str(&run_capture(&["docker", "version"]).await);
out.push_str("\n```\n\n");
out.push_str("## docker compose ps -a\n```\n");
out.push_str(&run_capture(&["docker", "compose", "ps", "-a"]).await);
out.push_str("\n```\n\n");
out.push_str("## docker system df\n```\n");
out.push_str(&run_capture(&["docker", "system", "df"]).await);
out.push_str("\n```\n\n");
out.push_str("## config.env (redacted)\n```\n");
out.push_str(&redacted_config());
out.push_str("\n```\n\n");
out.push_str("## App-Events (letzte 100)\n```\n");
for e in crate::events::list().await {
out.push_str(&format!("[{}] {}: {}\n", e.ts_iso, e.kind, e.message));
}
out.push_str("\n```\n\n");
out.push_str("## Container-Logs (letzte 50 Zeilen pro Service)\n");
for svc in crate::services::default_services() {
out.push_str(&format!("\n### {}\n```\n", svc.id));
out.push_str(&run_capture(&[
"docker", "compose", "logs", "--no-color", "--tail", "50", &svc.id,
]).await);
out.push_str("\n```\n");
}
std::fs::write(&path, out.as_bytes()).map_err(|e| format!("write {}: {e}", path.display()))?;
let bytes = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
crate::events::info(format!("Diagnose-Bundle: {filename} ({} KB)", bytes / 1024)).await;
Ok(DiagnoseResult { filename, bytes })
}
async fn run_capture(argv: &[&str]) -> String {
let mut cmd = Command::new(argv[0]);
cmd.current_dir(services::compose_dir());
for a in &argv[1..] {
cmd.arg(a);
}
match cmd.output().await {
Ok(o) => format!(
"{}{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
),
Err(e) => format!("(spawn fehlgeschlagen: {e})"),
}
}
const REDACT_PREFIXES: &[&str] = &[
"POSTGRES_PASSWORD",
"JWT_SECRET",
"ADMIN_UI_PASSWORD",
"ANON_KEY",
"SERVICE_ROLE_KEY",
"SMTP_PASS",
];
fn redacted_config() -> String {
let map = crate::config::load().unwrap_or_default();
let mut out = String::new();
for (k, v) in &map {
let redacted = REDACT_PREFIXES.iter().any(|p| k.contains(p));
if redacted {
out.push_str(&format!("{k}=<REDACTED {} chars>\n", v.len()));
} else {
out.push_str(&format!("{k}={v}\n"));
}
}
out
}