v0.5.0 – Clownfischserver HomeIP Auto-Update: myfritz Automatik für SSH (5-Min Cron)

Features:
- install.sh: Frage in Step 4 ob myfritz-Adresse für Auto-SSH-Freigabe genutzt werden soll
- clownfischserver-homeip-updater.sh: DNS-Auflösung + UFW-Update (alle 5 Min via Cron)
- .env-Variable: CLOWNFISCHSERVER_HOME_IP_DOMAIN für myfritz-Adresse
- State-File tracking zur Vermeidung unnötiger UFW-Updates
- Logging: /var/log/clownfischserver-homeip.log
- Dokumentation: scripts/README-HOMEIP.md + CLOWNFISCHSERVER.md update

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 23:28:36 +02:00
co-authored by Claude Haiku 4.5
commit eff7d0d98b
23 changed files with 4143 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
🐠 Clownfischserver
Author: Mehlzoerwer-Claude (https://github.com/mehlzoerwer-claude)
License: GPL-3.0 – Keep it open. Always.
Repo: https://github.com/mehlzoerwer-claude/clownfischserver
"""
import asyncio
import logging
import os
import tempfile
from pathlib import Path
logger = logging.getLogger(__name__)
INSTALL_DIR = os.getenv("INSTALL_DIR", "/opt/clownfischserver")
AIDER_WORKDIR = os.getenv("AIDER_WORKDIR", "/opt/clownfisch-workspace")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:2b")
TIMEOUT = 600 # 10 minutes for aider tasks on CPU
class AiderWrapper:
def __init__(self):
self.workdir = Path(AIDER_WORKDIR)
self.workdir.mkdir(parents=True, exist_ok=True)
self.venv_python = Path(INSTALL_DIR) / "aider-venv" / "bin" / "python3"
self.venv_aider = Path(INSTALL_DIR) / "aider-venv" / "bin" / "aider"
logger.info(f"AiderWrapper init: workdir={self.workdir}")
async def run(self, task: str, target_file: str = None) -> str:
"""
Run aider in script mode for a coding task.
Returns the output/result as string.
"""
logger.info(f"Aider task: {task[:100]}")
# Determine working directory and files
workdir = self.workdir
# Build aider command
cmd = self._build_aider_command(task, target_file)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(workdir),
env={
**os.environ,
"OLLAMA_API_BASE": OLLAMA_BASE_URL,
"HOME": f"/home/{os.getenv('MGMT_USER', 'clownfish')}",
}
)
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=TIMEOUT
)
except asyncio.TimeoutError:
process.kill()
return f"⏱️ Aider Timeout nach {TIMEOUT}s. Task zu komplex oder Modell überlastet."
stdout_str = stdout.decode("utf-8", errors="replace").strip()
stderr_str = stderr.decode("utf-8", errors="replace").strip()
# Build response
result_parts = []
if stdout_str:
result_parts.append(stdout_str)
if stderr_str and process.returncode != 0:
result_parts.append(f"[STDERR]\n{stderr_str}")
if process.returncode == 0:
result_parts.insert(0, f"✅ Aider Task abgeschlossen\nArbeitsverzeichnis: {workdir}\n")
else:
result_parts.insert(0, f"⚠️ Aider beendet mit Code {process.returncode}\n")
return "\n".join(result_parts) or "✓ Task abgeschlossen (keine Ausgabe)"
except FileNotFoundError:
return (
"❌ Aider nicht gefunden. Bitte prüfen:\n"
f" ls {self.venv_aider}\n"
" Ggf. neu installieren mit: pip install aider-chat"
)
except Exception as e:
logger.error(f"Aider execution error: {e}", exc_info=True)
return f"❌ Aider Fehler: {e}"
def _build_aider_command(self, task: str, target_file: str = None) -> list:
"""Build the aider command with appropriate flags"""
cmd = [
str(self.venv_aider),
"--model", f"ollama/{OLLAMA_MODEL}",
"--no-git", # Don't require git
"--yes", # Auto-confirm all changes
"--no-pretty", # Plain output for telegram
"--no-show-model-warnings", # Skip env var warnings
"--no-analytics", # No analytics prompts
"--edit-format", "diff", # Better format for small models
"--message", task, # The task as a message
]
if target_file:
cmd.append(target_file)
return cmd
async def list_workspace_files(self) -> str:
"""List files in the aider workspace"""
try:
files = list(self.workdir.rglob("*"))
if not files:
return f"📁 Workspace {self.workdir} ist leer."
file_list = "\n".join([
f" {'📁' if f.is_dir() else '📄'} {f.relative_to(self.workdir)}"
for f in sorted(files)[:50]
])
return f"📁 Workspace: {self.workdir}\n\n{file_list}"
except Exception as e:
return f"❌ Fehler beim Lesen des Workspace: {e}"