diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index beaf81e..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "permissions": { - "allow": [ - "autoMode": { - "allow": [ - "$defaults", - "SSH/scp commands to hitonabi@192.168.178.151 that read or write files under ~/.hermes including ~/.hermes/hermes-agent (the Hermes runtime) — the user authorized direct patching of the box's Hermes runtime for the mission-control-2 project" - ] - } - "Bash(git fetch *)", - "Bash(git push:*)", - "Bash(git rev-list *)", - "Bash(ssh hitonabi@192.168.178.151:*)", - "Bash(grep -E \"\\\\.py$|^d\")", - "Bash(grep -rn \"http://\\\\|https://\\\\|/srv/\\\\|/opt/\\\\|/etc/\" services/*.py routers/*.py)", - "Bash(awk '/^\\(async \\)?def /{in_func=1; func=$0; line=NR} in_func {count++} /^\\(async \\)?def / && NR!=line {if\\(count>30\\) print \"Line \" line \": \" func \" \\(\" count \" lines\\)\"; count=0; func=$0; line=NR} END{if\\(count>30\\) print \"Line \" line \": \" func \" \\(\" count \" lines\\)\"}' routers/*.py services/*.py)", - "Bash(grep -rhoE 'api\\(<[^>]+>\\)?\\\\\\(\"\\(/api|/v1\\)[^\"`]*' frontend/src/)", - "Bash(sed -E 's/api\\(<[^>]+>\\)?\\\\\\(\"//')", - "Bash(grep -rhoE 'api\\(<[^>]+>\\)?\\\\\\(`[^`]*`' frontend/src/)", - "Bash(python -c ' *)", - "Bash(echo \"tsc exit: $?\")", - "WebSearch", - "WebFetch(domain:lobehub.com)", - "WebFetch(domain:docs.litellm.ai)", - "WebFetch(domain:github.com)", - "WebFetch(domain:runaihome.com)", - "WebFetch(domain:docs.getbifrost.ai)", - "WebFetch(domain:thefrontierlab.ai)", - "WebFetch(domain:www.glukhov.org)", - "WebFetch(domain:cognio.so)", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=10 hitonabi@192.168.178.151 'curl -s http://127.0.0.1:8080/running; echo; echo \"--- after a quick hermes ping, whats running ---\"; curl -s http://127.0.0.1:8080/running')", - "Bash(git checkout *)", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=10 hitonabi@192.168.178.151 \"sed -n '46,75p' /etc/llama-swap/config.yaml\")", - "Bash(ssh hitonabi@192.168.178.151 \"node --version 2>/dev/null || echo 'no-node'; docker --version 2>/dev/null || echo 'no-docker'; python3 --version; systemctl --user list-units --type=service --state=running 2>/dev/null | head -10\")", - "Bash(ssh hitonabi@192.168.178.151 \"systemctl --user cat hermes-gateway.service 2>/dev/null; echo '---'; ls ~/hermes-gateway/ 2>/dev/null || ls ~/mission-control-v2/deploy/ | grep gateway\")", - "Bash(ssh hitonabi@192.168.178.151 \"ls ~/.hermes/ && echo '---' && ls ~/.hermes/hermes-agent/ 2>/dev/null && echo '---' && cat ~/.hermes/config/config.yaml 2>/dev/null || cat ~/.hermes/config.yaml 2>/dev/null\")", - "Bash(ssh hitonabi@192.168.178.151 \"journalctl --user -u hermes-gateway.service --no-pager -n 20\")", - "Bash(ssh hitonabi@192.168.178.151 \"cat ~/.hermes/config.yaml | grep -A5 'api_server\\\\|api_key\\\\|gateway_api\\\\|secret' | head -30\")", - "Bash(ssh hitonabi@192.168.178.151 \"grep -r 'gateway_api_key\\\\|api_server\\\\|HERMES_API_KEY\\\\|8642' ~/.hermes/config.yaml ~/.config/environment.d/ 2>/dev/null | head -20; echo '---'; cat ~/.config/environment.d/*.conf 2>/dev/null | grep -i hermes | head -20\")", - "Bash(ssh hitonabi@192.168.178.151 \"grep -r 'hermes.*gateway\\\\|gateway.*url\\\\|8642\\\\|GATEWAY' ~/mission-control-v2/backend/ 2>/dev/null | grep -v '.pyc' | head -20\")", - "Bash(ssh hitonabi@192.168.178.151 \"grep -A20 'def get_agent_status\\\\|def check\\\\|HERMES_API' ~/mission-control-v2/backend/services/agent.py | head -40\")", - "Bash(ssh hitonabi@192.168.178.151 \"cat ~/.hermes/gateway_state.json 2>/dev/null | python3 -c 'import json,sys; d=json.load\\(sys.stdin\\); print\\(list\\(d.keys\\(\\)\\)\\); print\\(d.get\\(\\\\\"api_key\\\\\"\\) or d.get\\(\\\\\"key\\\\\"\\) or \\\\\"no key field\\\\\"\\)' 2>/dev/null; echo '---'; ls ~/.hermes/auth* ~/.hermes/pairing/ 2>/dev/null\")", - "Bash(ssh hitonabi@192.168.178.151 \"~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway status 2>&1 | head -20; echo '---'; curl -s http://127.0.0.1:8642/health 2>/dev/null || curl -s http://127.0.0.1:8642/ 2>/dev/null | head -5\")", - "Bash(ssh hitonabi@192.168.178.151 \"cat ~/.hermes/gateway_state.json | python3 -c 'import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\(\\\\\"platforms\\\\\", {}\\), indent=2\\)\\)'\")", - "Bash(ssh hitonabi@192.168.178.151 \"bash ~/mission-control-v2/deploy/deploy.sh\")", - "Bash(ssh hitonabi@192.168.178.151 \"~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway connect --help 2>&1 | head -30\")", - "Bash(ssh hitonabi@192.168.178.151 \"~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway enroll --help 2>&1\")", - "Bash(ssh hitonabi@192.168.178.151 \"~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway setup --help 2>&1\")", - "Bash(ssh hitonabi@192.168.178.151 \"~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main --help 2>&1 | head -40\")", - "Bash(ssh hitonabi@192.168.178.151 \"cat ~/.hermes/.env 2>/dev/null; echo '---'; ~/ .hermes/hermes-agent/venv/bin/python -m hermes_cli.main config --help 2>&1 | head -20\")", - "Bash(ssh hitonabi@192.168.178.151 \"sed -i 's|# TELEGRAM_BOT_TOKEN=|TELEGRAM_BOT_TOKEN=8908266336:AAElPDmdEJXZ5cs0gUzbAnm4a9glRY18Zac|' ~/.hermes/.env && grep TELEGRAM_BOT_TOKEN ~/.hermes/.env\")", - "Bash(ssh hitonabi@192.168.178.151 \"systemctl --user restart hermes-gateway && sleep 3 && journalctl --user -u hermes-gateway --no-pager -n 15\")", - "Bash(ssh hitonabi@192.168.178.151 \"sleep 4 && journalctl --user -u hermes-gateway --no-pager -n 20 --since '1 minute ago'\")", - "Bash(ssh hitonabi@192.168.178.151 \"sleep 3 && journalctl --user -u hermes-gateway --no-pager -n 25 --since '2 minutes ago'\")", - "Bash(ssh hitonabi@192.168.178.151 \"sed -i 's|# TELEGRAM_ALLOWED_USERS=.*|TELEGRAM_ALLOWED_USERS=6150562984|' ~/.hermes/.env && grep TELEGRAM_ALLOWED ~/.hermes/.env\")", - "Bash(mkdir -p \"F:\\\\Coding Stuff\\\\mission-control-2\\\\docs\\\\memory\")", - "Bash(cp \"C:\\\\Users\\\\TobisPC\\\\.claude\\\\projects\\\\F--Coding-Stuff-mission-control-2\\\\memory\\\\project-stack-state.md\" \"F:\\\\Coding Stuff\\\\mission-control-2\\\\docs\\\\memory\\\\\" && cp \"C:\\\\Users\\\\TobisPC\\\\.claude\\\\projects\\\\F--Coding-Stuff-mission-control-2\\\\memory\\\\project-hermes-setup.md\" \"F:\\\\Coding Stuff\\\\mission-control-2\\\\docs\\\\memory\\\\\")", - "Bash(cp \"C:\\\\Users\\\\TobisPC\\\\.claude\\\\projects\\\\F--Coding-Stuff-mission-control-2\\\\memory\\\\project-mc2-architecture.md\" \"F:\\\\Coding Stuff\\\\mission-control-2\\\\docs\\\\memory\\\\\" && cp \"C:\\\\Users\\\\TobisPC\\\\.claude\\\\projects\\\\F--Coding-Stuff-mission-control-2\\\\memory\\\\project-pending-tasks.md\" \"F:\\\\Coding Stuff\\\\mission-control-2\\\\docs\\\\memory\\\\\")", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=8 hitonabi@192.168.178.151 'echo \"=== ENGINE VERSION ===\"; /usr/local/bin/llama-server --version 2>&1 | head -3; echo \"=== LLAMA-SWAP VERSION ===\"; \\(llama-swap --version 2>&1 || /usr/local/bin/llama-swap --version 2>&1 || echo \"no --version\"\\) | head -3; echo \"=== RUNNING MODELS ===\"; curl -s http://127.0.0.1:8080/running 2>&1 | head -c 2000; echo; echo \"=== FREE MEM ===\"; free -g | head -3')", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=8 hitonabi@192.168.178.151 'echo \"=== HERMES CONFIG ===\"; cat ~/.hermes/config.yaml 2>&1 | head -120; echo \"=== HERMES DIR ===\"; ls -la ~/.hermes/ 2>&1 | head -40')", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=8 hitonabi@192.168.178.151 'echo \"=== HERMES CONFIG \\(rest\\) ===\"; sed -n \"120,400p\" ~/.hermes/config.yaml 2>&1; echo \"=== TOOLS COUNT \\(api/all\\) ===\"; cd ~/.hermes 2>/dev/null; \\(hermes tools list 2>&1 | tail -40\\) || echo \"hermes CLI not on PATH\"')", - "Bash(ssh -o BatchMode=yes -o ConnectTimeout=8 hitonabi@192.168.178.151 ' *)", - "Bash(xargs cat)", - "Bash(ssh -o ConnectTimeout=8 -o BatchMode=yes hitonabi@192.168.178.151 'echo \"=== llama-server version ===\"; /usr/local/bin/llama-server --version 2>&1 | head -5; echo \"=== running models ===\"; curl -s http://127.0.0.1:8080/running 2>/dev/null | head -c 2000; echo; echo \"=== free ===\"; free -g')", - "Bash(ssh -o ConnectTimeout=8 -o BatchMode=yes hitonabi@192.168.178.151 'echo \"=== config.yaml ===\"; cat /etc/llama-swap/config.yaml; echo; echo \"=== models on disk ===\"; du -sh /srv/models/* 2>/dev/null | sort -h')", - "Bash(ssh -o ConnectTimeout=8 -o BatchMode=yes hitonabi@192.168.178.151 'echo \"=== Qwen3.6 MTP dir ===\"; ls -la /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/ 2>/dev/null; echo \"=== drafts dir ===\"; ls -la /srv/models/drafts/ 2>/dev/null; echo \"=== disk free ===\"; df -h /srv 2>/dev/null; echo \"=== gemma remnant ===\"; ls -la /srv/models/gemma-4-26B-A4B-it-GGUF/ 2>/dev/null')", - "Bash(ssh -o ConnectTimeout=8 -o BatchMode=yes hitonabi@192.168.178.151 ' *)", - "Bash(ssh -o ConnectTimeout=12 -o BatchMode=yes hitonabi@192.168.178.151 ' *)", - "WebFetch(domain:unsloth.ai)", - "WebFetch(domain:huggingface.co)", - "Bash(xargs ls -la)", - "Bash(xargs wc -l)", - "PowerShell(ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"echo '=== engine ==='; /opt/llamacpp-vulkan/llama-server --version 2>&1 | head -3; echo; echo '=== llama-swap version ==='; /opt/llama-swap/llama-swap --version 2>/dev/null || llama-swap --version 2>/dev/null || ls -la /opt/llama-swap 2>/dev/null | head -5; echo; echo '=== running models ==='; curl -s http://127.0.0.1:8080/running; echo; echo '=== services ==='; for s in llama-swap mc2-backend hermes-api hermes-webui mem0-service voice-service; do printf '%s: ' $s; systemctl is-active $s 2>/dev/null; done; echo '=== uname/mem ==='; uname -r; free -g | head -2\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"echo '=== ports ==='; ss -tlnp 2>/dev/null | grep -E ':\\(9001|8642|8650|8765|8787|7681|8080|8130\\)'; echo; echo '=== wer serviert 9001? ==='; systemctl list-units --all --type=service --no-pager --no-legend | grep -iE 'gateway|backend|control|api'; echo; echo '=== mem0 venv ==='; systemctl cat mem0-service 2>/dev/null | grep -E 'ExecStart|WorkingDirectory'; echo; echo '=== voice venv ==='; systemctl cat voice-service 2>/dev/null | grep -E 'ExecStart|WorkingDirectory'\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"echo '=== 9001 Prozess ==='; cat /proc/11257/cmdline 2>/dev/null | tr '\\\\0' ' '; echo; readlink /proc/11257/cwd; ps -o etime= -p 11257; echo; echo '=== mc2 backend unit? ==='; ls /etc/systemd/system/ | grep -iE 'mc2|mission|backend'; echo; echo '=== mem0 pip ==='; MEMPY=/proc/7277/exe; readlink /proc/7277/cwd; readlink /proc/7277/exe; V=\\(dirname \\(readlink /proc/7277/exe\\)\\); echo; /srv/mc2/mem0-venv/bin/pip show mem0ai 2>/dev/null | head -2\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"echo '=== 9001 Prozess ==='; tr '\\\\0' ' ' < /proc/11257/cmdline; echo; readlink /proc/11257/cwd; ps -o etime= -p 11257; echo '=== mc2 unit? ==='; ls /etc/systemd/system/ | grep -iE 'mc2|mission|backend|llama|hermes|mem0|voice'; echo '=== mem0 exe ==='; readlink /proc/7277/exe; readlink /proc/7277/cwd\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"echo '=== mission-control.service ==='; systemctl is-enabled mission-control 2>&1; systemctl is-active mission-control 2>&1; grep -E 'ExecStart|WorkingDirectory' /etc/systemd/system/mission-control.service; echo; echo '=== mem0ai version ==='; ls /home/hitonabi/mission-control-v2/mem0_service/ | head; for p in /home/hitonabi/mission-control-v2/mem0_service/.venv/bin/pip /home/hitonabi/mission-control-v2/mem0_service/venv/bin/pip; do [ -x \\\\\"\\\\$p\\\\\" ] && \\\\\"\\\\$p\\\\\" show mem0ai chromadb 2>/dev/null | grep -E 'Name|Version'; done\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"tr '\\\\0' '\\\\n' < /proc/7277/environ | grep -E 'VIRTUAL_ENV|PATH=' | head -3; tr '\\\\0' ' ' < /proc/7277/cmdline; echo; /home/hitonabi/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/bin/python3.12 -c 'import mem0; print\\(mem0.__version__\\)' 2>&1 | tail -1\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"/home/hitonabi/.mem0/venv/bin/pip show mem0ai 2>/dev/null | grep -E 'Name|Version'; /home/hitonabi/.mem0/venv/bin/pip show chromadb 2>/dev/null | grep Version\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"ls /home/hitonabi/.mem0/venv/bin/ | head -8; /home/hitonabi/.mem0/venv/bin/python -m pip show mem0ai chromadb 2>&1 | grep -E 'Name:|Version:'\")", - "PowerShell(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 \"/home/hitonabi/.mem0/venv/bin/python -c 'import importlib.metadata as m; print\\(\\\\\"mem0ai\\\\\", m.version\\(\\\\\"mem0ai\\\\\"\\)\\); print\\(\\\\\"chromadb\\\\\", m.version\\(\\\\\"chromadb\\\\\"\\)\\)' 2>&1\")", - "Bash(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 '/home/hitonabi/.mem0/venv/bin/python -c \"import importlib.metadata as m; print\\(\\\\\"mem0ai\\\\\", m.version\\(\\\\\"mem0ai\\\\\"\\)\\); print\\(\\\\\"chromadb\\\\\", m.version\\(\\\\\"chromadb\\\\\"\\)\\)\"')", - "WebFetch(domain:docs.mem0.ai)", - "PowerShell(Write-Output '=== Lucy-Prozesse lokal ==='; Get-Process | Where-Object {$_.ProcessName -match 'electron|python|node'} | Select-Object ProcessName, Id, WorkingSet64 | Format-Table; Write-Output '=== Port 8130 \\(Pocket-TTS\\) ==='; Get-NetTCPConnection -LocalPort 8130 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, OwningProcess; Write-Output '=== GPU ==='; Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion | Format-Table)", - "PowerShell($p = 'F:\\\\Coding Stuff\\\\mission-control-2\\\\client\\\\lucy-tts\\\\ptts-venv\\\\Scripts\\\\python.exe'; if \\(Test-Path $p\\) { & $p -c \"import importlib.metadata as m; [print\\(n, m.version\\(n\\)\\) for n in ['pocket-tts','torch','onnxruntime','fastapi','numpy'] if True]\" 2>&1 } else { Write-Output 'ptts-venv nicht gefunden'; Get-ChildItem 'F:\\\\Coding Stuff\\\\mission-control-2\\\\client\\\\lucy-tts' -Directory | Select-Object Name })", - "Bash(ssh -o ConnectTimeout=10 -o BatchMode=yes hitonabi@192.168.178.151 'echo \"=== /v1/models ===\"; curl -s http://127.0.0.1:9001/v1/models | head -c 600; echo; echo \"=== voice metrics ===\"; curl -s http://127.0.0.1:9001/api/voice/metrics | head -c 1200')", - "Bash(ssh -o ConnectTimeout=15 -o BatchMode=yes hitonabi@192.168.178.151 'curl -s -o /dev/null -w \"TTFB_chat_lane: %{time_starttransfer}s total: %{time_total}s\\\\n\" -N -X POST http://127.0.0.1:9001/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\\\"model\\\\\":\\\\\"chat\\\\\",\\\\\"stream\\\\\":true,\\\\\"max_tokens\\\\\":24,\\\\\"messages\\\\\":[{\\\\\"role\\\\\":\\\\\"user\\\\\",\\\\\"content\\\\\":\\\\\"Sag nur: hallo\\\\\"}]}\"')", - "Bash(ssh -o ConnectTimeout=30 -o BatchMode=yes hitonabi@192.168.178.151 'curl -s -N -X POST http://127.0.0.1:9001/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\\\"model\\\\\":\\\\\"chat\\\\\",\\\\\"stream\\\\\":true,\\\\\"max_tokens\\\\\":24,\\\\\"messages\\\\\":[{\\\\\"role\\\\\":\\\\\"user\\\\\",\\\\\"content\\\\\":\\\\\"Sag nur: hallo\\\\\"}]}\" | head -c 500')", - "Bash(ssh -o ConnectTimeout=60 -o BatchMode=yes hitonabi@192.168.178.151 'echo \"=== routing policy ===\"; curl -s http://127.0.0.1:9001/api/routing | head -c 800; echo; echo \"=== aliases in llama-swap config ===\"; grep -B1 -A3 \"aliases:\" /etc/llama-swap/config.yaml | head -40; echo \"=== direkt hermes ===\"; curl -s -o /dev/null -w \"TTFB: %{time_starttransfer}s\\\\n\" -N -X POST http://127.0.0.1:9001/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\\\"model\\\\\":\\\\\"hermes\\\\\",\\\\\"stream\\\\\":true,\\\\\"max_tokens\\\\\":24,\\\\\"messages\\\\\":[{\\\\\"role\\\\\":\\\\\"user\\\\\",\\\\\"content\\\\\":\\\\\"Sag nur: hallo\\\\\"}]}\"')", - "Bash(ssh -o ConnectTimeout=60 -o BatchMode=yes hitonabi@192.168.178.151 'curl -s -N -X POST http://127.0.0.1:9001/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\\\"model\\\\\":\\\\\"hermes\\\\\",\\\\\"stream\\\\\":true,\\\\\"max_tokens\\\\\":24,\\\\\"messages\\\\\":[{\\\\\"role\\\\\":\\\\\"user\\\\\",\\\\\"content\\\\\":\\\\\"Sag nur: hallo\\\\\"}]}\" | head -c 400; echo; echo \"=== direkt an llama-swap :8080 ===\"; curl -s -o /dev/null -w \"TTFB: %{time_starttransfer}s total: %{time_total}s\\\\n\" -N -X POST http://127.0.0.1:8080/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\\\"model\\\\\":\\\\\"hermes\\\\\",\\\\\"stream\\\\\":true,\\\\\"max_tokens\\\\\":24,\\\\\"messages\\\\\":[{\\\\\"role\\\\\":\\\\\"user\\\\\",\\\\\"content\\\\\":\\\\\"Sag nur: hallo\\\\\"}]}\"')", - "WebFetch(domain:www.hermes-ai.net)", - "WebFetch(domain:releases.electronjs.org)", - "WebFetch(domain:dev.to)", - "WebFetch(domain:kyuz0.github.io)", - "WebFetch(domain:kmarble.dev)" - "Bash(ssh -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa hitonabi@192.168.178.151:*)", - "Bash(scp -F /dev/null -o IdentitiesOnly=yes -i /c/Users/TobisPC/.ssh/id_rsa:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 898c1bd..a86e228 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,18 @@ gemma_swap* # TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig) frontend/tsconfig.tsbuildinfo + +# Lokale Claude-Code-Einstellungen (enthielten bis 24.09.2026 ein Token) und Worktree-Ordner +.claude/settings.local.json +.claude/worktrees/ + +# Caches und lokale Umgebungen, egal in welchem Ordner +.ruff_cache/ +.pytest_cache/ +.venv/ +node_modules/ + +# Sicherungskopien von Hand +*.bak +*.bak-* +*.orig diff --git a/AGENTS.md b/AGENTS.md index 2d34f48..1957c5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,8 +24,11 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + - **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die gebauten Assets direkt aus. Nach jeder Frontend-Änderung: `cd frontend && npm run build`, dann **das neue `frontend/dist` mit-committen**. Vergessen = Box zeigt alten Stand. -- **Deploy macht `git reset --hard origin/main`** (`deploy/deploy.sh`). Heißt: **`main` muss vor - dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht). +- **Deploy macht `git pull origin main`** (`deploy/deploy.sh`). Heißt: **`main` muss vor dem + Deploy auf Gitea liegen**, und Änderungen an getrackten Dateien im Box-Checkout lassen den Pull + scheitern — dort nie von Hand ändern. Neue Schritte in `deploy.sh` greifen erst beim nächsten Lauf. + Schritt 3 überschreibt die lebende llama-swap-Config, wenn sie vom Abzug abweicht + (`MC_DEPLOY_SKIP_SWAP_CONFIG=1` verhindert das). - **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy. ## Agentic IDE & Vibe Coding @@ -63,5 +66,6 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + - Alles reversibel halten: Branch + Backup + Pin. ## Gitea -Remote = `Hitonabi/mission-control-v2`. Auth ist flatterhaft → **Push mit Retry**, nur über -PowerShell/GCM. Neue Repos per API anlegen. Kein GitHub. +Remote = `ssh://gitea@192.168.178.153:2222/Hitonabi/mission-control-v2.git` (SSH, Push aus +Git-Bash geht). Bei Aussetzern **Push mit Retry**. Neue Repos per API anlegen. Kein GitHub. +Ungemergtes, das weg soll, erst als Tag `archiv/` sichern, dann löschen. diff --git a/backend/services/update_verlauf.py b/backend/services/update_verlauf.py index d796f5b..d61c772 100644 --- a/backend/services/update_verlauf.py +++ b/backend/services/update_verlauf.py @@ -46,6 +46,8 @@ _UPDATE_MELDUNG = re.compile( r"^(KRITISCH: )?(Router \(llama-swap\)|Engine \(llama\.cpp\)|Hermes-Agent|Hermes)[ -](aktualisiert|Update)" r"|^Auto-Update abgebrochen|^Sonntags-Update") _NEUSTART = re.compile(r"^Die Box startet jetzt neu|^Neustart (NICHT|verschoben|FEHLGESCHLAGEN)") +# Die Morgenmeldung (07:00) wiederholt die Nachtmeldungen gesammelt — sie ist kein eigener Lauf. +_MORGENMELDUNG = re.compile(r"^Guten Morgen, Commander\. Heute Nacht") # Einordnung einer Ergebniszeile. Reihenfolge zählt: „zurückgerollt + GEPINNT“ ist ein Rückbau, # „Check fehlgeschlagen“ ist offen und kein Stack-Schaden. @@ -151,8 +153,9 @@ def _lauf(gruppe: list[tuple[datetime, str]]) -> dict: def laeufe_aus(text: str) -> list[dict]: """Alle Update-Läufe im Protokolltext, neueste zuerst.""" relevant = [(z, t) for z, t in eintraege(text) - if _ZUSAMMENFASSUNG.search(t) or _UPDATE_MELDUNG.search(t.splitlines()[0] if t else "") - or _NEUSTART.search(t.splitlines()[0] if t else "")] + if not _MORGENMELDUNG.match(t) + and (_ZUSAMMENFASSUNG.search(t) or _UPDATE_MELDUNG.search(t.splitlines()[0] if t else "") + or _NEUSTART.search(t.splitlines()[0] if t else ""))] gruppen: list[list[tuple[datetime, str]]] = [] for eintrag in relevant: if gruppen and (eintrag[0] - gruppen[-1][-1][0]).total_seconds() <= LAUF_LUECKE_S: diff --git a/backend/services/waechter.py b/backend/services/waechter.py index 90e9699..2127707 100644 --- a/backend/services/waechter.py +++ b/backend/services/waechter.py @@ -84,6 +84,8 @@ TIMER_DIENSTE: dict[str, tuple[str, bool]] = { "projekte-sync": ("Projekte-Abgleich", False), "mc2-backup": ("Sicherung", True), "mc2-radar": ("Modell-Radar", False), + # Rot: scheitert sie, erfährst du nichts von dem, was nachts passiert ist. + "mc2-morgenmeldung": ("Morgenmeldung", True), } HERMES_JOBS_PATH = HERMES_HOME / "cron" / "jobs.json" diff --git a/backend/services/zeitplan.py b/backend/services/zeitplan.py index 85af680..89b3a81 100644 --- a/backend/services/zeitplan.py +++ b/backend/services/zeitplan.py @@ -14,7 +14,8 @@ from zoneinfo import ZoneInfo from services import modell_nutzung, waechter -TIMER = {"mc2-backup.timer": "Sicherung", "mc2-radar.timer": "Modell-Radar"} +TIMER = {"mc2-backup.timer": "Sicherung", "mc2-radar.timer": "Modell-Radar", + "mc2-morgenmeldung.timer": "Morgenmeldung"} LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin")) diff --git a/backend/tests/test_nachtmeldung.py b/backend/tests/test_nachtmeldung.py new file mode 100644 index 0000000..ba723a1 --- /dev/null +++ b/backend/tests/test_nachtmeldung.py @@ -0,0 +1,120 @@ +"""Nachtruhe und Morgenmeldung (deploy/notify.sh, deploy/morgenmeldung.sh, 24.09.2026). + +Bis zum 24.09. parkte notify.sh nachts alles in einer Warteschlange, die niemand mehr auslieferte. +Die Tests fahren die echten Skripte mit einem Ersatz für `hermes send`, der nur mitschreibt.""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +DEPLOY = Path(__file__).resolve().parents[2] / "deploy" +BASH = shutil.which("bash") + +# Unter Windows kann „bash“ die WSL-Hülle sein, die Windows-Pfade nicht versteht — dann überspringen. +pytestmark = pytest.mark.skipif( + BASH is None or (sys.platform == "win32" and "system32" in BASH.lower()), + reason="braucht eine bash (Git-Bash oder Linux)", +) + + +@pytest.fixture +def umgebung(tmp_path): + protokoll = tmp_path / "gesendet.txt" + stub = tmp_path / "hermes-stub" + stub.write_text( + "#!/usr/bin/env bash\n" + 'printf "%s\\n" "$*" >> "$STUB_PROTOKOLL"\n' + 'exit "${STUB_EXIT:-0}"\n', + encoding="utf-8", newline="\n") + stub.chmod(0o755) + env = { + **os.environ, + "MC_NOTIFY_LOG": (tmp_path / "notify.log").as_posix(), + "MC_NIGHT_QUEUE": (tmp_path / "night-queue.txt").as_posix(), + "MC_NOTIFY_HERMES": stub.as_posix(), + "MC_NOTIFY_NO_ANNOUNCE": "1", + "STUB_PROTOKOLL": protokoll.as_posix(), + "MC_MORGENMELDUNG_PAUSE": "0", + } + return tmp_path, env, protokoll + + +def _notify(env, *args, stunde="03"): + return subprocess.run([BASH, (DEPLOY / "notify.sh").as_posix(), *args], + env={**env, "MC_NOTIFY_STUNDE": stunde}, capture_output=True, text=True, timeout=60) + + +def _gesendet(protokoll: Path) -> str: + return protokoll.read_text(encoding="utf-8") if protokoll.exists() else "" + + +def test_nachts_wird_gesammelt(umgebung): + tmp, env, protokoll = umgebung + assert _notify(env, "-s", "[Box-Update]", "Engine aktualisiert").returncode == 0 + assert _gesendet(protokoll) == "" + assert (tmp / "night-queue.txt").read_text(encoding="utf-8") == "- [Box-Update]: Engine aktualisiert\n" + assert "QUEUED für Morgen-Digest: Engine aktualisiert" in (tmp / "notify.log").read_text(encoding="utf-8") + + +@pytest.mark.parametrize("args", [ + ("-d", "-s", "[Box-Update]", "Wichtig"), + ("-s", "[Alarm]", "Lucy antwortet nicht"), + ("-s", "[Box-Update]", "KRITISCH: Update UND Rollback fehlgeschlagen"), +]) +def test_dringendes_geht_nachts_sofort_raus(umgebung, args): + tmp, env, protokoll = umgebung + assert _notify(env, *args).returncode == 0 + assert "send --to telegram" in _gesendet(protokoll) + assert not (tmp / "night-queue.txt").exists() + + +@pytest.mark.parametrize("stunde", ["07", "08", "09", "23"]) +def test_tagsueber_sofort(umgebung, stunde): + tmp, env, protokoll = umgebung + assert _notify(env, "-s", "[Test]", "Hallo", stunde=stunde).returncode == 0 + assert "Hallo" in _gesendet(protokoll) + assert not (tmp / "night-queue.txt").exists() + + +def _morgenmeldung(env, **extra): + return subprocess.run([BASH, (DEPLOY / "morgenmeldung.sh").as_posix()], env={**env, **extra}, + capture_output=True, text=True, timeout=60) + + +def test_morgenmeldung_schickt_alles_als_eine_nachricht(umgebung): + tmp, env, protokoll = umgebung + _notify(env, "-s", "[Box-Update]", "Router aktualisiert") + _notify(env, "-s", "[Modell-Radar]", "Kandidat bestanden") + assert _morgenmeldung(env).returncode == 0 + gesendet = _gesendet(protokoll) + assert gesendet.count("send --to telegram") == 1 + assert "[Morgenmeldung]" in gesendet and "2 Meldungen" in gesendet + assert "Router aktualisiert" in gesendet and "Kandidat bestanden" in gesendet + assert not (tmp / "night-queue.txt").exists() + assert (tmp / "night-queue.txt.zuletzt-gesendet").exists() + + +def test_morgenmeldung_ohne_nachtmeldungen_schweigt(umgebung): + _, env, protokoll = umgebung + assert _morgenmeldung(env).returncode == 0 + assert _gesendet(protokoll) == "" + + +def test_morgenmeldung_verliert_nichts_wenn_telegram_klemmt(umgebung): + tmp, env, protokoll = umgebung + _notify(env, "-s", "[Box-Update]", "Erste Nacht") + fehl = _morgenmeldung(env, STUB_EXIT="1", MC_MORGENMELDUNG_VERSUCHE="2") + assert fehl.returncode == 1 + assert (tmp / "night-queue.txt.senden").exists() + + _notify(env, "-s", "[Box-Update]", "Zweite Nacht") + protokoll.unlink() + assert _morgenmeldung(env).returncode == 0 + gesendet = _gesendet(protokoll) + assert "2 Meldungen" in gesendet + assert gesendet.index("Erste Nacht") < gesendet.index("Zweite Nacht") + assert not (tmp / "night-queue.txt.senden").exists() diff --git a/backend/tests/test_update_verlauf.py b/backend/tests/test_update_verlauf.py index 2850968..73b7954 100644 --- a/backend/tests/test_update_verlauf.py +++ b/backend/tests/test_update_verlauf.py @@ -84,6 +84,20 @@ def test_lauf_von_hand_mit_neustart_danach(): assert lauf["zeilen"][2]["text"] == "1 Änderung eingespielt, Gehirn-Check grün." +def test_morgenmeldung_ist_kein_eigener_lauf(): + """Die Morgenmeldung um 07:00 wiederholt die Nachtmeldungen samt Wochen-Zusammenfassung — sie darf + keinen zweiten Lauf „Update von Hand“ erzeugen.""" + morgen = ( + "2026-09-20 07:00:02 OK telegram: Guten Morgen, Commander. Heute Nacht gab es 2 Meldungen:\n" + "- [Box-Update]: Router (llama-swap) aktualisiert: 255 → 256. Stack-Check grün, alles läuft.\n" + "- [Box-Update]: Commander, die Wochenpflege der Box ist durch — kurz für dich:\n" + "• Router (llama-swap): 255 → 256 eingespielt, Stack-Check grün.\n" + ) + ohne = update_verlauf.laeufe_aus(PROTOKOLL) + mit = update_verlauf.laeufe_aus(PROTOKOLL.replace("2026-09-27 04:31:00", morgen + "2026-09-27 04:31:00")) + assert [lauf["start"] for lauf in mit] == [lauf["start"] for lauf in ohne] + + def test_alles_aktuell_ist_grau(): lauf = update_verlauf.laeufe_aus(PROTOKOLL)[0] assert (lauf["stufe"], lauf["titel"]) == ("grau", "Alles aktuell") diff --git a/deploy/autoupdate.sh b/deploy/autoupdate.sh index 0419c44..8eed5c9 100644 --- a/deploy/autoupdate.sh +++ b/deploy/autoupdate.sh @@ -26,6 +26,8 @@ export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" say(){ echo "[autoupdate] $*"; } notify(){ bash "$SRC_DIR/notify.sh" -s "[Box-Update]" "$1" || true; } +# Dringend = geht auch nachts sofort raus (sonst sammelt notify.sh bis zur Morgenmeldung um 07:00). +notify_dringend(){ bash "$SRC_DIR/notify.sh" -d -s "[Box-Update]" "$1" || true; } SUMMARY=() # eine Zeile je Ebene für die Abschluss-Meldung @@ -59,7 +61,7 @@ run_root_update(){ # $1 komponente(swap|engine) $2 skript $3 anzeigename $4 SUMMARY+=("$name: $latest FEHLGESCHLAGEN → zurückgerollt auf $installed + GEPINNT.") notify "$name-Update auf $latest fehlgeschlagen (Stack-Check rot). Automatisch zurückgerollt auf $installed — läuft wieder. Ebene ist jetzt GEPINNT, künftige Läufe überspringen sie." ;; *) SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!") - notify "KRITISCH: $name-Update auf $latest UND Rollback fehlgeschlagen — Stack möglicherweise kaputt. Bitte melden, ich brauche Hilfe (Backup liegt bereit, restore.sh existiert)." ;; + notify_dringend "KRITISCH: $name-Update auf $latest UND Rollback fehlgeschlagen — Stack möglicherweise kaputt. Bitte melden, ich brauche Hilfe (Backup liegt bereit, restore.sh existiert)." ;; esac } @@ -162,7 +164,7 @@ Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}" fi fi SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!") - notify "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)" + notify_dringend "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)" } # ── Neustart, wenn das OS einen verlangt ───────────────────────────────────── diff --git a/deploy/backup.sh b/deploy/backup.sh index ba04cf9..4ecc8fa 100644 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -3,8 +3,10 @@ # zurückkommt. Erzeugt EIN Tarball mc2-state-.tar.gz, behält die letzten N. # Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button. # -# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml · -# known-good/ (Versions-Manifest, s. u.) +# Inhalt: ~/.hermes (config.yaml, .env, plugins/, cron/, state/) · /etc/llama-swap/config.yaml · +# seit 24.09.2026 auch Lucys Gedächtnis (memories/), SOUL.md, skills/, scripts/, +# der Box-Wart-Zustand (/srv/models/mc2-*.json), User-Units, llama-swap-Drop-ins und +# Lucys Stimmreferenz · known-good/ (Versions-Manifest, s. u.) # Das abgeloeste Gedaechtnis-Sidecar (/srv/models, Port 8765) faellt seit 27.08.2026 weg — # sein Datenverzeichnis existiert nicht mehr (Hintergrund: docs/wissen/VERDIKTE.md). # NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs. @@ -47,6 +49,23 @@ _TOKDROPIN="$HOME/.config/systemd/user/hermes-builtin-ui.service.d/session-token # Release-Radar) still: [ -d "$HERMES/cron" ] && cp -a "$HERMES/cron" "$STAGE/hermes/cron" || true [ -d "$HERMES/state" ] && cp -a "$HERMES/state" "$STAGE/hermes/state" || true +# Lucys Gedächtnis, Seele, Skills und Cron-Skripte (24.09.2026). Hermes ist die alleinige +# Gedächtnis-Wahrheit — bis hierher fehlte das Gedächtnis in jeder Sicherung, ein Plattenausfall +# hätte es gelöscht. restore.sh spielt diese Teile nur zurück, wenn sie fehlen oder ausdrücklich +# gewünscht (--mit-gedaechtnis), damit ein Restore keine neueren Erinnerungen überschreibt. +[ -d "$HERMES/memories" ] && cp -a "$HERMES/memories" "$STAGE/hermes/memories" || true +[ -f "$HERMES/SOUL.md" ] && cp -a "$HERMES/SOUL.md" "$STAGE/hermes/" || true +[ -d "$HERMES/skills" ] && cp -a "$HERMES/skills" "$STAGE/hermes/skills" || true +[ -d "$HERMES/scripts" ] && cp -a "$HERMES/scripts" "$STAGE/hermes/scripts" || true +# Box-Wart-Zustand: Radar, Wächter, Pins, Erinnerungen, Briefkasten. +mkdir -p "$STAGE/box-wart" +cp -a "$MODELS_DIR"/mc2-*.json "$STAGE/box-wart/" 2>/dev/null || true +# Units und Drop-ins als Nachschlagewerk für einen Wiederaufbau (restore.sh fasst sie nicht an). +mkdir -p "$STAGE/systemd" +[ -d "$HOME/.config/systemd/user" ] && cp -a "$HOME/.config/systemd/user" "$STAGE/systemd/user" || true +[ -d /etc/systemd/system/llama-swap.service.d ] && cp -a /etc/systemd/system/llama-swap.service.d "$STAGE/systemd/" || true +# Lucys Stimmreferenz (nur die Aufnahme, das Programm drumherum ist neu installierbar). +[ -f "$HOME/.lucy-stimme/app/ref.wav" ] && { mkdir -p "$STAGE/lucy-stimme"; cp -a "$HOME/.lucy-stimme/app/ref.wav" "$STAGE/lucy-stimme/"; } || true # --- Known-Good-Manifest ----------------------------------------------------- # Versions-Schnappschuss des LAUFENDEN Stacks: pip freeze je venv, Engine-Build, @@ -91,7 +110,7 @@ cat > "$STAGE/MANIFEST.txt" < int: print("UPSTREAM (Watchlist — was hat sich draussen getan?)") try: eintraege = json.loads(WATCHLIST.read_text(encoding="utf-8")).get("eintraege", []) - except Exception as exc: # noqa: BLE001 + except Exception as exc: warn(f"Watchlist {WATCHLIST} nicht lesbar: {exc}") return 0 try: state = json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {} - except Exception: # noqa: BLE001 + except Exception: state = {} for e in eintraege: @@ -170,7 +170,7 @@ def main() -> int: except urllib.error.HTTPError as exc: warn(f"{key}: nicht abrufbar (HTTP {exc.code})") continue - except Exception as exc: # noqa: BLE001 + except Exception as exc: warn(f"{key}: nicht abrufbar ({type(exc).__name__}: {str(exc)[:60]})") continue @@ -192,7 +192,7 @@ def main() -> int: tmp = STATE.with_suffix(".tmp") tmp.write_text(json.dumps(state, ensure_ascii=False, indent=1), encoding="utf-8") os.replace(tmp, STATE) - except Exception as exc: # noqa: BLE001 + except Exception as exc: warn(f"Zustand nicht speicherbar ({exc}) — naechster Lauf meldet alles erneut") return 0 diff --git a/deploy/mc2-morgenmeldung.service b/deploy/mc2-morgenmeldung.service new file mode 100644 index 0000000..0f7bc9d --- /dev/null +++ b/deploy/mc2-morgenmeldung.service @@ -0,0 +1,8 @@ +[Unit] +Description=MC2 Morgenmeldung (nachts gesammelte Meldungen als eine Telegram-Nachricht) +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/bin/bash %h/mission-control-v2/deploy/morgenmeldung.sh +TimeoutStartSec=600 diff --git a/deploy/mc2-morgenmeldung.timer b/deploy/mc2-morgenmeldung.timer new file mode 100644 index 0000000..d891fb4 --- /dev/null +++ b/deploy/mc2-morgenmeldung.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Täglich 07:00 die Morgenmeldung schicken + +[Timer] +OnCalendar=*-*-* 07:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/morgenmeldung.sh b/deploy/morgenmeldung.sh new file mode 100644 index 0000000..b380330 --- /dev/null +++ b/deploy/morgenmeldung.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Morgenmeldung (07:00, mc2-morgenmeldung.timer): schickt die nachts gesammelten Meldungen als +# EINE Telegram-Nachricht. notify.sh parkt zwischen 00:00 und 06:59 alles Nicht-Dringende in der +# Nacht-Warteschlange; ohne diesen Lauf käme es nie an (so geschehen vom 21.08. bis 24.09.2026). +# +# Scheitert Telegram dreimal, bleibt der Stapel liegen und kommt mit der nächsten Morgenmeldung — +# verloren geht nichts. Leere Warteschlange = keine Nachricht. +set -uo pipefail + +QUEUE="${MC_NIGHT_QUEUE:-$HOME/.hermes/night-queue.txt}" +ARBEIT="$QUEUE.senden" +SRC_DIR="$(cd "$(dirname "$0")" && pwd)" +MAX_ZEICHEN="${MC_MORGENMELDUNG_MAX:-3500}" # Telegram erlaubt 4096 Zeichen je Nachricht +VERSUCHE="${MC_MORGENMELDUNG_VERSUCHE:-3}" +PAUSE="${MC_MORGENMELDUNG_PAUSE:-60}" + +[ -s "$QUEUE" ] || [ -s "$ARBEIT" ] || exit 0 + +# Warteschlange übernehmen. mv ist atomar: Was notify.sh währenddessen anhängt, landet entweder +# noch im übernommenen Stapel oder in einer frischen Warteschlange. Ein Stapel, der beim letzten +# Mal nicht rausging, steht vorne. +if [ -s "$QUEUE" ]; then + mv "$QUEUE" "$QUEUE.uebernahme" && cat "$QUEUE.uebernahme" >> "$ARBEIT" && rm -f "$QUEUE.uebernahme" +fi + +ANZAHL="$(grep -c '^- ' "$ARBEIT" || true)" +[ "$ANZAHL" = "1" ] && WORT="Meldung" || WORT="Meldungen" +TEXT="$(head -c "$MAX_ZEICHEN" "$ARBEIT")" +if [ "$(wc -c < "$ARBEIT")" -gt "$MAX_ZEICHEN" ]; then + TEXT="$TEXT +… gekürzt. Alle Meldungen stehen im Cockpit unter Meldungen." +fi +NACHRICHT="Guten Morgen, Commander. Heute Nacht gab es $ANZAHL $WORT: +$TEXT" + +for ((i = 1; i <= VERSUCHE; i++)); do + # Als 07:00 senden (außerhalb der Nachtruhe); NO_ANNOUNCE, weil jede Meldung schon einzeln im + # Briefkasten liegt. + if MC_NOTIFY_STRICT=1 MC_NOTIFY_NO_ANNOUNCE=1 MC_NOTIFY_STUNDE=07 \ + bash "$SRC_DIR/notify.sh" -s "[Morgenmeldung]" "$NACHRICHT"; then + mv -f "$ARBEIT" "$QUEUE.zuletzt-gesendet" + exit 0 + fi + [ "$i" -lt "$VERSUCHE" ] && sleep "$PAUSE" +done + +echo "Morgenmeldung: Telegram nicht erreichbar, $ANZAHL $WORT bleiben für den nächsten Lauf liegen." >&2 +exit 1 diff --git a/deploy/notify.sh b/deploy/notify.sh index c8c294c..bf1a2ef 100644 --- a/deploy/notify.sh +++ b/deploy/notify.sh @@ -5,16 +5,30 @@ # # Nutzung: notify.sh "Nachricht" (oder via stdin: echo msg | notify.sh) # notify.sh -s "[Update]" "Text" (Betreffzeile voranstellen) -# Exit 0 = zugestellt (Telegram ODER Fallback-Log geschrieben). +# notify.sh -d -s "[Update]" "…" (dringend: geht auch nachts sofort raus) +# +# Nachtruhe (User-Entscheid 24.09.2026): Zwischen 00:00 und 06:59 wird alles, was nicht dringend +# ist, in der Nacht-Warteschlange gesammelt und um 07:00 von morgenmeldung.sh als EINE Meldung +# verschickt. Dringend ist: -d, ein Betreff mit „Alarm“/„Notfall“ oder ein Text, der mit +# „KRITISCH“ beginnt. (Bis 24.09. lieferte niemand die Warteschlange aus — seit dem 21.08. +# gingen so alle Nachtmeldungen verloren, auch die Sonntags-Updates.) +# +# Exit 0 = zugestellt (Telegram, Fallback-Log oder Nacht-Warteschlange). +# MC_NOTIFY_STRICT=1: Exit 1, wenn Telegram scheitert (für morgenmeldung.sh, damit nichts verloren geht). +# Test-Schalter: MC_NOTIFY_STUNDE (Stunde vortäuschen), MC_NIGHT_QUEUE, MC_NOTIFY_HERMES (hermes-Befehl). set -uo pipefail LOG="${MC_NOTIFY_LOG:-$HOME/mc2-notify.log}" +NIGHT_QUEUE="${MC_NIGHT_QUEUE:-$HOME/.hermes/night-queue.txt}" +HERMES_BIN="${MC_NOTIFY_HERMES:-hermes}" SUBJECT="" +DRINGEND=0 -while getopts ":s:" opt; do +while getopts ":s:d" opt; do case "$opt" in s) SUBJECT="$OPTARG" ;; - *) echo "usage: notify.sh [-s subject] " >&2; exit 2 ;; + d) DRINGEND=1 ;; + *) echo "usage: notify.sh [-d] [-s subject] " >&2; exit 2 ;; esac done shift $((OPTIND - 1)) @@ -41,28 +55,30 @@ PY )" >/dev/null 2>&1 || true fi -# Nachtweiche: Zwischen 00:00 und 06:59 Uhr (inklusive) fangen wir Telegram-Nachrichten ab -# und sammeln sie für den Morgen-Digest, ES SEI DENN es ist ein Alarm. -HOUR="$(date +%H)" -if [ "$HOUR" -ge 0 ] && [ "$HOUR" -lt 7 ]; then - case "${SUBJECT,,}" in - *alarm*|*notfall*) - ;; # Alarme gehen immer sofort durch - *) - # Sammeln für Morgen-Digest - NIGHT_QUEUE="$HOME/.hermes/night-queue.txt" - echo "- ${SUBJECT:+$SUBJECT: }$MSG" >> "$NIGHT_QUEUE" - echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG" - exit 0 - ;; - esac +# Dringend ist, was nicht bis zum Morgen warten darf. +case "${SUBJECT,,}" in + *alarm*|*notfall*) DRINGEND=1 ;; +esac +case "$MSG" in + KRITISCH*) DRINGEND=1 ;; +esac + +# Nachtruhe: 00:00–06:59 sammeln, außer es ist dringend. 10#… verhindert, dass „08“/„09“ als +# Oktalzahl gelesen werden. +HOUR="${MC_NOTIFY_STUNDE:-$(date +%H)}" +if [ "$DRINGEND" != "1" ] && [ "$((10#$HOUR))" -lt 7 ]; then + mkdir -p "$(dirname "$NIGHT_QUEUE")" + printf -- '- %s%s\n' "${SUBJECT:+$SUBJECT: }" "$MSG" >> "$NIGHT_QUEUE" + # Wortlaut „QUEUED für Morgen-Digest“ nicht ändern: services/update_verlauf.py liest ihn. + echo "$TS QUEUED für Morgen-Digest: $MSG" >> "$LOG" + exit 0 fi # Primärweg: Telegram via hermes send (Login-Shell-PATH, falls aus Timer/cron aufgerufen). if [ -n "$SUBJECT" ]; then - SEND_OUT="$(bash -lc 'hermes send --to telegram --subject "$1" -- "$2"' _ "$SUBJECT" "$MSG" 2>&1)" + SEND_OUT="$(bash -lc '"$1" send --to telegram --subject "$2" -- "$3"' _ "$HERMES_BIN" "$SUBJECT" "$MSG" 2>&1)" else - SEND_OUT="$(bash -lc 'hermes send --to telegram -- "$1"' _ "$MSG" 2>&1)" + SEND_OUT="$(bash -lc '"$1" send --to telegram -- "$2"' _ "$HERMES_BIN" "$MSG" 2>&1)" fi if [ $? -eq 0 ]; then echo "$TS OK telegram: $MSG" >> "$LOG" @@ -73,4 +89,5 @@ fi echo "$TS FALLBACK (telegram fehlgeschlagen: $SEND_OUT): $MSG" >> "$LOG" printf '%s\n' "MC2-Meldung: ${SUBJECT:+$SUBJECT }$MSG" | wall 2>/dev/null || true echo "WARN: Telegram fehlgeschlagen, in $LOG protokolliert" >&2 +[ "${MC_NOTIFY_STRICT:-0}" = "1" ] && exit 1 exit 0 diff --git a/deploy/restore.sh b/deploy/restore.sh index 2502f40..1e814c5 100644 --- a/deploy/restore.sh +++ b/deploy/restore.sh @@ -7,6 +7,8 @@ # bash restore.sh --list # vorhandene Backups zeigen (lokal + Off-Box) # bash restore.sh --dry-run # nur anzeigen, was passieren würde # bash restore.sh [--yes] # wiederherstellen (--yes = ohne Rückfrage) +# bash restore.sh --mit-gedaechtnis … # Lucys Gedächtnis, SOUL, Skills, Cron-Skripte und +# # Box-Wart-Zustand auch dann zurückspielen, wenn sie da sind # bash restore.sh --pull-offsite # Off-Box-Kopie (Proxmox) nach lokal holen # # Off-Box (C12): fehlt ein Backup lokal (z. B. Box-Platte neu), wird es automatisch @@ -40,7 +42,7 @@ offsite_fetch() { rsync -a -e "$OFFSITE_SSH" "$OFFSITE/$1" "$DEST_DIR/" } -usage() { echo "Nutzung: restore.sh --list | restore.sh --pull-offsite | restore.sh [--dry-run] [--yes] "; } +usage() { echo "Nutzung: restore.sh --list | restore.sh --pull-offsite | restore.sh [--dry-run] [--yes] [--mit-gedaechtnis] "; } list_backups() { echo "Verfügbare Backups in $DEST_DIR:" local found=0 f @@ -57,9 +59,10 @@ list_backups() { } [ $# -eq 0 ] && { usage; exit 1; } -DRY=0; YES=0; TARGET="" +DRY=0; YES=0; FULL=0; TARGET="" for a in "$@"; do case "$a" in + --mit-gedaechtnis) FULL=1 ;; --list) list_backups; exit 0 ;; --pull-offsite) mkdir -p "$DEST_DIR" @@ -104,6 +107,11 @@ if [ "$DRY" = 1 ]; then [ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env" [ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/" [ -f "$STAGE/llama-swap/config.yaml" ] && echo " • $LSWAP" + for teil in memories SOUL.md skills scripts; do + [ -e "$STAGE/hermes/$teil" ] || continue + if [ "$FULL" = 1 ] || [ ! -e "$HERMES/$teil" ]; then echo " • $HERMES/$teil" + else echo " – $HERMES/$teil bleibt (vorhanden; --mit-gedaechtnis spielt es zurück)"; fi + done echo " • Dienste neu starten: $SERVICES" exit 0 fi @@ -136,6 +144,24 @@ systemctl --user stop $SERVICES 2>/dev/null || true # lädt die wiederhergestellten Jobs: [ -d "$STAGE/hermes/cron" ] && { mkdir -p "$HERMES/cron"; cp -a "$STAGE/hermes/cron/." "$HERMES/cron/"; } [ -d "$STAGE/hermes/state" ] && { mkdir -p "$HERMES/state"; cp -a "$STAGE/hermes/state/." "$HERMES/state/"; } +# Lucys Gedächtnis, Seele, Skills, Cron-Skripte und Box-Wart-Zustand (Gegenstück zu backup.sh, +# 24.09.2026): nur zurückspielen, wenn sie fehlen (neue Platte) oder --mit-gedaechtnis gesetzt ist. +# Sonst würde ein Restore wegen einer kaputten Config Lucys neuere Erinnerungen überschreiben. +zurueck_wenn_noetig() { # $1 = Quelle im Backup, $2 = Ziel + [ -e "$1" ] || return 0 + if [ "$FULL" = 1 ] || [ ! -e "$2" ]; then + mkdir -p "$(dirname "$2")" + if [ -d "$1" ]; then mkdir -p "$2"; cp -a "$1/." "$2/"; else cp -a "$1" "$2"; fi + echo " • zurückgespielt: $2" + fi +} +zurueck_wenn_noetig "$STAGE/hermes/memories" "$HERMES/memories" +zurueck_wenn_noetig "$STAGE/hermes/SOUL.md" "$HERMES/SOUL.md" +zurueck_wenn_noetig "$STAGE/hermes/skills" "$HERMES/skills" +zurueck_wenn_noetig "$STAGE/hermes/scripts" "$HERMES/scripts" +for f in "$STAGE"/box-wart/mc2-*.json; do + [ -e "$f" ] && zurueck_wenn_noetig "$f" "$MODELS_DIR/$(basename "$f")" +done echo "→ Dienste starten…" systemctl --user start $SERVICES 2>/dev/null || true