Auto-Sync: 2026-03-08 07:59
This commit is contained in:
parent
4267cbf0c2
commit
96b267174b
4 changed files with 498 additions and 57 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# Arakava News — Live State
|
||||
> Auto-generiert: 2026-03-08 07:45
|
||||
> Auto-generiert: 2026-03-08 07:59
|
||||
|
||||
## Service Status
|
||||
| Service | CT | Status |
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
| WordPress Docker | 101 | running |
|
||||
|
||||
## Letzte Feed-Aktivität (Top 5)
|
||||
Heise Online: 2026-03-08 06:08:26
|
||||
Heise Online: 2026-03-08 06:08:26
|
||||
Rubikon.news: 2026-03-08 06:08:26
|
||||
Golem.de: 2026-03-08 05:08:34
|
||||
Heise Security: 2026-03-08 03:08:38
|
||||
|
|
@ -17,10 +17,10 @@
|
|||
## Fehler (letzte 24h)
|
||||
- Fehler gesamt: 0
|
||||
0
|
||||
- Letzter Fehler:
|
||||
- Letzter Fehler: keine
|
||||
|
||||
## OpenRouter Guthaben
|
||||
$5.09 verbleibend
|
||||
verbleibend
|
||||
|
||||
## URLs
|
||||
- Blog: https://arakavanews.com
|
||||
|
|
@ -40,25 +40,10 @@ $5.09 verbleibend
|
|||
| 600 | WordPress Mirror (Redundanz CT 101) | 100.92.205.101 |
|
||||
| 601 | RSS Manager Mirror (Redundanz CT 109) | — |
|
||||
|
||||
## Aktive Feeds (17)
|
||||
## Aktive Feeds
|
||||
| ID | Name | Schedule |
|
||||
|---|---|---|
|
||||
| 1 | Dr. Bines Substack | 08/14/20 Uhr |
|
||||
| 3 | NachDenkSeiten | 07/13/19 Uhr |
|
||||
| 4 | Tichys Einblick | 07:30/13:30/19:30 |
|
||||
| 5 | Junge Freiheit | 08/14/20 Uhr |
|
||||
| 6 | PAZ | 08:30/14:30/20:30 |
|
||||
| 7 | Apollo News | 09/15/21 Uhr |
|
||||
| 8 | Apolut | 09:30/15:30/21:30 |
|
||||
| 9 | Achgut.com | 10/16/22 Uhr |
|
||||
| 10 | Heise Security | alle 4h |
|
||||
| 11 | Golem.de | alle 2h |
|
||||
| 12 | Heise Online | alle 3h |
|
||||
| 13 | Rubikon.news | alle 3h |
|
||||
| 14 | Corona-Transition | alle 4h |
|
||||
| 15 | Photon.info (KI-Analyse) | alle 6h |
|
||||
| 16 | Antispiegel | 08:30/14:30/20:30 |
|
||||
| 17 | Riehle News | 09:00 Uhr |
|
||||
| — | (nicht abrufbar) | — |
|
||||
|
||||
## Code (CT 109: /opt/rss-manager/)
|
||||
poster.py, scheduler.py, app.py, db.py
|
||||
|
|
@ -70,4 +55,3 @@ poster.py, scheduler.py, app.py, db.py
|
|||
- 24.02.2026: Telegram auf HTML-Modus (Sonderzeichen-Fix)
|
||||
- 24.02.2026: Werbeartikel-Blacklist (Anzeige:, Sponsored, etc.)
|
||||
- 23.02.2026: Matomo von CT 113 → CT 109 migriert
|
||||
- 23.02.2026: CT 100/102/104/105/106/113 gelöscht
|
||||
|
|
|
|||
|
|
@ -1,62 +1,59 @@
|
|||
# Infrastruktur — Live State
|
||||
> Auto-generiert: 2026-03-08 07:45
|
||||
> Auto-generiert: 2026-03-08 07:59
|
||||
|
||||
## pve-hetzner Disk
|
||||
| Mount | Belegt |
|
||||
|---|---|
|
||||
| / (root) | 12% von 98G |
|
||||
| /var/lib/vz (VMs/CTs) | 3% von 2.9T |
|
||||
| / (root) | 83G 12% |
|
||||
| /var/lib/vz (VMs/CTs) | 2.7T 3% |
|
||||
|
||||
## Aktive Container auf pve-hetzner
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 101 | wordpress-v2 | 100.91.212.19 | WordPress + MySQL (Docker) |
|
||||
| 103 | seafile | 100.75.247.60 | Seafile (seafile.orbitalo.net) |
|
||||
| 103 | seafile | 100.75.247.60 | Seafile |
|
||||
| 109 | rss-manager | 100.113.244.101 | RSS Manager + Matomo |
|
||||
| 110 | portainer | 100.109.206.43 | Portainer Docker UI |
|
||||
| 111 | forgejo | 100.89.246.60 | Forgejo Git (http://100.89.246.60:3000) |
|
||||
| 111 | forgejo | 100.89.246.60 | Forgejo Git |
|
||||
| 112 | fuenfvoracht | 100.73.171.62 | FuenfVorAcht Telegram Bot |
|
||||
| 113 | redax-wp | 100.69.243.16 | Redakteur WordPress KI-Autor |
|
||||
| 115 | flugscanner-hub | 100.92.161.97 | Flugpreisscanner Hub |
|
||||
| 116 | homelab-ai-bot | 100.123.47.7 | Hausmeister Telegram Bot |
|
||||
| 144 | muldenstein-backup | — | Backup-Archiv |
|
||||
| 999 | cluster-docu | 100.79.8.49 | Dokumentation (http://100.79.8.49:8080) |
|
||||
|
||||
## Gelöschte Container (24.02.2026)
|
||||
| CT | Name | Grund |
|
||||
|---|---|---|
|
||||
| 100 | traefik | Abgelöst durch Cloudflare Tunnel |
|
||||
| 102 | dify | Experiment fehlgeschlagen |
|
||||
| 104 | n8n | Nicht aktiv genutzt |
|
||||
| 105 | debian-12 | Nicht genutzt |
|
||||
| 106 | wordpress-news | Abgelöst durch CT 101 |
|
||||
| 113 | matomo | Integriert in CT 109 |
|
||||
| 999 | cluster-docu | 100.79.8.49 | Dokumentation |
|
||||
|
||||
## Container auf pve1 (Kambodscha)
|
||||
| CT | Name | Dienste |
|
||||
|---|---|---|
|
||||
| 136 | gold-silber-v3 | Edelmetall-Bot (Tailscale: 100.72.230.87) |
|
||||
| 143 | smart-home | ioBroker + Grafana + InfluxDB |
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 136 | gold-silber-v3 | 100.72.230.87 | Edelmetall-Bot |
|
||||
| 143 | smart-home | — | ioBroker + Grafana + InfluxDB |
|
||||
|
||||
## Container auf pve3 (Muldenstein)
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 139 | Syncthing-Muldenstein | — | Syncthing |
|
||||
| 141 | syncthing | — | Syncthing |
|
||||
| 142 | WG-easy | — | WireGuard |
|
||||
| 143 | Raspi-Broker | — | MQTT Broker |
|
||||
| 145 | flugscanner-mu | — | Flugpreisscanner Node |
|
||||
| 504 | projektscan-template | — | Projektscan |
|
||||
| 600 | wp-mirror | 100.92.205.101 | WordPress Mirror (Redundanz CT 101) |
|
||||
| 601 | rss-mirror | — | RSS Manager Mirror (Redundanz CT 109) |
|
||||
| 145 | flugscanner-mu | — | Flugpreisscanner Node |
|
||||
|
||||
## Routing
|
||||
- Cloudflare Tunnel CT 101: arakavanews.com → :80
|
||||
- Cloudflare Tunnel CT 101: arakava-news-2.orbitalo.net → 301 → arakavanews.com
|
||||
- Cloudflare Tunnel CT 109: matomo.orbitalo.net → :80
|
||||
- Cloudflare Tunnel CT 600: Standby (WordPress Mirror)
|
||||
- Cloudflare Tunnel CT 601: Standby (RSS Manager Mirror)
|
||||
- Kein Traefik, kein PBS-Gateway mehr
|
||||
## Routing (Cloudflare Tunnels)
|
||||
- CT 101: arakavanews.com → :80 (aktiv)
|
||||
- CT 101: arakava-news-2.orbitalo.net → 301→arakavanews.com (aktiv)
|
||||
- CT 109: matomo.orbitalo.net → :80 (aktiv)
|
||||
- CT 600: arakavanews.com → :80 (Standby)
|
||||
- CT 601: rss-manager → :8080 (Standby)
|
||||
|
||||
## Zugangsdaten
|
||||
- pve-hetzner: root / Astral-Proxmox!2026
|
||||
- pve1: root / astral66
|
||||
- Alle CTs: root / astral66
|
||||
- Seafile: admin@orbitalo.net / astral66
|
||||
- Forgejo: orbitalo / astral66
|
||||
|
||||
## Telegram Bots
|
||||
| Bot | Token (Auszug) | Chat-ID |
|
||||
|---|---|---|
|
||||
| Mutter (@MutterbotAI_bot) | 8551565940:... | 674951792 |
|
||||
| Bot | Zweck |
|
||||
|---|---|
|
||||
| @MutterbotAI_bot | Watchdog-Alerts |
|
||||
| @Orbitalo_Hausmeister_bot | Homelab AI-Bot |
|
||||
|
|
|
|||
460
scripts/sync_state.py
Executable file
460
scripts/sync_state.py
Executable file
|
|
@ -0,0 +1,460 @@
|
|||
#!/usr/bin/env python3
|
||||
"""sync_state.py — Ersetzt sync-state.sh mit Core-Modules.
|
||||
|
||||
Läuft alle 15 Min auf pve-hetzner via Cron.
|
||||
- Generiert STATE.md Dateien aus homelab.conf + Live-Daten
|
||||
- Service-Watchdog mit Telegram-Alerts
|
||||
- Git Commit & Push nach Forgejo
|
||||
|
||||
Nutzt dieselben Core-Module wie MCP-Server und Telegram-Bot.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("PATH", "/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/bin:/sbin:/bin")
|
||||
|
||||
CORE_PATH = Path("/root/homelab-mcp/core")
|
||||
sys.path.insert(0, str(CORE_PATH.parent))
|
||||
|
||||
from core import config
|
||||
|
||||
REPO = Path("/opt/homelab-brain")
|
||||
DEBOUNCE_DIR = Path("/tmp/homelab_watchdog")
|
||||
DEBOUNCE_DIR.mkdir(exist_ok=True)
|
||||
|
||||
NOW = datetime.now()
|
||||
DATE = NOW.strftime("%Y-%m-%d %H:%M")
|
||||
CHANGED = False
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
|
||||
# ── Telegram ──────────────────────────────────────────
|
||||
|
||||
def tg_alert(cfg: config.HomelabConfig, key: str, msg: str):
|
||||
token = cfg.telegram.get("tg_mutter_token", "")
|
||||
chat_id = cfg.raw.get("TG_CHAT_ID", "")
|
||||
if not token or not chat_id:
|
||||
return
|
||||
|
||||
lockfile = DEBOUNCE_DIR / f"{key}.lock"
|
||||
now = int(time.time())
|
||||
if lockfile.exists():
|
||||
try:
|
||||
last = int(lockfile.read_text().strip())
|
||||
if now - last < 600:
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
subprocess.run([
|
||||
"curl", "-s", "-X", "POST",
|
||||
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||
"-d", f"chat_id={chat_id}",
|
||||
"-d", f"text=Homelab Watchdog%0A%0A{msg}",
|
||||
"-d", "parse_mode=Markdown",
|
||||
], capture_output=True, timeout=10)
|
||||
lockfile.write_text(str(now))
|
||||
log(f"Alert gesendet: {key}")
|
||||
|
||||
|
||||
def tg_recovery(cfg: config.HomelabConfig, key: str, name: str):
|
||||
token = cfg.telegram.get("tg_mutter_token", "")
|
||||
chat_id = cfg.raw.get("TG_CHAT_ID", "")
|
||||
lockfile = DEBOUNCE_DIR / f"{key}.lock"
|
||||
|
||||
if lockfile.exists() and token and chat_id:
|
||||
subprocess.run([
|
||||
"curl", "-s", "-X", "POST",
|
||||
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||
"-d", f"chat_id={chat_id}",
|
||||
"-d", f"text=*{name}* wieder online",
|
||||
"-d", "parse_mode=Markdown",
|
||||
], capture_output=True, timeout=10)
|
||||
lockfile.unlink(missing_ok=True)
|
||||
log(f"Recovery: {name} wieder online")
|
||||
|
||||
|
||||
# ── Service Checks (pct exec) ────────────────────────
|
||||
|
||||
def pct_exec(ct: int, cmd: str, timeout: int = 15) -> str:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["pct", "exec", str(ct), "--", "bash", "-c", cmd],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
return r.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, Exception):
|
||||
return ""
|
||||
|
||||
|
||||
def check_service(cfg: config.HomelabConfig, ct: int, service: str, name: str) -> str:
|
||||
status = "unknown"
|
||||
for attempt in range(3):
|
||||
result = pct_exec(ct, f"systemctl is-active {service}")
|
||||
if result == "active":
|
||||
status = "active"
|
||||
break
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
else:
|
||||
status = result or "unknown"
|
||||
|
||||
if status != "active":
|
||||
tg_alert(cfg, f"service_{service}",
|
||||
f"*{name}* ist DOWN%0AService: {service}%0ACT: {ct}%0AStatus: {status}")
|
||||
return "DOWN"
|
||||
|
||||
tg_recovery(cfg, f"service_{service}", name)
|
||||
return "active"
|
||||
|
||||
|
||||
def check_docker(cfg: config.HomelabConfig, ct: int, container: str, name: str) -> str:
|
||||
status = "unknown"
|
||||
for attempt in range(3):
|
||||
result = pct_exec(ct, f"docker inspect --format='{{{{.State.Status}}}}' {container}")
|
||||
if result == "running":
|
||||
status = "running"
|
||||
break
|
||||
if attempt < 2:
|
||||
time.sleep(2)
|
||||
else:
|
||||
status = result or "unknown"
|
||||
|
||||
if status != "running":
|
||||
tg_alert(cfg, container.replace("-", "_"),
|
||||
f"*{name}* ist DOWN%0AStatus: {status}%0ACT: {ct}")
|
||||
else:
|
||||
tg_recovery(cfg, container.replace("-", "_"), name)
|
||||
return status
|
||||
|
||||
|
||||
# ── STATE.md Generatoren ─────────────────────────────
|
||||
|
||||
def generate_arakava_state(cfg: config.HomelabConfig) -> str:
|
||||
log("Sammle Arakava News Status...")
|
||||
|
||||
rss_status = pct_exec(109, "systemctl is-active rss-manager") or "unknown"
|
||||
wp_status = pct_exec(101, "docker inspect --format='{{.State.Status}}' wordpress-app") or "unknown"
|
||||
|
||||
feed_cmd = (
|
||||
"python3 -c \""
|
||||
"import sqlite3;"
|
||||
" db = sqlite3.connect('/opt/rss-manager/rss_manager.db');"
|
||||
" rows = db.execute('SELECT name, last_run FROM feeds WHERE enabled=1 ORDER BY last_run DESC LIMIT 5').fetchall();"
|
||||
" [print(f' {r[0]}: {r[1] or chr(110)+chr(105)+chr(101)}') for r in rows]"
|
||||
"\""
|
||||
)
|
||||
feed_activity = pct_exec(109, feed_cmd) or " (nicht abrufbar)"
|
||||
|
||||
or_key = cfg.api_keys.get("openrouter_key", "")
|
||||
or_cmd = (
|
||||
"python3 -c \""
|
||||
"import requests\n"
|
||||
"try:\n"
|
||||
" r = requests.get('https://openrouter.ai/api/v1/auth/key',"
|
||||
" headers={'Authorization': 'Bearer " + or_key + "'}, timeout=5)\n"
|
||||
" d = r.json().get('data', {})\n"
|
||||
" remaining = float(d.get('limit', 20)) - float(d.get('usage', 0))\n"
|
||||
" print(f'${remaining:.2f} verbleibend')\n" # noqa: not an f-string, goes to shell
|
||||
"except Exception as e:\n"
|
||||
" print(f'(nicht abrufbar: {e})')\n"
|
||||
"\""
|
||||
)
|
||||
or_balance = pct_exec(109, or_cmd) or "(nicht abrufbar)"
|
||||
|
||||
errors = pct_exec(109, "grep -c 'ERROR' /opt/rss-manager/logs/service.log 2>/dev/null || echo 0") or "0"
|
||||
last_error = pct_exec(109, "grep 'ERROR' /opt/rss-manager/logs/service.log 2>/dev/null | tail -1 || echo 'keine'") or "keine"
|
||||
|
||||
ct_101 = config.get_container(cfg, vmid=101)
|
||||
ct_109 = config.get_container(cfg, vmid=109)
|
||||
ct_600 = config.get_container(cfg, vmid=600)
|
||||
ct_601 = config.get_container(cfg, vmid=601)
|
||||
|
||||
rss_url = f"http://{ct_109.tailscale_ip}:8080" if ct_109 and ct_109.tailscale_ip else "—"
|
||||
matomo_url = f"https://{cfg.domains.get('matomo', '')}"
|
||||
blog_url = f"https://{cfg.domains.get('primary', '')}"
|
||||
admin_url = f"{blog_url}/wp-admin"
|
||||
pw_admin = cfg.passwords.get("wp_admin", "?")
|
||||
pw_default = cfg.passwords.get("default", "?")
|
||||
|
||||
feed_table_cmd = (
|
||||
"python3 -c \""
|
||||
"import sqlite3;"
|
||||
" db = sqlite3.connect('/opt/rss-manager/rss_manager.db');"
|
||||
" rows = db.execute('SELECT id, name, schedule FROM feeds WHERE enabled=1 ORDER BY id').fetchall();"
|
||||
" [print(f'| {r[0]} | {r[1]} | {r[2]} |') for r in rows]"
|
||||
"\""
|
||||
)
|
||||
feed_table = pct_exec(109, feed_table_cmd) or "| — | (nicht abrufbar) | — |"
|
||||
|
||||
def ct_row(ct, extra=""):
|
||||
if not ct:
|
||||
return "| ? | ? | ? | ? |"
|
||||
s = extra or ct.services
|
||||
return f"| {ct.vmid} | {s} | {ct.tailscale_ip or '—'} |"
|
||||
|
||||
return f"""# Arakava News — Live State
|
||||
> Auto-generiert: {DATE}
|
||||
|
||||
## Service Status
|
||||
| Service | CT | Status |
|
||||
|---|---|---|
|
||||
| rss-manager | 109 | {rss_status} |
|
||||
| WordPress Docker | 101 | {wp_status} |
|
||||
|
||||
## Letzte Feed-Aktivität (Top 5)
|
||||
{feed_activity}
|
||||
|
||||
## Fehler (letzte 24h)
|
||||
- Fehler gesamt: {errors}
|
||||
- Letzter Fehler: {last_error}
|
||||
|
||||
## OpenRouter Guthaben
|
||||
{or_balance}
|
||||
|
||||
## URLs
|
||||
- Blog: {blog_url}
|
||||
- Admin: {admin_url} (admin / {pw_admin})
|
||||
- RSS Manager: {rss_url} (admin / {pw_default})
|
||||
- Matomo: {matomo_url} (admin / {pw_default})
|
||||
|
||||
## Container (Primary — pve-hetzner)
|
||||
| CT | Dienst | Tailscale |
|
||||
|---|---|---|
|
||||
{ct_row(ct_101)}
|
||||
{ct_row(ct_109)}
|
||||
|
||||
## Container (Mirror — pve3 Muldenstein)
|
||||
| CT | Dienst | Tailscale |
|
||||
|---|---|---|
|
||||
{ct_row(ct_600)}
|
||||
{ct_row(ct_601)}
|
||||
|
||||
## Aktive Feeds
|
||||
| ID | Name | Schedule |
|
||||
|---|---|---|
|
||||
{feed_table}
|
||||
|
||||
## Code (CT 109: /opt/rss-manager/)
|
||||
poster.py, scheduler.py, app.py, db.py
|
||||
|
||||
## Änderungshistorie
|
||||
- 08.03.2026: Domain arakavanews.com live, Mirror CT 600/601 auf pve3
|
||||
- 08.03.2026: homelab.conf als zentrale Quelle der Wahrheit
|
||||
- 24.02.2026: Scheduler Lock gegen Doppelstarts
|
||||
- 24.02.2026: Telegram auf HTML-Modus (Sonderzeichen-Fix)
|
||||
- 24.02.2026: Werbeartikel-Blacklist (Anzeige:, Sponsored, etc.)
|
||||
- 23.02.2026: Matomo von CT 113 → CT 109 migriert
|
||||
"""
|
||||
|
||||
|
||||
def generate_infra_state(cfg: config.HomelabConfig) -> str:
|
||||
log("Sammle Infrastruktur Status...")
|
||||
|
||||
disk_root = subprocess.run(
|
||||
["df", "-h", "/"], capture_output=True, text=True
|
||||
).stdout.strip().split("\n")
|
||||
disk_root_info = " ".join(disk_root[-1].split()[3:5]) if len(disk_root) > 1 else "n/a"
|
||||
|
||||
disk_data = "n/a"
|
||||
r = subprocess.run(["df", "-h", "/var/lib/vz"], capture_output=True, text=True)
|
||||
if r.returncode == 0:
|
||||
parts = r.stdout.strip().split("\n")
|
||||
if len(parts) > 1:
|
||||
disk_data = " ".join(parts[-1].split()[3:5])
|
||||
|
||||
hetzner_cts = [c for c in cfg.containers if c.host == "pve-hetzner"]
|
||||
pve1_cts = [c for c in cfg.containers if c.host == "pve1"]
|
||||
pve3_cts = [c for c in cfg.containers if c.host == "pve3"]
|
||||
|
||||
def ct_table(cts, cols=("CT", "Name", "Tailscale IP", "Dienste")):
|
||||
header = "| " + " | ".join(cols) + " |"
|
||||
sep = "|" + "|".join(["---"] * len(cols)) + "|"
|
||||
rows = []
|
||||
for c in sorted(cts, key=lambda x: x.vmid):
|
||||
ts = c.tailscale_ip or "—"
|
||||
rows.append(f"| {c.vmid} | {c.name} | {ts} | {c.services} |")
|
||||
return f"{header}\n{sep}\n" + "\n".join(rows) if rows else "(keine)"
|
||||
|
||||
tunnel_lines = []
|
||||
for t in cfg.tunnels:
|
||||
status_label = "Standby" if t.status == "standby" else "aktiv"
|
||||
tunnel_lines.append(f"- CT {t.ct_id}: {t.domain} → {t.target} ({status_label})")
|
||||
tunnel_text = "\n".join(tunnel_lines) if tunnel_lines else "- keine"
|
||||
|
||||
pw_hetzner = cfg.passwords.get("hetzner", "?")
|
||||
pw_default = cfg.passwords.get("default", "?")
|
||||
|
||||
return f"""# Infrastruktur — Live State
|
||||
> Auto-generiert: {DATE}
|
||||
|
||||
## pve-hetzner Disk
|
||||
| Mount | Belegt |
|
||||
|---|---|
|
||||
| / (root) | {disk_root_info} |
|
||||
| /var/lib/vz (VMs/CTs) | {disk_data} |
|
||||
|
||||
## Aktive Container auf pve-hetzner
|
||||
{ct_table(hetzner_cts)}
|
||||
|
||||
## Container auf pve1 (Kambodscha)
|
||||
{ct_table(pve1_cts)}
|
||||
|
||||
## Container auf pve3 (Muldenstein)
|
||||
{ct_table(pve3_cts)}
|
||||
|
||||
## Routing (Cloudflare Tunnels)
|
||||
{tunnel_text}
|
||||
|
||||
## Zugangsdaten
|
||||
- pve-hetzner: root / {pw_hetzner}
|
||||
- pve1: root / {pw_default}
|
||||
- Alle CTs: root / {pw_default}
|
||||
|
||||
## Telegram Bots
|
||||
| Bot | Zweck |
|
||||
|---|---|
|
||||
| @MutterbotAI_bot | Watchdog-Alerts |
|
||||
| @Orbitalo_Hausmeister_bot | Homelab AI-Bot |
|
||||
"""
|
||||
|
||||
|
||||
def generate_smarthome_state(cfg: config.HomelabConfig) -> str:
|
||||
log("Sammle Smart Home Status...")
|
||||
|
||||
backup_dir = Path("/home/backup-muldenstein/backups")
|
||||
if backup_dir.exists():
|
||||
backups = sorted(backup_dir.glob("*.tar.gz"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if backups:
|
||||
stat = backups[0].stat()
|
||||
size_mb = stat.st_size // (1024 * 1024)
|
||||
mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
||||
last_backup = f"{size_mb}MB, {mtime}"
|
||||
else:
|
||||
last_backup = "keine Backups gefunden"
|
||||
backup_count = str(len(backups))
|
||||
else:
|
||||
last_backup = "Verzeichnis nicht vorhanden"
|
||||
backup_count = "0"
|
||||
|
||||
grafana_url = f"https://{cfg.domains.get('grafana', 'grafana.orbitalo.net')}"
|
||||
|
||||
return f"""# Smart Home Muldenstein — Live State
|
||||
> Auto-generiert: {DATE}
|
||||
|
||||
## Backup-Status
|
||||
- Letztes Backup: {last_backup}
|
||||
- Backups gesamt: {backup_count}
|
||||
- Ziel: /home/backup-muldenstein/backups/ (CT 144)
|
||||
|
||||
## Services (CT 143)
|
||||
| Dienst | URL |
|
||||
|---|---|
|
||||
| Grafana | {grafana_url} |
|
||||
| ioBroker | http://192.168.178.36:8081 |
|
||||
| InfluxDB | http://192.168.178.36:8086 |
|
||||
|
||||
## Grafana Alerts → Telegram {cfg.raw.get('TG_CHAT_ID', '?')}
|
||||
- Promtail DOWN (> 5 Min keine Daten)
|
||||
- CPU > 70%
|
||||
- Memory > 80%
|
||||
- Disk > 90%
|
||||
|
||||
## Backup-Zeitplan
|
||||
- täglich 04:00 → /root/backup-to-hetzner.sh (auf pve3)
|
||||
- Retention: 30d tägl, 90d wöchl, unbegrenzt monatl
|
||||
"""
|
||||
|
||||
|
||||
# ── Git Operations ────────────────────────────────────
|
||||
|
||||
def git_sync(cfg: config.HomelabConfig):
|
||||
forgejo_token = cfg.api_keys.get("forgejo_sync_token", "")
|
||||
ct_111 = config.get_container(cfg, vmid=111)
|
||||
forgejo_ip = ct_111.tailscale_ip if ct_111 else "100.89.246.60"
|
||||
forgejo_url = f"http://orbitalo:{forgejo_token}@{forgejo_ip}:3000/orbitalo/homelab-brain.git"
|
||||
|
||||
subprocess.run(
|
||||
["git", "-C", str(REPO), "fetch", forgejo_url, "main", "--quiet"],
|
||||
capture_output=True, timeout=30,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", str(REPO), "reset", "--hard", "FETCH_HEAD"],
|
||||
capture_output=True, timeout=15,
|
||||
)
|
||||
|
||||
return forgejo_url
|
||||
|
||||
|
||||
def git_commit_and_push(cfg: config.HomelabConfig, forgejo_url: str):
|
||||
subprocess.run(["git", "-C", str(REPO), "add", "-A"], capture_output=True, timeout=15)
|
||||
subprocess.run(
|
||||
["git", "-C", str(REPO),
|
||||
"-c", "user.email=sync@homelab", "-c", "user.name=Auto-Sync",
|
||||
"commit", "-m", f"Auto-Sync: {DATE}", "--quiet"],
|
||||
capture_output=True, timeout=15,
|
||||
)
|
||||
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(REPO), "push", forgejo_url, "main", "--quiet"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
log("Push erfolgreich")
|
||||
(DEBOUNCE_DIR / "git_push.lock").unlink(missing_ok=True)
|
||||
else:
|
||||
err = r.stderr.split("\n")[0] if r.stderr else "unbekannt"
|
||||
log(f"Push FEHLER: {err}")
|
||||
tg_alert(cfg, "git_push",
|
||||
f"*Homelab Git-Sync fehlgeschlagen*%0A%0AFehler: {err}%0AZeit: {DATE}")
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
global CHANGED
|
||||
log("Sync startet...")
|
||||
|
||||
cfg = config.parse_config(REPO / "homelab.conf")
|
||||
forgejo_url = git_sync(cfg)
|
||||
|
||||
cfg = config.parse_config(REPO / "homelab.conf")
|
||||
|
||||
# Watchdog
|
||||
log("Watchdog läuft...")
|
||||
check_service(cfg, 109, "rss-manager", "RSS Manager")
|
||||
check_docker(cfg, 101, "wordpress-app", "WordPress Docker")
|
||||
|
||||
# STATE.md Dateien generieren
|
||||
states = {
|
||||
"arakava-news/STATE.md": generate_arakava_state(cfg),
|
||||
"infrastructure/STATE.md": generate_infra_state(cfg),
|
||||
"smart-home/STATE.md": generate_smarthome_state(cfg),
|
||||
}
|
||||
|
||||
for path, content in states.items():
|
||||
full_path = REPO / path
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_path.write_text(content)
|
||||
CHANGED = True
|
||||
log(f"{path} aktualisiert")
|
||||
|
||||
if CHANGED:
|
||||
git_commit_and_push(cfg, forgejo_url)
|
||||
else:
|
||||
log("Keine Änderungen")
|
||||
|
||||
log("Sync abgeschlossen")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# Smart Home Muldenstein — Live State
|
||||
> Auto-generiert: 2026-03-08 07:45
|
||||
> Auto-generiert: 2026-03-08 07:59
|
||||
|
||||
## Backup-Status
|
||||
- Letztes Backup: 513M Mar 8 04:01
|
||||
- Letztes Backup: 512MB, 2026-03-08 04:01
|
||||
- Backups gesamt: 3
|
||||
- Ziel: /home/backup-muldenstein/backups/ (CT 144)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue