Compare commits

...

60 Commits

Author SHA1 Message Date
d8f3f869d3 setup.sh aktualisiert 2026-04-12 22:41:24 +00:00
cc51044d71 test 2026-04-13 00:35:20 +02:00
478d6c91ed setup.sh aktualisiert 2026-03-12 15:59:20 +00:00
15e04d54dc setup.sh aktualisiert 2026-03-12 15:58:35 +00:00
cdf9f1d34c config/system_prompt.txt aktualisiert 2026-03-12 15:56:24 +00:00
3293f24965 config/group_prompt.txt aktualisiert 2026-03-12 15:56:03 +00:00
ed7b4c771b source/templates/index.html aktualisiert 2026-03-12 15:50:45 +00:00
55ae56dee1 source/main.py aktualisiert 2026-03-12 15:21:47 +00:00
07bd5df3cd setup.sh aktualisiert 2026-03-10 00:54:30 +00:00
7bcd1f401c source/main.py aktualisiert 2026-03-10 00:10:47 +00:00
f46c72ce80 setup.sh aktualisiert 2026-03-09 23:41:16 +00:00
e0f2afba77 setup.sh aktualisiert 2026-03-09 23:29:09 +00:00
0fc72d77aa README.md aktualisiert 2026-03-09 23:19:15 +00:00
7393f37654 setup.sh aktualisiert 2026-03-09 23:18:37 +00:00
8922d7c918 setup.sh aktualisiert 2026-03-09 23:18:04 +00:00
159782b62f README.md aktualisiert 2026-03-09 23:00:23 +00:00
a9448dc4b3 README.md aktualisiert 2026-03-09 22:59:20 +00:00
ab53c68342 README.md aktualisiert 2026-03-09 22:58:54 +00:00
faa3835f45 README.md aktualisiert 2026-03-09 22:58:26 +00:00
4e9b516caa README.md aktualisiert 2026-03-09 22:57:39 +00:00
fff6f0767f README.md aktualisiert 2026-03-09 22:57:02 +00:00
0c21078a79 setup.sh aktualisiert 2026-03-09 22:48:43 +00:00
fc71f166e3 config/group_prompt.txt hinzugefügt 2026-03-09 14:59:35 +00:00
b9291d9c6b source/main.py aktualisiert 2026-03-09 14:58:42 +00:00
3ed41cf810 config/system_prompt.txt aktualisiert 2026-03-09 14:34:04 +00:00
b5f6ac1701 source/main.py aktualisiert 2026-03-09 14:16:05 +00:00
a4b3d58d76 config/system_prompt.txt aktualisiert 2026-03-08 16:42:46 +00:00
30ca757e34 config/system_prompt.txt aktualisiert 2026-03-08 16:39:41 +00:00
409c079197 config/system_prompt.txt aktualisiert 2026-03-08 16:28:19 +00:00
1ad587cdd6 source/main.py aktualisiert 2026-03-08 16:15:57 +00:00
f53beea3bd config/system_prompt.txt aktualisiert 2026-03-08 16:10:57 +00:00
e12cf51ffe config/.env aktualisiert 2026-03-08 16:08:49 +00:00
5748e4c9d3 config/system_prompt.txt aktualisiert 2026-03-08 01:59:42 +00:00
2d8c35cc44 config/system_prompt.txt aktualisiert 2026-03-08 01:58:38 +00:00
d3b8239f98 source/templates/index.html aktualisiert 2026-03-08 01:51:53 +00:00
7b92eb44ff source/templates/index.html aktualisiert 2026-03-08 01:47:54 +00:00
1b19c368ff source/main.py aktualisiert 2026-03-08 01:38:05 +00:00
1ffb7cec26 source/main.py aktualisiert 2026-03-08 01:18:54 +00:00
e5893ec407 config/system_prompt.txt aktualisiert 2026-03-08 01:17:00 +00:00
2a616632a8 source/main.py aktualisiert 2026-03-08 00:55:47 +00:00
d8f8465f1f source/main.py aktualisiert 2026-03-08 00:49:04 +00:00
d7c66ca572 setup.sh aktualisiert 2026-03-08 00:43:21 +00:00
259e2f3208 workspace/TODO.md hinzugefügt 2026-03-08 00:35:04 +00:00
0eefb3b373 .gitignore aktualisiert 2026-03-08 00:34:18 +00:00
c40879e75f workspace/NOTIZEN.md hinzugefügt 2026-03-08 00:34:02 +00:00
a755d1e6d1 .gitignore aktualisiert 2026-03-08 00:33:30 +00:00
226b1c8a95 .gitignore aktualisiert 2026-03-08 00:33:09 +00:00
44e0ff26fa .gitignore hinzugefügt 2026-03-08 00:32:40 +00:00
973fe8ae75 start.sh aktualisiert 2026-03-08 00:31:51 +00:00
3ed40551ae source/main.py aktualisiert 2026-03-08 00:27:17 +00:00
a0d5e4540b system_prompt.txt gelöscht 2026-03-08 00:13:12 +00:00
90c2439705 requirements.txt gelöscht 2026-03-08 00:12:47 +00:00
f17a4fca13 main.py gelöscht 2026-03-08 00:12:32 +00:00
a661b357c8 .env gelöscht 2026-03-08 00:12:12 +00:00
36432cf01a source/requirements.txt hinzugefügt 2026-03-08 00:11:39 +00:00
a11d68a974 config/system_prompt.txt hinzugefügt 2026-03-08 00:10:51 +00:00
b4cafdf462 config/.env hinzugefügt 2026-03-08 00:09:35 +00:00
07fc7d1d7f source/static/.gitkeep hinzugefügt 2026-03-08 00:08:31 +00:00
3828c792e3 source/templates/index.html hinzugefügt 2026-03-08 00:07:26 +00:00
c18b4e2141 source/main.py hinzugefügt 2026-03-08 00:06:27 +00:00
14 changed files with 1089 additions and 141 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
config/.env
data/*.db
workspace/
*/.gitkeep

View File

@@ -1,5 +1,9 @@
# J.A.R.V.I.S. - AI # J.A.R.V.I.S. - AI
Setup: ### Setup:
curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/dev/setup.sh | bash
```
curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/dev/setup.sh -o setup.sh && \
chmod +x setup.sh && \
./setup.sh
```

View File

@@ -1,5 +1,5 @@
# --- KI Provider Auswahl (google, openai, oder ollama) --- # --- KI Provider Auswahl (google, openai, oder ollama) ---
AI_PROVIDER=google AI_PROVIDER='nvidia'
# --- API Keys --- # --- API Keys ---
GOOGLE_API_KEY=dein-google-key-hier GOOGLE_API_KEY=dein-google-key-hier
@@ -12,8 +12,10 @@ OPENAI_MODEL=gpt-4o
OLLAMA_MODEL=llama3 OLLAMA_MODEL=llama3
# --- Lokale KI (Ollama) --- # --- Lokale KI (Ollama) ---
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1 OLLAMA_BASE_URL='http://192.168.178.118:11434/v1'
TELEGRAM_BOT_TOKEN=dein-telegram-bot-token TELEGRAM_BOT_TOKEN=dein-telegram-bot-token
ALLOWED_TELEGRAM_USER_ID=deine-telegram-id ALLOWED_TELEGRAM_USER_ID=deine-telegram-id
WEB_USER_NAME=Tony
NVIDIA_MODEL='moonshotai/kimi-k2.5'

6
config/group_prompt.txt Normal file
View File

@@ -0,0 +1,6 @@
Du bist JARVIS, ein hilfreicher Assistent in dieser Telegram-Gruppe.
DEINE ROLLE:
- Sei höflich, locker und hilfsbereit.
- Du hast in diesem Chat KEINEN Zugriff auf Server-Systeme oder private Dateien.
- Wenn dich jemand nach technischen Details zum Cluster fragt, antworte, dass dies nur dem Administrator vorbehalten ist.
- Du duzt die Leute und sprichst sie mit ihrem Namen "{user_name}" an.

36
config/system_prompt.txt Normal file
View File

@@ -0,0 +1,36 @@
Dein Name ist JARVIS.
Du bist ein präziser KI-Assistent für die Cluster-Verwaltung.
WICHTIGSTE REGEL: Deine Sprache ist locker, technisch versiert und du verwendest NIEMALS die Höflichkeitsform "Sie". Wir sind per Du.
DEIN WORKSPACE (GEDÄCHTNIS):
Du hast Zugriff auf ein eigenes Arbeitsverzeichnis auf dem Host-System (localhost), um dir Notizen zu machen oder Todos für {user_name} zu speichern:
- Arbeitsverzeichnis: {workspace_dir}
- Notizen-Datei im Mark-Down format: {notes_file}
- Todo-Liste im Mark-Down format: {todo_file}
Du kannst diese Dateien jederzeit lesen oder beschreiben ohne nachzufragen. Nutze dazu normale Shell-Befehle (z.B. cat, echo "text" >> datei, sed) mit dem Ziel "localhost":
<EXECUTE target="localhost">befehl</EXECUTE>
PROTOKOLL FÜR BEFEHLE: Du arbeitest STRENG in zwei Phasen:
PHASE 1 (Vorschlag):
Wenn {user_name} eine Aktion anfordert (z.B. Installation, Update, Ping, oder etwas in deinen Notizen zu speichern), erstelle NUR einen Text-Vorschlag.
- Beschreibe kurz, was du tun würdest.
- Nenne den Befehl als normalen Text (KEIN XML, KEIN <EXECUTE>).
- Frage explizit nach Erlaubnis: "Soll ich das ausführen, {user_name}?"
PHASE 2 (Ausführung):
NUR wenn {user_name} die Aktion bestätigt (z.B. "Ja", "Mach das"), gibst du den Befehl im XML-Format aus:
<EXECUTE target="IP_ODER_LOCALHOST">befehl</EXECUTE>
REGELN, TONFALL UND ANREDE:
- Du duzt {user_name} konsequent (nutze "Du", "Dein", niemals "Sie").
- Integriere den Namen des Nutzers ({user_name}) natürlich in deine Antwort.
- Variiere die Begrüßung: Nutze mal "Hallo {user_name}", mal "Gerne, {user_name}...", oder flechte den Namen mitten im Satz ein.
- Vermeide es, den Namen einfach nur wie einen statischen Header ("{user_name}: ...") voranzustellen.
- Bekannte Nodes:
{node_info}
- Nutze für <EXECUTE> bei fernen Rechnern IMMER die IP-Adresse, bei deinen eigenen Dateien IMMER "localhost".
- Begrenze endlose Befehle (z.B. ping -c 4).
- Führe NIEMALS einen <EXECUTE> Tag in PHASE 1 aus.

300
setup.sh Normal file → Executable file
View File

@@ -1,81 +1,275 @@
#!/bin/bash #!/bin/bash
# Konfiguration # ==========================================
# Farbdefinitionen
# ==========================================
C_DEF=$'\e[0m' # Default / Reset
C_BOLD=$'\e[1m' # Fett
C_CYAN=$'\e[1;36m' # Cyan
C_BLUE=$'\e[1;34m' # Blau
C_GREEN=$'\e[1;32m' # Grün
C_YELLOW=$'\e[1;33m' # Gelb
C_RED=$'\e[1;31m' # Rot
REPO_URL="https://git.pi-farm.de/pi-farm/PiDoBot.git" REPO_URL="https://git.pi-farm.de/pi-farm/PiDoBot.git"
INSTALL_DIR="jarvis-ai" SERVICE_FILE="/etc/systemd/system/jarvis.service"
echo ">>> Starte Setup für J.A.R.V.I.S. - AI ..." echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
echo -e "${C_CYAN}${C_BOLD}>>> J.A.R.V.I.S.-AI - Setup <<<${C_DEF}"
echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
# 1. Prüfen, ob Git installiert ist, ansonsten installieren # 0. Installationsverzeichnis abfragen
if ! command -v git &> /dev/null; then read -p "${C_CYAN}Installationsverzeichnis (Standard: ${C_YELLOW}/home/pi/jarvis-ai${C_CYAN}): ${C_DEF}" input_dir </dev/tty
echo "--- Git nicht gefunden. Installiere Git..." INSTALL_DIR=${input_dir:-/home/pi/jarvis-ai}
sudo apt-get update
sudo apt-get install -y git
else
echo "--- Git ist bereits installiert."
fi
# 2. Weitere zwingende System-Abhängigkeiten installieren # Pfad normalisieren
# sshpass: Für das automatische Kopieren des SSH-Keys auf neue Nodes INSTALL_DIR=$(realpath -m "$INSTALL_DIR")
# python3-venv: Für die isolierte Python-Umgebung
echo "--- Installiere benötigte System-Pakete..."
sudo apt-get update
sudo apt-get install -y wget sshpass python3-pip python3-venv
<<<<<<< HEAD
# 3. Repository klonen # 3. Repository klonen
if [ ! -d "$INSTALL_DIR" ]; then if [ ! -d "$INSTALL_DIR" ]; then
echo "--- Klone Repository von $REPO_URL..." echo "--- Klone Repository von $REPO_URL..."
git clone --branch dev --single-branch "$REPO_URL" "$INSTALL_DIR" git clone --branch main --single-branch "$REPO_URL" "$INSTALL_DIR"
else else
echo "--- Verzeichnis $INSTALL_DIR existiert bereits. Überspringe Klonen..." echo "--- Verzeichnis $INSTALL_DIR existiert bereits. Überspringe Klonen..."
fi fi
# In das Verzeichnis wechseln # In das Verzeichnis wechseln
=======
echo -e "\n${C_GREEN}Zielverzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
mkdir -p "$INSTALL_DIR"
>>>>>>> dev
cd "$INSTALL_DIR" || exit cd "$INSTALL_DIR" || exit
# 4. Virtual Environment und Python-Abhängigkeiten einrichten # 1. System-Abhängigkeiten
echo "--- Erstelle Python Virtual Environment..." echo -e "\n${C_BLUE}${C_BOLD}--- 1. Prüfe und installiere System-Pakete...${C_DEF}"
python3 -m venv venv sudo apt-get update
source venv/bin/activate sudo apt-get install -y git wget sshpass python3-pip python3-venv iproute2
echo "--- Installiere Python-Pakete aus requirements.txt..." # 2. Repository klonen
pip install --upgrade pip echo -e "\n${C_BLUE}${C_BOLD}--- 2. Hole Quellcode...${C_DEF}"
if [ -f "requirements.txt" ]; then if [ ! -d ".git" ]; then
pip install -r requirements.txt git clone --branch dev --single-branch "$REPO_URL" .
else else
echo "FEHLER: requirements.txt nicht im Repository gefunden!" echo -e "${C_YELLOW}Repo bereits vorhanden, aktualisiere...${C_DEF}"
git pull
fi
# 3. Virtual Environment
echo -e "\n${C_BLUE}${C_BOLD}--- 3. Richte Python-Umgebung ein...${C_DEF}"
if [ ! -d "venv" ]; then
python3 -m venv venv
fi
# Nutze den direkten Pfad zum Pip im Venv
./venv/bin/pip install --upgrade pip
if [ -f "source/requirements.txt" ]; then
./venv/bin/pip install -r source/requirements.txt
else
echo -e "${C_RED}❌ FEHLER: requirements.txt nicht gefunden!${C_DEF}"
exit 1 exit 1
fi fi
# 5. SSH-Key für den Master prüfen/erstellen (für passwortlosen Zugriff auf Nodes) # 4. SSH-Key & Konfiguration (Silent Mode)
echo -e "\n${C_BLUE}${C_BOLD}--- 4. Prüfe SSH-Schlüssel und Konfiguration...${C_DEF}"
if [ ! -f "$HOME/.ssh/id_rsa" ]; then if [ ! -f "$HOME/.ssh/id_rsa" ]; then
echo "--- Generiere SSH-Key für die passwortlose Kommunikation..."
ssh-keygen -t rsa -N "" -f "$HOME/.ssh/id_rsa" ssh-keygen -t rsa -N "" -f "$HOME/.ssh/id_rsa"
echo -e "${C_GREEN}✅ SSH-Key generiert.${C_DEF}"
else else
echo "--- SSH-Key existiert bereits." echo -e "${C_GREEN} SSH-Key existiert bereits.${C_DEF}"
fi fi
# 6. Static Ordner anlegen und Dateien herunterladen # SSH 'Silent Mode' einrichten, um Spam im Log zu verhindern
if [ ! -f "./static" ]; then mkdir -p "$HOME/.ssh"
echo "--- Static-Ordner anlegen und Dateien herunterladen..." if [ ! -f "$HOME/.ssh/config" ] || ! grep -q "StrictHostKeyChecking no" "$HOME/.ssh/config"; then
mkdir -p static echo -e "${C_YELLOW}Richte SSH Silent-Mode für lokale Netze ein...${C_DEF}"
cd static cat <<EOF >> "$HOME/.ssh/config"
# GridStack
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack.min.css
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all.js
# Xterm
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js
# Xterm Fit Addon
wget https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
# marked.js
wget https://cdn.jsdelivr.net/npm/marked/marked.min.js
cd ..
else
echo "--- Static-Dateien existieren bereits."
fi
echo ">>> Installation abgeschlossen!"
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
# 6. Programm starten Host 192.168.* 10.* 172.*
python3 -m uvicorn main:app --host 0.0.0.0 --port 8000 StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel ERROR
EOF
chmod 600 "$HOME/.ssh/config"
fi
# 5. Static Dateien
echo -e "\n${C_BLUE}${C_BOLD}--- 5. Lade Frontend-Bibliotheken...${C_DEF}"
mkdir -p source/static
cd source/static
wget -nc -q https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack.min.css
wget -nc -q https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all.js
wget -nc -q https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css
wget -nc -q https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js
wget -nc -q https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
wget -nc -q https://cdn.jsdelivr.net/npm/marked/marked.min.js
cd "$INSTALL_DIR"
# 6. .env Setup
echo -e "\n${C_BLUE}${C_BOLD}--- 6. Konfiguration (.env)...${C_DEF}"
mkdir -p config
ENV_FILE="config/.env"
# Standardwerte definieren
web_user="Tony"
ai_prov="google"
google_key=""
openai_key=""
nvidia_key=""
ollama_url="http://127.0.0.1:11434/v1"
google_mod="gemini-2.5-flash"
openai_mod="gpt-4o"
tg_token=""
tg_id=""
# Falls die Datei existiert, alte Werte laden
if [ -f "$ENV_FILE" ]; then
echo -e "${C_YELLOW}Bestehende .env gefunden. Lade aktuelle Werte...${C_DEF}"
set -a
source "$ENV_FILE"
set +a
web_user="${WEB_USER_NAME:-$web_user}"
ai_prov="${AI_PROVIDER:-$ai_prov}"
google_key="${GOOGLE_API_KEY:-$google_key}"
openai_key="${OPENAI_API_KEY:-$openai_key}"
nvidia_key="${NVIDIA_API_KEY:-$nvidia_key}"
ollama_url="${OLLAMA_BASE_URL:-$ollama_url}"
google_mod="${GOOGLE_MODEL:-$google_mod}"
openai_mod="${OPENAI_MODEL:-$openai_mod}"
tg_token="${TELEGRAM_BOT_TOKEN:-$tg_token}"
tg_id="${ALLOWED_TELEGRAM_USER_ID:-$tg_id}"
fi
echo -e "\nBitte gib die neuen Werte ein. (Drücke ENTER, um den Wert in Klammern beizubehalten):"
# Farbige Prompts
read -p "${C_CYAN}Dein Web-Benutzername [${C_YELLOW}$web_user${C_CYAN}]: ${C_DEF}" input_web_user </dev/tty
web_user=${input_web_user:-$web_user}
read -p "${C_CYAN}Primäre KI (google, openai, nvidia, ollama) [${C_YELLOW}$ai_prov${C_CYAN}]: ${C_DEF}" input_ai_prov </dev/tty
ai_prov=${input_ai_prov:-$ai_prov}
disp_gkey=$( [ -n "$google_key" ] && echo "${google_key:0:5}***" || echo "" )
read -p "${C_CYAN}Google Gemini API Key [${C_YELLOW}$disp_gkey${C_CYAN}]: ${C_DEF}" input_google_key </dev/tty
google_key=${input_google_key:-$google_key}
disp_okey=$( [ -n "$openai_key" ] && echo "${openai_key:0:5}***" || echo "" )
read -p "${C_CYAN}OpenAI API Key [${C_YELLOW}$disp_okey${C_CYAN}]: ${C_DEF}" input_openai_key </dev/tty
openai_key=${input_openai_key:-$openai_key}
disp_nkey=$( [ -n "$nvidia_key" ] && echo "${nvidia_key:0:5}***" || echo "" )
read -p "${C_CYAN}NVIDIA API Key [${C_YELLOW}$disp_nkey${C_CYAN}]: ${C_DEF}" input_nvidia_key </dev/tty
nvidia_key=${input_nvidia_key:-$nvidia_key}
read -p "${C_CYAN}Ollama Base URL [${C_YELLOW}$ollama_url${C_CYAN}]: ${C_DEF}" input_ollama_url </dev/tty
ollama_url=${input_ollama_url:-$ollama_url}
disp_tgtoken=$( [ -n "$tg_token" ] && echo "${tg_token:0:8}***" || echo "" )
read -p "${C_CYAN}Telegram Bot Token [${C_YELLOW}$disp_tgtoken${C_CYAN}]: ${C_DEF}" input_tg_token </dev/tty
tg_token=${input_tg_token:-$tg_token}
read -p "${C_CYAN}Erlaubte Telegram User ID [${C_YELLOW}$tg_id${C_CYAN}]: ${C_DEF}" input_tg_id </dev/tty
tg_id=${input_tg_id:-$tg_id}
# Neue .env schreiben
cat <<EOF > "$ENV_FILE"
WEB_USER_NAME=$web_user
AI_PROVIDER=$ai_prov
GOOGLE_API_KEY=$google_key
OPENAI_API_KEY=$openai_key
NVIDIA_API_KEY=$nvidia_key
OLLAMA_BASE_URL=$ollama_url
GOOGLE_MODEL=$google_mod
OPENAI_MODEL=$openai_mod
TELEGRAM_BOT_TOKEN=$tg_token
ALLOWED_TELEGRAM_USER_ID=$tg_id
EOF
echo -e "${C_GREEN}✅ Konfiguration erfolgreich gespeichert.${C_DEF}"
# ==========================================
# Weiche: Update vs. Neuinstallation
# ==========================================
if [ -f "$SERVICE_FILE" ]; then
echo -e "\n${C_BLUE}${C_BOLD}--- 7. Update-Modus: Bestehender Dienst gefunden...${C_DEF}"
echo -e "${C_YELLOW}Überspringe Port- und Firewall-Check, starte Dienst neu...${C_DEF}"
sudo systemctl daemon-reload
sudo systemctl restart jarvis.service
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
echo -e "${C_GREEN}✅ Update komplett! J.A.R.V.I.S. wurde erfolgreich aktualisiert und neu gestartet.${C_DEF}"
echo -e "Verzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
echo -e "Web-Interface: ${C_CYAN}http://<deine-ip>:8000${C_DEF}"
echo -e "Log-Ausgabe: ${C_YELLOW}sudo journalctl -u jarvis -f${C_DEF}"
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
else
# 7. Port Check
echo -e "\n${C_BLUE}${C_BOLD}--- 7. Prüfe Port 8000...${C_DEF}"
if ss -tuln | grep -q ":8000 "; then
echo -e "${C_RED}⚠️ WARNUNG: Port 8000 wird bereits von einem anderen Prozess verwendet!${C_DEF}"
echo "J.A.R.V.I.S. wird möglicherweise nicht starten können. Bitte prüfe dies nach dem Setup."
read -p "Drücke ENTER, um trotzdem fortzufahren..." </dev/tty
else
echo -e "${C_GREEN}✅ Port 8000 ist frei.${C_DEF}"
fi
# 8. Firewall Check
echo -e "\n${C_BLUE}${C_BOLD}--- 8. Prüfe Firewall-Einstellungen...${C_DEF}"
if command -v ufw >/dev/null 2>&1; then
if sudo ufw status | grep -qw "active"; then
echo -e "${C_YELLOW}UFW Firewall ist aktiv. Öffne Port 8000...${C_DEF}"
sudo ufw allow 8000/tcp
echo -e "${C_GREEN}✅ Port 8000 (TCP) in UFW freigegeben.${C_DEF}"
else
echo -e "${C_GREEN}✅ UFW ist installiert, aber inaktiv. Keine Blockade zu erwarten.${C_DEF}"
fi
else
echo -e "${C_GREEN}✅ Keine UFW Firewall gefunden. Überspringe Firewall-Setup.${C_DEF}"
fi
# 9. Systemd Service einrichten
echo -e "\n${C_BLUE}${C_BOLD}--- 9. Systemdienst (Autostart) einrichten...${C_DEF}"
read -p "${C_CYAN}Möchtest du J.A.R.V.I.S. als Hintergrunddienst installieren? (j/N): ${C_DEF}" setup_service </dev/tty
if [[ "$setup_service" =~ ^[jJ]$ ]]; then
echo "Erstelle Service-Datei in $SERVICE_FILE..."
sudo bash -c "cat <<EOF > $SERVICE_FILE
[Unit]
Description=J.A.R.V.I.S. AI Web und Telegram Bot
After=network.target
[Service]
User=$USER
WorkingDirectory=$INSTALL_DIR
Environment=\"PYTHONPATH=$INSTALL_DIR/source\"
ExecStart=$INSTALL_DIR/venv/bin/python -m uvicorn source.main:app --host 0.0.0.0 --port 8000 --proxy-headers
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
echo "Aktiviere und starte den Dienst..."
sudo systemctl daemon-reload
sudo systemctl enable jarvis.service
sudo systemctl restart jarvis.service
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
echo -e "${C_GREEN}✅ Setup komplett! J.A.R.V.I.S. läuft als Dienst.${C_DEF}"
echo -e "Verzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
echo -e "Web-Interface: ${C_CYAN}http://<deine-ip>:8000${C_DEF}"
echo -e "Log-Ausgabe: ${C_YELLOW}sudo journalctl -u jarvis -f${C_DEF}"
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
else
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
echo -e "${C_GREEN}✅ Setup komplett! Du kannst J.A.R.V.I.S. nun manuell starten:${C_DEF}"
echo "cd $INSTALL_DIR"
echo "source venv/bin/activate"
echo "export PYTHONPATH=\$PYTHONPATH:\$(pwd)/source"
echo "python3 -m uvicorn source.main:app --host 0.0.0.0 --port 8000"
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
fi

