From 1f4e5ed388d9b6d629082f36582fbefcf7822487 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Mar 2026 14:50:08 +0700 Subject: [PATCH] 3 neue Datenquellen: Forgejo (Issues/Commits), Seafile (Cloud-Speicher), PBS (Backups) --- homelab-ai-bot/context.py | 22 +++++ homelab-ai-bot/core/forgejo_client.py | 91 ++++++++++++++++++++ homelab-ai-bot/core/pbs_client.py | 118 ++++++++++++++++++++++++++ homelab-ai-bot/core/seafile_client.py | 95 +++++++++++++++++++++ homelab-ai-bot/llm.py | 24 ++++++ 5 files changed, 350 insertions(+) create mode 100644 homelab-ai-bot/core/forgejo_client.py create mode 100644 homelab-ai-bot/core/pbs_client.py create mode 100644 homelab-ai-bot/core/seafile_client.py diff --git a/homelab-ai-bot/context.py b/homelab-ai-bot/context.py index df9dd17f..22788208 100644 --- a/homelab-ai-bot/context.py +++ b/homelab-ai-bot/context.py @@ -7,6 +7,7 @@ import re sys.path.insert(0, os.path.dirname(__file__)) from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client +from core import forgejo_client, seafile_client, pbs_client def _load_config(): @@ -147,6 +148,24 @@ def _tool_get_wordpress_stats() -> str: return wordpress_client.format_overview(cfg) +def _tool_get_forgejo_status() -> str: + cfg = _load_config() + forgejo_client.init(cfg) + return forgejo_client.format_overview() + + +def _tool_get_seafile_status() -> str: + cfg = _load_config() + seafile_client.init(cfg) + return seafile_client.format_overview() + + +def _tool_get_backup_status() -> str: + cfg = _load_config() + pbs_client.init(cfg) + return pbs_client.format_overview() + + def _tool_get_feed_stats() -> str: cfg = _load_config() ct_109 = config.get_container(cfg, vmid=109) @@ -179,4 +198,7 @@ def get_tool_handlers() -> dict: "get_server_warnings": lambda: _tool_get_server_warnings(), "get_wordpress_stats": lambda: _tool_get_wordpress_stats(), "get_feed_stats": lambda: _tool_get_feed_stats(), + "get_forgejo_status": lambda: _tool_get_forgejo_status(), + "get_seafile_status": lambda: _tool_get_seafile_status(), + "get_backup_status": lambda: _tool_get_backup_status(), } diff --git a/homelab-ai-bot/core/forgejo_client.py b/homelab-ai-bot/core/forgejo_client.py new file mode 100644 index 00000000..3353db9e --- /dev/null +++ b/homelab-ai-bot/core/forgejo_client.py @@ -0,0 +1,91 @@ +"""Forgejo REST API Client — Issues, Repos, Commits.""" + +import requests +from typing import Optional + +FORGEJO_URL = "" +FORGEJO_TOKEN = "" +REPO = "orbitalo/homelab-brain" + + +def init(cfg): + global FORGEJO_URL, FORGEJO_TOKEN + ct_111 = None + for c in cfg.containers: + if c.vmid == 111 and c.host == "pve-hetzner": + ct_111 = c + break + ip = ct_111.tailscale_ip if ct_111 else "100.89.246.60" + FORGEJO_URL = f"http://{ip}:3000/api/v1" + FORGEJO_TOKEN = cfg.api_keys.get("forgejo_token", cfg.raw.get("FORGEJO_TOKEN", "")) + + +def _get(endpoint: str, params: dict = None) -> Optional[dict | list]: + if not FORGEJO_URL or not FORGEJO_TOKEN: + return None + try: + r = requests.get( + f"{FORGEJO_URL}{endpoint}", + headers={"Authorization": f"token {FORGEJO_TOKEN}"}, + params=params or {}, + timeout=10, + ) + r.raise_for_status() + return r.json() + except Exception: + return None + + +def get_open_issues(repo: str = REPO) -> list[dict]: + data = _get(f"/repos/{repo}/issues", {"state": "open", "limit": 20}) + if not data: + return [] + results = [] + for i in data: + labels = [l["name"] for l in i.get("labels", [])] + results.append({ + "number": i["number"], + "title": i["title"], + "labels": labels, + "created": i["created_at"][:10], + }) + return results + + +def get_recent_commits(repo: str = REPO, limit: int = 10) -> list[dict]: + data = _get(f"/repos/{repo}/commits", {"limit": limit}) + if not data: + return [] + results = [] + for c in data: + msg = c["commit"]["message"].split("\n")[0][:80] + date = c["commit"]["author"]["date"][:16] + results.append({"date": date, "message": msg}) + return results + + +def get_repos() -> list[dict]: + data = _get("/repos/search", {"limit": 20}) + if not data or "data" not in data: + return [] + return [{"name": r["full_name"], "issues": r["open_issues_count"]} for r in data["data"]] + + +def format_overview() -> str: + issues = get_open_issues() + commits = get_recent_commits(limit=5) + + lines = [f"Forgejo — {len(issues)} offene Issues\n"] + if issues: + for i in issues: + lbl = f" [{', '.join(i['labels'])}]" if i["labels"] else "" + lines.append(f" #{i['number']}: {i['title']}{lbl}") + else: + lines.append(" Keine offenen Issues.") + + if commits: + lines.append("\nLetzte Commits:") + for c in commits: + lines.append(f" {c['date']} {c['message']}") + + return "\n".join(lines) diff --git a/homelab-ai-bot/core/pbs_client.py b/homelab-ai-bot/core/pbs_client.py new file mode 100644 index 00000000..49481990 --- /dev/null +++ b/homelab-ai-bot/core/pbs_client.py @@ -0,0 +1,118 @@ +"""Proxmox Backup Server REST API Client — Backup-Status und Datastore-Info.""" + +import requests +import urllib3 +from datetime import datetime +from typing import Optional + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +PBS_URL = "" +PBS_USER = "root@pam" +PBS_PASS = "" +_ticket_cache = "" + + +def init(cfg): + global PBS_URL, PBS_PASS, _ticket_cache + _ticket_cache = "" + pbs_ip = cfg.raw.get("SRV_PBS_MU", "100.99.139.22") + PBS_URL = f"https://{pbs_ip}:8007/api2/json" + PBS_PASS = cfg.passwords.get("default", "") + + +def _get_ticket() -> str: + global _ticket_cache + if _ticket_cache: + return _ticket_cache + try: + r = requests.post( + f"{PBS_URL}/access/ticket", + data={"username": PBS_USER, "password": PBS_PASS}, + verify=False, timeout=5, + ) + r.raise_for_status() + _ticket_cache = r.json()["data"]["ticket"] + return _ticket_cache + except Exception: + return "" + + +def _get(endpoint: str) -> Optional[dict | list]: + ticket = _get_ticket() + if not ticket: + return None + try: + r = requests.get( + f"{PBS_URL}{endpoint}", + cookies={"PBSAuthCookie": ticket}, + verify=False, timeout=10, + ) + r.raise_for_status() + return r.json().get("data") + except Exception: + return None + + +def get_datastore_usage() -> list[dict]: + data = _get("/status/datastore-usage") + if not data: + return [] + results = [] + for d in data: + total = d.get("total", 0) / 1024**3 + used = d.get("used", 0) / 1024**3 + avail = d.get("avail", 0) / 1024**3 + pct = (used / total * 100) if total > 0 else 0 + results.append({ + "store": d.get("store", "?"), + "total_gb": total, + "used_gb": used, + "avail_gb": avail, + "used_pct": pct, + }) + return results + + +def get_recent_snapshots(datastore: str = "nvme-pool", limit: int = 10) -> list[dict]: + data = _get(f"/admin/datastore/{datastore}/snapshots") + if not data: + return [] + data.sort(key=lambda s: s.get("backup-time", 0), reverse=True) + results = [] + for s in data[:limit]: + ts = datetime.fromtimestamp(s.get("backup-time", 0)) + results.append({ + "id": s.get("backup-id", "?"), + "type": s.get("backup-type", "?"), + "time": ts.strftime("%Y-%m-%d %H:%M"), + "size_gb": s.get("size", 0) / 1024**3, + }) + return results + + +def get_snapshot_count(datastore: str = "nvme-pool") -> int: + data = _get(f"/admin/datastore/{datastore}/snapshots") + return len(data) if data else 0 + + +def format_overview() -> str: + stores = get_datastore_usage() + if not stores: + return "PBS nicht erreichbar." + + lines = ["PBS Muldenstein — Backup-Status\n"] + lines.append("Datastores:") + for s in stores: + if s["total_gb"] < 1: + continue + lines.append(f" {s['store']}: {s['used_gb']:.0f}/{s['total_gb']:.0f} GB ({s['used_pct']:.0f}%)") + + snaps = get_recent_snapshots(limit=5) + total_snaps = get_snapshot_count() + if snaps: + lines.append(f"\nLetzte Backups ({total_snaps} total):") + for s in snaps: + lines.append(f" {s['time']} CT {s['id']} ({s['size_gb']:.1f} GB)") + + return "\n".join(lines) diff --git a/homelab-ai-bot/core/seafile_client.py b/homelab-ai-bot/core/seafile_client.py new file mode 100644 index 00000000..8a8a8a25 --- /dev/null +++ b/homelab-ai-bot/core/seafile_client.py @@ -0,0 +1,95 @@ +"""Seafile REST API Client — Speicherplatz und Bibliotheken.""" + +import requests +from typing import Optional + +SEAFILE_URL = "" +SEAFILE_USER = "" +SEAFILE_PASS = "" +_token_cache = "" + + +def init(cfg): + global SEAFILE_URL, SEAFILE_USER, SEAFILE_PASS, _token_cache + _token_cache = "" + ct_103 = None + for c in cfg.containers: + if c.vmid == 103 and c.host == "pve-hetzner": + ct_103 = c + break + ip = "10.10.10.103" + SEAFILE_URL = f"http://{ip}:8080" + SEAFILE_USER = cfg.raw.get("SEAFILE_ADMIN_EMAIL", "admin@orbitalo.net") + SEAFILE_PASS = cfg.passwords.get("default", "") + + +def _get_token() -> str: + global _token_cache + if _token_cache: + return _token_cache + try: + r = requests.post( + f"{SEAFILE_URL}/api2/auth-token/", + data={"username": SEAFILE_USER, "password": SEAFILE_PASS}, + timeout=5, + ) + r.raise_for_status() + _token_cache = r.json()["token"] + return _token_cache + except Exception: + return "" + + +def _get(endpoint: str) -> Optional[dict | list]: + token = _get_token() + if not token: + return None + try: + r = requests.get( + f"{SEAFILE_URL}{endpoint}", + headers={"Authorization": f"Token {token}"}, + timeout=10, + ) + r.raise_for_status() + return r.json() + except Exception: + return None + + +def get_account_info() -> dict: + data = _get("/api2/account/info/") + if not data: + return {"error": "nicht erreichbar"} + return { + "usage_gb": data.get("usage", 0) / 1024**3, + "total_gb": data.get("total", 0) / 1024**3, + "email": data.get("email", ""), + } + + +def get_libraries() -> list[dict]: + data = _get("/api2/repos/") + if not data: + return [] + results = [] + for r in data: + results.append({ + "name": r.get("name", "?"), + "size_mb": r.get("size", 0) / 1024**2, + "encrypted": r.get("encrypted", False), + }) + return results + + +def format_overview() -> str: + acct = get_account_info() + if "error" in acct: + return "Seafile nicht erreichbar." + + libs = get_libraries() + lines = [f"Seafile — {acct['usage_gb']:.1f} GB belegt\n"] + if libs: + lines.append("Bibliotheken:") + for lib in libs: + lines.append(f" {lib['name']}: {lib['size_mb']:.0f} MB") + return "\n".join(lines) diff --git a/homelab-ai-bot/llm.py b/homelab-ai-bot/llm.py index 48c37dcb..cd21ae5a 100644 --- a/homelab-ai-bot/llm.py +++ b/homelab-ai-bot/llm.py @@ -122,6 +122,30 @@ TOOLS = [ "parameters": {"type": "object", "properties": {}, "required": []}, }, }, + { + "type": "function", + "function": { + "name": "get_forgejo_status", + "description": "Forgejo Git-Server: Offene Issues/TODOs, letzte Commits, Repos", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "get_seafile_status", + "description": "Seafile Cloud-Speicher: Belegter Speicherplatz, Bibliotheken", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, + { + "type": "function", + "function": { + "name": "get_backup_status", + "description": "Proxmox Backup Server (PBS): Datastore-Belegung, letzte Backups, Snapshot-Anzahl", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, ]