From 7a37b749f3fe4144076f29e6d200ef4b0e40f7e0 Mon Sep 17 00:00:00 2001 From: Auto-Sync Date: Fri, 17 Apr 2026 21:15:08 +0200 Subject: [PATCH] Auto-Sync: 2026-04-17 21:15 --- arakava-news/STATE.md | 6 +- homelab-ai-bot/tools/rag.py.bak-multiquery | 501 +++++++++++++++++++++ homelab.conf.bak-20260417-211337 | 310 +++++++++++++ infrastructure/STATE.md | 2 +- smart-home/STATE.md | 2 +- 5 files changed, 816 insertions(+), 5 deletions(-) create mode 100644 homelab-ai-bot/tools/rag.py.bak-multiquery create mode 100644 homelab.conf.bak-20260417-211337 diff --git a/arakava-news/STATE.md b/arakava-news/STATE.md index c75813c6..9c085b8a 100644 --- a/arakava-news/STATE.md +++ b/arakava-news/STATE.md @@ -1,5 +1,5 @@ # Arakava News — Live State -> Auto-generiert: 2026-04-17 21:00 +> Auto-generiert: 2026-04-17 21:15 ## Service Status | Service | CT | Status | @@ -8,11 +8,11 @@ | WordPress Docker | 101 | running | ## Letzte Feed-Aktivität (Top 5) -Heise Security: 2026-04-17 17:41:02 +NachDenkSeiten: 2026-04-17 19:00:17 + Heise Security: 2026-04-17 17:41:02 Golem.de: 2026-04-17 17:41:02 Norbert Häring: 2026-04-17 17:40:53 Heise Online: 2026-04-17 16:41:00 - Rubikon.news: 2026-04-17 16:40:52 ## Fehler (letzte 24h) - Fehler gesamt: 0 diff --git a/homelab-ai-bot/tools/rag.py.bak-multiquery b/homelab-ai-bot/tools/rag.py.bak-multiquery new file mode 100644 index 00000000..4fd33016 --- /dev/null +++ b/homelab-ai-bot/tools/rag.py.bak-multiquery @@ -0,0 +1,501 @@ +"""RAG Dokumentensuche — Elasticsearch direkt (Hybrid: kNN + deutscher Text). + +RAGFlow bleibt Ingestion; Suche geht direkt an ES (Issue #51). +""" + +import base64 +import json +import logging +import re +import urllib.error +import urllib.request + +log = logging.getLogger("tools.rag") + +ES_BASE = "http://100.109.101.12:1200" +ES_USER = "elastic" +ES_PASS = "infini_rag_flow" +ES_INDEX = "ragflow_61f51c8c279011f1a174bd19863ba33e" +KB_ID = "dc24edda27a311f19fe7fb811de6f016" +OLLAMA_EMBED_URL = "http://100.84.255.83:11434/api/embeddings" +EMBED_MODEL = "nomic-embed-text" + +# Cross-Encoder Reranking (CT 123, pve-hetzner LAN) +RERANKER_URL = "http://10.10.10.123:8099" +RERANK_CANDIDATES = 15 +RERANK_TIMEOUT = 45 +RERANK_SNIPPET_CHARS = 512 + +MIN_TOP_K = 5 +# Breite Übersichten: mehr ES-Runden, mehr distinct Treffer (pro vollem Pfad docnm_kwd) +MAX_TOP_K_NORMAL = 25 +MAX_TOP_K_WIDE = 60 +ES_SIZE_CAP = 200 + +TOOLS = [ + { + "type": "function", + "function": { + "name": "rag_search", + "description": ( + "Durchsucht die private Dokumenten-Wissensbasis (>21.000 Dokumente: " + "Vertraege, Versicherungen, Rente, Finanzamt, Familiendokumente, " + "Anleitungen, Buecher, persoenliche Unterlagen). " + "Nutze dieses Tool wenn der User nach einem bestimmten Dokument, " + "Vertrag, Brief oder persoenlicher Information fragt. " + "Bei breiten Fragen ('welche Versicherungen', Jahreskosten, Listen) " + "top_k=15 oder hoeher setzen." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": ( + "Suchanfrage: Dokumentname, Thema oder Inhalt. Kurz und praezise, " + "z.B. 'Familienbuch Opa Oma' oder 'Grundsteuer Erklaerung 2024'" + ), + }, + "top_k": { + "type": "integer", + "description": "Anzahl Ergebnisse (5-25, Standard 10)", + "default": 10, + }, + }, + "required": ["query"], + }, + }, + }, +] + +SYSTEM_PROMPT_EXTRA = """RAG DOKUMENTENSUCHE — PFLICHT-REGELN: +Du hast Zugriff auf eine private Wissensbasis mit >21.000 Dokumenten (Vertraege, Versicherungen, Rente, Finanzamt, Familiendokumente, Anleitungen, Buecher, persoenliche Unterlagen, Arbeitsvertraege, Kindergeld, Reisepass, Personalausweis, KFZ, Mietvertraege, Bausparvertraege, Rechnungen). + +WANN rag_search AUFRUFEN — IMMER bei diesen Fragen: +- "habe ich..." / "gibt es..." / "wo ist..." / "finde..." / "zeig mir..." + Dokument/Vertrag/Versicherung/Bescheid +- Jede Frage nach persoenlichen Unterlagen, Vertraegen, Versicherungen, Rechnungen, Bescheiden +- AUCH wenn du glaubst die Antwort zu kennen — das Gedaechtnis ist NICHT die Wissensbasis! +- AUCH wenn das Thema im Gedaechtnis steht — trotzdem rag_search aufrufen fuer vollstaendige Antwort + +WANN NICHT: Nur bei reinen Homelab/IT-Fragen, Smalltalk, oder wenn der User explizit NICHT nach Dokumenten fragt. + +SUCHANFRAGE: Kurze Keywords, KEINE ganzen Saetze. Beispiele: +- "Familienbuch" / "Grundsteuer Erklaerung" / "Haftpflicht" / "Kindergeld" / "Mietvertrag" / "Arbeitsvertrag" / "Reisepass" + +ERGEBNISSE AUSWERTEN: +- Bei breiten Fragen ("welche Versicherungen", Jahreskosten, Listen): top_k=15-25, ALLE Treffer aus der Tool-Antwort abarbeiten +- Liste die gefundenen Dokumente mit Ordner und kurzem Inhalt auf +- ERFINDE KEINE Details die nicht im Ergebnis stehen +- Der Ordnerpfad (vor dem Dateinamen, getrennt durch __) zeigt die Kategorie +- Wenn rag_search Treffer liefert: IMMER auflisten, auch wenn Inhalt unvollstaendig +- Mehrere Treffer zur gleichen Versicherung/Gesellschaft: jede Sparte/Dokumentart separat nennen (Kfz, Rechtsschutz, Haftpflicht, Sach, Ausland, Kranken), mit Dateiname/Ordner +- Antworte NIEMALS "keine gefunden" oder "nicht gespeichert" OHNE vorher rag_search aufgerufen zu haben""" + + +def _basic_auth_header() -> str: + token = base64.b64encode(f"{ES_USER}:{ES_PASS}".encode()).decode() + return f"Basic {token}" + + +def _ollama_embed(text: str) -> list | None: + body = json.dumps({"model": EMBED_MODEL, "prompt": text}).encode() + req = urllib.request.Request( + OLLAMA_EMBED_URL, + data=body, + method="POST", + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + data = json.load(resp) + emb = data.get("embedding") + if not emb: + return None + if len(emb) != 768: + log.warning("Unexpected embedding dimension %s", len(emb)) + return emb + except Exception as e: + log.error("Ollama embed error: %s", e) + return None + + +def _ocr_note(text: str) -> str: + if not text or len(text) < 40: + return "" + non_alnum = sum(1 for c in text if not c.isalnum() and not c.isspace()) + ratio = non_alnum / max(len(text), 1) + words = re.findall(r"\w+", text, re.UNICODE) + avg_len = (sum(len(w) for w in words) / len(words)) if words else 0.0 + if ratio > 0.15 or avg_len < 2.0: + return " [OCR vermutlich schlecht]" + return "" + + +def _folder_from_docname(name: str) -> str: + """Extrahiert den Ordnerpfad aus docnm_kwd (__ = Trenner).""" + parts = name.rsplit("__", 1) + if len(parts) == 2: + return parts[0].replace("__", " > ").replace("_", " ") + return "" + + +def _dedup_key(name: str) -> str: + """Normalisiert Dokumentnamen fuer Deduplizierung. + + Extrahiert nur den Dateinamen (nach letztem __), ignoriert + Dateiendung und Kopie-Marker wie (1), (2). + 'Ordner__Foo(1).pdf' und 'Anderer__Foo.txt' werden als gleich behandelt. + """ + fname = name.rsplit("__", 1)[-1] if "__" in name else name + key = re.sub(r"\.(pdf|txt|docx?|xlsx?|csv|png|jpg|jpeg)$", "", fname, flags=re.IGNORECASE) + key = re.sub(r"\s*\(\d+\)\s*$", "", key).rstrip() + return key.lower() + + +def _dedup_key_full_doc(name: str) -> str: + """Ein Chunk pro vollem docnm_kwd — gleicher Dateiname in verschiedenen Ordnern bleibt getrennt.""" + return re.sub(r"\s+", " ", (name or "").strip().lower()) + + +def _es_hybrid_search(query: str, es_size: int) -> dict: + qvec = _ollama_embed(query) + if not qvec: + return {"_error": "Embedding fehlgeschlagen (Ollama nicht erreichbar?)."} + + es_size = min(ES_SIZE_CAP, max(es_size, 20)) + kb_filter = {"term": {"kb_id": KB_ID}} + body = { + "size": es_size, + "knn": { + "field": "q_768_vec", + "query_vector": qvec, + "k": es_size, + "num_candidates": min(500, max(es_size * 5, 150)), + "filter": [kb_filter], + }, + "query": { + "bool": { + "filter": [kb_filter], + "should": [ + {"match": {"content_de": {"query": query, "boost": 2.0}}}, + {"match": {"content_ltks": {"query": query.lower(), "boost": 0.4}}}, + {"match": {"docnm_kwd": {"query": query, "boost": 3.0}}}, + ], + "minimum_should_match": 0, + } + }, + } + url = f"{ES_BASE}/{ES_INDEX}/_search" + req = urllib.request.Request( + url, + data=json.dumps(body).encode(), + method="POST", + headers={ + "Content-Type": "application/json", + "Authorization": _basic_auth_header(), + }, + ) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + return json.load(resp) + except urllib.error.HTTPError as e: + err = e.read().decode(errors="replace")[:800] + log.error("ES HTTP %s: %s", e.code, err) + return {"_error": f"ES HTTP {e.code}: {err}"} + except Exception as e: + log.error("ES search error: %s", e) + return {"_error": str(e)} + + +def _snippet_for_rerank(src: dict) -> str: + doc_name = src.get("docnm_kwd") or "" + raw = src.get("content_with_weight") or src.get("content_de") or "" + prefix = doc_name[:120] + "\n" if doc_name else "" + return prefix + raw[:RERANK_SNIPPET_CHARS] + + +def _rerank_hits(query: str, hits: list) -> tuple[list, bool]: + """Rerankt mit Cross-Encoder, kombiniert Score mit ES-Rang (RRF).""" + if not hits or not RERANKER_URL: + return hits, False + to_score = hits[:RERANK_CANDIDATES] + docs = [] + for h in to_score: + src = h.get("_source") or {} + docs.append(_snippet_for_rerank(src)) + if not any((d or "").strip() for d in docs): + return hits, False + body = json.dumps({"query": query, "documents": docs}).encode() + url = f"{RERANKER_URL.rstrip('/')}/rerank" + req = urllib.request.Request( + url, + data=body, + method="POST", + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=RERANK_TIMEOUT) as resp: + data = json.load(resp) + scores = data.get("scores") or [] + if len(scores) != len(to_score): + log.warning( + "rerank score count mismatch: %s vs %s", + len(scores), + len(to_score), + ) + return hits, False + + k = 60 + rr_ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True) + rr_rank_map = {i: rank + 1 for rank, i in enumerate(rr_ranked)} + + combined: list[tuple[float, int]] = [] + for idx in range(len(to_score)): + es_rank = idx + 1 + rr_rank = rr_rank_map[idx] + rrf = 1.0 / (k + es_rank) + 1.0 / (k + rr_rank) + combined.append((rrf, idx)) + combined.sort(key=lambda x: x[0], reverse=True) + + new_order: list = [] + for rrf, idx in combined: + h = to_score[idx] + h["_rerank_score"] = float(scores[idx]) + h["_rrf_score"] = float(rrf) + new_order.append(h) + rest = hits[RERANK_CANDIDATES:] + return new_order + rest, True + except Exception as e: + log.warning("rerank failed (fallback to ES): %s", e) + return hits, False + + +def _is_wide_recall_query(q: str) -> bool: + """Übersichts-/Listen-/Kostenfragen: mehrfach suchen und mergen.""" + ql = (q or "").lower() + if any(x in ql for x in ("welche versicherung", "alle versicherung", "versicherungen habe")): + return True + if "versicherung" in ql and any( + x in ql + for x in ( + "welche", + "alle", + "liste", + "überblick", + "ueberblick", + "kosten", + "beitrag", + "jähr", + "jaehr", + "jahres", + "gesamt", + "summe", + "übersicht", + "uebersicht", + ) + ): + return True + costish = any( + x in ql + for x in ( + "kosten", + "kostet", + "wie viel", + "wieviel", + "beitrag", + "beiträge", + "beitraege", + "eur", + "euro", + "jähr", + "jaehr", + "jahreskosten", + "prämie", + "praemie", + ) + ) + broad = any( + x in ql + for x in ( + "liste", + "übersicht", + "uebersicht", + "alle", + "gesamt", + "summe", + "jährlich", + "jaehrlich", + ) + ) + return costish and broad + + +# Zusatzanfragen decken Sparten + Gesellschaften ab (Recall) +_WIDE_SUBQUERIES = [ + "Versicherung Beitragsrechnung Jahresbeitrag", + "Wohngebäudeversicherung Gebäude Beitrag", + "Hausratversicherung Beitrag Ergo", + "Haftpflichtversicherung Beitrag GARANTA", + "Kfz Kasko Haftpflicht Beitragsrechnung", + "Rechtsschutzversicherung Beitrag", + "Lebensversicherung Beitrag", + "Krankenversicherung PKV Beitrag", + "Sachversicherung LVM Beitrag", + "LVM AutoPlus Versicherungsschein", + "Allianz Versicherung Police", + "Nürnberger Versicherung Beitrag", + "Ergo Versicherung Police", + "Unfallversicherung Berufsunfähigkeit", + "Bausparvertrag Bauspar", + "Ford Transit Nutzfahrzeug Versicherung", + "Kfz Versicherungsschein Beitrag jährlich", +] + + +def _merge_hits_from_queries( + queries: list[str], + es_size: int, + pool_cap: int, + *, + full_path_dedup: bool = False, +) -> tuple[list, str | None]: + """Führt mehrere Hybrid-Suchen aus; pro Dedup-Key höchster Score.""" + best: dict[str, dict] = {} + last_err: str | None = None + + def dkey(dn: str) -> str: + return _dedup_key_full_doc(dn) if full_path_dedup else _dedup_key(dn) + + def absorb(hits: list) -> None: + for h in hits: + src = h.get("_source") or {} + dn = src.get("docnm_kwd") or "?" + dk = dkey(dn) + sc = float(h.get("_score") or 0.0) + old = best.get(dk) + if old is None or sc > float(old.get("_score") or 0.0): + best[dk] = h + + for q in queries: + q = (q or "").strip() + if not q: + continue + data = _es_hybrid_search(q, es_size) + if "_error" in data: + last_err = str(data["_error"]) + log.warning("wide_recall subquery fail %s: %s", q[:40], last_err) + continue + absorb((data.get("hits") or {}).get("hits") or []) + + merged = sorted(best.values(), key=lambda h: float(h.get("_score") or 0.0), reverse=True) + return merged[:pool_cap], last_err + + +def handle_rag_search(query: str, top_k: int = 8, **kw): + if not query or not query.strip(): + return "rag_search: query fehlt." + + qstrip = query.strip() + wide = _is_wide_recall_query(qstrip) + cap = MAX_TOP_K_WIDE if wide else MAX_TOP_K_NORMAL + top_k = max(MIN_TOP_K, min(int(top_k or 10), cap)) + es_size = min(ES_SIZE_CAP, max(top_k * 10, 70)) + + if wide: + subqs = [qstrip] + for sq in _WIDE_SUBQUERIES: + if sq.lower() not in qstrip.lower(): + subqs.append(sq) + pool_cap = max(top_k * 5, 120) + hits, err = _merge_hits_from_queries( + subqs[:22], + es_size, + pool_cap=pool_cap, + full_path_dedup=True, + ) + if err and not hits: + return f"Fehler bei der Dokumentensuche: {err}" + header = ( + f"**Breitensuche ({len(subqs[:22])} Anfragen, Dedup=voller Pfad) '{qstrip}' — " + f"{len(hits)} Kandidaten, zeige bis {top_k}:**\n" + ) + snip_len = 400 + else: + data = _es_hybrid_search(qstrip, es_size) + if "_error" in data: + return f"Fehler bei der Dokumentensuche: {data['_error']}" + hits = (data.get("hits") or {}).get("hits") or [] + header = f"**Dokumente fuer '{qstrip}' (bis {top_k}):**\n" + snip_len = 650 + + if not hits: + return f"Keine Ergebnisse fuer '{qstrip}' in der Wissensbasis gefunden." + + hits, reranked = _rerank_hits(qstrip, hits) + + seen_docs: set[str] = set() + lines: list[str] = [] + count = 0 + + def out_dkey(doc_name: str) -> str: + return _dedup_key_full_doc(doc_name) if wide else _dedup_key(doc_name) + + for h in hits: + if count >= top_k: + break + src = h.get("_source") or {} + doc_name = src.get("docnm_kwd") or "?" + dk = out_dkey(doc_name) + if dk in seen_docs: + continue + seen_docs.add(dk) + + if "_rrf_score" in h: + score = float(h["_rrf_score"]) + score_label = "RRF" + elif "_rerank_score" in h: + score = float(h["_rerank_score"]) + score_label = "Rerank" + else: + score = float(h.get("_score") or 0.0) + score_label = "ES" + raw = src.get("content_with_weight") or src.get("content_de") or "" + content = raw[:snip_len].strip() + ocr = _ocr_note(raw) + folder = _folder_from_docname(doc_name) + filename = doc_name.rsplit("__", 1)[-1] if "__" in doc_name else doc_name + folder_line = f" Ordner: {folder}" if folder else "" + + lines.append( + f"---\n**{count + 1}. {filename}** ({score_label}: {score:.3f}){ocr}" + ) + if folder_line: + lines.append(folder_line) + if content: + lines.append(f"```\n{content}\n```") + count += 1 + + if count == 0: + return f"Keine Dokumente fuer '{qstrip}' gefunden." + + hdr = header.rstrip() + ( + " _(Cross-Encoder reranked)_" if reranked else "" + ) + "\n" + lines.insert(0, hdr) + tail = ( + "\n---\n(Ende der Ergebnisse. Nur diese Dokumente in dieser Runde. " + + ( + "Bei Summen/Zahlen: alle Treffer prüfen; OCR kann unvollständig sein." + if wide + else "" + ) + + ")" + ) + lines.append(tail) + + return "\n".join(lines) + + +HANDLERS = { + "rag_search": handle_rag_search, +} diff --git a/homelab.conf.bak-20260417-211337 b/homelab.conf.bak-20260417-211337 new file mode 100644 index 00000000..e6563a44 --- /dev/null +++ b/homelab.conf.bak-20260417-211337 @@ -0,0 +1,310 @@ +# ============================================================ +# homelab.conf — EINZIGE QUELLE DER WAHRHEIT +# ============================================================ +# Wenn sich eine IP, URL, ein Container oder Passwort ändert: +# → NUR DIESE DATEI editieren. +# → sync-state.sh liest hieraus und generiert alles andere. +# → Niemals STATE.md, MOTDs oder Issues manuell pflegen. +# ============================================================ + +# --- DOMAINS --- +DOMAIN_PRIMARY="arakavanews.com" +DOMAIN_OLD="arakava-news-2.orbitalo.net" +DOMAIN_MATOMO="matomo.orbitalo.net" +DOMAIN_SEAFILE="seafile.orbitalo.net" +DOMAIN_GRAFANA="grafana.orbitalo.net" +DOMAIN_PDM="pdm.orbitalo.info" +DOMAIN_RSS="rss-manager.orbitalo.net" +DOMAIN_REDAX="redax.orbitalo.net" + +# ============================================================ +# SERVER — Eindeutige Benennung nach Standort +# ============================================================ +# Kambodscha (KA): 3 Server, LAN 192.168.0.x +# Muldenstein (MU): 3 Server (1 offline), LAN 192.168.178.x +# Ramsin (HE): 1 Server bei Helmut +# Hetzner DC: 1 Server +# ============================================================ + +# --- HETZNER --- +SRV_HETZNER="100.88.230.59" + +# --- KAMBODSCHA (3 Server, Takeo) --- +SRV_KA1="100.122.56.60" +SRV_KA1_LOCAL="192.168.0.197" +SRV_KA1_HOSTNAME="pve-ka-1" + +SRV_KA2="100.120.126.95" +SRV_KA2_LOCAL="192.168.0.198" +SRV_KA2_HOSTNAME="pve-ka-2" + +SRV_KA3="100.103.90.94" +SRV_KA3_LOCAL="192.168.0.199" +SRV_KA3_HOSTNAME="pve-ka-3" + +# --- PHNOM PENH (2 Server, Kondo — pp-cluster) --- +SRV_PP1="100.126.26.46" +SRV_PP1_LOCAL="192.168.0.171" +SRV_PP1_HOSTNAME="pve-pp-1" + +SRV_PP2="100.95.156.25" +SRV_PP2_LOCAL="192.168.0.227" +SRV_PP2_HOSTNAME="pve-pp-2" + +# --- MULDENSTEIN (3 Server, pve-mu-1 aktuell offline) --- +# SRV_MU1="???" +# SRV_MU1_HOSTNAME="pve-mu-1" + +SRV_MU2="100.99.101.37" +SRV_MU2_LOCAL="192.168.178.123" +SRV_MU2_HOSTNAME="pve-mu-2" + +SRV_MU3="100.109.101.12" +SRV_MU3_LOCAL="192.168.178.250" +SRV_MU3_HOSTNAME="pve-mu-3" + +# --- RAMSIN (bei Helmut) --- +SRV_HE="100.87.235.11" +SRV_HE_HOSTNAME="pve-he" + +# --- CURSOR / MONITORING BOT (CT 116 auf pve-mu-2, Muldenstein) --- +SRV_CURSOR="100.88.230.74" +SRV_CURSOR_HOSTNAME="monitoring-bot" + +# --- KI-SERVER (Windows, Muldenstein — Cursor + GPU-Workloads) --- +SRV_KI="100.84.255.83" +SRV_KI_HOSTNAME="KI-Server" +SRV_KI_USER="wutti" +SRV_KI_GPU="NVIDIA RTX 3090 24GB" +SRV_KI_OS="Windows 10 Build 26200" +SRV_KI_SSH="ssh ki-server (Key-Auth via monitoring-bot SOCKS5)" + +# --- BACKUP (PBS) --- +SRV_PBS_MU="100.99.139.22" +SRV_PBS_KA="lokal" + +# --- PASSWÖRTER --- +PW_HETZNER="Astral-Proxmox!2026" +PW_DEFAULT="astral66" +PW_WP_ADMIN="eJIyhW0p5PFacjvvKGufKeXS" +PW_5V8_USER="Holgerhh" +PW_5V8_PASS="ddlhh" +PW_EDELMETALL_DASHBOARD="" +PW_PDM_USER="root" +PW_PDM_PASS="astral66" + +# ============================================================ +# CONTAINER — Format: CT__="name|tailscale_ip|dienste" +# Servercodes: HZ=Hetzner, KA1/2/3=Kambodscha, MU2/3=Muldenstein, HE=Ramsin +# ============================================================ + +# --- pve-hetzner (Hauptinfrastruktur) --- +CT_101_HZ="wordpress-v2|100.91.212.19|WordPress + MySQL (Docker) — arakavanews.com" +CT_103_HZ="seafile|100.75.247.60|Seafile (Docker)" +CT_109_HZ="rss-manager|100.113.244.101|RSS Manager + Matomo — WP intern via http://10.10.10.101" +CT_110_HZ="portainer|100.109.206.43|Portainer Docker UI + Loki Stack" +CT_111_HZ="forgejo|100.89.246.60|Forgejo Git Server" +CT_112_HZ="fuenfvoracht|100.73.171.62|FuenfVorAcht Telegram Bot" +CT_113_HZ="redax-wp|100.69.243.16|Redakteur WordPress KI-Autor + DeutschlandBlog" +CT_115_HZ="flugscanner-hub|100.92.161.97|Flugpreisscanner Hub + Scheduler" +CT_116_HZ="homelab-ai-bot (1 GB RAM)|100.123.47.7|Hausmeister Bot (Qwen3-VL 30B via Ollama/KI-Server, Text+Vision) + Save.TV Web-UI + web_search via SearXNG" +CT_121_HZ="deep-research|100.74.196.29|Open Deep Research + SearXNG — LangGraph API auf Port 2024" +CT_117_HZ="memory-service|100.121.192.94|Memory Service API (FastAPI + SQLite)" +CT_144_HZ="muldenstein-backup|—|Backup-Archiv (Read-Only)" +CT_999_HZ="cluster-docu|100.79.8.49|Dokumentation" + +# --- pve-ka-1 (Kambodscha, Hauptserver) --- +CT_110_KA1="uptime-kuma|—|Uptime Monitoring" +CT_115_KA1="flugscanner-asia|100.112.190.22|Scraping-Node Asia" +CT_118_KA1="Django-Klon-Neu|—|Django App (Taxi)" +CT_134_KA1="gold-silber-v3|100.72.230.87|Edelmetall Dashboard + Telegram Bot" +CT_200_KA1="doc-converter|—|Dokument-Konverter" +CT_888_KA1="MCP-Proxmox|—|MCP Server" +CT_999_KA1="cluster-docu|—|Dokumentation" +VM_100_KA1="debian|—|Debian VM" + +# --- pve-pp-1 (Phnom Penh, Kondo — Arbeitsmaschine) --- +CT_100_PP1="yt-desktop|100.112.224.39|XFCE Desktop + xrdp + Chromium/Firefox + Seafile-Sync (Videos) + NFS-Mount Torrents" +CT_103_PP1="torrent|—|qBittorrent Web-UI :8080 (192.168.0.129) + NFS-Export → CT 100" + +# --- pve-pp-2 (Phnom Penh, Kondo — Reserve/Standby) --- +CT_101_PP2="yt-desktop-standby|—|Standby-Kopie CT 100 (gestoppt)" +CT_102_PP2="torrent|—|qBittorrent Web-UI :8080 (192.168.0.193)" + +# --- pve-ka-2 (Kambodscha, Shop-Server) --- +CT_504_KA2="Shop-Template|—|Shop Template (stopped)" +CT_8000_KA2="Kunde0-Shop|—|Kunde 0 Shop (stopped)" +CT_8010_KA2="Kunde1-Shop|—|Kunde 1 Shop (stopped)" + +# --- pve-ka-3 (Kambodscha, Webcam + Dienste) --- +CT_101_KA3="freshrss|—|FreshRSS Reader" +CT_103_KA3="Intercity-Taxi|—|Intercity Taxi App" +CT_104_KA3="bt-search|—|BT Search" +CT_141_KA3="llm-router-dev|—|LLM Router Entwicklung" +CT_600_KA3="webcam|100.80.76.118|Restreamer + Dahua 4K Cam → cam.arakavanews.com" +VM_500_KA3="frigate-vm|100.104.64.99|Frigate NVR + Coral TPU — 3 Kameras, GUI :5000" + +# --- pve-mu-2 (Muldenstein, Shop- & Entwicklungsserver) --- +CT_111_MU2="uptimekuma|—|Uptime Monitoring" +CT_112_MU2="myspeed|—|Internet Speedtest" +CT_113_MU2="pve-scripts-local|—|PVE Helper Scripts" +CT_114_MU2="djangoadmin|—|Django Admin" +CT_115_MU2="Takeo-PC-Shop-Engl|—|PC Shop (englisch)" +CT_116_MU2="monitoring-bot|100.88.230.74|Cursor IDE + Tailscale-Gateway (userspace) + SSH-Hub — CT 116 auf pve-mu-2 (Debian 13, 3.4GB RAM)" +CT_117_MU2="Intercity-Taxi|—|Intercity Taxi" +CT_123_MU2="Kofi-Shop-PP|—|Kofi Shop Phnom Penh" +CT_128_MU2="rustdeskserver|—|RustDesk Remote Desktop" +CT_130_MU2="PC-Shop-Takeo|—|PC Shop Takeo" +CT_131_MU2="PC-Shopp-PP|—|PC Shop Phnom Penh" +CT_136_MU2="Seleniumbase|—|Selenium Scraping" +CT_140_MU2="Alfredo-Pizza|—|Pizza Alfredo" +CT_150_MU2="Pizza-Express-Wolfen|—|Pizza Express Wolfen" +CT_160_MU2="Red-Pizza|—|Red Pizza" +CT_180_MU2="Mellensa-Pizza|—|Mellensa Pizza" +CT_190_MU2="Ali-Baba|—|Ali Baba" +CT_200_MU2="Pizza-Di-Angelo|—|Pizza Di Angelo" +CT_500_MU2="Test-Shop|—|Test Shop" +CT_501_MU2="Test-Shop-Prod|—|Test Shop Produktion" +CT_502_MU2="Test-Shop-2|—|Test Shop 2" + +# --- pve-mu-3 (Muldenstein, Infrastruktur + Mirrors) --- +CT_139_MU3="Syncthing-Muldenstein|—|Syncthing" +CT_141_MU3="syncthing|—|Syncthing" +CT_142_MU3="WG-easy|—|WireGuard VPN" +CT_143_MU3="Raspi-Broker|—|ioBroker MQTT Broker" +CT_145_MU3="flugscanner-mu|100.75.182.15|Flugpreisscanner Node DE" +CT_504_MU3="projektscan-template|—|Projektscan Template" +CT_600_MU3="wp-mirror|100.92.205.101|WordPress Mirror (Redundanz CT 101)" +CT_601_MU3="rss-mirror|—|RSS Manager Mirror (Redundanz CT 109)" +CT_700_MU3="ragflow|192.168.178.154|RAGFlow PDF-RAG (Docker, Ollama/KI-Server, Synology SMB) — ~13k PDFs" +VM_144_MU3="BT-Bridge|—|BT Bridge VM" + +# --- pve-he (Ramsin, bei Helmut) --- +# Container noch nicht inventarisiert + +# --- TELEGRAM BOTS --- +TG_CHAT_ID="674951792" +TG_MUTTER_TOKEN="8551565940:AAHIUpZND-tCNGv9yEoNPRyPt4GxEPYBJdE" +TG_FUENFVORACHT_TOKEN="8799990587:AAEoQuohGdoJ2WudoOHs_j5Ns3iwft6OlFc" +TG_EDELMETALL_TOKEN="8262992299:AAEf8YHPsz42ZdP85DV7JqC4822Ts75GqF4" +TG_HAUSMEISTER_TOKEN="8390233104:AAHdgF6r7qZsQEZHIBHPV1ky3v-6-YULvj8" + +# --- PROXMOX API TOKENS --- +PVE_TOKEN_HETZNER_NAME="mcp-homelab" +PVE_TOKEN_HETZNER_VALUE="e986d3d5-36c0-425c-b1bb-20ed650a8065" + +# --- API KEYS --- +FORGEJO_TOKEN="b874766bdf357bd4c32fa4369d0c588fc6193336" +FORGEJO_SYNC_TOKEN="b874766bdf357bd4c32fa4369d0c588fc6193336" +GITHUB_PAT="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" +OPENROUTER_KEY="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" +OPENAI_API_KEY="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" +MEMORY_API_TOKEN="Ai8eeQibV6Z1RWc7oNPim4PXB4vILU1nRW2-XgRcX2M" +MEMORY_API_URL="http://100.121.192.94:8400" +MATOMO_TOKEN="7d3987d48dcd7fdf9776bd81a4da1778" +MATOMO_URL="http://100.113.244.101" +MATOMO_SITE_ID="1" + +# --- HOMELAB MCP-SERVER (auf pve-hetzner Host) --- +MCP_PATH="/root/homelab-mcp" +MCP_VENV="/root/homelab-mcp/.venv" +MCP_TOOLS="homelab_overview,homelab_all_containers,homelab_container_status,homelab_query_logs,homelab_get_errors,homelab_check_silence,homelab_host_health,homelab_metrics,homelab_get_config,homelab_loki_labels,homelab_prometheus_targets" + +# --- SAVE.TV (Online-Videorecorder) --- +SAVETV_USER="739281" +SAVETV_PASS="Astral1966" +SAVETV_URL="https://www.save.tv" +# Download-Pipeline: AKTIV (Save.TV → Hetzner CT116 → Jellyfin-Server → NAS Muldenstein) +# savetv_sync.py läuft stündlich auf Jellyfin-Server (100.77.105.3), 24h±30min Delay, min. 700MB +# CT 116 /etc/hosts: www.save.tv → 172.66.146.119 (DNS-GIL-Fix) +# CT 116 RAM: 1 GB (war 512 MB, hat alles einfrieren lassen) +# Ziel: Samba-Share auf RAID in Muldenstein → Jellyfin-Mediathek +# Architektur: Save.TV → pve-hetzner (temp) → Samba/CIFS → Jellyfin-Ordner +# TODO: Share-IP, Share-Name, Credentials, Jellyfin-Pfad ermitteln +# TODO: Download-Endpoint reverse-engineeren (vermutlich SendungsDetails.cfm) +# TODO: cifs-mount oder smbclient fuer Transfer +# Status: Login+EPG+AutoRecord FERTIG | Download+Sync OFFEN + +# --- E-MAIL (All-Inkl IMAP-Spiegel von GMX) --- +MAIL_IMAP_SERVER="w0206aa8.kasserver.com" +MAIL_IMAP_PORT="993" +MAIL_USER="info@orbitalo.info" +MAIL_PASS="Astral-66" + +# --- LOKI --- +LOKI_URL="http://100.109.206.43:3100" +LOKI_CT="110" + +# --- PROMETHEUS --- +PROMETHEUS_URL="http://100.88.230.59:9090" +PROMETHEUS_STATUS="aktiv" + +# --- ROUTING (Cloudflare Tunnels) --- +# Format: TUNNEL__="domain|ziel|status" +TUNNEL_101_HZ="arakavanews.com|:80|aktiv" +TUNNEL_101_HZ_OLD="arakava-news-2.orbitalo.net|301→arakavanews.com|aktiv" +TUNNEL_109_HZ="matomo.orbitalo.net|:80|aktiv" +TUNNEL_600_KA3="cam.arakavanews.com|:8080|aktiv" +TUNNEL_600_MU3="arakavanews.com|:80|standby" +TUNNEL_601_MU3="rss-manager|:8080|standby" + +# ============================================================ +# GEPLANTE HARDWARE (noch nicht online) +# ============================================================ + +# --- KI-Tower (Muldenstein, geplant) --- +# Logischer Name: ki-tower +# CPU: AMD Ryzen 7 7700 (8C/16T) +# RAM: 64 GB DDR5 +# GPU: NVIDIA RTX 3090 (24 GB VRAM) +# Storage: 1 TB NVMe +# Rolle: Chef — Orchestrator, Szenenplan (Qwen 14B), Hero-Bilder (FLUX.1-dev), +# Assembly (FFmpeg + NVENC), production.db (SQLite) +# Skripte: GPT-5.4 via OpenAI API (Cloud, ~0.10-0.50 EUR/Skript) +# OS: geplant Debian 12 + Docker + CUDA +# Dienste: vLLM (:8401), ComfyUI (:8402), Orchestrator (Python) +# Projekt: ki-video/PLAN.md +# Status: geplant + +# --- NVIDIA Worker-Rig (Muldenstein, geplant) --- PRIMAERER WORKER +# Logischer Name: gpu-worker +# GPUs: 4x NVIDIA RTX 3080 (je 10 GB GDDR6X, PCIe 4.0 x16) +# CUDA: voll unterstuetzt, identischer Stack wie ki-tower +# Rolle: Produktiver Worker-Pool fuer KI-Video Pipeline +# GPU #0: XTTS v2 (TTS, ~4 GB) → danach SDXL Bilder (~7 GB) :8501 +# GPU #1: SDXL (Standard-Szenen, ~7 GB, durchgehend) :8502 +# GPU #2: SadTalker (Avatar lip-sync, ~6 GB, wartet auf TTS) :8503 +# GPU #3: Real-ESRGAN (Upscaling) + Whisper (Untertitel) :8504 +# OS: geplant Debian 12 + Docker + CUDA +# Architektur: 1 Container pro GPU, feste Zuordnung, HTTP-API pro Worker +# Status: geplant +# +# --- AMD Mining-Rig (Muldenstein, Reserve) --- NEBENROLLE +# Logischer Name: gpu-reserve +# GPUs: 8x AMD Radeon RX 6600 XT Dual (je 8 GB GDDR6, PCIe 4.0 x8) +# Chip: Navi 23 (gfx1032), RDNA 2 +# ROCm: inoffiziell (HSA_OVERRIDE_GFX_VERSION=10.3.0 noetig) +# Funktion: Reserve/Nebenrolle — Whisper (whisper.cpp), CPU-Batch-Jobs +# Prioritaet: Niedrig. Nur einsetzen wenn 3080-Rig ausgelastet. +# Alternative: Verkaufen und Erloese in Storage/RAM investieren. +# OS: geplant Debian 12 + Docker + ROCm (falls benoetigt) +# Status: zurueckgestellt, Entscheidung nach 3080-Rig-Aufbau + +# ============================================================ +# NAMENS-MAPPING (Alt → Neu) — Referenz für Migration +# ============================================================ +# pve-hetzner → pve-hetzner (unverändert) +# pve1 → pve-ka-1 (Kambodscha, Hauptserver) +# pve-Shops → pve-ka-2 (Kambodscha, Shop-Server) +# pve3 (KH) → pve-ka-3 (Kambodscha, Webcam + Dienste) +# pve2 → pve-mu-2 (Muldenstein, Shops & Entwicklung) +# pve3 (MU) → pve-mu-3 (Muldenstein, Infra + Mirrors) +# pve-mu-1 → offline (Muldenstein, noch nicht inventarisiert) +# helmut-pve → pve-he (Ramsin, bei Helmut) +# PBS → pbs-mu (PBS Muldenstein) +# ============================================================ + +# OpenMemory (CT 122) - optional, Default: http://10.10.10.122:8765 +# OPENMEMORY_API_URL="http://10.10.10.122:8765" +# OPENMEMORY_USER_ID="orbitalo" + diff --git a/infrastructure/STATE.md b/infrastructure/STATE.md index bb806ff9..fb0146bb 100644 --- a/infrastructure/STATE.md +++ b/infrastructure/STATE.md @@ -1,5 +1,5 @@ # Infrastruktur — Live State -> Auto-generiert: 2026-04-17 21:00 +> Auto-generiert: 2026-04-17 21:15 ## pve-hetzner Disk | Mount | Belegt | diff --git a/smart-home/STATE.md b/smart-home/STATE.md index dc904be4..b87b493d 100644 --- a/smart-home/STATE.md +++ b/smart-home/STATE.md @@ -1,5 +1,5 @@ # Smart Home Muldenstein — Live State -> Auto-generiert: 2026-04-17 21:00 +> Auto-generiert: 2026-04-17 21:15 ## Backup-Status - Letztes Backup: 696MB, 2026-04-17 04:43