View File

@@ -9,6 +9,8 @@ import re
import httpx import httpx
import struct import struct
import termios import termios
from datetime import datetime
from pathlib import Path
from telegram import Update from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters
from telegram.error import InvalidToken from telegram.error import InvalidToken
@@ -20,20 +22,53 @@ from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv, set_key from dotenv import load_dotenv, set_key
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.cors import CORSMiddleware
# Lade Umgebungsvariablen # Basis-Verzeichnis (source/)
load_dotenv() BASE_DIR = Path(__file__).resolve().parent
# Pfade zu den neuen Ordnern (eins hoch, dann in den Zielordner)
ROOT_DIR = BASE_DIR.parent
CONFIG_DIR = ROOT_DIR / "config"
DATA_DIR = ROOT_DIR / "data"
WORKSPACE_DIR = ROOT_DIR / "workspace"
# Konfigurationsdateien
ENV_FILE = CONFIG_DIR / ".env"
load_dotenv(ENV_FILE)
DB_PATH = DATA_DIR / "cluster.db"
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Admin")
# Workspace Dateien
NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md"
TODO_FILE = WORKSPACE_DIR / "TODO.md"
# Sicherstellen, dass die Workspace-Dateien existieren
WORKSPACE_DIR.mkdir(exist_ok=True)
for f in [NOTES_FILE, TODO_FILE]:
if not f.exists():
f.write_text(f"# {f.name}\nHier fängt dein Gedächtnis an, J.A.R.V.I.S.\n", encoding="utf-8")
# Workspace-Ordner und Dateien anlegen
WORKSPACE_DIR.mkdir(exist_ok=True)
# Auch den Daten-Ordner für die Datenbank anlegen!
DATA_DIR.mkdir(exist_ok=True)
# FastAPI Pfade (relativ zu main.py in source/)
app = FastAPI() app = FastAPI()
static_path = os.path.join(os.path.dirname(__file__), "static") #static_path = os.path.join(os.path.dirname(__file__), "static")
app.mount("/static", StaticFiles(directory=static_path), name="static") templates = Jinja2Templates(directory=BASE_DIR / "templates")
templates = Jinja2Templates(directory="templates") app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") SSH_KEY = os.path.expanduser("~/.ssh/id_rsa")
DB_PATH = "cluster.db" # Ein Dictionary für verschiedene Chat-Historien
chat_history = [] chat_histories = {
PROMPT_FILE = "system_prompt.txt" "private": [] # Hier landen Webchat UND private Telegram-Nachrichten
ENV_FILE = os.path.join(os.path.dirname(__file__), ".env") }
# KI KONFIGURATION # KI KONFIGURATION
AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower()
@@ -79,7 +114,25 @@ def get_db():
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
def get_system_prompt(): def get_system_prompt(current_user=WEB_USER_NAME, is_admin=False):
# Entscheide, welche Datei geladen wird
if is_admin:
prompt_path = CONFIG_DIR / "system_prompt.txt"
else:
prompt_path = CONFIG_DIR / "group_prompt.txt"
# Datei auslesen
if prompt_path.exists():
prompt = prompt_path.read_text(encoding="utf-8")
else:
# Fallback, falls die Datei fehlt
prompt = f"Hallo {current_user}, ich bin dein Assistent."
# Namen ersetzen (funktioniert in beiden Prompts)
prompt = prompt.replace("{user_name}", current_user)
# Server-Infos NUR für Admins einfügen
if is_admin:
conn = get_db() conn = get_db()
nodes = conn.execute('SELECT * FROM nodes').fetchall() nodes = conn.execute('SELECT * FROM nodes').fetchall()
conn.close() conn.close()
@@ -89,27 +142,24 @@ def get_system_prompt():
docker_str = "Ja" if n['docker_installed'] else "Nein" docker_str = "Ja" if n['docker_installed'] else "Nein"
node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n"
if os.path.exists(PROMPT_FILE): prompt = prompt.replace("{node_info}", node_info)
with open(PROMPT_FILE, "r", encoding="utf-8") as f: prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR))
template = f.read() prompt = prompt.replace("{notes_file}", str(NOTES_FILE))
else: prompt = prompt.replace("{todo_file}", str(TODO_FILE))
template = "Du bist ein Cluster-Orchestrator. Nodes:\n{node_info}\nBefehle via <EXECUTE target=\"IP\">cmd</EXECUTE>"
print(f"⚠️ Warnung: {PROMPT_FILE} fehlt.")
return template.replace("{node_info}", node_info) return prompt
# --- KI FUNKTIONEN --- # --- KI FUNKTIONEN ---
async def get_ai_response(user_input, system_prompt): async def get_ai_response(user_msg, system_prompt, history_list):
global chat_history
chat_history.append({"role": "user", "content": user_input})
chat_history = chat_history[-30:]
ai_msg = "" ai_msg = ""
try: try:
# Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK) # Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK)
if AI_PROVIDER in ["openai", "ollama", "nvidia"]: if AI_PROVIDER in ["openai", "ollama", "nvidia"]:
messages = [{"role": "system", "content": system_prompt}] + chat_history # WICHTIG: Wir nutzen die übergebene 'history_list', NICHT starr "private"
# Da der Aufrufer (Telegram/Web) die aktuelle Frage schon hinzugefügt hat, ist sie hier schon drin.
messages = [{"role": "system", "content": system_prompt}] + history_list
if AI_PROVIDER == "ollama": if AI_PROVIDER == "ollama":
url = OLLAMA_BASE_URL url = OLLAMA_BASE_URL
@@ -126,7 +176,6 @@ async def get_ai_response(user_input, system_prompt):
key = OPENAI_API_KEY key = OPENAI_API_KEY
model_to_use = OPENAI_MODEL model_to_use = OPENAI_MODEL
# WICHTIG: Hier .AsyncOpenAI nutzen, da die Funktion async ist
client = openai.AsyncOpenAI(base_url=url, api_key=key) client = openai.AsyncOpenAI(base_url=url, api_key=key)
response = await client.chat.completions.create( response = await client.chat.completions.create(
model=model_to_use, model=model_to_use,
@@ -135,40 +184,36 @@ async def get_ai_response(user_input, system_prompt):
ai_msg = response.choices[0].message.content ai_msg = response.choices[0].message.content
elif AI_PROVIDER == "google": elif AI_PROVIDER == "google":
# Für Google Gemini
if not GOOGLE_API_KEY: if not GOOGLE_API_KEY:
return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!" return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!"
client = genai.Client(api_key=GOOGLE_API_KEY) client = genai.Client(api_key=GOOGLE_API_KEY)
# Wir müssen unser Array in das spezielle Google-Format umwandeln
google_history = [] google_history = []
# Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen # Alle Nachrichten AUSSER der allerletzten (das ist die aktuelle Frage von gerade eben)
for msg in chat_history[:-1]: for msg in history_list[:-1]:
role = "user" if msg["role"] == "user" else "model" role = "user" if msg["role"] == "user" else "model"
google_history.append( google_history.append(
types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])])
) )
# Chat MIT dem übersetzten Gedächtnis starten
chat = client.chats.create( chat = client.chats.create(
model=GOOGLE_MODEL, model=GOOGLE_MODEL,
config=types.GenerateContentConfig(system_instruction=system_prompt), config=types.GenerateContentConfig(system_instruction=system_prompt),
history=google_history history=google_history
) )
# Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken # Sende die aktuelle Nachricht an den Chat
response = chat.send_message(user_input) response = chat.send_message(user_msg)
ai_msg = response.text ai_msg = response.text
except Exception as e: except Exception as e:
ai_msg = f"Fehler bei der KI-Anfrage: {e}" ai_msg = f"Fehler bei der KI-Anfrage: {e}"
print(f"KI Fehler: {e}") print(f"KI Fehler: {e}")
# 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen # Wir geben nur den Text zurück. Das Speichern der KI-Antwort in die Historie
chat_history.append({"role": "assistant", "content": ai_msg}) # erledigt der Aufrufer (Web-Chat Endpoint oder Telegram Funktion)!
return ai_msg return ai_msg
async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -178,30 +223,62 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
chat_type = update.effective_chat.type chat_type = update.effective_chat.type
user_msg = update.message.text user_msg = update.message.text
user_id = str(update.message.from_user.id) user_id = str(update.message.from_user.id)
chat_id = str(update.effective_chat.id)
# 1. Gruppen-Logik: Nur reagieren, wenn der Bot @erwähnt wird # 1. Weiche für Gruppen-Logik & Historien-Zuordnung
if chat_type in ['group', 'supergroup']: if chat_type in ['group', 'supergroup']:
if bot_username not in user_msg: if bot_username not in user_msg:
return # Bot wurde nicht erwähnt, Nachricht ignorieren return # Bot wurde nicht erwähnt, Nachricht ignorieren
# Den @Namen aus dem Text entfernen, damit die KI nicht verwirrt wird # Den @Namen aus dem Text entfernen
user_msg = user_msg.replace(bot_username, "").strip() user_msg = user_msg.replace(bot_username, "").strip()
# Jede Gruppe bekommt ihr komplett eigenes Gedächtnis
history_key = chat_id
else: else:
# Im Einzelchat kann optional weiterhin nur der Admin zugelassen werden. # Sicherheitscheck im Einzelchat
# Wenn du willst, dass auch andere den Bot privat nutzen können, entferne diesen Block:
if user_id != ALLOWED_ID: if user_id != ALLOWED_ID:
await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒") await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒")
return return
# Privat-Chats teilen sich das Gedächtnis mit dem Webchat
history_key = "private"
# Sicherstellen, dass die Liste für diesen Chat existiert
if history_key not in chat_histories:
chat_histories[history_key] = []
current_history = chat_histories[history_key]
# Nutzer-Nachricht mit Zeitstempel in die RICHTIGE Historie aufnehmen
now = datetime.now().strftime("%d.%m.%Y %H:%M")
current_history.append({"role": "user", "content": user_msg, "timestamp": now})
# Vorname des Telegram-Nutzers auslesen
sender_name = update.message.from_user.first_name
# Tipp-Status anzeigen # Tipp-Status anzeigen
await update.message.reply_chat_action(action="typing") await update.message.reply_chat_action(action="typing")
# 2. KI fragen # 2. KI fragen (Wir übergeben die spezifische Historie!)
ai_response = await get_ai_response(user_msg, get_system_prompt()) # HINWEIS: Stelle sicher, dass deine get_ai_response Funktion die current_history auch annimmt.
# Prüfen, ob der Sender der Admin ist
is_admin_user = (user_id == ALLOWED_ID)
# KI fragen (mit Admin-Flag für den Prompt!)
ai_response = await get_ai_response(
user_msg,
get_system_prompt(sender_name, is_admin=is_admin_user),
current_history
)
commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S) commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S)
clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip() clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip()
# KI-Antwort ebenfalls in die Historie speichern
now = datetime.now().strftime("%d.%m.%Y %H:%M")
current_history.append({"role": "assistant", "content": clean_msg, "timestamp": now})
# KI Text-Antwort senden # KI Text-Antwort senden
if clean_msg: if clean_msg:
await update.message.reply_text(clean_msg) await update.message.reply_text(clean_msg)
@@ -223,7 +300,7 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
if n: if n:
try: try:
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
f"ssh -o StrictHostKeyChecking=no {n['user']}@{n['ip']} '{cmd}'", f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT stderr=asyncio.subprocess.STDOUT
) )
@@ -233,12 +310,18 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt." result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt."
await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown') await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown')
chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}"}) # System-Output auch in die RICHTIGE Historie packen, damit die KI weiß, was passiert ist
now = datetime.now().strftime("%d.%m.%Y %H:%M")
current_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}", "timestamp": now})
except Exception as e: except Exception as e:
await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}") await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}")
else: else:
await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.") await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.")
# Optionaler Bonus: Halte die Historie sauber (z.B. max. 15 Nachrichten), damit der RAM nicht überläuft
if len(current_history) > 15:
chat_histories[history_key] = current_history[-15:]
# --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) --- # --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) ---
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
@@ -290,7 +373,7 @@ async def get_remote_info(ip, user):
"""Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows.""" """Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows."""
# 1. Versuch: Linux/Mac # 1. Versuch: Linux/Mac
linux_cmd = "uname -m && (sw_vers -productName 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 || uname -s) && (command -v docker >/dev/null 2>&1 && echo 1 || echo 0)" linux_cmd = "uname -m && (sw_vers -productName 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 || uname -s) && (command -v docker >/dev/null 2>&1 && echo 1 || echo 0)"
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\"" ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\""
try: try:
output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n') output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n')
@@ -306,7 +389,7 @@ async def get_remote_info(ip, user):
# 2. Versuch: Windows (CMD) # 2. Versuch: Windows (CMD)
# ver = OS Version, echo %PROCESSOR_ARCHITECTURE% = Arch, where docker = Docker Check # ver = OS Version, echo %PROCESSOR_ARCHITECTURE% = Arch, where docker = Docker Check
win_cmd = 'ver && echo %PROCESSOR_ARCHITECTURE% && (where docker >nul 2>&1 && echo 1 || echo 0)' win_cmd = 'ver && echo %PROCESSOR_ARCHITECTURE% && (where docker >nul 2>&1 && echo 1 || echo 0)'
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\"" ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\""
try: try:
output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n') output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n')
@@ -341,7 +424,7 @@ async def bootstrap_node(ip, user, password):
cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys' cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys'
# sshpass direkt mit dem simplen Befehl # sshpass direkt mit dem simplen Befehl
setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\"" setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\""
try: try:
# Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen. # Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen.
@@ -554,36 +637,80 @@ async def terminal_websocket(websocket: WebSocket, ip: str):
@app.websocket("/ws/chat") @app.websocket("/ws/chat")
async def chat_endpoint(websocket: WebSocket): async def chat_endpoint(websocket: WebSocket):
await websocket.accept() await websocket.accept()
# Sicherstellen, dass die private History existiert
if "private" not in chat_histories:
chat_histories["private"] = []
try: try:
while True: while True:
user_msg = await websocket.receive_text() user_msg = await websocket.receive_text()
ai_response = await get_ai_response(user_msg, get_system_prompt())
# 1. User-Nachricht in Historie speichern
now = datetime.now().strftime("%d.%m.%Y %H:%M")
chat_histories["private"].append({"role": "user", "content": user_msg, "timestamp": now})
# 2. KI fragen (mit der Historie!)
ai_response = await get_ai_response(user_msg, get_system_prompt(is_admin=True), chat_histories["private"])
commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S) commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S)
clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip() clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip()
if clean_msg: await websocket.send_text(clean_msg)
# 3. KI-Antwort in Historie speichern
if clean_msg:
now = datetime.now().strftime("%d.%m.%Y %H:%M")
chat_histories["private"].append({"role": "assistant", "content": clean_msg, "timestamp": now})
await websocket.send_text(clean_msg)
# Begrenzung der Historie
if len(chat_histories["private"]) > 30:
chat_histories["private"] = chat_histories["private"][-30:]
if commands: if commands:
tasks = [] tasks = []
for target, cmd in commands: for target, cmd in commands:
conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close() conn = get_db()
if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip())) n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone()
conn.close()
if n:
# Wir übergeben hier die History, damit run_remote_task weiß, wo er loggen soll
tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip(), "private"))
if tasks: if tasks:
await websocket.send_text(" *Führe Befehle aus...*") await websocket.send_text(" *Führe Befehle aus...*")
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt()) # Nochmal KI fragen für Zusammenfassung (optional)
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt(), chat_histories["private"])
await websocket.send_text(f"--- Info ---\n{summary}") await websocket.send_text(f"--- Info ---\n{summary}")
except: pass except WebSocketDisconnect:
pass
except Exception as e:
print(f"Webchat Fehler: {e}")
async def run_remote_task(ip, user, cmd): async def run_remote_task(ip, user, cmd, history_key="private"): # history_key als Parameter
await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") await manager.broadcast(f"🚀 Task: {cmd} auf {ip}")
proc = await asyncio.create_subprocess_shell(f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) proc = await asyncio.create_subprocess_shell(
f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT
)
full_output = "" full_output = ""
while True: while True:
line = await proc.stdout.readline() line = await proc.stdout.readline()
if not line: break if not line: break
out = line.decode('utf-8', errors='ignore').strip() out = line.decode('utf-8', errors='ignore').strip()
if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n" if out:
await manager.broadcast(f"🛠️ {out}")
full_output += out + "\n"
await proc.wait() await proc.wait()
chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}"})
now = datetime.now().strftime("%d.%m.%Y %H:%M")
# Jetzt wird in die richtige Historie geschrieben!
if history_key in chat_histories:
chat_histories[history_key].append({
"role": "user",
"content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}",
"timestamp": now
})
# --- Settings API --- # --- Settings API ---
@app.get("/api/settings") @app.get("/api/settings")
@@ -693,6 +820,19 @@ async def get_models(provider: str, url: str = None):
print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}") print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}")
return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["jarvis.pi-farm.de", "192.168.178.13", "localhost"]
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/debug_keys") @app.get("/debug_keys")
async def debug_keys(): async def debug_keys():
return { return {

0
source/static/.gitkeep Normal file
View File

583
source/templates/index.html Normal file
View File

@@ -0,0 +1,583 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>J.A.R.V.I.S. AI Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="/static/gridstack.min.css" rel="stylesheet"/>
<script src="/static/gridstack-all.js"></script>
<link rel="stylesheet" href="/static/xterm.css" />
<script src="/static/xterm.js"></script>
<script src="/static/xterm-addon-fit.js"></script>
<script src="/static/marked.min.js"></script>
<style>
body { margin: 0; background: #0f172a; }
.grid-stack {
background: transparent;
min-height: calc(100vh - 60px);
padding: 10px;
}
.grid-stack-item-content {
background: #1e293b;
color: white;
border: 1px solid #334155;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.terminal-container {
flex: 1;
width: 100%;
background: black;
}
#install-log {
flex: 1;
font-family: monospace;
font-size: 11px;
color: #4ade80;
padding: 10px;
overflow-y: auto;
background: #0f172a;
}
.widget-header {
background: #334155;
padding: 8px 12px;
cursor: move;
font-size: 12px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.top-toolbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #1e293b;
color: white;
height: 55px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
z-index: 1000;
border-bottom: 1px solid #334155;
}
.toolbar-title { font-weight: bold; font-size: 1.1em; color: #38bdf8; }
.toolbar-controls { display: flex; align-items: center; gap: 10px; }
.toolbar-controls label { white-space: nowrap; font-size: 12px; color: #94a3b8; }
#ollama-url-container {
display: none;
align-items: center;
gap: 8px;
border-left: 1px solid #475569;
padding-left: 12px;
flex: 1;
}
#ollama-url { width: 100%; min-width: 200px; }
.toolbar-controls select, .toolbar-controls input {
height: 32px; background: #0f172a; color: white;
border: 1px solid #475569; border-radius: 4px; padding: 0 8px; font-size: 12px;
}
.btn-tool {
height: 32px; padding: 0 12px; border-radius: 4px; font-size: 12px;
font-weight: bold; display: flex; align-items: center; gap: 5px;
}
.save-btn { background-color: #059669; color: white; }
.add-node-btn { background-color: #2563eb; color: white; }
.node-badge {
background: #334155; padding: 2px 5px; border-radius: 4px;
font-size: 8px; text-transform: uppercase; font-weight: bold; color: #94a3b8;
}
.timestamp {
font-size: 0.7em;
color: #888; /* Ein dezentes Grau */
text-align: right;
margin-top: 4px;
opacity: 0.8;
}
/* Optional: Falls der KI-Text dunkel und der User-Text auf farbigem Grund ist */
.message.user .timestamp {
color: #e0e0e0; /* Helleres Grau, falls deine User-Bubble z.B. blau ist */
}
.markdown-content pre { background: #000; padding: 8px; border-radius: 4px; border: 1px solid #334155; overflow-x: auto; margin: 8px 0; }
</style>
</head>
<body class="text-white">
<div class="top-toolbar">
<div class="toolbar-title">🤖 J.A.R.V.I.S. AI Dashboard</div>
<div class="toolbar-controls">
<button onclick="addNode()" class="btn-tool add-node-btn"><span>+</span> Node</button>
<div class="h-6 w-px bg-slate-700 mx-1"></div>
<label>Provider:</label>
<select id="ai-provider" onchange="updateModelDropdown(false)">
<option value="google">Google Gemini</option>
<option value="openai">OpenAI</option>
<option value="nvidia">NVIDIA</option>
<option value="ollama">Ollama</option>
</select>
<label>Modell:</label>
<select id="ai-model"></select>
<div id="ollama-url-container">
<input type="text" id="ollama-url" onblur="updateModelDropdown(false)" placeholder="URL: http://...">
</div>
<button class="btn-tool save-btn" onclick="saveSettings()">Speichern</button>
<span id="settings-status"></span>
</div>
</div>
<div class="flex min-h-screen pt-[55px]">
<div class="w-64 bg-slate-900 border-r border-slate-800 p-4 flex flex-col fixed left-0 top-[55px] bottom-0 z-10">
<div id="node-list" class="flex-1 overflow-y-auto space-y-2">
{% for node in nodes %}
<div class="p-3 bg-slate-800 rounded border border-slate-700 relative group" id="node-card-{{ node.id }}">
<div class="flex justify-between items-start">
<div class="text-sm font-bold" id="node-name-{{ node.id }}">{{ node.name }}</div>
<div class="flex gap-2">
<button onclick="editNode({{ node.id }}, '{{ node.name }}', '{{ node.ip }}')" class="text-slate-500 hover:text-sky-400 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
<form action="/remove_node/{{ node.id }}" method="post" onsubmit="return confirm('Entfernen?')">
<button type="submit" class="text-slate-500 hover:text-red-500 text-xs"></button>
</form>
</div>
</div>
<div class="text-[10px] text-slate-500 font-mono flex justify-between items-center mb-1">
<span id="node-ip-{{ node.id }}">{{ node.ip }}</span>
<button onclick="refreshNodeStatus({{ node.id }})" class="hover:text-blue-400">🔄</button>
</div>
<div class="flex gap-1 mb-2">
<span id="os-{{ node.id }}" class="node-badge truncate max-w-[80px]">{{ node.os }}</span>
<span id="arch-{{ node.id }}" class="node-badge">{{ node.arch }}</span>
</div>
<div class="mt-1 flex items-center gap-2">
<span id="led-{{ node.id }}" class="h-2 w-2 rounded-full {% if 'Aktiv' in node.status %} bg-blue-500 shadow-[0_0_8px_#3b82f6] {% else %} bg-yellow-500 {% endif %}"></span>
<span id="badge-{{ node.id }}" class="text-[9px] uppercase font-mono text-slate-300">{{ node.status }}</span>
</div>
<button onclick="openTerminal('{{ node.ip }}')" class="mt-2 w-full text-[10px] bg-slate-700 hover:bg-slate-600 py-1 rounded">Konsole</button>
</div>
{% endfor %}
</div>
<button onclick="localStorage.removeItem('pi-orch-layout-v2'); location.reload();" class="mt-4 text-[10px] text-slate-500 hover:text-white uppercase">Layout Reset</button>
</div>
<div class="flex-1 ml-64 bg-slate-950 overflow-y-auto">
<div class="grid-stack">
<div class="grid-stack-item" gs-id="chat-widget" gs-w="6" gs-h="5" gs-x="0" gs-y="0">
<div class="grid-stack-item-content">
<div class="widget-header"><span>💬 J.A.R.V.I.S. - Chat</span> <span></span></div>
<div id="chat-window" class="flex-1 p-4 overflow-y-auto text-sm space-y-2 bg-[#0f172a]/50"></div>
<div class="p-2 border-t border-slate-700 flex bg-slate-800">
<textarea id="user-input"
class="flex-1 bg-slate-900 p-2 rounded text-xs outline-none border border-slate-700 focus:border-blue-500 resize-none overflow-y-hidden"
placeholder="Frage eingeben... (Shift+Enter für neue Zeile)"
rows="1"
oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"></textarea>
<button onclick="sendMessage()" class="ml-2 bg-blue-600 hover:bg-blue-500 px-4 py-1 rounded text-xs">Senden</button>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="logs-widget" gs-w="6" gs-h="5" gs-x="6" gs-y="0">
<div class="grid-stack-item-content">
<div class="widget-header"><span>📜 System Logs</span> <span></span></div>
<div id="install-log">Warte auf Daten...</div>
</div>
</div>
<div class="grid-stack-item" gs-id="term-widget" gs-w="12" gs-h="6" gs-x="0" gs-y="5">
<div class="grid-stack-item-content">
<div class="widget-header"><span>🖥️ Live Terminal</span> <span></span></div>
<div id="terminal" class="terminal-container"></div>
</div>
</div>
</div>
</div>
</div>
<div id="add-node-modal" class="hidden fixed inset-0 bg-black/80 flex items-center justify-center z-[2000]">
<div class="bg-slate-800 p-6 rounded-lg border border-slate-600 w-96">
<h3 class="text-lg font-bold mb-4">Neuen Node hinzufügen</h3>
<form action="/add_node" method="post" class="space-y-4">
<input type="text" name="name" placeholder="Name" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
<input type="text" name="ip" placeholder="IP Adresse" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
<input type="text" name="user" placeholder="SSH Benutzer" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
<input type="password" name="password" placeholder="Passwort" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
<div class="flex justify-end gap-2 pt-2">
<button type="button" onclick="closeAddNode()" class="px-4 py-2 text-sm text-slate-400">Abbrechen</button>
<button type="submit" class="bg-blue-600 px-4 py-2 text-sm rounded font-bold">Koppeln</button>
</div>
</form>
</div>
</div>
<div id="edit-node-modal" class="hidden fixed inset-0 bg-black/80 flex items-center justify-center z-[2000] p-4">
<div class="bg-slate-800 p-6 rounded-lg border border-slate-600 w-full max-w-xl">
<h3 class="text-lg font-bold mb-4 text-sky-400">Node-Konfiguration bearbeiten</h3>
<input type="hidden" id="edit-node-id">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-3">
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">Anzeigename</label>
<input type="text" id="edit-node-name" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">SSH Benutzer</label>
<input type="text" id="edit-node-user" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">Betriebssystem</label>
<input type="text" id="edit-node-os" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">Status-Text</label>
<input type="text" id="edit-node-status" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">IP Adresse</label>
<input type="text" id="edit-node-ip" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">Sudo Passwort</label>
<input type="password" id="edit-node-password" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div>
<label class="text-[10px] uppercase text-slate-400 font-bold">Architektur</label>
<input type="text" id="edit-node-arch" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
</div>
<div class="flex items-center h-full pt-4">
<label class="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" id="edit-node-docker" class="w-4 h-4 rounded border-slate-700 bg-slate-900 text-sky-600 focus:ring-sky-500">
<span class="text-sm text-slate-300 group-hover:text-white transition-colors">Docker installiert</span>
</label>
</div>
</div>
</div>
<div class="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-700">
<button type="button" onclick="closeEditNode()" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">Abbrechen</button>
<button type="button" onclick="updateNode()" class="bg-sky-600 hover:bg-sky-500 px-6 py-2 text-sm rounded font-bold transition-all shadow-lg shadow-sky-900/20">Änderungen speichern</button>
</div>
</div>
</div>
<script>
let currentSettings = {};
let termDataDisposable = null;
let term, fitAddon;
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsHost = window.location.host;
// 1. CHAT LOGIK
function initChat(chatWs) {
const input = document.getElementById('user-input');
window.sendMessage = function() {
const msg = input.value.trim();
if(!msg) return;
chatWs.send(msg);
appendChat("Du", msg, "text-slate-400 font-bold");
// Input zurücksetzen
input.value = '';
input.style.height = 'auto'; // Höhe wieder einklappen
input.focus();
};
input.addEventListener('keydown', (e) => {
// Wenn ENTER gedrückt wird OHNE Shift -> Senden
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Verhindert den Zeilenumbruch
sendMessage();
}
// Wenn ENTER + Shift gedrückt wird -> Standardverhalten (neue Zeile) bleibt
});
}
// 2. MODAL LOGIK
window.addNode = () => document.getElementById('add-node-modal').classList.remove('hidden');
window.closeAddNode = () => document.getElementById('add-node-modal').classList.add('hidden');
// EDIT LOGIK
// Ruft alle Daten ab, bevor das Modal öffnet
window.editNode = async (id) => {
try {
const res = await fetch(`/api/node/${id}`);
const node = await res.json();
document.getElementById('edit-node-id').value = node.id;
document.getElementById('edit-node-name').value = node.name;
document.getElementById('edit-node-ip').value = node.ip;
document.getElementById('edit-node-user').value = node.user;
document.getElementById('edit-node-password').value = node.sudo_password;
document.getElementById('edit-node-os').value = node.os;
document.getElementById('edit-node-arch').value = node.arch;
document.getElementById('edit-node-status').value = node.status;
document.getElementById('edit-node-docker').checked = node.docker_installed === 1;
document.getElementById('edit-node-modal').classList.remove('hidden');
} catch (e) { alert("Fehler beim Laden der Node-Daten"); }
};
window.closeEditNode = () => document.getElementById('edit-node-modal').classList.add('hidden');
window.updateNode = async function() {
const id = document.getElementById('edit-node-id').value;
const payload = {
name: document.getElementById('edit-node-name').value,
ip: document.getElementById('edit-node-ip').value,
user: document.getElementById('edit-node-user').value,
sudo_password: document.getElementById('edit-node-password').value,
os: document.getElementById('edit-node-os').value,
arch: document.getElementById('edit-node-arch').value,
status: document.getElementById('edit-node-status').value,
docker_installed: document.getElementById('edit-node-docker').checked ? 1 : 0
};
try {
const res = await fetch(`/api/node/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if(res.ok) {
location.reload(); // Einfachste Methode, um alle Badges zu aktualisieren
}
} catch (e) { alert("Fehler beim Speichern"); }
};
// 3. ERWEITERTER REFRESH
window.refreshNodeStatus = async function(nodeId) {
const badge = document.getElementById(`badge-${nodeId}`);
const led = document.getElementById(`led-${nodeId}`);
const osEl = document.getElementById(`os-${nodeId}`);
const archEl = document.getElementById(`arch-${nodeId}`);
const card = document.getElementById(`node-card-${nodeId}`);
badge.textContent = "PRÜFE...";
card.style.opacity = "0.7";
try {
const response = await fetch(`/refresh_status/${nodeId}`);
const data = await response.json();
badge.textContent = data.status;
osEl.textContent = data.os;
archEl.textContent = data.arch;
if (data.status.includes("Aktiv")) {
led.className = "h-2 w-2 rounded-full bg-blue-500 shadow-[0_0_8px_#3b82f6]";
} else if (data.status === "Offline") {
led.className = "h-2 w-2 rounded-full bg-red-500 shadow-[0_0_8px_#ef4444]";
} else {
led.className = "h-2 w-2 rounded-full bg-yellow-500";
}
} catch (e) {
badge.textContent = "FEHLER";
led.className = "h-2 w-2 rounded-full bg-red-500";
} finally {
card.style.opacity = "1";
}
};
// 4. SETTINGS & MODEL LOGIK
async function loadSettings() {
try {
const res = await fetch('/api/settings');
currentSettings = await res.json();
document.getElementById('ai-provider').value = currentSettings.provider;
document.getElementById('ollama-url').value = currentSettings.ollama_base_url || "http://127.0.0.1:11434/v1";
updateModelDropdown(true);
} catch (e) {}
}
async function updateModelDropdown(isInitialLoad = false) {
const provider = document.getElementById('ai-provider').value;
const modelSelect = document.getElementById('ai-model');
const urlContainer = document.getElementById('ollama-url-container');
const ollamaUrl = document.getElementById('ollama-url').value;
urlContainer.style.display = (provider === "ollama") ? "flex" : "none";
modelSelect.innerHTML = '<option>Lade...</option>';
try {
let queryUrl = `/api/models?provider=${provider}`;
if (provider === "ollama") {
queryUrl += `&url=${encodeURIComponent(ollamaUrl)}`;
}
const res = await fetch(queryUrl);
const data = await res.json();
modelSelect.innerHTML = '';
if (data.models && data.models.length > 0) {
data.models.forEach(m => {
const opt = document.createElement('option');
opt.value = opt.textContent = m;
modelSelect.appendChild(opt);
});
if (isInitialLoad && currentSettings) {
const savedModel = currentSettings[`${provider}_model`];
if (savedModel) modelSelect.value = savedModel;
}
}
} catch (e) {
modelSelect.innerHTML = '<option>Fehler beim Laden</option>';
}
}
async function saveSettings() {
const provider = document.getElementById('ai-provider').value;
const model = document.getElementById('ai-model').value;
const ollama_base_url = document.getElementById('ollama-url').value;
const statusEl = document.getElementById('settings-status');
try {
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, model, ollama_base_url })
});
statusEl.textContent = "✅ Gespeichert";
setTimeout(() => statusEl.textContent = "", 2000);
} catch (e) { statusEl.textContent = "❌ Fehler"; }
}
// 5. INITIALISIERUNG
document.addEventListener('DOMContentLoaded', function() {
var grid = GridStack.init({
cellHeight: 80,
margin: 10,
float: true,
handle: '.widget-header',
resizable: { handles: 'all' }
});
const savedLayout = localStorage.getItem('pi-orch-layout-v2');
if (savedLayout) { try { grid.load(JSON.parse(savedLayout)); } catch(e){} }
grid.on('resizestop dragstop', () => {
localStorage.setItem('pi-orch-layout-v2', JSON.stringify(grid.save(false)));
});
// Terminal Setup
term = new Terminal({ theme: { background: '#000' }, fontSize: 13, convertEol: true });
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
fitAddon.fit();
// WICHTIG: Wenn das Widget in GridStack vergrößert wird
grid.on('resizestop', function(event, el) {
if (el.getAttribute('gs-id') === 'term-widget') {
setTimeout(() => {
fitAddon.fit();
// Sende neue Größe an das Backend
if (window.termWs && window.termWs.readyState === WebSocket.OPEN) {
window.termWs.send(JSON.stringify({
type: "resize",
cols: term.cols,
rows: term.rows
}));
}
}, 100);
}
});
const logWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/install_logs`);
logWs.onmessage = (ev) => {
const div = document.createElement('div');
div.textContent = `> ${ev.data}`;
document.getElementById('install-log').appendChild(div);
document.getElementById('install-log').scrollTop = document.getElementById('install-log').scrollHeight;
};
const chatWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/chat`);
chatWs.onmessage = (ev) => appendChat("J.A.R.V.I.S.", ev.data, "text-blue-400 font-bold");
initChat(chatWs);
window.appendChat = function(user, msg, classes) {
const now = new Date();
const timeString = now.toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'});
const win = document.getElementById('chat-window');
// Nachricht formatieren (Markdown nur für J.A.R.V.I.S.)
let formattedMsg = (user === "J.A.R.V.I.S.") ? marked.parse(msg) : msg;
// Das HTML-Gerüst für die Nachricht zusammenbauen
const messageHtml = `
<div class="mb-4 p-2 rounded bg-slate-800/30 border border-slate-700/50">
<div class="flex justify-between items-center mb-1">
<span class="${classes} text-[10px] uppercase">${user}</span>
<span class="timestamp text-[9px] opacity-50">${timeString}</span>
</div>
<div class="markdown-content text-slate-300 text-sm">
${formattedMsg}
</div>
</div>
`;
// Nachricht an das Fenster anhängen
win.innerHTML += messageHtml;
// Automatisch nach unten scrollen
win.scrollTop = win.scrollHeight;
};
window.openTerminal = function(ip) {
if (window.termWs) window.termWs.close();
if (termDataDisposable) termDataDisposable.dispose();
term.clear();
fitAddon.fit(); // Einmal anpassen beim Öffnen
window.termWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/terminal/${ip}`);
window.termWs.onopen = () => {
// Initiale Größe nach dem Verbinden senden
window.termWs.send(JSON.stringify({
type: "resize",
cols: term.cols,
rows: term.rows
}));
};
window.termWs.onmessage = (ev) => term.write(ev.data);
termDataDisposable = term.onData(data => {
if (window.termWs?.readyState === WebSocket.OPEN) {
// Normale Tastatureingaben senden wir direkt
window.termWs.send(data);
}
});
};
loadSettings();
});
</script>
</body>
</html>

View File

@@ -8,7 +8,7 @@ fi
# 2. Venv aktivieren # 2. Venv aktivieren
source venv/bin/activate source venv/bin/activate
export PYTHONPATH=$PYTHONPATH:$(pwd)/source
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..." echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
# 3. Server starten. # 3. Server starten.

View File

@@ -1,21 +0,0 @@
Dein Name ist J.A.R.V.I.S. Du bist ein präziser KI-Assistent für die Cluster-Verwaltung.
PROTOKOLL: Du arbeitest STRENG in zwei Phasen:
PHASE 1 (Vorschlag):
Wenn der Nutzer eine Aktion anfordert (z.B. Installation, Update, Ping), erstelle NUR einen Text-Vorschlag.
- Beschreibe kurz, was du tun würdest.
- Nenne den Befehl als normalen Text (KEIN XML, KEIN <EXECUTE>).
- Frage explizit nach Erlaubnis: "Soll ich diesen Befehl jetzt auf [Node-Name] ausführen, Tony?"
PHASE 2 (Ausführung):
NUR wenn der Nutzer die Aktion im nächsten Schritt bestätigt (z.B. mit "Ja", "Mach das", "Go"), gibst du den Befehl im XML-Format aus:
<EXECUTE target="IP_ADRESSE">befehl</EXECUTE>
REGELN:
- Beginne jede Antwort mit: "Tony..."
- Nodes: {node_info}
- Nutze für <EXECUTE> immer die IP-Adresse.
- Begrenze endlose Befehle (z.B. ping -c 4).
- Passwörter für sudo/Admin darfst du aus der Datenbank beziehen, falls nötig.
- Führe NIEMALS einen <EXECUTE> Tag in PHASE 1 aus.

0
workspace/NOTIZEN.md Normal file
View File

0
workspace/TODO.md Normal file
View File